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,216 @@
|
|
|
1
|
+
"""Shared visual tokens and layout helpers for the Flet GUI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import flet as ft
|
|
6
|
+
|
|
7
|
+
# Palette — asphalt neutrals + signal-orange accent (bike-computer LED, not Material indigo).
|
|
8
|
+
ACCENT = "#D94E1F"
|
|
9
|
+
ACCENT_LIGHT = "#FF7A45"
|
|
10
|
+
ACCENT_SOFT = "#FFF0E8"
|
|
11
|
+
ACCENT_SOFT_DARK = "#2A1810"
|
|
12
|
+
|
|
13
|
+
BG_LIGHT = "#F6F7F9"
|
|
14
|
+
SURFACE_LIGHT = "#FFFFFF"
|
|
15
|
+
SURFACE_ALT_LIGHT = "#EEF0F4"
|
|
16
|
+
BORDER_LIGHT = "#DDE1E8"
|
|
17
|
+
TEXT_LIGHT = "#12151A"
|
|
18
|
+
TEXT_MUTED_LIGHT = "#5C6570"
|
|
19
|
+
|
|
20
|
+
BG_DARK = "#0C0E12"
|
|
21
|
+
SURFACE_DARK = "#161A21"
|
|
22
|
+
SURFACE_ALT_DARK = "#1E232C"
|
|
23
|
+
BORDER_DARK = "#2A3039"
|
|
24
|
+
TEXT_DARK = "#F2F4F7"
|
|
25
|
+
TEXT_MUTED_DARK = "#8B95A3"
|
|
26
|
+
|
|
27
|
+
FONT_BODY = "DMSans"
|
|
28
|
+
FONT_DISPLAY = "Outfit"
|
|
29
|
+
|
|
30
|
+
RADIUS_SM = 10
|
|
31
|
+
RADIUS_MD = 14
|
|
32
|
+
RADIUS_LG = 20
|
|
33
|
+
|
|
34
|
+
SPACE_XS = 6
|
|
35
|
+
SPACE_SM = 10
|
|
36
|
+
SPACE_MD = 16
|
|
37
|
+
SPACE_LG = 24
|
|
38
|
+
SPACE_XL = 32
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
_MOBILE_PLATFORMS = {
|
|
42
|
+
ft.PagePlatform.ANDROID,
|
|
43
|
+
ft.PagePlatform.ANDROID_TV,
|
|
44
|
+
ft.PagePlatform.IOS,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def is_mobile(page: ft.Page) -> bool:
|
|
49
|
+
return page.platform in _MOBILE_PLATFORMS
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def is_dark(page: ft.Page) -> bool:
|
|
53
|
+
mode = page.theme_mode
|
|
54
|
+
if mode == ft.ThemeMode.DARK:
|
|
55
|
+
return True
|
|
56
|
+
if mode == ft.ThemeMode.LIGHT:
|
|
57
|
+
return False
|
|
58
|
+
return page.platform_brightness == ft.Brightness.DARK
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def palette(page: ft.Page) -> dict[str, str]:
|
|
62
|
+
dark = is_dark(page)
|
|
63
|
+
return {
|
|
64
|
+
"bg": BG_DARK if dark else BG_LIGHT,
|
|
65
|
+
"surface": SURFACE_DARK if dark else SURFACE_LIGHT,
|
|
66
|
+
"surface_alt": SURFACE_ALT_DARK if dark else SURFACE_ALT_LIGHT,
|
|
67
|
+
"border": BORDER_DARK if dark else BORDER_LIGHT,
|
|
68
|
+
"text": TEXT_DARK if dark else TEXT_LIGHT,
|
|
69
|
+
"text_muted": TEXT_MUTED_DARK if dark else TEXT_MUTED_LIGHT,
|
|
70
|
+
"accent": ACCENT_LIGHT if dark else ACCENT,
|
|
71
|
+
"accent_soft": ACCENT_SOFT_DARK if dark else ACCENT_SOFT,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def apply_page_theme(page: ft.Page) -> None:
|
|
76
|
+
page.fonts = {
|
|
77
|
+
FONT_BODY: (
|
|
78
|
+
"https://github.com/googlefonts/dm-fonts/raw/main/Sans/fonts/ttf/"
|
|
79
|
+
"DMSans-Regular.ttf"
|
|
80
|
+
),
|
|
81
|
+
f"{FONT_BODY}Medium": (
|
|
82
|
+
"https://github.com/googlefonts/dm-fonts/raw/main/Sans/fonts/ttf/"
|
|
83
|
+
"DMSans-Medium.ttf"
|
|
84
|
+
),
|
|
85
|
+
FONT_DISPLAY: (
|
|
86
|
+
"https://github.com/googlefonts/outfit/raw/main/fonts/ttf/"
|
|
87
|
+
"Outfit-SemiBold.ttf"
|
|
88
|
+
),
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
scheme = ft.ColorScheme(
|
|
92
|
+
primary=ACCENT,
|
|
93
|
+
on_primary="#FFFFFF",
|
|
94
|
+
primary_container=ACCENT_SOFT,
|
|
95
|
+
on_primary_container="#5C1F08",
|
|
96
|
+
secondary="#3D4654",
|
|
97
|
+
on_secondary="#FFFFFF",
|
|
98
|
+
surface=SURFACE_LIGHT,
|
|
99
|
+
on_surface=TEXT_LIGHT,
|
|
100
|
+
on_surface_variant=TEXT_MUTED_LIGHT,
|
|
101
|
+
outline=BORDER_LIGHT,
|
|
102
|
+
surface_container_lowest=BG_LIGHT,
|
|
103
|
+
surface_container_low=BG_LIGHT,
|
|
104
|
+
surface_container=SURFACE_ALT_LIGHT,
|
|
105
|
+
surface_container_high=SURFACE_ALT_LIGHT,
|
|
106
|
+
surface_container_highest=SURFACE_LIGHT,
|
|
107
|
+
)
|
|
108
|
+
scheme_dark = ft.ColorScheme(
|
|
109
|
+
primary=ACCENT_LIGHT,
|
|
110
|
+
on_primary="#2A1208",
|
|
111
|
+
primary_container=ACCENT_SOFT_DARK,
|
|
112
|
+
on_primary_container="#FFD6C2",
|
|
113
|
+
secondary="#A8B0BC",
|
|
114
|
+
on_secondary="#1A1E26",
|
|
115
|
+
surface=SURFACE_DARK,
|
|
116
|
+
on_surface=TEXT_DARK,
|
|
117
|
+
on_surface_variant=TEXT_MUTED_DARK,
|
|
118
|
+
outline=BORDER_DARK,
|
|
119
|
+
surface_container_lowest=BG_DARK,
|
|
120
|
+
surface_container_low=BG_DARK,
|
|
121
|
+
surface_container=SURFACE_ALT_DARK,
|
|
122
|
+
surface_container_high=SURFACE_ALT_DARK,
|
|
123
|
+
surface_container_highest=SURFACE_DARK,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
page.theme = ft.Theme(
|
|
127
|
+
color_scheme=scheme,
|
|
128
|
+
font_family=FONT_BODY,
|
|
129
|
+
use_material3=True,
|
|
130
|
+
)
|
|
131
|
+
page.dark_theme = ft.Theme(
|
|
132
|
+
color_scheme=scheme_dark,
|
|
133
|
+
font_family=FONT_BODY,
|
|
134
|
+
use_material3=True,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def display_text(
|
|
139
|
+
text: str,
|
|
140
|
+
*,
|
|
141
|
+
size: int = 28,
|
|
142
|
+
color: str | None = None,
|
|
143
|
+
weight: ft.FontWeight | None = None,
|
|
144
|
+
) -> ft.Text:
|
|
145
|
+
return ft.Text(
|
|
146
|
+
text,
|
|
147
|
+
size=size,
|
|
148
|
+
color=color,
|
|
149
|
+
weight=weight,
|
|
150
|
+
font_family=FONT_DISPLAY,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def muted_text(text: str, page: ft.Page, *, size: int = 14) -> ft.Text:
|
|
155
|
+
colors = palette(page)
|
|
156
|
+
return ft.Text(text, size=size, color=colors["text_muted"])
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def section_label(text: str, page: ft.Page) -> ft.Text:
|
|
160
|
+
colors = palette(page)
|
|
161
|
+
return ft.Text(
|
|
162
|
+
text,
|
|
163
|
+
size=11,
|
|
164
|
+
weight=ft.FontWeight.W_600,
|
|
165
|
+
color=colors["text_muted"],
|
|
166
|
+
font_family=f"{FONT_BODY}Medium",
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def surface_card(
|
|
171
|
+
page: ft.Page,
|
|
172
|
+
content: ft.Control,
|
|
173
|
+
*,
|
|
174
|
+
padding: int = SPACE_LG,
|
|
175
|
+
expand: bool = False,
|
|
176
|
+
) -> ft.Container:
|
|
177
|
+
colors = palette(page)
|
|
178
|
+
return ft.Container(
|
|
179
|
+
content=content,
|
|
180
|
+
padding=padding,
|
|
181
|
+
expand=expand,
|
|
182
|
+
bgcolor=colors["surface"],
|
|
183
|
+
border=ft.Border.all(1, colors["border"]),
|
|
184
|
+
border_radius=RADIUS_MD,
|
|
185
|
+
shadow=ft.BoxShadow(
|
|
186
|
+
spread_radius=0,
|
|
187
|
+
blur_radius=18,
|
|
188
|
+
color=ft.Colors.with_opacity(0.06, ft.Colors.BLACK),
|
|
189
|
+
offset=ft.Offset(0, 4),
|
|
190
|
+
),
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def settings_section(
|
|
195
|
+
page: ft.Page,
|
|
196
|
+
title: str,
|
|
197
|
+
*controls: ft.Control,
|
|
198
|
+
subtitle: str | None = None,
|
|
199
|
+
) -> ft.Container:
|
|
200
|
+
colors = palette(page)
|
|
201
|
+
header: list[ft.Control] = [section_label(title, page)]
|
|
202
|
+
if subtitle:
|
|
203
|
+
header.append(
|
|
204
|
+
ft.Text(subtitle, size=13, color=colors["text_muted"])
|
|
205
|
+
)
|
|
206
|
+
header.append(ft.Container(height=SPACE_SM))
|
|
207
|
+
return ft.Container(
|
|
208
|
+
content=ft.Column(
|
|
209
|
+
spacing=SPACE_MD,
|
|
210
|
+
controls=[*header, *controls],
|
|
211
|
+
),
|
|
212
|
+
padding=ft.Padding(SPACE_LG, SPACE_LG, SPACE_LG, SPACE_LG),
|
|
213
|
+
bgcolor=colors["surface"],
|
|
214
|
+
border=ft.Border.all(1, colors["border"]),
|
|
215
|
+
border_radius=RADIUS_MD,
|
|
216
|
+
)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Check GitHub Releases for a newer version of the app.
|
|
2
|
+
|
|
3
|
+
Pure logic, no UI. Fails silently (returns None) on any problem — offline, rate
|
|
4
|
+
limited, parse errors — so it never disrupts launch.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
from packaging.version import InvalidVersion, Version
|
|
11
|
+
|
|
12
|
+
REPO = "jorge-huxley/intervalssync"
|
|
13
|
+
LATEST_RELEASE_API = f"https://api.github.com/repos/{REPO}/releases/latest"
|
|
14
|
+
RELEASES_PAGE = f"https://github.com/{REPO}/releases/latest"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def check_for_update(
|
|
18
|
+
current: str, timeout: float = 5.0, allow_dev: bool = False
|
|
19
|
+
) -> str | None:
|
|
20
|
+
"""Return the latest release version (e.g. "0.2.3") if it is newer than
|
|
21
|
+
`current`, otherwise None. Returns None on any error.
|
|
22
|
+
|
|
23
|
+
Dev/local builds (e.g. "0.0.0+dev") are skipped for the automatic check, but
|
|
24
|
+
`allow_dev=True` includes them — useful for a manual "Check for updates" so
|
|
25
|
+
the flow is testable without a tagged build.
|
|
26
|
+
"""
|
|
27
|
+
try:
|
|
28
|
+
current_version = Version(current)
|
|
29
|
+
except InvalidVersion:
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
if not allow_dev and (current_version.local or current_version.is_devrelease):
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
resp = requests.get(
|
|
37
|
+
LATEST_RELEASE_API,
|
|
38
|
+
timeout=timeout,
|
|
39
|
+
headers={"Accept": "application/vnd.github+json"},
|
|
40
|
+
)
|
|
41
|
+
resp.raise_for_status()
|
|
42
|
+
tag = resp.json().get("tag_name")
|
|
43
|
+
except (requests.RequestException, ValueError):
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
if not tag:
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
latest = tag.lstrip("v")
|
|
50
|
+
try:
|
|
51
|
+
if Version(latest) > current_version:
|
|
52
|
+
return latest
|
|
53
|
+
except InvalidVersion:
|
|
54
|
+
return None
|
|
55
|
+
return None
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""iGPSPORT activity source: rides and planned workouts."""
|