mirror of
https://github.com/cagnulein/qdomyos-zwift.git
synced 2026-02-18 00:17:41 +01:00
Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
777ccefc5e | ||
|
|
17a4c838b7 | ||
|
|
9214ab63ad | ||
|
|
04f919fe86 | ||
|
|
a106438fb0 | ||
|
|
c336043bb2 | ||
|
|
f70f4717f7 | ||
|
|
f2da15c477 | ||
|
|
83690df66e | ||
|
|
cafc785420 | ||
|
|
0b1e6606cb | ||
|
|
82d9e252ac | ||
|
|
1a6ef489d0 | ||
|
|
d6028c33b6 | ||
|
|
66554e0a4a | ||
|
|
aa1058f6be | ||
|
|
33589f27d3 | ||
|
|
ae1147025e | ||
|
|
189c27eeb0 | ||
|
|
dab74e4bfd | ||
|
|
8267661f70 | ||
|
|
204897680c | ||
|
|
b93df0d0c9 | ||
|
|
038c347cc2 | ||
|
|
4683084ed2 | ||
|
|
ae131c7cad | ||
|
|
67f5446fc2 | ||
|
|
4ce8e1b20c | ||
|
|
57cdf246a9 | ||
|
|
4cc0b2520b | ||
|
|
8f4364f525 | ||
|
|
df0ab76187 | ||
|
|
b7530dcdff | ||
|
|
12dc37191e | ||
|
|
3b0b7e4bd9 | ||
|
|
a3e6640e3f | ||
|
|
27922da186 | ||
|
|
efb3b74149 | ||
|
|
c7cc127ea0 | ||
|
|
c1dbaf0f05 | ||
|
|
cbbc054ec8 | ||
|
|
cb0e7a9389 | ||
|
|
7256cb5ef5 | ||
|
|
f731651c71 | ||
|
|
38e9987fff | ||
|
|
28c4084780 | ||
|
|
02f4154577 | ||
|
|
6e8ef20efd | ||
|
|
7b01867934 |
@@ -580,6 +580,12 @@
|
||||
87EB918A27EE5FE7002535E1 /* qdomyoszwift_qmltyperegistrations.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EB917F27EE5FE7002535E1 /* qdomyoszwift_qmltyperegistrations.cpp */; };
|
||||
87EB918B27EE5FE7002535E1 /* moc_inappproductqmltype.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EB918027EE5FE7002535E1 /* moc_inappproductqmltype.cpp */; };
|
||||
87EB918C27EE5FE7002535E1 /* moc_inappproduct.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EB918127EE5FE7002535E1 /* moc_inappproduct.cpp */; };
|
||||
87EBB2A62D39214E00348B15 /* moc_workoutloaderworker.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EBB2A02D39214E00348B15 /* moc_workoutloaderworker.cpp */; };
|
||||
87EBB2A72D39214E00348B15 /* workoutmodel.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EBB2A52D39214E00348B15 /* workoutmodel.cpp */; };
|
||||
87EBB2A82D39214E00348B15 /* workoutloaderworker.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EBB2A32D39214E00348B15 /* workoutloaderworker.cpp */; };
|
||||
87EBB2A92D39214E00348B15 /* moc_fitdatabaseprocessor.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EBB29F2D39214E00348B15 /* moc_fitdatabaseprocessor.cpp */; };
|
||||
87EBB2AA2D39214E00348B15 /* fitdatabaseprocessor.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EBB29E2D39214E00348B15 /* fitdatabaseprocessor.cpp */; };
|
||||
87EBB2AB2D39214E00348B15 /* moc_workoutmodel.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EBB2A12D39214E00348B15 /* moc_workoutmodel.cpp */; };
|
||||
87EFB56E25BD703D0039DD5A /* proformtreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EFB56C25BD703C0039DD5A /* proformtreadmill.cpp */; };
|
||||
87EFB57025BD704A0039DD5A /* moc_proformtreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EFB56F25BD704A0039DD5A /* moc_proformtreadmill.cpp */; };
|
||||
87EFE45927A518F5006EA1C3 /* nautiluselliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EFE45827A518F5006EA1C3 /* nautiluselliptical.cpp */; };
|
||||
@@ -1653,6 +1659,15 @@
|
||||
87EB917F27EE5FE7002535E1 /* qdomyoszwift_qmltyperegistrations.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = qdomyoszwift_qmltyperegistrations.cpp; sourceTree = "<group>"; };
|
||||
87EB918027EE5FE7002535E1 /* moc_inappproductqmltype.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_inappproductqmltype.cpp; sourceTree = "<group>"; };
|
||||
87EB918127EE5FE7002535E1 /* moc_inappproduct.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_inappproduct.cpp; sourceTree = "<group>"; };
|
||||
87EBB29D2D39214E00348B15 /* fitdatabaseprocessor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = fitdatabaseprocessor.h; path = ../src/fitdatabaseprocessor.h; sourceTree = SOURCE_ROOT; };
|
||||
87EBB29E2D39214E00348B15 /* fitdatabaseprocessor.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = fitdatabaseprocessor.cpp; path = ../src/fitdatabaseprocessor.cpp; sourceTree = SOURCE_ROOT; };
|
||||
87EBB29F2D39214E00348B15 /* moc_fitdatabaseprocessor.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_fitdatabaseprocessor.cpp; sourceTree = "<group>"; };
|
||||
87EBB2A02D39214E00348B15 /* moc_workoutloaderworker.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_workoutloaderworker.cpp; sourceTree = "<group>"; };
|
||||
87EBB2A12D39214E00348B15 /* moc_workoutmodel.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_workoutmodel.cpp; sourceTree = "<group>"; };
|
||||
87EBB2A22D39214E00348B15 /* workoutloaderworker.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = workoutloaderworker.h; path = ../src/workoutloaderworker.h; sourceTree = SOURCE_ROOT; };
|
||||
87EBB2A32D39214E00348B15 /* workoutloaderworker.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = workoutloaderworker.cpp; path = ../src/workoutloaderworker.cpp; sourceTree = SOURCE_ROOT; };
|
||||
87EBB2A42D39214E00348B15 /* workoutmodel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = workoutmodel.h; path = ../src/workoutmodel.h; sourceTree = SOURCE_ROOT; };
|
||||
87EBB2A52D39214E00348B15 /* workoutmodel.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = workoutmodel.cpp; path = ../src/workoutmodel.cpp; sourceTree = SOURCE_ROOT; };
|
||||
87EFB56C25BD703C0039DD5A /* proformtreadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = proformtreadmill.cpp; path = ../src/devices/proformtreadmill/proformtreadmill.cpp; sourceTree = "<group>"; };
|
||||
87EFB56D25BD703C0039DD5A /* proformtreadmill.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = proformtreadmill.h; path = ../src/devices/proformtreadmill/proformtreadmill.h; sourceTree = "<group>"; };
|
||||
87EFB56F25BD704A0039DD5A /* moc_proformtreadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_proformtreadmill.cpp; sourceTree = "<group>"; };
|
||||
@@ -2240,6 +2255,15 @@
|
||||
2EB56BE3C2D93CDAB0C52E67 /* Sources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
87EBB29D2D39214E00348B15 /* fitdatabaseprocessor.h */,
|
||||
87EBB29E2D39214E00348B15 /* fitdatabaseprocessor.cpp */,
|
||||
87EBB29F2D39214E00348B15 /* moc_fitdatabaseprocessor.cpp */,
|
||||
87EBB2A02D39214E00348B15 /* moc_workoutloaderworker.cpp */,
|
||||
87EBB2A12D39214E00348B15 /* moc_workoutmodel.cpp */,
|
||||
87EBB2A22D39214E00348B15 /* workoutloaderworker.h */,
|
||||
87EBB2A32D39214E00348B15 /* workoutloaderworker.cpp */,
|
||||
87EBB2A42D39214E00348B15 /* workoutmodel.h */,
|
||||
87EBB2A52D39214E00348B15 /* workoutmodel.cpp */,
|
||||
878C9DC62DF01C16001114D5 /* moc_speraxtreadmill.cpp */,
|
||||
878C9DC72DF01C16001114D5 /* speraxtreadmill.h */,
|
||||
878C9DC82DF01C16001114D5 /* speraxtreadmill.cpp */,
|
||||
@@ -3805,6 +3829,12 @@
|
||||
8768C9022BBC12B80099DBE1 /* socket_loopback_client.c in Compile Sources */,
|
||||
87C5F0B926285E5F0067A1B5 /* mimehtml.cpp in Compile Sources */,
|
||||
27E452D452B62D0948DF0755 /* sessionline.cpp in Compile Sources */,
|
||||
87EBB2A62D39214E00348B15 /* moc_workoutloaderworker.cpp in Compile Sources */,
|
||||
87EBB2A72D39214E00348B15 /* workoutmodel.cpp in Compile Sources */,
|
||||
87EBB2A82D39214E00348B15 /* workoutloaderworker.cpp in Compile Sources */,
|
||||
87EBB2A92D39214E00348B15 /* moc_fitdatabaseprocessor.cpp in Compile Sources */,
|
||||
87EBB2AA2D39214E00348B15 /* fitdatabaseprocessor.cpp in Compile Sources */,
|
||||
87EBB2AB2D39214E00348B15 /* moc_workoutmodel.cpp in Compile Sources */,
|
||||
E40895A73216AC52D35083D9 /* signalhandler.cpp in Compile Sources */,
|
||||
873CD22427EF8E18000131BC /* inappproductqmltype.cpp in Compile Sources */,
|
||||
87DF68BF25E2675100FCDA46 /* moc_schwinnic4bike.cpp in Compile Sources */,
|
||||
@@ -4381,7 +4411,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1110;
|
||||
CURRENT_PROJECT_VERSION = 1115;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = NO;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = "ADB_HOST=1";
|
||||
@@ -4417,6 +4447,7 @@
|
||||
../../Qt/5.15.2/ios/include/QtCore/5.15.2/QtCore/private,
|
||||
../../Qt/5.15.2/ios/include/QtCore/5.15.2,
|
||||
../../Qt/5.15.2/ios/include/QtCore/5.15.2/QtCore/,
|
||||
../../Qt/5.15.2/ios/include/QtSql,
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
/Users/cagnulein/Qt/5.15.2/ios/plugins/platforms,
|
||||
@@ -4575,7 +4606,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1110;
|
||||
CURRENT_PROJECT_VERSION = 1115;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -4613,6 +4644,7 @@
|
||||
../../Qt/5.15.2/ios/include/QtCore/5.15.2/QtCore/private,
|
||||
../../Qt/5.15.2/ios/include/QtCore/5.15.2,
|
||||
../../Qt/5.15.2/ios/include/QtCore/5.15.2/QtCore/,
|
||||
../../Qt/5.15.2/ios/include/QtSql,
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
/Users/cagnulein/Qt/5.15.2/ios/plugins/platforms,
|
||||
@@ -4805,7 +4837,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1110;
|
||||
CURRENT_PROJECT_VERSION = 1115;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = YES;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
@@ -4901,7 +4933,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1110;
|
||||
CURRENT_PROJECT_VERSION = 1115;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = YES;
|
||||
@@ -4993,7 +5025,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1110;
|
||||
CURRENT_PROJECT_VERSION = 1115;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
|
||||
ENABLE_BITCODE = YES;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -5109,7 +5141,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1110;
|
||||
CURRENT_PROJECT_VERSION = 1115;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
|
||||
ENABLE_BITCODE = YES;
|
||||
|
||||
51
src/PreviewChart.qml
Normal file
51
src/PreviewChart.qml
Normal file
@@ -0,0 +1,51 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Layouts 1.3
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Controls.Material 2.0
|
||||
import Qt.labs.settings 1.0
|
||||
import QtWebView 1.1
|
||||
|
||||
ColumnLayout {
|
||||
signal popupclose()
|
||||
id: column1
|
||||
spacing: 10
|
||||
anchors.fill: parent
|
||||
Settings {
|
||||
id: settings
|
||||
}
|
||||
WebView {
|
||||
id: webView
|
||||
anchors.fill: parent
|
||||
url: "http://localhost:" + settings.value("template_inner_QZWS_port") + "/previewchart/chart.htm"
|
||||
visible: true
|
||||
onLoadingChanged: {
|
||||
if (loadRequest.errorString) {
|
||||
console.error(loadRequest.errorString);
|
||||
console.error("port " + settings.value("template_inner_QZWS_port"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: chartJscheckStartFromWeb
|
||||
interval: 200; running: true; repeat: true
|
||||
onTriggered: {if(rootItem.startRequested) {rootItem.startRequested = false; rootItem.stopRequested = false; stackView.pop(); }}
|
||||
}
|
||||
|
||||
Button {
|
||||
id: closeButton
|
||||
height: 50
|
||||
width: parent.width
|
||||
text: "Close"
|
||||
Layout.alignment: Qt.AlignCenter | Qt.AlignVCenter
|
||||
onClicked: {
|
||||
popupclose();
|
||||
}
|
||||
anchors {
|
||||
bottom: parent.bottom
|
||||
}
|
||||
}
|
||||
Component.onCompleted: {
|
||||
headerToolbar.visible = true;
|
||||
}
|
||||
}
|
||||
227
src/WorkoutsHistory.qml
Normal file
227
src/WorkoutsHistory.qml
Normal file
@@ -0,0 +1,227 @@
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
import QtCharts 2.15
|
||||
|
||||
Page {
|
||||
id: workoutHistoryPage
|
||||
|
||||
// Signal for chart preview
|
||||
signal fitfile_preview_clicked(var url)
|
||||
|
||||
// Sport type to icon mapping (using FIT_SPORT values)
|
||||
function getSportIcon(sport) {
|
||||
switch(parseInt(sport)) {
|
||||
case 1: // FIT_SPORT_RUNNING
|
||||
case 11: // FIT_SPORT_WALKING
|
||||
return "🏃"; // Running/Walking
|
||||
case 2: // FIT_SPORT_CYCLING
|
||||
return "🚴"; // Cycling
|
||||
case 4: // FIT_SPORT_FITNESS_EQUIPMENT (Elliptical)
|
||||
return "⭕"; // Elliptical
|
||||
case 15: // FIT_SPORT_ROWING
|
||||
return "🚣"; // Rowing
|
||||
case 84: // FIT_SPORT_JUMPROPE
|
||||
return "🪢"; // Jump Rope
|
||||
default:
|
||||
return "💪"; // Generic workout
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 10
|
||||
|
||||
// Header
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 60
|
||||
color: "#f5f5f5"
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "Workout History"
|
||||
font.pixelSize: 24
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
|
||||
// Loading indicator
|
||||
BusyIndicator {
|
||||
id: loadingIndicator
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
visible: workoutModel ? (workoutModel.isLoading || workoutModel.isDatabaseProcessing) : false
|
||||
running: visible
|
||||
}
|
||||
|
||||
// Database processing message
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
visible: workoutModel ? workoutModel.isDatabaseProcessing : false
|
||||
text: "Processing workout files...\nThis may take a few moments on first startup."
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
color: "#666666"
|
||||
font.pixelSize: 16
|
||||
}
|
||||
|
||||
// Workout List
|
||||
ListView {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
model: workoutModel
|
||||
spacing: 8
|
||||
clip: true
|
||||
|
||||
delegate: SwipeDelegate {
|
||||
id: swipeDelegate
|
||||
width: parent.width
|
||||
height: 135
|
||||
|
||||
Component.onCompleted: {
|
||||
console.log("Delegate data:", JSON.stringify({
|
||||
sport: sport,
|
||||
title: title,
|
||||
date: date,
|
||||
duration: duration,
|
||||
distance: distance,
|
||||
calories: calories,
|
||||
id: id
|
||||
}))
|
||||
}
|
||||
|
||||
swipe.right: Rectangle {
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
color: "#FF4444"
|
||||
clip: true
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.rightMargin: 20
|
||||
|
||||
Text {
|
||||
text: "🗑️ Delete"
|
||||
color: "white"
|
||||
font.pixelSize: 16
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
swipe.onCompleted: {
|
||||
// Show confirmation dialog
|
||||
confirmDialog.workoutId = model.id
|
||||
confirmDialog.workoutTitle = model.title
|
||||
confirmDialog.open()
|
||||
}
|
||||
|
||||
// Card-like container
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 8
|
||||
radius: 10
|
||||
color: "white"
|
||||
border.color: "#e0e0e0"
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
spacing: 16
|
||||
|
||||
// Sport icon
|
||||
Column {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Text {
|
||||
text: getSportIcon(sport)
|
||||
font.pixelSize: 32
|
||||
}
|
||||
}
|
||||
|
||||
// Workout info
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 4
|
||||
|
||||
Text {
|
||||
text: title
|
||||
font.bold: true
|
||||
font.pixelSize: 18
|
||||
}
|
||||
|
||||
Text {
|
||||
text: date
|
||||
color: "#666666"
|
||||
}
|
||||
|
||||
// Stats row
|
||||
RowLayout {
|
||||
spacing: 16
|
||||
|
||||
Text {
|
||||
text: "⏱ " + duration
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "📏 " + distance.toFixed(2) + " km"
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: 16
|
||||
|
||||
Text {
|
||||
text: "🔥 " + Math.round(calories) + " kcal"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
console.log("Workout clicked, ID:", model.id)
|
||||
|
||||
// Get workout details from the model
|
||||
var details = workoutModel.getWorkoutDetails(model.id)
|
||||
console.log("Workout details:", JSON.stringify(details))
|
||||
|
||||
// Emit signal with file URL for chart preview
|
||||
console.log("Emitting fitfile_preview_clicked with path:", details.filePath)
|
||||
var fileUrl = Qt.resolvedUrl("file://" + details.filePath)
|
||||
console.log("Converted to URL:", fileUrl)
|
||||
workoutHistoryPage.fitfile_preview_clicked(fileUrl)
|
||||
|
||||
// Push the ChartJsTest view
|
||||
stackView.push("PreviewChart.qml")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Confirmation Dialog
|
||||
Dialog {
|
||||
id: confirmDialog
|
||||
|
||||
property int workoutId
|
||||
property string workoutTitle
|
||||
|
||||
title: "Delete Workout"
|
||||
modal: true
|
||||
standardButtons: Dialog.Ok | Dialog.Cancel
|
||||
|
||||
x: (parent.width - width) / 2
|
||||
y: (parent.height - height) / 2
|
||||
|
||||
Text {
|
||||
text: "Are you sure you want to delete '" + confirmDialog.workoutTitle + "'?"
|
||||
}
|
||||
|
||||
onAccepted: {
|
||||
workoutModel.deleteWorkout(confirmDialog.workoutId)
|
||||
swipeDelegate.swipe.close()
|
||||
}
|
||||
onRejected: {
|
||||
swipeDelegate.swipe.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0"?>
|
||||
<manifest package="org.cagnulen.qdomyoszwift" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionName="2.19.2" android:versionCode="1116" android:installLocation="auto">
|
||||
<manifest package="org.cagnulen.qdomyoszwift" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionName="2.19.2" android:versionCode="1114" android:installLocation="auto">
|
||||
<!-- The following comment will be replaced upon deployment with default permissions based on the dependencies of the application.
|
||||
Remove the comment if you do not require these default permissions. -->
|
||||
<!-- %%INSERT_PERMISSIONS -->
|
||||
|
||||
@@ -82,7 +82,7 @@ import com.android.billingclient.api.QueryProductDetailsResult;
|
||||
** Add Dependencies below to build.gradle file:
|
||||
|
||||
dependencies {
|
||||
def billing_version = "8.0.0"
|
||||
def billing_version = "4.0.0"
|
||||
implementation "com.android.billingclient:billing:$billing_version"
|
||||
}
|
||||
|
||||
@@ -152,23 +152,18 @@ public class InAppPurchase implements PurchasesUpdatedListener
|
||||
public void onPurchasesUpdated(BillingResult billingResult, List<Purchase> purchases) {
|
||||
|
||||
int responseCode = billingResult.getResponseCode();
|
||||
QLog.d(TAG, "onPurchasesUpdated called. Response code: " + responseCode + ", Debug message: " + billingResult.getDebugMessage());
|
||||
|
||||
if (purchases == null) {
|
||||
QLog.e(TAG, "Purchase failed: Data missing from result (purchases is null)");
|
||||
purchaseFailed(purchaseRequestCode, FAILUREREASON_ERROR, "Data missing from result");
|
||||
return;
|
||||
}
|
||||
|
||||
if (billingResult.getResponseCode() == RESULT_OK) {
|
||||
QLog.d(TAG, "Purchase successful, handling " + purchases.size() + " purchases");
|
||||
handlePurchase(purchases);
|
||||
} else if (responseCode == RESULT_USER_CANCELED) {
|
||||
QLog.d(TAG, "Purchase cancelled by user");
|
||||
purchaseFailed(purchaseRequestCode, FAILUREREASON_USERCANCELED, "");
|
||||
} else {
|
||||
String errorString = getErrorString(responseCode);
|
||||
QLog.e(TAG, "Purchase failed with error: " + errorString + " (code: " + responseCode + ")");
|
||||
purchaseFailed(purchaseRequestCode, FAILUREREASON_ERROR, errorString);
|
||||
}
|
||||
}
|
||||
@@ -292,7 +287,7 @@ public class InAppPurchase implements PurchasesUpdatedListener
|
||||
List<ProductDetails> productDetailsList = productDetailsResult.getProductDetailsList();
|
||||
|
||||
if (billingResult.getResponseCode() != RESULT_OK) {
|
||||
QLog.e(TAG, "Unable to launch Google Play purchase screen. Response code: " + billingResult.getResponseCode() + ", Debug message: " + billingResult.getDebugMessage());
|
||||
QLog.e(TAG, "Unable to launch Google Play purchase screen");
|
||||
String errorString = getErrorString(requestCode);
|
||||
purchaseFailed(requestCode, FAILUREREASON_ERROR, errorString);
|
||||
return;
|
||||
@@ -303,19 +298,9 @@ public class InAppPurchase implements PurchasesUpdatedListener
|
||||
}
|
||||
|
||||
ProductDetails productDetails = productDetailsList.get(0);
|
||||
BillingFlowParams.ProductDetailsParams.Builder productDetailsParamsBuilder = BillingFlowParams.ProductDetailsParams.newBuilder()
|
||||
.setProductDetails(productDetails);
|
||||
|
||||
// For subscriptions, we need to set the offer token
|
||||
if (productDetails.getSubscriptionOfferDetails() != null && !productDetails.getSubscriptionOfferDetails().isEmpty()) {
|
||||
String offerToken = productDetails.getSubscriptionOfferDetails().get(0).getOfferToken();
|
||||
QLog.d(TAG, "Setting offer token for subscription: " + offerToken);
|
||||
productDetailsParamsBuilder.setOfferToken(offerToken);
|
||||
} else {
|
||||
QLog.w(TAG, "No subscription offer details found for product: " + identifier);
|
||||
}
|
||||
|
||||
BillingFlowParams.ProductDetailsParams productDetailsParams = productDetailsParamsBuilder.build();
|
||||
BillingFlowParams.ProductDetailsParams productDetailsParams = BillingFlowParams.ProductDetailsParams.newBuilder()
|
||||
.setProductDetails(productDetails)
|
||||
.build();
|
||||
|
||||
BillingFlowParams purchaseParams = BillingFlowParams.newBuilder()
|
||||
.setProductDetailsParamsList(java.util.Arrays.asList(productDetailsParams))
|
||||
|
||||
349
src/fitdatabaseprocessor.cpp
Normal file
349
src/fitdatabaseprocessor.cpp
Normal file
@@ -0,0 +1,349 @@
|
||||
#include "fitdatabaseprocessor.h"
|
||||
#include <QSqlQuery>
|
||||
#include <QSqlError>
|
||||
#include <QCryptographicHash>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QDebug>
|
||||
#include <QDirIterator>
|
||||
#include <QSqlDatabase>
|
||||
#include <QDateTime>
|
||||
|
||||
const QString FitDatabaseProcessor::DB_CONNECTION_NAME = "FitProcessor";
|
||||
|
||||
FitDatabaseProcessor::FitDatabaseProcessor(const QString& dbPath, QObject* parent)
|
||||
: QObject(parent)
|
||||
, dbPath(dbPath)
|
||||
, stopRequested(0)
|
||||
{
|
||||
moveToThread(&workerThread);
|
||||
connect(&workerThread, &QThread::finished, this, &QObject::deleteLater);
|
||||
}
|
||||
|
||||
FitDatabaseProcessor::~FitDatabaseProcessor() {
|
||||
stopProcessing();
|
||||
workerThread.wait();
|
||||
|
||||
QSqlDatabase::removeDatabase(DB_CONNECTION_NAME);
|
||||
}
|
||||
|
||||
bool FitDatabaseProcessor::initializeDatabase() {
|
||||
QMutexLocker locker(&mutex);
|
||||
|
||||
if (QSqlDatabase::contains(DB_CONNECTION_NAME)) {
|
||||
db = QSqlDatabase::database(DB_CONNECTION_NAME);
|
||||
} else {
|
||||
db = QSqlDatabase::addDatabase("QSQLITE", DB_CONNECTION_NAME);
|
||||
db.setDatabaseName(dbPath);
|
||||
}
|
||||
|
||||
if (!db.open()) {
|
||||
emit error("Failed to open database: " + db.lastError().text());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Start transaction for table creation
|
||||
db.transaction();
|
||||
|
||||
QSqlQuery query(db);
|
||||
|
||||
// Create workouts table - Only storing summary data
|
||||
if (!query.exec("CREATE TABLE IF NOT EXISTS workouts ("
|
||||
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
|
||||
"file_hash TEXT UNIQUE,"
|
||||
"file_path TEXT,"
|
||||
"workout_name TEXT,"
|
||||
"sport_type INTEGER,"
|
||||
"start_time DATETIME,"
|
||||
"end_time DATETIME,"
|
||||
"total_time INTEGER," // in seconds
|
||||
"total_distance REAL," // in km
|
||||
"total_calories INTEGER,"
|
||||
"avg_heart_rate INTEGER,"
|
||||
"max_heart_rate INTEGER,"
|
||||
"avg_cadence INTEGER,"
|
||||
"max_cadence INTEGER,"
|
||||
"avg_speed REAL,"
|
||||
"max_speed REAL,"
|
||||
"avg_power INTEGER,"
|
||||
"max_power INTEGER,"
|
||||
"total_ascent REAL,"
|
||||
"total_descent REAL,"
|
||||
"avg_stride_length REAL,"
|
||||
"total_strides INTEGER,"
|
||||
"processed_at DATETIME DEFAULT CURRENT_TIMESTAMP"
|
||||
")")) {
|
||||
db.rollback();
|
||||
emit error("Failed to create workouts table: " + query.lastError().text());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create index for better performance
|
||||
query.exec("CREATE INDEX IF NOT EXISTS idx_workout_start_time ON workouts(start_time)");
|
||||
|
||||
// Add workout_name column if it doesn't exist (for existing databases)
|
||||
query.exec("ALTER TABLE workouts ADD COLUMN workout_name TEXT");
|
||||
|
||||
return db.commit();
|
||||
}
|
||||
|
||||
void FitDatabaseProcessor::processDirectory(const QString& dirPath) {
|
||||
currentDirPath = dirPath;
|
||||
stopRequested.storeRelease(0);
|
||||
|
||||
if (!workerThread.isRunning()) {
|
||||
connect(&workerThread, &QThread::started, this, &FitDatabaseProcessor::doWork);
|
||||
workerThread.start();
|
||||
}
|
||||
}
|
||||
|
||||
void FitDatabaseProcessor::stopProcessing() {
|
||||
stopRequested.storeRelease(1);
|
||||
workerThread.quit();
|
||||
}
|
||||
|
||||
QString FitDatabaseProcessor::getFileHash(const QString& filePath) {
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
QCryptographicHash hash(QCryptographicHash::Sha256);
|
||||
if (!hash.addData(&file)) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
return hash.result().toHex();
|
||||
}
|
||||
|
||||
bool FitDatabaseProcessor::isFileProcessed(const QString& filePath) {
|
||||
QString fileHash = getFileHash(filePath);
|
||||
if (fileHash.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QSqlQuery query(db);
|
||||
query.prepare("SELECT COUNT(*) FROM workouts WHERE file_hash = ?");
|
||||
query.addBindValue(fileHash);
|
||||
|
||||
if (!query.exec() || !query.next()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return query.value(0).toInt() > 0;
|
||||
}
|
||||
|
||||
bool FitDatabaseProcessor::saveWorkout(const QString& filePath,
|
||||
const QList<SessionLine>& session,
|
||||
FIT_SPORT sport,
|
||||
const QString& workoutName,
|
||||
int elapsedSeconds,
|
||||
qint64& workoutId) {
|
||||
if (session.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QString fileHash = getFileHash(filePath);
|
||||
if (fileHash.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Calculate aggregate values
|
||||
double totalDistance = session.last().distance - session.first().distance;
|
||||
int maxHr = 0, totalHr = 0, hrCount = 0;
|
||||
int maxCadence = 0, totalCadence = 0, cadenceCount = 0;
|
||||
double maxSpeed = 0, totalSpeed = 0, speedCount = 0;
|
||||
int maxPower = 0, totalPower = 0, powerCount = 0;
|
||||
double totalAscent = 0, totalDescent = 0;
|
||||
double lastElevation = session.first().coordinate.altitude();
|
||||
|
||||
for (const SessionLine& point : session) {
|
||||
// Heart rate
|
||||
if (point.heart > 0) {
|
||||
maxHr = qMax(maxHr, static_cast<int>(point.heart));
|
||||
totalHr += point.heart;
|
||||
hrCount++;
|
||||
}
|
||||
|
||||
// Cadence
|
||||
if (point.cadence > 0) {
|
||||
maxCadence = qMax(maxCadence, static_cast<int>(point.cadence));
|
||||
totalCadence += point.cadence;
|
||||
cadenceCount++;
|
||||
}
|
||||
|
||||
// Speed
|
||||
if (point.speed > 0) {
|
||||
maxSpeed = qMax(maxSpeed, point.speed);
|
||||
totalSpeed += point.speed;
|
||||
speedCount++;
|
||||
}
|
||||
|
||||
// Power
|
||||
if (point.watt > 0) {
|
||||
maxPower = qMax(maxPower, static_cast<int>(point.watt));
|
||||
totalPower += point.watt;
|
||||
powerCount++;
|
||||
}
|
||||
|
||||
// Elevation changes
|
||||
if (point.coordinate.isValid()) {
|
||||
double currentElevation = point.coordinate.altitude();
|
||||
if (lastElevation > 0) {
|
||||
double diff = currentElevation - lastElevation;
|
||||
if (diff > 0) totalAscent += diff;
|
||||
else totalDescent += qAbs(diff);
|
||||
}
|
||||
lastElevation = currentElevation;
|
||||
}
|
||||
}
|
||||
|
||||
QSqlQuery query(db);
|
||||
query.prepare("INSERT INTO workouts ("
|
||||
"file_hash, file_path, workout_name, sport_type, start_time, end_time, "
|
||||
"total_time, total_distance, total_calories, "
|
||||
"avg_heart_rate, max_heart_rate, avg_cadence, max_cadence, "
|
||||
"avg_speed, max_speed, avg_power, max_power, "
|
||||
"total_ascent, total_descent, avg_stride_length, total_strides"
|
||||
") VALUES ("
|
||||
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?"
|
||||
")");
|
||||
|
||||
query.addBindValue(fileHash);
|
||||
query.addBindValue(filePath);
|
||||
query.addBindValue(workoutName);
|
||||
query.addBindValue(static_cast<int>(sport));
|
||||
query.addBindValue(session.first().time);
|
||||
query.addBindValue(session.last().time);
|
||||
query.addBindValue(elapsedSeconds);
|
||||
query.addBindValue(totalDistance);
|
||||
query.addBindValue(session.last().calories);
|
||||
query.addBindValue(hrCount > 0 ? totalHr / hrCount : 0);
|
||||
query.addBindValue(maxHr);
|
||||
query.addBindValue(cadenceCount > 0 ? totalCadence / cadenceCount : 0);
|
||||
query.addBindValue(maxCadence);
|
||||
query.addBindValue(speedCount > 0 ? totalSpeed / speedCount : 0);
|
||||
query.addBindValue(maxSpeed);
|
||||
query.addBindValue(powerCount > 0 ? totalPower / powerCount : 0);
|
||||
query.addBindValue(maxPower);
|
||||
query.addBindValue(totalAscent);
|
||||
query.addBindValue(totalDescent);
|
||||
query.addBindValue(session.last().instantaneousStrideLengthCM);
|
||||
query.addBindValue(session.last().stepCount);
|
||||
|
||||
if (!query.exec()) {
|
||||
emit error("Failed to save workout: " + query.lastError().text());
|
||||
return false;
|
||||
}
|
||||
|
||||
workoutId = query.lastInsertId().toLongLong();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FitDatabaseProcessor::processFitFile(const QString& filePath) {
|
||||
if (isFileProcessed(filePath)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
QList<SessionLine> session;
|
||||
FIT_SPORT sport = FIT_SPORT_INVALID;
|
||||
QString workoutName = ""; // Initialize to empty string
|
||||
|
||||
try {
|
||||
qfit::open(filePath, &session, &sport, &workoutName);
|
||||
|
||||
if (session.isEmpty()) {
|
||||
emit error("No data found in file: " + filePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Debug logging
|
||||
qDebug() << "Processing FIT file:" << filePath;
|
||||
qDebug() << "Sport type detected:" << static_cast<int>(sport);
|
||||
qDebug() << "Session duration (elapsedTime):" << session.last().elapsedTime;
|
||||
qDebug() << "Workout name from FIT:" << workoutName;
|
||||
|
||||
// Validate elapsed time (should be reasonable, between 1 minute and 24 hours)
|
||||
int elapsedSeconds = session.last().elapsedTime;
|
||||
if (elapsedSeconds < 60 || elapsedSeconds > 86400) {
|
||||
qDebug() << "Warning: Unusual elapsed time detected:" << elapsedSeconds << "seconds. Using session duration calculation.";
|
||||
// Calculate duration from first to last record
|
||||
elapsedSeconds = session.first().time.secsTo(session.last().time);
|
||||
if (elapsedSeconds < 60 || elapsedSeconds > 86400) {
|
||||
qDebug() << "Warning: Still unusual duration. Setting to 1 minute minimum.";
|
||||
elapsedSeconds = qMax(60, qMin(86400, elapsedSeconds));
|
||||
}
|
||||
}
|
||||
|
||||
// Generate fallback workout name based on sport and duration if not found in FIT file
|
||||
if (workoutName.isEmpty()) {
|
||||
QString sportName;
|
||||
switch (sport) {
|
||||
case FIT_SPORT_RUNNING:
|
||||
case FIT_SPORT_WALKING:
|
||||
sportName = "Run";
|
||||
break;
|
||||
case FIT_SPORT_CYCLING:
|
||||
sportName = "Ride";
|
||||
break;
|
||||
case FIT_SPORT_FITNESS_EQUIPMENT:
|
||||
sportName = "Elliptical";
|
||||
break;
|
||||
case FIT_SPORT_ROWING:
|
||||
sportName = "Row";
|
||||
break;
|
||||
default:
|
||||
sportName = "Workout";
|
||||
qDebug() << "Unknown sport type, using default. Sport value:" << static_cast<int>(sport);
|
||||
break;
|
||||
}
|
||||
|
||||
int totalMinutes = elapsedSeconds / 60;
|
||||
workoutName = QString("%1 minutes %2").arg(totalMinutes).arg(sportName);
|
||||
qDebug() << "Generated fallback workout name:" << workoutName;
|
||||
}
|
||||
|
||||
db.transaction();
|
||||
|
||||
qint64 workoutId;
|
||||
if (!saveWorkout(filePath, session, sport, workoutName, elapsedSeconds, workoutId)) {
|
||||
db.rollback();
|
||||
return false;
|
||||
}
|
||||
|
||||
return db.commit();
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
emit error(QString("Error processing file %1: %2").arg(filePath, e.what()));
|
||||
db.rollback();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void FitDatabaseProcessor::doWork() {
|
||||
if (!initializeDatabase()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QDir dir(currentDirPath);
|
||||
QStringList fitFiles = dir.entryList(QStringList() << "*.fit" << "*.FIT", QDir::Files);
|
||||
int totalFiles = fitFiles.size();
|
||||
int processedFiles = 0;
|
||||
|
||||
for (const QString& fileName : fitFiles) {
|
||||
if (stopRequested.loadAcquire()) {
|
||||
break;
|
||||
}
|
||||
|
||||
QString filePath = dir.absoluteFilePath(fileName);
|
||||
|
||||
if (processFitFile(filePath)) {
|
||||
emit fileProcessed(fileName);
|
||||
}
|
||||
|
||||
processedFiles++;
|
||||
emit progress(processedFiles, totalFiles);
|
||||
}
|
||||
|
||||
emit processingStopped();
|
||||
}
|
||||
56
src/fitdatabaseprocessor.h
Normal file
56
src/fitdatabaseprocessor.h
Normal file
@@ -0,0 +1,56 @@
|
||||
#ifndef FITDATABASEPROCESSOR_H
|
||||
#define FITDATABASEPROCESSOR_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QThread>
|
||||
#include <QSqlDatabase>
|
||||
#include <QString>
|
||||
#include <QDir>
|
||||
#include <QMutex>
|
||||
#include <QAtomicInt>
|
||||
#include "qfit.h"
|
||||
|
||||
class FitDatabaseProcessor : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit FitDatabaseProcessor(const QString& dbPath, QObject* parent = nullptr);
|
||||
~FitDatabaseProcessor();
|
||||
|
||||
void processDirectory(const QString& dirPath);
|
||||
void stopProcessing();
|
||||
|
||||
static const QString DB_CONNECTION_NAME;
|
||||
|
||||
signals:
|
||||
void processingStopped();
|
||||
void fileProcessed(const QString& filename);
|
||||
void progress(int processedFiles, int totalFiles);
|
||||
void error(const QString& errorMessage);
|
||||
|
||||
private slots:
|
||||
void doWork();
|
||||
|
||||
private:
|
||||
bool initializeDatabase();
|
||||
bool processFitFile(const QString& filePath);
|
||||
bool isFileProcessed(const QString& filePath);
|
||||
QString getFileHash(const QString& filePath);
|
||||
|
||||
// Method for handling workout summary data
|
||||
bool saveWorkout(const QString& filePath,
|
||||
const QList<SessionLine>& session,
|
||||
FIT_SPORT sport,
|
||||
const QString& workoutName,
|
||||
int elapsedSeconds,
|
||||
qint64& workoutId);
|
||||
|
||||
QThread workerThread;
|
||||
QString dbPath;
|
||||
QString currentDirPath;
|
||||
QAtomicInt stopRequested;
|
||||
QMutex mutex;
|
||||
QSqlDatabase db;
|
||||
};
|
||||
|
||||
#endif // FITDATABASEPROCESSOR_H
|
||||
@@ -8,10 +8,12 @@
|
||||
#include <jni.h>
|
||||
#include <QAndroidJniObject>
|
||||
#endif
|
||||
#include "fitdatabaseprocessor.h"
|
||||
#include "material.h"
|
||||
#include "qfit.h"
|
||||
#include "simplecrypt.h"
|
||||
#include "templateinfosenderbuilder.h"
|
||||
#include "workoutmodel.h"
|
||||
#include "zwiftworkout.h"
|
||||
|
||||
#include <QAbstractOAuth2>
|
||||
@@ -574,6 +576,7 @@ homeform::homeform(QQmlApplicationEngine *engine, bluetooth *bl) {
|
||||
QObject::connect(stack, SIGNAL(profile_open_clicked(QUrl)), this, SLOT(profile_open_clicked(QUrl)));
|
||||
QObject::connect(stack, SIGNAL(trainprogram_preview(QUrl)), this, SLOT(trainprogram_preview(QUrl)));
|
||||
QObject::connect(stack, SIGNAL(gpxpreview_open_clicked(QUrl)), this, SLOT(gpxpreview_open_clicked(QUrl)));
|
||||
QObject::connect(stack, SIGNAL(fitfile_preview_clicked(QUrl)), this, SLOT(fitfile_preview_clicked(QUrl)));
|
||||
QObject::connect(stack, SIGNAL(trainprogram_zwo_loaded(QString)), this, SLOT(trainprogram_zwo_loaded(QString)));
|
||||
QObject::connect(stack, SIGNAL(gpx_open_clicked(QUrl)), this, SLOT(gpx_open_clicked(QUrl)));
|
||||
QObject::connect(stack, SIGNAL(gpx_save_clicked()), this, SLOT(gpx_save_clicked()));
|
||||
@@ -642,7 +645,20 @@ homeform::homeform(QQmlApplicationEngine *engine, bluetooth *bl) {
|
||||
if (!QFile(getWritableAppDir() + "gpx/" + itGpx.fileName()).exists()) {
|
||||
QFile::copy(":/gpx/" + itGpx.fileName(), getWritableAppDir() + "gpx/" + itGpx.fileName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QDirIterator itFit(getWritableAppDir(), QStringList() << "*.fit", QDir::Files);
|
||||
qDebug() << itFit.path();
|
||||
QDir().mkdir(getWritableAppDir() + "fit");
|
||||
while (itFit.hasNext()) {
|
||||
qDebug() << itFit.filePath() << itFit.fileName() << itFit.filePath().replace(itFit.path(), "");
|
||||
if (!QFile(getWritableAppDir() + "fit/" + itFit.next().replace(itFit.path(), "")).exists() && !itFit.fileName().contains("backup")) {
|
||||
if(QFile::copy(itFit.filePath(), getWritableAppDir() + "fit/" + itFit.filePath().replace(itFit.path(), "")))
|
||||
QFile::remove(itFit.filePath());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
|
||||
QString bluetoothName = getBluetoothName();
|
||||
@@ -669,6 +685,30 @@ homeform::homeform(QQmlApplicationEngine *engine, bluetooth *bl) {
|
||||
}
|
||||
#endif
|
||||
|
||||
auto processor = new FitDatabaseProcessor(getWritableAppDir() + "ddb.sqlite");
|
||||
connect(processor, &FitDatabaseProcessor::fileProcessed,
|
||||
this, [](const QString& filename) {
|
||||
qDebug() << "FitDatabaseProcessor Processing:" << filename;
|
||||
});
|
||||
connect(processor, &FitDatabaseProcessor::progress,
|
||||
this, [](int processed, int total) {
|
||||
qDebug() << "FitDatabaseProcessor Progress:" << processed << "/" << total;
|
||||
});
|
||||
connect(processor, &FitDatabaseProcessor::error,
|
||||
this, [](const QString& error) {
|
||||
qDebug() << "FitDatabaseProcessor Error:" << error;
|
||||
});
|
||||
WorkoutModel* workoutModel = new WorkoutModel(getWritableAppDir() + "ddb.sqlite");
|
||||
engine->rootContext()->setContextProperty("workoutModel", workoutModel);
|
||||
|
||||
connect(processor, &FitDatabaseProcessor::processingStopped,
|
||||
this, [workoutModel]() {
|
||||
qDebug() << "FitDatabaseProcessor Processing stopped - refreshing workout model";
|
||||
workoutModel->setDatabaseProcessing(false);
|
||||
workoutModel->refresh();
|
||||
});
|
||||
processor->processDirectory(getWritableAppDir() + "fit");
|
||||
|
||||
m_speech.setLocale(QLocale::English);
|
||||
|
||||
#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
|
||||
@@ -1108,7 +1148,7 @@ void homeform::backup() {
|
||||
QFile::remove(filename);
|
||||
qfit::save(filename, Session, dev->deviceType(),
|
||||
qobject_cast<m3ibike *>(dev) ? QFIT_PROCESS_DISTANCENOISE : QFIT_PROCESS_NONE,
|
||||
stravaPelotonWorkoutType, dev->bluetoothDevice.name());
|
||||
stravaPelotonWorkoutType, workoutName(), dev->bluetoothDevice.name());
|
||||
|
||||
index++;
|
||||
if (index > 1) {
|
||||
@@ -6955,7 +6995,8 @@ void homeform::update() {
|
||||
(bluetoothManager->device()->elapsedTime().hour() * 3600),
|
||||
|
||||
lapTrigger, totalStrokes, avgStrokesRate, maxStrokesRate, avgStrokesLength,
|
||||
bluetoothManager->device()->currentCordinate(), strideLength, groundContact, verticalOscillation, stepCount,
|
||||
bluetoothManager->device()->currentCordinate(), strideLength, groundContact, verticalOscillation, stepCount,
|
||||
target_cadence->value().toDouble(), target_power->value().toDouble(), target_resistance->value().toDouble(),
|
||||
bluetoothManager->device()->CoreBodyTemperature.value(), bluetoothManager->device()->SkinTemperature.value(), bluetoothManager->device()->HeatStrainIndex.value());
|
||||
|
||||
Session.append(s);
|
||||
@@ -7188,7 +7229,7 @@ void homeform::fit_save_clicked() {
|
||||
QString path = getWritableAppDir();
|
||||
bluetoothdevice *dev = bluetoothManager->device();
|
||||
if (dev) {
|
||||
QString filename = path +
|
||||
QString filename = path + "fit/" +
|
||||
QDateTime::currentDateTime().toString().replace(QStringLiteral(":"), QStringLiteral("_")) +
|
||||
QStringLiteral(".fit");
|
||||
|
||||
@@ -7304,6 +7345,33 @@ void homeform::gpx_open_clicked(const QUrl &fileName) {
|
||||
}
|
||||
}
|
||||
|
||||
void homeform::fitfile_preview_clicked(const QUrl &fileName) {
|
||||
qDebug() << QStringLiteral("fitfile_preview_clicked called with URL:") << fileName;
|
||||
qDebug() << QStringLiteral("URL toString:") << fileName.toString();
|
||||
qDebug() << QStringLiteral("URL toLocalFile:") << fileName.toLocalFile();
|
||||
|
||||
// Use the full file path directly instead of reconstructing it
|
||||
QString filePath = fileName.toLocalFile();
|
||||
QFile file(filePath);
|
||||
qDebug() << "Opening FIT file:" << filePath;
|
||||
|
||||
if (file.exists()) {
|
||||
QList<SessionLine> a;
|
||||
FIT_SPORT sport;
|
||||
QString workoutName;
|
||||
qfit::open(filePath, &a, &sport, &workoutName);
|
||||
qDebug() << "FIT file read:" << a.size() << "records, sport:" << sport << "workoutName:" << workoutName;
|
||||
if (!a.isEmpty()) {
|
||||
this->innerTemplateManager->previewSessionOnChart(&a, sport);
|
||||
emit previewFitFile(filePath, QTime(0,0,0,0).addSecs(a.last().elapsedTime).toString());
|
||||
} else {
|
||||
qDebug() << "No data read from FIT file";
|
||||
}
|
||||
} else {
|
||||
qDebug() << "FIT file does not exist:" << filePath;
|
||||
}
|
||||
}
|
||||
|
||||
void homeform::gpxpreview_open_clicked(const QUrl &fileName) {
|
||||
qDebug() << QStringLiteral("gpxpreview_open_clicked") << fileName;
|
||||
|
||||
|
||||
@@ -860,6 +860,7 @@ class homeform : public QObject {
|
||||
void profile_open_clicked(const QUrl &fileName);
|
||||
void trainprogram_preview(const QUrl &fileName);
|
||||
void gpxpreview_open_clicked(const QUrl &fileName);
|
||||
void fitfile_preview_clicked(const QUrl &fileName);
|
||||
void trainprogram_zwo_loaded(const QString &comp);
|
||||
void gpx_open_clicked(const QUrl &fileName);
|
||||
void gpx_save_clicked();
|
||||
@@ -950,6 +951,9 @@ class homeform : public QObject {
|
||||
void previewWorkoutPointsChanged(int value);
|
||||
void previewWorkoutDescriptionChanged(QString value);
|
||||
void previewWorkoutTagsChanged(QString value);
|
||||
|
||||
void previewFitFile(const QString &filename, const QString &result);
|
||||
|
||||
void stravaAuthUrlChanged(QString value);
|
||||
void stravaWebVisibleChanged(bool value);
|
||||
void pelotonAuthUrlChanged(QString value);
|
||||
|
||||
39
src/inner_templates/previewchart/.eslintrc.js
Normal file
39
src/inner_templates/previewchart/.eslintrc.js
Normal file
@@ -0,0 +1,39 @@
|
||||
module.exports = {
|
||||
'env': {
|
||||
'browser': true,
|
||||
'es2021': true,
|
||||
'commonjs': true,
|
||||
'es6': true,
|
||||
'jquery': true
|
||||
},
|
||||
'extends': 'eslint:recommended',
|
||||
'parserOptions': {
|
||||
'ecmaVersion': 12,
|
||||
//'sourceType': 'module'
|
||||
},
|
||||
'globals': {
|
||||
'host_url': true,
|
||||
'MainWSQueueElement': true,
|
||||
'Chart': true,
|
||||
'get_template_name': true
|
||||
},
|
||||
'rules': {
|
||||
'indent': [
|
||||
'error',
|
||||
4
|
||||
],
|
||||
'linebreak-style': [
|
||||
'error',
|
||||
'windows'
|
||||
],
|
||||
'quotes': [
|
||||
'error',
|
||||
'single'
|
||||
],
|
||||
'semi': [
|
||||
'error',
|
||||
'always'
|
||||
],
|
||||
'no-unused-vars': ['off']
|
||||
}
|
||||
};
|
||||
3
src/inner_templates/previewchart/.jshintrc
Normal file
3
src/inner_templates/previewchart/.jshintrc
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"esversion": 6
|
||||
}
|
||||
BIN
src/inner_templates/previewchart/ajax-loader.gif
Normal file
BIN
src/inner_templates/previewchart/ajax-loader.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
161
src/inner_templates/previewchart/chart.htm
Normal file
161
src/inner_templates/previewchart/chart.htm
Normal file
@@ -0,0 +1,161 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Line Chart</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<script src="resize-observer.min.js"></script>
|
||||
<script src="jquery-3.6.0.min.js"></script>
|
||||
<script src="chartjs.3.4.1.min.js"></script>
|
||||
<script src="moment.js"></script>
|
||||
<script src="chartjs-adapter-moment.js"></script>
|
||||
<script src="chartjs-plugin-annotation.min.js"></script>
|
||||
<script src="globals.js"></script>
|
||||
<script src="main_ws_manager.js"></script>
|
||||
<script src="dochart.js"></script>
|
||||
<style>
|
||||
canvas{
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body style="background-color:#1d2330">
|
||||
<div class="workoutName" align="center" style='color: white; font-size: 18px; font-weight:700; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"
|
||||
'></div>
|
||||
<div class="instructorName" align="center" style='color: white; font-size: 14px; font-weight:700; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"
|
||||
'></div>
|
||||
<div class="workoutStartDate" align="center" style='color: yellow; font-size: 14px; font-weight:700; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"
|
||||
'></div>
|
||||
</br>
|
||||
<hr width="50%" style='color: white;' />
|
||||
<table width="100%" align="justify" background="qzlogo.png" style="background-size: 15%; background-repeat: no-repeat; background-position: center;">
|
||||
<tr align="center" style='color: grey; font-size: 12px; font-weight:500; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"
|
||||
'>
|
||||
<td>Avg Output</td>
|
||||
<td>Max Output</td>
|
||||
<td>Total Output</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="summary_watts_avg" align="center" style='color: white; font-size: 16px; font-weight:500; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"
|
||||
'>0 W</td>
|
||||
<td class="summary_watts_max" align="center" style='color: white; font-size: 16px; font-weight:500; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"
|
||||
'>0 W</td>
|
||||
<td class="summary_jouls" align="center" style='color: white; font-size: 16px; font-weight:500; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"
|
||||
'>0 kJ</td>
|
||||
</tr>
|
||||
<tr align="center" style='color: grey; font-size: 12px; font-weight:500; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"
|
||||
'>
|
||||
<td>Calories</td>
|
||||
<td>Distance</td>
|
||||
<td>AVG Cadence</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="summary_calories" align="center" style='color: white; font-size: 16px; font-weight:500; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"
|
||||
'>0 kcal</td>
|
||||
<td class="summary_distance" align="center" style='color: white; font-size: 16px; font-weight:500; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"
|
||||
'>0 km</td>
|
||||
<td class="summary_cadence_avg" align="center" style='color: white; font-size: 16px; font-weight:500; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"
|
||||
'>0 rpm</td>
|
||||
</tr>
|
||||
<tr align="center" style='color: grey; font-size: 12px; font-weight:500; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"
|
||||
'>
|
||||
<td>Max Cadence</td>
|
||||
<td>AVG Resistance</td>
|
||||
<td>AVG Heart Rate</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="summary_cadence_max" align="center" style='color: white; font-size: 16px; font-weight:500; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"
|
||||
'>0 rpm</td>
|
||||
<td class="summary_resistance_avg" align="center" style='color: white; font-size: 16px; font-weight:500; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"
|
||||
'>0 lvl</td>
|
||||
<td class="summary_heart_avg" align="center" style='color: white; font-size: 16px; font-weight:500; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"
|
||||
'>0 bpm</td>
|
||||
</tr>
|
||||
<tr align="center" style='color: grey; font-size: 12px; font-weight:500; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"
|
||||
'>
|
||||
<td>Max Heart Rate</td>
|
||||
<td>AVG Speed</td>
|
||||
<td>Max Speed</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="summary_heart_max" align="center" style='color: white; font-size: 16px; font-weight:500; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"
|
||||
'>0 bpm</td>
|
||||
<td class="summary_speed_avg" align="center" style='color: white; font-size: 16px; font-weight:500; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"
|
||||
'>0 km/h</td>
|
||||
<td class="summary_speed_max" align="center" style='color: white; font-size: 16px; font-weight:500; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"
|
||||
'>0 km/h</td>
|
||||
</tr>
|
||||
</table>
|
||||
</br>
|
||||
<div style="background-color:white; border: 0px solid #aaa; border-radius: 10px; overflow: hidden;">
|
||||
<canvas id="canvas"></canvas>
|
||||
</div>
|
||||
<div style="width: 100%; overflow: hidden;">
|
||||
<div class="watts_avg" align="left" style='display: inline-block;flaot: left; color: rgb(156,163,175); font-size: 14px; font-weight:700; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"
|
||||
'></div>
|
||||
<div class="watts_max" align="right" style='display: inline-block;float: right; overflow: hidden;color: rgb(156,163,175); font-size: 14px; font-weight:700; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"
|
||||
'></div>
|
||||
</div>
|
||||
</br></br>
|
||||
<div style="background-color:white; border: 0px solid #aaa; border-radius: 10px; overflow: hidden;">
|
||||
<canvas id="canvasHeart"></canvas>
|
||||
</div>
|
||||
<div style="width: 100%; overflow: hidden;">
|
||||
<div class="heart_avg" align="left" style='display: inline-block;flaot: left; color: rgb(156,163,175); font-size: 14px; font-weight:700; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"
|
||||
'></div>
|
||||
<div class="heart_max" align="right" style='display: inline-block;float: right; overflow: hidden;color: rgb(156,163,175); font-size: 14px; font-weight:700; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"
|
||||
'></div>
|
||||
</div>
|
||||
</br></br>
|
||||
<div style="background-color:white; border: 0px solid #aaa; border-radius: 10px; overflow: hidden;">
|
||||
<canvas id="canvasResistance"></canvas>
|
||||
</div>
|
||||
</br></br>
|
||||
<div style="background-color:white; border: 0px solid #aaa; border-radius: 10px; overflow: hidden;">
|
||||
<canvas id="canvasPelotonResistance"></canvas>
|
||||
</div>
|
||||
</br></br>
|
||||
<div style="background-color:white; border: 0px solid #aaa; border-radius: 10px; overflow: hidden;">
|
||||
<canvas id="canvasCadence"></canvas>
|
||||
</div>
|
||||
</br></br>
|
||||
<div style="background-color:white; border: 0px solid #aaa; border-radius: 10px; overflow: hidden;">
|
||||
<canvas id="canvasPowerDistribution"></canvas>
|
||||
</div>
|
||||
</br></br>
|
||||
<div style="background-color:white; border: 0px solid #aaa; border-radius: 10px; overflow: hidden;">
|
||||
<canvas id="canvasSpeedInclination"></canvas>
|
||||
</div>
|
||||
|
||||
<style>#loading {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
position: fixed;
|
||||
display: block;
|
||||
opacity: 0.7;
|
||||
background-color: #fff;
|
||||
z-index: 99;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#loading-image {
|
||||
position: fixed;
|
||||
top: 20%;
|
||||
left: 50%;
|
||||
/* bring your own prefixes */
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 100;
|
||||
}
|
||||
</style>
|
||||
<div id="loading">
|
||||
<img src="ajax-loader.gif" id="loading-image" title="working..." />
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,8 @@
|
||||
/*!
|
||||
* chartjs-adapter-moment v1.0.0
|
||||
* https://www.chartjs.org
|
||||
* (c) 2021 chartjs-adapter-moment Contributors
|
||||
* Released under the MIT license
|
||||
*/
|
||||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(require("moment"),require("chart.js")):"function"==typeof define&&define.amd?define(["moment","chart.js"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).moment,e.Chart)}(this,(function(e,t){"use strict";function n(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var f=n(e);const a={datetime:"MMM D, YYYY, h:mm:ss a",millisecond:"h:mm:ss.SSS a",second:"h:mm:ss a",minute:"h:mm a",hour:"hA",day:"MMM D",week:"ll",month:"MMM YYYY",quarter:"[Q]Q - YYYY",year:"YYYY"};t._adapters._date.override("function"==typeof f.default?{_id:"moment",formats:function(){return a},parse:function(e,t){return"string"==typeof e&&"string"==typeof t?e=f.default(e,t):e instanceof f.default||(e=f.default(e)),e.isValid()?e.valueOf():null},format:function(e,t){return f.default(e).format(t)},add:function(e,t,n){return f.default(e).add(t,n).valueOf()},diff:function(e,t,n){return f.default(e).diff(f.default(t),n)},startOf:function(e,t,n){return e=f.default(e),"isoWeek"===t?(n=Math.trunc(Math.min(Math.max(0,n),6)),e.isoWeekday(n).startOf("day").valueOf()):e.startOf(t).valueOf()},endOf:function(e,t){return f.default(e).endOf(t).valueOf()}}:{})}));
|
||||
//# sourceMappingURL=chartjs-adapter-moment.min.js.map
|
||||
7
src/inner_templates/previewchart/chartjs-plugin-annotation.min.js
vendored
Normal file
7
src/inner_templates/previewchart/chartjs-plugin-annotation.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
13
src/inner_templates/previewchart/chartjs.3.4.1.min.js
vendored
Normal file
13
src/inner_templates/previewchart/chartjs.3.4.1.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1237
src/inner_templates/previewchart/dochart.js
Normal file
1237
src/inner_templates/previewchart/dochart.js
Normal file
File diff suppressed because it is too large
Load Diff
9
src/inner_templates/previewchart/globals.js
Normal file
9
src/inner_templates/previewchart/globals.js
Normal file
@@ -0,0 +1,9 @@
|
||||
const host_url = (!location.host || location.host.length == 0)?'192.168.25.24:7666':location.host;
|
||||
|
||||
function get_template_name() {
|
||||
let splits = location.pathname.split('/');
|
||||
if (splits.length>=2)
|
||||
return splits[splits.length - 2];
|
||||
else
|
||||
return '';
|
||||
}
|
||||
2
src/inner_templates/previewchart/jquery-3.6.0.min.js
vendored
Normal file
2
src/inner_templates/previewchart/jquery-3.6.0.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
132
src/inner_templates/previewchart/main_ws_manager.js
Normal file
132
src/inner_templates/previewchart/main_ws_manager.js
Normal file
@@ -0,0 +1,132 @@
|
||||
let main_ws = null;
|
||||
let main_ws_queue = [];
|
||||
|
||||
class MainWSQueueElement {
|
||||
constructor(msg_to_send, _inner_process, timeout, retry_num) {
|
||||
this.msg_to_send = msg_to_send;
|
||||
this.needs_to_send = msg_to_send != null;
|
||||
this.timeout = timeout || 5000;
|
||||
this.retry_num = retry_num || 1;
|
||||
this.timer = null;
|
||||
this.resolve = null;
|
||||
this.reject = null;
|
||||
this._inner_process = _inner_process;
|
||||
}
|
||||
|
||||
inner_process_msg(msg) {
|
||||
if (this._inner_process)
|
||||
return this._inner_process(msg);
|
||||
else
|
||||
return {};
|
||||
}
|
||||
|
||||
process_arrived_msg(msg) {
|
||||
let out = this.inner_process_msg(msg);
|
||||
if (out) {
|
||||
if (this.timer !==null) {
|
||||
clearTimeout(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
if (this.resolve)
|
||||
setTimeout(function() { this.resolve(out); }.bind(this), 0);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
enqueue() {
|
||||
main_ws_enqueue(this);
|
||||
return new Promise(function(resolve, reject) {
|
||||
this.resolve = resolve;
|
||||
this.reject = reject;
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
pop_msg_to_send() {
|
||||
if (this.needs_to_send) {
|
||||
this.needs_to_send = false;
|
||||
if (this.retry_num < 0 || this.retry_num > 0)
|
||||
this.timer = setTimeout(function() {
|
||||
this.timer = null;
|
||||
if (this.retry_num == 0) {
|
||||
main_ws_dequeue(this);
|
||||
if (this.reject) {
|
||||
this.reject(new Error('Timeout error detected'));
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.needs_to_send = true;
|
||||
main_ws_enqueue();
|
||||
}
|
||||
}.bind(this), this.timeout);
|
||||
if (this.retry_num > 0)
|
||||
this.retry_num--;
|
||||
return this.msg_to_send;
|
||||
}
|
||||
else
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function main_ws_dequeue(el) {
|
||||
let idx = main_ws_queue.indexOf(el);
|
||||
if (idx >= 0) {
|
||||
main_ws_queue.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
|
||||
function main_ws_enqueue(el) {
|
||||
if (el)
|
||||
main_ws_queue.push(el);
|
||||
if (main_ws)
|
||||
main_ws_queue_process();
|
||||
}
|
||||
|
||||
function main_ws_queue_process(msg) {
|
||||
if (!main_ws)
|
||||
return;
|
||||
let jsonobj;
|
||||
for (let i = 0; i < main_ws_queue.length; i++) {
|
||||
let el = main_ws_queue[i];
|
||||
if ((jsonobj = el.pop_msg_to_send())) {
|
||||
let logString = JSON.stringify(jsonobj);
|
||||
main_ws.send(logString);
|
||||
console.log('WS >> ' + logString);
|
||||
}
|
||||
else if (msg) {
|
||||
if (el.process_arrived_msg(msg)) {
|
||||
main_ws_queue.splice(i, 1);
|
||||
i--;
|
||||
msg = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function main_ws_connect() {
|
||||
let socket = new WebSocket((location.protocol == 'https:'?'wss://' : 'ws://') + host_url + '/' + get_template_name() + '-ws');
|
||||
socket.onopen = function (event) {
|
||||
console.log('Upgrade HTTP connection OK');
|
||||
main_ws = socket;
|
||||
main_ws_queue_process();
|
||||
};
|
||||
socket.onclose = function(e) {
|
||||
main_ws = null;
|
||||
console.log('Socket is closed. Reconnect will be attempted in 30 second.', e.reason);
|
||||
setTimeout(function() {
|
||||
main_ws_connect();
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
socket.onerror = function(err) {
|
||||
main_ws = null;
|
||||
console.error('Socket encountered error: ', err.message, 'Closing socket');
|
||||
socket.close();
|
||||
};
|
||||
socket.onmessage = function (event) {
|
||||
console.log(event.data);
|
||||
let msg = JSON.parse(event.data);
|
||||
main_ws_queue_process(msg);
|
||||
};
|
||||
}
|
||||
main_ws_connect();
|
||||
15
src/inner_templates/previewchart/moment.js
Normal file
15
src/inner_templates/previewchart/moment.js
Normal file
File diff suppressed because one or more lines are too long
1
src/inner_templates/previewchart/resize-observer.min.js
vendored
Normal file
1
src/inner_templates/previewchart/resize-observer.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!function(){return function e(t,r,n){function i(s,a){if(!r[s]){if(!t[s]){var c="function"==typeof require&&require;if(!a&&c)return c(s,!0);if(o)return o(s,!0);var u=new Error("Cannot find module '"+s+"'");throw u.code="MODULE_NOT_FOUND",u}var f=r[s]={exports:{}};t[s][0].call(f.exports,function(e){return i(t[s][1][e]||e)},f,f.exports,e,t,r,n)}return r[s].exports}for(var o="function"==typeof require&&require,s=0;s<n.length;s++)i(n[s]);return i}}()({1:[function(e,t,r){"use strict";Object.defineProperty(r,"__esModule",{value:!0});r.ContentRect=function(e){if("getBBox"in e){var t=e.getBBox();return Object.freeze({height:t.height,left:0,top:0,width:t.width})}var r=window.getComputedStyle(e);return Object.freeze({height:parseFloat(r.height||"0"),left:parseFloat(r.paddingLeft||"0"),top:parseFloat(r.paddingTop||"0"),width:parseFloat(r.width||"0")})}},{}],2:[function(e,t,r){"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n=e("./ContentRect"),i=function(){function e(e){this.target=e,this.$$broadcastWidth=this.$$broadcastHeight=0}return Object.defineProperty(e.prototype,"broadcastWidth",{get:function(){return this.$$broadcastWidth},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"broadcastHeight",{get:function(){return this.$$broadcastHeight},enumerable:!0,configurable:!0}),e.prototype.isActive=function(){var e=n.ContentRect(this.target);return!!e&&(e.width!==this.broadcastWidth||e.height!==this.broadcastHeight)},e}();r.ResizeObservation=i},{"./ContentRect":1}],3:[function(e,t,r){"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n=e("./ResizeObservation"),i=e("./ResizeObserverEntry"),o=[],s=function(){function e(e){this.$$observationTargets=[],this.$$activeTargets=[],this.$$skippedTargets=[];var t=function(e){if(void 0===e)return"Failed to construct 'ResizeObserver': 1 argument required, but only 0 present.";if("function"!=typeof e)return"Failed to construct 'ResizeObserver': The callback provided as parameter 1 is not a function."}(e);if(t)throw TypeError(t);this.$$callback=e,o.push(this)}return e.prototype.observe=function(e){var t=a("observe",e);if(t)throw TypeError(t);c(this.$$observationTargets,e)>0||(this.$$observationTargets.push(new n.ResizeObservation(e)),p())},e.prototype.unobserve=function(e){var t=a("unobserve",e);if(t)throw TypeError(t);var r=c(this.$$observationTargets,e);r<0||(this.$$observationTargets.splice(r,1),g())},e.prototype.disconnect=function(){this.$$observationTargets=[],this.$$activeTargets=[]},e}();function a(e,t){return void 0===t?"Failed to execute '"+e+"' on 'ResizeObserver': 1 argument required, but only 0 present.":t instanceof window.Element?void 0:"Failed to execute '"+e+"' on 'ResizeObserver': parameter 1 is not of type 'Element'."}function c(e,t){for(var r=0;r<e.length;r+=1)if(e[r].target===t)return r;return-1}r.ResizeObserver=s;var u,f=function(e){o.forEach(function(t){t.$$activeTargets=[],t.$$skippedTargets=[],t.$$observationTargets.forEach(function(r){r.isActive()&&(d(r.target)>e?t.$$activeTargets.push(r):t.$$skippedTargets.push(r))})})},v=function(){var e=1/0;return o.forEach(function(t){if(t.$$activeTargets.length){var r=[];t.$$activeTargets.forEach(function(t){var n=new i.ResizeObserverEntry(t.target);r.push(n),t.$$broadcastWidth=n.contentRect.width,t.$$broadcastHeight=n.contentRect.height;var o=d(t.target);o<e&&(e=o)}),t.$$callback(r,t),t.$$activeTargets=[]}}),e},d=function(e){for(var t=0;e.parentNode;)e=e.parentNode,t+=1;return t},h=function(){var e,t=0;for(f(t);o.some(function(e){return!!e.$$activeTargets.length});)t=v(),f(t);o.some(function(e){return!!e.$$skippedTargets.length})&&(e=new window.ErrorEvent("ResizeLoopError",{message:"ResizeObserver loop completed with undelivered notifications."}),window.dispatchEvent(e))},p=function(){u||b()},b=function e(){u=window.requestAnimationFrame(function(){h(),e()})},g=function(){u&&!o.some(function(e){return!!e.$$observationTargets.length})&&(window.cancelAnimationFrame(u),u=void 0)};r.install=function(){return window.ResizeObserver=s}},{"./ResizeObservation":2,"./ResizeObserverEntry":4}],4:[function(e,t,r){"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n=e("./ContentRect"),i=function(){return function(e){this.target=e,this.contentRect=n.ContentRect(e)}}();r.ResizeObserverEntry=i},{"./ContentRect":1}],5:[function(e,t,r){"use strict";Object.defineProperty(r,"__esModule",{value:!0}),e("./ResizeObserver").install()},{"./ResizeObserver":3}]},{},[5]);
|
||||
11
src/main.qml
11
src/main.qml
@@ -22,10 +22,12 @@ ApplicationWindow {
|
||||
signal gpxpreview_open_clicked(url name)
|
||||
signal profile_open_clicked(url name)
|
||||
signal trainprogram_open_clicked(url name)
|
||||
signal fitfile_preview_clicked(url name)
|
||||
signal trainprogram_open_other_folder(url name)
|
||||
signal gpx_open_other_folder(url name)
|
||||
signal trainprogram_preview(url name)
|
||||
signal trainprogram_zwo_loaded(string s)
|
||||
signal fitfile_preview(string s)
|
||||
signal gpx_save_clicked()
|
||||
signal fit_save_clicked()
|
||||
signal refresh_bluetooth_devices_clicked()
|
||||
@@ -716,6 +718,15 @@ ApplicationWindow {
|
||||
}
|
||||
}
|
||||
|
||||
ItemDelegate {
|
||||
text: qsTr("📅 Workouts History")
|
||||
width: parent.width
|
||||
onClicked: {
|
||||
stackView.push("WorkoutsHistory.qml")
|
||||
stackView.currentItem.fitfile_preview_clicked.connect(fitfile_preview_clicked)
|
||||
drawer.close()
|
||||
}
|
||||
}
|
||||
ItemDelegate {
|
||||
text: qsTr("👜Swag Bag")
|
||||
width: parent.width
|
||||
|
||||
@@ -62,6 +62,9 @@ void MainWindow::update() {
|
||||
double strideLength = 0;
|
||||
double groundContact = 0;
|
||||
double verticalOscillation = 0;
|
||||
double target_cadence = 0;
|
||||
double target_watt = 0;
|
||||
double target_resistance = 0;
|
||||
double stepCount = 0;
|
||||
|
||||
ui->speed->setText(QString::number(bluetoothManager->device()->currentSpeed().value(), 'f', 2));
|
||||
@@ -179,6 +182,7 @@ void MainWindow::update() {
|
||||
false, totalStrokes, avgStrokesRate, maxStrokesRate, avgStrokesLength,
|
||||
bluetoothManager->device()->currentCordinate(), strideLength, groundContact,
|
||||
verticalOscillation, stepCount,
|
||||
target_cadence, target_watt, target_resistance,
|
||||
bluetoothManager->device()->CoreBodyTemperature.value(), bluetoothManager->device()->SkinTemperature.value(),
|
||||
bluetoothManager->device()->HeatStrainIndex.value() // TODO add lap
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
include(../defaults.pri)
|
||||
QT += bluetooth widgets xml positioning quick networkauth websockets texttospeech location multimedia
|
||||
QTPLUGIN += qavfmediaplayer
|
||||
QT+= charts core-private
|
||||
QT+= charts core-private sql concurrent
|
||||
|
||||
qtHaveModule(httpserver) {
|
||||
QT += httpserver
|
||||
@@ -106,6 +106,7 @@ SOURCES += \
|
||||
$$PWD/devices/echelonstairclimber/echelonstairclimber.cpp \
|
||||
$$PWD/devices/technogymbike/technogymbike.cpp \
|
||||
$$PWD/devices/trxappgateusbelliptical/trxappgateusbelliptical.cpp \
|
||||
$$PWD/fitdatabaseprocessor.cpp \
|
||||
$$PWD/devices/trxappgateusbrower/trxappgateusbrower.cpp \
|
||||
$$PWD/logwriter.cpp \
|
||||
$$PWD/mqtt/qmqttauthenticationproperties.cpp \
|
||||
@@ -121,6 +122,8 @@ SOURCES += \
|
||||
$$PWD/mqtt/qmqtttopicname.cpp \
|
||||
$$PWD/mqtt/qmqtttype.cpp \
|
||||
$$PWD/osc.cpp \
|
||||
$$PWD/workoutloaderworker.cpp \
|
||||
$$PWD/workoutmodel.cpp \
|
||||
QTelnet.cpp \
|
||||
devices/bkoolbike/bkoolbike.cpp \
|
||||
devices/csafe/csafe.cpp \
|
||||
@@ -376,6 +379,7 @@ HEADERS += \
|
||||
$$PWD/devices/trxappgateusbelliptical/trxappgateusbelliptical.h \
|
||||
$$PWD/devices/trxappgateusbrower/trxappgateusbrower.h \
|
||||
$$PWD/ergtable.h \
|
||||
$$PWD/fitdatabaseprocessor.h \
|
||||
$$PWD/inclinationresistancetable.h \
|
||||
$$PWD/logwriter.h \
|
||||
$$PWD/osc.h \
|
||||
@@ -408,6 +412,8 @@ HEADERS += \
|
||||
$$PWD/mqtt/qmqtttype.h \
|
||||
$$PWD/treadmillErgTable.h \
|
||||
$$PWD/wheelcircumference.h \
|
||||
$$PWD/workoutloaderworker.h \
|
||||
$$PWD/workoutmodel.h \
|
||||
QTelnet.h \
|
||||
devices/bkoolbike/bkoolbike.h \
|
||||
devices/csafe/csafe.h \
|
||||
|
||||
172
src/qfit.cpp
172
src/qfit.cpp
@@ -1,11 +1,11 @@
|
||||
#include "qfit.h"
|
||||
|
||||
#include <QSettings>
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <ostream>
|
||||
#include <QDir>
|
||||
|
||||
#include "QSettings"
|
||||
|
||||
#include "fit_date_time.hpp"
|
||||
#include "fit_encode.hpp"
|
||||
@@ -89,6 +89,14 @@ void qfit::save(const QString &filename, QList<SessionLine> session, bluetoothde
|
||||
}
|
||||
fileIdMesg.SetTimeCreated(session.at(firstRealIndex).time.toSecsSinceEpoch() - 631065600L);
|
||||
|
||||
fit::UserProfileMesg userMesg;
|
||||
userMesg.SetWeight(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat());
|
||||
userMesg.SetAge(settings.value(QZSettings::age, QZSettings::default_age).toUInt());
|
||||
userMesg.SetGender(settings.value(QZSettings::sex, QZSettings::default_sex).toString().startsWith(QZSettings::default_sex) ? FIT_GENDER_MALE
|
||||
: FIT_GENDER_FEMALE);
|
||||
userMesg.SetFriendlyName(
|
||||
settings.value(QZSettings::user_nickname, QZSettings::default_user_nickname).toString().toStdWString());
|
||||
|
||||
fit::FileCreatorMesg fileCreatorMesg;
|
||||
if(fit_file_garmin_device_training_effect) {
|
||||
fileCreatorMesg.SetSoftwareVersion(975);
|
||||
@@ -151,6 +159,51 @@ void qfit::save(const QString &filename, QList<SessionLine> session, bluetoothde
|
||||
qDebug() << "average speed from the fit file" << speed_avg;
|
||||
}
|
||||
|
||||
encode.Open(file);
|
||||
encode.Write(fileIdMesg);
|
||||
encode.Write(userMesg);
|
||||
|
||||
// Declare developer field descriptions (but don't write them yet)
|
||||
fit::FieldDescriptionMesg activityTitle;
|
||||
activityTitle.SetDeveloperDataIndex(0);
|
||||
activityTitle.SetFieldDefinitionNumber(0);
|
||||
activityTitle.SetFitBaseTypeId(FIT_BASE_TYPE_STRING);
|
||||
activityTitle.SetFieldName(0, L"Activity Title");
|
||||
activityTitle.SetUnits(0, L"Title");
|
||||
activityTitle.SetNativeMesgNum(FIT_MESG_NUM_SESSION);
|
||||
|
||||
fit::FieldDescriptionMesg targetCadenceMesg;
|
||||
targetCadenceMesg.SetDeveloperDataIndex(0);
|
||||
targetCadenceMesg.SetFieldDefinitionNumber(1);
|
||||
targetCadenceMesg.SetFitBaseTypeId(FIT_BASE_TYPE_FLOAT64);
|
||||
targetCadenceMesg.SetFieldName(0, L"Target Cadence");
|
||||
targetCadenceMesg.SetUnits(0, L"rpm");
|
||||
targetCadenceMesg.SetNativeMesgNum(FIT_MESG_NUM_RECORD);
|
||||
|
||||
fit::FieldDescriptionMesg targetWattMesg;
|
||||
targetWattMesg.SetDeveloperDataIndex(0);
|
||||
targetWattMesg.SetFieldDefinitionNumber(2);
|
||||
targetWattMesg.SetFitBaseTypeId(FIT_BASE_TYPE_FLOAT64);
|
||||
targetWattMesg.SetFieldName(0, L"Target Watt");
|
||||
targetWattMesg.SetUnits(0, L"watts");
|
||||
targetWattMesg.SetNativeMesgNum(FIT_MESG_NUM_RECORD);
|
||||
|
||||
fit::FieldDescriptionMesg targetResistanceMesg;
|
||||
targetResistanceMesg.SetDeveloperDataIndex(0);
|
||||
targetResistanceMesg.SetFieldDefinitionNumber(3);
|
||||
targetResistanceMesg.SetFitBaseTypeId(FIT_BASE_TYPE_FLOAT64);
|
||||
targetResistanceMesg.SetFieldName(0, L"Target Resistance");
|
||||
targetResistanceMesg.SetUnits(0, L"resistance");
|
||||
targetResistanceMesg.SetNativeMesgNum(FIT_MESG_NUM_RECORD);
|
||||
|
||||
fit::FieldDescriptionMesg ftpSessionMesg;
|
||||
ftpSessionMesg.SetDeveloperDataIndex(0);
|
||||
ftpSessionMesg.SetFieldDefinitionNumber(4);
|
||||
ftpSessionMesg.SetFitBaseTypeId(FIT_BASE_TYPE_FLOAT64);
|
||||
ftpSessionMesg.SetFieldName(0, L"FTP");
|
||||
ftpSessionMesg.SetUnits(0, L"FTP");
|
||||
ftpSessionMesg.SetNativeMesgNum(FIT_MESG_NUM_SESSION);
|
||||
|
||||
fit::SessionMesg sessionMesg;
|
||||
sessionMesg.SetTimestamp(session.at(firstRealIndex).time.toSecsSinceEpoch() - 631065600L);
|
||||
sessionMesg.SetStartTime(session.at(firstRealIndex).time.toSecsSinceEpoch() - 631065600L);
|
||||
@@ -255,7 +308,7 @@ void qfit::save(const QString &filename, QList<SessionLine> session, bluetoothde
|
||||
// Create developer field descriptions for custom temperature fields
|
||||
fit::FieldDescriptionMesg coreTemperatureFieldDesc;
|
||||
coreTemperatureFieldDesc.SetDeveloperDataIndex(0);
|
||||
coreTemperatureFieldDesc.SetFieldDefinitionNumber(0);
|
||||
coreTemperatureFieldDesc.SetFieldDefinitionNumber(5);
|
||||
coreTemperatureFieldDesc.SetFitBaseTypeId(FIT_BASE_TYPE_FLOAT32);
|
||||
coreTemperatureFieldDesc.SetFieldName(0, L"core_temperature");
|
||||
coreTemperatureFieldDesc.SetUnits(0, L"°C");
|
||||
@@ -264,7 +317,7 @@ void qfit::save(const QString &filename, QList<SessionLine> session, bluetoothde
|
||||
|
||||
fit::FieldDescriptionMesg skinTemperatureFieldDesc;
|
||||
skinTemperatureFieldDesc.SetDeveloperDataIndex(0);
|
||||
skinTemperatureFieldDesc.SetFieldDefinitionNumber(1);
|
||||
skinTemperatureFieldDesc.SetFieldDefinitionNumber(6);
|
||||
skinTemperatureFieldDesc.SetFitBaseTypeId(FIT_BASE_TYPE_FLOAT32);
|
||||
skinTemperatureFieldDesc.SetFieldName(0, L"skin_temperature");
|
||||
skinTemperatureFieldDesc.SetUnits(0, L"°C");
|
||||
@@ -273,13 +326,22 @@ void qfit::save(const QString &filename, QList<SessionLine> session, bluetoothde
|
||||
|
||||
fit::FieldDescriptionMesg heatStrainIndexFieldDesc;
|
||||
heatStrainIndexFieldDesc.SetDeveloperDataIndex(0);
|
||||
heatStrainIndexFieldDesc.SetFieldDefinitionNumber(2);
|
||||
heatStrainIndexFieldDesc.SetFieldDefinitionNumber(7);
|
||||
heatStrainIndexFieldDesc.SetFitBaseTypeId(FIT_BASE_TYPE_FLOAT32);
|
||||
heatStrainIndexFieldDesc.SetFieldName(0, L"heat_strain_index");
|
||||
heatStrainIndexFieldDesc.SetUnits(0, L"a.u.");
|
||||
heatStrainIndexFieldDesc.SetNativeMesgNum(FIT_MESG_NUM_RECORD);
|
||||
heatStrainIndexFieldDesc.SetNativeFieldNum(255); // Use invalid field number to indicate custom field
|
||||
|
||||
fit::DeveloperField ftpSessionField(ftpSessionMesg, devIdMesg);
|
||||
ftpSessionField.AddValue(settings.value(QZSettings::ftp, QZSettings::default_ftp).toDouble());
|
||||
|
||||
fit::DeveloperField activityTitleField(activityTitle, devIdMesg);
|
||||
activityTitleField.SetSTRINGValue(workoutName.toStdWString());
|
||||
|
||||
sessionMesg.AddDeveloperField(activityTitleField);
|
||||
sessionMesg.AddDeveloperField(ftpSessionField);
|
||||
|
||||
fit::ActivityMesg activityMesg;
|
||||
activityMesg.SetTimestamp(session.at(firstRealIndex).time.toSecsSinceEpoch() - 631065600L);
|
||||
activityMesg.SetTotalTimerTime(session.last().elapsedTime);
|
||||
@@ -298,11 +360,16 @@ void qfit::save(const QString &filename, QList<SessionLine> session, bluetoothde
|
||||
eventMesg.SetData(0);
|
||||
eventMesg.SetEventGroup(0);
|
||||
eventMesg.SetTimestamp(session.at(firstRealIndex).time.toSecsSinceEpoch() - 631065600L);
|
||||
|
||||
encode.Open(file);
|
||||
encode.Write(fileIdMesg);
|
||||
encode.Write(fileCreatorMesg);
|
||||
encode.Write(devIdMesg);
|
||||
|
||||
// Write developer field descriptions (declared earlier)
|
||||
encode.Write(activityTitle);
|
||||
encode.Write(targetCadenceMesg);
|
||||
encode.Write(targetWattMesg);
|
||||
encode.Write(targetResistanceMesg);
|
||||
encode.Write(ftpSessionMesg);
|
||||
|
||||
encode.Write(coreTemperatureFieldDesc);
|
||||
encode.Write(skinTemperatureFieldDesc);
|
||||
encode.Write(heatStrainIndexFieldDesc);
|
||||
@@ -373,6 +440,9 @@ void qfit::save(const QString &filename, QList<SessionLine> session, bluetoothde
|
||||
lapMesg.SetSport(FIT_SPORT_CYCLING);
|
||||
}
|
||||
|
||||
encode.Write(sessionMesg);
|
||||
encode.Write(activityMesg);
|
||||
|
||||
SessionLine sl;
|
||||
if (processFlag & QFIT_PROCESS_DISTANCENOISE) {
|
||||
double distanceOld = -1.0;
|
||||
@@ -401,6 +471,19 @@ void qfit::save(const QString &filename, QList<SessionLine> session, bluetoothde
|
||||
|
||||
fit::RecordMesg newRecord;
|
||||
sl = session.at(i);
|
||||
|
||||
fit::DeveloperField targetCadenceField(targetCadenceMesg, devIdMesg);
|
||||
targetCadenceField.AddValue(sl.target_cadence);
|
||||
newRecord.AddDeveloperField(targetCadenceField);
|
||||
|
||||
fit::DeveloperField targetWattField(targetWattMesg, devIdMesg);
|
||||
targetWattField.AddValue(sl.target_watt);
|
||||
newRecord.AddDeveloperField(targetWattField);
|
||||
|
||||
fit::DeveloperField targetResistanceField(targetResistanceMesg, devIdMesg);
|
||||
targetResistanceField.AddValue(sl.target_resistance);
|
||||
newRecord.AddDeveloperField(targetResistanceField);
|
||||
|
||||
// fit::DateTime date((time_t)session.at(i).time.toSecsSinceEpoch());
|
||||
newRecord.SetHeartRate(sl.heart);
|
||||
uint8_t cad = sl.cadence;
|
||||
@@ -486,8 +569,6 @@ void qfit::save(const QString &filename, QList<SessionLine> session, bluetoothde
|
||||
lapMesg.SetLapTrigger(FIT_LAP_TRIGGER_SESSION_END);
|
||||
lapMesg.SetMessageIndex(lap_index++);
|
||||
encode.Write(lapMesg);
|
||||
encode.Write(sessionMesg);
|
||||
encode.Write(activityMesg);
|
||||
|
||||
if (!encode.Close()) {
|
||||
|
||||
@@ -506,9 +587,12 @@ class Listener : public fit::FileIdMesgListener,
|
||||
public fit::DeviceInfoMesgListener,
|
||||
public fit::MesgListener,
|
||||
public fit::DeveloperFieldDescriptionListener,
|
||||
public fit::RecordMesgListener {
|
||||
public fit::RecordMesgListener,
|
||||
public fit::SessionMesgListener {
|
||||
public:
|
||||
QList<SessionLine> *sessionOpening = nullptr;
|
||||
FIT_SPORT *sport = nullptr;
|
||||
QString *workoutName = nullptr;
|
||||
|
||||
static void PrintValues(const fit::FieldBase &field) {
|
||||
for (FIT_UINT8 j = 0; j < (FIT_UINT8)field.GetNumValues(); j++) {
|
||||
@@ -630,6 +714,7 @@ class Listener : public fit::FileIdMesgListener,
|
||||
printf(" Activity type: %d\n", mesg.GetActivityType());
|
||||
}
|
||||
|
||||
|
||||
switch (mesg.GetActivityType()) // The Cycling field is dynamic
|
||||
{
|
||||
case FIT_ACTIVITY_TYPE_WALKING:
|
||||
@@ -720,7 +805,8 @@ class Listener : public fit::FileIdMesgListener,
|
||||
if (!s.coordinate.isValid()) {
|
||||
s.elevationGain = record.GetAltitude();
|
||||
}
|
||||
s.time = QDateTime::fromSecsSinceEpoch(record.GetTimestamp());
|
||||
s.time = QDateTime::fromSecsSinceEpoch(record.GetTimestamp() + 631065600L);
|
||||
s.elapsedTime = (sessionOpening->count() ? sessionOpening->at(0).time : s.time).secsTo(s.time);
|
||||
sessionOpening->append(s);
|
||||
}
|
||||
}
|
||||
@@ -730,9 +816,35 @@ class Listener : public fit::FileIdMesgListener,
|
||||
printf(" App Version: %d\n", desc.GetApplicationVersion());
|
||||
printf(" Field Number: %d\n", desc.GetFieldDefinitionNumber());
|
||||
}
|
||||
|
||||
void OnMesg(fit::SessionMesg &mesg) override {
|
||||
printf("Session Message:\n");
|
||||
|
||||
// Extract sport type from SessionMesg
|
||||
if (sport != nullptr && mesg.IsSportValid()) {
|
||||
*sport = mesg.GetSport();
|
||||
printf(" Sport type from session: %d\n", static_cast<int>(*sport));
|
||||
}
|
||||
|
||||
if (workoutName != nullptr) {
|
||||
for (auto devField : mesg.GetDeveloperFields()) {
|
||||
std::string fieldName = devField.GetName();
|
||||
if (fieldName == "Activity Title") {
|
||||
std::wstring wWorkoutName = devField.GetSTRINGValue(0);
|
||||
*workoutName = QString::fromStdWString(wWorkoutName);
|
||||
printf(" Found Activity Title: %s\n", workoutName->toStdString().c_str());
|
||||
} else if (fieldName == "Instructor Name" || fieldName == "Coach Name") {
|
||||
// Future: handle instructor name if needed
|
||||
printf(" Found Instructor/Coach field: %s\n", fieldName.c_str());
|
||||
} else if (!fieldName.empty()) {
|
||||
printf(" Other developer field: %s\n", fieldName.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void qfit::open(const QString &filename, QList<SessionLine> *output) {
|
||||
void qfit::open(const QString &filename, QList<SessionLine> *output, FIT_SPORT *sport) {
|
||||
std::fstream file;
|
||||
#ifdef _WIN32
|
||||
file.open(QString(filename).toLocal8Bit().constData(), std::ios::in | std::ios::binary);
|
||||
@@ -752,12 +864,48 @@ void qfit::open(const QString &filename, QList<SessionLine> *output) {
|
||||
std::istream &s = file;
|
||||
fit::MesgBroadcaster mesgBroadcaster;
|
||||
Listener listener;
|
||||
listener.sport = sport;
|
||||
listener.sessionOpening = output;
|
||||
mesgBroadcaster.AddListener((fit::FileIdMesgListener &)listener);
|
||||
mesgBroadcaster.AddListener((fit::UserProfileMesgListener &)listener);
|
||||
mesgBroadcaster.AddListener((fit::MonitoringMesgListener &)listener);
|
||||
mesgBroadcaster.AddListener((fit::DeviceInfoMesgListener &)listener);
|
||||
mesgBroadcaster.AddListener((fit::RecordMesgListener &)listener);
|
||||
mesgBroadcaster.AddListener((fit::SessionMesgListener &)listener);
|
||||
mesgBroadcaster.AddListener((fit::MesgListener &)listener);
|
||||
decode.Read(&s, &mesgBroadcaster, &mesgBroadcaster, &listener);
|
||||
|
||||
file.close();
|
||||
}
|
||||
|
||||
void qfit::open(const QString &filename, QList<SessionLine> *output, FIT_SPORT *sport, QString *workoutName) {
|
||||
std::fstream file;
|
||||
#ifdef _WIN32
|
||||
file.open(QString(filename).toLocal8Bit().constData(), std::ios::in | std::ios::binary);
|
||||
#else
|
||||
file.open(filename.toStdString(), std::ios::in | std::ios::binary);
|
||||
#endif
|
||||
|
||||
if (!file.is_open()) {
|
||||
std::system_error(errno, std::system_category(), "failed to open " + filename.toStdString());
|
||||
qDebug() << "opened " << filename << errno;
|
||||
printf("Error opening file ExampleActivity.fit\n");
|
||||
return;
|
||||
}
|
||||
|
||||
fit::Decode decode;
|
||||
std::istream &s = file;
|
||||
fit::MesgBroadcaster mesgBroadcaster;
|
||||
Listener listener;
|
||||
listener.sport = sport;
|
||||
listener.sessionOpening = output;
|
||||
listener.workoutName = workoutName;
|
||||
mesgBroadcaster.AddListener((fit::FileIdMesgListener &)listener);
|
||||
mesgBroadcaster.AddListener((fit::UserProfileMesgListener &)listener);
|
||||
mesgBroadcaster.AddListener((fit::MonitoringMesgListener &)listener);
|
||||
mesgBroadcaster.AddListener((fit::DeviceInfoMesgListener &)listener);
|
||||
mesgBroadcaster.AddListener((fit::RecordMesgListener &)listener);
|
||||
mesgBroadcaster.AddListener((fit::SessionMesgListener &)listener);
|
||||
mesgBroadcaster.AddListener((fit::MesgListener &)listener);
|
||||
decode.Read(&s, &mesgBroadcaster, &mesgBroadcaster, &listener);
|
||||
|
||||
|
||||
@@ -18,7 +18,8 @@ class qfit : public QObject {
|
||||
explicit qfit(QObject *parent = nullptr);
|
||||
static void save(const QString &filename, QList<SessionLine> session, bluetoothdevice::BLUETOOTH_TYPE type,
|
||||
uint32_t processFlag = QFIT_PROCESS_NONE, FIT_SPORT overrideSport = FIT_SPORT_INVALID, QString workoutName = "", QString bluetooth_device_name = "");
|
||||
static void open(const QString &filename, QList<SessionLine>* output);
|
||||
static void open(const QString &filename, QList<SessionLine>* output, FIT_SPORT *sport);
|
||||
static void open(const QString &filename, QList<SessionLine>* output, FIT_SPORT *sport, QString *workoutName);
|
||||
|
||||
signals:
|
||||
};
|
||||
|
||||
13
src/qml.qrc
13
src/qml.qrc
@@ -84,6 +84,18 @@
|
||||
<file>gpx/Richmond Park.gpx</file>
|
||||
<file>gpx/St Andrews to Pittenweem.gpx</file>
|
||||
<file>GPXList.qml</file>
|
||||
<file>WorkoutsHistory.qml</file>
|
||||
<file>inner_templates/previewchart/ajax-loader.gif</file>
|
||||
<file>inner_templates/previewchart/chart.htm</file>
|
||||
<file>inner_templates/previewchart/chartjs-adapter-moment.js</file>
|
||||
<file>inner_templates/previewchart/chartjs-plugin-annotation.min.js</file>
|
||||
<file>inner_templates/previewchart/chartjs.3.4.1.min.js</file>
|
||||
<file>inner_templates/previewchart/dochart.js</file>
|
||||
<file>inner_templates/previewchart/globals.js</file>
|
||||
<file>inner_templates/previewchart/jquery-3.6.0.min.js</file>
|
||||
<file>inner_templates/previewchart/main_ws_manager.js</file>
|
||||
<file>inner_templates/previewchart/moment.js</file>
|
||||
<file>inner_templates/previewchart/resize-observer.min.js</file>
|
||||
<file>videoPlayback.qml</file>
|
||||
<file>settings-tiles.qml</file>
|
||||
<file>inner_templates/floating/floating.htm</file>
|
||||
@@ -114,6 +126,7 @@
|
||||
<file>StaticAccordionElement.qml</file>
|
||||
<file>inner_templates/chartjs/dotreadmillchartlive.js</file>
|
||||
<file>inner_templates/chartjs/treadmillchartlive.htm</file>
|
||||
<file>PreviewChart.qml</file>
|
||||
<file>WebPelotonAuth.qml</file>
|
||||
<file>inner_templates/floating/hfloating.htm</file>
|
||||
</qresource>
|
||||
|
||||
@@ -5,6 +5,7 @@ SessionLine::SessionLine(double speed, int8_t inclination, double distance, uint
|
||||
double elevationGain, uint32_t elapsed, bool lap, uint32_t totalStrokes, double avgStrokesRate,
|
||||
double maxStrokesRate, double avgStrokesLength, const QGeoCoordinate coordinate,
|
||||
double instantaneousStrideLengthCM, double groundContactMS, double verticalOscillationMM, double stepCount,
|
||||
double target_cadence, double target_watt, double target_resistance,
|
||||
double coreTemp, double bodyTemp, double heatStrainIndex,
|
||||
const QDateTime &time) {
|
||||
this->speed = speed;
|
||||
@@ -29,6 +30,9 @@ SessionLine::SessionLine(double speed, int8_t inclination, double distance, uint
|
||||
this->instantaneousStrideLengthCM = instantaneousStrideLengthCM;
|
||||
this->groundContactMS = groundContactMS;
|
||||
this->verticalOscillationMM = verticalOscillationMM;
|
||||
this->target_cadence = target_cadence;
|
||||
this->target_watt = target_watt;
|
||||
this->target_resistance = target_resistance;
|
||||
this->stepCount = stepCount;
|
||||
this->coreTemp = coreTemp;
|
||||
this->bodyTemp = bodyTemp;
|
||||
|
||||
@@ -32,6 +32,9 @@ class SessionLine {
|
||||
double instantaneousStrideLengthCM;
|
||||
double groundContactMS;
|
||||
double verticalOscillationMM;
|
||||
double target_cadence;
|
||||
double target_watt;
|
||||
double target_resistance;
|
||||
double stepCount;
|
||||
double coreTemp;
|
||||
double bodyTemp;
|
||||
@@ -43,6 +46,7 @@ class SessionLine {
|
||||
double elevationGain, uint32_t elapsed, bool lap, uint32_t totalStrokes, double avgStrokesRate,
|
||||
double maxStrokesRate, double avgStrokesLength, const QGeoCoordinate coordinate,
|
||||
double instantaneousStrideLengthCM, double groundContactMS, double verticalOscillationMM, double stepCount,
|
||||
double target_cadence, double target_watt, double target_resistance,
|
||||
double coreTemp, double bodyTemp, double heatStrainIndex,
|
||||
const QDateTime &time = QDateTime::currentDateTime());
|
||||
};
|
||||
|
||||
@@ -236,6 +236,13 @@ void TemplateInfoSenderBuilder::clearSessionArray() {
|
||||
}
|
||||
}
|
||||
|
||||
void TemplateInfoSenderBuilder::clearPreviewSessionArray() {
|
||||
int len = previewSessionArray.count();
|
||||
for (int i = 0; i < len; i++) {
|
||||
previewSessionArray.removeAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
void TemplateInfoSenderBuilder::start(bluetoothdevice *dev) {
|
||||
device = nullptr;
|
||||
clearSessionArray();
|
||||
@@ -516,6 +523,14 @@ void TemplateInfoSenderBuilder::onAppendActivityDescription(const QJsonValue &ms
|
||||
tempSender->send(out.toJson());
|
||||
}
|
||||
|
||||
void TemplateInfoSenderBuilder::onGetPreviewSessionArray(TemplateInfoSender *tempSender) {
|
||||
QJsonObject main;
|
||||
main[QStringLiteral("content")] = previewSessionArray;
|
||||
main[QStringLiteral("msg")] = QStringLiteral("R_getpreviewsessionarray");
|
||||
QJsonDocument out(main);
|
||||
tempSender->send(out.toJson());
|
||||
}
|
||||
|
||||
void TemplateInfoSenderBuilder::onGetSessionArray(TemplateInfoSender *tempSender) {
|
||||
QJsonObject main;
|
||||
main[QStringLiteral("content")] = sessionArray;
|
||||
@@ -879,6 +894,9 @@ void TemplateInfoSenderBuilder::onDataReceived(const QByteArray &data) {
|
||||
} else if (msg == QStringLiteral("getsessionarray")) {
|
||||
onGetSessionArray(sender);
|
||||
return;
|
||||
} else if (msg == QStringLiteral("getpreviewsessionarray")) {
|
||||
onGetPreviewSessionArray(sender);
|
||||
return;
|
||||
}
|
||||
if (msg == QStringLiteral("start")) {
|
||||
onStart(sender);
|
||||
@@ -1167,3 +1185,134 @@ void TemplateInfoSenderBuilder::workoutEventStateChanged(bluetoothdevice::WORKOU
|
||||
clearSessionArray();
|
||||
}
|
||||
}
|
||||
|
||||
void TemplateInfoSenderBuilder::previewSessionOnChart(QList<SessionLine> *session, FIT_SPORT sport) {
|
||||
previewSessionOnChart(session, sport, "");
|
||||
}
|
||||
|
||||
void TemplateInfoSenderBuilder::previewSessionOnChart(QList<SessionLine> *session, FIT_SPORT sport, const QString &workoutName) {
|
||||
auto startTime = std::chrono::high_resolution_clock::now();
|
||||
qDebug() << "previewSessionOnChart: Starting with" << session->size() << "elements";
|
||||
|
||||
clearPreviewSessionArray();
|
||||
auto afterClear = std::chrono::high_resolution_clock::now();
|
||||
qDebug() << "Clear took:" << std::chrono::duration_cast<std::chrono::milliseconds>(afterClear - startTime).count() << "ms";
|
||||
|
||||
buildContext(true);
|
||||
auto afterContext = std::chrono::high_resolution_clock::now();
|
||||
qDebug() << "buildContext took:" << std::chrono::duration_cast<std::chrono::milliseconds>(afterContext - afterClear).count() << "ms";
|
||||
|
||||
if (session->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Pre-calculate stats for performance
|
||||
double avgWatts = 0, maxWatts = 0, avgHeart = 0, maxHeart = 0, avgSpeed = 0, maxSpeed = 0, avgCadence = 0, maxCadence = 0;
|
||||
double totalWatts = 0, totalHeart = 0, totalSpeed = 0, totalCadence = 0;
|
||||
int validWatts = 0, validHeart = 0, validSpeed = 0, validCadence = 0;
|
||||
|
||||
// Single optimized pass
|
||||
for (const SessionLine &s : *session) {
|
||||
if (s.watt > 0) { totalWatts += s.watt; validWatts++; maxWatts = qMax((double)s.watt, maxWatts); }
|
||||
if (s.heart > 0) { totalHeart += s.heart; validHeart++; maxHeart = qMax((double)s.heart, maxHeart); }
|
||||
if (s.speed > 0) { totalSpeed += s.speed; validSpeed++; maxSpeed = qMax(s.speed, maxSpeed); }
|
||||
if (s.cadence > 0) { totalCadence += s.cadence; validCadence++; maxCadence = qMax((double)s.cadence, maxCadence); }
|
||||
}
|
||||
|
||||
avgWatts = validWatts > 0 ? totalWatts / validWatts : 0;
|
||||
avgHeart = validHeart > 0 ? totalHeart / validHeart : 0;
|
||||
avgSpeed = validSpeed > 0 ? totalSpeed / validSpeed : 0;
|
||||
avgCadence = validCadence > 0 ? totalCadence / validCadence : 0;
|
||||
|
||||
auto afterStats = std::chrono::high_resolution_clock::now();
|
||||
qDebug() << "Stats calculation took:" << std::chrono::duration_cast<std::chrono::milliseconds>(afterStats - afterContext).count() << "ms";
|
||||
|
||||
// Pre-calculate common values outside the loop
|
||||
QSettings settings;
|
||||
const QString nickName = settings.value(QZSettings::user_nickname, QZSettings::default_user_nickname).toString();
|
||||
const QString displayNickName = nickName.isEmpty() ? QStringLiteral("N/A") : nickName;
|
||||
const QString displayWorkoutName = workoutName.isEmpty() ? QStringLiteral("FIT Workout") : workoutName;
|
||||
const QString instructorName = QStringLiteral("");
|
||||
|
||||
// Get properly formatted date from first session element for summary display
|
||||
const QString workoutStartDateFormatted = !session->isEmpty() ? session->first().time.toString() : QStringLiteral("");
|
||||
|
||||
auto afterPreCalc = std::chrono::high_resolution_clock::now();
|
||||
qDebug() << "Pre-calculation took:" << std::chrono::duration_cast<std::chrono::milliseconds>(afterPreCalc - afterStats).count() << "ms";
|
||||
|
||||
// Reserve space for better performance
|
||||
previewSessionArray = QJsonArray();
|
||||
|
||||
qDebug() << "Starting main loop with" << session->size() << "elements";
|
||||
int processedItems = 0;
|
||||
|
||||
// Pre-create template QJsonObject for reuse
|
||||
QJsonObject itemTemplate;
|
||||
itemTemplate[QStringLiteral("speed_avg")] = avgSpeed;
|
||||
itemTemplate[QStringLiteral("speed_max")] = maxSpeed;
|
||||
itemTemplate[QStringLiteral("heart_avg")] = avgHeart;
|
||||
itemTemplate[QStringLiteral("heart_max")] = maxHeart;
|
||||
itemTemplate[QStringLiteral("watts_avg")] = avgWatts;
|
||||
itemTemplate[QStringLiteral("watts_max")] = maxWatts;
|
||||
itemTemplate[QStringLiteral("workoutName")] = displayWorkoutName;
|
||||
itemTemplate[QStringLiteral("instructorName")] = instructorName;
|
||||
itemTemplate[QStringLiteral("nickName")] = displayNickName;
|
||||
itemTemplate[QStringLiteral("cadence_avg")] = avgCadence;
|
||||
itemTemplate[QStringLiteral("cadence_max")] = maxCadence;
|
||||
itemTemplate[QStringLiteral("workoutStartDate")] = workoutStartDateFormatted;
|
||||
|
||||
// Simple optimized loop - no coordinates
|
||||
for (const SessionLine &s : *session) {
|
||||
QJsonObject item = itemTemplate; // Copy template (includes workoutStartDate)
|
||||
|
||||
// Time calculation (optimized)
|
||||
const int totalSeconds = s.elapsedTime;
|
||||
item[QStringLiteral("elapsed_s")] = totalSeconds % 60;
|
||||
item[QStringLiteral("elapsed_m")] = (totalSeconds % 3600) / 60;
|
||||
item[QStringLiteral("elapsed_h")] = totalSeconds / 3600;
|
||||
|
||||
// Variable properties only
|
||||
item[QStringLiteral("speed")] = s.speed;
|
||||
item[QStringLiteral("calories")] = s.calories;
|
||||
item[QStringLiteral("distance")] = s.distance;
|
||||
item[QStringLiteral("heart")] = s.heart;
|
||||
item[QStringLiteral("elevation")] = s.elevationGain;
|
||||
item[QStringLiteral("watts")] = s.watt;
|
||||
|
||||
// Sport-specific properties
|
||||
if (sport == FIT_SPORT_CYCLING) {
|
||||
item[QStringLiteral("cadence")] = s.cadence;
|
||||
item[QStringLiteral("resistance")] = s.resistance;
|
||||
} else if (sport == FIT_SPORT_ROWING) {
|
||||
item[QStringLiteral("cadence")] = s.cadence;
|
||||
item[QStringLiteral("resistance")] = s.resistance;
|
||||
item[QStringLiteral("strokescount")] = static_cast<int>(s.totalStrokes);
|
||||
item[QStringLiteral("strokeslength")] = static_cast<double>(s.avgStrokesLength);
|
||||
} else if (sport == FIT_SPORT_RUNNING || sport == FIT_SPORT_WALKING) {
|
||||
item[QStringLiteral("inclination")] = s.inclination;
|
||||
item[QStringLiteral("stridelength")] = s.instantaneousStrideLengthCM;
|
||||
item[QStringLiteral("groundcontact")] = s.groundContactMS;
|
||||
item[QStringLiteral("verticaloscillation")] = s.verticalOscillationMM;
|
||||
} else if (sport == FIT_SUB_SPORT_ELLIPTICAL) {
|
||||
item[QStringLiteral("inclination")] = s.inclination;
|
||||
}
|
||||
|
||||
previewSessionArray.append(item);
|
||||
processedItems++;
|
||||
|
||||
// Log progress every 1000 items
|
||||
if (processedItems % 1000 == 0) {
|
||||
auto currentTime = std::chrono::high_resolution_clock::now();
|
||||
qDebug() << "Processed" << processedItems << "items, elapsed:"
|
||||
<< std::chrono::duration_cast<std::chrono::milliseconds>(currentTime - afterPreCalc).count() << "ms";
|
||||
}
|
||||
}
|
||||
|
||||
auto endTime = std::chrono::high_resolution_clock::now();
|
||||
auto totalTime = std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime).count();
|
||||
auto loopTime = std::chrono::duration_cast<std::chrono::milliseconds>(endTime - afterPreCalc).count();
|
||||
|
||||
qDebug() << "previewSessionOnChart: Added" << previewSessionArray.size() << "elements to preview array";
|
||||
qDebug() << "Total time:" << totalTime << "ms, Main loop time:" << loopTime << "ms";
|
||||
qDebug() << "Average per item:" << (session->size() > 0 ? (double)loopTime / session->size() : 0) << "ms";
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#ifndef TEMPLATEINFOSENDERBUILDER_H
|
||||
#define TEMPLATEINFOSENDERBUILDER_H
|
||||
#include "fit_profile.hpp"
|
||||
#include "devices/bluetoothdevice.h"
|
||||
#include "templateinfosender.h"
|
||||
#include <QHash>
|
||||
@@ -21,6 +22,9 @@ class TemplateInfoSenderBuilder : public QObject {
|
||||
void stop();
|
||||
QStringList templateIdList() const;
|
||||
~TemplateInfoSenderBuilder();
|
||||
|
||||
void previewSessionOnChart(QList<SessionLine> *session, FIT_SPORT sport);
|
||||
void previewSessionOnChart(QList<SessionLine> *session, FIT_SPORT sport, const QString &workoutName);
|
||||
signals:
|
||||
void activityDescriptionChanged(QString newDescription);
|
||||
void chartSaved(QString filename);
|
||||
@@ -45,11 +49,13 @@ class TemplateInfoSenderBuilder : public QObject {
|
||||
QString activityDescription;
|
||||
void createTemplatesFromFolder(const QString &idInfo, const QString &folder, QStringList &dirTemplates);
|
||||
void clearSessionArray();
|
||||
void clearPreviewSessionArray();
|
||||
bluetoothdevice *device = nullptr;
|
||||
QTimer updateTimer;
|
||||
QString masterId;
|
||||
QStringList foldersToLook;
|
||||
QJsonArray sessionArray;
|
||||
QJsonArray previewSessionArray;
|
||||
QHash<QString, QVariant> context;
|
||||
QJSEngine *engine = nullptr;
|
||||
TemplateInfoSenderBuilder(QObject *parent);
|
||||
@@ -82,6 +88,7 @@ class TemplateInfoSenderBuilder : public QObject {
|
||||
void onGetTrainingProgram(const QJsonValue &msgContent, TemplateInfoSender *tempSender);
|
||||
void onAppendActivityDescription(const QJsonValue &msgContent, TemplateInfoSender *tempSender);
|
||||
void onGetSessionArray(TemplateInfoSender *tempSender);
|
||||
void onGetPreviewSessionArray(TemplateInfoSender *tempSender);
|
||||
void onGetLatLon(TemplateInfoSender *tempSender);
|
||||
void onNextInclination300Meters(TemplateInfoSender *tempSender);
|
||||
void onGetGPXBase64(TemplateInfoSender *tempSender);
|
||||
|
||||
95
src/workoutloaderworker.cpp
Normal file
95
src/workoutloaderworker.cpp
Normal file
@@ -0,0 +1,95 @@
|
||||
// workoutloaderworker.cpp
|
||||
#include "workoutloaderworker.h"
|
||||
#include <QSqlQuery>
|
||||
#include <QSqlError>
|
||||
#include <QSqlDatabase>
|
||||
#include <QDebug>
|
||||
#include <QDateTime>
|
||||
|
||||
WorkoutLoaderWorker::WorkoutLoaderWorker(const QString& dbPath, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_dbPath(dbPath)
|
||||
{
|
||||
}
|
||||
|
||||
void WorkoutLoaderWorker::loadWorkouts() {
|
||||
// Create database connection for this thread
|
||||
{
|
||||
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", FitDatabaseProcessor::DB_CONNECTION_NAME + "_worker");
|
||||
db.setDatabaseName(m_dbPath);
|
||||
if (!db.open()) {
|
||||
qDebug() << "Failed to open database in worker:" << db.lastError().text();
|
||||
emit loadingFinished(QList<QVariantMap>());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
QList<QVariantMap> workouts;
|
||||
QSqlDatabase db = QSqlDatabase::database(FitDatabaseProcessor::DB_CONNECTION_NAME + "_worker");
|
||||
QSqlQuery query(db);
|
||||
query.prepare("SELECT id, sport_type, workout_name, start_time, total_time, "
|
||||
"total_distance, total_calories FROM workouts "
|
||||
"ORDER BY start_time DESC");
|
||||
|
||||
if (!query.exec()) {
|
||||
qDebug() << "Failed to load workouts:" << query.lastError().text();
|
||||
emit loadingFinished(workouts);
|
||||
return;
|
||||
}
|
||||
|
||||
while (query.next()) {
|
||||
QVariantMap workout;
|
||||
workout["id"] = query.value("id");
|
||||
workout["sport"] = query.value("sport_type");
|
||||
|
||||
// Format the date
|
||||
QDateTime dateTime = query.value("start_time").toDateTime();
|
||||
workout["date"] = dateTime.toString("yyyy-MM-dd HH:mm");
|
||||
|
||||
// Convert total_time (seconds) to formatted duration
|
||||
int totalSeconds = query.value("total_time").toInt();
|
||||
int hours = totalSeconds / 3600;
|
||||
int minutes = (totalSeconds % 3600) / 60;
|
||||
int seconds = totalSeconds % 60;
|
||||
workout["duration"] = QString("%1:%2:%3")
|
||||
.arg(hours, 2, 10, QLatin1Char('0'))
|
||||
.arg(minutes, 2, 10, QLatin1Char('0'))
|
||||
.arg(seconds, 2, 10, QLatin1Char('0'));
|
||||
|
||||
workout["distance"] = query.value("total_distance").toDouble();
|
||||
workout["calories"] = query.value("total_calories");
|
||||
|
||||
// Use workout name from database if available, otherwise generate fallback
|
||||
QString workoutName = query.value("workout_name").toString();
|
||||
if (workoutName.isEmpty()) {
|
||||
QString sportName;
|
||||
int sportType = query.value("sport_type").toInt();
|
||||
switch (sportType) {
|
||||
case 1: // FIT_SPORT_RUNNING
|
||||
case 11: // FIT_SPORT_WALKING
|
||||
sportName = "Run"; break;
|
||||
case 2: // FIT_SPORT_CYCLING
|
||||
sportName = "Ride"; break;
|
||||
case 4: // FIT_SPORT_FITNESS_EQUIPMENT (Elliptical)
|
||||
sportName = "Elliptical"; break;
|
||||
case 15: // FIT_SPORT_ROWING
|
||||
sportName = "Row"; break;
|
||||
case 84: // FIT_SPORT_JUMPROPE
|
||||
sportName = "Jump Rope"; break;
|
||||
default: sportName = "Workout"; break;
|
||||
}
|
||||
|
||||
int totalMinutes = query.value("total_time").toInt() / 60;
|
||||
workoutName = QString("%1 minutes %2").arg(totalMinutes).arg(sportName);
|
||||
}
|
||||
workout["title"] = workoutName;
|
||||
|
||||
workouts.append(workout);
|
||||
}
|
||||
|
||||
emit loadingFinished(workouts);
|
||||
}
|
||||
|
||||
void WorkoutLoaderWorker::cleanup() {
|
||||
QSqlDatabase::removeDatabase(FitDatabaseProcessor::DB_CONNECTION_NAME + "_worker");
|
||||
}
|
||||
25
src/workoutloaderworker.h
Normal file
25
src/workoutloaderworker.h
Normal file
@@ -0,0 +1,25 @@
|
||||
// workoutloaderworker.h
|
||||
#ifndef WORKOUTLOADERWORKER_H
|
||||
#define WORKOUTLOADERWORKER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QVariantMap>
|
||||
#include "fitdatabaseprocessor.h"
|
||||
|
||||
class WorkoutLoaderWorker : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit WorkoutLoaderWorker(const QString& dbPath, QObject *parent = nullptr);
|
||||
|
||||
public slots:
|
||||
void loadWorkouts();
|
||||
void cleanup();
|
||||
|
||||
signals:
|
||||
void loadingFinished(const QList<QVariantMap>& workouts);
|
||||
|
||||
private:
|
||||
QString m_dbPath;
|
||||
};
|
||||
|
||||
#endif // WORKOUTLOADERWORKER_H
|
||||
235
src/workoutmodel.cpp
Normal file
235
src/workoutmodel.cpp
Normal file
@@ -0,0 +1,235 @@
|
||||
#include "workoutmodel.h"
|
||||
#include "workoutloaderworker.h"
|
||||
#include <QSqlQuery>
|
||||
#include <QSqlError>
|
||||
#include <QDebug>
|
||||
#include <QDateTime>
|
||||
|
||||
WorkoutModel::WorkoutModel(const QString& dbPath, QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
, m_isLoading(false)
|
||||
, m_isDatabaseProcessing(true)
|
||||
, m_dbPath(dbPath)
|
||||
{
|
||||
// Create main database connection
|
||||
{
|
||||
m_db = QSqlDatabase::addDatabase("QSQLITE", FitDatabaseProcessor::DB_CONNECTION_NAME + "_main");
|
||||
m_db.setDatabaseName(dbPath);
|
||||
if (!m_db.open()) {
|
||||
qDebug() << "Failed to open database in main thread:" << m_db.lastError().text();
|
||||
return;
|
||||
}
|
||||
}
|
||||
m_db = QSqlDatabase::database(FitDatabaseProcessor::DB_CONNECTION_NAME + "_main");
|
||||
|
||||
// Create worker and move to thread
|
||||
m_workerThread = new QThread(this);
|
||||
m_worker = new WorkoutLoaderWorker(dbPath);
|
||||
|
||||
// Connect signals/slots
|
||||
connect(m_workerThread, &QThread::finished, m_worker, &WorkoutLoaderWorker::cleanup);
|
||||
connect(m_workerThread, &QThread::finished, m_worker, &QObject::deleteLater);
|
||||
connect(this, &WorkoutModel::loadWorkoutsRequested, m_worker, &WorkoutLoaderWorker::loadWorkouts);
|
||||
connect(m_worker, &WorkoutLoaderWorker::loadingFinished, this, &WorkoutModel::onWorkoutsLoaded,
|
||||
Qt::QueuedConnection);
|
||||
|
||||
m_worker->moveToThread(m_workerThread);
|
||||
|
||||
m_workerThread->start();
|
||||
|
||||
// Initial load
|
||||
refresh();
|
||||
}
|
||||
|
||||
WorkoutModel::~WorkoutModel() {
|
||||
m_workerThread->quit();
|
||||
m_workerThread->wait();
|
||||
QSqlDatabase::removeDatabase(FitDatabaseProcessor::DB_CONNECTION_NAME + "_main");
|
||||
}
|
||||
|
||||
void WorkoutModel::refresh() {
|
||||
if (m_isLoading) return;
|
||||
|
||||
m_isLoading = true;
|
||||
emit loadingStatusChanged();
|
||||
emit loadWorkoutsRequested();
|
||||
}
|
||||
|
||||
void WorkoutModel::onWorkoutsLoaded(const QList<QVariantMap>& workouts) {
|
||||
beginResetModel();
|
||||
m_workouts = workouts;
|
||||
endResetModel();
|
||||
|
||||
m_isLoading = false;
|
||||
emit loadingStatusChanged();
|
||||
}
|
||||
|
||||
bool WorkoutModel::isLoading() const {
|
||||
return m_isLoading;
|
||||
}
|
||||
|
||||
bool WorkoutModel::isDatabaseProcessing() const {
|
||||
return m_isDatabaseProcessing;
|
||||
}
|
||||
|
||||
void WorkoutModel::setDatabaseProcessing(bool processing) {
|
||||
if (m_isDatabaseProcessing != processing) {
|
||||
m_isDatabaseProcessing = processing;
|
||||
emit databaseProcessingChanged();
|
||||
}
|
||||
}
|
||||
|
||||
bool WorkoutModel::deleteWorkout(int workoutId) {
|
||||
QSqlQuery query(m_db);
|
||||
|
||||
// Get the file path before deleting
|
||||
query.prepare("SELECT file_path FROM workouts WHERE id = ?");
|
||||
query.addBindValue(workoutId);
|
||||
QString filePath;
|
||||
if (query.exec() && query.next()) {
|
||||
filePath = query.value("file_path").toString();
|
||||
}
|
||||
|
||||
// Delete the workout record
|
||||
query.prepare("DELETE FROM workouts WHERE id = ?");
|
||||
query.addBindValue(workoutId);
|
||||
|
||||
if (!query.exec()) {
|
||||
qDebug() << "Failed to delete workout:" << query.lastError().text();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Optionally, you could also delete the FIT file here if desired
|
||||
// if (!filePath.isEmpty()) {
|
||||
// QFile::remove(filePath);
|
||||
// }
|
||||
|
||||
// Refresh the model
|
||||
refresh();
|
||||
return true;
|
||||
}
|
||||
|
||||
QVariantMap WorkoutModel::getWorkoutDetails(int workoutId) {
|
||||
QVariantMap details;
|
||||
|
||||
// First get the summary data from database
|
||||
QSqlQuery query(m_db);
|
||||
query.prepare("SELECT * FROM workouts WHERE id = ?");
|
||||
query.addBindValue(workoutId);
|
||||
|
||||
if (!query.exec() || !query.next()) {
|
||||
qDebug() << "Failed to get workout details:" << query.lastError().text();
|
||||
return details;
|
||||
}
|
||||
|
||||
// Add file path to details
|
||||
details["filePath"] = query.value("file_path"); // Add this line
|
||||
|
||||
// Fill in the summary data
|
||||
details["id"] = query.value("id");
|
||||
details["sport"] = query.value("sport_type");
|
||||
details["startTime"] = query.value("start_time").toDateTime().toString("yyyy-MM-dd HH:mm:ss");
|
||||
details["endTime"] = query.value("end_time").toDateTime().toString("yyyy-MM-dd HH:mm:ss");
|
||||
details["duration"] = query.value("total_time");
|
||||
details["distance"] = query.value("total_distance");
|
||||
details["calories"] = query.value("total_calories");
|
||||
details["avgHeartRate"] = query.value("avg_heart_rate");
|
||||
details["maxHeartRate"] = query.value("max_heart_rate");
|
||||
details["avgCadence"] = query.value("avg_cadence");
|
||||
details["maxCadence"] = query.value("max_cadence");
|
||||
details["avgSpeed"] = query.value("avg_speed");
|
||||
details["maxSpeed"] = query.value("max_speed");
|
||||
details["avgPower"] = query.value("avg_power");
|
||||
details["maxPower"] = query.value("max_power");
|
||||
details["totalAscent"] = query.value("total_ascent");
|
||||
details["totalDescent"] = query.value("total_descent");
|
||||
|
||||
// Now load detailed data from the FIT file for charts
|
||||
QString filePath = query.value("file_path").toString();
|
||||
if (QFile::exists(filePath)) {
|
||||
QList<SessionLine> session;
|
||||
FIT_SPORT sport;
|
||||
qfit::open(filePath, &session, &sport);
|
||||
|
||||
if (!session.isEmpty()) {
|
||||
QVariantList timestamps, heartRates, speeds, power, cadence;
|
||||
|
||||
// Get first timestamp to calculate relative times
|
||||
qint64 startTime = session.first().time.toSecsSinceEpoch();
|
||||
|
||||
for (const SessionLine& point : session) {
|
||||
// Convert elapsed time to minutes for x-axis
|
||||
double minutes = point.elapsedTime / 60.0;
|
||||
timestamps.append(minutes);
|
||||
heartRates.append(point.heart);
|
||||
speeds.append(point.speed);
|
||||
power.append(point.watt);
|
||||
cadence.append(point.cadence);
|
||||
}
|
||||
|
||||
details["chartData"] = QVariantMap{
|
||||
{"timestamps", timestamps},
|
||||
{"heartRates", heartRates},
|
||||
{"speeds", speeds},
|
||||
{"power", power},
|
||||
{"cadence", cadence}
|
||||
};
|
||||
}
|
||||
} else {
|
||||
qDebug() << "FIT file not found:" << filePath;
|
||||
// Return empty chart data if file not found
|
||||
details["chartData"] = QVariantMap{
|
||||
{"timestamps", QVariantList()},
|
||||
{"heartRates", QVariantList()},
|
||||
{"speeds", QVariantList()},
|
||||
{"power", QVariantList()},
|
||||
{"cadence", QVariantList()}
|
||||
};
|
||||
}
|
||||
|
||||
return details;
|
||||
}
|
||||
|
||||
int WorkoutModel::rowCount(const QModelIndex &parent) const {
|
||||
if (parent.isValid())
|
||||
return 0;
|
||||
return m_workouts.count();
|
||||
}
|
||||
|
||||
QVariant WorkoutModel::data(const QModelIndex &index, int role) const {
|
||||
if (!index.isValid() || index.row() >= m_workouts.count())
|
||||
return QVariant();
|
||||
|
||||
const QVariantMap &workout = m_workouts[index.row()];
|
||||
|
||||
switch (role) {
|
||||
case SportRole:
|
||||
return workout["sport"];
|
||||
case TitleRole:
|
||||
return workout["title"];
|
||||
case DateRole:
|
||||
return workout["date"];
|
||||
case DurationRole:
|
||||
return workout["duration"];
|
||||
case DistanceRole:
|
||||
return workout["distance"];
|
||||
case CaloriesRole:
|
||||
return workout["calories"];
|
||||
case IdRole:
|
||||
return workout["id"];
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> WorkoutModel::roleNames() const {
|
||||
QHash<int, QByteArray> roles;
|
||||
roles[SportRole] = "sport";
|
||||
roles[TitleRole] = "title";
|
||||
roles[DateRole] = "date";
|
||||
roles[DurationRole] = "duration";
|
||||
roles[DistanceRole] = "distance";
|
||||
roles[CaloriesRole] = "calories";
|
||||
roles[IdRole] = "id";
|
||||
return roles;
|
||||
}
|
||||
62
src/workoutmodel.h
Normal file
62
src/workoutmodel.h
Normal file
@@ -0,0 +1,62 @@
|
||||
#ifndef WORKOUTMODEL_H
|
||||
#define WORKOUTMODEL_H
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QSqlDatabase>
|
||||
#include <QThread>
|
||||
#include "fitdatabaseprocessor.h"
|
||||
|
||||
class WorkoutLoaderWorker;
|
||||
|
||||
class WorkoutModel : public QAbstractListModel {
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(bool isLoading READ isLoading NOTIFY loadingStatusChanged)
|
||||
Q_PROPERTY(bool isDatabaseProcessing READ isDatabaseProcessing NOTIFY databaseProcessingChanged)
|
||||
|
||||
public:
|
||||
enum WorkoutRoles {
|
||||
SportRole = Qt::UserRole + 1,
|
||||
TitleRole,
|
||||
DateRole,
|
||||
DurationRole,
|
||||
DistanceRole,
|
||||
CaloriesRole,
|
||||
IdRole
|
||||
};
|
||||
|
||||
explicit WorkoutModel(const QString& dbPath, QObject *parent = nullptr);
|
||||
~WorkoutModel();
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
Q_INVOKABLE void refresh();
|
||||
Q_INVOKABLE QVariantMap getWorkoutDetails(int workoutId);
|
||||
Q_INVOKABLE bool deleteWorkout(int workoutId);
|
||||
|
||||
bool isLoading() const;
|
||||
bool isDatabaseProcessing() const;
|
||||
|
||||
public slots:
|
||||
void setDatabaseProcessing(bool processing);
|
||||
|
||||
signals:
|
||||
void loadWorkoutsRequested();
|
||||
void loadingStatusChanged();
|
||||
void databaseProcessingChanged();
|
||||
|
||||
private slots:
|
||||
void onWorkoutsLoaded(const QList<QVariantMap>& workouts);
|
||||
|
||||
private:
|
||||
QList<QVariantMap> m_workouts;
|
||||
QSqlDatabase m_db;
|
||||
QThread* m_workerThread;
|
||||
WorkoutLoaderWorker* m_worker;
|
||||
bool m_isLoading;
|
||||
bool m_isDatabaseProcessing;
|
||||
QString m_dbPath;
|
||||
};
|
||||
|
||||
#endif // WORKOUTMODEL_H
|
||||
@@ -33,7 +33,7 @@ win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../src/release/ -lqdom
|
||||
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../src/debug/ -lqdomyos-zwift
|
||||
else:unix: LIBS += -L$$OUT_PWD/../src/ -lqdomyos-zwift
|
||||
|
||||
INCLUDEPATH += $$PWD/../src $$PWD/../src/devices
|
||||
INCLUDEPATH += $$PWD/../src $$PWD/../src/devices $$PWD/../src/fit-sdk
|
||||
DEPENDPATH += $$PWD/../src $$PWD/../src/devices
|
||||
|
||||
win32-g++:CONFIG(release, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../src/release/libqdomyos-zwift.a
|
||||
|
||||
Reference in New Issue
Block a user