summaryrefslogtreecommitdiffstats
path: root/mobile-widgets
diff options
context:
space:
mode:
authorGravatar Dirk Hohndel <dirk@hohndel.org>2016-04-04 22:02:03 -0700
committerGravatar Dirk Hohndel <dirk@hohndel.org>2016-04-04 22:33:58 -0700
commit7be962bfc2879a72c32ff67518731347dcdff6de (patch)
treed05bf7ab234a448ee37a15b608e2b939f2285d07 /mobile-widgets
parent2d760a7bff71c46c5aeba37c40d236ea16eefea2 (diff)
downloadsubsurface-7be962bfc2879a72c32ff67518731347dcdff6de.tar.gz
Move subsurface-core to core and qt-mobile to mobile-widgets
Having subsurface-core as a directory name really messes with autocomplete and is obviously redundant. Simmilarly, qt-mobile caused an autocomplete conflict and also was inconsistent with the desktop-widget name for the directory containing the "other" UI. And while cleaning up the resulting change in the path name for include files, I decided to clean up those even more to make them consistent overall. This could have been handled in more commits, but since this requires a make clean before the build, it seemed more sensible to do it all in one. Signed-off-by: Dirk Hohndel <dirk@hohndel.org>
Diffstat (limited to 'mobile-widgets')
-rw-r--r--mobile-widgets/qml/About.qml59
-rw-r--r--mobile-widgets/qml/CloudCredentials.qml84
-rw-r--r--mobile-widgets/qml/DiveDetails.qml216
-rw-r--r--mobile-widgets/qml/DiveDetailsEdit.qml236
-rw-r--r--mobile-widgets/qml/DiveDetailsView.qml303
-rw-r--r--mobile-widgets/qml/DiveList.qml302
-rw-r--r--mobile-widgets/qml/DownloadFromDiveComputer.qml125
-rw-r--r--mobile-widgets/qml/GpsList.qml128
-rw-r--r--mobile-widgets/qml/Log.qml40
-rw-r--r--mobile-widgets/qml/Preferences.qml74
-rw-r--r--mobile-widgets/qml/StartPage.qml42
-rw-r--r--mobile-widgets/qml/SubsurfaceButton.qml26
-rw-r--r--mobile-widgets/qml/TextButton.qml37
-rw-r--r--mobile-widgets/qml/ThemeTest.qml115
-rw-r--r--mobile-widgets/qml/TopBar.qml59
-rw-r--r--mobile-widgets/qml/dive.jpgbin0 -> 235727 bytes
-rw-r--r--mobile-widgets/qml/icons/context-menu.pngbin0 -> 641 bytes
-rw-r--r--mobile-widgets/qml/icons/context-menu.svg1
-rw-r--r--mobile-widgets/qml/icons/main-menu.pngbin0 -> 112 bytes
-rw-r--r--mobile-widgets/qml/icons/main-menu.svg1
-rw-r--r--mobile-widgets/qml/icons/menu-back.pngbin0 -> 3715 bytes
-rw-r--r--mobile-widgets/qml/icons/menu-edit.pngbin0 -> 7369 bytes
-rw-r--r--mobile-widgets/qml/main.qml360
-rw-r--r--mobile-widgets/qml/mobile-resources.qrc66
-rw-r--r--mobile-widgets/qml/theme/Theme.qml57
-rw-r--r--mobile-widgets/qml/theme/Units.qml99
-rw-r--r--mobile-widgets/qml/theme/qmldir2
-rw-r--r--mobile-widgets/qmlmanager.cpp1078
-rw-r--r--mobile-widgets/qmlmanager.h162
-rw-r--r--mobile-widgets/qmlprofile.cpp111
-rw-r--r--mobile-widgets/qmlprofile.h40
31 files changed, 3823 insertions, 0 deletions
diff --git a/mobile-widgets/qml/About.qml b/mobile-widgets/qml/About.qml
new file mode 100644
index 000000000..b1ca6e6bc
--- /dev/null
+++ b/mobile-widgets/qml/About.qml
@@ -0,0 +1,59 @@
+import QtQuick 2.3
+import QtQuick.Controls 1.2
+import QtQuick.Layouts 1.1
+import org.kde.kirigami 1.0 as Kirigami
+import org.subsurfacedivelog.mobile 1.0
+
+Kirigami.ScrollablePage {
+ id: aboutPage
+ property int pageWidth: subsurfaceTheme.columnWidth - Kirigami.Units.smallSpacing
+ title: "About Subsurface-mobile"
+
+ ColumnLayout {
+ spacing: Kirigami.Units.largeSpacing
+ width: aboutPage.width
+ Layout.margins: Kirigami.Units.gridUnit / 2
+
+
+ Kirigami.Heading {
+ text: "About Subsurface-mobile"
+ Layout.topMargin: Kirigami.Units.gridUnit
+ Layout.alignment: Qt.AlignHCenter
+ Layout.maximumWidth: pageWidth
+ wrapMode: TextEdit.WrapAtWordBoundaryOrAnywhere
+ }
+ Image {
+ id: image
+ source: "qrc:/qml/subsurface-mobile-icon.png"
+ width: pageWidth / 2
+ height: width
+ fillMode: Image.Stretch
+ Layout.alignment: Qt.AlignCenter
+ horizontalAlignment: Image.AlignHCenter
+ }
+
+ Kirigami.Heading {
+ text: "A mobile version of the free Subsurface divelog software.\n" +
+ "View your dive logs while on the go."
+ level: 4
+ Layout.alignment: Qt.AlignHCenter
+ Layout.topMargin: Kirigami.Units.largeSpacing * 3
+ Layout.maximumWidth: pageWidth
+ wrapMode: TextEdit.WrapAtWordBoundaryOrAnywhere
+ anchors.horizontalCenter: parent.Center
+ horizontalAlignment: Text.AlignHCenter
+ }
+
+ Kirigami.Heading {
+ text: "Version: " + manager.getVersion() + "\n\n© Subsurface developer team\n2011-2016"
+ level: 5
+ font.pointSize: subsurfaceTheme.smallPointSize + 1
+ Layout.alignment: Qt.AlignHCenter
+ Layout.topMargin: Kirigami.Units.largeSpacing
+ Layout.maximumWidth: pageWidth
+ wrapMode: TextEdit.WrapAtWordBoundaryOrAnywhere
+ anchors.horizontalCenter: parent.Center
+ horizontalAlignment: Text.AlignHCenter
+ }
+ }
+}
diff --git a/mobile-widgets/qml/CloudCredentials.qml b/mobile-widgets/qml/CloudCredentials.qml
new file mode 100644
index 000000000..aa7c57651
--- /dev/null
+++ b/mobile-widgets/qml/CloudCredentials.qml
@@ -0,0 +1,84 @@
+import QtQuick 2.3
+import QtQuick.Controls 1.2
+import QtQuick.Window 2.2
+import QtQuick.Dialogs 1.2
+import QtQuick.Layouts 1.1
+import org.kde.kirigami 1.0 as Kirigami
+import org.subsurfacedivelog.mobile 1.0
+
+Item {
+ id: loginWindow
+ height: outerLayout.height + 2 * Kirigami.Units.gridUnit
+
+ property string username: login.text;
+ property string password: password.text;
+
+ function saveCredentials() {
+ manager.cloudUserName = login.text
+ manager.cloudPassword = password.text
+ manager.saveCloudCredentials()
+ }
+
+ ColumnLayout {
+ id: outerLayout
+ width: subsurfaceTheme.columnWidth - 2 * Kirigami.Units.gridUnit
+
+ onVisibleChanged: {
+ if (visible && manager.accessingCloud < 0) {
+ manager.appendTextToLog("Credential scrn: show kbd was: " + (Qt.inputMethod.isVisible ? "visible" : "invisible"))
+ Qt.inputMethod.show()
+ login.forceActiveFocus()
+ } else {
+ manager.appendTextToLog("Credential scrn: hide kbd was: " + (Qt.inputMethod.isVisible ? "visible" : "invisible"))
+ Qt.inputMethod.hide()
+ }
+ }
+
+ Kirigami.Heading {
+ text: "Cloud credentials"
+ level: headingLevel
+ Layout.bottomMargin: Kirigami.Units.largeSpacing / 2
+ }
+
+ Kirigami.Label {
+ text: "Email"
+ }
+
+ TextField {
+ id: login
+ text: manager.cloudUserName
+ Layout.fillWidth: true
+ inputMethodHints: Qt.ImhEmailCharactersOnly |
+ Qt.ImhNoAutoUppercase
+ }
+
+ Kirigami.Label {
+ text: "Password"
+ }
+
+ TextField {
+ id: password
+ text: manager.cloudPassword
+ echoMode: TextInput.Password
+ inputMethodHints: Qt.ImhSensitiveData |
+ Qt.ImhHiddenText |
+ Qt.ImhNoAutoUppercase
+ Layout.fillWidth: true
+ }
+ GridLayout {
+ columns: 2
+
+ CheckBox {
+ checked: false
+ id: showPassword
+ onCheckedChanged: {
+ password.echoMode = checked ? TextInput.Normal : TextInput.Password
+ }
+ }
+ Kirigami.Label {
+ text: "Show password"
+ }
+ }
+ Item { width: Kirigami.Units.gridUnit; height: width }
+ }
+}
diff --git a/mobile-widgets/qml/DiveDetails.qml b/mobile-widgets/qml/DiveDetails.qml
new file mode 100644
index 000000000..108833470
--- /dev/null
+++ b/mobile-widgets/qml/DiveDetails.qml
@@ -0,0 +1,216 @@
+import QtQuick 2.4
+import QtQuick.Controls 1.4
+import QtQuick.Controls.Styles 1.4
+import QtQuick.Dialogs 1.2
+import QtQuick.Layouts 1.2
+import org.subsurfacedivelog.mobile 1.0
+import org.kde.kirigami 1.0 as Kirigami
+
+Kirigami.Page {
+ id: diveDetailsPage
+ property alias currentIndex: diveDetailsListView.currentIndex
+ property alias dive_id: detailsEdit.dive_id
+ property alias number: detailsEdit.number
+ property alias date: detailsEdit.dateText
+ property alias airtemp: detailsEdit.airtempText
+ property alias watertemp: detailsEdit.watertempText
+ property alias buddy: detailsEdit.buddyText
+ property alias divemaster: detailsEdit.divemasterText
+ property alias depth: detailsEdit.depthText
+ property alias duration: detailsEdit.durationText
+ property alias location: detailsEdit.locationText
+ property alias notes: detailsEdit.notesText
+ property alias suit: detailsEdit.suitText
+ property alias weight: detailsEdit.weightText
+ property alias startpressure: detailsEdit.startpressureText
+ property alias endpressure: detailsEdit.endpressureText
+ property alias gasmix: detailsEdit.gasmixText
+
+ topPadding: applicationWindow().header.Layout.preferredHeight
+ leftPadding: 0
+ rightPadding: 0
+ bottomPadding: 0
+
+ title: diveDetailsListView.currentItem.modelData.dive.location
+ state: "view"
+
+ states: [
+ State {
+ name: "view"
+ PropertyChanges { target: diveDetailsPage; contextualActions: Qt.platform.os == "ios" ? [ deleteAction, backAction ] : [ deleteAction ] }
+ PropertyChanges { target: detailsEditScroll; opened: false }
+ },
+ State {
+ name: "edit"
+ PropertyChanges { target: diveDetailsPage; contextualActions: Qt.platform.os == "ios" ? [ cancelAction ] : null }
+ PropertyChanges { target: detailsEditScroll; opened: true }
+ },
+ State {
+ name: "add"
+ PropertyChanges { target: diveDetailsPage; contextualActions: Qt.platform.os == "ios" ? [ cancelAction ] : null }
+ PropertyChanges { target: detailsEditScroll; opened: true }
+ }
+
+ ]
+
+ property QtObject deleteAction: Action {
+ text: "Delete dive"
+ iconName: "trash-empty"
+ onTriggered: {
+ contextDrawer.close()
+ var deletedId = diveDetailsListView.currentItem.modelData.dive.id
+ manager.deleteDive(deletedId)
+ stackView.pop()
+ showPassiveNotification("Dive deleted", 3000, "Undo",
+ function() {
+ manager.undoDelete(deletedId)
+ });
+ }
+ }
+
+ property QtObject cancelAction: Kirigami.Action {
+ text: state === "edit" ? "Cancel edit" : "Cancel dive add"
+ iconName: "dialog-cancel"
+ onTriggered: {
+ contextDrawer.close()
+ if (state === "add")
+ returnTopPage()
+ else
+ endEditMode()
+ }
+ }
+
+ property QtObject backAction: Action {
+ text: "Back to dive list"
+ iconName: "go-previous"
+ onTriggered: {
+ contextDrawer.close()
+ returnTopPage()
+ }
+ }
+
+ mainAction: Action {
+ iconName: state !== "view" ? "document-save" : "document-edit"
+ onTriggered: {
+ if (state === "edit" || state === "add") {
+ detailsEdit.saveData()
+ } else {
+ startEditMode()
+ }
+ }
+ }
+
+ onBackRequested: {
+ if (state === "edit") {
+ endEditMode()
+ event.accepted = true;
+ } else if (state === "add") {
+ endEditMode()
+ stackView.pop()
+ event.accepted = true;
+ }
+ // if we were in view mode, don't accept the event and pop the page
+ }
+
+ function showDiveIndex(index) {
+ currentIndex = index;
+ diveDetailsListView.positionViewAtIndex(index, ListView.Beginning);
+ }
+
+ function endEditMode() {
+ // if we were adding a dive, we need to remove it
+ if (state === "add")
+ manager.addDiveAborted(dive_id)
+ // just cancel the edit/add state
+ state = "view";
+ Qt.inputMethod.hide();
+ }
+
+ function startEditMode() {
+ // set things up for editing - so make sure that the detailsEdit has
+ // all the right data (using the property aliases set up above)
+ dive_id = diveDetailsListView.currentItem.modelData.dive.id
+ number = diveDetailsListView.currentItem.modelData.dive.number
+ date = diveDetailsListView.currentItem.modelData.dive.date + " " + diveDetailsListView.currentItem.modelData.dive.time
+ location = diveDetailsListView.currentItem.modelData.dive.location
+ duration = diveDetailsListView.currentItem.modelData.dive.duration
+ depth = diveDetailsListView.currentItem.modelData.dive.depth
+ airtemp = diveDetailsListView.currentItem.modelData.dive.airTemp
+ watertemp = diveDetailsListView.currentItem.modelData.dive.waterTemp
+ suit = diveDetailsListView.currentItem.modelData.dive.suit
+ buddy = diveDetailsListView.currentItem.modelData.dive.buddy
+ divemaster = diveDetailsListView.currentItem.modelData.dive.divemaster
+ notes = diveDetailsListView.currentItem.modelData.dive.notes
+ if (diveDetailsListView.currentItem.modelData.dive.singleWeight) {
+ // we have only one weight, go ahead, have fun and edit it
+ weight = diveDetailsListView.currentItem.modelData.dive.sumWeight
+ } else {
+ // careful when translating, this text is "magic" in DiveDetailsEdit.qml
+ weight = "cannot edit multiple weight systems"
+ }
+ if (diveDetailsListView.currentItem.modelData.dive.getCylinder != "Multiple" ) {
+ startpressure = diveDetailsListView.currentItem.modelData.dive.startPressure
+ endpressure = diveDetailsListView.currentItem.modelData.dive.endPressure
+ gasmix = diveDetailsListView.currentItem.modelData.dive.firstGas
+ } else {
+ // careful when translating, this text is "magic" in DiveDetailsEdit.qml
+ startpressure = "cannot edit multiple cylinders"
+ endpressure = "cannot edit multiple cylinders"
+ gasmix = "cannot edit multiple gases"
+ }
+
+ diveDetailsPage.state = "edit"
+ }
+
+ onWidthChanged: diveDetailsListView.positionViewAtIndex(diveDetailsListView.currentIndex, ListView.Beginning);
+
+ Item {
+ anchors.fill: parent
+ ScrollView {
+ id: diveDetailList
+ anchors.fill: parent
+ ListView {
+ id: diveDetailsListView
+ anchors.fill: parent
+ model: diveModel
+ currentIndex: -1
+ boundsBehavior: Flickable.StopAtBounds
+ maximumFlickVelocity: parent.width * 5
+ orientation: ListView.Horizontal
+ focus: true
+ clip: true
+ snapMode: ListView.SnapOneItem
+ onMovementEnded: {
+ currentIndex = indexAt(contentX+1, 1);
+ }
+ delegate: ScrollView {
+ id: internalScrollView
+ width: diveDetailsListView.width
+ height: diveDetailsListView.height
+ property var modelData: model
+ Flickable {
+ //contentWidth: parent.width
+ contentHeight: diveDetails.height
+ boundsBehavior: Flickable.StopAtBounds
+ DiveDetailsView {
+ id: diveDetails
+ width: internalScrollView.width
+ }
+ }
+ }
+ }
+ }
+ Kirigami.OverlaySheet {
+ id: detailsEditScroll
+ anchors.fill: parent
+ onOpenedChanged: {
+ if (!opened) {
+ endEditMode()
+ }
+ }
+ DiveDetailsEdit {
+ id: detailsEdit
+ }
+ }
+ }
+}
diff --git a/mobile-widgets/qml/DiveDetailsEdit.qml b/mobile-widgets/qml/DiveDetailsEdit.qml
new file mode 100644
index 000000000..e4338b3b8
--- /dev/null
+++ b/mobile-widgets/qml/DiveDetailsEdit.qml
@@ -0,0 +1,236 @@
+import QtQuick 2.3
+import QtQuick.Controls 1.2
+import QtQuick.Controls.Styles 1.2
+import QtQuick.Dialogs 1.2
+import QtQuick.Layouts 1.1
+import org.subsurfacedivelog.mobile 1.0
+import org.kde.kirigami 1.0 as Kirigami
+
+Item {
+ id: detailsEdit
+ property int dive_id
+ property int number
+ property alias dateText: txtDate.text
+ property alias locationText: txtLocation.text
+ property string gpsText
+ property alias airtempText: txtAirTemp.text
+ property alias watertempText: txtWaterTemp.text
+ property alias suitText: txtSuit.text
+ property alias buddyText: txtBuddy.text
+ property alias divemasterText: txtDiveMaster.text
+ property alias notesText: txtNotes.text
+ property alias durationText: txtDuration.text
+ property alias depthText: txtDepth.text
+ property alias weightText: txtWeight.text
+ property alias startpressureText: txtStartPressure.text
+ property alias endpressureText: txtEndPressure.text
+ property alias gasmixText: txtGasMix.text
+
+ function saveData() {
+ // apply the changes to the dive_table
+ manager.commitChanges(dive_id, detailsEdit.dateText, detailsEdit.locationText, detailsEdit.gpsText, detailsEdit.durationText,
+ detailsEdit.depthText, detailsEdit.airtempText, detailsEdit.watertempText, detailsEdit.suitText,
+ detailsEdit.buddyText, detailsEdit.divemasterText, detailsEdit.weightText, detailsEdit.notesText,
+ detailsEdit.startpressureText, detailsEdit.endpressureText, detailsEdit.gasmixText)
+ // trigger the profile to be redrawn
+ QMLProfile.diveId = dive_id
+
+ // apply the changes to the dive detail view - since the edit could have changed the order
+ // first make sure that we are looking at the correct dive - our model allows us to look
+ // up the index based on the unique dive_id
+ var newIdx = diveModel.getIdxForId(dive_id)
+ diveDetailsListView.currentIndex = newIdx
+ diveDetailsListView.currentItem.modelData.date = detailsEdit.dateText
+ diveDetailsListView.currentItem.modelData.location = detailsEdit.locationText
+ diveDetailsListView.currentItem.modelData.duration = detailsEdit.durationText
+ diveDetailsListView.currentItem.modelData.depth = detailsEdit.depthText
+ diveDetailsListView.currentItem.modelData.airtemp = detailsEdit.airtempText
+ diveDetailsListView.currentItem.modelData.watertemp = detailsEdit.watertempText
+ diveDetailsListView.currentItem.modelData.suit = detailsEdit.suitText
+ diveDetailsListView.currentItem.modelData.buddy = detailsEdit.buddyText
+ diveDetailsListView.currentItem.modelData.divemaster = detailsEdit.divemasterText
+ diveDetailsListView.currentItem.modelData.notes = detailsEdit.notesText
+ diveDetailsPage.state = "view"
+ Qt.inputMethod.hide()
+ // now make sure we directly show the saved dive (this may be a new dive, or it may have moved)
+ showDiveIndex(newIdx)
+ }
+
+ height: editArea.height
+ ColumnLayout {
+ id: editArea
+ spacing: Kirigami.Units.smallSpacing
+ width: subsurfaceTheme.columnWidth - 2 * Kirigami.Units.gridUnit
+
+ GridLayout {
+ id: editorDetails
+ width: parent.width
+ columns: 2
+
+ Kirigami.Heading {
+ Layout.columnSpan: 2
+ text: "Dive " + number
+ }
+ Kirigami.Label {
+ Layout.alignment: Qt.AlignRight
+ text: "Date:"
+ }
+ TextField {
+ id: txtDate;
+ Layout.fillWidth: true
+ }
+ Kirigami.Label {
+ Layout.alignment: Qt.AlignRight
+ text: "Location:"
+ }
+ TextField {
+ id: txtLocation;
+ Layout.fillWidth: true
+ }
+
+ // we should add a checkbox here that allows the user
+ // to add the current location as the dive location
+ // (think of someone adding a dive while on the boat or
+ // at the dive site)
+ Kirigami.Label {
+ Layout.alignment: Qt.AlignRight
+ text: "Use current\nGPS location:"
+ }
+ CheckBox {
+ id: checkboxGPS
+ onCheckedChanged: {
+ if (checked)
+ gpsText = manager.getCurrentPosition()
+ }
+ }
+
+ Kirigami.Label {
+ Layout.alignment: Qt.AlignRight
+ text: "Depth:"
+ }
+ TextField {
+ id: txtDepth
+ Layout.fillWidth: true
+ validator: RegExpValidator { regExp: /[^-]*/ }
+ }
+ Kirigami.Label {
+ Layout.alignment: Qt.AlignRight
+ text: "Duration:"
+ }
+ TextField {
+ id: txtDuration
+ Layout.fillWidth: true
+ validator: RegExpValidator { regExp: /[^-]*/ }
+ }
+
+ Kirigami.Label {
+ Layout.alignment: Qt.AlignRight
+ text: "Air Temp:"
+ }
+ TextField {
+ id: txtAirTemp
+ Layout.fillWidth: true
+ }
+
+ Kirigami.Label {
+ Layout.alignment: Qt.AlignRight
+ text: "Water Temp:"
+ }
+ TextField {
+ id: txtWaterTemp
+ Layout.fillWidth: true
+ }
+
+ Kirigami.Label {
+ Layout.alignment: Qt.AlignRight
+ text: "Suit:"
+ }
+ TextField {
+ id: txtSuit
+ Layout.fillWidth: true
+ }
+
+ Kirigami.Label {
+ Layout.alignment: Qt.AlignRight
+ text: "Buddy:"
+ }
+ TextField {
+ id: txtBuddy
+ Layout.fillWidth: true
+ }
+
+ Kirigami.Label {
+ Layout.alignment: Qt.AlignRight
+ text: "Dive Master:"
+ }
+ TextField {
+ id: txtDiveMaster
+ Layout.fillWidth: true
+ }
+
+ Kirigami.Label {
+ Layout.alignment: Qt.AlignRight
+ text: "Weight:"
+ }
+ TextField {
+ id: txtWeight
+ readOnly: (text == "cannot edit multiple weight systems" ? true : false)
+ Layout.fillWidth: true
+ }
+
+ Kirigami.Label {
+ Layout.alignment: Qt.AlignRight
+ text: "Gas mix:"
+ }
+ TextField {
+ id: txtGasMix
+ readOnly: (text == "cannot edit multiple gases" ? true : false)
+ Layout.fillWidth: true
+ validator: RegExpValidator { regExp: /(EAN100|EAN\d\d|AIR|100|\d{1,2}|\d{1,2}\/\d{1,2})/ }
+ }
+
+ Kirigami.Label {
+ Layout.alignment: Qt.AlignRight
+ text: "Start Pressure:"
+ }
+ TextField {
+ id: txtStartPressure
+ readOnly: (text == "cannot edit multiple cylinders" ? true : false)
+ Layout.fillWidth: true
+ }
+
+ Kirigami.Label {
+ Layout.alignment: Qt.AlignRight
+ text: "End Pressure:"
+ }
+ TextField {
+ id: txtEndPressure
+ readOnly: (text == "cannot edit multiple cylinders" ? true : false)
+ Layout.fillWidth: true
+ }
+
+
+ Kirigami.Label {
+ Layout.columnSpan: 2
+ Layout.alignment: Qt.AlignLeft
+ text: "Notes:"
+ }
+ TextArea {
+ Layout.columnSpan: 2
+ width: parent.width
+ id: txtNotes
+ textFormat: TextEdit.RichText
+ focus: true
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ Layout.minimumHeight: Kirigami.Units.gridUnit * 6
+ selectByMouse: true
+ wrapMode: TextEdit.WrapAtWordBoundaryOrAnywhere
+ }
+ }
+ Item {
+ height: Kirigami.Units.gridUnit * 3
+ width: height // just to make sure the spacer doesn't produce scrollbars, but also isn't null
+ }
+ }
+}
diff --git a/mobile-widgets/qml/DiveDetailsView.qml b/mobile-widgets/qml/DiveDetailsView.qml
new file mode 100644
index 000000000..ef1dc5605
--- /dev/null
+++ b/mobile-widgets/qml/DiveDetailsView.qml
@@ -0,0 +1,303 @@
+import QtQuick 2.3
+/*
+import QtWebView 1.0
+*/
+import QtQuick.Controls 1.2
+import QtQuick.Controls.Styles 1.2
+import QtQuick.Dialogs 1.2
+import QtQuick.Layouts 1.1
+import org.subsurfacedivelog.mobile 1.0
+import org.kde.kirigami 1.0 as Kirigami
+
+Item {
+ id: detailsView
+ property real gridWidth: subsurfaceTheme.columnWidth - 2 * Kirigami.Units.gridUnit
+ property real col1Width: gridWidth * 0.23
+ property real col2Width: gridWidth * 0.37
+ property real col3Width: gridWidth * 0.20
+ property real col4Width: gridWidth * 0.20
+
+ width: SubsurfaceTheme.columnWidth
+ height: mainLayout.implicitHeight + bottomLayout.implicitHeight + Kirigami.Units.iconSizes.large
+ Rectangle {
+ z: 99
+ color: Kirigami.Theme.textColor
+ opacity: 0.3
+ width: Kirigami.Units.smallSpacing/4
+ anchors {
+ right: parent.right
+ top: parent.top
+ bottom: parent.bottom
+ }
+ }
+ GridLayout {
+ id: mainLayout
+ anchors {
+ top: parent.top
+ left: parent.left
+ right: parent.right
+ margins: Math.round(Kirigami.Units.gridUnit / 2)
+ }
+ columns: 4
+ rowSpacing: Kirigami.Units.smallSpacing * 2
+ columnSpacing: Kirigami.Units.smallSpacing
+
+ Kirigami.Heading {
+ id: detailsViewHeading
+ Layout.fillWidth: true
+ text: dive.location
+ font.underline: dive.gps !== ""
+ Layout.columnSpan: 4
+ wrapMode: TextEdit.WrapAtWordBoundaryOrAnywhere
+ Layout.topMargin: Kirigami.Units.largeSpacing
+ MouseArea {
+ anchors.fill: parent
+ onClicked: {
+ if (dive.gps !== "")
+ showMap(dive.gps)
+ }
+ }
+ }
+ Kirigami.Label {
+ id: dateLabel
+ text: "Date: "
+ opacity: 0.6
+ Layout.alignment: Qt.AlignRight
+ }
+ Kirigami.Label {
+ text: dive.date + " " + dive.time
+ wrapMode: TextEdit.WrapAtWordBoundaryOrAnywhere
+ Layout.columnSpan: 2
+ }
+ Kirigami.Label {
+ id: numberText
+ text: "#" + dive.number
+ color: Kirigami.Theme.textColor
+ wrapMode: TextEdit.WrapAtWordBoundaryOrAnywhere
+ }
+
+ Kirigami.Label {
+ id: depthLabel
+ text: "Depth: "
+ opacity: 0.6
+ Layout.alignment: Qt.AlignRight
+ }
+ Kirigami.Label {
+ text: dive.depth
+ Layout.fillWidth: true
+ wrapMode: TextEdit.WrapAtWordBoundaryOrAnywhere
+ }
+ Kirigami.Label {
+ text: "Duration: "
+ opacity: 0.6
+ Layout.alignment: Qt.AlignRight
+ }
+ Kirigami.Label {
+ text: dive.duration
+ wrapMode: TextEdit.WrapAtWordBoundaryOrAnywhere
+ }
+
+ QMLProfile {
+ id: qmlProfile
+ visible: !dive.noDive
+ Layout.fillWidth: true
+ Layout.preferredHeight: Layout.minimumHeight
+ Layout.minimumHeight: width * 0.75
+ Layout.columnSpan: 4
+ clip: false
+ Rectangle {
+ color: "transparent"
+ opacity: 0.6
+ border.width: 1
+ border.color: Kirigami.Theme.textColor;
+ anchors.fill: parent
+ }
+ }
+ Kirigami.Label {
+ id: noProfile
+ visible: dive.noDive
+ Layout.fillWidth: true
+ Layout.columnSpan: 4
+ Layout.margins: Kirigami.Units.gridUnit
+ horizontalAlignment: Text.AlignHCenter
+ text: "No profile to show"
+ }
+ }
+ GridLayout {
+ id: bottomLayout
+ anchors {
+ top: mainLayout.bottom
+ left: parent.left
+ right: parent.right
+ margins: Math.round(Kirigami.Units.gridUnit / 2)
+ }
+ columns: 4
+ rowSpacing: Kirigami.Units.smallSpacing * 2
+ columnSpacing: Kirigami.Units.smallSpacing
+
+ Kirigami.Heading {
+ Layout.fillWidth: true
+ level: 3
+ text: "Dive Details"
+ Layout.columnSpan: 4
+ }
+
+ // first row - here we set up the column widths - total is 90% of width
+ Kirigami.Label {
+ text: "Suit:"
+ wrapMode: Text.WrapAtWordBoundaryOrAnywhere
+ opacity: 0.6
+ Layout.maximumWidth: detailsView.col1Width
+ Layout.preferredWidth: detailsView.col1Width
+ Layout.alignment: Qt.AlignRight
+ }
+ Kirigami.Label {
+ id: txtSuit
+ text: dive.suit
+ wrapMode: TextEdit.WrapAtWordBoundaryOrAnywhere
+ Layout.maximumWidth: detailsView.col2Width
+ Layout.preferredWidth: detailsView.col2Width
+ }
+
+ Kirigami.Label {
+ text: "Air Temp:"
+ wrapMode: Text.WrapAtWordBoundaryOrAnywhere
+ opacity: 0.6
+ Layout.maximumWidth: detailsView.col3Width
+ Layout.preferredWidth: detailsView.col3Width
+ Layout.alignment: Qt.AlignRight
+ }
+ Kirigami.Label {
+ id: txtAirTemp
+ text: dive.airTemp
+ wrapMode: TextEdit.WrapAtWordBoundaryOrAnywhere
+ Layout.maximumWidth: detailsView.col4Width
+ Layout.preferredWidth: detailsView.col4Width
+ }
+
+ Kirigami.Label {
+ text: "Cylinder:"
+ wrapMode: Text.WrapAtWordBoundaryOrAnywhere
+ opacity: 0.6
+ Layout.maximumWidth: detailsView.col1Width
+ Layout.preferredWidth: detailsView.col1Width
+ Layout.alignment: Qt.AlignRight
+ }
+ Kirigami.Label {
+ id: txtCylinder
+ text: dive.getCylinder
+ wrapMode: TextEdit.WrapAtWordBoundaryOrAnywhere
+ Layout.maximumWidth: detailsView.col2Width
+ Layout.preferredWidth: detailsView.col2Width
+ }
+
+ Kirigami.Label {
+ text: "Water Temp:"
+ wrapMode: Text.WrapAtWordBoundaryOrAnywhere
+ opacity: 0.6
+ Layout.maximumWidth: detailsView.col3Width
+ Layout.preferredWidth: detailsView.col3Width
+ Layout.alignment: Qt.AlignRight
+ }
+ Kirigami.Label {
+ id: txtWaterTemp
+ text: dive.waterTemp
+ wrapMode: TextEdit.WrapAtWordBoundaryOrAnywhere
+ Layout.maximumWidth: detailsView.col4Width
+ Layout.preferredWidth: detailsView.col4Width
+ }
+
+ Kirigami.Label {
+ text: "Dive Master:"
+ wrapMode: Text.WrapAtWordBoundaryOrAnywhere
+ opacity: 0.6
+ Layout.maximumWidth: detailsView.col1Width
+ Layout.preferredWidth: detailsView.col1Width
+ Layout.alignment: Qt.AlignRight
+ }
+ Kirigami.Label {
+ id: txtDiveMaster
+ text: dive.divemaster
+ wrapMode: TextEdit.WrapAtWordBoundaryOrAnywhere
+ Layout.maximumWidth: detailsView.col2Width
+ Layout.preferredWidth: detailsView.col2Width
+ }
+
+ Kirigami.Label {
+ text: "Weight:"
+ wrapMode: Text.WrapAtWordBoundaryOrAnywhere
+ opacity: 0.6
+ Layout.maximumWidth: detailsView.col3Width
+ Layout.preferredWidth: detailsView.col3Width
+ Layout.alignment: Qt.AlignRight
+ }
+ Kirigami.Label {
+ id: txtWeight
+ text: dive.sumWeight
+ wrapMode: TextEdit.WrapAtWordBoundaryOrAnywhere
+ Layout.maximumWidth: detailsView.col4Width
+ Layout.preferredWidth: detailsView.col4Width
+ }
+
+ Kirigami.Label {
+ text: "Buddy:"
+ wrapMode: Text.WrapAtWordBoundaryOrAnywhere
+ opacity: 0.6
+ Layout.maximumWidth: detailsView.col1Width
+ Layout.preferredWidth: detailsView.col1Width
+ Layout.alignment: Qt.AlignRight
+ }
+ Kirigami.Label {
+ id: txtBuddy
+ text: dive.buddy
+ wrapMode: TextEdit.WrapAtWordBoundaryOrAnywhere
+ Layout.maximumWidth: detailsView.col2Width
+ Layout.preferredWidth: detailsView.col2Width
+ }
+
+ Kirigami.Label {
+ text: "SAC:"
+ wrapMode: Text.WrapAtWordBoundaryOrAnywhere
+ opacity: 0.6
+ Layout.maximumWidth: detailsView.col3Width
+ Layout.preferredWidth: detailsView.col3Width
+ Layout.alignment: Qt.AlignRight
+ }
+ Kirigami.Label {
+ id: txtSAC
+ text: dive.sac
+ wrapMode: TextEdit.WrapAtWordBoundaryOrAnywhere
+ Layout.maximumWidth: detailsView.col4Width
+ Layout.preferredWidth: detailsView.col4Width
+ }
+
+ Kirigami.Heading {
+ Layout.fillWidth: true
+ level: 3
+ text: "Notes"
+ wrapMode: Text.WrapAtWordBoundaryOrAnywhere
+ Layout.columnSpan: 4
+ }
+
+ Kirigami.Label {
+ id: txtNotes
+ text: dive.notes
+ focus: true
+ Layout.columnSpan: 4
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ //selectByMouse: true
+ wrapMode: TextEdit.WrapAtWordBoundaryOrAnywhere
+ }
+ Item {
+ Layout.columnSpan: 4
+ Layout.fillWidth: true
+ Layout.minimumHeight: Kirigami.Units.gridUnit * 3
+ }
+ Component.onCompleted: {
+ qmlProfile.setMargin(Kirigami.Units.smallSpacing)
+ qmlProfile.diveId = model.dive.id;
+ qmlProfile.update();
+ }
+ }
+}
diff --git a/mobile-widgets/qml/DiveList.qml b/mobile-widgets/qml/DiveList.qml
new file mode 100644
index 000000000..95af9a973
--- /dev/null
+++ b/mobile-widgets/qml/DiveList.qml
@@ -0,0 +1,302 @@
+import QtQuick 2.4
+import QtQuick.Controls 1.2
+import QtQuick.Layouts 1.2
+import QtQuick.Window 2.2
+import QtQuick.Dialogs 1.2
+import org.kde.kirigami 1.0 as Kirigami
+import org.subsurfacedivelog.mobile 1.0
+
+Kirigami.ScrollablePage {
+ id: page
+ objectName: "DiveList"
+ title: "Subsurface-mobile"
+ background: Rectangle {
+ color: Kirigami.Theme.viewBackgroundColor
+ }
+
+ property int credentialStatus: manager.credentialStatus
+ property int numDives: diveListView.count
+ property color textColor: subsurfaceTheme.diveListTextColor
+
+ function scrollToTop() {
+ diveListView.positionViewAtBeginning()
+ }
+
+ Component {
+ id: diveDelegate
+ Kirigami.AbstractListItem {
+ enabled: true
+ supportsMouseEvents: true
+ checked: diveListView.currentIndex === model.index
+ width: parent.width
+
+ property real detailsOpacity : 0
+ property int horizontalPadding: Kirigami.Units.gridUnit / 2 - Kirigami.Units.smallSpacing + 1
+
+ // When clicked, the mode changes to details view
+ onClicked: {
+ if (detailsWindow.state === "view") {
+ diveListView.currentIndex = index
+ detailsWindow.showDiveIndex(index);
+ stackView.push(detailsWindow);
+ }
+ }
+
+ property bool deleteButtonVisible: false
+
+ onPressAndHold: {
+ deleteButtonVisible = true
+ timer.restart()
+ }
+
+ Row {
+ width: parent.width - Kirigami.Units.gridUnit
+ height: childrenRect.height - Kirigami.Units.smallSpacing
+ spacing: horizontalPadding
+ add: Transition {
+ NumberAnimation { property: "opacity"; from: 0; to: 1.0; duration: 400 }
+ NumberAnimation { property: "scale"; from: 0; to: 1.0; duration: 400 }
+ }
+ Item {
+ id: diveListEntry
+ width: parent.width - Kirigami.Units.gridUnit
+ height: childrenRect.height - Kirigami.Units.smallSpacing
+
+ Kirigami.Label {
+ id: locationText
+ text: dive.location
+ font.weight: Font.Light
+ elide: Text.ElideRight
+ maximumLineCount: 1 // needed for elide to work at all
+ color: textColor
+ anchors {
+ left: parent.left
+ leftMargin: horizontalPadding
+ top: parent.top
+ right: dateLabel.left
+ }
+ }
+ Kirigami.Label {
+ id: dateLabel
+ text: dive.date + " " + dive.time
+ font.pointSize: subsurfaceTheme.smallPointSize
+ color: textColor
+ anchors {
+ right: parent.right
+ top: parent.top
+ }
+ }
+ Row {
+ anchors {
+ left: parent.left
+ leftMargin: horizontalPadding
+ right: parent.right
+ rightMargin: horizontalPadding
+ topMargin: - Kirigami.Units.smallSpacing * 2
+ bottom: numberText.bottom
+ }
+ Kirigami.Label {
+ text: 'Depth: '
+ font.pointSize: subsurfaceTheme.smallPointSize
+ color: textColor
+ }
+ Kirigami.Label {
+ text: dive.depth
+ width: Math.max(Kirigami.Units.gridUnit * 3, paintedWidth) // helps vertical alignment throughout listview
+ font.pointSize: subsurfaceTheme.smallPointSize
+ color: textColor
+ }
+ Kirigami.Label {
+ text: 'Duration: '
+ font.pointSize: subsurfaceTheme.smallPointSize
+ color: textColor
+ }
+ Kirigami.Label {
+ text: dive.duration
+ font.pointSize: subsurfaceTheme.smallPointSize
+ color: textColor
+ }
+ }
+ Kirigami.Label {
+ id: numberText
+ text: "#" + dive.number
+ font.pointSize: subsurfaceTheme.smallPointSize
+ color: textColor
+ anchors {
+ right: parent.right
+ top: locationText.bottom
+ topMargin: - Kirigami.Units.smallSpacing * 2
+ }
+ }
+ }
+ Rectangle {
+ visible: deleteButtonVisible
+ height: diveListEntry.height - Kirigami.Units.smallSpacing
+ width: height - 3 * Kirigami.Units.smallSpacing
+ color: "#FF3030"
+ antialiasing: true
+ radius: Kirigami.Units.smallSpacing
+ Kirigami.Icon {
+ anchors {
+ horizontalCenter: parent.horizontalCenter
+ verticalCenter: parent.verticalCenter
+ }
+ source: "trash-empty"
+ }
+ MouseArea {
+ anchors.fill: parent
+ enabled: parent.visible
+ onClicked: {
+ parent.visible = false
+ timer.stop()
+ manager.deleteDive(dive.id)
+ }
+ }
+ }
+ Item {
+ Timer {
+ id: timer
+ interval: 4000
+ onTriggered: {
+ deleteButtonVisible = false
+ }
+ }
+ }
+ }
+ }
+ }
+
+ Component {
+ id: tripHeading
+ Item {
+ width: page.width - Kirigami.Units.gridUnit
+ height: childrenRect.height + Kirigami.Units.smallSpacing * 2 + Math.max(2, Kirigami.Units.gridUnit / 2)
+
+ Kirigami.Heading {
+ id: sectionText
+ text: {
+ // if the tripMeta (which we get as "section") ends in ::-- we know
+ // that there's no trip -- otherwise strip the meta information before
+ // the :: and show the trip location
+ var shownText
+ var endsWithDoubleDash = /::--$/;
+ if (endsWithDoubleDash.test(section) || section === "--") {
+ shownText = ""
+ } else {
+ shownText = section.replace(/.*::/, "")
+ }
+ shownText
+ }
+ anchors {
+ top: parent.top
+ left: parent.left
+ topMargin: Math.max(2, Kirigami.Units.gridUnit / 2)
+ leftMargin: Kirigami.Units.gridUnit / 2
+ right: parent.right
+ }
+ color: textColor
+ level: 2
+ }
+ Rectangle {
+ height: Math.max(2, Kirigami.Units.gridUnit / 12) // we want a thicker line
+ anchors {
+ top: sectionText.bottom
+ left: parent.left
+ leftMargin: Kirigami.Units.gridUnit * -2
+ rightMargin: Kirigami.Units.gridUnit * -2
+ right: parent.right
+ }
+ color: subsurfaceTheme.accentColor
+ }
+ }
+ }
+
+ ScrollView {
+ id: startPageWrapper
+ anchors.fill: parent
+ opacity: (diveListView.count > 0 && (credentialStatus == QMLManager.VALID || credentialStatus == QMLManager.VALID_EMAIL)) ? 0 : 1
+ visible: opacity > 0
+ Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration } }
+ onVisibleChanged: {
+ if (visible) {
+ page.mainAction = page.saveAction
+ } else {
+ page.mainAction = page.addDiveAction
+ }
+ }
+
+ StartPage {
+ id: startPage
+ }
+ }
+
+ ListView {
+ id: diveListView
+ anchors.fill: parent
+ opacity: 0.8 - startPageWrapper.opacity
+ visible: opacity > 0
+ model: diveModel
+ currentIndex: -1
+ delegate: diveDelegate
+ //boundsBehavior: Flickable.StopAtBounds
+ maximumFlickVelocity: parent.height * 5
+ bottomMargin: Kirigami.Units.iconSizes.medium + Kirigami.Units.gridUnit
+ cacheBuffer: 0 // seems to avoid empty rendered profiles
+ section.property: "dive.tripMeta"
+ section.criteria: ViewSection.FullString
+ section.delegate: tripHeading
+ header: Kirigami.Heading {
+ x: Kirigami.Units.gridUnit / 2
+ height: paintedHeight + Kirigami.Units.gridUnit / 2
+ verticalAlignment: Text.AlignBottom
+ text: "Dive Log"
+ }
+ Connections {
+ target: detailsWindow
+ onCurrentIndexChanged: diveListView.currentIndex = detailsWindow.currentIndex
+ }
+ Connections {
+ target: stackView
+ onDepthChanged: {
+ if (stackView.depth === 1) {
+ diveListView.currentIndex = -1;
+ }
+ }
+ }
+ Connections {
+ target: header
+ onTitleBarClicked: {
+ // if we can see the dive list and it's not at the top already, go to the top,
+ // otherwise have the title bar handle the click (for bread-crumb navigation)
+ if (stackView.currentItem.objectName === "DiveList" && diveListView.contentY > Kirigami.Units.gridUnit) {
+ diveListView.positionViewAtBeginning()
+ event.accepted = true
+ } else {
+ event.accepted = false
+ }
+ }
+ }
+
+ }
+
+ property QtObject addDiveAction: Action {
+ iconName: "list-add"
+ onTriggered: {
+ startAddDive()
+ }
+ }
+
+ property QtObject saveAction: Action {
+ iconName: "document-save"
+ onTriggered: {
+ startPage.saveCredentials();
+ }
+ }
+
+ onBackRequested: {
+ if (startPageWrapper.visible && diveListView.count > 0 && manager.credentialStatus != QMLManager.INVALID) {
+ manager.credentialStatus = oldStatus
+ event.accepted = true;
+ }
+ }
+}
diff --git a/mobile-widgets/qml/DownloadFromDiveComputer.qml b/mobile-widgets/qml/DownloadFromDiveComputer.qml
new file mode 100644
index 000000000..a062ffaa0
--- /dev/null
+++ b/mobile-widgets/qml/DownloadFromDiveComputer.qml
@@ -0,0 +1,125 @@
+import QtQuick 2.3
+import QtQuick.Controls 1.2
+import QtQuick.Controls.Styles 1.2
+import QtQuick.Window 2.2
+import QtQuick.Dialogs 1.2
+import QtQuick.Layouts 1.1
+import org.subsurfacedivelog.mobile 1.0
+import org.kde.kirigami 1.0 as Kirigami
+
+Kirigami.Page {
+ id: diveComputerDownloadWindow
+ anchors.top:parent.top
+ width: parent.width
+ height: parent.height
+ Layout.fillWidth: true;
+ title: "Dive Computer"
+
+/* this can be done by hitting the back key
+ contextualActions: [
+ Action {
+ text: "Close Preferences"
+ iconName: "dialog-cancel"
+ onTriggered: {
+ stackView.pop()
+ contextDrawer.close()
+ }
+ }
+ ]
+ */
+ ColumnLayout {
+ anchors.top: parent.top
+ height: parent.height
+ width: parent.width
+ Layout.fillWidth: true
+ RowLayout {
+ anchors.top:parent.top
+ Layout.fillWidth: true
+ Text { text: " Vendor name : " }
+ ComboBox { Layout.fillWidth: true }
+ }
+ RowLayout {
+ Text { text: " Dive Computer:" }
+ ComboBox { Layout.fillWidth: true }
+ }
+ RowLayout {
+ Text { text: " Progress:" }
+ Layout.fillWidth: true
+ ProgressBar { Layout.fillWidth: true }
+ }
+ RowLayout {
+ SubsurfaceButton {
+ text: "Download"
+ onClicked: {
+ text: "Retry"
+ stackView.pop();
+ }
+ }
+ SubsurfaceButton {
+ id:quitbutton
+ text: "Quit"
+ onClicked: {
+ stackView.pop();
+ }
+ }
+ }
+ RowLayout {
+ Text {
+ text: " Downloaded dives"
+ }
+ }
+ TableView {
+ width: parent.width
+ Layout.fillWidth: true // The tableview should fill
+ Layout.fillHeight: true // all remaining vertical space
+ height: parent.height // on this screen
+ TableViewColumn {
+ width: parent.width / 2
+ role: "datetime"
+ title: "Date / Time"
+ }
+ TableViewColumn {
+ width: parent.width / 4
+ role: "duration"
+ title: "Duration"
+ }
+ TableViewColumn {
+ width: parent.width / 4
+ role: "depth"
+ title: "Depth"
+ }
+ }
+ RowLayout {
+ Layout.fillWidth: true
+ SubsurfaceButton {
+ text: "Accept"
+ onClicked: {
+ stackView.pop();
+ }
+ }
+ SubsurfaceButton {
+ text: "Quit"
+ onClicked: {
+ stackView.pop();
+ }
+ }
+ Text {
+ text: "" // Spacer between 2 button groups
+ Layout.fillWidth: true
+ }
+ SubsurfaceButton {
+ text: "Select All"
+ }
+ SubsurfaceButton {
+ id: unselectbutton
+ text: "Unselect All"
+ }
+ }
+ RowLayout { // spacer to make space for silly button
+ Layout.minimumHeight: 1.2 * unselectbutton.height
+ Text {
+ text:""
+ }
+ }
+ }
+}
diff --git a/mobile-widgets/qml/GpsList.qml b/mobile-widgets/qml/GpsList.qml
new file mode 100644
index 000000000..6903acd80
--- /dev/null
+++ b/mobile-widgets/qml/GpsList.qml
@@ -0,0 +1,128 @@
+import QtQuick 2.3
+import QtQuick.Controls 1.2
+import QtQuick.Controls.Styles 1.2
+import QtQuick.Window 2.2
+import QtQuick.Dialogs 1.2
+import QtQuick.Layouts 1.1
+import QtQuick.Window 2.2
+import org.subsurfacedivelog.mobile 1.0
+import org.kde.kirigami 1.0 as Kirigami
+
+Kirigami.ScrollablePage {
+ id: gpsListWindow
+ width: parent.width - Kirigami.Units.gridUnit
+ anchors.margins: Kirigami.Units.gridUnit / 2
+ objectName: "gpsList"
+ title: "GPS Fixes"
+
+/* this can be done by hitting the back key
+ contextualActions: [
+ Action {
+ text: "Close GPS list"
+ iconName: "dialog-cancel"
+ onTriggered: {
+ stackView.pop()
+ contextDrawer.close()
+ }
+ }
+ ]
+ */
+ Component {
+ id: gpsDelegate
+ Kirigami.SwipeListItem {
+ id: gpsFix
+ enabled: true
+ width: parent.width
+ property int horizontalPadding: Kirigami.Units.gridUnit / 2 - Kirigami.Units.smallSpacing + 1
+
+ Kirigami.BasicListItem {
+ supportsMouseEvents: true
+ width: parent.width - Kirigami.Units.gridUnit
+ height: childrenRect.height - Kirigami.Units.smallSpacing
+ GridLayout {
+ columns: 4
+ id: timeAndName
+ anchors {
+ left: parent.left
+ leftMargin: horizontalPadding
+ right: parent.right
+ rightMargin: horizontalPadding
+ }
+ Kirigami.Label {
+ text: 'Date: '
+ opacity: 0.6
+ font.pointSize: subsurfaceTheme.smallPointSize
+ }
+ Kirigami.Label {
+ text: date
+ Layout.preferredWidth: Math.max(parent.width / 5, paintedWidth)
+ font.pointSize: subsurfaceTheme.smallPointSize
+ }
+ Kirigami.Label {
+ text: 'Name: '
+ opacity: 0.6
+ font.pointSize: subsurfaceTheme.smallPointSize
+ }
+ Kirigami.Label {
+ text: name
+ Layout.preferredWidth: Math.max(parent.width / 5, paintedWidth)
+ font.pointSize: subsurfaceTheme.smallPointSize
+ }
+ Kirigami.Label {
+ text: 'Latitude: '
+ opacity: 0.6
+ font.pointSize: subsurfaceTheme.smallPointSize
+ }
+ Kirigami.Label {
+ text: latitude
+ font.pointSize: subsurfaceTheme.smallPointSize
+ }
+ Kirigami.Label {
+ text: 'Longitude: '
+ opacity: 0.6
+ font.pointSize: subsurfaceTheme.smallPointSize
+ }
+ Kirigami.Label {
+ text: longitude
+ font.pointSize: subsurfaceTheme.smallPointSize
+ }
+ }
+ }
+ actions: [
+ Kirigami.Action {
+ iconName: "trash-empty"
+ onTriggered: {
+ print("delete this!")
+ manager.deleteGpsFix(when)
+ }
+ },
+ Kirigami.Action {
+ iconName: "gps"
+ onTriggered: {
+ showMap(latitude + " " + longitude)
+ }
+ }
+
+ ]
+ }
+ }
+
+ ListView {
+ id: gpsListView
+ anchors.fill: parent
+ model: gpsModel
+ currentIndex: -1
+ delegate: gpsDelegate
+ boundsBehavior: Flickable.StopAtBounds
+ maximumFlickVelocity: parent.height * 5
+ cacheBuffer: Math.max(5000, parent.height * 5)
+ focus: true
+ clip: true
+ header: Kirigami.Heading {
+ x: Kirigami.Units.gridUnit / 2
+ height: paintedHeight + Kirigami.Units.gridUnit / 2
+ verticalAlignment: Text.AlignBottom
+ text: "List of stored GPS fixes"
+ }
+ }
+}
diff --git a/mobile-widgets/qml/Log.qml b/mobile-widgets/qml/Log.qml
new file mode 100644
index 000000000..d617901de
--- /dev/null
+++ b/mobile-widgets/qml/Log.qml
@@ -0,0 +1,40 @@
+import QtQuick 2.3
+import QtQuick.Controls 1.2
+import QtQuick.Controls.Styles 1.2
+import QtQuick.Window 2.2
+import QtQuick.Dialogs 1.2
+import QtQuick.Layouts 1.1
+import QtQuick.Window 2.2
+import org.subsurfacedivelog.mobile 1.0
+import org.kde.kirigami 1.0 as Kirigami
+
+Kirigami.ScrollablePage {
+ id: logWindow
+ width: parent.width - Kirigami.Units.gridUnit
+ anchors.margins: Kirigami.Units.gridUnit / 2
+ objectName: "Log"
+ title: "Application Log"
+
+ property int pageWidth: subsurfaceTheme.columnWidth - Kirigami.Units.smallSpacing
+
+ ColumnLayout {
+ width: pageWidth
+ spacing: Kirigami.Units.smallSpacing
+ Kirigami.Heading {
+ text: "Application Log"
+ }
+ Kirigami.Label {
+ id: logContent
+ width: parent.width
+ Layout.preferredWidth: parent.width
+ Layout.maximumWidth: parent.width
+ wrapMode: TextEdit.WrapAtWordBoundaryOrAnywhere
+ text: manager.logText
+ }
+ Rectangle {
+ color: "transparent"
+ height: Kirigami.Units.gridUnit * 2
+ width: pageWidth
+ }
+ }
+}
diff --git a/mobile-widgets/qml/Preferences.qml b/mobile-widgets/qml/Preferences.qml
new file mode 100644
index 000000000..3ec96d198
--- /dev/null
+++ b/mobile-widgets/qml/Preferences.qml
@@ -0,0 +1,74 @@
+import QtQuick 2.3
+import QtQuick.Controls 1.2
+import QtQuick.Window 2.2
+import QtQuick.Dialogs 1.2
+import QtQuick.Layouts 1.1
+import org.kde.kirigami 1.0 as Kirigami
+import org.subsurfacedivelog.mobile 1.0
+
+Kirigami.Page {
+
+ title: "Preferences"
+ mainAction: Action {
+ text: "Save"
+ iconName: "document-save"
+ onTriggered: {
+ manager.distanceThreshold = distanceThreshold.text
+ manager.timeThreshold = timeThreshold.text
+ manager.savePreferences()
+ stackView.pop()
+ }
+ }
+
+ GridLayout {
+
+ signal accept
+
+ columns: 2
+ width: parent.width - Kirigami.Units.gridUnit
+ anchors {
+ fill: parent
+ margins: Kirigami.Units.gridUnit / 2
+ }
+
+ Kirigami.Heading {
+ text: "Preferences"
+ Layout.bottomMargin: Kirigami.Units.largeSpacing / 2
+ Layout.columnSpan: 2
+ }
+
+ Kirigami.Heading {
+ text: "Subsurface GPS data webservice"
+ level: 3
+ Layout.topMargin: Kirigami.Units.largeSpacing
+ Layout.bottomMargin: Kirigami.Units.largeSpacing / 2
+ Layout.columnSpan: 2
+ }
+
+ Kirigami.Label {
+ text: "Distance threshold (meters)"
+ Layout.alignment: Qt.AlignRight
+ }
+
+ TextField {
+ id: distanceThreshold
+ text: manager.distanceThreshold
+ Layout.fillWidth: true
+ }
+
+ Kirigami.Label {
+ text: "Time threshold (minutes)"
+ Layout.alignment: Qt.AlignRight
+ }
+
+ TextField {
+ id: timeThreshold
+ text: manager.timeThreshold
+ Layout.fillWidth: true
+ }
+
+ Item {
+ Layout.fillHeight: true
+ }
+ }
+}
diff --git a/mobile-widgets/qml/StartPage.qml b/mobile-widgets/qml/StartPage.qml
new file mode 100644
index 000000000..2d70cfcb3
--- /dev/null
+++ b/mobile-widgets/qml/StartPage.qml
@@ -0,0 +1,42 @@
+import QtQuick 2.5
+import QtQuick.Controls 1.2
+import QtQuick.Controls.Styles 1.2
+import QtQuick.Layouts 1.1
+import org.kde.kirigami 1.0 as Kirigami
+import org.subsurfacedivelog.mobile 1.0
+
+ColumnLayout {
+ id: startpage
+ width: subsurfaceTheme.columnWidth
+
+ function saveCredentials() { cloudCredentials.saveCredentials() }
+
+ Kirigami.Heading {
+ Layout.margins: Kirigami.Units.gridUnit
+ text: "Subsurface-mobile"
+ }
+ Kirigami.Label {
+ id: explanationText
+ Layout.fillWidth: true
+ Layout.margins: Kirigami.Units.gridUnit
+ Layout.topMargin: 0
+ text: "In order to use Subsurface-mobile you need to have a Subsurface cloud storage account " +
+ "(which can be created with the Subsurface desktop application)."
+ wrapMode: Text.WordWrap
+ }
+ Kirigami.Label {
+ id: messageArea
+ Layout.fillWidth: true
+ Layout.margins: Kirigami.Units.gridUnit
+ Layout.topMargin: 0
+ text: manager.startPageText
+ wrapMode: Text.WordWrap
+ }
+ CloudCredentials {
+ id: cloudCredentials
+ Layout.fillWidth: true
+ Layout.margins: Kirigami.Units.gridUnit
+ Layout.topMargin: 0
+ property int headingLevel: 3
+ }
+}
diff --git a/mobile-widgets/qml/SubsurfaceButton.qml b/mobile-widgets/qml/SubsurfaceButton.qml
new file mode 100644
index 000000000..174d44659
--- /dev/null
+++ b/mobile-widgets/qml/SubsurfaceButton.qml
@@ -0,0 +1,26 @@
+import QtQuick 2.5
+import QtQuick.Controls 1.2
+import QtQuick.Controls.Styles 1.2
+import org.kde.kirigami 1.0 as Kirigami
+
+Button {
+ style: ButtonStyle {
+ padding {
+ top: Kirigami.Units.smallSpacing * 2
+ left: Kirigami.Units.smallSpacing * 4
+ right: Kirigami.Units.smallSpacing * 4
+ bottom: Kirigami.Units.smallSpacing * 2
+ }
+ background: Rectangle {
+ border.width: 1
+ radius: height / 3
+ color: control.pressed ? subsurfaceTheme.shadedColor : subsurfaceTheme.accentColor
+ }
+ label: Text{
+ text: control.text
+ color: subsurfaceTheme.accentTextColor
+ verticalAlignment: Text.AlignVCenter
+ horizontalAlignment: Text.AlignHCenter
+ }
+ }
+}
diff --git a/mobile-widgets/qml/TextButton.qml b/mobile-widgets/qml/TextButton.qml
new file mode 100644
index 000000000..3e5a36735
--- /dev/null
+++ b/mobile-widgets/qml/TextButton.qml
@@ -0,0 +1,37 @@
+import QtQuick 2.3
+
+Rectangle {
+ id: container
+
+ property alias text: label.text
+
+ signal clicked
+
+ width: label.width + 20; height: label.height + 6
+ smooth: true
+ radius: 10
+
+ gradient: Gradient {
+ GradientStop { id: gradientStop; position: 0.0; color: palette.light }
+ GradientStop { position: 1.0; color: palette.button }
+ }
+
+ SystemPalette { id: palette }
+
+ MouseArea {
+ id: mouseArea
+ anchors.fill: parent
+ onClicked: { container.clicked() }
+ }
+
+ Text {
+ id: label
+ anchors.centerIn: parent
+ }
+
+ states: State {
+ name: "pressed"
+ when: mouseArea.pressed
+ PropertyChanges { target: gradientStop; color: palette.dark }
+ }
+}
diff --git a/mobile-widgets/qml/ThemeTest.qml b/mobile-widgets/qml/ThemeTest.qml
new file mode 100644
index 000000000..c0916aea0
--- /dev/null
+++ b/mobile-widgets/qml/ThemeTest.qml
@@ -0,0 +1,115 @@
+import QtQuick 2.5
+import QtQuick.Controls 1.4
+import QtQuick.Layouts 1.1
+import QtQuick.Window 2.2
+import org.kde.kirigami 1.0 as Kirigami
+
+Kirigami.Page {
+
+ title: "Theme Information"
+/* this can be done by hitting the back key
+ contextualActions: [
+ Action {
+ text: "Close Theme info"
+ iconName: "dialog-cancel"
+ onTriggered: {
+ stackView.pop()
+ contextDrawer.close()
+ }
+ }
+ ]
+ */
+ GridLayout {
+ id: themetest
+ columns: 2
+ anchors.margins: Kirigami.Units.gridUnit / 2
+
+ Kirigami.Heading {
+ Layout.columnSpan: 2
+ text: "Theme Information"
+ }
+
+ Kirigami.Heading {
+ text: "Screen"
+ Layout.columnSpan: 2
+ level: 3
+ }
+ FontMetrics {
+ id: fm
+ }
+
+ Kirigami.Label {
+ text: "Geometry (pixels):"
+ }
+ Kirigami.Label {
+ text: rootItem.width + "x" + rootItem.height
+ }
+
+ Kirigami.Label {
+ text: "Geometry (gridUnits):"
+ }
+ Kirigami.Label {
+ text: Math.round(rootItem.width / Kirigami.Units.gridUnit) + "x" + Math.round(rootItem.height / Kirigami.Units.gridUnit)
+ }
+
+ Kirigami.Label {
+ text: "Units.gridUnit:"
+ }
+ Kirigami.Label {
+ text: Kirigami.Units.gridUnit
+ }
+
+ Kirigami.Label {
+ text: "Units.devicePixelRatio:"
+ }
+ Kirigami.Label {
+ text: Screen.devicePixelRatio
+ }
+
+ Kirigami.Heading {
+ text: "Font Metrics"
+ level: 3
+ Layout.columnSpan: 2
+ }
+
+ Kirigami.Label {
+ text: "FontMetrics pointSize:"
+ }
+ Kirigami.Label {
+ text: fm.font.pointSize
+ }
+
+ Kirigami.Label {
+ text: "FontMetrics pixelSize:"
+ }
+ Kirigami.Label {
+ text: fm.height
+ }
+
+ Kirigami.Label {
+ text: "FontMetrics devicePixelRatio:"
+ }
+ Kirigami.Label {
+ text: fm.height / fm.font.pointSize
+ }
+
+ Kirigami.Label {
+ text: "Text item pixelSize:"
+ }
+ Text {
+ text: font.pixelSize
+ }
+
+ Kirigami.Label {
+ text: "Text item pointSize:"
+ }
+ Text {
+ text: font.pointSize
+ }
+
+ Kirigami.Label {
+ Layout.columnSpan: 2
+ Layout.fillHeight: true
+ }
+ }
+}
diff --git a/mobile-widgets/qml/TopBar.qml b/mobile-widgets/qml/TopBar.qml
new file mode 100644
index 000000000..024b818b0
--- /dev/null
+++ b/mobile-widgets/qml/TopBar.qml
@@ -0,0 +1,59 @@
+import QtQuick 2.3
+import QtQuick.Controls 1.2
+import QtQuick.Controls.Styles 1.2
+import QtQuick.Window 2.2
+import QtQuick.Dialogs 1.2
+import QtQuick.Layouts 1.1
+import QtQuick.Window 2.2
+import org.kde.kirigami 1.0 as Kirigami
+import org.subsurfacedivelog.mobile 1.0
+
+Rectangle {
+ id: topPart
+
+ color: subsurfaceTheme.accentColor
+ Layout.minimumHeight: Math.round(Kirigami.Units.gridUnit * 1.5)
+ Layout.fillWidth: true
+ Layout.margins: 0
+ RowLayout {
+ anchors.verticalCenter: topPart.verticalCenter
+ Item {
+ Layout.preferredHeight: subsurfaceLogo.height
+ Layout.leftMargin: Kirigami.Units.gridUnit / 4
+ Image {
+ id: subsurfaceLogo
+ source: "qrc:/qml/subsurface-mobile-icon.png"
+ anchors {
+ verticalCenter: parent.Center
+ left: parent.left
+ }
+ width: Math.round(Kirigami.Units.gridUnit)
+ height: width
+ }
+ Kirigami.Label {
+ text: qsTr("Subsurface-mobile")
+ font.pointSize: Math.round(Kirigami.Theme.defaultFont.pointSize)
+ height: subsurfaceLogo.height
+ anchors {
+ left: subsurfaceLogo.right
+ leftMargin: Math.round(Kirigami.Units.gridUnit / 2)
+ }
+ font.weight: Font.Light
+ verticalAlignment: Text.AlignVCenter
+ Layout.fillWidth: false
+ color: subsurfaceTheme.accentTextColor
+ }
+ }
+ Item {
+ Layout.fillWidth: true
+ }
+ }
+ MouseArea {
+ anchors.fill: topPart
+ onClicked: {
+ if (stackView.depth == 1 && showingDiveList) {
+ scrollToTop()
+ }
+ }
+ }
+}
diff --git a/mobile-widgets/qml/dive.jpg b/mobile-widgets/qml/dive.jpg
new file mode 100644
index 000000000..56445648a
--- /dev/null
+++ b/mobile-widgets/qml/dive.jpg
Binary files differ
diff --git a/mobile-widgets/qml/icons/context-menu.png b/mobile-widgets/qml/icons/context-menu.png
new file mode 100644
index 000000000..df34cfd4f
--- /dev/null
+++ b/mobile-widgets/qml/icons/context-menu.png
Binary files differ
diff --git a/mobile-widgets/qml/icons/context-menu.svg b/mobile-widgets/qml/icons/context-menu.svg
new file mode 100644
index 000000000..e0750c57e
--- /dev/null
+++ b/mobile-widgets/qml/icons/context-menu.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><path d="M24 16c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 4c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm0 12c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4z"/></svg> \ No newline at end of file
diff --git a/mobile-widgets/qml/icons/main-menu.png b/mobile-widgets/qml/icons/main-menu.png
new file mode 100644
index 000000000..20729b8f5
--- /dev/null
+++ b/mobile-widgets/qml/icons/main-menu.png
Binary files differ
diff --git a/mobile-widgets/qml/icons/main-menu.svg b/mobile-widgets/qml/icons/main-menu.svg
new file mode 100644
index 000000000..1e89193f5
--- /dev/null
+++ b/mobile-widgets/qml/icons/main-menu.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><path d="M6 36h36v-4H6v4zm0-10h36v-4H6v4zm0-14v4h36v-4H6z"/></svg>
diff --git a/mobile-widgets/qml/icons/menu-back.png b/mobile-widgets/qml/icons/menu-back.png
new file mode 100644
index 000000000..dc96b7728
--- /dev/null
+++ b/mobile-widgets/qml/icons/menu-back.png
Binary files differ
diff --git a/mobile-widgets/qml/icons/menu-edit.png b/mobile-widgets/qml/icons/menu-edit.png
new file mode 100644
index 000000000..ea7dd055a
--- /dev/null
+++ b/mobile-widgets/qml/icons/menu-edit.png
Binary files differ
diff --git a/mobile-widgets/qml/main.qml b/mobile-widgets/qml/main.qml
new file mode 100644
index 000000000..f4f6ea28b
--- /dev/null
+++ b/mobile-widgets/qml/main.qml
@@ -0,0 +1,360 @@
+import QtQuick 2.4
+import QtQuick.Controls 1.2
+import QtQuick.Controls.Styles 1.2
+import QtQuick.Window 2.2
+import QtQuick.Dialogs 1.2
+import QtQuick.Layouts 1.1
+import QtQuick.Window 2.2
+import org.subsurfacedivelog.mobile 1.0
+import org.kde.kirigami 1.0 as Kirigami
+
+Kirigami.ApplicationWindow {
+ id: rootItem
+ title: qsTr("Subsurface-mobile")
+
+ header.minimumHeight: 0
+ header.preferredHeight: Kirigami.Units.gridUnit
+ header.maximumHeight: Kirigami.Units.gridUnit * 2
+ property bool fullscreen: true
+ property int oldStatus: -1
+ property alias accessingCloud: manager.accessingCloud
+ property QtObject notification: null
+ property bool showingDiveList: false
+ property alias syncToCloud: manager.syncToCloud
+ onAccessingCloudChanged: {
+ if (accessingCloud >= 0) {
+ // we now keep updating this to show progress, so timing out after 30 seconds is more useful
+ // but should still be very conservative
+ showPassiveNotification("Accessing Subsurface Cloud Storage " + accessingCloud +"%", 30000);
+ } else {
+ hidePassiveNotification();
+ }
+ }
+
+ FontMetrics {
+ id: fontMetrics
+ }
+
+ visible: false
+ opacity: 0
+
+ function returnTopPage() {
+ for (var i=stackView.depth; i>1; i--) {
+ stackView.pop()
+ }
+ detailsWindow.endEditMode()
+ }
+
+ function scrollToTop() {
+ diveList.scrollToTop()
+ }
+
+ function showMap(location) {
+ var urlPrefix = "https://www.google.com/maps/place/"
+ var locationPair = location + "/@" + location
+ var urlSuffix = ",5000m/data=!3m1!1e3!4m2!3m1!1s0x0:0x0"
+ Qt.openUrlExternally(urlPrefix + locationPair + urlSuffix)
+
+ }
+
+ function startAddDive() {
+ detailsWindow.state = "add"
+ detailsWindow.dive_id = manager.addDive();
+ detailsWindow.number = manager.getNumber(detailsWindow.dive_id)
+ detailsWindow.date = manager.getDate(detailsWindow.dive_id)
+ detailsWindow.airtemp = ""
+ detailsWindow.watertemp = ""
+ detailsWindow.buddy = ""
+ detailsWindow.depth = ""
+ detailsWindow.divemaster = ""
+ detailsWindow.notes = ""
+ detailsWindow.location = ""
+ detailsWindow.duration = ""
+ detailsWindow.suit = ""
+ detailsWindow.weight = ""
+ detailsWindow.gasmix = ""
+ detailsWindow.startpressure = ""
+ detailsWindow.endpressure = ""
+ stackView.push(detailsWindow)
+ }
+
+ globalDrawer: Kirigami.GlobalDrawer {
+ title: "Subsurface"
+ titleIcon: "qrc:/qml/subsurface-mobile-icon.png"
+
+ bannerImageSource: "dive.jpg"
+ actions: [
+ Kirigami.Action {
+ text: "Dive list"
+ onTriggered: {
+ manager.appendTextToLog("requested dive list with credential status " + manager.credentialStatus)
+ if (manager.credentialStatus == QMLManager.UNKNOWN) {
+ // the user has asked to change credentials - if the credentials before that
+ // were valid, go back to dive list
+ if (oldStatus == QMLManager.VALID || oldStatus == QMLManager.VALID_EMAIL) {
+ manager.credentialStatus = oldStatus
+ }
+ }
+ returnTopPage()
+ globalDrawer.close()
+ }
+ },
+ Kirigami.Action {
+ text: "Cloud credentials"
+ onTriggered: {
+ returnTopPage()
+ oldStatus = manager.credentialStatus
+ if (diveList.numDives > 0) {
+ manager.startPageText = "Enter different credentials or return to dive list"
+ } else {
+ manager.startPageText = "Enter valid cloud storage credentials"
+ }
+
+ manager.credentialStatus = QMLManager.UNKNOWN
+ }
+ },
+ Kirigami.Action {
+ text: "Manage dives"
+ enabled: manager.credentialStatus === QMLManager.VALID || manager.credentialStatus === QMLManager.VALID_EMAIL
+ /*
+ * disable for the beta to avoid confusion
+ Action {
+ text: "Download from computer"
+ onTriggered: {
+ detailsWindow.endEditMode()
+ stackView.push(downloadDivesWindow)
+ }
+ }
+ */
+ Kirigami.Action {
+ text: "Add dive manually"
+ onTriggered: {
+ returnTopPage() // otherwise odd things happen with the page stack
+ startAddDive()
+ }
+ }
+ Kirigami.Action {
+ text: "Manual sync with cloud"
+ onTriggered: {
+ globalDrawer.close()
+ detailsWindow.endEditMode()
+ manager.saveChanges();
+ }
+ }
+ Kirigami.Action {
+ text: syncToCloud ? "Disable auto cloud sync" : "Enable auto cloud sync"
+ onTriggered: {
+ syncToCloud = !syncToCloud
+ if (!syncToCloud) {
+ var alertText = "Turning off automatic sync to cloud causes all data to only be stored locally.\n"
+ alertText += "This can be very useful in situations with limited or no network access.\n"
+ alertText += "Please chose 'Manual sync with cloud' if you have network connectivity\n"
+ alertText += "and want to sync your data to cloud storage."
+ showPassiveNotification(alertText, 10000)
+ }
+ }
+ }
+ },
+
+ Kirigami.Action {
+ text: "GPS"
+ enabled: manager.credentialStatus === QMLManager.VALID || manager.credentialStatus === QMLManager.VALID_EMAIL
+ Kirigami.Action {
+ text: "GPS-tag dives"
+ onTriggered: {
+ manager.applyGpsData();
+ }
+ }
+
+ Kirigami.Action {
+ text: "Upload GPS data"
+ onTriggered: {
+ manager.sendGpsData();
+ }
+ }
+
+ Kirigami.Action {
+ text: "Download GPS data"
+ onTriggered: {
+ manager.downloadGpsData();
+ }
+ }
+
+ Kirigami.Action {
+ text: "Show GPS fixes"
+ onTriggered: {
+ returnTopPage()
+ manager.populateGpsData();
+ stackView.push(gpsWindow)
+ }
+ }
+
+ Kirigami.Action {
+ text: "Clear GPS cache"
+ onTriggered: {
+ manager.clearGpsData();
+ }
+ }
+ Kirigami.Action {
+ text: "Preferences"
+ onTriggered: {
+ stackView.push(prefsWindow)
+ detailsWindow.endEditMode()
+ }
+ }
+ },
+
+ Kirigami.Action {
+ text: "Developer"
+ Kirigami.Action {
+ text: "App log"
+ onTriggered: {
+ stackView.push(logWindow)
+ }
+ }
+
+ Kirigami.Action {
+ text: "Theme information"
+ onTriggered: {
+ stackView.push(themetest)
+ }
+ }
+ },
+ Kirigami.Action {
+ text: "User manual"
+ onTriggered: {
+ Qt.openUrlExternally("https://subsurface-divelog.org/documentation/subsurface-mobile-user-manual/")
+ }
+ },
+ Kirigami.Action {
+ text: "About"
+ onTriggered: {
+ stackView.push(aboutWindow)
+ detailsWindow.endEditMode()
+ }
+ }
+ ] // end actions
+
+ MouseArea {
+ height: childrenRect.height
+ width: Kirigami.Units.gridUnit * 10
+ CheckBox {
+ //text: "Run location service"
+ id: locationCheckbox
+ anchors {
+ left: parent.left
+ top: parent.top
+ }
+ checked: manager.locationServiceEnabled
+ onCheckedChanged: {
+ manager.locationServiceEnabled = checked;
+ }
+ }
+ Kirigami.Label {
+ x: Kirigami.Units.gridUnit * 1.5
+ anchors {
+ left: locationCheckbox.right
+ //leftMargin: units.smallSpacing
+ verticalCenter: locationCheckbox.verticalCenter
+ }
+ text: "Run location service"
+ }
+ onClicked: {
+ print("Click.")
+ locationCheckbox.checked = !locationCheckbox.checked
+ }
+ }
+ }
+
+ contextDrawer: Kirigami.ContextDrawer {
+ id: contextDrawer
+ actions: rootItem.pageStack.currentPage ? rootItem.pageStack.currentPage.contextualActions : null
+ title: "Actions"
+ }
+
+ QtObject {
+ id: subsurfaceTheme
+ property int titlePointSize: Math.round(fontMetrics.font.pointSize * 1.5)
+ property int smallPointSize: Math.round(fontMetrics.font.pointSize * 0.8)
+ property color accentColor: "#2d5b9a"
+ property color shadedColor: "#132744"
+ property color accentTextColor: "#ececec"
+ property color diveListTextColor: "#000000" // the Kirigami theme text color is too light
+ property int columnWidth: Math.round(rootItem.width/(Kirigami.Units.gridUnit*30)) > 0 ? Math.round(rootItem.width / Math.round(rootItem.width/(Kirigami.Units.gridUnit*30))) : rootItem.width
+ }
+/*
+ toolBar: TopBar {
+ width: parent.width
+ height: Layout.minimumHeight
+ }
+ */
+
+ property Item stackView: pageStack
+ pageStack.initialPage: DiveList {
+ anchors.fill: detailsPage
+ id: diveList
+ opacity: 0
+ Behavior on opacity {
+ NumberAnimation {
+ duration: 200
+ easing.type: Easing.OutQuad
+ }
+ }
+
+ }
+
+ QMLManager {
+ id: manager
+ }
+
+ Preferences {
+ id: prefsWindow
+ visible: false
+ }
+
+ About {
+ id: aboutWindow
+ visible: false
+ }
+
+ DiveDetails {
+ id: detailsWindow
+ visible: false
+ width: parent.width
+ height: parent.height
+ }
+
+ DownloadFromDiveComputer {
+ id: downloadDivesWindow
+ visible: false
+ }
+
+ Log {
+ id: logWindow
+ visible: false
+ }
+
+ GpsList {
+ id: gpsWindow
+ visible: false
+ }
+
+ ThemeTest {
+ id: themetest
+ visible: false
+ }
+
+ Component.onCompleted: {
+ Kirigami.Theme.highlightColor = subsurfaceTheme.accentColor
+ manager.finishSetup();
+ rootItem.visible = true
+ diveList.opacity = 1
+ rootItem.opacity = 1
+ }
+ Behavior on opacity {
+ NumberAnimation {
+ duration: 200
+ easing.type: Easing.OutQuad
+ }
+ }
+}
diff --git a/mobile-widgets/qml/mobile-resources.qrc b/mobile-widgets/qml/mobile-resources.qrc
new file mode 100644
index 000000000..e6c1fba65
--- /dev/null
+++ b/mobile-widgets/qml/mobile-resources.qrc
@@ -0,0 +1,66 @@
+<RCC>
+ <qresource prefix="/qml">
+ <file>main.qml</file>
+ <file>TextButton.qml</file>
+ <file>Preferences.qml</file>
+ <file>About.qml</file>
+ <file>CloudCredentials.qml</file>
+ <file>DiveList.qml</file>
+ <file>DiveDetails.qml</file>
+ <file>DiveDetailsEdit.qml</file>
+ <file>DiveDetailsView.qml</file>
+ <file>DownloadFromDiveComputer.qml</file>
+ <file>GpsList.qml</file>
+ <file>Log.qml</file>
+ <file>TopBar.qml</file>
+ <file>ThemeTest.qml</file>
+ <file>StartPage.qml</file>
+ <file>dive.jpg</file>
+ <file>SubsurfaceButton.qml</file>
+ <file alias="subsurface-mobile-icon.png">../../icons/subsurface-mobile-icon.png</file>
+ <file alias="main-menu.png">icons/main-menu.png</file>
+ <file alias="context-menu.png">icons/context-menu.png</file>
+ <file alias="menu-edit.png">icons/menu-edit.png</file>
+ <file alias="menu-back.png">icons/menu-back.png</file>
+ </qresource>
+ <qresource prefix="/imports">
+ <file alias="org/kde/kirigami/qmldir">kirigami/qmldir</file>
+ <file alias="org/kde/kirigami/Action.qml">kirigami/Action.qml</file>
+ <file alias="org/kde/kirigami/ApplicationWindow.qml">kirigami/ApplicationWindow.qml</file>
+ <file alias="org/kde/kirigami/BasicListItem.qml">kirigami/BasicListItem.qml</file>
+ <file alias="org/kde/kirigami/GlobalDrawer.qml">kirigami/GlobalDrawer.qml</file>
+ <file alias="org/kde/kirigami/ContextDrawer.qml">kirigami/ContextDrawer.qml</file>
+ <file alias="org/kde/kirigami/Page.qml">kirigami/Page.qml</file>
+ <file alias="org/kde/kirigami/ScrollablePage.qml">kirigami/ScrollablePage.qml</file>
+ <file alias="org/kde/kirigami/Icon.qml">kirigami/Icon.qml</file>
+ <file alias="org/kde/kirigami/Heading.qml">kirigami/Heading.qml</file>
+ <file alias="org/kde/kirigami/OverlaySheet.qml">kirigami/OverlaySheet.qml</file>
+ <file alias="org/kde/kirigami/ApplicationHeader.qml">kirigami/ApplicationHeader.qml</file>
+ <file alias="org/kde/kirigami/private/PageRow.qml">kirigami/private/PageRow.qml</file>
+ <file alias="org/kde/kirigami/Label.qml">kirigami/Label.qml</file>
+ <file alias="org/kde/kirigami/AbstractListItem.qml">kirigami/AbstractListItem.qml</file>
+ <file alias="org/kde/kirigami/SwipeListItem.qml">kirigami/SwipeListItem.qml</file>
+ <file alias="org/kde/kirigami/OverlayDrawer.qml">kirigami/OverlayDrawer.qml</file>
+ <file alias="org/kde/kirigami/Theme.qml">kirigami/Theme.qml</file>
+ <file alias="org/kde/kirigami/Units.qml">kirigami/Units.qml</file>
+ <file alias="org/kde/kirigami/private/RefreshableScrollView.qml">kirigami/private/RefreshableScrollView.qml</file>
+ <file alias="org/kde/kirigami/private/ActionButton.qml">kirigami/private/ActionButton.qml</file>
+ <file alias="org/kde/kirigami/private/BackButton.qml">kirigami/private/BackButton.qml</file>
+ <file alias="org/kde/kirigami/private/MenuIcon.qml">kirigami/private/MenuIcon.qml</file>
+ <file alias="org/kde/kirigami/private/ContextIcon.qml">kirigami/private/ContextIcon.qml</file>
+ <file alias="org/kde/kirigami/private/AbstractDrawer.qml">kirigami/private/AbstractDrawer.qml</file>
+ <file alias="org/kde/kirigami/private/PageStack.js">kirigami/private/PageStack.js</file>
+ <file alias="org/kde/kirigami/private/PassiveNotification.qml">kirigami/private/PassiveNotification.qml</file>
+ <file alias="org/kde/kirigami/icons/go-next.svg">kirigami/icons/go-next.svg</file>
+ <file alias="org/kde/kirigami/icons/go-previous.svg">kirigami/icons/go-previous.svg</file>
+ <file alias="org/kde/kirigami/icons/distribute-horizontal-x.svg">kirigami/icons/distribute-horizontal-x.svg</file>
+ <file alias="org/kde/kirigami/icons/document-edit.svg">kirigami/icons/document-edit.svg</file>
+ <file alias="org/kde/kirigami/icons/document-save.svg">kirigami/icons/document-save.svg</file>
+ <file alias="org/kde/kirigami/icons/view-readermode.svg">kirigami/icons/view-readermode.svg</file>
+ <file alias="org/kde/kirigami/icons/dialog-cancel.svg">kirigami/icons/dialog-cancel.svg</file>
+ <file alias="org/kde/kirigami/icons/application-menu.svg">kirigami/icons/application-menu.svg</file>
+ <file alias="org/kde/kirigami/icons/gps.svg">kirigami/icons/gps.svg</file>
+ <file alias="org/kde/kirigami/icons/trash-empty.svg">kirigami/icons/trash-empty.svg</file>
+ <file alias="org/kde/kirigami/icons/list-add.svg">kirigami/icons/list-add.svg</file>
+ </qresource>
+</RCC>
diff --git a/mobile-widgets/qml/theme/Theme.qml b/mobile-widgets/qml/theme/Theme.qml
new file mode 100644
index 000000000..2c51ae00f
--- /dev/null
+++ b/mobile-widgets/qml/theme/Theme.qml
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2015 Marco Martin <mart@kde.org>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Library General Public License as
+ * published by the Free Software Foundation; either version 2, 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 Library General Public License for more details
+ *
+ * You should have received a copy of the GNU Library General Public
+ * License along with this program; if not, write to the
+ * Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+import QtQuick 2.0
+
+//pragma Singleton
+
+/*!
+ \qmltype Theme
+ \inqmlmodule Material 0.1
+
+ \brief Provides access to standard colors that follow the Material Design specification.
+
+ See \l {http://www.google.com/design/spec/style/color.html#color-ui-color-application} for
+ details about choosing a color scheme for your application.
+ */
+QtObject {
+ id: theme
+
+ property color textColor: Qt.rgba(0,0,0, 0.54)
+
+ property color highlightColor: "#2196F3"
+ property color backgroundColor: "#f3f3f3"
+ property color linkColor: "#2196F3"
+ property color visitedLinkColor: "#2196F3"
+
+ property color buttonTextColor: Qt.rgba(0,0,0, 0.54)
+ property color buttonBackgroundColor: "#f3f3f3"
+ property color buttonHoverColor: "#2196F3"
+ property color buttonFocusColor: "#2196F3"
+
+ property color viewTextColor: Qt.rgba(0,0,0, 0.54)
+ property color viewBackgroundColor: "#f3f3f3"
+ property color viewHoverColor: "#2196F3"
+ property color viewFocusColor: "#2196F3"
+
+ property color complementaryTextColor: "#f3f3f3"
+ property color complementaryBackgroundColor: Qt.rgba(0,0,0, 0.54)
+ property color complementaryHoverColor: "#2196F3"
+ property color complementaryFocusColor: "#2196F3"
+}
diff --git a/mobile-widgets/qml/theme/Units.qml b/mobile-widgets/qml/theme/Units.qml
new file mode 100644
index 000000000..7cfa5c23b
--- /dev/null
+++ b/mobile-widgets/qml/theme/Units.qml
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2015 Marco Martin <mart@kde.org>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Library General Public License as
+ * published by the Free Software Foundation; either version 2, 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 Library General Public License for more details
+ *
+ * You should have received a copy of the GNU Library General Public
+ * License along with this program; if not, write to the
+ * Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+import QtQuick 2.5
+import QtQuick.Window 2.2
+
+//pragma Singleton
+
+
+QtObject {
+ id: units
+
+ /**
+ * The fundamental unit of space that should be used for sizes, expressed in pixels.
+ * Given the screen has an accurate DPI settings, it corresponds to a width of
+ * the capital letter M
+ */
+ property int gridUnit: fontMetrics.height
+
+ /**
+ * units.iconSizes provides access to platform-dependent icon sizing
+ *
+ * The icon sizes provided are normalized for different DPI, so icons
+ * will scale depending on the DPI.
+ *
+ * Icon sizes from KIconLoader, adjusted to devicePixelRatio:
+ * * small
+ * * smallMedium
+ * * medium
+ * * large
+ * * huge
+ * * enormous
+ *
+ * Not devicePixelRation-adjusted::
+ * * desktop
+ */
+ property QtObject iconSizes: QtObject {
+ property int small: 16 * devicePixelRatio
+ property int smallMedium: 22 * devicePixelRatio
+ property int medium: 32 * devicePixelRatio
+ property int large: 48 * devicePixelRatio
+ property int huge: 64 * devicePixelRatio
+ property int enormous: 128 * devicePixelRatio
+ }
+
+ /**
+ * units.smallSpacing is the amount of spacing that should be used around smaller UI elements,
+ * for example as spacing in Columns. Internally, this size depends on the size of
+ * the default font as rendered on the screen, so it takes user-configured font size and DPI
+ * into account.
+ */
+ property int smallSpacing: gridUnit/4
+
+ /**
+ * units.largeSpacing is the amount of spacing that should be used inside bigger UI elements,
+ * for example between an icon and the corresponding text. Internally, this size depends on
+ * the size of the default font as rendered on the screen, so it takes user-configured font
+ * size and DPI into account.
+ */
+ property int largeSpacing: gridUnit
+
+ /**
+ * The ratio between physical and device-independent pixels. This value does not depend on the \
+ * size of the configured font. If you want to take font sizes into account when scaling elements,
+ * use theme.mSize(theme.defaultFont), units.smallSpacing and units.largeSpacing.
+ * The devicePixelRatio follows the definition of "device independent pixel" by Microsoft.
+ */
+ property real devicePixelRatio: Screen.devicePixelRatio
+
+ /**
+ * units.longDuration should be used for longer, screen-covering animations, for opening and
+ * closing of dialogs and other "not too small" animations
+ */
+ property int longDuration: 250
+
+ /**
+ * units.shortDuration should be used for short animations, such as accentuating a UI event,
+ * hover events, etc..
+ */
+ property int shortDuration: 150
+
+ property QtObject fontMetrics: FontMetrics {}
+}
diff --git a/mobile-widgets/qml/theme/qmldir b/mobile-widgets/qml/theme/qmldir
new file mode 100644
index 000000000..c654dbad6
--- /dev/null
+++ b/mobile-widgets/qml/theme/qmldir
@@ -0,0 +1,2 @@
+#singleton Units Units.qml
+#//singleton Theme Theme.qml
diff --git a/mobile-widgets/qmlmanager.cpp b/mobile-widgets/qmlmanager.cpp
new file mode 100644
index 000000000..0bfde62aa
--- /dev/null
+++ b/mobile-widgets/qmlmanager.cpp
@@ -0,0 +1,1078 @@
+#include "qmlmanager.h"
+#include <QUrl>
+#include <QSettings>
+#include <QDebug>
+#include <QNetworkAccessManager>
+#include <QAuthenticator>
+#include <QDesktopServices>
+#include <QTextDocument>
+#include <QRegularExpression>
+#include <QApplication>
+#include <QElapsedTimer>
+
+#include "qt-models/divelistmodel.h"
+#include "qt-models/gpslistmodel.h"
+#include "core/divelist.h"
+#include "core/device.h"
+#include "core/pref.h"
+#include "core/qthelper.h"
+#include "core/qt-gui.h"
+#include "core/git-access.h"
+#include "core/cloudstorage.h"
+
+QMLManager *QMLManager::m_instance = NULL;
+
+#define RED_FONT QLatin1Literal("<font color=\"red\">")
+#define END_FONT QLatin1Literal("</font>")
+
+static void appendTextToLogStandalone(const char *text)
+{
+ QMLManager *self = QMLManager::instance();
+ if (self)
+ self->appendTextToLog(QString(text));
+}
+
+extern "C" int gitProgressCB(int percent, const char *text)
+{
+ static QElapsedTimer timer;
+ static qint64 lastTime = 0;
+ static int lastPercent = -100;
+
+ if (!timer.isValid() || percent == 0) {
+ timer.restart();
+ lastTime = 0;
+ lastPercent = -100;
+ }
+ QMLManager *self = QMLManager::instance();
+ if (self) {
+ qint64 elapsed = timer.elapsed();
+ // don't show the same status twice in 200ms
+ if (percent == lastPercent && elapsed - lastTime < 200)
+ return 0;
+ self->loadDiveProgress(percent);
+ QString logText = QString::number(elapsed / 1000.0, 'f', 1) + " / " + QString::number((elapsed - lastTime) / 1000.0, 'f', 3) +
+ QString(" : git progress %1 (%2)").arg(percent).arg(text);
+ self->appendTextToLog(logText);
+ qDebug() << logText;
+ qApp->processEvents();
+ qApp->flush();
+ lastTime = elapsed;
+ }
+ // return 0 so that we don't end the download
+ return 0;
+}
+
+QMLManager::QMLManager() : m_locationServiceEnabled(false),
+ m_verboseEnabled(false),
+ reply(0),
+ deletedDive(0),
+ deletedTrip(0),
+ m_credentialStatus(UNKNOWN),
+ m_lastDevicePixelRatio(1.0),
+ alreadySaving(false)
+{
+ m_instance = this;
+ connect(qobject_cast<QApplication *>(QApplication::instance()), &QApplication::applicationStateChanged, this, &QMLManager::applicationStateChanged);
+ appendTextToLog(getUserAgent());
+ appendTextToLog(QStringLiteral("build with Qt Version %1, runtime from Qt Version %2").arg(QT_VERSION_STR).arg(qVersion()));
+ qDebug() << "Starting" << getUserAgent();
+ qDebug() << QStringLiteral("build with Qt Version %1, runtime from Qt Version %2").arg(QT_VERSION_STR).arg(qVersion());
+ setStartPageText(tr("Starting..."));
+ setAccessingCloud(-1);
+ setSyncToCloud(true);
+ // create location manager service
+ locationProvider = new GpsLocation(&appendTextToLogStandalone, this);
+ set_git_update_cb(&gitProgressCB);
+
+ // make sure we know if the current cloud repo has been successfully synced
+ syncLoadFromCloud();
+}
+
+void QMLManager::applicationStateChanged(Qt::ApplicationState state)
+{
+ if (!timer.isValid())
+ timer.start();
+ QString stateText;
+ switch (state) {
+ case Qt::ApplicationActive: stateText = "active"; break;
+ case Qt::ApplicationHidden: stateText = "hidden"; break;
+ case Qt::ApplicationSuspended: stateText = "suspended"; break;
+ case Qt::ApplicationInactive: stateText = "inactive"; break;
+ default: stateText = QString("none of the four: 0x") + QString::number(state, 16);
+ }
+ stateText.prepend(QString::number(timer.elapsed() / 1000.0,'f', 3) + ": AppState changed to ");
+ stateText.append(" with ");
+ stateText.append((alreadySaving ? QLatin1Literal("") : QLatin1Literal("no ")) + QLatin1Literal("save ongoing"));
+ stateText.append(" and ");
+ stateText.append((unsaved_changes() ? QLatin1Literal("") : QLatin1Literal("no ")) + QLatin1Literal("unsaved changes"));
+ appendTextToLog(stateText);
+ qDebug() << stateText;
+
+ if (!alreadySaving && state == Qt::ApplicationInactive && unsaved_changes()) {
+ // FIXME
+ // make sure the user sees that we are saving data if they come back
+ // while this is running
+ alreadySaving = true;
+ saveChanges();
+ alreadySaving = false;
+ appendTextToLog(QString::number(timer.elapsed() / 1000.0,'f', 3) + ": done saving to git local / remote");
+ mark_divelist_changed(false);
+ }
+}
+
+void QMLManager::openLocalThenRemote(QString url)
+{
+ clear_dive_file_data();
+ QByteArray fileNamePrt = QFile::encodeName(url);
+ bool glo = prefs.git_local_only;
+ prefs.git_local_only = true;
+ int error = parse_file(fileNamePrt.data());
+ setAccessingCloud(-1);
+ prefs.git_local_only = glo;
+ if (error) {
+ appendTextToLog(QStringLiteral("loading dives from cache failed %1").arg(error));
+ } else {
+ // if we can load from the cache, we know that we have at least a valid email
+ if (credentialStatus() == UNKNOWN)
+ setCredentialStatus(VALID_EMAIL);
+ prefs.unit_system = informational_prefs.unit_system;
+ if (informational_prefs.unit_system == IMPERIAL)
+ informational_prefs.units = IMPERIAL_units;
+ prefs.units = informational_prefs.units;
+ int i;
+ struct dive *d;
+ process_dives(false, false);
+ DiveListModel::instance()->clear();
+ for_each_dive (i, d) {
+ DiveListModel::instance()->addDive(d);
+ }
+ appendTextToLog(QStringLiteral("%1 dives loaded from cache").arg(i));
+ }
+ appendTextToLog(QStringLiteral("have cloud credentials, trying to connect"));
+ tryRetrieveDataFromBackend();
+}
+
+void QMLManager::finishSetup()
+{
+ // Initialize cloud credentials.
+ setCloudUserName(prefs.cloud_storage_email);
+ setCloudPassword(prefs.cloud_storage_password);
+ // if the cloud credentials are valid, we should get the GPS Webservice ID as well
+ QString url;
+ if (!cloudUserName().isEmpty() &&
+ !cloudPassword().isEmpty() &&
+ getCloudURL(url) == 0) {
+ openLocalThenRemote(url);
+ } else {
+ setCredentialStatus(INCOMPLETE);
+ appendTextToLog(QStringLiteral("no cloud credentials"));
+ setStartPageText(RED_FONT + tr("Please enter valid cloud credentials.") + END_FONT);
+ }
+ setDistanceThreshold(prefs.distance_threshold);
+ setTimeThreshold(prefs.time_threshold / 60);
+}
+
+QMLManager::~QMLManager()
+{
+ m_instance = NULL;
+}
+
+QMLManager *QMLManager::instance()
+{
+ return m_instance;
+}
+
+void QMLManager::savePreferences()
+{
+ QSettings s;
+ s.beginGroup("LocationService");
+ s.setValue("time_threshold", timeThreshold() * 60);
+ prefs.time_threshold = timeThreshold() * 60;
+ s.setValue("distance_threshold", distanceThreshold());
+ prefs.distance_threshold = distanceThreshold();
+ s.sync();
+}
+
+#define CLOUDURL QString(prefs.cloud_base_url)
+#define CLOUDREDIRECTURL CLOUDURL + "/cgi-bin/redirect.pl"
+
+void QMLManager::saveCloudCredentials()
+{
+ QSettings s;
+ bool cloudCredentialsChanged = false;
+ s.beginGroup("CloudStorage");
+ s.setValue("email", cloudUserName());
+ s.setValue("password", cloudPassword());
+ s.sync();
+ if (!same_string(prefs.cloud_storage_email, qPrintable(cloudUserName()))) {
+ free(prefs.cloud_storage_email);
+ prefs.cloud_storage_email = strdup(qPrintable(cloudUserName()));
+ cloudCredentialsChanged = true;
+ }
+
+ cloudCredentialsChanged |= !same_string(prefs.cloud_storage_password, qPrintable(cloudPassword()));
+
+ if (!same_string(prefs.cloud_storage_password, qPrintable(cloudPassword()))) {
+ free(prefs.cloud_storage_password);
+ prefs.cloud_storage_password = strdup(qPrintable(cloudPassword()));
+ }
+ if (cloudUserName().isEmpty() || cloudPassword().isEmpty()) {
+ setStartPageText(RED_FONT + tr("Please enter valid cloud credentials.") + END_FONT);
+ } else if (cloudCredentialsChanged) {
+ free(prefs.userid);
+ prefs.userid = NULL;
+ syncLoadFromCloud();
+ QString url;
+ getCloudURL(url);
+ manager()->clearAccessCache(); // remove any chached credentials
+ clear_git_id(); // invalidate our remembered GIT SHA
+ clear_dive_file_data();
+ DiveListModel::instance()->clear();
+ GpsListModel::instance()->clear();
+ setStartPageText(tr("Attempting to open cloud storage with new credentials"));
+ openLocalThenRemote(url);
+ }
+}
+
+void QMLManager::checkCredentialsAndExecute(execute_function_type execute)
+{
+ // if the cloud credentials are present, we should try to get the GPS Webservice ID
+ // and (if we haven't done so) load the dive list
+ if (!same_string(prefs.cloud_storage_email, "") &&
+ !same_string(prefs.cloud_storage_password, "")) {
+ setAccessingCloud(0);
+ setStartPageText(tr("Testing cloud credentials"));
+ appendTextToLog("Have credentials, let's see if they are valid");
+ connect(manager(), &QNetworkAccessManager::authenticationRequired, this, &QMLManager::provideAuth, Qt::UniqueConnection);
+ connect(manager(), &QNetworkAccessManager::finished, this, execute, Qt::UniqueConnection);
+ QUrl url(CLOUDREDIRECTURL);
+ request = QNetworkRequest(url);
+ request.setRawHeader("User-Agent", getUserAgent().toUtf8());
+ request.setRawHeader("Accept", "text/html");
+ reply = manager()->get(request);
+ connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(handleError(QNetworkReply::NetworkError)));
+ connect(reply, &QNetworkReply::sslErrors, this, &QMLManager::handleSslErrors);
+ }
+}
+
+void QMLManager::tryRetrieveDataFromBackend()
+{
+ checkCredentialsAndExecute(&QMLManager::retrieveUserid);
+}
+
+void QMLManager::provideAuth(QNetworkReply *reply, QAuthenticator *auth)
+{
+ if (auth->user() == QString(prefs.cloud_storage_email) &&
+ auth->password() == QString(prefs.cloud_storage_password)) {
+ // OK, credentials have been tried and didn't work, so they are invalid
+ appendTextToLog("Cloud credentials are invalid");
+ setStartPageText(RED_FONT + tr("Cloud credentials are invalid") + END_FONT);
+ setCredentialStatus(INVALID);
+ reply->disconnect();
+ reply->abort();
+ reply->deleteLater();
+ return;
+ }
+ auth->setUser(prefs.cloud_storage_email);
+ auth->setPassword(prefs.cloud_storage_password);
+}
+
+void QMLManager::handleSslErrors(const QList<QSslError> &errors)
+{
+ setStartPageText(RED_FONT + tr("Cannot open cloud storage: Error creating https connection") + END_FONT);
+ Q_FOREACH (QSslError e, errors) {
+ qDebug() << e.errorString();
+ }
+ reply->abort();
+ reply->deleteLater();
+ setAccessingCloud(-1);
+}
+
+void QMLManager::handleError(QNetworkReply::NetworkError nError)
+{
+ QString errorString = reply->errorString();
+ qDebug() << "handleError" << nError << errorString;
+ setStartPageText(RED_FONT + tr("Cannot open cloud storage: %1").arg(errorString) + END_FONT);
+ reply->abort();
+ reply->deleteLater();
+ setAccessingCloud(-1);
+}
+
+void QMLManager::retrieveUserid()
+{
+ if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute) != 302) {
+ appendTextToLog(QStringLiteral("Cloud storage connection not working correctly: %1").arg(QString(reply->readAll())));
+ setAccessingCloud(-1);
+ return;
+ }
+ setCredentialStatus(VALID);
+ QString userid(prefs.userid);
+ if (userid.isEmpty()) {
+ if (same_string(prefs.cloud_storage_email, "") || same_string(prefs.cloud_storage_password, "")) {
+ appendTextToLog("cloud user name or password are empty, can't retrieve web user id");
+ setAccessingCloud(-1);
+ return;
+ }
+ appendTextToLog(QStringLiteral("calling getUserid with user %1").arg(prefs.cloud_storage_email));
+ userid = locationProvider->getUserid(prefs.cloud_storage_email, prefs.cloud_storage_password);
+ }
+ if (!userid.isEmpty()) {
+ // overwrite the existing userid
+ free(prefs.userid);
+ prefs.userid = strdup(qPrintable(userid));
+ QSettings s;
+ s.setValue("subsurface_webservice_uid", prefs.userid);
+ s.sync();
+ }
+ loadDivesWithValidCredentials();
+}
+
+void QMLManager::loadDiveProgress(int percent)
+{
+ QString text(tr("Loading dive list from cloud storage."));
+ setAccessingCloud(percent);
+ while (percent > 0) {
+ text.append(".");
+ percent -= 10;
+ }
+ setStartPageText(text);
+}
+
+void QMLManager::loadDivesWithValidCredentials()
+{
+ if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute) != 302) {
+ appendTextToLog(QStringLiteral("Cloud storage connection not working correctly: ") + reply->readAll());
+ setStartPageText(RED_FONT + tr("Cannot connect to cloud storage") + END_FONT);
+ setAccessingCloud(-1);
+ return;
+ }
+ setCredentialStatus(VALID);
+ appendTextToLog("Cloud credentials valid, loading dives...");
+ setStartPageText("Cloud credentials valid, loading dives...");
+ git_storage_update_progress(0, "load dives with valid credentials");
+ QString url;
+ if (getCloudURL(url)) {
+ QString errorString(get_error_string());
+ appendTextToLog(errorString);
+ setStartPageText(RED_FONT + tr("Cloud storage error: %1").arg(errorString) + END_FONT);
+ setAccessingCloud(-1);
+ return;
+ }
+ QByteArray fileNamePrt = QFile::encodeName(url);
+ if (check_git_sha(fileNamePrt.data()) == 0) {
+ qDebug() << "local cache was current, no need to modify dive list";
+ appendTextToLog("Cloud sync shows local cache was current");
+ setLoadFromCloud(true);
+ setAccessingCloud(-1);
+ return;
+ }
+ clear_dive_file_data();
+ DiveListModel::instance()->clear();
+
+ int error = parse_file(fileNamePrt.data());
+ setAccessingCloud(-1);
+ if (!error) {
+ report_error("filename is now %s", fileNamePrt.data());
+ const char *error_string = get_error_string();
+ appendTextToLog(error_string);
+ set_filename(fileNamePrt.data(), true);
+ } else {
+ report_error("failed to open file %s", fileNamePrt.data());
+ QString errorString(get_error_string());
+ appendTextToLog(errorString);
+ setStartPageText(RED_FONT + tr("Cloud storage error: %1").arg(errorString) + END_FONT);
+ return;
+ }
+ prefs.unit_system = informational_prefs.unit_system;
+ if (informational_prefs.unit_system == IMPERIAL)
+ informational_prefs.units = IMPERIAL_units;
+ prefs.units = informational_prefs.units;
+ process_dives(false, false);
+
+ int i;
+ struct dive *d;
+
+ for_each_dive (i, d) {
+ DiveListModel::instance()->addDive(d);
+ }
+ appendTextToLog(QStringLiteral("%1 dives loaded").arg(i));
+ if (dive_table.nr == 0)
+ setStartPageText(tr("Cloud storage open successfully. No dives in dive list."));
+ setLoadFromCloud(true);
+}
+
+void QMLManager::refreshDiveList()
+{
+ int i;
+ struct dive *d;
+ DiveListModel::instance()->clear();
+ for_each_dive (i, d) {
+ DiveListModel::instance()->addDive(d);
+ }
+}
+
+// update the dive and return the notes field, stripped of the HTML junk
+void QMLManager::commitChanges(QString diveId, QString date, QString location, QString gps, QString duration, QString depth,
+ QString airtemp, QString watertemp, QString suit, QString buddy, QString diveMaster, QString weight, QString notes,
+ QString startpressure, QString endpressure, QString gasmix)
+{
+#define DROP_EMPTY_PLACEHOLDER(_s) if ((_s) == QLatin1Literal("--")) (_s).clear()
+
+ DROP_EMPTY_PLACEHOLDER(location);
+ DROP_EMPTY_PLACEHOLDER(duration);
+ DROP_EMPTY_PLACEHOLDER(depth);
+ DROP_EMPTY_PLACEHOLDER(airtemp);
+ DROP_EMPTY_PLACEHOLDER(watertemp);
+ DROP_EMPTY_PLACEHOLDER(suit);
+ DROP_EMPTY_PLACEHOLDER(buddy);
+ DROP_EMPTY_PLACEHOLDER(diveMaster);
+ DROP_EMPTY_PLACEHOLDER(weight);
+ DROP_EMPTY_PLACEHOLDER(gasmix);
+ DROP_EMPTY_PLACEHOLDER(startpressure);
+ DROP_EMPTY_PLACEHOLDER(endpressure);
+ DROP_EMPTY_PLACEHOLDER(notes);
+
+#undef DROP_EMPTY_PLACEHOLDER
+
+ struct dive *d = get_dive_by_uniq_id(diveId.toInt());
+ // notes comes back as rich text - let's convert this into plain text
+ QTextDocument doc;
+ doc.setHtml(notes);
+ notes = doc.toPlainText();
+
+ if (!d) {
+ qDebug() << "don't touch this... no dive";
+ return;
+ }
+ bool diveChanged = false;
+ bool needResort = false;
+
+ invalidate_dive_cache(d);
+ if (date != get_dive_date_string(d->when)) {
+ diveChanged = needResort = true;
+ QDateTime newDate;
+ // what a pain - Qt will not parse dates if the day of the week is incorrect
+ // so if the user changed the date but didn't update the day of the week (most likely behavior, actually),
+ // we need to make sure we don't try to parse that
+ QString format(QString(prefs.date_format) + QChar(' ') + prefs.time_format);
+ if (format.contains(QLatin1String("ddd")) || format.contains(QLatin1String("dddd"))) {
+ QString dateFormatToDrop = format.contains(QLatin1String("ddd")) ? QStringLiteral("ddd") : QStringLiteral("dddd");
+ QDateTime ts;
+ QLocale loc = getLocale();
+ ts.setMSecsSinceEpoch(d->when * 1000L);
+ QString drop = loc.toString(ts.toUTC(), dateFormatToDrop);
+ format.replace(dateFormatToDrop, "");
+ date.replace(drop, "");
+ }
+ newDate = QDateTime::fromString(date, format);
+ if (!newDate.isValid()) {
+ qDebug() << "unable to parse date" << date << "with the given format" << format;
+ QRegularExpression isoDate("\\d+-\\d+-\\d+[^\\d]+\\d+:\\d+");
+ if (date.contains(isoDate)) {
+ newDate = QDateTime::fromString(date, "yyyy-M-d h:m:s");
+ if (newDate.isValid())
+ goto parsed;
+ newDate = QDateTime::fromString(date, "yy-M-d h:m:s");
+ if (newDate.isValid())
+ goto parsed;
+ }
+ QRegularExpression isoDateNoSecs("\\d+-\\d+-\\d+[^\\d]+\\d+");
+ if (date.contains(isoDateNoSecs)) {
+ newDate = QDateTime::fromString(date, "yyyy-M-d h:m");
+ if (newDate.isValid())
+ goto parsed;
+ newDate = QDateTime::fromString(date, "yy-M-d h:m");
+ if (newDate.isValid())
+ goto parsed;
+ }
+ QRegularExpression usDate("\\d+/\\d+/\\d+[^\\d]+\\d+:\\d+:\\d+");
+ if (date.contains(usDate)) {
+ newDate = QDateTime::fromString(date, "M/d/yyyy h:m:s");
+ if (newDate.isValid())
+ goto parsed;
+ newDate = QDateTime::fromString(date, "M/d/yy h:m:s");
+ if (newDate.isValid())
+ goto parsed;
+ newDate = QDateTime::fromString(date.toLower(), "M/d/yyyy h:m:sap");
+ if (newDate.isValid())
+ goto parsed;
+ newDate = QDateTime::fromString(date.toLower(), "M/d/yy h:m:sap");
+ if (newDate.isValid())
+ goto parsed;
+ }
+ QRegularExpression usDateNoSecs("\\d+/\\d+/\\d+[^\\d]+\\d+:\\d+");
+ if (date.contains(usDateNoSecs)) {
+ newDate = QDateTime::fromString(date, "M/d/yyyy h:m");
+ if (newDate.isValid())
+ goto parsed;
+ newDate = QDateTime::fromString(date, "M/d/yy h:m");
+ if (newDate.isValid())
+ goto parsed;
+ newDate = QDateTime::fromString(date.toLower(), "M/d/yyyy h:map");
+ if (newDate.isValid())
+ goto parsed;
+ newDate = QDateTime::fromString(date.toLower(), "M/d/yy h:map");
+ if (newDate.isValid())
+ goto parsed;
+ }
+ QRegularExpression leDate("\\d+\\.\\d+\\.\\d+[^\\d]+\\d+:\\d+:\\d+");
+ if (date.contains(leDate)) {
+ newDate = QDateTime::fromString(date, "d.M.yyyy h:m:s");
+ if (newDate.isValid())
+ goto parsed;
+ newDate = QDateTime::fromString(date, "d.M.yy h:m:s");
+ if (newDate.isValid())
+ goto parsed;
+ }
+ QRegularExpression leDateNoSecs("\\d+\\.\\d+\\.\\d+[^\\d]+\\d+:\\d+");
+ if (date.contains(leDateNoSecs)) {
+ newDate = QDateTime::fromString(date, "d.M.yyyy h:m");
+ if (newDate.isValid())
+ goto parsed;
+ newDate = QDateTime::fromString(date, "d.M.yy h:m");
+ if (newDate.isValid())
+ goto parsed;
+ }
+ }
+parsed:
+ if (newDate.isValid()) {
+ // stupid Qt... two digit years are always 19xx - WTF???
+ // so if adding a hundred years gets you into something before a year from now...
+ // add a hundred years.
+ if (newDate.addYears(100) < QDateTime::currentDateTime().addYears(1))
+ newDate = newDate.addYears(100);
+ d->dc.when = d->when = newDate.toMSecsSinceEpoch() / 1000 + gettimezoneoffset(newDate.toMSecsSinceEpoch() / 1000);
+ } else {
+ qDebug() << "none of our parsing attempts worked for the date string";
+ }
+ }
+ struct dive_site *ds = get_dive_site_by_uuid(d->dive_site_uuid);
+ char *locationtext = NULL;
+ if (ds)
+ locationtext = ds->name;
+ if (!same_string(locationtext, qPrintable(location))) {
+ diveChanged = true;
+ // this is not ideal - and it's missing the gps information
+ // but for now let's just create a new dive site
+ ds = get_dive_site_by_uuid(create_dive_site(qPrintable(location), d->when));
+ d->dive_site_uuid = ds->uuid;
+ }
+ if (!gps.isEmpty()) {
+ QString gpsString = getCurrentPosition();
+ if (gpsString != QString("waiting for the next gps location")) {
+ qDebug() << "from commitChanges call to getCurrentPosition returns" << gpsString;
+ double lat, lon;
+ if (parseGpsText(qPrintable(gpsString), &lat, &lon)) {
+ struct dive_site *ds = get_dive_site_by_uuid(d->dive_site_uuid);
+ if (ds) {
+ ds->latitude.udeg = lat * 1000000;
+ ds->longitude.udeg = lon * 1000000;
+ } else {
+ degrees_t latData, lonData;
+ latData.udeg = lat;
+ lonData.udeg = lon;
+ d->dive_site_uuid = create_dive_site_with_gps("new site", latData, lonData, d->when);
+ }
+ qDebug() << "set up dive site with new GPS data";
+ }
+ } else {
+ qDebug() << "still don't have a position - will need to implement some sort of callback";
+ }
+ }
+ if (get_dive_duration_string(d->duration.seconds, tr("h:"), tr("min")) != duration) {
+ diveChanged = true;
+ int h = 0, m = 0, s = 0;
+ QRegExp r1(QStringLiteral("(\\d*)\\s*%1[\\s,:]*(\\d*)\\s*%2[\\s,:]*(\\d*)\\s*%3").arg(tr("h")).arg(tr("min")).arg(tr("sec")), Qt::CaseInsensitive);
+ QRegExp r2(QStringLiteral("(\\d*)\\s*%1[\\s,:]*(\\d*)\\s*%2").arg(tr("h")).arg(tr("min")), Qt::CaseInsensitive);
+ QRegExp r3(QStringLiteral("(\\d*)\\s*%1").arg(tr("min")), Qt::CaseInsensitive);
+ QRegExp r4(QStringLiteral("(\\d*):(\\d*):(\\d*)"));
+ QRegExp r5(QStringLiteral("(\\d*):(\\d*)"));
+ QRegExp r6(QStringLiteral("(\\d*)"));
+ if (r1.indexIn(duration) >= 0) {
+ h = r1.cap(1).toInt();
+ m = r1.cap(2).toInt();
+ s = r1.cap(3).toInt();
+ } else if (r2.indexIn(duration) >= 0) {
+ h = r2.cap(1).toInt();
+ m = r2.cap(2).toInt();
+ } else if (r3.indexIn(duration) >= 0) {
+ m = r3.cap(1).toInt();
+ } else if (r4.indexIn(duration) >= 0) {
+ h = r4.cap(1).toInt();
+ m = r4.cap(2).toInt();
+ s = r4.cap(3).toInt();
+ } else if (r5.indexIn(duration) >= 0) {
+ h = r5.cap(1).toInt();
+ m = r5.cap(2).toInt();
+ } else if (r6.indexIn(duration) >= 0) {
+ m = r6.cap(1).toInt();
+ }
+ d->dc.duration.seconds = d->duration.seconds = h * 3600 + m * 60 + s;
+ if (same_string(d->dc.model, "manually added dive")) {
+ free(d->dc.sample);
+ d->dc.sample = 0;
+ d->dc.samples = 0;
+ } else {
+ qDebug() << "changing the duration on a dive that wasn't manually added - Uh-oh";
+ }
+
+ }
+ if (get_depth_string(d->maxdepth.mm, true, true) != depth) {
+ int depthValue = parseLengthToMm(depth);
+ // the QML code should stop negative depth, but massively huge depth can make
+ // the profile extremely slow or even run out of memory and crash, so keep
+ // the depth <= 500m
+ if (0 <= depthValue && depthValue <= 500000) {
+ diveChanged = true;
+ d->maxdepth.mm = depthValue;
+ if (same_string(d->dc.model, "manually added dive")) {
+ d->dc.maxdepth.mm = d->maxdepth.mm;
+ free(d->dc.sample);
+ d->dc.sample = 0;
+ d->dc.samples = 0;
+ }
+ }
+ }
+ if (get_temperature_string(d->airtemp, true) != airtemp) {
+ diveChanged = true;
+ d->airtemp.mkelvin = parseTemperatureToMkelvin(airtemp);
+ }
+ if (get_temperature_string(d->watertemp, true) != watertemp) {
+ diveChanged = true;
+ d->watertemp.mkelvin = parseTemperatureToMkelvin(watertemp);
+ }
+ // not sure what we'd do if there was more than one weight system
+ // defined - for now just ignore that case
+ if (weightsystem_none((void *)&d->weightsystem[1])) {
+ if (get_weight_string(d->weightsystem[0].weight, true) != weight) {
+ diveChanged = true;
+ d->weightsystem[0].weight.grams = parseWeightToGrams(weight);
+ }
+ }
+ // start and end pressures for first cylinder only
+ if (get_pressure_string(d->cylinder[0].start, true) != startpressure || get_pressure_string(d->cylinder[0].end, true) != endpressure) {
+ diveChanged = true;
+ d->cylinder[0].start.mbar = parsePressureToMbar(startpressure);
+ d->cylinder[0].end.mbar = parsePressureToMbar(endpressure);
+ if (d->cylinder[0].end.mbar > d->cylinder[0].start.mbar)
+ d->cylinder[0].end.mbar = d->cylinder[0].start.mbar;
+ }
+ // gasmix for first cylinder
+ if (get_gas_string(d->cylinder[0].gasmix) != gasmix) {
+ int o2 = parseGasMixO2(gasmix);
+ int he = parseGasMixHE(gasmix);
+ // the QML code SHOULD only accept valid gas mixes, but just to make sure
+ if (o2 >= 0 && o2 <= 1000 &&
+ he >= 0 && he <= 1000 &&
+ o2 + he <= 1000) {
+ diveChanged = true;
+ d->cylinder[0].gasmix.o2.permille = o2;
+ d->cylinder[0].gasmix.he.permille = he;
+ }
+ }
+ if (!same_string(d->suit, qPrintable(suit))) {
+ diveChanged = true;
+ free(d->suit);
+ d->suit = strdup(qPrintable(suit));
+ }
+ if (!same_string(d->buddy, qPrintable(buddy))) {
+ diveChanged = true;
+ free(d->buddy);
+ d->buddy = strdup(qPrintable(buddy));
+ }
+ if (!same_string(d->divemaster, qPrintable(diveMaster))) {
+ diveChanged = true;
+ free(d->divemaster);
+ d->divemaster = strdup(qPrintable(diveMaster));
+ }
+ if (!same_string(d->notes, qPrintable(notes))) {
+ diveChanged = true;
+ free(d->notes);
+ d->notes = strdup(qPrintable(notes));
+ }
+ // now that we have it all figured out, let's see what we need
+ // to update
+ DiveListModel *dm = DiveListModel::instance();
+ int oldModelIdx = dm->getDiveIdx(d->id);
+ int oldIdx = get_idx_by_uniq_id(d->id);
+ if (needResort) {
+ // we know that the only thing that might happen in a resort is that
+ // this one dive moves to a different spot in the dive list
+ sort_table(&dive_table);
+ int newIdx = get_idx_by_uniq_id(d->id);
+ if (newIdx != oldIdx) {
+ DiveObjectHelper *newDive = new DiveObjectHelper(d);
+ DiveListModel::instance()->removeDive(oldModelIdx);
+ DiveListModel::instance()->insertDive(oldModelIdx - (newIdx - oldIdx), newDive);
+ diveChanged = false; // because we already modified things
+ }
+ }
+ if (diveChanged) {
+ if (d->maxdepth.mm == d->dc.maxdepth.mm &&
+ d->maxdepth.mm > 0 &&
+ same_string(d->dc.model, "manually added dive") &&
+ d->dc.samples == 0) {
+ // so we have depth > 0, a manually added dive and no samples
+ // let's create an actual profile so the desktop version can work it
+ // first clear out the mean depth (or the fake_dc() function tries
+ // to be too clever
+ d->meandepth.mm = d->dc.meandepth.mm = 0;
+ d->dc = *fake_dc(&d->dc, true);
+ }
+ DiveListModel::instance()->updateDive(oldModelIdx, d);
+ }
+ if (diveChanged || needResort)
+ // we no longer save right away, but only the next time the app is not
+ // in the foreground (or when explicitly requested)
+ mark_divelist_changed(true);
+
+}
+
+void QMLManager::saveChanges()
+{
+ if (!loadFromCloud()) {
+ appendTextToLog("Don't save dives without loading from the cloud, first.");
+ return;
+ }
+ appendTextToLog("Saving dives.");
+ git_storage_update_progress(0, "saveChanges"); // reset the timers
+ QString fileName;
+ if (getCloudURL(fileName)) {
+ appendTextToLog(get_error_string());
+ return;
+ }
+ if (prefs.git_local_only == false) {
+ setAccessingCloud(0);
+ qApp->processEvents(); // make sure that the notification is actually shown
+ }
+ if (save_dives(fileName.toUtf8().data())) {
+ appendTextToLog(get_error_string());
+ setAccessingCloud(-1);
+ return;
+ }
+ setAccessingCloud(-1);
+ appendTextToLog("Updated dive list saved.");
+ set_filename(fileName.toUtf8().data(), true);
+ mark_divelist_changed(false);
+}
+
+void QMLManager::undoDelete(int id)
+{
+ if (!deletedDive || deletedDive->id != id) {
+ qDebug() << "can't find the deleted dive";
+ return;
+ }
+ if (deletedTrip)
+ insert_trip(&deletedTrip);
+ if (deletedDive->divetrip) {
+ struct dive_trip *trip = deletedDive->divetrip;
+ tripflag_t tripflag = deletedDive->tripflag; // this gets overwritten in add_dive_to_trip()
+ deletedDive->divetrip = NULL;
+ deletedDive->next = NULL;
+ deletedDive->pprev = NULL;
+ add_dive_to_trip(deletedDive, trip);
+ deletedDive->tripflag = tripflag;
+ }
+ record_dive(deletedDive);
+ DiveListModel::instance()->addDive(deletedDive);
+ // make sure the changes get saved if the app is no longer in the foreground
+ // or if the user requests a save
+ mark_divelist_changed(true);
+ deletedDive = NULL;
+ deletedTrip = NULL;
+}
+
+void QMLManager::deleteDive(int id)
+{
+ struct dive *d = get_dive_by_uniq_id(id);
+ if (!d) {
+ qDebug() << "oops, trying to delete non-existing dive";
+ return;
+ }
+ // clean up (or create) the storage for the deleted dive and trip (if applicable)
+ if (!deletedDive)
+ deletedDive = alloc_dive();
+ else
+ clear_dive(deletedDive);
+ copy_dive(d, deletedDive);
+ if (!deletedTrip) {
+ deletedTrip = (struct dive_trip *)calloc(1, sizeof(struct dive_trip));
+ } else {
+ free(deletedTrip->location);
+ free(deletedTrip->notes);
+ memset(deletedTrip, 0, sizeof(struct dive_trip));
+ }
+ // if this is the last dive in that trip, remember the trip as well
+ if (d->divetrip && d->divetrip->nrdives == 1) {
+ *deletedTrip = *d->divetrip;
+ deletedTrip->location = copy_string(d->divetrip->location);
+ deletedTrip->notes = copy_string(d->divetrip->notes);
+ deletedTrip->nrdives = 0;
+ deletedDive->divetrip = deletedTrip;
+ }
+ DiveListModel::instance()->removeDiveById(id);
+ delete_single_dive(get_idx_by_uniq_id(id));
+ // make sure the changes get saved if the app is no longer in the foreground
+ // or if the user requests a save
+ mark_divelist_changed(true);
+}
+
+QString QMLManager::addDive()
+{
+ appendTextToLog("Adding new dive.");
+ return DiveListModel::instance()->startAddDive();
+}
+
+void QMLManager::addDiveAborted(int id)
+{
+ DiveListModel::instance()->removeDiveById(id);
+}
+
+QString QMLManager::getCurrentPosition()
+{
+ return locationProvider->currentPosition();
+}
+
+void QMLManager::applyGpsData()
+{
+ if (locationProvider->applyLocations())
+ refreshDiveList();
+}
+
+void QMLManager::sendGpsData()
+{
+ locationProvider->uploadToServer();
+}
+
+void QMLManager::downloadGpsData()
+{
+ locationProvider->downloadFromServer();
+ populateGpsData();
+}
+
+void QMLManager::populateGpsData()
+{
+ if (GpsListModel::instance())
+ GpsListModel::instance()->update();
+}
+
+void QMLManager::clearGpsData()
+{
+ locationProvider->clearGpsData();
+ populateGpsData();
+}
+
+void QMLManager::deleteGpsFix(quint64 when)
+{
+ locationProvider->deleteGpsFix(when);
+ populateGpsData();
+}
+
+
+QString QMLManager::logText() const
+{
+ QString logText = m_logText + QString("\nNumer of GPS fixes: %1").arg(locationProvider->getGpsNum());
+ return logText;
+}
+
+void QMLManager::setLogText(const QString &logText)
+{
+ m_logText = logText;
+ emit logTextChanged();
+}
+
+void QMLManager::appendTextToLog(const QString &newText)
+{
+ m_logText += "\n" + newText;
+ emit logTextChanged();
+}
+
+bool QMLManager::locationServiceEnabled() const
+{
+ return m_locationServiceEnabled;
+}
+
+void QMLManager::setLocationServiceEnabled(bool locationServiceEnabled)
+{
+ m_locationServiceEnabled = locationServiceEnabled;
+ locationProvider->serviceEnable(m_locationServiceEnabled);
+}
+
+bool QMLManager::verboseEnabled() const
+{
+ return m_verboseEnabled;
+}
+
+void QMLManager::setVerboseEnabled(bool verboseMode)
+{
+ m_verboseEnabled = verboseMode;
+ verbose = verboseMode;
+ qDebug() << "verbose is" << verbose;
+ emit verboseEnabledChanged();
+}
+
+QString QMLManager::cloudPassword() const
+{
+ return m_cloudPassword;
+}
+
+void QMLManager::setCloudPassword(const QString &cloudPassword)
+{
+ m_cloudPassword = cloudPassword;
+ emit cloudPasswordChanged();
+}
+
+QString QMLManager::cloudUserName() const
+{
+ return m_cloudUserName;
+}
+
+void QMLManager::setCloudUserName(const QString &cloudUserName)
+{
+ m_cloudUserName = cloudUserName.toLower();
+ emit cloudUserNameChanged();
+}
+
+int QMLManager::distanceThreshold() const
+{
+ return m_distanceThreshold;
+}
+
+void QMLManager::setDistanceThreshold(int distance)
+{
+ m_distanceThreshold = distance;
+ emit distanceThresholdChanged();
+}
+
+int QMLManager::timeThreshold() const
+{
+ return m_timeThreshold;
+}
+
+void QMLManager::setTimeThreshold(int time)
+{
+ m_timeThreshold = time;
+ emit timeThresholdChanged();
+}
+
+bool QMLManager::loadFromCloud() const
+{
+ return m_loadFromCloud;
+}
+
+void QMLManager::syncLoadFromCloud()
+{
+ QSettings s;
+ QString cloudMarker = QLatin1Literal("loadFromCloud") + QString(prefs.cloud_storage_email);
+ m_loadFromCloud = s.contains(cloudMarker) && s.value(cloudMarker).toBool();
+}
+
+void QMLManager::setLoadFromCloud(bool done)
+{
+ QSettings s;
+ QString cloudMarker = QLatin1Literal("loadFromCloud") + QString(prefs.cloud_storage_email);
+ s.setValue(cloudMarker, done);
+ m_loadFromCloud = done;
+ emit loadFromCloudChanged();
+}
+
+QString QMLManager::startPageText() const
+{
+ return m_startPageText;
+}
+
+void QMLManager::setStartPageText(const QString& text)
+{
+ m_startPageText = text;
+ emit startPageTextChanged();
+}
+
+// this is an enum, but I don't know how to do enums in QML
+QMLManager::credentialStatus_t QMLManager::credentialStatus() const
+{
+ return m_credentialStatus;
+}
+
+void QMLManager::setCredentialStatus(const credentialStatus_t value)
+{
+ if (m_credentialStatus != value) {
+ m_credentialStatus = value;
+ emit credentialStatusChanged();
+ }
+}
+
+// where in the QML dive list is that dive?
+int QMLManager::getIndex(const QString &diveId)
+{
+ int dive_id = diveId.toInt();
+ int idx = DiveListModel::instance()->getDiveIdx(dive_id);
+ return idx;
+}
+
+QString QMLManager::getNumber(const QString& diveId)
+{
+ int dive_id = diveId.toInt();
+ struct dive *d = get_dive_by_uniq_id(dive_id);
+ QString number;
+ if (d)
+ number = QString::number(d->number);
+ return number;
+}
+
+QString QMLManager::getDate(const QString& diveId)
+{
+ int dive_id = diveId.toInt();
+ struct dive *d = get_dive_by_uniq_id(dive_id);
+ QString datestring;
+ if (d)
+ datestring = get_dive_date_string(d->when);
+ return datestring;
+}
+
+QString QMLManager::getVersion() const
+{
+ QRegExp versionRe(".*:([()\\.,\\d]+).*");
+ if (!versionRe.exactMatch(getUserAgent()))
+ return QString();
+
+ return versionRe.cap(1);
+}
+
+int QMLManager::accessingCloud() const
+{
+ return m_accessingCloud;
+}
+
+void QMLManager::setAccessingCloud(int status)
+{
+ m_accessingCloud = status;
+ emit accessingCloudChanged();
+}
+
+bool QMLManager::syncToCloud() const
+{
+ return m_syncToCloud;
+}
+
+void QMLManager::setSyncToCloud(bool status)
+{
+ m_syncToCloud = status;
+ prefs.git_local_only = !status;
+ prefs.cloud_background_sync = status;
+ QSettings s;
+ s.beginGroup("CloudStorage");
+ s.setValue("git_local_only", prefs.git_local_only);
+ s.setValue("cloud_background_sync", prefs.cloud_background_sync);
+ emit syncToCloudChanged();
+}
+
+qreal QMLManager::lastDevicePixelRatio()
+{
+ return m_lastDevicePixelRatio;
+}
+
+void QMLManager::screenChanged(QScreen *screen)
+{
+ m_lastDevicePixelRatio = screen->devicePixelRatio();
+ emit sendScreenChanged(screen);
+}
diff --git a/mobile-widgets/qmlmanager.h b/mobile-widgets/qmlmanager.h
new file mode 100644
index 000000000..bd55f68e4
--- /dev/null
+++ b/mobile-widgets/qmlmanager.h
@@ -0,0 +1,162 @@
+#ifndef QMLMANAGER_H
+#define QMLMANAGER_H
+
+#include <QObject>
+#include <QString>
+#include <QNetworkAccessManager>
+#include <QScreen>
+#include <QElapsedTimer>
+
+#include "core/gpslocation.h"
+
+class QMLManager : public QObject {
+ Q_OBJECT
+ Q_ENUMS(credentialStatus_t)
+ Q_PROPERTY(QString cloudUserName READ cloudUserName WRITE setCloudUserName NOTIFY cloudUserNameChanged)
+ Q_PROPERTY(QString cloudPassword READ cloudPassword WRITE setCloudPassword NOTIFY cloudPasswordChanged)
+ Q_PROPERTY(QString logText READ logText WRITE setLogText NOTIFY logTextChanged)
+ Q_PROPERTY(bool locationServiceEnabled READ locationServiceEnabled WRITE setLocationServiceEnabled NOTIFY locationServiceEnabledChanged)
+ Q_PROPERTY(int distanceThreshold READ distanceThreshold WRITE setDistanceThreshold NOTIFY distanceThresholdChanged)
+ Q_PROPERTY(int timeThreshold READ timeThreshold WRITE setTimeThreshold NOTIFY timeThresholdChanged)
+ Q_PROPERTY(bool loadFromCloud READ loadFromCloud WRITE setLoadFromCloud NOTIFY loadFromCloudChanged)
+ Q_PROPERTY(QString startPageText READ startPageText WRITE setStartPageText NOTIFY startPageTextChanged)
+ Q_PROPERTY(bool verboseEnabled READ verboseEnabled WRITE setVerboseEnabled NOTIFY verboseEnabledChanged)
+ Q_PROPERTY(credentialStatus_t credentialStatus READ credentialStatus WRITE setCredentialStatus NOTIFY credentialStatusChanged)
+ Q_PROPERTY(int accessingCloud READ accessingCloud WRITE setAccessingCloud NOTIFY accessingCloudChanged)
+ Q_PROPERTY(bool syncToCloud READ syncToCloud WRITE setSyncToCloud NOTIFY syncToCloudChanged)
+
+public:
+ QMLManager();
+ ~QMLManager();
+
+ enum credentialStatus_t {
+ INCOMPLETE,
+ UNKNOWN,
+ INVALID,
+ VALID_EMAIL,
+ VALID
+ };
+
+ static QMLManager *instance();
+
+ QString cloudUserName() const;
+ void setCloudUserName(const QString &cloudUserName);
+
+ QString cloudPassword() const;
+ void setCloudPassword(const QString &cloudPassword);
+
+ bool locationServiceEnabled() const;
+ void setLocationServiceEnabled(bool locationServiceEnable);
+
+ bool verboseEnabled() const;
+ void setVerboseEnabled(bool verboseMode);
+
+ int distanceThreshold() const;
+ void setDistanceThreshold(int distance);
+
+ int timeThreshold() const;
+ void setTimeThreshold(int time);
+
+ bool loadFromCloud() const;
+ void setLoadFromCloud(bool done);
+ void syncLoadFromCloud();
+
+ QString startPageText() const;
+ void setStartPageText(const QString& text);
+
+ credentialStatus_t credentialStatus() const;
+ void setCredentialStatus(const credentialStatus_t value);
+
+ QString logText() const;
+ void setLogText(const QString &logText);
+
+ int accessingCloud() const;
+ void setAccessingCloud(int status);
+
+ bool syncToCloud() const;
+ void setSyncToCloud(bool status);
+
+ typedef void (QMLManager::*execute_function_type)();
+
+public slots:
+ void applicationStateChanged(Qt::ApplicationState state);
+ void savePreferences();
+ void saveCloudCredentials();
+ void checkCredentialsAndExecute(execute_function_type execute);
+ void tryRetrieveDataFromBackend();
+ void handleError(QNetworkReply::NetworkError nError);
+ void handleSslErrors(const QList<QSslError> &errors);
+ void retrieveUserid();
+ void loadDivesWithValidCredentials();
+ void loadDiveProgress(int percent);
+ void provideAuth(QNetworkReply *reply, QAuthenticator *auth);
+ void commitChanges(QString diveId, QString date, QString location,
+ QString gps, QString duration, QString depth,
+ QString airtemp, QString watertemp, QString suit,
+ QString buddy, QString diveMaster, QString weight, QString notes,
+ QString startpressure, QString endpressure, QString gasmix);
+
+ void saveChanges();
+ void deleteDive(int id);
+ void undoDelete(int id);
+ QString addDive();
+ void addDiveAborted(int id);
+ void applyGpsData();
+ void sendGpsData();
+ void downloadGpsData();
+ void populateGpsData();
+ void clearGpsData();
+ void finishSetup();
+ void openLocalThenRemote(QString url);
+ int getIndex(const QString& diveId);
+ QString getNumber(const QString& diveId);
+ QString getDate(const QString& diveId);
+ QString getCurrentPosition();
+ QString getVersion() const;
+ void deleteGpsFix(quint64 when);
+ void refreshDiveList();
+ void screenChanged(QScreen *screen);
+ qreal lastDevicePixelRatio();
+ void appendTextToLog(const QString &newText);
+
+private:
+ QString m_cloudUserName;
+ QString m_cloudPassword;
+ QString m_ssrfGpsWebUserid;
+ QString m_startPageText;
+ QString m_logText;
+ bool m_locationServiceEnabled;
+ bool m_verboseEnabled;
+ int m_distanceThreshold;
+ int m_timeThreshold;
+ GpsLocation *locationProvider;
+ bool m_loadFromCloud;
+ static QMLManager *m_instance;
+ QNetworkReply *reply;
+ QNetworkRequest request;
+ struct dive *deletedDive;
+ struct dive_trip *deletedTrip;
+ int m_accessingCloud;
+ bool m_syncToCloud;
+ credentialStatus_t m_credentialStatus;
+ qreal m_lastDevicePixelRatio;
+ QElapsedTimer timer;
+ bool alreadySaving;
+
+signals:
+ void cloudUserNameChanged();
+ void cloudPasswordChanged();
+ void locationServiceEnabledChanged();
+ void verboseEnabledChanged();
+ void logTextChanged();
+ void timeThresholdChanged();
+ void distanceThresholdChanged();
+ void loadFromCloudChanged();
+ void startPageTextChanged();
+ void credentialStatusChanged();
+ void accessingCloudChanged();
+ void syncToCloudChanged();
+ void sendScreenChanged(QScreen *screen);
+};
+
+#endif
diff --git a/mobile-widgets/qmlprofile.cpp b/mobile-widgets/qmlprofile.cpp
new file mode 100644
index 000000000..b023741ef
--- /dev/null
+++ b/mobile-widgets/qmlprofile.cpp
@@ -0,0 +1,111 @@
+#include "qmlprofile.h"
+#include "qmlmanager.h"
+#include "profile-widget/profilewidget2.h"
+#include "core/dive.h"
+#include "core/metrics.h"
+#include <QTransform>
+#include <QScreen>
+
+QMLProfile::QMLProfile(QQuickItem *parent) :
+ QQuickPaintedItem(parent),
+ m_devicePixelRatio(1.0),
+ m_margin(0)
+{
+ setAntialiasing(true);
+ m_profileWidget = new ProfileWidget2(0);
+ m_profileWidget->setProfileState();
+ m_profileWidget->setPrintMode(true);
+ m_profileWidget->setFontPrintScale(0.8);
+ connect(QMLManager::instance(), &QMLManager::sendScreenChanged, this, &QMLProfile::screenChanged);
+ setDevicePixelRatio(QMLManager::instance()->lastDevicePixelRatio());
+}
+
+QMLProfile::~QMLProfile()
+{
+ m_profileWidget->deleteLater();
+}
+
+void QMLProfile::paint(QPainter *painter)
+{
+ // let's look at the intended size of the content and scale our scene accordingly
+ QRect painterRect = painter->viewport();
+ QRect profileRect = m_profileWidget->viewport()->rect();
+ // qDebug() << "profile viewport and painter viewport" << profileRect << painterRect;
+ qreal sceneSize = 104; // that should give us 2% margin all around (100x100 scene)
+ qreal dprComp = devicePixelRatio() * painterRect.width() / profileRect.width();
+ qreal sx = painterRect.width() / sceneSize / dprComp;
+ qreal sy = painterRect.height() / sceneSize / dprComp;
+
+ // next figure out the weird magic by which we need to shift the painter so the profile is shown
+ int dpr = rint(devicePixelRatio());
+ qreal magicShiftFactor = (dpr == 2 ? 0.25 : (dpr == 3 ? 0.33 : 0.0));
+
+ // now set up the transformations scale the profile and
+ // shift the painter (taking its existing transformation into account)
+ QTransform profileTransform = QTransform();
+ profileTransform.scale(sx, sy);
+ QTransform painterTransform = painter->transform();
+ painterTransform.translate(-painterRect.width() * magicShiftFactor ,-painterRect.height() * magicShiftFactor);
+
+#if PROFILE_SCALING_DEBUG
+ // some debugging messages to help adjust this in case the magic above is insufficient
+ QMLManager::instance()->appendTextToLog(QString("dpr %1 profile viewport %2 %3 painter viewport %4 %5").arg(dpr).arg(profileRect.width()).arg(profileRect.height())
+ .arg(painterRect.width()).arg(painterRect.height()));
+ QMLManager::instance()->appendTextToLog(QString("profile matrix %1 %2 %3 %4 %5 %6 %7 %8 %9").arg(profileTransform.m11()).arg(profileTransform.m12()).arg(profileTransform.m13())
+ .arg(profileTransform.m21()).arg(profileTransform.m22()).arg(profileTransform.m23())
+ .arg(profileTransform.m31()).arg(profileTransform.m32()).arg(profileTransform.m33()));
+ QMLManager::instance()->appendTextToLog(QString("painter matrix %1 %2 %3 %4 %5 %6 %7 %8 %9").arg(painterTransform.m11()).arg(painterTransform.m12()).arg(painterTransform.m13())
+ .arg(painterTransform.m21()).arg(painterTransform.m22()).arg(painterTransform.m23())
+ .arg(painterTransform.m31()).arg(painterTransform.m32()).arg(painterTransform.m33()));
+ qDebug() << "profile scaled by" << profileTransform.m11() << profileTransform.m22() << "and translated by" << profileTransform.m31() << profileTransform.m32();
+ qDebug() << "exist profile transform" << m_profileWidget->transform() << "painter transform" << painter->transform();
+#endif
+ // apply the transformation
+ painter->setTransform(painterTransform);
+ m_profileWidget->setTransform(profileTransform);
+
+ // finally, render the profile
+ m_profileWidget->render(painter);
+}
+
+void QMLProfile::setMargin(int margin)
+{
+ m_margin = margin;
+}
+
+QString QMLProfile::diveId() const
+{
+ return m_diveId;
+}
+
+void QMLProfile::setDiveId(const QString &diveId)
+{
+ m_diveId = diveId;
+ struct dive *d = get_dive_by_uniq_id(m_diveId.toInt());
+ if (m_diveId.toInt() < 1)
+ return;
+ if (!d)
+ return;
+ qDebug() << "setDiveId called with valid dive" << d->number;
+ m_profileWidget->plotDive(d, true);
+}
+
+qreal QMLProfile::devicePixelRatio() const
+{
+ return m_devicePixelRatio;
+}
+
+void QMLProfile::setDevicePixelRatio(qreal dpr)
+{
+ if (dpr != m_devicePixelRatio) {
+ m_devicePixelRatio = dpr;
+ m_profileWidget->setFontPrintScale(0.8 * dpr);
+ updateDevicePixelRatio(dpr);
+ emit devicePixelRatioChanged();
+ }
+}
+
+void QMLProfile::screenChanged(QScreen *screen)
+{
+ setDevicePixelRatio(screen->devicePixelRatio());
+}
diff --git a/mobile-widgets/qmlprofile.h b/mobile-widgets/qmlprofile.h
new file mode 100644
index 000000000..c8a77d700
--- /dev/null
+++ b/mobile-widgets/qmlprofile.h
@@ -0,0 +1,40 @@
+#ifndef QMLPROFILE_H
+#define QMLPROFILE_H
+
+#include <QQuickPaintedItem>
+
+class ProfileWidget2;
+
+class QMLProfile : public QQuickPaintedItem
+{
+ Q_OBJECT
+ Q_PROPERTY(QString diveId READ diveId WRITE setDiveId NOTIFY diveIdChanged)
+ Q_PROPERTY(qreal devicePixelRatio READ devicePixelRatio WRITE setDevicePixelRatio NOTIFY devicePixelRatioChanged)
+
+public:
+ explicit QMLProfile(QQuickItem *parent = 0);
+ virtual ~QMLProfile();
+
+ void paint(QPainter *painter);
+
+ QString diveId() const;
+ void setDiveId(const QString &diveId);
+ qreal devicePixelRatio() const;
+ void setDevicePixelRatio(qreal dpr);
+
+public slots:
+ void setMargin(int margin);
+ void screenChanged(QScreen *screen);
+private:
+ QString m_diveId;
+ qreal m_devicePixelRatio;
+ int m_margin;
+ ProfileWidget2 *m_profileWidget;
+
+signals:
+ void rightAlignedChanged();
+ void diveIdChanged();
+ void devicePixelRatioChanged();
+};
+
+#endif // QMLPROFILE_H