Compare commits

..

49 Commits

Author SHA1 Message Date
Roberto Viola
777ccefc5e Add Android 16 API 36 compatibility with WindowInsetsController
- Create CustomQtActivity extending QtActivity for Android 16 support
- Replace deprecated setSystemUiVisibility with WindowInsetsController
- Maintain backward compatibility with older Android versions
- Fix header toolbar visibility issues on Android 16

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-02 19:50:13 +02:00
Roberto Viola
17a4c838b7 Merge branch 'master' into workout-history 2025-07-02 14:37:45 +02:00
Roberto Viola
9214ab63ad Update project.pbxproj 2025-07-02 11:58:44 +02:00
Roberto Viola
04f919fe86 Merge branch 'master' into workout-history 2025-07-02 11:44:18 +02:00
Roberto Viola
a106438fb0 Update project.pbxproj 2025-07-01 10:50:29 +02:00
Roberto Viola
c336043bb2 Merge branch 'master' into workout-history 2025-07-01 10:39:43 +02:00
Roberto Viola
f70f4717f7 fix build 2025-06-30 13:31:49 +02:00
Roberto Viola
f2da15c477 Merge branch 'master' into workout-history 2025-06-30 13:19:59 +02:00
Roberto Viola
83690df66e Update qdomyos-zwift-tests.pro 2025-06-30 08:23:19 +02:00
Roberto Viola
cafc785420 Update project.pbxproj 2025-06-30 06:45:16 +02:00
Roberto Viola
0b1e6606cb Update qfit.cpp 2025-06-30 06:44:44 +02:00
Roberto Viola
82d9e252ac Update qfit.cpp 2025-06-30 06:33:38 +02:00
Roberto Viola
1a6ef489d0 Update project.pbxproj 2025-06-29 20:33:23 +02:00
Roberto Viola
d6028c33b6 rogue bike fix 2025-06-29 20:31:09 +02:00
Roberto Viola
66554e0a4a Merge branch 'master' into workout-history 2025-06-29 20:24:26 +02:00
Roberto Viola
aa1058f6be optimized! 2025-06-29 19:10:32 +02:00
Roberto Viola
33589f27d3 fixing summary 2025-06-29 13:35:22 +02:00
Roberto Viola
ae1147025e improving 2025-06-29 13:30:39 +02:00
Roberto Viola
189c27eeb0 it kind of works 2025-06-29 12:50:19 +02:00
Roberto Viola
dab74e4bfd claude fixes 2025-06-29 09:23:49 +02:00
Roberto Viola
8267661f70 build fix 2025-06-28 14:47:04 +02:00
Roberto Viola
204897680c build fix 2025-06-28 14:46:14 +02:00
Roberto Viola
b93df0d0c9 fix build 2025-06-28 14:41:45 +02:00
Roberto Viola
038c347cc2 fix build 2025-06-28 14:37:21 +02:00
Roberto Viola
4683084ed2 Merge branch 'master' into workout-history 2025-06-28 14:26:23 +02:00
Roberto Viola
ae131c7cad adding kcal on the summary 2025-01-16 15:34:58 +01:00
Roberto Viola
67f5446fc2 details start to work! 2025-01-16 15:11:20 +01:00
Roberto Viola
4ce8e1b20c preview of the fit file is almost ready 2025-01-16 14:45:21 +01:00
Roberto Viola
57cdf246a9 removed workoutdetails because db would be too heavy. let's open the fit files 2025-01-16 14:03:19 +01:00
Roberto Viola
4cc0b2520b data fixed 2025-01-16 12:57:03 +01:00
Roberto Viola
8f4364f525 kind of works on ios 2025-01-16 12:47:18 +01:00
Roberto Viola
df0ab76187 the workout history works with the db! 2025-01-16 11:45:35 +01:00
Roberto Viola
b7530dcdff Merge branch 'master' into workout-history 2025-01-16 10:26:35 +01:00
Roberto Viola
12dc37191e Merge branch 'master' into workout-history 2025-01-16 10:07:55 +01:00
Roberto Viola
3b0b7e4bd9 adding fit file processor 2025-01-16 08:47:44 +01:00
Roberto Viola
a3e6640e3f fixing 2024-12-05 16:49:41 +01:00
Roberto Viola
27922da186 Merge branch 'master' into workout-history 2024-12-05 16:42:16 +01:00
Roberto Viola
efb3b74149 connection works 2024-05-07 12:24:25 +02:00
Roberto Viola
c7cc127ea0 emitting signal not tested 2024-05-07 12:00:08 +02:00
Roberto Viola
c1dbaf0f05 let's work on build up the list 2024-05-07 11:27:17 +02:00
Roberto Viola
cbbc054ec8 fix build 2024-05-07 10:39:56 +02:00
Roberto Viola
cb0e7a9389 Merge branch 'master' into workout-history 2024-05-07 10:19:00 +02:00
Roberto Viola
7256cb5ef5 added target cadence, watt and resistance to fit file along with user info 2022-07-19 08:11:29 +02:00
Roberto Viola
f731651c71 build fixed 2022-07-18 14:18:32 +02:00
Roberto Viola
38e9987fff sport type added to preview function 2022-07-18 12:24:33 +02:00
Roberto Viola
28c4084780 using a different template for the preview charts 2022-07-16 15:16:18 +02:00
Roberto Viola
02f4154577 workout history works with a bluetooth device connected 2022-07-15 13:47:06 +02:00
Roberto Viola
6e8ef20efd Merge branch 'master' into workout-history 2022-07-15 11:03:55 +02:00
Roberto Viola
7b01867934 preparing form... 2022-07-14 14:30:33 +02:00
37 changed files with 3209 additions and 46 deletions

View File

@@ -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
View 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
View 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()
}
}
}

View File

@@ -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 -->

View File

@@ -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))

View 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();
}

View 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

View File

@@ -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;

View File

@@ -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);

View 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']
}
};

View File

@@ -0,0 +1,3 @@
{
"esversion": 6
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View 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>

View File

@@ -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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View 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 '';
}

File diff suppressed because one or more lines are too long

View 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();

File diff suppressed because one or more lines are too long

View 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]);

View File

@@ -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

View File

@@ -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
);

View File

@@ -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 \

View File

@@ -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);

View File

@@ -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:
};

View File

@@ -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>

View File

@@ -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;

View File

@@ -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());
};

View File

@@ -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";
}

View File

@@ -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);

View 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
View 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
View 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
View 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

View File

@@ -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