intervalssync 0.9.3__py3-none-any.whl
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.
- intervalssync/__init__.py +3 -0
- intervalssync/_dropbox_app_key.py +7 -0
- intervalssync/_version.py +1 -0
- intervalssync/bryton/__init__.py +1 -0
- intervalssync/bryton/api.py +49 -0
- intervalssync/bryton/core.py +249 -0
- intervalssync/bryton/ddp.py +231 -0
- intervalssync/bryton/exceptions.py +13 -0
- intervalssync/bryton/fit_encode.py +363 -0
- intervalssync/bryton/workout.py +342 -0
- intervalssync/cli/__init__.py +5 -0
- intervalssync/cli/config.py +53 -0
- intervalssync/cli/env.py +189 -0
- intervalssync/cli/main.py +637 -0
- intervalssync/dropbox_client.py +143 -0
- intervalssync/gui/__init__.py +1 -0
- intervalssync/gui/app.py +352 -0
- intervalssync/gui/config.py +102 -0
- intervalssync/gui/profile_sync_ui.py +225 -0
- intervalssync/gui/secrets.py +135 -0
- intervalssync/gui/settings_view.py +716 -0
- intervalssync/gui/support_gamification.py +658 -0
- intervalssync/gui/sync_view.py +486 -0
- intervalssync/gui/system.py +21 -0
- intervalssync/gui/theme.py +216 -0
- intervalssync/gui/update_check.py +55 -0
- intervalssync/igpsport/__init__.py +1 -0
- intervalssync/igpsport/core.py +520 -0
- intervalssync/igpsport/interval_info.py +269 -0
- intervalssync/igpsport/profile_sync.py +391 -0
- intervalssync/igpsport/region.py +80 -0
- intervalssync/igpsport/workout.py +503 -0
- intervalssync/igpsport/zone_map.py +143 -0
- intervalssync/intervals_icu.py +201 -0
- intervalssync-0.9.3.dist-info/METADATA +179 -0
- intervalssync-0.9.3.dist-info/RECORD +39 -0
- intervalssync-0.9.3.dist-info/WHEEL +4 -0
- intervalssync-0.9.3.dist-info/entry_points.txt +5 -0
- intervalssync-0.9.3.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
"""Sync view: one-click sync with a progress bar, live log and activity list."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import flet as ft
|
|
8
|
+
|
|
9
|
+
from . import config as config_module
|
|
10
|
+
from . import secrets as secrets_module
|
|
11
|
+
from ..bryton.core import SyncConfig as BrytonSyncConfig, sync as bryton_sync
|
|
12
|
+
from ..bryton.exceptions import BrytonSyncError
|
|
13
|
+
from ..bryton.workout import (
|
|
14
|
+
BrytonWorkoutUploadConfig,
|
|
15
|
+
apply_uploaded_bryton_workout_map,
|
|
16
|
+
upload_workouts as bryton_upload_workouts,
|
|
17
|
+
)
|
|
18
|
+
from ..igpsport.core import SyncConfig as IgpSyncConfig, SyncError, sync as igpsport_sync
|
|
19
|
+
from ..dropbox_client import get_dropbox_app_key
|
|
20
|
+
from ..igpsport.workout import WorkoutUploadConfig, apply_uploaded_workout_map, upload_workouts
|
|
21
|
+
from . import support_gamification
|
|
22
|
+
from . import theme
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def build_sync_view(
|
|
26
|
+
page: ft.Page,
|
|
27
|
+
config: config_module.AppConfig,
|
|
28
|
+
store: secrets_module.SecretStore,
|
|
29
|
+
) -> ft.Control:
|
|
30
|
+
colors = theme.palette(page)
|
|
31
|
+
mobile = theme.is_mobile(page)
|
|
32
|
+
progress = ft.ProgressBar(
|
|
33
|
+
visible=False,
|
|
34
|
+
color=colors["accent"],
|
|
35
|
+
bgcolor=colors["surface_alt"],
|
|
36
|
+
bar_height=3,
|
|
37
|
+
border_radius=2,
|
|
38
|
+
)
|
|
39
|
+
log = ft.ListView(spacing=4, auto_scroll=True, height=280, expand=False)
|
|
40
|
+
stats_refs = support_gamification.StatsCardRefs(
|
|
41
|
+
number=ft.Ref[ft.Text](),
|
|
42
|
+
rank=ft.Ref[ft.Text](),
|
|
43
|
+
breakdown=ft.Ref[ft.Text](),
|
|
44
|
+
target=ft.Ref[ft.Text](),
|
|
45
|
+
remaining=ft.Ref[ft.Text](),
|
|
46
|
+
progress_bar=ft.Ref[ft.ProgressBar](),
|
|
47
|
+
)
|
|
48
|
+
stats_card = support_gamification.build_stats_card(page, config, stats_refs)
|
|
49
|
+
|
|
50
|
+
def _action_button(label: str, icon: str, *, outlined: bool = False) -> ft.FilledButton | ft.OutlinedButton:
|
|
51
|
+
label_size = 13 if mobile else 14
|
|
52
|
+
content = ft.Row(
|
|
53
|
+
spacing=theme.SPACE_XS,
|
|
54
|
+
alignment=ft.MainAxisAlignment.CENTER,
|
|
55
|
+
controls=[
|
|
56
|
+
ft.Icon(icon, size=16 if mobile else 18),
|
|
57
|
+
ft.Text(
|
|
58
|
+
label,
|
|
59
|
+
size=label_size,
|
|
60
|
+
font_family=f"{theme.FONT_BODY}Medium",
|
|
61
|
+
overflow=ft.TextOverflow.ELLIPSIS,
|
|
62
|
+
max_lines=1,
|
|
63
|
+
expand=True,
|
|
64
|
+
text_align=ft.TextAlign.CENTER,
|
|
65
|
+
),
|
|
66
|
+
],
|
|
67
|
+
)
|
|
68
|
+
pad = theme.SPACE_SM if mobile else theme.SPACE_MD
|
|
69
|
+
style = ft.ButtonStyle(
|
|
70
|
+
shape=ft.RoundedRectangleBorder(radius=theme.RADIUS_SM),
|
|
71
|
+
padding=ft.Padding(pad, theme.SPACE_SM, pad, theme.SPACE_SM),
|
|
72
|
+
)
|
|
73
|
+
if outlined:
|
|
74
|
+
style = ft.ButtonStyle(
|
|
75
|
+
shape=style.shape,
|
|
76
|
+
padding=style.padding,
|
|
77
|
+
side=ft.BorderSide(1, colors["border"]),
|
|
78
|
+
color=colors["text"],
|
|
79
|
+
)
|
|
80
|
+
button: ft.FilledButton | ft.OutlinedButton = ft.OutlinedButton(content=content, style=style)
|
|
81
|
+
else:
|
|
82
|
+
button = ft.FilledButton(content=content, style=style)
|
|
83
|
+
button.expand = True
|
|
84
|
+
return button
|
|
85
|
+
|
|
86
|
+
def _source_card(title: str, sync_btn: ft.Control, upload_btn: ft.Control) -> ft.Container:
|
|
87
|
+
return ft.Container(
|
|
88
|
+
content=ft.Column(
|
|
89
|
+
spacing=theme.SPACE_MD,
|
|
90
|
+
controls=[
|
|
91
|
+
ft.Text(
|
|
92
|
+
title,
|
|
93
|
+
size=15,
|
|
94
|
+
weight=ft.FontWeight.W_600,
|
|
95
|
+
font_family=f"{theme.FONT_BODY}Medium",
|
|
96
|
+
color=colors["text"],
|
|
97
|
+
),
|
|
98
|
+
ft.Column(
|
|
99
|
+
spacing=theme.SPACE_SM,
|
|
100
|
+
controls=[
|
|
101
|
+
ft.Row(spacing=0, controls=[sync_btn]),
|
|
102
|
+
ft.Row(spacing=0, controls=[upload_btn]),
|
|
103
|
+
],
|
|
104
|
+
),
|
|
105
|
+
],
|
|
106
|
+
),
|
|
107
|
+
padding=theme.SPACE_LG,
|
|
108
|
+
bgcolor=colors["surface"],
|
|
109
|
+
border=ft.Border.all(1, colors["border"]),
|
|
110
|
+
border_radius=theme.RADIUS_MD,
|
|
111
|
+
expand=True,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
sync_igp_button = _action_button("Sync activities", ft.Icons.SYNC)
|
|
115
|
+
sync_bryton_button = _action_button("Sync activities", ft.Icons.SYNC)
|
|
116
|
+
upload_igp_workouts_button = _action_button(
|
|
117
|
+
"Upload workouts", ft.Icons.FITNESS_CENTER_OUTLINED, outlined=True
|
|
118
|
+
)
|
|
119
|
+
upload_bryton_workouts_button = _action_button(
|
|
120
|
+
"Upload workouts", ft.Icons.FITNESS_CENTER_OUTLINED, outlined=True
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
action_cards: list[ft.Container] = []
|
|
124
|
+
if config.enable_igpsport:
|
|
125
|
+
action_cards.append(_source_card("iGPSPORT", sync_igp_button, upload_igp_workouts_button))
|
|
126
|
+
if config.enable_bryton:
|
|
127
|
+
action_cards.append(_source_card("Bryton", sync_bryton_button, upload_bryton_workouts_button))
|
|
128
|
+
|
|
129
|
+
if len(action_cards) == 1:
|
|
130
|
+
action_area = ft.Row(
|
|
131
|
+
alignment=ft.MainAxisAlignment.CENTER,
|
|
132
|
+
controls=[ft.Container(content=action_cards[0], width=340)],
|
|
133
|
+
)
|
|
134
|
+
elif action_cards:
|
|
135
|
+
if mobile:
|
|
136
|
+
action_area = ft.Column(spacing=theme.SPACE_MD, controls=action_cards)
|
|
137
|
+
else:
|
|
138
|
+
action_area = ft.Row(spacing=theme.SPACE_MD, controls=action_cards)
|
|
139
|
+
else:
|
|
140
|
+
action_area = theme.muted_text("Enable a source in Settings to sync.", page)
|
|
141
|
+
|
|
142
|
+
action_buttons: list[ft.FilledButton | ft.OutlinedButton] = []
|
|
143
|
+
if config.enable_igpsport:
|
|
144
|
+
action_buttons.extend([sync_igp_button, upload_igp_workouts_button])
|
|
145
|
+
if config.enable_bryton:
|
|
146
|
+
action_buttons.extend([sync_bryton_button, upload_bryton_workouts_button])
|
|
147
|
+
|
|
148
|
+
def set_buttons_enabled(enabled: bool) -> None:
|
|
149
|
+
for button in action_buttons:
|
|
150
|
+
button.disabled = not enabled
|
|
151
|
+
|
|
152
|
+
def append_log(message: str) -> None:
|
|
153
|
+
line_color = colors["text"]
|
|
154
|
+
if message.startswith("✗"):
|
|
155
|
+
line_color = ft.Colors.RED_400
|
|
156
|
+
elif message.startswith("\nDone"):
|
|
157
|
+
line_color = colors["accent"]
|
|
158
|
+
log.controls.append(
|
|
159
|
+
ft.Text(
|
|
160
|
+
message,
|
|
161
|
+
size=12,
|
|
162
|
+
selectable=True,
|
|
163
|
+
font_family="Courier New",
|
|
164
|
+
color=line_color,
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
page.update()
|
|
168
|
+
|
|
169
|
+
def _on_sync_complete(*, activities: int = 0, workouts: int = 0) -> None:
|
|
170
|
+
milestone = support_gamification.record_uploads(
|
|
171
|
+
config,
|
|
172
|
+
activities=activities,
|
|
173
|
+
workouts=workouts,
|
|
174
|
+
)
|
|
175
|
+
support_gamification.update_stats_display(page, config, stats_refs)
|
|
176
|
+
if milestone is not None:
|
|
177
|
+
|
|
178
|
+
async def _celebrate() -> None:
|
|
179
|
+
await support_gamification.show_milestone_dialog(page, milestone)
|
|
180
|
+
|
|
181
|
+
page.run_task(_celebrate)
|
|
182
|
+
|
|
183
|
+
def run_igpsport_sync(
|
|
184
|
+
igp_password: str,
|
|
185
|
+
api_key: str | None,
|
|
186
|
+
dropbox_refresh_token: str | None,
|
|
187
|
+
dropbox_app_key: str | None,
|
|
188
|
+
) -> None:
|
|
189
|
+
sync_config = IgpSyncConfig(
|
|
190
|
+
igp_user=config.igp_user,
|
|
191
|
+
igp_password=igp_password,
|
|
192
|
+
igp_region=config.igp_region,
|
|
193
|
+
intervals_api_key=api_key,
|
|
194
|
+
dropbox_refresh_token=dropbox_refresh_token,
|
|
195
|
+
dropbox_app_key=dropbox_app_key,
|
|
196
|
+
max_activities=config.max_activities,
|
|
197
|
+
download_dir=config.download_dir,
|
|
198
|
+
delete_after_upload=config.delete_after_upload,
|
|
199
|
+
force_resync=config.force_resync,
|
|
200
|
+
activity_type=config.activity_type,
|
|
201
|
+
list_activities=config.step_list_activities,
|
|
202
|
+
get_download_url=config.step_get_download_url,
|
|
203
|
+
download_fit=config.step_download_fit,
|
|
204
|
+
upload_intervals=config.step_upload_intervals,
|
|
205
|
+
upload_dropbox=config.upload_dropbox,
|
|
206
|
+
dropbox_folder=config.dropbox_folder,
|
|
207
|
+
dropbox_date_filenames=config.dropbox_date_filenames,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
uploaded_count = 0
|
|
211
|
+
try:
|
|
212
|
+
result = igpsport_sync(sync_config, progress=append_log)
|
|
213
|
+
uploaded_count = result.uploaded
|
|
214
|
+
append_log(
|
|
215
|
+
f"\nDone — intervals uploaded {result.uploaded}, "
|
|
216
|
+
f"Dropbox uploaded {result.uploaded_dropbox}, "
|
|
217
|
+
f"downloaded {result.downloaded}, "
|
|
218
|
+
f"skipped {result.skipped}, Dropbox skipped {result.skipped_dropbox}, "
|
|
219
|
+
f"failed {result.failed}, Dropbox failed {result.failed_dropbox}."
|
|
220
|
+
)
|
|
221
|
+
except SyncError as exc:
|
|
222
|
+
append_log(f"✗ {exc}")
|
|
223
|
+
except Exception as exc: # noqa: BLE001 — surface any failure to the user
|
|
224
|
+
append_log(f"✗ Unexpected error: {exc}")
|
|
225
|
+
finally:
|
|
226
|
+
if uploaded_count > 0:
|
|
227
|
+
_on_sync_complete(activities=uploaded_count)
|
|
228
|
+
progress.visible = False
|
|
229
|
+
set_buttons_enabled(True)
|
|
230
|
+
page.update()
|
|
231
|
+
|
|
232
|
+
def run_bryton_sync(
|
|
233
|
+
bryton_password: str,
|
|
234
|
+
api_key: str | None,
|
|
235
|
+
dropbox_refresh_token: str | None,
|
|
236
|
+
dropbox_app_key: str | None,
|
|
237
|
+
) -> None:
|
|
238
|
+
sync_config = BrytonSyncConfig(
|
|
239
|
+
bryton_email=config.bryton_user,
|
|
240
|
+
bryton_password=bryton_password,
|
|
241
|
+
intervals_api_key=api_key,
|
|
242
|
+
dropbox_refresh_token=dropbox_refresh_token,
|
|
243
|
+
dropbox_app_key=dropbox_app_key,
|
|
244
|
+
max_activities=config.max_activities,
|
|
245
|
+
download_dir=Path(config.download_dir),
|
|
246
|
+
delete_after_upload=config.delete_after_upload,
|
|
247
|
+
force_resync=config.force_resync,
|
|
248
|
+
activity_type=config.activity_type,
|
|
249
|
+
list_activities=config.step_list_activities,
|
|
250
|
+
download_fit=config.step_download_fit,
|
|
251
|
+
upload_intervals=config.step_upload_intervals,
|
|
252
|
+
upload_dropbox=config.upload_dropbox,
|
|
253
|
+
dropbox_folder=config.dropbox_folder,
|
|
254
|
+
dropbox_date_filenames=config.dropbox_date_filenames,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
uploaded_count = 0
|
|
258
|
+
try:
|
|
259
|
+
result = bryton_sync(sync_config, progress=append_log)
|
|
260
|
+
uploaded_count = result.uploaded
|
|
261
|
+
append_log(
|
|
262
|
+
f"\nDone — intervals uploaded {result.uploaded}, "
|
|
263
|
+
f"Dropbox uploaded {result.uploaded_dropbox}, "
|
|
264
|
+
f"downloaded {result.downloaded}, "
|
|
265
|
+
f"skipped {result.skipped}, Dropbox skipped {result.skipped_dropbox}, "
|
|
266
|
+
f"failed {result.failed}, Dropbox failed {result.failed_dropbox}."
|
|
267
|
+
)
|
|
268
|
+
except BrytonSyncError as exc:
|
|
269
|
+
append_log(f"✗ {exc}")
|
|
270
|
+
except Exception as exc: # noqa: BLE001 — surface any failure to the user
|
|
271
|
+
append_log(f"✗ Unexpected error: {exc}")
|
|
272
|
+
finally:
|
|
273
|
+
if uploaded_count > 0:
|
|
274
|
+
_on_sync_complete(activities=uploaded_count)
|
|
275
|
+
progress.visible = False
|
|
276
|
+
set_buttons_enabled(True)
|
|
277
|
+
page.update()
|
|
278
|
+
|
|
279
|
+
def run_upload_igp_workouts(igp_password: str, api_key: str) -> None:
|
|
280
|
+
upload_config = WorkoutUploadConfig(
|
|
281
|
+
igp_user=config.igp_user,
|
|
282
|
+
igp_password=igp_password,
|
|
283
|
+
igp_region=config.igp_region,
|
|
284
|
+
intervals_api_key=api_key,
|
|
285
|
+
workout_days_ahead=config.workout_days_ahead,
|
|
286
|
+
uploaded_workouts=dict(config.uploaded_workouts),
|
|
287
|
+
force_resync=config.force_resync,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
uploaded_count = 0
|
|
291
|
+
try:
|
|
292
|
+
result = upload_workouts(upload_config, progress=append_log)
|
|
293
|
+
if result.uploaded_map or result.pruned_keys:
|
|
294
|
+
apply_uploaded_workout_map(config.uploaded_workouts, result)
|
|
295
|
+
config_module.save(config)
|
|
296
|
+
uploaded_count = result.uploaded
|
|
297
|
+
append_log(
|
|
298
|
+
f"\nDone — uploaded {result.uploaded}, "
|
|
299
|
+
f"skipped {result.skipped}, no steps {result.no_steps}, "
|
|
300
|
+
f"failed {result.failed}."
|
|
301
|
+
)
|
|
302
|
+
except SyncError as exc:
|
|
303
|
+
append_log(f"✗ {exc}")
|
|
304
|
+
except Exception as exc: # noqa: BLE001 — surface any failure to the user
|
|
305
|
+
append_log(f"✗ Unexpected error: {exc}")
|
|
306
|
+
finally:
|
|
307
|
+
if uploaded_count > 0:
|
|
308
|
+
_on_sync_complete(workouts=uploaded_count)
|
|
309
|
+
progress.visible = False
|
|
310
|
+
set_buttons_enabled(True)
|
|
311
|
+
page.update()
|
|
312
|
+
|
|
313
|
+
def run_upload_bryton_workouts(bryton_password: str, api_key: str) -> None:
|
|
314
|
+
upload_config = BrytonWorkoutUploadConfig(
|
|
315
|
+
bryton_email=config.bryton_user,
|
|
316
|
+
bryton_password=bryton_password,
|
|
317
|
+
intervals_api_key=api_key,
|
|
318
|
+
workout_days_ahead=config.workout_days_ahead,
|
|
319
|
+
uploaded_workouts=dict(config.uploaded_bryton_workouts),
|
|
320
|
+
force_resync=config.force_resync,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
uploaded_count = 0
|
|
324
|
+
try:
|
|
325
|
+
result = bryton_upload_workouts(upload_config, progress=append_log)
|
|
326
|
+
if result.uploaded_map or result.pruned_keys:
|
|
327
|
+
apply_uploaded_bryton_workout_map(config.uploaded_bryton_workouts, result)
|
|
328
|
+
config_module.save(config)
|
|
329
|
+
uploaded_count = result.uploaded
|
|
330
|
+
append_log(
|
|
331
|
+
f"\nDone — uploaded {result.uploaded}, "
|
|
332
|
+
f"skipped {result.skipped}, no steps {result.no_steps}, "
|
|
333
|
+
f"failed {result.failed}."
|
|
334
|
+
)
|
|
335
|
+
except BrytonSyncError as exc:
|
|
336
|
+
append_log(f"✗ {exc}")
|
|
337
|
+
except Exception as exc: # noqa: BLE001 — surface any failure to the user
|
|
338
|
+
append_log(f"✗ Unexpected error: {exc}")
|
|
339
|
+
finally:
|
|
340
|
+
if uploaded_count > 0:
|
|
341
|
+
_on_sync_complete(workouts=uploaded_count)
|
|
342
|
+
progress.visible = False
|
|
343
|
+
set_buttons_enabled(True)
|
|
344
|
+
page.update()
|
|
345
|
+
|
|
346
|
+
async def on_sync_igp_click(_: ft.ControlEvent) -> None:
|
|
347
|
+
igp_password = await store.get(secrets_module.IGP_PASSWORD)
|
|
348
|
+
if not config.igp_user or not igp_password:
|
|
349
|
+
page.show_dialog(ft.SnackBar(ft.Text("Add your iGPSPORT credentials in Settings first.")))
|
|
350
|
+
return
|
|
351
|
+
api_key = await store.get(secrets_module.INTERVALS_API_KEY)
|
|
352
|
+
dropbox_refresh_token = await store.get(secrets_module.DROPBOX_REFRESH_TOKEN)
|
|
353
|
+
dropbox_app_key = get_dropbox_app_key()
|
|
354
|
+
|
|
355
|
+
log.controls.clear()
|
|
356
|
+
progress.visible = True
|
|
357
|
+
set_buttons_enabled(False)
|
|
358
|
+
page.update()
|
|
359
|
+
page.run_thread(
|
|
360
|
+
run_igpsport_sync,
|
|
361
|
+
igp_password,
|
|
362
|
+
api_key,
|
|
363
|
+
dropbox_refresh_token,
|
|
364
|
+
dropbox_app_key,
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
async def on_sync_bryton_click(_: ft.ControlEvent) -> None:
|
|
368
|
+
bryton_password = await store.get(secrets_module.BRYTON_PASSWORD)
|
|
369
|
+
if not config.bryton_user or not bryton_password:
|
|
370
|
+
page.show_dialog(ft.SnackBar(ft.Text("Add your Bryton credentials in Settings first.")))
|
|
371
|
+
return
|
|
372
|
+
api_key = await store.get(secrets_module.INTERVALS_API_KEY)
|
|
373
|
+
dropbox_refresh_token = await store.get(secrets_module.DROPBOX_REFRESH_TOKEN)
|
|
374
|
+
dropbox_app_key = get_dropbox_app_key()
|
|
375
|
+
|
|
376
|
+
log.controls.clear()
|
|
377
|
+
progress.visible = True
|
|
378
|
+
set_buttons_enabled(False)
|
|
379
|
+
page.update()
|
|
380
|
+
page.run_thread(
|
|
381
|
+
run_bryton_sync,
|
|
382
|
+
bryton_password,
|
|
383
|
+
api_key,
|
|
384
|
+
dropbox_refresh_token,
|
|
385
|
+
dropbox_app_key,
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
async def on_upload_igp_workouts_click(_: ft.ControlEvent) -> None:
|
|
389
|
+
igp_password = await store.get(secrets_module.IGP_PASSWORD)
|
|
390
|
+
if not config.igp_user or not igp_password:
|
|
391
|
+
page.show_dialog(ft.SnackBar(ft.Text("Add your iGPSPORT credentials in Settings first.")))
|
|
392
|
+
return
|
|
393
|
+
api_key = await store.get(secrets_module.INTERVALS_API_KEY)
|
|
394
|
+
if not api_key:
|
|
395
|
+
page.show_dialog(
|
|
396
|
+
ft.SnackBar(ft.Text("Add your intervals.icu API key in Settings first."))
|
|
397
|
+
)
|
|
398
|
+
return
|
|
399
|
+
|
|
400
|
+
log.controls.clear()
|
|
401
|
+
progress.visible = True
|
|
402
|
+
set_buttons_enabled(False)
|
|
403
|
+
page.update()
|
|
404
|
+
page.run_thread(run_upload_igp_workouts, igp_password, api_key)
|
|
405
|
+
|
|
406
|
+
async def on_upload_bryton_workouts_click(_: ft.ControlEvent) -> None:
|
|
407
|
+
bryton_password = await store.get(secrets_module.BRYTON_PASSWORD)
|
|
408
|
+
if not config.bryton_user or not bryton_password:
|
|
409
|
+
page.show_dialog(ft.SnackBar(ft.Text("Add your Bryton credentials in Settings first.")))
|
|
410
|
+
return
|
|
411
|
+
api_key = await store.get(secrets_module.INTERVALS_API_KEY)
|
|
412
|
+
if not api_key:
|
|
413
|
+
page.show_dialog(
|
|
414
|
+
ft.SnackBar(ft.Text("Add your intervals.icu API key in Settings first."))
|
|
415
|
+
)
|
|
416
|
+
return
|
|
417
|
+
|
|
418
|
+
log.controls.clear()
|
|
419
|
+
progress.visible = True
|
|
420
|
+
set_buttons_enabled(False)
|
|
421
|
+
page.update()
|
|
422
|
+
page.run_thread(run_upload_bryton_workouts, bryton_password, api_key)
|
|
423
|
+
|
|
424
|
+
sync_igp_button.on_click = on_sync_igp_click
|
|
425
|
+
sync_bryton_button.on_click = on_sync_bryton_click
|
|
426
|
+
upload_igp_workouts_button.on_click = on_upload_igp_workouts_click
|
|
427
|
+
upload_bryton_workouts_button.on_click = on_upload_bryton_workouts_click
|
|
428
|
+
|
|
429
|
+
log_panel = ft.Container(
|
|
430
|
+
content=ft.Column(
|
|
431
|
+
spacing=theme.SPACE_SM,
|
|
432
|
+
controls=[
|
|
433
|
+
ft.Row(
|
|
434
|
+
alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
|
|
435
|
+
controls=[
|
|
436
|
+
theme.section_label("Activity log", page),
|
|
437
|
+
ft.Text(
|
|
438
|
+
"live",
|
|
439
|
+
size=10,
|
|
440
|
+
color=colors["accent"],
|
|
441
|
+
font_family=f"{theme.FONT_BODY}Medium",
|
|
442
|
+
),
|
|
443
|
+
],
|
|
444
|
+
),
|
|
445
|
+
ft.Container(
|
|
446
|
+
content=log,
|
|
447
|
+
expand=True,
|
|
448
|
+
padding=theme.SPACE_MD,
|
|
449
|
+
bgcolor=colors["surface_alt"],
|
|
450
|
+
border_radius=theme.RADIUS_SM,
|
|
451
|
+
border=ft.Border(left=ft.BorderSide(3, colors["accent"])),
|
|
452
|
+
),
|
|
453
|
+
],
|
|
454
|
+
),
|
|
455
|
+
padding=theme.SPACE_LG,
|
|
456
|
+
bgcolor=colors["surface"],
|
|
457
|
+
border=ft.Border.all(1, colors["border"]),
|
|
458
|
+
border_radius=theme.RADIUS_MD,
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
return ft.Column(
|
|
462
|
+
spacing=theme.SPACE_LG,
|
|
463
|
+
controls=[
|
|
464
|
+
ft.Column(
|
|
465
|
+
spacing=theme.SPACE_SM,
|
|
466
|
+
controls=[
|
|
467
|
+
theme.display_text("Your rides, synced", size=26, color=colors["text"]),
|
|
468
|
+
theme.muted_text(
|
|
469
|
+
"Pull recent activities from iGPSPORT or Bryton into intervals.icu, "
|
|
470
|
+
"or push planned workouts the other way.",
|
|
471
|
+
page,
|
|
472
|
+
),
|
|
473
|
+
],
|
|
474
|
+
),
|
|
475
|
+
action_area,
|
|
476
|
+
progress,
|
|
477
|
+
log_panel,
|
|
478
|
+
stats_card,
|
|
479
|
+
*(
|
|
480
|
+
[support_gamification.build_dev_milestone_panel(page, config, stats_refs)]
|
|
481
|
+
if support_gamification.dev_mode_enabled()
|
|
482
|
+
else []
|
|
483
|
+
),
|
|
484
|
+
ft.Container(height=theme.SPACE_MD),
|
|
485
|
+
],
|
|
486
|
+
)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Small OS integration helpers for the desktop GUI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def open_folder(path: str | Path) -> None:
|
|
12
|
+
"""Open `path` in the OS file manager, creating it first if needed."""
|
|
13
|
+
folder = Path(path)
|
|
14
|
+
folder.mkdir(parents=True, exist_ok=True)
|
|
15
|
+
|
|
16
|
+
if sys.platform == "win32":
|
|
17
|
+
os.startfile(folder) # type: ignore[attr-defined] # Windows-only
|
|
18
|
+
elif sys.platform == "darwin":
|
|
19
|
+
subprocess.run(["open", str(folder)], check=False)
|
|
20
|
+
else:
|
|
21
|
+
subprocess.run(["xdg-open", str(folder)], check=False)
|