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,594 @@
1
+ """
2
+ Export progress dialog with AnkiConnect/APKG support.
3
+ """
4
+
5
+ from typing import List
6
+ from pathlib import Path
7
+ from PySide6.QtWidgets import (
8
+ QDialog, QVBoxLayout, QLabel, QProgressBar,
9
+ QPushButton, QTextEdit, QDialogButtonBox, QFileDialog
10
+ )
11
+ from PySide6.QtCore import Qt, QThread, Signal, Slot
12
+
13
+ from ankigammon.models import Decision
14
+ from ankigammon.anki.ankiconnect import AnkiConnect
15
+ from ankigammon.anki.apkg_exporter import ApkgExporter
16
+ from ankigammon.anki.card_generator import CardGenerator
17
+ from ankigammon.renderer.svg_board_renderer import SVGBoardRenderer
18
+ from ankigammon.renderer.color_schemes import SCHEMES
19
+ from ankigammon.settings import Settings
20
+ from ankigammon.utils.gnubg_analyzer import GNUBGAnalyzer
21
+ from ankigammon.parsers.gnubg_parser import GNUBGParser
22
+
23
+
24
+ class AnalysisWorker(QThread):
25
+ """
26
+ Background thread for GnuBG analysis of positions.
27
+
28
+ Signals:
29
+ progress(int, int): current, total
30
+ status_message(str): status update
31
+ finished(bool, str, List[Decision]): success, message, analyzed_decisions
32
+ """
33
+
34
+ progress = Signal(int, int)
35
+ status_message = Signal(str)
36
+ finished = Signal(bool, str, list)
37
+
38
+ def __init__(self, decisions: List[Decision], settings: Settings):
39
+ super().__init__()
40
+ self.decisions = decisions
41
+ self.settings = settings
42
+ self._cancelled = False
43
+
44
+ def cancel(self):
45
+ """Request cancellation of the analysis."""
46
+ self._cancelled = True
47
+
48
+ def run(self):
49
+ """Analyze positions with GnuBG in background (parallel processing)."""
50
+ try:
51
+ analyzer = GNUBGAnalyzer(
52
+ gnubg_path=self.settings.gnubg_path,
53
+ analysis_ply=self.settings.gnubg_analysis_ply
54
+ )
55
+
56
+ # Find positions that need analysis
57
+ positions_to_analyze = [(i, d) for i, d in enumerate(self.decisions) if not d.candidate_moves]
58
+ total = len(positions_to_analyze)
59
+
60
+ if total == 0:
61
+ self.finished.emit(True, "No analysis needed", self.decisions)
62
+ return
63
+
64
+ analyzed_decisions = list(self.decisions) # Copy list
65
+
66
+ # Prepare position IDs for batch analysis
67
+ position_ids = [d.xgid for _, d in positions_to_analyze]
68
+
69
+ # Progress callback for parallel analysis
70
+ def progress_callback(completed: int, total_positions: int):
71
+ if self._cancelled:
72
+ return
73
+ self.progress.emit(completed, total_positions)
74
+ self.status_message.emit(
75
+ f"Analyzing position {completed} of {total_positions} with GnuBG ({self.settings.gnubg_analysis_ply}-ply)..."
76
+ )
77
+
78
+ # Analyze all positions in parallel
79
+ self.status_message.emit(
80
+ f"Starting analysis of {total} position(s) with GnuBG ({self.settings.gnubg_analysis_ply}-ply)..."
81
+ )
82
+ analysis_results = analyzer.analyze_positions_parallel(
83
+ position_ids,
84
+ progress_callback=progress_callback
85
+ )
86
+
87
+ # Check for cancellation after batch completes
88
+ if self._cancelled:
89
+ self.finished.emit(False, "Analysis cancelled by user", self.decisions)
90
+ return
91
+
92
+ # Parse results and update decisions
93
+ for idx, (pos_idx, decision) in enumerate(positions_to_analyze):
94
+ gnubg_output, decision_type = analysis_results[idx]
95
+
96
+ analyzed_decision = GNUBGParser.parse_analysis(
97
+ gnubg_output,
98
+ decision.xgid,
99
+ decision_type
100
+ )
101
+
102
+ # Preserve user-added metadata from original decision
103
+ analyzed_decision.note = decision.note
104
+ analyzed_decision.source_file = decision.source_file
105
+ analyzed_decision.game_number = decision.game_number
106
+ analyzed_decision.move_number = decision.move_number
107
+ analyzed_decision.position_image_path = decision.position_image_path
108
+
109
+ analyzed_decisions[pos_idx] = analyzed_decision
110
+
111
+ self.finished.emit(True, f"Analyzed {total} position(s)", analyzed_decisions)
112
+
113
+ except Exception as e:
114
+ self.finished.emit(False, f"Analysis failed: {str(e)}", self.decisions)
115
+
116
+
117
+ class ExportWorker(QThread):
118
+ """
119
+ Background thread for export operations.
120
+
121
+ Signals:
122
+ progress(float): progress as percentage (0.0 to 1.0)
123
+ status_message(str): status update
124
+ finished(bool, str): success, message
125
+ """
126
+
127
+ progress = Signal(float)
128
+ status_message = Signal(str)
129
+ finished = Signal(bool, str)
130
+
131
+ def __init__(
132
+ self,
133
+ decisions: List[Decision],
134
+ settings: Settings,
135
+ export_method: str,
136
+ output_path: str = None
137
+ ):
138
+ super().__init__()
139
+ self.decisions = decisions
140
+ self.settings = settings
141
+ self.export_method = export_method
142
+ self.output_path = output_path
143
+ self._cancelled = False
144
+
145
+ def cancel(self):
146
+ """Request cancellation of the export."""
147
+ self._cancelled = True
148
+
149
+ def run(self):
150
+ """Execute export in background thread."""
151
+ try:
152
+ if self.export_method == "ankiconnect":
153
+ self._export_ankiconnect()
154
+ else:
155
+ self._export_apkg()
156
+ except Exception as e:
157
+ self.finished.emit(False, f"Export failed: {str(e)}")
158
+
159
+ def _export_ankiconnect(self):
160
+ """Export via AnkiConnect."""
161
+ self.status_message.emit("Connecting to Anki...")
162
+ self.progress.emit(0.0)
163
+
164
+ # Test connection
165
+ client = AnkiConnect(deck_name=self.settings.deck_name)
166
+ if not client.test_connection():
167
+ self.finished.emit(False, "Could not connect to Anki. Is Anki running with AnkiConnect installed?")
168
+ return
169
+
170
+ # Create model and deck if needed
171
+ self.status_message.emit("Setting up Anki deck...")
172
+ try:
173
+ client.create_model()
174
+ client.create_deck()
175
+ except Exception as e:
176
+ self.finished.emit(False, f"Failed to setup Anki deck: {str(e)}")
177
+ return
178
+
179
+ # Generate cards
180
+ self.status_message.emit("Generating cards...")
181
+
182
+ # Create renderer with color scheme and orientation
183
+ color_scheme = SCHEMES.get(self.settings.color_scheme, SCHEMES['classic'])
184
+ renderer = SVGBoardRenderer(
185
+ color_scheme=color_scheme,
186
+ orientation=self.settings.board_orientation
187
+ )
188
+
189
+ # Export decisions
190
+ total = len(self.decisions)
191
+ for i, decision in enumerate(self.decisions):
192
+ # Check for cancellation
193
+ if self._cancelled:
194
+ self.finished.emit(False, "Export cancelled by user")
195
+ return
196
+
197
+ # Calculate base progress for this position
198
+ base_progress = i / total
199
+ position_progress_range = 1.0 / total # How much progress this position represents
200
+
201
+ # Calculate sub-steps for progress tracking: render, score matrix (if applicable), generate card
202
+ has_score_matrix = (
203
+ decision.decision_type.name == 'CUBE_ACTION' and
204
+ decision.match_length > 0 and
205
+ self.settings.get('generate_score_matrix', False) and
206
+ self.settings.is_gnubg_available()
207
+ )
208
+ matrix_steps = (decision.match_length - 1) ** 2 if has_score_matrix else 0
209
+ total_substeps = 2 + matrix_steps # render + matrix + generate card
210
+
211
+ current_substep = [0] # Mutable counter for nested function
212
+
213
+ # Progress callback for sub-steps
214
+ def progress_callback(message: str):
215
+ current_substep[0] += 1
216
+ # Calculate progress within this position (cap at 95% until Anki add completes)
217
+ substep_progress = min(current_substep[0] / total_substeps, 0.95)
218
+ overall_progress = base_progress + (substep_progress * position_progress_range)
219
+ self.progress.emit(overall_progress)
220
+ self.status_message.emit(f"Position {i+1}/{total}: {message}")
221
+
222
+ # Create card generator with progress callback
223
+ output_dir = Path.home() / '.ankigammon' / 'cards'
224
+ card_gen = CardGenerator(
225
+ output_dir=output_dir,
226
+ show_options=self.settings.show_options,
227
+ interactive_moves=self.settings.interactive_moves,
228
+ renderer=renderer,
229
+ progress_callback=progress_callback
230
+ )
231
+
232
+ self.progress.emit(base_progress)
233
+
234
+ # Generate card with progress tracking
235
+ card_data = card_gen.generate_card(decision)
236
+
237
+ # Add to Anki
238
+ self.status_message.emit(f"Position {i+1}/{total}: Adding to Anki...")
239
+ self.progress.emit(base_progress + (0.95 * position_progress_range))
240
+ try:
241
+ client.add_note(
242
+ front=card_data['front'],
243
+ back=card_data['back'],
244
+ tags=card_data.get('tags', [])
245
+ )
246
+ except Exception as e:
247
+ self.finished.emit(False, f"Failed to add card {i+1}: {str(e)}")
248
+ return
249
+
250
+ # Update progress after card is successfully added
251
+ self.progress.emit((i + 1) / total)
252
+
253
+ self.finished.emit(True, f"Successfully exported {total} card(s) to Anki")
254
+
255
+ def _export_apkg(self):
256
+ """Export to APKG file."""
257
+ self.status_message.emit("Generating APKG file...")
258
+ self.progress.emit(0.0)
259
+
260
+ if not self.output_path:
261
+ self.finished.emit(False, "No output path specified for APKG export")
262
+ return
263
+
264
+ try:
265
+ # Use existing APKG exporter
266
+ output_dir = Path.home() / '.ankigammon' / 'cards'
267
+ exporter = ApkgExporter(
268
+ output_dir=output_dir,
269
+ deck_name=self.settings.deck_name
270
+ )
271
+
272
+ # Custom export loop with progress tracking
273
+ from ankigammon.renderer.color_schemes import get_scheme
274
+ from ankigammon.renderer.svg_board_renderer import SVGBoardRenderer
275
+ from ankigammon.anki.card_generator import CardGenerator
276
+ import genanki
277
+
278
+ scheme = get_scheme(self.settings.color_scheme)
279
+ renderer = SVGBoardRenderer(
280
+ color_scheme=scheme,
281
+ orientation=self.settings.board_orientation
282
+ )
283
+
284
+ # Generate cards
285
+ total = len(self.decisions)
286
+ for i, decision in enumerate(self.decisions):
287
+ # Check for cancellation
288
+ if self._cancelled:
289
+ self.finished.emit(False, "Export cancelled by user")
290
+ return
291
+
292
+ # Calculate base progress for this position
293
+ base_progress = i / total
294
+ position_progress_range = 1.0 / total
295
+
296
+ # Calculate sub-steps for progress tracking
297
+ has_score_matrix = (
298
+ decision.decision_type.name == 'CUBE_ACTION' and
299
+ decision.match_length > 0 and
300
+ self.settings.get('generate_score_matrix', False) and
301
+ self.settings.is_gnubg_available()
302
+ )
303
+ matrix_steps = (decision.match_length - 1) ** 2 if has_score_matrix else 0
304
+ total_substeps = 2 + matrix_steps # render + matrix + generate card
305
+
306
+ current_substep = [0]
307
+
308
+ # Progress callback for sub-steps
309
+ def apkg_progress_callback(message: str):
310
+ current_substep[0] += 1
311
+ substep_progress = min(current_substep[0] / total_substeps, 0.95)
312
+ overall_progress = base_progress + (substep_progress * position_progress_range)
313
+ self.progress.emit(overall_progress)
314
+ self.status_message.emit(f"Position {i+1}/{total}: {message}")
315
+
316
+ # Create card generator with progress callback
317
+ card_gen = CardGenerator(
318
+ output_dir=output_dir,
319
+ show_options=self.settings.show_options,
320
+ interactive_moves=self.settings.interactive_moves,
321
+ renderer=renderer,
322
+ progress_callback=apkg_progress_callback
323
+ )
324
+
325
+ self.progress.emit(base_progress)
326
+
327
+ # Generate card
328
+ card_data = card_gen.generate_card(decision, card_id=f"card_{i}")
329
+
330
+ # Create note
331
+ note = genanki.Note(
332
+ model=exporter.model,
333
+ fields=[card_data['front'], card_data['back']],
334
+ tags=card_data['tags']
335
+ )
336
+
337
+ # Add to deck
338
+ exporter.deck.add_note(note)
339
+
340
+ # Update progress after card added
341
+ self.progress.emit((i + 1) / total)
342
+
343
+ # Write APKG file
344
+ self.status_message.emit("Writing APKG file...")
345
+ package = genanki.Package(exporter.deck)
346
+ package.write_to_file(str(self.output_path))
347
+
348
+ self.progress.emit(1.0)
349
+ self.finished.emit(True, f"Successfully created {self.output_path}")
350
+ except Exception as e:
351
+ self.finished.emit(False, f"APKG export failed: {str(e)}")
352
+
353
+
354
+ class ExportDialog(QDialog):
355
+ """Dialog for exporting positions to Anki."""
356
+
357
+ def __init__(
358
+ self,
359
+ decisions: List[Decision],
360
+ settings: Settings,
361
+ parent=None
362
+ ):
363
+ super().__init__(parent)
364
+ self.decisions = decisions
365
+ self.settings = settings
366
+ self.worker = None
367
+ self.analysis_worker = None
368
+ self._closing = False # Flag to track if user requested close
369
+
370
+ self.setWindowTitle("Export to Anki")
371
+ self.setModal(True)
372
+ self.setMinimumWidth(500)
373
+
374
+ self._setup_ui()
375
+
376
+ def _setup_ui(self):
377
+ """Initialize the user interface."""
378
+ layout = QVBoxLayout(self)
379
+
380
+ # Info label with deck name
381
+ info = QLabel(f"Exporting {len(self.decisions)} position(s)")
382
+ info.setStyleSheet("font-size: 13px; color: #a6adc8; margin-bottom: 4px;")
383
+ layout.addWidget(info)
384
+
385
+ # Deck name label (modern styling)
386
+ deck_label = QLabel(f"<span style='font-size: 16px; font-weight: 600; color: #cdd6f4;'>{self.settings.deck_name}</span>")
387
+ deck_label.setStyleSheet("padding: 12px 16px; background-color: rgba(137, 180, 250, 0.08); border-radius: 8px;")
388
+ layout.addWidget(deck_label)
389
+
390
+ # Progress bar (use percentage-based progress)
391
+ self.progress_bar = QProgressBar()
392
+ self.progress_bar.setRange(0, 100)
393
+ layout.addWidget(self.progress_bar)
394
+
395
+ # Status label
396
+ self.status_label = QLabel(f"Ready to export {len(self.decisions)} position(s) to deck {self.settings.deck_name}")
397
+ layout.addWidget(self.status_label)
398
+
399
+ # Log text (hidden initially)
400
+ self.log_text = QTextEdit()
401
+ self.log_text.setReadOnly(True)
402
+ self.log_text.setMaximumHeight(150)
403
+ self.log_text.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
404
+ self.log_text.hide()
405
+ layout.addWidget(self.log_text)
406
+
407
+ # Buttons
408
+ self.button_box = QDialogButtonBox()
409
+ self.btn_export = QPushButton("Export")
410
+ self.btn_export.setCursor(Qt.PointingHandCursor)
411
+ self.btn_export.clicked.connect(self.start_export)
412
+ self.btn_close = QPushButton("Close")
413
+ self.btn_close.setCursor(Qt.PointingHandCursor)
414
+ self.btn_close.clicked.connect(self.close_dialog)
415
+
416
+ self.button_box.addButton(self.btn_export, QDialogButtonBox.AcceptRole)
417
+ self.button_box.addButton(self.btn_close, QDialogButtonBox.RejectRole)
418
+ layout.addWidget(self.button_box)
419
+
420
+ def closeEvent(self, event):
421
+ """Handle window close event (X button, ESC key, etc)."""
422
+ # Use the same close logic
423
+ analysis_running = self.analysis_worker and self.analysis_worker.isRunning()
424
+ export_running = self.worker and self.worker.isRunning()
425
+
426
+ if analysis_running or export_running:
427
+ # Request cancellation and ignore this close event
428
+ self._closing = True
429
+ self.btn_close.setEnabled(False)
430
+
431
+ if analysis_running:
432
+ self.analysis_worker.cancel()
433
+ self.status_label.setText("Cancelling analysis...")
434
+ elif export_running:
435
+ self.worker.cancel()
436
+ self.status_label.setText("Cancelling export...")
437
+
438
+ event.ignore() # Don't close yet
439
+ return
440
+
441
+ # No workers running, allow close
442
+ event.accept()
443
+
444
+ @Slot()
445
+ def close_dialog(self):
446
+ """Handle close button click - cancel any running operations."""
447
+ # Check if any workers are running
448
+ analysis_running = self.analysis_worker and self.analysis_worker.isRunning()
449
+ export_running = self.worker and self.worker.isRunning()
450
+
451
+ if analysis_running or export_running:
452
+ # Set closing flag and request cancellation
453
+ self._closing = True
454
+ self.btn_close.setEnabled(False)
455
+
456
+ if analysis_running:
457
+ self.analysis_worker.cancel()
458
+ self.status_label.setText("Cancelling analysis...")
459
+ elif export_running:
460
+ self.worker.cancel()
461
+ self.status_label.setText("Cancelling export...")
462
+
463
+ # The finished signals will handle actually closing the dialog
464
+ return
465
+
466
+ # No workers running, close immediately
467
+ self.reject()
468
+
469
+ @Slot()
470
+ def start_export(self):
471
+ """Start export process in background thread."""
472
+ self.btn_export.setEnabled(False)
473
+
474
+ # Get output path for APKG if needed
475
+ self.output_path = None
476
+ if self.settings.export_method == "apkg":
477
+ # Use last directory if available, otherwise use home directory
478
+ if self.settings.last_apkg_directory:
479
+ default_path = Path(self.settings.last_apkg_directory) / f"{self.settings.deck_name}.apkg"
480
+ else:
481
+ default_path = Path.home() / f"{self.settings.deck_name}.apkg"
482
+
483
+ self.output_path, _ = QFileDialog.getSaveFileName(
484
+ self,
485
+ "Save APKG File",
486
+ str(default_path),
487
+ "Anki Deck Package (*.apkg)"
488
+ )
489
+ if not self.output_path:
490
+ self.btn_export.setEnabled(True)
491
+ return
492
+
493
+ # Save the directory for next time
494
+ self.settings.last_apkg_directory = str(Path(self.output_path).parent)
495
+
496
+ # Check if any positions need GnuBG analysis
497
+ needs_analysis = [d for d in self.decisions if not d.candidate_moves]
498
+
499
+ if needs_analysis:
500
+ # Run analysis first
501
+ self.status_label.setText(f"Analyzing {len(needs_analysis)} position(s) with GnuBG...")
502
+ self.analysis_worker = AnalysisWorker(self.decisions, self.settings)
503
+ self.analysis_worker.progress.connect(self.on_analysis_progress)
504
+ self.analysis_worker.status_message.connect(self.on_status_message)
505
+ self.analysis_worker.finished.connect(self.on_analysis_finished)
506
+ self.analysis_worker.start()
507
+ else:
508
+ # No analysis needed, proceed with export
509
+ self._start_export_worker()
510
+
511
+ def _start_export_worker(self):
512
+ """Start the actual export worker (after analysis if needed)."""
513
+ # Create worker thread
514
+ self.worker = ExportWorker(
515
+ self.decisions,
516
+ self.settings,
517
+ self.settings.export_method,
518
+ self.output_path
519
+ )
520
+
521
+ # Connect signals
522
+ self.worker.progress.connect(self.on_progress)
523
+ self.worker.status_message.connect(self.on_status_message)
524
+ self.worker.finished.connect(self.on_finished)
525
+
526
+ # Start export
527
+ self.worker.start()
528
+
529
+ @Slot(int, int)
530
+ def on_analysis_progress(self, current, total):
531
+ """Update progress bar for analysis (0-50% of total progress)."""
532
+ # Analysis takes first half of progress bar (0-50%)
533
+ self.progress_bar.setValue(int((current / total) * 50))
534
+
535
+ @Slot(bool, str, list)
536
+ def on_analysis_finished(self, success, message, analyzed_decisions):
537
+ """Handle analysis completion."""
538
+ # Check if user requested to close
539
+ if self._closing:
540
+ self.reject()
541
+ return
542
+
543
+ if success:
544
+ # Update decisions with analyzed versions
545
+ self.decisions = analyzed_decisions
546
+ self.status_label.setText(f"{message} - Starting export...")
547
+ # Proceed with export
548
+ self._start_export_worker()
549
+ else:
550
+ # Analysis failed
551
+ self.status_label.setText(f"Analysis failed: {message}")
552
+ self.log_text.append(f"ERROR: {message}")
553
+ self.btn_export.setEnabled(True)
554
+
555
+ @Slot(float)
556
+ def on_progress(self, progress_fraction):
557
+ """Update progress bar for export (50-100% of total progress).
558
+
559
+ Args:
560
+ progress_fraction: Progress as a fraction from 0.0 to 1.0
561
+ """
562
+ # Export takes second half of progress bar (50-100%)
563
+ # If no analysis was needed, this will go from 0-100% as expected
564
+ # If analysis was performed, this will go from 50-100%
565
+ if hasattr(self, 'analysis_worker') and self.analysis_worker is not None:
566
+ # Analysis was performed, map 0.0-1.0 to 50-100%
567
+ self.progress_bar.setValue(50 + int(progress_fraction * 50))
568
+ else:
569
+ # No analysis, map 0.0-1.0 to 0-100%
570
+ self.progress_bar.setValue(int(progress_fraction * 100))
571
+
572
+ @Slot(str)
573
+ def on_status_message(self, message):
574
+ """Update status label."""
575
+ self.status_label.setText(message)
576
+ self.log_text.append(message)
577
+ if self.log_text.isHidden():
578
+ self.log_text.show()
579
+
580
+ @Slot(bool, str)
581
+ def on_finished(self, success, message):
582
+ """Handle export completion."""
583
+ # Check if user requested to close
584
+ if self._closing:
585
+ self.reject()
586
+ return
587
+
588
+ self.status_label.setText(message)
589
+ self.log_text.append(f"\n{'SUCCESS' if success else 'FAILED'}: {message}")
590
+
591
+ if success:
592
+ self.btn_export.setEnabled(False)
593
+ else:
594
+ self.btn_export.setEnabled(True) # Allow retry