-
-
Notifications
You must be signed in to change notification settings - Fork 18
Expand file tree
/
Copy pathmain.py
More file actions
450 lines (373 loc) · 16.5 KB
/
main.py
File metadata and controls
450 lines (373 loc) · 16.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
"""
DLSS Updater - Flet UI Entry Point
Async/await-based modern Material Design interface
"""
import sys
import os
import sysconfig
# Free-threaded Python 3.14 GIL configuration
# Note: PyInstaller spec file sets 'X gil=0' OPTION to force GIL disabled at bootloader level
# This env var is for child processes and documentation purposes
_is_free_threaded = bool(sysconfig.get_config_var('Py_GIL_DISABLED'))
if _is_free_threaded:
# Set environment variable for child processes and to indicate intent
os.environ['PYTHON_GIL'] = '0'
# Suppress GIL re-enablement warnings from C extensions that haven't declared compatibility
# This is defense-in-depth; the PyInstaller OPTION should handle most cases
import warnings
warnings.filterwarnings(
"ignore",
message=".*GIL has been enabled.*",
category=RuntimeWarning
)
# Install faster event loop based on platform (must be done before any asyncio usage)
if sys.platform == 'win32':
try:
import winloop
winloop.install()
except ImportError:
pass # winloop not installed, use default event loop
elif sys.platform == 'linux':
try:
import uvloop
uvloop.install()
except ImportError:
pass # uvloop not installed, use default event loop
import asyncio
import logging
import flet as ft
# Core imports
from dlss_updater.logger import setup_logger
from dlss_updater.utils import check_dependencies, is_admin, run_as_admin # Admin functions used on Windows only
from dlss_updater.platform_utils import IS_WINDOWS, IS_LINUX
from dlss_updater.ui_flet.views.main_view import MainView
from dlss_updater.task_registry import register_task
def ensure_flet_directories():
"""
Ensure Flet framework directories exist before initialization.
On Linux (especially Flatpak), Flet expects ~/.flet/bin to exist but may not
be able to create it due to sandbox restrictions. We create it proactively
to prevent FileNotFoundError during Flet initialization.
See: https://github.com/Recol/DLSS-Updater/issues/122
https://github.com/Recol/DLSS-Updater/issues/127
"""
if IS_LINUX:
from pathlib import Path
flet_bin_dir = Path.home() / ".flet" / "bin"
try:
flet_bin_dir.mkdir(parents=True, exist_ok=True)
except OSError as e:
# Log but don't fail - Flet might still work
import logging
logging.getLogger("DLSSUpdater").warning(
f"Could not create Flet directory {flet_bin_dir}: {e}"
)
def _detect_initial_theme() -> bool:
"""
Load theme preference from config or default to dark.
Note: OS theme detection (darkdetect) was removed due to blocking calls
that caused GUI freezes on Linux Flatpak/Wayland and Windows with AV software.
See: https://github.com/Recol/DLSS-Updater/issues/XXX
Returns:
True if dark mode should be used, False for light mode
"""
try:
from dlss_updater.config import config_manager
# Load saved theme preference (user override or previous selection)
theme_pref = config_manager.get("Appearance", "theme", fallback="dark")
return theme_pref == "dark"
except Exception:
return True # Default to dark on any error
async def main(page: ft.Page):
"""
Main async entry point for the Flet application
Args:
page: The Flet page instance
"""
# Configure page
page.title = "DLSS Updater"
page.window.min_width = 700
page.window.min_height = 500
page.padding = 0
page.spacing = 0
# Restore saved window state (position, size, maximized)
from dlss_updater.config import config_manager
_saved_window = config_manager.get_window_state()
page.window.width = max(_saved_window["width"], 700)
page.window.height = max(_saved_window["height"], 500)
if _saved_window["top"] is not None and _saved_window["left"] is not None:
page.window.top = _saved_window["top"]
page.window.left = _saved_window["left"]
if _saved_window["maximized"]:
page.window.maximized = True
# Detect initial theme (OS preference or user override)
is_dark = _detect_initial_theme()
# Set theme based on detection
page.theme_mode = ft.ThemeMode.DARK if is_dark else ft.ThemeMode.LIGHT
page.bgcolor = "#2E2E2E" if is_dark else "#FAFBFC"
# Create Material 3 theme with custom colors
page.theme = ft.Theme(
color_scheme_seed="#2D6E88" if is_dark else "#1A5A70",
use_material3=True,
)
page.dark_theme = ft.Theme(
color_scheme_seed="#2D6E88",
use_material3=True,
)
# Get logger instance
logger = logging.getLogger("DLSSUpdater")
logger.info("DLSS Updater (Flet) starting...")
# Create startup loading overlay with theme-aware colors
from dlss_updater.ui_flet.theme.colors import MD3Colors
startup_overlay = ft.Container(
content=ft.Column(
controls=[
ft.ProgressRing(color=MD3Colors.get_primary(is_dark), width=50, height=50),
ft.Text("Loading...", size=16, color=MD3Colors.get_on_surface(is_dark)),
],
alignment=ft.MainAxisAlignment.CENTER,
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
spacing=20,
),
expand=True,
alignment=ft.Alignment.CENTER,
bgcolor=MD3Colors.get_background(is_dark),
)
page.add(startup_overlay)
page.update()
# Initialize whitelist in background (non-blocking)
async def init_whitelist():
"""Initialize whitelist asynchronously"""
try:
from dlss_updater.whitelist import initialize_whitelist
await initialize_whitelist()
except asyncio.CancelledError:
logger.info("Whitelist initialization cancelled")
raise
except Exception as e:
logger.warning(f"Failed to initialize whitelist: {e}")
# Non-critical, continue without whitelist
# Run database and DLL cache initialization in parallel
async def init_database():
"""Initialize database asynchronously"""
try:
from dlss_updater.database import db_manager
logger.info("Initializing database...")
await db_manager.initialize()
logger.info("Database initialized successfully")
# Run cleanup operations in parallel
cleanup_tasks = []
cleanup_tasks.append(db_manager.cleanup_duplicate_backups())
cleanup_tasks.append(db_manager.cleanup_duplicate_games())
results = await asyncio.gather(*cleanup_tasks, return_exceptions=True)
for i, result in enumerate(results):
if isinstance(result, Exception):
logger.warning(f"Cleanup task {i} failed: {result}")
elif result and result > 0:
task_name = "backup" if i == 0 else "game"
logger.info(f"Cleaned up {result} duplicate {task_name} entries on startup")
except Exception as e:
logger.error(f"Failed to initialize database: {e}", exc_info=True)
# Continue without database - app should still work
async def init_dll_cache():
"""Initialize DLL cache asynchronously with progress notification"""
snackbar = main_view.get_dll_cache_snackbar()
try:
from dlss_updater.dll_repository import initialize_dll_cache_async
await snackbar.show_initializing()
logger.info("Initializing DLL cache (async)...")
async def on_progress(current, total, message):
await snackbar.update_progress(current, total, message)
await initialize_dll_cache_async(progress_callback=on_progress)
logger.info("DLL cache initialized successfully")
await snackbar.show_complete()
except asyncio.CancelledError:
logger.info("DLL cache initialization cancelled")
raise
except Exception as e:
logger.error(f"Failed to initialize DLL cache: {e}", exc_info=True)
await snackbar.show_error(f"Cache init failed: {str(e)[:40]}")
async def update_steam_list():
"""Update Steam app list in background"""
try:
from dlss_updater.steam_integration import update_steam_app_list_if_needed
await update_steam_app_list_if_needed()
except asyncio.CancelledError:
logger.info("Steam list update cancelled")
raise
except Exception as e:
logger.warning(f"Failed to update Steam app list: {e}")
# Non-critical, continue
# Initialize database first (fast operation, needed before UI)
await init_database()
# Clear startup overlay and show main UI
page.controls.clear()
page.update()
# Create and add main view (show UI immediately)
main_view = MainView(page, logger)
await main_view.initialize()
# Add to page
page.add(main_view)
page.update()
# Register shutdown handler for graceful cleanup
# IMPORTANT: Use prevent_close + on_event + destroy pattern for proper process termination
# page.on_close alone does NOT terminate the Flet/Flutter subprocess, leaving the app running
# See: https://flet.dev/docs/controls/page/#app-exit-confirmation
_shutdown_in_progress = False
# --- Window state persistence (debounced save on resize/move) ---
_window_save_task: asyncio.Task | None = None
def _save_window_state_now():
"""Capture current window geometry and persist to config (sync)."""
if page.window.maximized:
# Only persist the maximized flag; keep the last normal geometry
# so restoring from maximized lands at the previous size/position.
try:
prev = config_manager.get_window_state()
config_manager.save_window_state(
width=prev["width"],
height=prev["height"],
top=prev["top"],
left=prev["left"],
maximized=True,
)
except Exception:
pass
return
try:
config_manager.save_window_state(
width=page.window.width or 900,
height=page.window.height or 700,
top=page.window.top,
left=page.window.left,
maximized=False,
)
except Exception as ex:
logger.debug(f"Failed to save window state: {ex}")
async def _debounced_window_save():
"""Save window state after 300ms of no further changes."""
await asyncio.sleep(0.3)
_save_window_state_now()
def _schedule_window_save():
"""Schedule a debounced save of window state."""
nonlocal _window_save_task
if _window_save_task and not _window_save_task.done():
_window_save_task.cancel()
_window_save_task = register_task(
asyncio.create_task(_debounced_window_save()),
"window_state_save"
)
async def handle_window_event(e: ft.WindowEvent):
"""Handle window events: close requests and state persistence."""
nonlocal _shutdown_in_progress
if e.type == ft.WindowEventType.CLOSE:
if _shutdown_in_progress:
return # Already shutting down, avoid double-shutdown
_shutdown_in_progress = True
# Save final window state immediately before shutdown
_save_window_state_now()
# Show shutdown progress dialog for visual feedback
from dlss_updater.ui_flet.dialogs.shutdown_progress_dialog import ShutdownProgressDialog
progress_dialog = ShutdownProgressDialog(page, logger)
try:
progress_dialog.show()
# Run cleanup with progress callback
await main_view.shutdown(progress_callback=progress_dialog.update_step)
# Show completion state briefly
progress_dialog.show_complete()
except Exception as ex:
logger.error(f"Shutdown error: {ex}")
finally:
# Close dialog before destroying window
try:
progress_dialog.close()
except Exception:
pass
# CRITICAL: Destroy window to terminate the Flet/Flutter process
try:
await page.window.destroy()
except Exception:
# Force exit if destroy fails
import os
os._exit(0)
elif e.type in (
ft.WindowEventType.RESIZED,
ft.WindowEventType.MOVED,
):
_schedule_window_save()
elif e.type in (
ft.WindowEventType.MAXIMIZE,
ft.WindowEventType.UNMAXIMIZE,
):
# Save maximized state immediately (no debounce needed)
_save_window_state_now()
page.window.prevent_close = True
page.window.on_event = handle_window_event
# Debounced resize handler for high-DPI display optimization
# Prevents excessive layout recalculations during rapid window resizing
# while maintaining responsive behavior for different screen sizes
_resize_task: asyncio.Task | None = None
_resize_debounce_ms = 100 # Wait 100ms after last resize before updating
async def _debounced_resize_update():
"""Perform actual resize update after debounce delay."""
await asyncio.sleep(_resize_debounce_ms / 1000)
# ResponsiveRow handles layout automatically, just trigger update
if page and hasattr(page, 'update'):
try:
page.update()
except Exception:
pass # Page may be closing
def on_page_resized(e):
"""Debounced resize handler - prevents excessive updates during rapid resizing."""
nonlocal _resize_task
# Cancel any pending resize update
if _resize_task and not _resize_task.done():
_resize_task.cancel()
# Schedule new debounced update - register for proper shutdown
_resize_task = register_task(
asyncio.create_task(_debounced_resize_update()),
"resize_debounce"
)
page.on_resized = on_page_resized
logger.info("UI initialized successfully")
# Now initialize DLL cache, whitelist, and Steam list in background (after UI is visible)
# This allows the user to see the app immediately while heavy init happens
# Register tasks for graceful shutdown cancellation
register_task(asyncio.create_task(init_whitelist()), "init_whitelist")
register_task(asyncio.create_task(init_dll_cache()), "init_dll_cache")
register_task(asyncio.create_task(update_steam_list()), "update_steam_list")
def check_prerequisites():
"""Check dependencies and admin privileges before launching UI"""
logger = setup_logger()
# Check dependencies
logger.info("Checking dependencies...")
if not check_dependencies():
logger.error("Dependency check failed")
sys.exit(1)
# Check admin privileges - Windows requires elevation for writing to game directories
if IS_WINDOWS:
if not is_admin():
logger.warning("Application requires administrator privileges")
logger.info("Attempting to restart with admin rights...")
run_as_admin()
sys.exit(0)
logger.info("Admin privileges confirmed")
# Linux: No elevation needed - Flatpak sandboxing handles filesystem permissions
# DLL cache initialization is now done asynchronously after UI loads
# to avoid blocking the window from appearing
logger.info("DLL cache will be initialized after UI loads...")
return logger
if __name__ == "__main__":
# Check prerequisites before launching UI
check_prerequisites()
# Ensure Flet directories exist (Linux Flatpak fix for Issues #122 & #127)
ensure_flet_directories()
# Workaround for Flet bug: is_linux_server() only checks DISPLAY, not WAYLAND_DISPLAY
# On Wayland-only sessions (e.g., Fedora/Nobara), DISPLAY is not set, causing Flet
# to incorrectly detect a "headless server" and force web server mode on port 8000
# See: https://github.com/Recol/DLSS-Updater/issues/122
import os
if sys.platform == 'linux':
if os.environ.get('WAYLAND_DISPLAY') and not os.environ.get('DISPLAY'):
os.environ['DISPLAY'] = ':0' # Prevent Flet's web server fallback
# Launch Flet app with async main - uses ft.run() for Flet 0.80.4+
ft.run(main)