tbr-deal-finder 0.2.1__py3-none-any.whl → 0.3.1__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 (32) hide show
  1. tbr_deal_finder/__init__.py +1 -5
  2. tbr_deal_finder/__main__.py +7 -0
  3. tbr_deal_finder/book.py +16 -8
  4. tbr_deal_finder/cli.py +13 -27
  5. tbr_deal_finder/config.py +2 -2
  6. tbr_deal_finder/desktop_updater.py +147 -0
  7. tbr_deal_finder/gui/__init__.py +0 -0
  8. tbr_deal_finder/gui/main.py +725 -0
  9. tbr_deal_finder/gui/pages/__init__.py +1 -0
  10. tbr_deal_finder/gui/pages/all_books.py +93 -0
  11. tbr_deal_finder/gui/pages/all_deals.py +63 -0
  12. tbr_deal_finder/gui/pages/base_book_page.py +291 -0
  13. tbr_deal_finder/gui/pages/book_details.py +604 -0
  14. tbr_deal_finder/gui/pages/latest_deals.py +370 -0
  15. tbr_deal_finder/gui/pages/settings.py +389 -0
  16. tbr_deal_finder/retailer/amazon.py +58 -7
  17. tbr_deal_finder/retailer/amazon_custom_auth.py +79 -0
  18. tbr_deal_finder/retailer/audible.py +2 -1
  19. tbr_deal_finder/retailer/chirp.py +55 -11
  20. tbr_deal_finder/retailer/kindle.py +31 -19
  21. tbr_deal_finder/retailer/librofm.py +53 -20
  22. tbr_deal_finder/retailer/models.py +31 -1
  23. tbr_deal_finder/retailer_deal.py +38 -14
  24. tbr_deal_finder/tracked_books.py +24 -18
  25. tbr_deal_finder/utils.py +64 -2
  26. tbr_deal_finder/version_check.py +40 -0
  27. {tbr_deal_finder-0.2.1.dist-info → tbr_deal_finder-0.3.1.dist-info}/METADATA +18 -87
  28. tbr_deal_finder-0.3.1.dist-info/RECORD +38 -0
  29. {tbr_deal_finder-0.2.1.dist-info → tbr_deal_finder-0.3.1.dist-info}/entry_points.txt +1 -0
  30. tbr_deal_finder-0.2.1.dist-info/RECORD +0 -25
  31. {tbr_deal_finder-0.2.1.dist-info → tbr_deal_finder-0.3.1.dist-info}/WHEEL +0 -0
  32. {tbr_deal_finder-0.2.1.dist-info → tbr_deal_finder-0.3.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,725 @@
1
+ import asyncio
2
+ import os
3
+ import base64
4
+
5
+ import flet as ft
6
+
7
+ from tbr_deal_finder.config import Config
8
+ from tbr_deal_finder.book import Book, BookFormat
9
+ from tbr_deal_finder.migrations import make_migrations
10
+ from tbr_deal_finder.retailer import RETAILER_MAP
11
+ from tbr_deal_finder.retailer.models import Retailer
12
+ from tbr_deal_finder.retailer_deal import get_latest_deals
13
+ from tbr_deal_finder.desktop_updater import check_for_desktop_updates
14
+
15
+ from tbr_deal_finder.gui.pages.settings import SettingsPage
16
+ from tbr_deal_finder.gui.pages.all_deals import AllDealsPage
17
+ from tbr_deal_finder.gui.pages.latest_deals import LatestDealsPage
18
+ from tbr_deal_finder.gui.pages.all_books import AllBooksPage
19
+ from tbr_deal_finder.gui.pages.book_details import BookDetailsPage
20
+
21
+
22
+ class TBRDealFinderApp:
23
+ def __init__(self, page: ft.Page):
24
+ self.page = page
25
+ self.config = None
26
+ self.current_page = "all_deals"
27
+ self.selected_book = None
28
+ self.update_info = None # Store update information
29
+
30
+ # Initialize pages
31
+ self.settings_page = SettingsPage(self)
32
+ self.all_deals_page = AllDealsPage(self)
33
+ self.latest_deals_page = LatestDealsPage(self)
34
+ self.all_books_page = AllBooksPage(self)
35
+ self.book_details_page = BookDetailsPage(self)
36
+
37
+ self.setup_page()
38
+ self.load_config()
39
+ self.build_layout()
40
+ self.check_for_updates_silently()
41
+
42
+ def setup_page(self):
43
+ """Configure the main page settings"""
44
+ self.page.title = "TBR Deal Finder"
45
+ self.page.theme_mode = ft.ThemeMode.DARK
46
+ self.page.padding = 0
47
+ self.page.spacing = 0
48
+ self.page.window.width = 1200
49
+ self.page.window.height = 800
50
+ self.page.window.min_width = 800
51
+ self.page.window.min_height = 600
52
+
53
+ def load_config(self):
54
+ """Load configuration or create default"""
55
+ try:
56
+ self.config = Config.load()
57
+ except FileNotFoundError:
58
+ # Will prompt for config setup
59
+ self.config = None
60
+
61
+ def load_logo_as_base64(self):
62
+ """Load the logo image and convert to base64"""
63
+ try:
64
+ # Get the path relative to the main script location
65
+ logo_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "assets", "icon.png")
66
+ if not os.path.exists(logo_path):
67
+ # Try alternative path for packaged app
68
+ logo_path = os.path.join("assets", "icon.png")
69
+
70
+ if os.path.exists(logo_path):
71
+ with open(logo_path, "rb") as image_file:
72
+ encoded_string = base64.b64encode(image_file.read()).decode()
73
+ return encoded_string # Return just the base64 string, not the data URL
74
+ except Exception as e:
75
+ print(f"Could not load logo: {e}")
76
+ return None
77
+
78
+ def refresh_navigation(self):
79
+ """Refresh the navigation container to update the update indicator"""
80
+ # Rebuild just the navigation part
81
+ logo_base64 = self.load_logo_as_base64()
82
+
83
+ # Create logo widget or fallback
84
+ if logo_base64:
85
+ logo_widget = ft.Image(
86
+ src_base64=logo_base64,
87
+ width=80,
88
+ height=80,
89
+ fit=ft.ImageFit.CONTAIN
90
+ )
91
+ else:
92
+ logo_widget = ft.Icon(
93
+ ft.Icons.BOOK,
94
+ size=64,
95
+ color=ft.Colors.BLUE_400
96
+ )
97
+
98
+ # Create bottom section with logo and update indicator
99
+ bottom_section_widgets = [logo_widget]
100
+
101
+ # Add update indicator if update is available
102
+ if self.update_info:
103
+ update_indicator = ft.Container(
104
+ content=ft.Text(
105
+ "Update Available",
106
+ size=11,
107
+ color=ft.Colors.ORANGE_400,
108
+ weight=ft.FontWeight.BOLD,
109
+ text_align=ft.TextAlign.CENTER
110
+ ),
111
+ padding=ft.padding.only(top=8, left=4, right=4, bottom=4),
112
+ alignment=ft.alignment.center,
113
+ on_click=lambda e: self.show_update_notification(),
114
+ border_radius=4,
115
+ bgcolor=ft.Colors.ORANGE_50,
116
+ border=ft.border.all(1, ft.Colors.ORANGE_400)
117
+ )
118
+ bottom_section_widgets.append(update_indicator)
119
+
120
+ # Update the bottom container content
121
+ self.nav_container.content.controls[1].content = ft.Column(
122
+ bottom_section_widgets,
123
+ horizontal_alignment=ft.CrossAxisAlignment.CENTER,
124
+ spacing=0
125
+ )
126
+
127
+ self.page.update()
128
+
129
+ def build_layout(self):
130
+ """Build the main application layout"""
131
+ # Top app bar with settings cog
132
+ app_bar = ft.AppBar(
133
+ title=ft.Text("TBR Deal Finder", size=20, weight=ft.FontWeight.BOLD, color=ft.Colors.WHITE60),
134
+ center_title=False,
135
+ bgcolor=ft.Colors.BLUE_GREY_900,
136
+ actions=[
137
+ ft.IconButton(
138
+ icon=ft.Icons.SETTINGS,
139
+ tooltip="Settings",
140
+ on_click=self.show_settings
141
+ )
142
+ ]
143
+ )
144
+
145
+ # Load logo as base64
146
+ logo_base64 = self.load_logo_as_base64()
147
+
148
+ # Create logo widget or fallback
149
+ if logo_base64:
150
+ logo_widget = ft.Image(
151
+ src_base64=logo_base64,
152
+ width=80,
153
+ height=80,
154
+ fit=ft.ImageFit.CONTAIN
155
+ )
156
+ else:
157
+ # Fallback to an icon if logo can't be loaded
158
+ logo_widget = ft.Icon(
159
+ ft.Icons.BOOK,
160
+ size=64,
161
+ color=ft.Colors.BLUE_400
162
+ )
163
+
164
+ # Navigation rail (left sidebar)
165
+ nav_rail = ft.NavigationRail(
166
+ selected_index=0,
167
+ label_type=ft.NavigationRailLabelType.ALL,
168
+ min_width=200,
169
+ min_extended_width=200,
170
+ group_alignment=-1.0,
171
+ destinations=[
172
+ ft.NavigationRailDestination(
173
+ icon=ft.Icons.LOCAL_OFFER,
174
+ selected_icon=ft.Icons.LOCAL_OFFER_OUTLINED,
175
+ label="All Deals"
176
+ ),
177
+ ft.NavigationRailDestination(
178
+ icon=ft.Icons.NEW_RELEASES,
179
+ selected_icon=ft.Icons.NEW_RELEASES_OUTLINED,
180
+ label="Latest Deals"
181
+ ),
182
+ ft.NavigationRailDestination(
183
+ icon=ft.Icons.LIBRARY_BOOKS,
184
+ selected_icon=ft.Icons.LOCAL_LIBRARY_OUTLINED,
185
+ label="My Books"
186
+ ),
187
+ ],
188
+ on_change=self.nav_changed,
189
+ expand=True # This allows NavigationRail to expand within the Column
190
+ )
191
+
192
+ # Store reference for later use
193
+ self.nav_rail = nav_rail
194
+
195
+ # Create bottom section with logo and update indicator
196
+ bottom_section_widgets = [logo_widget]
197
+
198
+ # Add update indicator if update is available
199
+ if self.update_info:
200
+ update_indicator = ft.Container(
201
+ content=ft.Text(
202
+ "Update Available",
203
+ size=11,
204
+ color=ft.Colors.ORANGE_400,
205
+ weight=ft.FontWeight.BOLD,
206
+ text_align=ft.TextAlign.CENTER
207
+ ),
208
+ padding=ft.padding.only(top=8, left=4, right=4, bottom=4),
209
+ alignment=ft.alignment.center,
210
+ on_click=lambda e: self.show_update_notification(),
211
+ border_radius=4,
212
+ bgcolor=ft.Colors.ORANGE_50,
213
+ border=ft.border.all(1, ft.Colors.ORANGE_400)
214
+ )
215
+ bottom_section_widgets.append(update_indicator)
216
+
217
+ # Create navigation container with logo at bottom
218
+ self.nav_container = ft.Container(
219
+ content=ft.Column([
220
+ nav_rail,
221
+ ft.Container(
222
+ content=ft.Column(
223
+ bottom_section_widgets,
224
+ horizontal_alignment=ft.CrossAxisAlignment.CENTER,
225
+ spacing=0
226
+ ),
227
+ padding=ft.padding.only(bottom=20),
228
+ alignment=ft.alignment.center
229
+ )
230
+ ],
231
+ spacing=0,
232
+ expand=True), # Column should expand vertically
233
+ width=200 # Fixed width, no horizontal expand
234
+ )
235
+
236
+ # Main content area
237
+ self.content_area = ft.Container(
238
+ content=self.get_current_page_content(),
239
+ expand=True,
240
+ padding=20
241
+ )
242
+
243
+ # Main layout with sidebar and content
244
+ main_layout = ft.Row(
245
+ [
246
+ self.nav_container,
247
+ ft.VerticalDivider(width=1),
248
+ self.content_area
249
+ ],
250
+ expand=True,
251
+ spacing=0
252
+ )
253
+
254
+ # Add everything to page
255
+ self.page.appbar = app_bar
256
+ self.page.add(main_layout)
257
+ self.page.update()
258
+
259
+ def nav_changed(self, e):
260
+ """Handle navigation rail selection changes"""
261
+ destinations = ["all_deals", "latest_deals", "all_books"]
262
+ if e.control.selected_index < len(destinations):
263
+ self.current_page = destinations[e.control.selected_index]
264
+
265
+ # Only clear all page states when clicking on Latest Deals
266
+ # Other pages maintain their state when navigated to
267
+ if self.current_page == "latest_deals":
268
+ self.refresh_all_pages()
269
+
270
+ self.update_content()
271
+
272
+ def update_content(self):
273
+ """Update the main content area"""
274
+ self.content_area.content = self.get_current_page_content()
275
+ self.page.update()
276
+
277
+ def refresh_current_page(self):
278
+ """Refresh the current page by clearing its state and reloading data"""
279
+ if self.current_page == "all_deals":
280
+ self.all_deals_page.refresh_page_state()
281
+ elif self.current_page == "latest_deals":
282
+ self.latest_deals_page.refresh_page_state()
283
+ elif self.current_page == "all_books":
284
+ self.all_books_page.refresh_page_state()
285
+
286
+ def refresh_all_pages(self):
287
+ """Refresh all pages by clearing their state and reloading data"""
288
+ self.all_deals_page.refresh_page_state()
289
+ self.latest_deals_page.refresh_page_state()
290
+ self.all_books_page.refresh_page_state()
291
+
292
+ def get_current_page_content(self):
293
+ """Get content for the current page"""
294
+ if self.config is None and self.current_page != "settings":
295
+ return self.get_config_prompt()
296
+
297
+ if self.current_page == "all_deals":
298
+ return self.all_deals_page.build()
299
+ elif self.current_page == "latest_deals":
300
+ return self.latest_deals_page.build()
301
+ elif self.current_page == "all_books":
302
+ return self.all_books_page.build()
303
+ elif self.current_page == "book_details":
304
+ return self.book_details_page.build()
305
+ elif self.current_page == "settings":
306
+ return self.settings_page.build()
307
+ else:
308
+ return ft.Text("Page not found")
309
+
310
+ def get_config_prompt(self):
311
+ """Show config setup prompt when no config exists"""
312
+ return ft.Container(
313
+ content=ft.Column([
314
+ ft.Icon(ft.Icons.SETTINGS, size=64, color=ft.Colors.GREY_400),
315
+ ft.Text(
316
+ "Welcome to TBR Deal Finder!",
317
+ size=24,
318
+ weight=ft.FontWeight.BOLD,
319
+ text_align=ft.TextAlign.CENTER
320
+ ),
321
+ ft.Text(
322
+ "You need to configure your settings before getting started.",
323
+ size=16,
324
+ color=ft.Colors.GREY_600,
325
+ text_align=ft.TextAlign.CENTER
326
+ ),
327
+ ft.ElevatedButton(
328
+ "Configure Settings",
329
+ icon=ft.Icons.SETTINGS,
330
+ on_click=self.show_settings
331
+ )
332
+ ],
333
+ alignment=ft.MainAxisAlignment.CENTER,
334
+ horizontal_alignment=ft.CrossAxisAlignment.CENTER,
335
+ spacing=20),
336
+ alignment=ft.alignment.center
337
+ )
338
+
339
+ def show_settings(self, e=None):
340
+ """Show settings page"""
341
+ self.current_page = "settings"
342
+ self.nav_rail.selected_index = None # Deselect nav items
343
+ self.update_content()
344
+
345
+ def show_book_details(self, book: Book, format_type: BookFormat = None):
346
+ """Show book details page"""
347
+ self.selected_book = book
348
+
349
+ # Set the initial format if specified
350
+ if format_type is not None and format_type != BookFormat.NA:
351
+ self.book_details_page.set_initial_format(format_type)
352
+ else:
353
+ # Reset selected format so it uses default logic
354
+ self.book_details_page.selected_format = None
355
+
356
+ self.current_page = "book_details"
357
+ self.nav_rail.selected_index = None
358
+ self.update_content()
359
+
360
+ def go_back_to_deals(self):
361
+ """Return to deals page from book details"""
362
+ self.current_page = "all_deals"
363
+ self.nav_rail.selected_index = 0
364
+ # Refresh the page when returning to it
365
+ self.refresh_current_page()
366
+ self.update_content()
367
+
368
+ def config_updated(self, new_config: Config):
369
+ """Handle config updates"""
370
+ self.config = new_config
371
+ if self.current_page == "settings":
372
+ self.current_page = "all_deals"
373
+ self.nav_rail.selected_index = 0
374
+ # Refresh the page when returning from settings
375
+ self.refresh_current_page()
376
+ self.update_content()
377
+
378
+ async def run_latest_deals(self):
379
+ """Run the latest deals check with progress tracking using GUI auth"""
380
+ if not self.config:
381
+ return False
382
+
383
+ try:
384
+ # First authenticate all retailers using GUI dialogs
385
+ await self.auth_all_configured_retailers()
386
+ # Then fetch the deals (retailers should already be authenticated)
387
+ return await get_latest_deals(self.config)
388
+ except Exception as e:
389
+ return False
390
+
391
+ async def auth_all_configured_retailers(self):
392
+ for retailer_str in self.config.tracked_retailers:
393
+ retailer = RETAILER_MAP[retailer_str]()
394
+
395
+ # Skip if already authenticated
396
+ if retailer.user_is_authed():
397
+ continue
398
+
399
+ # Use GUI auth instead of CLI auth
400
+ await self.show_auth_dialog(retailer)
401
+
402
+ async def show_auth_dialog(self, retailer: Retailer):
403
+ """Show authentication dialog for retailer login"""
404
+
405
+ auth_context = retailer.gui_auth_context
406
+ title = auth_context.title
407
+ fields = auth_context.fields
408
+ message = auth_context.message
409
+ user_copy_context = auth_context.user_copy_context
410
+ pop_up_type = auth_context.pop_up_type
411
+
412
+ # Store the dialog reference at instance level temporarily
413
+ self._auth_dialog_result = None
414
+ self._auth_dialog_complete = False
415
+
416
+ def close_dialog():
417
+ dialog.open = False
418
+ self.page.update()
419
+
420
+ async def handle_submit(e=None):
421
+ form_data = {}
422
+ for field in fields:
423
+ field_name = field["name"]
424
+ field_ref = field.get("ref")
425
+ if field_ref:
426
+ form_data[field_name] = field_ref.value
427
+
428
+ try:
429
+ result = await retailer.gui_auth(form_data)
430
+ if result:
431
+ close_dialog()
432
+ self._auth_dialog_result = True
433
+ self._auth_dialog_complete = True
434
+ else:
435
+ # Show error in dialog
436
+ error_text.value = "Login failed, please try again"
437
+ error_text.visible = True
438
+ self.page.update()
439
+ except Exception as ex:
440
+ self._auth_dialog_result = False
441
+ self._auth_dialog_complete = True
442
+
443
+ # Build dialog with error text
444
+ error_text = ft.Text("", color=ft.Colors.RED, visible=False)
445
+ content_controls = [error_text]
446
+
447
+ if message:
448
+ content_controls.append(
449
+ ft.Text(message, selectable=True)
450
+ )
451
+
452
+ # Add user copy context if available
453
+ if user_copy_context:
454
+ def copy_to_clipboard(e):
455
+ self.page.set_clipboard(user_copy_context)
456
+ copy_button.text = "Copied!"
457
+ copy_button.icon = ft.Icons.CHECK
458
+ self.page.update()
459
+ # Reset button after 2 seconds
460
+ import threading
461
+ def reset_button():
462
+ import time
463
+ time.sleep(.25)
464
+ copy_button.text = "Copy"
465
+ copy_button.icon = ft.Icons.COPY
466
+ copy_button.update()
467
+ threading.Thread(target=reset_button, daemon=True).start()
468
+
469
+ copy_button = ft.ElevatedButton(
470
+ "Copy",
471
+ icon=ft.Icons.COPY,
472
+ on_click=copy_to_clipboard,
473
+ style=ft.ButtonStyle(
474
+ bgcolor=ft.Colors.BLUE_100,
475
+ color=ft.Colors.BLUE_900
476
+ )
477
+ )
478
+
479
+ content_controls.extend([
480
+ ft.Text("Copy this:", weight=ft.FontWeight.BOLD, size=12),
481
+ ft.Container(
482
+ content=ft.Text(
483
+ user_copy_context,
484
+ selectable=True,
485
+ size=11,
486
+ color=ft.Colors.GREY_700,
487
+ height=80
488
+ ),
489
+ bgcolor=ft.Colors.GREY_100,
490
+ padding=10,
491
+ border_radius=5,
492
+ border=ft.border.all(1, ft.Colors.GREY_300)
493
+ ),
494
+ copy_button,
495
+ ft.Divider()
496
+ ])
497
+
498
+ if fields and pop_up_type == "form":
499
+ for field in fields:
500
+ field_type = field.get("type", "text")
501
+ field_ref = ft.TextField(
502
+ label=field["label"],
503
+ password=field_type == "password",
504
+ keyboard_type=ft.KeyboardType.EMAIL if field_type == "email" else ft.KeyboardType.TEXT,
505
+ autofocus=field == fields[0], # Focus first field
506
+ height=60
507
+
508
+ )
509
+ field["ref"] = field_ref # Store reference
510
+ content_controls.append(field_ref)
511
+
512
+ # Dialog actions
513
+ actions = []
514
+ if pop_up_type == "form" and fields:
515
+ actions.extend([
516
+ ft.ElevatedButton("Login", on_click=handle_submit)
517
+ ])
518
+ else:
519
+ actions.append(
520
+ ft.TextButton("OK", on_click=close_dialog)
521
+ )
522
+
523
+ # Create dialog
524
+ dialog = ft.AlertDialog(
525
+ title=ft.Text(title) if title else None,
526
+ content=ft.Column(
527
+ content_controls,
528
+ width=400,
529
+ height=None,
530
+ scroll=ft.ScrollMode.AUTO, # Enable scrolling
531
+ spacing=10
532
+ ),
533
+ actions=actions,
534
+ modal=True
535
+ )
536
+
537
+ # Show dialog
538
+ self.page.overlay.append(dialog)
539
+ dialog.open = True
540
+ self.page.update()
541
+
542
+ # Poll for completion
543
+ while not self._auth_dialog_complete:
544
+ await asyncio.sleep(0.1)
545
+
546
+ result = self._auth_dialog_result
547
+
548
+ # Clean up
549
+ self._auth_dialog_result = None
550
+ self._auth_dialog_complete = False
551
+
552
+ return result
553
+
554
+ def check_for_updates_silently(self):
555
+ """Check for updates silently without showing dialogs - only update the indicator"""
556
+ try:
557
+ update_info = check_for_desktop_updates()
558
+
559
+ if update_info:
560
+ self.update_info = update_info
561
+ else:
562
+ self.update_info = None
563
+
564
+ self.refresh_navigation() # Update the navigation indicator
565
+ except Exception as e:
566
+ # Silently fail - don't show errors for automatic update checks
567
+ print(f"Silent update check failed: {e}")
568
+
569
+ def check_for_updates_manual(self):
570
+ """Check for updates manually when user clicks button."""
571
+
572
+ # Check for updates
573
+ update_info = check_for_desktop_updates()
574
+
575
+ if update_info:
576
+ # Update available - show banner
577
+ self.update_info = update_info
578
+ self.refresh_navigation() # Update the navigation to show indicator
579
+ self.show_update_notification()
580
+ else:
581
+ # No update available - show up-to-date message
582
+ # Clear any existing update info and refresh navigation
583
+ self.update_info = None
584
+ self.refresh_navigation() # Update the navigation to hide indicator
585
+ self.show_up_to_date_message()
586
+
587
+ def show_update_notification(self):
588
+ """Show update notification dialog."""
589
+ if not self.update_info:
590
+ return
591
+
592
+ def close_dialog(e):
593
+ dialog.open = False
594
+ self.page.update()
595
+
596
+ def view_release_and_close(e):
597
+ self.view_release_notes(e)
598
+ close_dialog(e)
599
+
600
+ def download_and_close(e):
601
+ self.download_update(e)
602
+ close_dialog(e)
603
+
604
+ # Create update dialog
605
+ dialog = ft.AlertDialog(
606
+ title=ft.Row([
607
+ ft.Icon(ft.Icons.SYSTEM_UPDATE, color=ft.Colors.BLUE, size=30),
608
+ ft.Text("Update Available", weight=ft.FontWeight.BOLD)
609
+ ], spacing=10),
610
+ content=ft.Column([
611
+ ft.Text(f"Version {self.update_info['version']} is now available!"),
612
+ ft.Text("Would you like to download the update?", color=ft.Colors.GREY_600)
613
+ ], spacing=10, tight=True),
614
+ actions=[
615
+ ft.TextButton("View Release Notes", on_click=view_release_and_close),
616
+ ft.ElevatedButton("Download Update", on_click=download_and_close),
617
+ ft.TextButton("Later", on_click=close_dialog),
618
+ ],
619
+ modal=True
620
+ )
621
+
622
+ self.page.overlay.append(dialog)
623
+ dialog.open = True
624
+ self.page.update()
625
+
626
+ def show_up_to_date_message(self):
627
+ """Show message that app is up to date."""
628
+ def close_dialog(e):
629
+ dialog.open = False
630
+ self.page.update()
631
+
632
+ # Create up-to-date dialog
633
+ dialog = ft.AlertDialog(
634
+ title=ft.Row([
635
+ ft.Icon(ft.Icons.CHECK_CIRCLE, color=ft.Colors.GREEN, size=30),
636
+ ft.Text("Up to Date", weight=ft.FontWeight.BOLD)
637
+ ], spacing=10),
638
+ content=ft.Text("You're running the latest version!"),
639
+ actions=[
640
+ ft.ElevatedButton("OK", on_click=close_dialog),
641
+ ],
642
+ modal=True
643
+ )
644
+
645
+ self.page.overlay.append(dialog)
646
+ dialog.open = True
647
+ self.page.update()
648
+
649
+ def view_release_notes(self, e):
650
+ """Open release notes in browser."""
651
+ if self.update_info:
652
+ import webbrowser
653
+ webbrowser.open(self.update_info['release_url'])
654
+
655
+ def download_update(self, e):
656
+ """Handle update download."""
657
+ if not self.update_info or not self.update_info.get('download_url'):
658
+ return
659
+
660
+ # For now, open download URL in browser
661
+ # In a more advanced implementation, you could download in-app
662
+ import webbrowser
663
+ webbrowser.open(self.update_info['download_url'])
664
+
665
+ # Show download instructions
666
+ self.show_download_instructions()
667
+
668
+ def show_download_instructions(self):
669
+ """Show instructions for installing the downloaded update."""
670
+ def close_dialog(e):
671
+ dialog.open = False
672
+ self.page.update()
673
+
674
+ instructions = {
675
+ "darwin": "1. Download will start in your browser\n2. Open the downloaded .dmg file\n3. Drag the app to Applications folder\n4. Restart the application",
676
+ "windows": "1. Download will start in your browser\n2. Run the downloaded .exe installer\n3. Follow the installation wizard\n4. Restart the application",
677
+ }
678
+
679
+ import platform
680
+ current_platform = platform.system().lower()
681
+ instruction_text = instructions.get(current_platform, "Download the update and follow installation instructions.")
682
+
683
+ dialog = ft.AlertDialog(
684
+ title=ft.Text("Update Installation"),
685
+ content=ft.Column([
686
+ ft.Text(f"Version {self.update_info['version']} Download Instructions:"),
687
+ ft.Text(instruction_text, selectable=True),
688
+ ft.Divider(),
689
+ ft.Text("Release Notes:", weight=ft.FontWeight.BOLD),
690
+ ft.Text(
691
+ self.update_info.get('release_notes', 'No release notes available.')[:500] +
692
+ ('...' if len(self.update_info.get('release_notes', '')) > 500 else ''),
693
+ selectable=True
694
+ )
695
+ ], width=400, height=300, scroll=ft.ScrollMode.AUTO),
696
+ actions=[
697
+ ft.TextButton("OK", on_click=close_dialog)
698
+ ],
699
+ modal=True
700
+ )
701
+
702
+ self.page.overlay.append(dialog)
703
+ dialog.open = True
704
+ self.page.update()
705
+
706
+
707
+
708
+ def check_for_updates_button(self):
709
+ """Check for updates when button is clicked."""
710
+ self.check_for_updates_manual()
711
+
712
+
713
+ def main():
714
+ """Main entry point for the GUI application"""
715
+ os.environ.setdefault("ENTRYPOINT", "GUI")
716
+ make_migrations()
717
+
718
+ def app_main(page: ft.Page):
719
+ TBRDealFinderApp(page)
720
+
721
+ ft.app(target=app_main)
722
+
723
+
724
+ if __name__ == "__main__":
725
+ main()