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.
- tbr_deal_finder/__init__.py +1 -5
- tbr_deal_finder/__main__.py +7 -0
- tbr_deal_finder/book.py +16 -8
- tbr_deal_finder/cli.py +13 -27
- tbr_deal_finder/config.py +2 -2
- tbr_deal_finder/desktop_updater.py +147 -0
- tbr_deal_finder/gui/__init__.py +0 -0
- tbr_deal_finder/gui/main.py +725 -0
- tbr_deal_finder/gui/pages/__init__.py +1 -0
- tbr_deal_finder/gui/pages/all_books.py +93 -0
- tbr_deal_finder/gui/pages/all_deals.py +63 -0
- tbr_deal_finder/gui/pages/base_book_page.py +291 -0
- tbr_deal_finder/gui/pages/book_details.py +604 -0
- tbr_deal_finder/gui/pages/latest_deals.py +370 -0
- tbr_deal_finder/gui/pages/settings.py +389 -0
- tbr_deal_finder/retailer/amazon.py +58 -7
- tbr_deal_finder/retailer/amazon_custom_auth.py +79 -0
- tbr_deal_finder/retailer/audible.py +2 -1
- tbr_deal_finder/retailer/chirp.py +55 -11
- tbr_deal_finder/retailer/kindle.py +31 -19
- tbr_deal_finder/retailer/librofm.py +53 -20
- tbr_deal_finder/retailer/models.py +31 -1
- tbr_deal_finder/retailer_deal.py +38 -14
- tbr_deal_finder/tracked_books.py +24 -18
- tbr_deal_finder/utils.py +64 -2
- tbr_deal_finder/version_check.py +40 -0
- {tbr_deal_finder-0.2.1.dist-info → tbr_deal_finder-0.3.1.dist-info}/METADATA +18 -87
- tbr_deal_finder-0.3.1.dist-info/RECORD +38 -0
- {tbr_deal_finder-0.2.1.dist-info → tbr_deal_finder-0.3.1.dist-info}/entry_points.txt +1 -0
- tbr_deal_finder-0.2.1.dist-info/RECORD +0 -25
- {tbr_deal_finder-0.2.1.dist-info → tbr_deal_finder-0.3.1.dist-info}/WHEEL +0 -0
- {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()
|