property alias minute: timePicker.minute
property Item timePicker: timePicker
+ //: Can have two values: "LTR" if remaining time in timer item should be written in "[value] [unit]" order i.e. "2 min", or "RTL" i.e. right-to-left like in Arabic writing systems
property bool leftToRight: qsTr("LTR") !== "RTL"
canAccept: true
id: timerLabelColumn
anchors.centerIn: parent
- //: Can have two values: "LTR" if remaining time in timer item should be written in "[value] [unit]" order i.e. "2 min", or "RTL" i.e. right-to-left like in Arabic writing systems
spacing: -Theme.paddingMedium
Row {
spacing: Theme.paddingSmall
SilicaFlickable {
id: journeyEditView
anchors.fill: parent
- contentHeight: journeyEditColumn.implicitHeight
+ contentHeight: headerItem.height + Theme.paddingLarge + (isPortrait ?
+ (journeyEditColumnFirst.implicitHeight + journeyEditColumnSecond.implicitHeight) :
+ Math.max(journeyEditColumnFirst.implicitHeight, journeyEditColumnSecond.implicitHeight))
VerticalScrollDecorator {}
+ DialogHeader {
+ id: headerItem
+ title: journeyEditDialog.title
+ }
+
Column {
- id: journeyEditColumn
+ id: journeyEditColumnFirst
spacing: Theme.paddingMedium
- width: parent.width
-
- DialogHeader {
- title: journeyEditDialog.title
- }
+ width: isPortrait ? parent.width : (parent.width * 0.5)
+ x: 0
+ y: headerItem.height
ValueButton {
id: startDate
})
}
}
+ }
+
+ Column {
+ id: journeyEditColumnSecond
+ spacing: Theme.paddingMedium
+ width: isPortrait ? parent.width : (parent.width * 0.5)
+ x: isPortrait ? 0 : (parent.width * 0.5)
+ y: isPortrait ? journeyEditColumnFirst.y + journeyEditColumnFirst.height + Theme.paddingLarge : journeyEditColumnFirst.y
TextField {
id: faster
placeholderText: label
text: overtakenby >= 0 ? "" + overtakenby : ""
horizontalAlignment: TextInput.AlignLeft
- EnterKey.iconSource: "image://theme/icon-m-enter-next"
+ EnterKey.iconSource: "image://theme/icon-m-enter-accept"
EnterKey.onClicked: journeyEditDialog.accept()
}
}
+
}
onAccepted: {
SilicaFlickable {
id: journeyEditView
anchors.fill: parent
- contentHeight: journeyEditColumn.implicitHeight
+ contentHeight: journeyEditColumn.implicitHeight + headerItem.height
VerticalScrollDecorator {}
+ PageHeader {
+ id: headerItem
+ title: journeyInfoPage.title
+ }
+
Column {
id: journeyEditColumn
spacing: Theme.paddingLarge
- width: parent.width
-
- PageHeader {
- title: journeyInfoPage.title
- }
+ width: isPortrait ? parent.width : parent.width * 0.5
+ y: headerItem.height
InfoRow {
id: startDate
// The effective value will be restricted by ApplicationWindow.allowedOrientations
allowedOrientations: Orientation.All
- property int columnwidth: page.width - 2 * Theme.horizontalPageMargin
+ property int columnwidth: page.width - (2 * Theme.horizontalPageMargin) - 4 * Theme.paddingLarge
SilicaListView {
id: listView
model: journeymodel
anchors.fill: parent
- header: PageHeader {
- title: qsTr("Journey list")
+ header: Column {
+ width: page.width
+ spacing: Theme.paddingLarge
+ height: implicitHeight + Theme.paddingLarge
+
+ PageHeader {
+ title: qsTr("Journey list")
+ }
+
+ Row {
+ spacing: Theme.paddingLarge
+ x: Theme.horizontalPageMargin
+ y: headerItem.height
+
+ Label {
+ width: columnwidth * 0.34
+ text: "Date"
+ color: Theme.secondaryColor
+ }
+ Label {
+ width: columnwidth * 0.18
+ text: "Start"
+ color: Theme.secondaryColor
+ }
+ Label {
+ width: columnwidth * 0.18
+ text: "Length"
+ color: Theme.secondaryColor
+ }
+ Label {
+ width: columnwidth * 0.15
+ text: "+"
+ color: Theme.secondaryColor
+ horizontalAlignment: Text.AlignRight
+ }
+ Label {
+ width: columnwidth * 0.15
+ text: "-"
+ color: Theme.secondaryColor
+ horizontalAlignment: Text.AlignRight
+ }
+ }
}
+
delegate: ListItem {
id: delegate
menu: journeyMenuComponent
x: Theme.horizontalPageMargin
Label {
- width: columnwidth / 3.0
+ width: columnwidth * 0.34
text: Qt.formatDate(journeymodel.epochToDate(start), "d MMM yyyy")
color: delegate.highlighted ? Theme.highlightColor : Theme.primaryColor
}
Label {
- width: columnwidth / 3.0
+ width: columnwidth * 0.18
text: Qt.formatTime(journeymodel.epochToTime(start), "hh:mm")
color: delegate.highlighted ? Theme.highlightColor : Theme.primaryColor
}
Label {
- width: columnwidth / 3.0
+ width: columnwidth * 0.18
text: Qt.formatTime(new Date(0, 0, 0, 0, 0, duration), 'hh:mm')
color: delegate.highlighted ? Theme.highlightColor : Theme.primaryColor
}
+ Label {
+ width: columnwidth * 0.15
+ text: overtook
+ color: delegate.highlighted ? Theme.highlightColor : Theme.primaryColor
+ horizontalAlignment: Text.AlignRight
+ }
+ Label {
+ width: columnwidth * 0.15
+ text: overtakenby
+ color: delegate.highlighted ? Theme.highlightColor : Theme.primaryColor
+ horizontalAlignment: Text.AlignRight
+ }
}
onClicked: pageStack.push(Qt.resolvedUrl("JourneyInfo.qml"), {title: "Journey info", index: index, start: journeymodel.epochToDateTime(start), duration: duration, overtook: overtook, overtakenby: overtakenby})
SilicaFlickable {
anchors.fill: parent
+ VerticalScrollDecorator {}
+
// PullDownMenu and PushUpMenu must be declared in SilicaFlickable, SilicaListView or SilicaGridView
PullDownMenu {
MenuItem {
}
// Tell SilicaFlickable the height of its content.
- contentHeight: column.height
+ contentHeight: column.implicitHeight + Theme.paddingLarge
// Place our content in a Column. The PageHeader is always placed at the top
// of the page, followed by our content.
SilicaFlickable {
id: statsView
anchors.fill: parent
- contentHeight: statsColumn.implicitHeight
+ contentHeight: statsColumn.implicitHeight + headerItem.height
VerticalScrollDecorator {}
+ PageHeader {
+ id: headerItem
+ title: qsTr("Stats")
+ }
+
Column {
id: statsColumn
spacing: Theme.paddingLarge
- width: parent.width
-
- PageHeader {
- title: qsTr("Stats")
- }
+ width: isPortrait ? parent.width : parent.width * 0.5
+ y: headerItem.height
InfoRow {
label: qsTr("Journeys:")
- value: "0"
+ value: currentStatus.getJourneyCount()
midlineRatio: 0.7
midlineMin: Theme.fontSizeSmall * 10
- midlineMax: Theme.fontSizeSmall * 15
+ midlineMax: Theme.fontSizeSmall * 20
pixelSize: Theme.fontSizeMedium
labelTextBold: true
+ horizontalAlignment: Text.AlignRight
}
InfoRow {
label: qsTr("Time spent cycling:")
- value: "0"
- midlineRatio: 0.7
+ value: currentStatus.getFormattedTime(currentStatus.getTimeSpentCycling(), 0, 5)
+ midlineRatio: 0.5
midlineMin: Theme.fontSizeSmall * 10
- midlineMax: Theme.fontSizeSmall * 15
+ midlineMax: Theme.fontSizeSmall * 20
pixelSize: Theme.fontSizeMedium
labelTextBold: true
+ horizontalAlignment: Text.AlignRight
}
InfoRow {
label: qsTr("Average journey duration:")
- value: "0"
- midlineRatio: 0.7
+ value: currentStatus.getFormattedTime(currentStatus.getAverageDuration(), 1, 5)
+ midlineRatio: 0.6
midlineMin: Theme.fontSizeSmall * 10
- midlineMax: Theme.fontSizeSmall * 15
+ midlineMax: Theme.fontSizeSmall * 20
pixelSize: Theme.fontSizeMedium
labelTextBold: true
+ horizontalAlignment: Text.AlignRight
}
InfoRow {
label: qsTr("Speed percentile:")
- value: "0%"
+ value: Math.round(100.0 - currentStatus.getSpeedPercentile() * 100) + "%"
midlineRatio: 0.7
midlineMin: Theme.fontSizeSmall * 10
- midlineMax: Theme.fontSizeSmall * 15
+ midlineMax: Theme.fontSizeSmall * 20
pixelSize: Theme.fontSizeMedium
labelTextBold: true
+ horizontalAlignment: Text.AlignRight
}
}
}
qmlRegisterSingletonType<Settings>("harbour.pedalo.settings", 1, 0, "Settings", Settings::provider);
JourneyModel journeys;
- Status currentStatus;
+ Status currentStatus(journeys);
Settings::getInstance().setMainStatus(currentStatus);
Settings::getInstance().loadSettings();
date.setMSecsSinceEpoch(epoch);
return date;
}
+
+QList<Journey> const & JourneyModel::getData() const {
+ return journeys;
+}
Q_INVOKABLE static QDate epochToDate(quint64 epoch);
Q_INVOKABLE static QTime epochToTime(quint64 epoch);
Q_INVOKABLE static QDateTime epochToDateTime(quint64 epoch);
+
+ QList<Journey> const & getData() const;
signals:
// General signals
void journeysChanged();
#include <QDateTime>
+#include <QDebug>
#include "status.h"
-Status::Status(QObject *parent) : QObject(parent),
+Status::Status(JourneyModel &journeymodel, QObject *parent) : QObject(parent),
cycling(false),
- startTime(0u)
+ startTime(0u),
+ journeymodel(journeymodel)
{
}
setCycling(true);
setStartTime(QDateTime::currentMSecsSinceEpoch());
}
+
+quint64 Status::getJourneyCount() const {
+ return journeymodel.rowCount();
+}
+
+quint64 Status::getTimeSpentCycling() const {
+ quint64 time;
+ QList<Journey> const & journeys = journeymodel.getData();
+
+ time = 0u;
+ foreach(Journey journey, journeys) {
+ time += journey.getDuration();
+ }
+
+ return time;
+}
+
+double Status::getAverageDuration() const {
+ quint64 time = getTimeSpentCycling();
+ quint64 count = Status::getJourneyCount();
+
+ return ((double)time / (double)count);
+}
+
+double Status::getSpeedPercentile() const {
+ quint64 overtook;
+ quint64 overtakenby;
+ QList<Journey> const & journeys = journeymodel.getData();
+ double percentile;
+
+ overtook = 0u;
+ overtakenby = 0u;
+ foreach(Journey journey, journeys) {
+ overtook += journey.getOvertook();
+ overtakenby += journey.getOvertakenBy();
+ }
+
+ percentile = (double)overtook / (double)(overtook + overtakenby);
+
+ return percentile;
+}
+
+QString Status::getFormattedTime(quint64 seconds, int min, int max) {
+ static const QString plural[5] = {"s", "m", "h", "d", "y"};
+ static const QString singular[5] = {"s", "m", "h", "d", "y"};
+ static const quint64 base[5] = {60, 60, 24, 365, (quint64)-1};
+ quint64 remaining;
+ quint64 portion;
+ QString formatted;
+ QList<quint64> portions;
+
+ min = qBound(0, min, static_cast<int>(sizeof(base)));
+ max = qBound(0, max, static_cast<int>(sizeof(base)));
+
+ remaining = seconds;
+ for (int unit = 0; unit < max; unit++) {
+ portion = (unit == max - 1) ? remaining : remaining % base[unit];
+ portions << portion;
+ remaining /= base[unit];
+ qDebug() << plural[unit] << ": " << portion;
+ }
+
+ formatted = "";
+ for (int unit = max - 1; unit >= min; unit--) {
+ portion = portions[unit];
+
+ if (portion != 0) {
+ if (formatted.length() > 0) {
+ formatted += " ";
+ }
+ formatted += QString::number(portion) + " " + ((portion == 1) ? singular[unit] : plural[unit]);
+ }
+ }
+
+ return formatted;
+}
+
#include <QObject>
+#include "journeymodel.h"
+
class Status : public QObject
{
Q_OBJECT
public:
- explicit Status(QObject *parent = nullptr);
+ explicit Status(JourneyModel &journeymodel, QObject *parent = nullptr);
Q_PROPERTY(bool cycling READ getCycling WRITE setCycling NOTIFY cyclingChanged)
Q_PROPERTY(quint64 startTime READ getStartTime WRITE setStartTime NOTIFY startTimeChanged)
+ // Current journey
bool getCycling() const;
quint64 getStartTime() const;
Q_INVOKABLE void startJourney();
Q_INVOKABLE quint64 getDuration() const;
+ // Statistics
+ Q_INVOKABLE quint64 getJourneyCount() const;
+ Q_INVOKABLE quint64 getTimeSpentCycling() const;
+ Q_INVOKABLE double getAverageDuration() const;
+ Q_INVOKABLE double getSpeedPercentile() const;
+
+ Q_INVOKABLE static QString getFormattedTime(quint64 seconds, int min, int max);
+
signals:
void cyclingChanged(bool cycling);
void startTimeChanged(quint64 startTime);
private:
bool cycling;
quint64 startTime;
+ JourneyModel &journeymodel;
};
#endif // STATUS_H
<name>DurationEditDialog</name>
<message>
<source>LTR</source>
+ <extracomment>Can have two values: "LTR" if remaining time in timer item should be written in "[value] [unit]" order i.e. "2 min", or "RTL" i.e. right-to-left like in Arabic writing systems</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<name>DurationEditDialog</name>
<message>
<source>LTR</source>
+ <extracomment>Can have two values: "LTR" if remaining time in timer item should be written in "[value] [unit]" order i.e. "2 min", or "RTL" i.e. right-to-left like in Arabic writing systems</extracomment>
<translation type="unfinished"></translation>
</message>
<message>