incyclist-services 1.7.76 → 1.7.78
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/cjs/activities/list/service.js +1 -1
- package/lib/cjs/activities/ride/service.js +2 -1
- package/lib/cjs/workouts/base/graph/index.js +18 -0
- package/lib/cjs/workouts/base/graph/series.js +205 -0
- package/lib/cjs/workouts/base/graph/types.js +2 -0
- package/lib/cjs/workouts/base/model/index.js +0 -1
- package/lib/cjs/workouts/calendar/service.js +14 -0
- package/lib/cjs/workouts/index.js +2 -0
- package/lib/cjs/workouts/list/cards/index.js +0 -1
- package/lib/cjs/workouts/list/index.js +0 -1
- package/lib/cjs/workouts/list/pageService.js +362 -5
- package/lib/cjs/workouts/list/service.js +39 -12
- package/lib/cjs/workouts/page/index.js +17 -0
- package/lib/cjs/workouts/page/service.js +455 -0
- package/lib/cjs/workouts/page/types.js +2 -0
- package/lib/cjs/workouts/ride/index.js +1 -0
- package/lib/cjs/workouts/ride/page/index.js +18 -0
- package/lib/cjs/workouts/ride/page/service.js +498 -0
- package/lib/cjs/workouts/ride/page/types.js +2 -0
- package/lib/cjs/workouts/ride/service.js +1 -1
- package/lib/cjs/workouts/types.js +2 -0
- package/lib/esm/activities/list/service.js +1 -1
- package/lib/esm/activities/ride/service.js +2 -1
- package/lib/esm/workouts/base/graph/index.js +2 -0
- package/lib/esm/workouts/base/graph/series.js +197 -0
- package/lib/esm/workouts/base/graph/types.js +1 -0
- package/lib/esm/workouts/base/model/index.js +0 -1
- package/lib/esm/workouts/calendar/service.js +14 -0
- package/lib/esm/workouts/index.js +2 -0
- package/lib/esm/workouts/list/cards/index.js +0 -1
- package/lib/esm/workouts/list/index.js +0 -1
- package/lib/esm/workouts/list/pageService.js +362 -5
- package/lib/esm/workouts/list/service.js +39 -12
- package/lib/esm/workouts/page/index.js +1 -0
- package/lib/esm/workouts/page/service.js +451 -0
- package/lib/esm/workouts/page/types.js +1 -0
- package/lib/esm/workouts/ride/index.js +1 -0
- package/lib/esm/workouts/ride/page/index.js +2 -0
- package/lib/esm/workouts/ride/page/service.js +494 -0
- package/lib/esm/workouts/ride/page/types.js +1 -0
- package/lib/esm/workouts/ride/service.js +1 -1
- package/lib/esm/workouts/types.js +1 -0
- package/lib/types/types.d.ts +1 -0
- package/lib/types/workouts/base/graph/index.d.ts +2 -0
- package/lib/types/workouts/base/graph/series.d.ts +18 -0
- package/lib/types/workouts/base/graph/types.d.ts +11 -0
- package/lib/types/workouts/base/model/index.d.ts +0 -1
- package/lib/types/workouts/base/parsers/intervals/parser.d.ts +2 -1
- package/lib/types/workouts/calendar/service.d.ts +1 -0
- package/lib/types/workouts/index.d.ts +2 -0
- package/lib/types/workouts/list/cards/index.d.ts +0 -1
- package/lib/types/workouts/list/index.d.ts +0 -1
- package/lib/types/workouts/list/pageService.d.ts +136 -4
- package/lib/types/workouts/list/service.d.ts +4 -0
- package/lib/types/workouts/list/types.d.ts +1 -1
- package/lib/types/workouts/page/index.d.ts +1 -0
- package/lib/types/workouts/page/service.d.ts +57 -0
- package/lib/types/workouts/page/types.d.ts +95 -0
- package/lib/types/workouts/ride/index.d.ts +1 -0
- package/lib/types/workouts/ride/page/index.d.ts +2 -0
- package/lib/types/workouts/ride/page/service.d.ts +70 -0
- package/lib/types/workouts/ride/page/types.d.ts +71 -0
- package/lib/types/workouts/ride/service.d.ts +2 -1
- package/lib/types/workouts/ride/types.d.ts +3 -0
- package/lib/types/workouts/types.d.ts +8 -0
- package/package.json +1 -1
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { Workout } from "../model";
|
|
2
|
+
export const WORKOUT_ZONE_COLORS = [
|
|
3
|
+
'white',
|
|
4
|
+
'#7f7f7f',
|
|
5
|
+
'#338cff',
|
|
6
|
+
'#59bf59',
|
|
7
|
+
'#ffcc3f',
|
|
8
|
+
'#ff6639',
|
|
9
|
+
'#ff330c',
|
|
10
|
+
'#ea39ff'
|
|
11
|
+
];
|
|
12
|
+
function getMinPower(step, ftp) {
|
|
13
|
+
if (step?.power === undefined)
|
|
14
|
+
return undefined;
|
|
15
|
+
const p = step.power;
|
|
16
|
+
if (p.max !== undefined && p.min !== undefined && p.max === p.min)
|
|
17
|
+
return 0;
|
|
18
|
+
if (p.min === undefined)
|
|
19
|
+
return undefined;
|
|
20
|
+
let res = Math.round(p.min);
|
|
21
|
+
if (p.type === 'watt' && ftp !== undefined)
|
|
22
|
+
res = Math.round(p.min / ftp * 100);
|
|
23
|
+
return res;
|
|
24
|
+
}
|
|
25
|
+
function getMaxPower(step, ftp) {
|
|
26
|
+
if (step?.power === undefined)
|
|
27
|
+
return 0;
|
|
28
|
+
const p = step.power;
|
|
29
|
+
if (p.max === undefined)
|
|
30
|
+
return undefined;
|
|
31
|
+
let res = Math.round(p.max);
|
|
32
|
+
if (p.type === 'watt' && ftp !== undefined)
|
|
33
|
+
res = Math.round(p.max / ftp * 100);
|
|
34
|
+
return res;
|
|
35
|
+
}
|
|
36
|
+
function getZone(power, ftp) {
|
|
37
|
+
if (power === undefined)
|
|
38
|
+
return undefined;
|
|
39
|
+
if (ftp === undefined)
|
|
40
|
+
return 0;
|
|
41
|
+
if (power <= 55)
|
|
42
|
+
return 1;
|
|
43
|
+
if (power <= 75)
|
|
44
|
+
return 2;
|
|
45
|
+
if (power <= 90)
|
|
46
|
+
return 3;
|
|
47
|
+
if (power <= 105)
|
|
48
|
+
return 4;
|
|
49
|
+
if (power <= 120)
|
|
50
|
+
return 5;
|
|
51
|
+
if (power <= 150)
|
|
52
|
+
return 6;
|
|
53
|
+
return 7;
|
|
54
|
+
}
|
|
55
|
+
export function getStepDuration(step) {
|
|
56
|
+
if (step.duration <= 60)
|
|
57
|
+
return `${step.duration}s`;
|
|
58
|
+
if (step.duration <= 3600)
|
|
59
|
+
return `${step.duration / 60}min`;
|
|
60
|
+
return `${step.duration / 3600}h`;
|
|
61
|
+
}
|
|
62
|
+
function formatPowerText(low, high, unit, rangePrefix) {
|
|
63
|
+
if (low !== undefined && high !== undefined && low !== high)
|
|
64
|
+
return `${rangePrefix}${Math.round(low)}-${Math.round(high)}${unit}`;
|
|
65
|
+
if (high !== undefined)
|
|
66
|
+
return `${Math.round(high)}${unit}`;
|
|
67
|
+
return '';
|
|
68
|
+
}
|
|
69
|
+
function appendConvertedSuffix(text, low, high, unit, ftp) {
|
|
70
|
+
if (text === '' || ftp === undefined)
|
|
71
|
+
return text;
|
|
72
|
+
const isPct = unit === '%';
|
|
73
|
+
const convert = (v) => isPct ? Math.round(v / 100 * ftp) : Math.round(v * 100 / ftp);
|
|
74
|
+
const suffixUnit = isPct ? 'W' : '%';
|
|
75
|
+
const range = (low !== undefined && high !== undefined && low !== high)
|
|
76
|
+
? `${convert(low)}-${convert(high)}`
|
|
77
|
+
: `${convert(high)}`;
|
|
78
|
+
return `${text} (${range}${suffixUnit})`;
|
|
79
|
+
}
|
|
80
|
+
export function getStepPower(step, ftp) {
|
|
81
|
+
if (step.power === undefined)
|
|
82
|
+
return '';
|
|
83
|
+
const { min, max, type } = step.power;
|
|
84
|
+
const unit = type === 'watt' ? 'W' : '%';
|
|
85
|
+
const ascending = step.steady || step.cooldown === false;
|
|
86
|
+
const low = ascending ? min : max;
|
|
87
|
+
const high = ascending ? max : min;
|
|
88
|
+
const rangePrefix = step.steady ? '' : 'Ramp ';
|
|
89
|
+
const stepText = formatPowerText(low, high, unit, rangePrefix);
|
|
90
|
+
return appendConvertedSuffix(stepText, low, high, unit, ftp);
|
|
91
|
+
}
|
|
92
|
+
function resolvePowerWatts(power, ftp) {
|
|
93
|
+
if (!power)
|
|
94
|
+
return {};
|
|
95
|
+
const toWatts = (v) => {
|
|
96
|
+
if (v === undefined)
|
|
97
|
+
return undefined;
|
|
98
|
+
if (power.type === 'watt')
|
|
99
|
+
return Math.round(v);
|
|
100
|
+
return ftp === undefined ? undefined : Math.round(v / 100 * ftp);
|
|
101
|
+
};
|
|
102
|
+
return { min: toWatts(power.min), max: toWatts(power.max) };
|
|
103
|
+
}
|
|
104
|
+
function formatRange(low, high, unit, rangePrefix = '') {
|
|
105
|
+
if (low !== undefined && high !== undefined && Math.round(low) !== Math.round(high))
|
|
106
|
+
return `${rangePrefix}${Math.round(low)}-${Math.round(high)}${unit}`;
|
|
107
|
+
const v = high ?? low;
|
|
108
|
+
return v === undefined ? '' : `${Math.round(v)}${unit}`;
|
|
109
|
+
}
|
|
110
|
+
export function getStepTargetText(step, ftp, powerOverrideWatts) {
|
|
111
|
+
const { min: minPower, max: maxPower } = powerOverrideWatts ?? resolvePowerWatts(step.power, ftp);
|
|
112
|
+
const ascending = step.steady || step.cooldown === false;
|
|
113
|
+
const lowPower = ascending ? minPower : maxPower;
|
|
114
|
+
const highPower = ascending ? maxPower : minPower;
|
|
115
|
+
const rangePrefix = step.steady ? '' : 'Ramp ';
|
|
116
|
+
const powerText = formatRange(lowPower, highPower, 'W', rangePrefix);
|
|
117
|
+
const hrText = formatRange(step.hrm?.min, step.hrm?.max, 'HR');
|
|
118
|
+
const cadenceText = formatRange(step.cadence?.min, step.cadence?.max, ' rpm');
|
|
119
|
+
const parts = [powerText, hrText, cadenceText].filter(Boolean);
|
|
120
|
+
return parts.length ? parts.join(' at ') : 'free';
|
|
121
|
+
}
|
|
122
|
+
function coerceToWorkout(workout) {
|
|
123
|
+
if (workout instanceof Workout)
|
|
124
|
+
return workout;
|
|
125
|
+
try {
|
|
126
|
+
return new Workout(workout);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function buildSteadyBar(step, stepStart, ftp, absValues) {
|
|
133
|
+
const x0 = stepStart;
|
|
134
|
+
const x = stepStart + step.getDuration();
|
|
135
|
+
let y = getMaxPower(step, ftp);
|
|
136
|
+
let y0 = getMinPower(step, ftp);
|
|
137
|
+
const zone = getZone(y, ftp);
|
|
138
|
+
if (absValues && ftp) {
|
|
139
|
+
y = ftp * y / 100;
|
|
140
|
+
y0 = ftp * y0 / 100;
|
|
141
|
+
}
|
|
142
|
+
if (y === undefined || Number.isNaN(y))
|
|
143
|
+
return undefined;
|
|
144
|
+
if (y0 !== undefined && !Number.isNaN(y0))
|
|
145
|
+
return { x, x0, y, y0, zone };
|
|
146
|
+
if (y0 === undefined)
|
|
147
|
+
return { x, x0, y, y0: 0, zone };
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
function buildRampBars(wo, step, stepStart, ftp, absValues) {
|
|
151
|
+
const bars = [];
|
|
152
|
+
const stepSize = step.getDuration() / 10;
|
|
153
|
+
for (let i = 0; i < 10; i++) {
|
|
154
|
+
const x0 = stepStart + i * stepSize;
|
|
155
|
+
const x = x0 + stepSize;
|
|
156
|
+
const limit = wo.getLimits(x0 + 0.01);
|
|
157
|
+
let y = getMaxPower(limit, ftp);
|
|
158
|
+
const zone = getZone(y, ftp);
|
|
159
|
+
if (absValues && ftp)
|
|
160
|
+
y = ftp * y / 100;
|
|
161
|
+
if (y !== undefined && !Number.isNaN(y))
|
|
162
|
+
bars.push({ x, x0, y, y0: 0, zone });
|
|
163
|
+
}
|
|
164
|
+
return bars;
|
|
165
|
+
}
|
|
166
|
+
export function getFlattenedSteps(workout) {
|
|
167
|
+
const wo = coerceToWorkout(workout);
|
|
168
|
+
if (!wo)
|
|
169
|
+
return [];
|
|
170
|
+
const result = [];
|
|
171
|
+
let stepStart = 0;
|
|
172
|
+
for (let limits = wo.getLimits(0, true); limits !== undefined; limits = wo.getLimits(stepStart + 0.01, true)) {
|
|
173
|
+
const step = limits.step;
|
|
174
|
+
const duration = step.getDuration();
|
|
175
|
+
result.push({ step, start: stepStart, end: stepStart + duration, duration });
|
|
176
|
+
stepStart += duration;
|
|
177
|
+
}
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
export function getWorkoutGraphSeries(workout, opts) {
|
|
181
|
+
const { ftp, absValues = false } = opts ?? {};
|
|
182
|
+
const wo = coerceToWorkout(workout);
|
|
183
|
+
if (!wo)
|
|
184
|
+
return [];
|
|
185
|
+
const bars = [];
|
|
186
|
+
getFlattenedSteps(wo).forEach(({ step, start: stepStart }) => {
|
|
187
|
+
if (step.steady) {
|
|
188
|
+
const bar = buildSteadyBar(step, stepStart, ftp, absValues);
|
|
189
|
+
if (bar)
|
|
190
|
+
bars.push(bar);
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
bars.push(...buildRampBars(wo, step, stepStart, ftp, absValues));
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
return bars;
|
|
197
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -133,6 +133,20 @@ let WorkoutCalendarService = (() => {
|
|
|
133
133
|
}
|
|
134
134
|
return [];
|
|
135
135
|
}
|
|
136
|
+
getScheduledToday() {
|
|
137
|
+
try {
|
|
138
|
+
if (!this.workouts)
|
|
139
|
+
return undefined;
|
|
140
|
+
const now = new Date();
|
|
141
|
+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
142
|
+
const entry = this.workouts.find(w => w.day?.toLocaleDateString() === today.toLocaleDateString());
|
|
143
|
+
return entry ? this.getScheduledWorkout(entry) : undefined;
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
this.logError(err, 'getScheduledToday');
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
136
150
|
setActive(active) {
|
|
137
151
|
try {
|
|
138
152
|
this.listActive = active;
|
|
@@ -36,6 +36,12 @@ import { Singleton } from "../../base/types";
|
|
|
36
36
|
import { IncyclistPageService } from "../../base/pages";
|
|
37
37
|
import { Injectable } from "../../base/decorators";
|
|
38
38
|
import { useAppState } from "../../appstate";
|
|
39
|
+
import { Observer } from "../../base/types/observer";
|
|
40
|
+
import { useUserSettings } from "../../settings";
|
|
41
|
+
import { useWorkoutCalendar } from "../calendar";
|
|
42
|
+
import { useWorkoutList } from "./service";
|
|
43
|
+
const DEFAULT_IMPORT_GROUP = 'My Workouts';
|
|
44
|
+
const UPCOMING_COLLAPSED_COUNT = 3;
|
|
39
45
|
let WorkoutListPageService = (() => {
|
|
40
46
|
let _classDecorators = [Singleton];
|
|
41
47
|
let _classDescriptor;
|
|
@@ -43,34 +49,66 @@ let WorkoutListPageService = (() => {
|
|
|
43
49
|
let _classThis;
|
|
44
50
|
let _classSuper = IncyclistPageService;
|
|
45
51
|
let _instanceExtraInitializers = [];
|
|
52
|
+
let _getWorkoutList_decorators;
|
|
53
|
+
let _getWorkoutCalendar_decorators;
|
|
54
|
+
let _getUserSettings_decorators;
|
|
46
55
|
let _getAppState_decorators;
|
|
47
56
|
var WorkoutListPageService = class extends _classSuper {
|
|
48
57
|
static { _classThis = this; }
|
|
49
58
|
static {
|
|
50
59
|
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
60
|
+
_getWorkoutList_decorators = [Injectable];
|
|
61
|
+
_getWorkoutCalendar_decorators = [Injectable];
|
|
62
|
+
_getUserSettings_decorators = [Injectable];
|
|
51
63
|
_getAppState_decorators = [Injectable];
|
|
64
|
+
__esDecorate(this, null, _getWorkoutList_decorators, { kind: "method", name: "getWorkoutList", static: false, private: false, access: { has: obj => "getWorkoutList" in obj, get: obj => obj.getWorkoutList }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
65
|
+
__esDecorate(this, null, _getWorkoutCalendar_decorators, { kind: "method", name: "getWorkoutCalendar", static: false, private: false, access: { has: obj => "getWorkoutCalendar" in obj, get: obj => obj.getWorkoutCalendar }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
66
|
+
__esDecorate(this, null, _getUserSettings_decorators, { kind: "method", name: "getUserSettings", static: false, private: false, access: { has: obj => "getUserSettings" in obj, get: obj => obj.getUserSettings }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
52
67
|
__esDecorate(this, null, _getAppState_decorators, { kind: "method", name: "getAppState", static: false, private: false, access: { has: obj => "getAppState" in obj, get: obj => obj.getAppState }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
53
68
|
__esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
|
|
54
69
|
WorkoutListPageService = _classThis = _classDescriptor.value;
|
|
55
70
|
if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
56
71
|
__runInitializers(_classThis, _classExtraInitializers);
|
|
57
72
|
}
|
|
73
|
+
groupFilter = (__runInitializers(this, _instanceExtraInitializers), null);
|
|
74
|
+
importPhase;
|
|
75
|
+
importGroup;
|
|
76
|
+
importingFileName;
|
|
77
|
+
importResult;
|
|
78
|
+
importError;
|
|
79
|
+
listObserver;
|
|
80
|
+
listUpdateHandler = this.emitPageUpdate.bind(this);
|
|
81
|
+
calendarUpdateHandler = this.emitPageUpdate.bind(this);
|
|
58
82
|
constructor() {
|
|
59
83
|
super('WorkoutListPage');
|
|
60
|
-
__runInitializers(this, _instanceExtraInitializers);
|
|
61
84
|
}
|
|
62
85
|
openPage() {
|
|
63
86
|
try {
|
|
64
87
|
super.openPage();
|
|
65
88
|
this.logEvent({ message: 'page shown', page: 'Workouts' });
|
|
66
|
-
|
|
89
|
+
const { observer } = this.getWorkoutList().open();
|
|
90
|
+
this.listObserver = observer;
|
|
91
|
+
this.listObserver?.on('started', this.listUpdateHandler);
|
|
92
|
+
this.listObserver?.on('loading', this.listUpdateHandler);
|
|
93
|
+
this.listObserver?.on('loaded', this.listUpdateHandler);
|
|
94
|
+
this.listObserver?.on('updated', this.listUpdateHandler);
|
|
95
|
+
this.getWorkoutCalendar().on('updated', this.calendarUpdateHandler);
|
|
96
|
+
this.emitPageUpdate();
|
|
67
97
|
}
|
|
68
98
|
catch (err) {
|
|
69
99
|
this.logError(err, 'openPage');
|
|
70
100
|
}
|
|
101
|
+
return this.getPageObserver();
|
|
71
102
|
}
|
|
72
103
|
closePage() {
|
|
73
104
|
try {
|
|
105
|
+
this.getWorkoutList().close();
|
|
106
|
+
this.listObserver?.off('started', this.listUpdateHandler);
|
|
107
|
+
this.listObserver?.off('loading', this.listUpdateHandler);
|
|
108
|
+
this.listObserver?.off('loaded', this.listUpdateHandler);
|
|
109
|
+
this.listObserver?.off('updated', this.listUpdateHandler);
|
|
110
|
+
this.getWorkoutCalendar().off('updated', this.calendarUpdateHandler);
|
|
111
|
+
delete this.listObserver;
|
|
74
112
|
this.logEvent({ message: 'page closed', page: 'Workouts' });
|
|
75
113
|
super.closePage();
|
|
76
114
|
}
|
|
@@ -79,10 +117,329 @@ let WorkoutListPageService = (() => {
|
|
|
79
117
|
}
|
|
80
118
|
}
|
|
81
119
|
getPageDisplayProps() {
|
|
82
|
-
|
|
83
|
-
|
|
120
|
+
try {
|
|
121
|
+
if (!this.getAppState().hasFeature('MOBILE_WORKOUTS')) {
|
|
122
|
+
return { pageType: 'placeholder' };
|
|
123
|
+
}
|
|
124
|
+
const service = this.getWorkoutList();
|
|
125
|
+
const loading = service.isStillLoading();
|
|
126
|
+
const upcoming = this.getUpcomingTrainingProps();
|
|
127
|
+
const allWorkouts = this.getAllWorkoutItems();
|
|
128
|
+
const groups = this.getGroupFilterProps();
|
|
129
|
+
const workouts = groups.selected ? allWorkouts.filter(w => w.group === groups.selected) : allWorkouts;
|
|
130
|
+
const selectedId = service.getSelected()?.id ?? null;
|
|
131
|
+
const isEmpty = allWorkouts.length === 0;
|
|
132
|
+
return { pageType: 'list', loading, upcoming, groups, workouts, selectedId, isEmpty };
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
this.logError(err, 'getPageDisplayProps');
|
|
136
|
+
return {};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
getWorkoutDetailsProps(id) {
|
|
140
|
+
try {
|
|
141
|
+
const card = this.findWorkoutCard(id);
|
|
142
|
+
if (!card)
|
|
143
|
+
return null;
|
|
144
|
+
const workout = card.getData();
|
|
145
|
+
const settingsProps = card.openSettings();
|
|
146
|
+
const isScheduled = card.getCardType() === 'ScheduledWorkout';
|
|
147
|
+
return {
|
|
148
|
+
id,
|
|
149
|
+
title: card.getTitle(),
|
|
150
|
+
description: workout?.description,
|
|
151
|
+
duration: settingsProps.duration,
|
|
152
|
+
workout,
|
|
153
|
+
ftp: settingsProps.settings?.ftp,
|
|
154
|
+
ftpRequired: settingsProps.ftpRequired,
|
|
155
|
+
useErgMode: settingsProps.settings?.useErgMode,
|
|
156
|
+
canStart: settingsProps.canStart,
|
|
157
|
+
canStartWorkoutOnly: settingsProps.canStartWorkoutOnly,
|
|
158
|
+
groups: settingsProps.categories,
|
|
159
|
+
group: settingsProps.category,
|
|
160
|
+
canDelete: card.canDelete(),
|
|
161
|
+
isScheduled,
|
|
162
|
+
date: isScheduled ? settingsProps.date : undefined
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
this.logError(err, 'getWorkoutDetailsProps');
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
getImportDisplayProps() {
|
|
171
|
+
try {
|
|
172
|
+
const phase = this.importPhase ?? 'landing';
|
|
173
|
+
const knownGroups = this.getKnownGroups();
|
|
174
|
+
const group = this.importGroup ?? this.getLastUsedImportGroup();
|
|
175
|
+
const props = { phase, group, knownGroups };
|
|
176
|
+
if (phase === 'importing')
|
|
177
|
+
props.importing = { fileName: this.importingFileName };
|
|
178
|
+
if (phase === 'result')
|
|
179
|
+
props.result = this.importResult;
|
|
180
|
+
if (phase === 'error')
|
|
181
|
+
props.error = this.importError;
|
|
182
|
+
return props;
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
this.logError(err, 'getImportDisplayProps');
|
|
186
|
+
return { phase: 'landing', group: DEFAULT_IMPORT_GROUP, knownGroups: [] };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
onSelectGroup(group) {
|
|
190
|
+
try {
|
|
191
|
+
this.groupFilter = group;
|
|
192
|
+
this.emitPageUpdate();
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
this.logError(err, 'onSelectGroup');
|
|
84
196
|
}
|
|
85
|
-
|
|
197
|
+
}
|
|
198
|
+
onOpenDetails(id) {
|
|
199
|
+
this.logEvent({ message: 'details opened', id });
|
|
200
|
+
}
|
|
201
|
+
onCloseDetails() {
|
|
202
|
+
this.logEvent({ message: 'details closed' });
|
|
203
|
+
}
|
|
204
|
+
onSetFtp(ftp) {
|
|
205
|
+
try {
|
|
206
|
+
const current = this.getWorkoutList().getStartSettings();
|
|
207
|
+
this.getWorkoutList().setStartSettings({ ...current, ftp });
|
|
208
|
+
this.emitPageUpdate();
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
this.logError(err, 'onSetFtp');
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
onSetErgMode(enabled) {
|
|
215
|
+
try {
|
|
216
|
+
const current = this.getWorkoutList().getStartSettings();
|
|
217
|
+
this.getWorkoutList().setStartSettings({ ...current, useErgMode: enabled });
|
|
218
|
+
this.emitPageUpdate();
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
this.logError(err, 'onSetErgMode');
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
onChangeGroup(id, group) {
|
|
225
|
+
try {
|
|
226
|
+
const card = this.findWorkoutCard(id);
|
|
227
|
+
card?.move(group);
|
|
228
|
+
this.emitPageUpdate();
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
this.logError(err, 'onChangeGroup');
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
async onDelete(id) {
|
|
235
|
+
try {
|
|
236
|
+
const observer = this.getWorkoutList().deleteWorkout(id);
|
|
237
|
+
const deleted = await observer.wait();
|
|
238
|
+
this.emitPageUpdate();
|
|
239
|
+
return !!deleted;
|
|
240
|
+
}
|
|
241
|
+
catch (err) {
|
|
242
|
+
this.logError(err, 'onDelete');
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
onStart(id, opts) {
|
|
247
|
+
try {
|
|
248
|
+
const card = this.findWorkoutCard(id);
|
|
249
|
+
if (!card)
|
|
250
|
+
return;
|
|
251
|
+
const settings = this.getWorkoutList().getStartSettings();
|
|
252
|
+
card.select({ ...settings, noRoute: opts?.noRoute });
|
|
253
|
+
this.getPageObserver()?.emit('start-ride', { id, noRoute: opts?.noRoute });
|
|
254
|
+
}
|
|
255
|
+
catch (err) {
|
|
256
|
+
this.logError(err, 'onStart');
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
onMarkForRoute(id) {
|
|
260
|
+
try {
|
|
261
|
+
const card = this.findWorkoutCard(id);
|
|
262
|
+
if (!card)
|
|
263
|
+
return;
|
|
264
|
+
const settings = this.getWorkoutList().getStartSettings();
|
|
265
|
+
card.select({ ...settings, noRoute: false });
|
|
266
|
+
this.emitPageUpdate();
|
|
267
|
+
}
|
|
268
|
+
catch (err) {
|
|
269
|
+
this.logError(err, 'onMarkForRoute');
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
onClearSelection() {
|
|
273
|
+
try {
|
|
274
|
+
this.getWorkoutList().unselect();
|
|
275
|
+
this.emitPageUpdate();
|
|
276
|
+
}
|
|
277
|
+
catch (err) {
|
|
278
|
+
this.logError(err, 'onClearSelection');
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
onImportOpen() {
|
|
282
|
+
try {
|
|
283
|
+
this.importPhase = 'landing';
|
|
284
|
+
this.importGroup = this.getLastUsedImportGroup();
|
|
285
|
+
delete this.importError;
|
|
286
|
+
delete this.importResult;
|
|
287
|
+
this.emitImportUpdate();
|
|
288
|
+
}
|
|
289
|
+
catch (err) {
|
|
290
|
+
this.logError(err, 'onImportOpen');
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
onImportGroupChange(name) {
|
|
294
|
+
try {
|
|
295
|
+
this.importGroup = name;
|
|
296
|
+
this.emitImportUpdate();
|
|
297
|
+
}
|
|
298
|
+
catch (err) {
|
|
299
|
+
this.logError(err, 'onImportGroupChange');
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
onImportFile(file) {
|
|
303
|
+
const observer = new Observer();
|
|
304
|
+
try {
|
|
305
|
+
const group = this.importGroup;
|
|
306
|
+
this.importPhase = 'importing';
|
|
307
|
+
this.importingFileName = file?.name;
|
|
308
|
+
this.emitImportUpdate();
|
|
309
|
+
this.getWorkoutList().import(file, { showImportCards: false })
|
|
310
|
+
.then(([card]) => {
|
|
311
|
+
if (group && group !== DEFAULT_IMPORT_GROUP)
|
|
312
|
+
card.move(group);
|
|
313
|
+
this.persistLastUsedImportGroup(group);
|
|
314
|
+
this.importPhase = 'result';
|
|
315
|
+
this.importResult = { workoutName: card.getTitle(), group };
|
|
316
|
+
observer.emit('success');
|
|
317
|
+
this.emitImportUpdate();
|
|
318
|
+
})
|
|
319
|
+
.catch((err) => {
|
|
320
|
+
this.importPhase = 'error';
|
|
321
|
+
this.importError = err?.message;
|
|
322
|
+
observer.emit('error', err);
|
|
323
|
+
this.emitImportUpdate();
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
catch (err) {
|
|
327
|
+
this.logError(err, 'onImportFile');
|
|
328
|
+
}
|
|
329
|
+
return observer;
|
|
330
|
+
}
|
|
331
|
+
onImportClose() {
|
|
332
|
+
try {
|
|
333
|
+
delete this.importPhase;
|
|
334
|
+
delete this.importingFileName;
|
|
335
|
+
delete this.importResult;
|
|
336
|
+
delete this.importError;
|
|
337
|
+
this.emitImportUpdate();
|
|
338
|
+
}
|
|
339
|
+
catch (err) {
|
|
340
|
+
this.logError(err, 'onImportClose');
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
getUpcomingTrainingProps() {
|
|
344
|
+
const calendar = this.getWorkoutCalendar();
|
|
345
|
+
const scheduled = calendar.getScheduledWorkouts();
|
|
346
|
+
if (!scheduled || scheduled.length === 0)
|
|
347
|
+
return null;
|
|
348
|
+
const todayId = calendar.getScheduledToday()?.id ?? null;
|
|
349
|
+
const selectedWorkout = this.getWorkoutList().getSelected();
|
|
350
|
+
const items = scheduled.map(w => ({
|
|
351
|
+
id: w.id,
|
|
352
|
+
title: w.workout?.name,
|
|
353
|
+
date: w.day,
|
|
354
|
+
duration: this.formatDuration(w.workout?.duration),
|
|
355
|
+
isToday: todayId !== null && w.id === todayId,
|
|
356
|
+
selected: selectedWorkout?.id !== undefined && selectedWorkout.id === w.workout?.id,
|
|
357
|
+
workout: w.workout
|
|
358
|
+
}));
|
|
359
|
+
return { items, collapsedCount: UPCOMING_COLLAPSED_COUNT, todayId };
|
|
360
|
+
}
|
|
361
|
+
getGroupFilterProps() {
|
|
362
|
+
const available = this.getKnownGroups();
|
|
363
|
+
return { available, selected: this.groupFilter };
|
|
364
|
+
}
|
|
365
|
+
getKnownGroups() {
|
|
366
|
+
const lists = this.getWorkoutList().getLists(false) ?? [];
|
|
367
|
+
return lists.filter(l => l.getId() !== 'scheduled').map(l => l.getTitle());
|
|
368
|
+
}
|
|
369
|
+
getAllWorkoutItems() {
|
|
370
|
+
const lists = this.getWorkoutList().getLists(false) ?? [];
|
|
371
|
+
const items = [];
|
|
372
|
+
lists.forEach(list => {
|
|
373
|
+
if (list.getId() === 'scheduled')
|
|
374
|
+
return;
|
|
375
|
+
list.getCards().forEach(card => {
|
|
376
|
+
if (card.getCardType() !== 'Workout')
|
|
377
|
+
return;
|
|
378
|
+
const workoutCard = card;
|
|
379
|
+
const props = workoutCard.getDisplayProperties();
|
|
380
|
+
items.push({
|
|
381
|
+
id: workoutCard.getId(),
|
|
382
|
+
title: props.title,
|
|
383
|
+
group: list.getTitle(),
|
|
384
|
+
duration: props.duration,
|
|
385
|
+
selected: props.selected,
|
|
386
|
+
canDelete: props.canDelete,
|
|
387
|
+
workout: props.workout
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
return items;
|
|
392
|
+
}
|
|
393
|
+
findWorkoutCard(id) {
|
|
394
|
+
const lists = this.getWorkoutList().getLists(false) ?? [];
|
|
395
|
+
for (const list of lists) {
|
|
396
|
+
const card = list.getCards().find(c => c.getId() === id);
|
|
397
|
+
if (card)
|
|
398
|
+
return card;
|
|
399
|
+
}
|
|
400
|
+
return undefined;
|
|
401
|
+
}
|
|
402
|
+
getLastUsedImportGroup() {
|
|
403
|
+
return this.getUserSettings().get('preferences.workouts.lastImportGroup', DEFAULT_IMPORT_GROUP);
|
|
404
|
+
}
|
|
405
|
+
persistLastUsedImportGroup(group) {
|
|
406
|
+
try {
|
|
407
|
+
this.getUserSettings().set('preferences.workouts.lastImportGroup', group);
|
|
408
|
+
}
|
|
409
|
+
catch (err) {
|
|
410
|
+
this.logError(err, 'persistLastUsedImportGroup');
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
formatDuration(duration) {
|
|
414
|
+
if (duration === undefined || duration === null)
|
|
415
|
+
return '';
|
|
416
|
+
if (duration < 120)
|
|
417
|
+
return `${duration.toFixed(0)}s`;
|
|
418
|
+
if (duration % 60 === 0)
|
|
419
|
+
return `${duration / 60}min`;
|
|
420
|
+
const secVal = duration % 60;
|
|
421
|
+
const minVal = (duration - secVal) / 60 % 60;
|
|
422
|
+
const h = Math.floor((duration - secVal - minVal * 60) / 3600);
|
|
423
|
+
const sec = secVal < 10 ? `0${secVal}` : secVal;
|
|
424
|
+
const min = minVal < 10 ? `0${minVal}` : minVal;
|
|
425
|
+
if (h > 0)
|
|
426
|
+
return `${h}:${min}:${sec}`;
|
|
427
|
+
return `${min}:${sec}`;
|
|
428
|
+
}
|
|
429
|
+
emitPageUpdate() {
|
|
430
|
+
this.getPageObserver()?.emit('page-update');
|
|
431
|
+
}
|
|
432
|
+
emitImportUpdate() {
|
|
433
|
+
this.getPageObserver()?.emit('import-update');
|
|
434
|
+
}
|
|
435
|
+
getWorkoutList() {
|
|
436
|
+
return useWorkoutList();
|
|
437
|
+
}
|
|
438
|
+
getWorkoutCalendar() {
|
|
439
|
+
return useWorkoutCalendar();
|
|
440
|
+
}
|
|
441
|
+
getUserSettings() {
|
|
442
|
+
return useUserSettings();
|
|
86
443
|
}
|
|
87
444
|
getAppState() {
|
|
88
445
|
return useAppState();
|