ankigammon 1.0.6__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.
Files changed (61) hide show
  1. ankigammon/__init__.py +7 -0
  2. ankigammon/__main__.py +6 -0
  3. ankigammon/analysis/__init__.py +13 -0
  4. ankigammon/analysis/score_matrix.py +391 -0
  5. ankigammon/anki/__init__.py +6 -0
  6. ankigammon/anki/ankiconnect.py +216 -0
  7. ankigammon/anki/apkg_exporter.py +111 -0
  8. ankigammon/anki/card_generator.py +1325 -0
  9. ankigammon/anki/card_styles.py +1054 -0
  10. ankigammon/gui/__init__.py +8 -0
  11. ankigammon/gui/app.py +192 -0
  12. ankigammon/gui/dialogs/__init__.py +10 -0
  13. ankigammon/gui/dialogs/export_dialog.py +594 -0
  14. ankigammon/gui/dialogs/import_options_dialog.py +201 -0
  15. ankigammon/gui/dialogs/input_dialog.py +762 -0
  16. ankigammon/gui/dialogs/note_dialog.py +93 -0
  17. ankigammon/gui/dialogs/settings_dialog.py +420 -0
  18. ankigammon/gui/dialogs/update_dialog.py +373 -0
  19. ankigammon/gui/format_detector.py +377 -0
  20. ankigammon/gui/main_window.py +1611 -0
  21. ankigammon/gui/resources/down-arrow.svg +3 -0
  22. ankigammon/gui/resources/icon.icns +0 -0
  23. ankigammon/gui/resources/icon.ico +0 -0
  24. ankigammon/gui/resources/icon.png +0 -0
  25. ankigammon/gui/resources/style.qss +402 -0
  26. ankigammon/gui/resources.py +26 -0
  27. ankigammon/gui/update_checker.py +259 -0
  28. ankigammon/gui/widgets/__init__.py +8 -0
  29. ankigammon/gui/widgets/position_list.py +166 -0
  30. ankigammon/gui/widgets/smart_input.py +268 -0
  31. ankigammon/models.py +356 -0
  32. ankigammon/parsers/__init__.py +7 -0
  33. ankigammon/parsers/gnubg_match_parser.py +1094 -0
  34. ankigammon/parsers/gnubg_parser.py +468 -0
  35. ankigammon/parsers/sgf_parser.py +290 -0
  36. ankigammon/parsers/xg_binary_parser.py +1097 -0
  37. ankigammon/parsers/xg_text_parser.py +688 -0
  38. ankigammon/renderer/__init__.py +5 -0
  39. ankigammon/renderer/animation_controller.py +391 -0
  40. ankigammon/renderer/animation_helper.py +191 -0
  41. ankigammon/renderer/color_schemes.py +145 -0
  42. ankigammon/renderer/svg_board_renderer.py +791 -0
  43. ankigammon/settings.py +315 -0
  44. ankigammon/thirdparty/__init__.py +7 -0
  45. ankigammon/thirdparty/xgdatatools/__init__.py +17 -0
  46. ankigammon/thirdparty/xgdatatools/xgimport.py +160 -0
  47. ankigammon/thirdparty/xgdatatools/xgstruct.py +1032 -0
  48. ankigammon/thirdparty/xgdatatools/xgutils.py +118 -0
  49. ankigammon/thirdparty/xgdatatools/xgzarc.py +260 -0
  50. ankigammon/utils/__init__.py +13 -0
  51. ankigammon/utils/gnubg_analyzer.py +590 -0
  52. ankigammon/utils/gnuid.py +577 -0
  53. ankigammon/utils/move_parser.py +204 -0
  54. ankigammon/utils/ogid.py +326 -0
  55. ankigammon/utils/xgid.py +387 -0
  56. ankigammon-1.0.6.dist-info/METADATA +352 -0
  57. ankigammon-1.0.6.dist-info/RECORD +61 -0
  58. ankigammon-1.0.6.dist-info/WHEEL +5 -0
  59. ankigammon-1.0.6.dist-info/entry_points.txt +2 -0
  60. ankigammon-1.0.6.dist-info/licenses/LICENSE +21 -0
  61. ankigammon-1.0.6.dist-info/top_level.txt +1 -0
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="12" height="8" viewBox="0 0 12 8">
2
+ <polygon points="6,8 0,0 12,0" fill="#cdd6f4" stroke="none"/>
3
+ </svg>
Binary file
Binary file
Binary file
@@ -0,0 +1,402 @@
1
+ /* Modern Dark Theme for AnkiGammon GUI - Inspired by Catppuccin Mocha */
2
+
3
+ /* Main Window and Dialogs */
4
+ QMainWindow, QDialog, QWidget {
5
+ background-color: #1e1e2e;
6
+ color: #cdd6f4;
7
+ }
8
+
9
+ /* Primary Buttons */
10
+ QPushButton {
11
+ background-color: #89b4fa;
12
+ color: #1e1e2e;
13
+ border: none;
14
+ padding: 10px 18px;
15
+ border-radius: 6px;
16
+ font-weight: 600;
17
+ font-size: 13px;
18
+ min-height: 28px;
19
+ }
20
+
21
+ QPushButton:hover {
22
+ background-color: #a0c8fc;
23
+ }
24
+
25
+ QPushButton:pressed {
26
+ background-color: #74c7ec;
27
+ }
28
+
29
+ QPushButton:disabled {
30
+ background-color: #313244;
31
+ color: #6c7086;
32
+ }
33
+
34
+ /* Settings and Secondary Buttons */
35
+ QPushButton#btn_settings {
36
+ background-color: transparent;
37
+ color: #cdd6f4;
38
+ border: 2px solid #45475a;
39
+ }
40
+
41
+ QPushButton#btn_settings:hover {
42
+ background-color: rgba(137, 180, 250, 0.12);
43
+ color: #f5e0dc;
44
+ border-color: #89b4fa;
45
+ }
46
+
47
+ QPushButton#btn_settings:pressed {
48
+ background-color: rgba(137, 180, 250, 0.18);
49
+ color: #f5e0dc;
50
+ border-color: #89b4fa;
51
+ }
52
+
53
+ /* Position List Widget */
54
+ QListWidget {
55
+ background-color: #181825;
56
+ border: 2px solid #313244;
57
+ border-radius: 8px;
58
+ padding: 6px;
59
+ outline: none;
60
+ }
61
+
62
+ QListWidget::item {
63
+ padding: 12px 10px;
64
+ border-radius: 6px;
65
+ margin: 2px 0px;
66
+ color: #cdd6f4;
67
+ }
68
+
69
+ QListWidget::item:selected {
70
+ background-color: #89b4fa;
71
+ color: #1e1e2e;
72
+ font-weight: 600;
73
+ }
74
+
75
+ QListWidget::item:hover {
76
+ background-color: #313244;
77
+ }
78
+
79
+ QListWidget::item:selected:hover {
80
+ background-color: #a0c8fc;
81
+ }
82
+
83
+ /* Group Boxes */
84
+ QGroupBox {
85
+ border: 2px solid #45475a;
86
+ border-radius: 8px;
87
+ margin-top: 16px;
88
+ font-weight: 700;
89
+ padding-top: 16px;
90
+ color: #f5e0dc;
91
+ font-size: 13px;
92
+ }
93
+
94
+ QGroupBox::title {
95
+ subcontrol-origin: margin;
96
+ subcontrol-position: top left;
97
+ padding: 4px 12px;
98
+ background-color: #1e1e2e;
99
+ border-radius: 4px;
100
+ }
101
+
102
+ /* Labels */
103
+ QLabel {
104
+ color: #cdd6f4;
105
+ font-size: 13px;
106
+ }
107
+
108
+ QLabel#title {
109
+ color: #f5e0dc;
110
+ font-size: 16px;
111
+ }
112
+
113
+ /* Input Fields */
114
+ QLineEdit, QComboBox {
115
+ padding: 8px 12px;
116
+ border: 2px solid #45475a;
117
+ border-radius: 6px;
118
+ background-color: #181825;
119
+ color: #cdd6f4;
120
+ selection-background-color: #89b4fa;
121
+ selection-color: #1e1e2e;
122
+ font-size: 13px;
123
+ }
124
+
125
+ QLineEdit:focus, QComboBox:focus {
126
+ border: 2px solid #89b4fa;
127
+ background-color: #1e1e2e;
128
+ }
129
+
130
+ QComboBox:hover {
131
+ border-color: #6c7086;
132
+ }
133
+
134
+ QComboBox:disabled {
135
+ background-color: #313244;
136
+ color: #6c7086;
137
+ border-color: #313244;
138
+ }
139
+
140
+ QComboBox::drop-down {
141
+ border: none;
142
+ padding-right: 8px;
143
+ }
144
+
145
+ QComboBox::down-arrow {
146
+ image: url(ankigammon/gui/resources/down-arrow.svg);
147
+ width: 12px;
148
+ height: 8px;
149
+ }
150
+
151
+ QComboBox:disabled::down-arrow {
152
+ opacity: 0.3;
153
+ }
154
+
155
+ QComboBox QAbstractItemView {
156
+ background-color: #181825;
157
+ border: 2px solid #45475a;
158
+ selection-background-color: #89b4fa;
159
+ selection-color: #1e1e2e;
160
+ color: #cdd6f4;
161
+ padding: 4px;
162
+ }
163
+
164
+ /* Checkboxes */
165
+ QCheckBox {
166
+ spacing: 10px;
167
+ color: #cdd6f4;
168
+ }
169
+
170
+ QCheckBox::indicator {
171
+ width: 20px;
172
+ height: 20px;
173
+ border-radius: 4px;
174
+ border: 3px solid #45475a;
175
+ background-color: #181825;
176
+ }
177
+
178
+ QCheckBox::indicator:hover {
179
+ border-color: #89b4fa;
180
+ }
181
+
182
+ QCheckBox::indicator:checked {
183
+ background-color: #89b4fa;
184
+ border: 3px solid #45475a;
185
+ }
186
+
187
+ QCheckBox::indicator:checked:hover {
188
+ background-color: #a0c8fc;
189
+ border-color: #89b4fa;
190
+ }
191
+
192
+ /* Radio Buttons */
193
+ QRadioButton {
194
+ spacing: 10px;
195
+ color: #cdd6f4;
196
+ }
197
+
198
+ QRadioButton::indicator {
199
+ width: 20px;
200
+ height: 20px;
201
+ border-radius: 10px;
202
+ border: 2px solid #45475a;
203
+ background-color: #181825;
204
+ }
205
+
206
+ QRadioButton::indicator:hover {
207
+ border-color: #89b4fa;
208
+ }
209
+
210
+ QRadioButton::indicator:checked {
211
+ background-color: #ffffff;
212
+ border: 5px solid #89b4fa;
213
+ }
214
+
215
+ /* Progress Bar */
216
+ QProgressBar {
217
+ border: 2px solid #45475a;
218
+ border-radius: 6px;
219
+ text-align: center;
220
+ background-color: #181825;
221
+ color: #ffffff;
222
+ font-weight: 700;
223
+ height: 24px;
224
+ }
225
+
226
+ QProgressBar::chunk {
227
+ background-color: qlineargradient(x1:0, y1:0, x2:1, y2:0,
228
+ stop:0 #89b4fa, stop:1 #74c7ec);
229
+ border-radius: 4px;
230
+ margin: 2px;
231
+ }
232
+
233
+ /* Text Edit */
234
+ QTextEdit {
235
+ background-color: #181825;
236
+ border: 2px solid #45475a;
237
+ border-radius: 6px;
238
+ padding: 10px;
239
+ color: #cdd6f4;
240
+ selection-background-color: #89b4fa;
241
+ selection-color: #1e1e2e;
242
+ font-family: 'Consolas', 'Monaco', monospace;
243
+ font-size: 12px;
244
+ }
245
+
246
+ /* Status Bar */
247
+ QStatusBar {
248
+ background-color: #181825;
249
+ color: #a6adc8;
250
+ border-top: 1px solid #313244;
251
+ }
252
+
253
+ QStatusBar::item {
254
+ border: none;
255
+ }
256
+
257
+ /* Menu Bar */
258
+ QMenuBar {
259
+ background-color: #181825;
260
+ color: #cdd6f4;
261
+ border-bottom: 2px solid #313244;
262
+ padding: 4px;
263
+ }
264
+
265
+ QMenuBar::item {
266
+ padding: 6px 14px;
267
+ border-radius: 4px;
268
+ background-color: transparent;
269
+ }
270
+
271
+ QMenuBar::item:selected {
272
+ background-color: #313244;
273
+ color: #f5e0dc;
274
+ }
275
+
276
+ QMenuBar::item:pressed {
277
+ background-color: #45475a;
278
+ }
279
+
280
+ /* Menus */
281
+ QMenu {
282
+ background-color: #181825;
283
+ border: 2px solid #313244;
284
+ border-radius: 8px;
285
+ padding: 6px;
286
+ }
287
+
288
+ QMenu::item {
289
+ padding: 8px 32px 8px 16px;
290
+ border-radius: 4px;
291
+ color: #cdd6f4;
292
+ }
293
+
294
+ QMenu::item:selected {
295
+ background-color: #89b4fa;
296
+ color: #1e1e2e;
297
+ }
298
+
299
+ QMenu::separator {
300
+ height: 2px;
301
+ background-color: #313244;
302
+ margin: 6px 10px;
303
+ }
304
+
305
+ QMenu::icon {
306
+ padding-left: 10px;
307
+ }
308
+
309
+ /* Dialog Button Box */
310
+ QDialogButtonBox QPushButton {
311
+ min-width: 90px;
312
+ padding: 8px 16px;
313
+ }
314
+
315
+ /* Scrollbars */
316
+ QScrollBar:vertical {
317
+ border: none;
318
+ background-color: #181825;
319
+ width: 12px;
320
+ border-radius: 6px;
321
+ }
322
+
323
+ QScrollBar::handle:vertical {
324
+ background-color: #45475a;
325
+ border-radius: 6px;
326
+ min-height: 30px;
327
+ }
328
+
329
+ QScrollBar::handle:vertical:hover {
330
+ background-color: #585b70;
331
+ }
332
+
333
+ QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
334
+ height: 0px;
335
+ }
336
+
337
+ QScrollBar:horizontal {
338
+ border: none;
339
+ background-color: #181825;
340
+ height: 12px;
341
+ border-radius: 6px;
342
+ }
343
+
344
+ QScrollBar::handle:horizontal {
345
+ background-color: #45475a;
346
+ border-radius: 6px;
347
+ min-width: 30px;
348
+ }
349
+
350
+ QScrollBar::handle:horizontal:hover {
351
+ background-color: #585b70;
352
+ }
353
+
354
+ QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {
355
+ width: 0px;
356
+ }
357
+
358
+ /* Tooltips */
359
+ QToolTip {
360
+ background-color: #313244;
361
+ color: #cdd6f4;
362
+ border: 2px solid #45475a;
363
+ border-radius: 6px;
364
+ padding: 8px;
365
+ font-size: 12px;
366
+ }
367
+
368
+ /* Message Boxes */
369
+ QMessageBox {
370
+ background-color: #1e1e2e;
371
+ }
372
+
373
+ QMessageBox QLabel {
374
+ color: #cdd6f4;
375
+ }
376
+
377
+ /* Tab Widgets (for future use) */
378
+ QTabWidget::pane {
379
+ border: 2px solid #313244;
380
+ border-radius: 8px;
381
+ background-color: #181825;
382
+ }
383
+
384
+ QTabBar::tab {
385
+ background-color: #313244;
386
+ color: #a6adc8;
387
+ padding: 10px 20px;
388
+ border-top-left-radius: 6px;
389
+ border-top-right-radius: 6px;
390
+ margin-right: 2px;
391
+ }
392
+
393
+ QTabBar::tab:selected {
394
+ background-color: #89b4fa;
395
+ color: #1e1e2e;
396
+ font-weight: 600;
397
+ }
398
+
399
+ QTabBar::tab:hover {
400
+ background-color: #45475a;
401
+ color: #cdd6f4;
402
+ }
@@ -0,0 +1,26 @@
1
+ """
2
+ Resource path utilities for GUI.
3
+ """
4
+ import sys
5
+ from pathlib import Path
6
+
7
+
8
+ def get_resource_path(relative_path: str) -> Path:
9
+ """
10
+ Get absolute path to resource, works for dev and PyInstaller.
11
+
12
+ Args:
13
+ relative_path: Relative path to resource (e.g., "gui/resources/icon.png")
14
+
15
+ Returns:
16
+ Path: Absolute path to resource
17
+ """
18
+ try:
19
+ # PyInstaller creates a temp folder and stores path in _MEIPASS
20
+ base_path = Path(sys._MEIPASS)
21
+ except AttributeError:
22
+ # Running in normal Python environment
23
+ # __file__ is in ankigammon/gui/resources.py, so parent.parent gets us to repo root
24
+ base_path = Path(__file__).parent.parent.parent
25
+
26
+ return base_path / relative_path
@@ -0,0 +1,259 @@
1
+ """Version update checker using GitHub Releases API."""
2
+
3
+ import json
4
+ import logging
5
+ import sys
6
+ from datetime import datetime, timedelta
7
+ from pathlib import Path
8
+ from typing import Optional, Dict
9
+ from urllib.parse import urlparse
10
+
11
+ from PySide6.QtCore import QThread, Signal
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ # GitHub repository info
16
+ REPO_OWNER = "Deinonychus999"
17
+ REPO_NAME = "AnkiGammon"
18
+ GITHUB_API_URL = f"https://api.github.com/repos/{REPO_OWNER}/{REPO_NAME}/releases/latest"
19
+ GITHUB_RELEASES_URL = f"https://github.com/{REPO_OWNER}/{REPO_NAME}/releases"
20
+
21
+
22
+ class VersionChecker:
23
+ """Check for updates via GitHub Releases API."""
24
+
25
+ def __init__(self, timeout: int = 5):
26
+ """Initialize version checker.
27
+
28
+ Args:
29
+ timeout: Request timeout in seconds
30
+ """
31
+ self.timeout = timeout
32
+
33
+ def check_latest_version(self) -> Optional[Dict]:
34
+ """Fetch latest release from GitHub API.
35
+
36
+ Returns:
37
+ Dict with release info or None if failed:
38
+ {
39
+ 'version': '1.0.6',
40
+ 'name': 'Version 1.0.6',
41
+ 'release_notes': '...',
42
+ 'download_url': 'https://...',
43
+ 'published_at': '2024-01-15T10:30:00Z'
44
+ }
45
+ """
46
+ try:
47
+ import requests
48
+ except ImportError:
49
+ logger.warning("requests library not available, cannot check for updates")
50
+ return None
51
+
52
+ try:
53
+ response = requests.get(
54
+ GITHUB_API_URL,
55
+ timeout=(self.timeout, self.timeout),
56
+ headers={'Accept': 'application/vnd.github+json'}
57
+ )
58
+ response.raise_for_status()
59
+
60
+ data = response.json()
61
+
62
+ # Skip pre-releases and drafts
63
+ if data.get('prerelease') or data.get('draft'):
64
+ logger.info("Skipping pre-release or draft")
65
+ return None
66
+
67
+ tag = data.get('tag_name', '').lstrip('v')
68
+
69
+ return {
70
+ 'version': tag,
71
+ 'name': data.get('name', tag),
72
+ 'release_notes': data.get('body', ''),
73
+ 'download_url': self._extract_download_url(data),
74
+ 'published_at': data.get('published_at', ''),
75
+ 'html_url': data.get('html_url', GITHUB_RELEASES_URL)
76
+ }
77
+ except Exception as e:
78
+ logger.warning(f"Failed to check for updates: {e}")
79
+ return None
80
+
81
+ def _extract_download_url(self, release_data: Dict) -> str:
82
+ """Extract appropriate download URL from release data.
83
+
84
+ Uses GitHub's /releases/latest/download/ URL pattern for direct downloads.
85
+ """
86
+ # Determine platform-specific filename
87
+ if sys.platform == 'win32':
88
+ # Windows: ankigammon-windows.zip
89
+ filename = 'ankigammon-windows.zip'
90
+ elif sys.platform == 'darwin':
91
+ # macOS: AnkiGammon-macOS.dmg
92
+ filename = 'AnkiGammon-macOS.dmg'
93
+ else:
94
+ # Linux: AnkiGammon-x86_64.AppImage
95
+ filename = 'AnkiGammon-x86_64.AppImage'
96
+
97
+ # Construct direct download URL using GitHub's /releases/latest/download/ pattern
98
+ download_url = f"https://github.com/{REPO_OWNER}/{REPO_NAME}/releases/latest/download/{filename}"
99
+
100
+ return download_url
101
+
102
+ def compare_versions(self, current: str, latest: str) -> bool:
103
+ """Check if latest version is newer than current.
104
+
105
+ Args:
106
+ current: Current version string (e.g., "1.0.6")
107
+ latest: Latest version string
108
+
109
+ Returns:
110
+ True if update is available
111
+ """
112
+ try:
113
+ from packaging.version import Version
114
+ return Version(latest) > Version(current)
115
+ except Exception as e:
116
+ logger.warning(f"Failed to compare versions: {e}")
117
+ return False
118
+
119
+
120
+ class VersionCheckCache:
121
+ """Manages version check caching to avoid excessive API calls."""
122
+
123
+ def __init__(self, cache_dir: Optional[Path] = None):
124
+ """Initialize cache manager.
125
+
126
+ Args:
127
+ cache_dir: Directory for cache file (defaults to ~/.ankigammon)
128
+ """
129
+ if cache_dir is None:
130
+ cache_dir = Path.home() / '.ankigammon'
131
+ self.cache_file = cache_dir / 'version_check.json'
132
+ self.cache_dir = cache_dir
133
+ self.cache_dir.mkdir(parents=True, exist_ok=True)
134
+
135
+ def should_check(self, min_hours_between_checks: int = 24) -> bool:
136
+ """Check if enough time has passed since last check.
137
+
138
+ Args:
139
+ min_hours_between_checks: Minimum hours between checks
140
+
141
+ Returns:
142
+ True if should check now
143
+ """
144
+ if not self.cache_file.exists():
145
+ return True
146
+
147
+ try:
148
+ with open(self.cache_file, 'r') as f:
149
+ cache = json.load(f)
150
+
151
+ last_check = datetime.fromisoformat(cache.get('last_check', ''))
152
+ time_since_check = datetime.now() - last_check
153
+
154
+ return time_since_check > timedelta(hours=min_hours_between_checks)
155
+ except (json.JSONDecodeError, ValueError, KeyError, OSError):
156
+ return True # If cache is corrupted, check anyway
157
+
158
+ def get_cached_update(self) -> Optional[Dict]:
159
+ """Get previously cached update info.
160
+
161
+ Returns:
162
+ Cached release info or None
163
+ """
164
+ if not self.cache_file.exists():
165
+ return None
166
+
167
+ try:
168
+ with open(self.cache_file, 'r') as f:
169
+ cache = json.load(f)
170
+ return cache.get('latest_release')
171
+ except (json.JSONDecodeError, KeyError, OSError):
172
+ return None
173
+
174
+ def save_check(self, release_info: Optional[Dict]):
175
+ """Save the result of a version check.
176
+
177
+ Args:
178
+ release_info: Release info dict or None
179
+ """
180
+ cache = {
181
+ 'last_check': datetime.now().isoformat(),
182
+ 'latest_release': release_info
183
+ }
184
+
185
+ try:
186
+ with open(self.cache_file, 'w') as f:
187
+ json.dump(cache, f, indent=2)
188
+ except OSError as e:
189
+ logger.warning(f"Failed to save version check cache: {e}")
190
+
191
+
192
+ class VersionCheckerThread(QThread):
193
+ """Background thread for non-blocking version checking."""
194
+
195
+ # Signals
196
+ update_available = Signal(dict) # Emitted when update found
197
+ check_complete = Signal() # Emitted when check done
198
+ check_failed = Signal() # Emitted when check failed (network error)
199
+
200
+ def __init__(self, current_version: str, force_check: bool = False):
201
+ """Initialize checker thread.
202
+
203
+ Args:
204
+ current_version: Current app version
205
+ force_check: If True, bypass cache and check immediately
206
+ """
207
+ super().__init__()
208
+ self.current_version = current_version
209
+ self.force_check = force_check
210
+ self.checker = VersionChecker()
211
+ self.cache = VersionCheckCache()
212
+
213
+ def run(self):
214
+ """Execute version check in background thread."""
215
+ try:
216
+ # Check if we should skip (unless forced)
217
+ if not self.force_check and not self.cache.should_check(min_hours_between_checks=24):
218
+ logger.info("Skipping version check (too recent)")
219
+ cached = self.cache.get_cached_update()
220
+ if cached:
221
+ self._check_and_emit(cached)
222
+ self.check_complete.emit()
223
+ return
224
+
225
+ # Fetch from GitHub
226
+ logger.info("Checking for updates...")
227
+ latest = self.checker.check_latest_version()
228
+
229
+ if latest:
230
+ # Cache the result
231
+ self.cache.save_check(latest)
232
+ self._check_and_emit(latest)
233
+ else:
234
+ # Network failure - try cached value
235
+ logger.info("Network check failed, using cache")
236
+ cached = self.cache.get_cached_update()
237
+ if cached:
238
+ self._check_and_emit(cached)
239
+ elif self.force_check:
240
+ # Manual check with no network and no cache = fail
241
+ self._check_failed = True
242
+ self.check_failed.emit()
243
+
244
+ self.check_complete.emit()
245
+ except Exception as e:
246
+ logger.error(f"Version check thread error: {e}")
247
+ self.check_complete.emit()
248
+
249
+ def _check_and_emit(self, release_info: Dict):
250
+ """Check if update is available and emit signal.
251
+
252
+ Args:
253
+ release_info: Release information dict
254
+ """
255
+ if self.checker.compare_versions(self.current_version, release_info['version']):
256
+ logger.info(f"Update available: {release_info['version']}")
257
+ self.update_available.emit(release_info)
258
+ else:
259
+ logger.info(f"Already up to date ({self.current_version})")
@@ -0,0 +1,8 @@
1
+ """
2
+ GUI widgets package.
3
+ """
4
+
5
+ __all__ = ['PositionListWidget', 'SmartInputWidget']
6
+
7
+ from .position_list import PositionListWidget
8
+ from .smart_input import SmartInputWidget