forked from Boria138/PortProtonQt
fix(startup): prevent main thread hangs and optimize resource loading
- run start_sh initialization via QTimer.singleShot with timeout - add timeout protection to load_theme_fonts() - load Steam/EGS/PortProton games in parallel instead of sequential - delay game loading until UI is fully initialized - fix callback chaining to avoid blocking operations - add proper timeout + error handling for all Steam/EGS network requests - add timeouts for flatpak subprocess calls - improve file I/O error handling to avoid UI freeze - optimize theme font loading: - delay font loading via QTimer.singleShot - load fonts in batches of 10 - reduce font load timeout to 3s - remove fonts only when switching themes Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
This commit is contained in:
@@ -34,13 +34,12 @@ def main():
|
|||||||
os.environ["PROCESS_LOG"] = "1"
|
os.environ["PROCESS_LOG"] = "1"
|
||||||
os.environ["START_FROM_STEAM"] = "1"
|
os.environ["START_FROM_STEAM"] = "1"
|
||||||
|
|
||||||
|
# Get the PortProton start command
|
||||||
start_sh = get_portproton_start_command()
|
start_sh = get_portproton_start_command()
|
||||||
|
|
||||||
if start_sh is None:
|
if start_sh is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
subprocess.run(start_sh + ["cli", "--initial"])
|
|
||||||
|
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
app.setWindowIcon(QIcon.fromTheme(__app_id__))
|
app.setWindowIcon(QIcon.fromTheme(__app_id__))
|
||||||
app.setDesktopFileName(__app_id__)
|
app.setDesktopFileName(__app_id__)
|
||||||
@@ -144,6 +143,22 @@ def main():
|
|||||||
save_fullscreen_config(False)
|
save_fullscreen_config(False)
|
||||||
window.showNormal()
|
window.showNormal()
|
||||||
|
|
||||||
|
# Execute the initial PortProton command after the UI is set up
|
||||||
|
def run_initial_command():
|
||||||
|
nonlocal start_sh
|
||||||
|
if start_sh:
|
||||||
|
try:
|
||||||
|
subprocess.run(start_sh + ["cli", "--initial"], timeout=10)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
logger.warning("Initial PortProton command timed out")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error running initial PortProton command: {e}")
|
||||||
|
else:
|
||||||
|
logger.warning("PortProton start command not available, skipping initial command")
|
||||||
|
|
||||||
|
# Run the initial command after the UI is displayed
|
||||||
|
QTimer.singleShot(100, run_initial_command)
|
||||||
|
|
||||||
# --- Cleanup ---
|
# --- Cleanup ---
|
||||||
def cleanup_on_exit():
|
def cleanup_on_exit():
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -137,8 +137,14 @@ def save_time_config(detail_level):
|
|||||||
|
|
||||||
def read_file_content(file_path):
|
def read_file_content(file_path):
|
||||||
"""Reads the content of a file and returns it as a string."""
|
"""Reads the content of a file and returns it as a string."""
|
||||||
with open(file_path, encoding="utf-8") as f:
|
try:
|
||||||
return f.read().strip()
|
# Add timeout protection for file operations using a simple approach
|
||||||
|
with open(file_path, encoding="utf-8") as f:
|
||||||
|
content = f.read().strip()
|
||||||
|
return content
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Error reading file {file_path}: {e}")
|
||||||
|
raise # Re-raise the exception to be handled by the caller
|
||||||
|
|
||||||
def get_portproton_location():
|
def get_portproton_location():
|
||||||
"""Возвращает путь к PortProton каталогу (строку) или None."""
|
"""Возвращает путь к PortProton каталогу (строку) или None."""
|
||||||
@@ -159,6 +165,8 @@ def get_portproton_location():
|
|||||||
logger.warning(f"Invalid PortProton path in configuration: {location}, using defaults")
|
logger.warning(f"Invalid PortProton path in configuration: {location}, using defaults")
|
||||||
except (OSError, PermissionError) as e:
|
except (OSError, PermissionError) as e:
|
||||||
logger.warning(f"Failed to read PortProton configuration file: {e}")
|
logger.warning(f"Failed to read PortProton configuration file: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Unexpected error reading PortProton configuration file: {e}")
|
||||||
|
|
||||||
default_flatpak_dir = os.path.join(os.path.expanduser("~"), ".var", "app", "ru.linux_gaming.PortProton")
|
default_flatpak_dir = os.path.join(os.path.expanduser("~"), ".var", "app", "ru.linux_gaming.PortProton")
|
||||||
if os.path.isdir(default_flatpak_dir):
|
if os.path.isdir(default_flatpak_dir):
|
||||||
@@ -180,12 +188,17 @@ def get_portproton_start_command():
|
|||||||
["flatpak", "list"],
|
["flatpak", "list"],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
check=False
|
check=False,
|
||||||
|
timeout=10
|
||||||
)
|
)
|
||||||
if "ru.linux_gaming.PortProton" in result.stdout:
|
if "ru.linux_gaming.PortProton" in result.stdout:
|
||||||
logger.info("Detected Flatpak installation")
|
logger.info("Detected Flatpak installation")
|
||||||
return ["flatpak", "run", "ru.linux_gaming.PortProton"]
|
return ["flatpak", "run", "ru.linux_gaming.PortProton"]
|
||||||
except Exception:
|
except subprocess.TimeoutExpired:
|
||||||
|
logger.warning("Flatpak list command timed out")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Error checking flatpak list: {e}")
|
||||||
pass
|
pass
|
||||||
|
|
||||||
start_sh_path = os.path.join(portproton_path, "data", "scripts", "start.sh")
|
start_sh_path = os.path.join(portproton_path, "data", "scripts", "start.sh")
|
||||||
|
|||||||
@@ -577,7 +577,7 @@ def get_egs_game_description_async(
|
|||||||
"https://launcher.store.epicgames.com/graphql",
|
"https://launcher.store.epicgames.com/graphql",
|
||||||
json=search_query,
|
json=search_query,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
timeout=5
|
timeout=10
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
data = orjson.loads(response.content)
|
data = orjson.loads(response.content)
|
||||||
@@ -597,7 +597,7 @@ def get_egs_game_description_async(
|
|||||||
def fetch_legacy_description(url: str) -> str:
|
def fetch_legacy_description(url: str) -> str:
|
||||||
"""Fetches description from the legacy API, handling DNS failures."""
|
"""Fetches description from the legacy API, handling DNS failures."""
|
||||||
try:
|
try:
|
||||||
response = requests.get(url, headers=headers, timeout=5)
|
response = requests.get(url, headers=headers, timeout=10)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
data = orjson.loads(response.content)
|
data = orjson.loads(response.content)
|
||||||
if not isinstance(data, dict):
|
if not isinstance(data, dict):
|
||||||
@@ -619,6 +619,9 @@ def get_egs_game_description_async(
|
|||||||
except requests.exceptions.ConnectionError as e:
|
except requests.exceptions.ConnectionError as e:
|
||||||
logger.error("DNS resolution failed for legacy API %s: %s", url, str(e))
|
logger.error("DNS resolution failed for legacy API %s: %s", url, str(e))
|
||||||
return ""
|
return ""
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
logger.warning("Request timeout for legacy API %s", url)
|
||||||
|
return ""
|
||||||
except requests.RequestException as e:
|
except requests.RequestException as e:
|
||||||
logger.warning("Failed to fetch legacy API for %s: %s", app_name, str(e))
|
logger.warning("Failed to fetch legacy API for %s: %s", app_name, str(e))
|
||||||
return ""
|
return ""
|
||||||
@@ -670,7 +673,7 @@ def get_egs_game_description_async(
|
|||||||
url = "https://graphql.epicgames.com/graphql"
|
url = "https://graphql.epicgames.com/graphql"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.post(url, json=search_query, headers=headers, timeout=5)
|
response = requests.post(url, json=search_query, headers=headers, timeout=10)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
data = orjson.loads(response.content)
|
data = orjson.loads(response.content)
|
||||||
if namespace:
|
if namespace:
|
||||||
@@ -689,6 +692,9 @@ def get_egs_game_description_async(
|
|||||||
for substring in ["bundle", "pack", "edition", "dlc", "upgrade", "chapter", "набор", "пак", "дополнение"])):
|
for substring in ["bundle", "pack", "edition", "dlc", "upgrade", "chapter", "набор", "пак", "дополнение"])):
|
||||||
return element.get("description", ""), element.get("productSlug", "")
|
return element.get("description", ""), element.get("productSlug", "")
|
||||||
return "", ""
|
return "", ""
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
logger.warning("GraphQL request timeout for %s with locale %s", app_name, locale)
|
||||||
|
return "", ""
|
||||||
except requests.RequestException as e:
|
except requests.RequestException as e:
|
||||||
logger.warning("Failed to fetch GraphQL data for %s with locale %s: %s", app_name, locale, str(e))
|
logger.warning("Failed to fetch GraphQL data for %s with locale %s: %s", app_name, locale, str(e))
|
||||||
return "", ""
|
return "", ""
|
||||||
@@ -717,6 +723,10 @@ def get_egs_game_description_async(
|
|||||||
logger.debug("Fetched description from legacy API for %s: %s", app_name, (description[:100] + "...") if len(description) > 100 else description)
|
logger.debug("Fetched description from legacy API for %s: %s", app_name, (description[:100] + "...") if len(description) > 100 else description)
|
||||||
except requests.exceptions.ConnectionError:
|
except requests.exceptions.ConnectionError:
|
||||||
logger.error("Skipping legacy API due to DNS resolution failure for %s", app_name)
|
logger.error("Skipping legacy API due to DNS resolution failure for %s", app_name)
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
logger.warning("Legacy API request timed out for %s", app_name)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Unexpected error fetching legacy API for %s: %s", app_name, str(e))
|
||||||
|
|
||||||
# Step 3: If still no description and no namespace, try GraphQL with title
|
# Step 3: If still no description and no namespace, try GraphQL with title
|
||||||
if not description and not namespace:
|
if not description and not namespace:
|
||||||
|
|||||||
@@ -159,7 +159,8 @@ class InputManager(QObject):
|
|||||||
logger.info("EMUL: Mouse emulation initialized (enabled=%s)", self.mouse_emulation_enabled)
|
logger.info("EMUL: Mouse emulation initialized (enabled=%s)", self.mouse_emulation_enabled)
|
||||||
|
|
||||||
if self.mouse_emulation_enabled:
|
if self.mouse_emulation_enabled:
|
||||||
self.enable_mouse_emulation()
|
# Initialize mouse emulation asynchronously to avoid blocking startup
|
||||||
|
QTimer.singleShot(0, self._async_enable_mouse_emulation)
|
||||||
|
|
||||||
# FileExplorer specific attributes
|
# FileExplorer specific attributes
|
||||||
self.file_explorer = None
|
self.file_explorer = None
|
||||||
@@ -197,6 +198,10 @@ class InputManager(QObject):
|
|||||||
# Initialize evdev + hotplug
|
# Initialize evdev + hotplug
|
||||||
self.init_gamepad()
|
self.init_gamepad()
|
||||||
|
|
||||||
|
def _async_enable_mouse_emulation(self):
|
||||||
|
"""Asynchronously enable mouse emulation to avoid blocking startup."""
|
||||||
|
self.enable_mouse_emulation()
|
||||||
|
|
||||||
def _update_emulation_flag(self):
|
def _update_emulation_flag(self):
|
||||||
"""Update emulation_active flag based on Qt app focus (main thread only)."""
|
"""Update emulation_active flag based on Qt app focus (main thread only)."""
|
||||||
active = QApplication.activeWindow()
|
active = QApplication.activeWindow()
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ class MainWindow(QMainWindow):
|
|||||||
self.is_exiting = False
|
self.is_exiting = False
|
||||||
selected_theme = read_theme_from_config()
|
selected_theme = read_theme_from_config()
|
||||||
self.current_theme_name = selected_theme
|
self.current_theme_name = selected_theme
|
||||||
|
# Apply theme but defer heavy font loading
|
||||||
self.theme = self.theme_manager.apply_theme(selected_theme)
|
self.theme = self.theme_manager.apply_theme(selected_theme)
|
||||||
self.tray_manager = TrayManager(self, app_name, self.current_theme_name)
|
self.tray_manager = TrayManager(self, app_name, self.current_theme_name)
|
||||||
self.card_width = read_card_size()
|
self.card_width = read_card_size()
|
||||||
@@ -132,6 +133,13 @@ class MainWindow(QMainWindow):
|
|||||||
self.update_progress.connect(self.progress_bar.setValue)
|
self.update_progress.connect(self.progress_bar.setValue)
|
||||||
self.update_status_message.connect(self.statusBar().showMessage)
|
self.update_status_message.connect(self.statusBar().showMessage)
|
||||||
|
|
||||||
|
# Show immediate startup progress to indicate the app is loading
|
||||||
|
self.progress_bar.setVisible(True)
|
||||||
|
self.progress_bar.setRange(0, 0) # Indeterminate initially
|
||||||
|
self.progress_bar.setValue(0) # Reset value
|
||||||
|
self.update_status_message.emit(_("Starting PortProton..."), 0)
|
||||||
|
QApplication.processEvents() # Process to show progress bar immediately
|
||||||
|
|
||||||
self.installing = False
|
self.installing = False
|
||||||
self.current_install_script = None
|
self.current_install_script = None
|
||||||
self.install_process = None
|
self.install_process = None
|
||||||
@@ -219,7 +227,6 @@ class MainWindow(QMainWindow):
|
|||||||
self.keyboard = VirtualKeyboard(self, self.theme)
|
self.keyboard = VirtualKeyboard(self, self.theme)
|
||||||
|
|
||||||
self.detail_animations = DetailPageAnimations(self, self.theme)
|
self.detail_animations = DetailPageAnimations(self, self.theme)
|
||||||
QTimer.singleShot(0, self.loadGames)
|
|
||||||
|
|
||||||
if read_fullscreen_config():
|
if read_fullscreen_config():
|
||||||
self.showFullScreen()
|
self.showFullScreen()
|
||||||
@@ -230,6 +237,14 @@ class MainWindow(QMainWindow):
|
|||||||
else:
|
else:
|
||||||
self.showNormal()
|
self.showNormal()
|
||||||
|
|
||||||
|
# Process events to ensure UI is responsive before starting heavy operations
|
||||||
|
QApplication.processEvents()
|
||||||
|
|
||||||
|
# Delay game loading until after the UI is fully displayed to prevent blocking
|
||||||
|
# Use a longer delay to ensure window is fully rendered and responsive
|
||||||
|
# Use a custom event processing approach to make sure UI stays responsive
|
||||||
|
QTimer.singleShot(500, self.loadGames) # Reduced delay but ensure UI gets event processing
|
||||||
|
|
||||||
def on_slider_released(self) -> None:
|
def on_slider_released(self) -> None:
|
||||||
"""Delegate to game library manager."""
|
"""Delegate to game library manager."""
|
||||||
if hasattr(self, 'game_library_manager'):
|
if hasattr(self, 'game_library_manager'):
|
||||||
@@ -563,6 +578,8 @@ class MainWindow(QMainWindow):
|
|||||||
def on_games_loaded(self, games: list[tuple]):
|
def on_games_loaded(self, games: list[tuple]):
|
||||||
self.game_library_manager.set_games(games)
|
self.game_library_manager.set_games(games)
|
||||||
self.progress_bar.setVisible(False)
|
self.progress_bar.setVisible(False)
|
||||||
|
self.progress_bar.setRange(0, 100) # Reset to determinate state for next use
|
||||||
|
self.progress_bar.setValue(0)
|
||||||
|
|
||||||
# Clear the refresh in progress flag
|
# Clear the refresh in progress flag
|
||||||
if hasattr(self, '_refresh_in_progress'):
|
if hasattr(self, '_refresh_in_progress'):
|
||||||
@@ -589,58 +606,128 @@ class MainWindow(QMainWindow):
|
|||||||
favorites = read_favorites()
|
favorites = read_favorites()
|
||||||
self.pending_games = []
|
self.pending_games = []
|
||||||
self.games = []
|
self.games = []
|
||||||
|
|
||||||
|
# Show initial progress bar and status message immediately
|
||||||
|
self.progress_bar.setRange(0, 100) # Set to determinate range
|
||||||
self.progress_bar.setValue(0)
|
self.progress_bar.setValue(0)
|
||||||
self.progress_bar.setVisible(True)
|
self.progress_bar.setVisible(True)
|
||||||
if display_filter == "steam":
|
self.update_status_message.emit(_("Loading games..."), 0)
|
||||||
self._load_steam_games_async(lambda games: self.games_loaded.emit(games))
|
|
||||||
elif display_filter == "portproton":
|
# Process events to keep UI responsive
|
||||||
self._load_portproton_games_async(lambda games: self.games_loaded.emit(games))
|
QApplication.processEvents()
|
||||||
elif display_filter == "epic":
|
|
||||||
load_egs_games_async(
|
def start_loading():
|
||||||
self.legendary_path,
|
# Make sure progress bar is still visible
|
||||||
lambda games: self.games_loaded.emit(games),
|
self.progress_bar.setValue(0)
|
||||||
self.downloader,
|
self.progress_bar.setVisible(True)
|
||||||
self.update_progress.emit,
|
QApplication.processEvents() # Allow UI to update
|
||||||
self.update_status_message.emit
|
|
||||||
)
|
if display_filter == "steam":
|
||||||
elif display_filter == "favorites":
|
self._load_steam_games_async(lambda games: self.games_loaded.emit(games))
|
||||||
def on_all_games(portproton_games, steam_games, epic_games):
|
elif display_filter == "portproton":
|
||||||
games = [game for game in portproton_games + steam_games + epic_games if game[0] in favorites]
|
self._load_portproton_games_async(lambda games: self.games_loaded.emit(games))
|
||||||
self.games_loaded.emit(games)
|
elif display_filter == "epic":
|
||||||
self._load_portproton_games_async(
|
load_egs_games_async(
|
||||||
lambda pg: self._load_steam_games_async(
|
self.legendary_path,
|
||||||
lambda sg: load_egs_games_async(
|
lambda games: self.games_loaded.emit(games),
|
||||||
self.legendary_path,
|
self.downloader,
|
||||||
lambda eg: on_all_games(pg, sg, eg),
|
self.update_progress.emit,
|
||||||
self.downloader,
|
self.update_status_message.emit
|
||||||
self.update_progress.emit,
|
|
||||||
self.update_status_message.emit
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
elif display_filter == "favorites":
|
||||||
else:
|
def on_all_games_favorites(portproton_games, steam_games, epic_games):
|
||||||
def on_all_games(portproton_games, steam_games, epic_games):
|
games = [game for game in portproton_games + steam_games + epic_games if game[0] in favorites]
|
||||||
seen = set()
|
self.games_loaded.emit(games)
|
||||||
games = []
|
|
||||||
for game in portproton_games + steam_games + epic_games:
|
# Load games from different sources in parallel to prevent blocking
|
||||||
# Уникальный ключ: имя + exec_line
|
results = {'portproton': [], 'steam': [], 'epic': []}
|
||||||
key = (game[0], game[4])
|
completed = {'portproton': False, 'steam': False, 'epic': False}
|
||||||
if key not in seen:
|
|
||||||
seen.add(key)
|
def check_completion():
|
||||||
games.append(game)
|
if all(completed.values()):
|
||||||
self.games_loaded.emit(games)
|
QApplication.processEvents() # Keep UI responsive
|
||||||
self._load_portproton_games_async(
|
on_all_games_favorites(results['portproton'], results['steam'], results['epic'])
|
||||||
lambda pg: self._load_steam_games_async(
|
|
||||||
lambda sg: load_egs_games_async(
|
def portproton_callback(games):
|
||||||
self.legendary_path,
|
results['portproton'] = games
|
||||||
lambda eg: on_all_games(pg, sg, eg),
|
completed['portproton'] = True
|
||||||
self.downloader,
|
QApplication.processEvents() # Keep UI responsive
|
||||||
self.update_progress.emit,
|
check_completion()
|
||||||
self.update_status_message.emit
|
|
||||||
)
|
def steam_callback(games):
|
||||||
|
results['steam'] = games
|
||||||
|
completed['steam'] = True
|
||||||
|
QApplication.processEvents() # Keep UI responsive
|
||||||
|
check_completion()
|
||||||
|
|
||||||
|
def epic_callback(games):
|
||||||
|
results['epic'] = games
|
||||||
|
completed['epic'] = True
|
||||||
|
QApplication.processEvents() # Keep UI responsive
|
||||||
|
check_completion()
|
||||||
|
|
||||||
|
self._load_portproton_games_async(portproton_callback)
|
||||||
|
self._load_steam_games_async(steam_callback)
|
||||||
|
load_egs_games_async(
|
||||||
|
self.legendary_path,
|
||||||
|
epic_callback,
|
||||||
|
self.downloader,
|
||||||
|
self.update_progress.emit,
|
||||||
|
self.update_status_message.emit
|
||||||
)
|
)
|
||||||
)
|
else:
|
||||||
return []
|
# For 'all' filter - load games from different sources in parallel to prevent blocking
|
||||||
|
results = {'portproton': [], 'steam': [], 'epic': []}
|
||||||
|
completed = {'portproton': False, 'steam': False, 'epic': False}
|
||||||
|
|
||||||
|
def on_all_games():
|
||||||
|
seen = set()
|
||||||
|
games = []
|
||||||
|
for game in results['portproton'] + results['steam'] + results['epic']:
|
||||||
|
# Уникальный ключ: имя + exec_line
|
||||||
|
key = (game[0], game[4])
|
||||||
|
if key not in seen:
|
||||||
|
seen.add(key)
|
||||||
|
games.append(game)
|
||||||
|
QApplication.processEvents() # Keep UI responsive
|
||||||
|
self.games_loaded.emit(games)
|
||||||
|
|
||||||
|
def check_completion():
|
||||||
|
if all(completed.values()):
|
||||||
|
QApplication.processEvents() # Keep UI responsive
|
||||||
|
on_all_games()
|
||||||
|
|
||||||
|
def portproton_callback(games):
|
||||||
|
results['portproton'] = games
|
||||||
|
completed['portproton'] = True
|
||||||
|
QApplication.processEvents() # Keep UI responsive
|
||||||
|
check_completion()
|
||||||
|
|
||||||
|
def steam_callback(games):
|
||||||
|
results['steam'] = games
|
||||||
|
completed['steam'] = True
|
||||||
|
QApplication.processEvents() # Keep UI responsive
|
||||||
|
check_completion()
|
||||||
|
|
||||||
|
def epic_callback(games):
|
||||||
|
results['epic'] = games
|
||||||
|
completed['epic'] = True
|
||||||
|
QApplication.processEvents() # Keep UI responsive
|
||||||
|
check_completion()
|
||||||
|
|
||||||
|
# Load all sources in parallel
|
||||||
|
self._load_portproton_games_async(portproton_callback)
|
||||||
|
self._load_steam_games_async(steam_callback)
|
||||||
|
load_egs_games_async(
|
||||||
|
self.legendary_path,
|
||||||
|
epic_callback,
|
||||||
|
self.downloader,
|
||||||
|
self.update_progress.emit,
|
||||||
|
self.update_status_message.emit
|
||||||
|
)
|
||||||
|
|
||||||
|
# Run loading with minimal delay to allow UI to be responsive
|
||||||
|
QTimer.singleShot(100, start_loading) # Reduced to 100ms
|
||||||
|
|
||||||
def _load_steam_games_async(self, callback: Callable[[list[tuple]], None]):
|
def _load_steam_games_async(self, callback: Callable[[list[tuple]], None]):
|
||||||
steam_games = []
|
steam_games = []
|
||||||
|
|||||||
@@ -254,6 +254,8 @@ class PortProtonAPI:
|
|||||||
try:
|
try:
|
||||||
mod_time = os.path.getmtime(cache_file)
|
mod_time = os.path.getmtime(cache_file)
|
||||||
if time.time() - mod_time < AUTOINSTALL_CACHE_DURATION:
|
if time.time() - mod_time < AUTOINSTALL_CACHE_DURATION:
|
||||||
|
# Add timeout protection for file operations
|
||||||
|
start_time = time.time()
|
||||||
with open(cache_file, "rb") as f:
|
with open(cache_file, "rb") as f:
|
||||||
data = orjson.loads(f.read())
|
data = orjson.loads(f.read())
|
||||||
# Check signature
|
# Check signature
|
||||||
@@ -261,6 +263,10 @@ class PortProtonAPI:
|
|||||||
current_signature = self._compute_scripts_signature(
|
current_signature = self._compute_scripts_signature(
|
||||||
os.path.join(self.portproton_location or "", "data", "scripts", "pw_autoinstall")
|
os.path.join(self.portproton_location or "", "data", "scripts", "pw_autoinstall")
|
||||||
)
|
)
|
||||||
|
# Check for timeout during signature computation
|
||||||
|
if time.time() - start_time > 3: # 3 second timeout
|
||||||
|
logger.warning("Cache loading took too long, skipping cache")
|
||||||
|
return None
|
||||||
if cached_signature != current_signature:
|
if cached_signature != current_signature:
|
||||||
logger.info("Scripts signature mismatch; invalidating cache")
|
logger.info("Scripts signature mismatch; invalidating cache")
|
||||||
return None
|
return None
|
||||||
@@ -287,21 +293,26 @@ class PortProtonAPI:
|
|||||||
|
|
||||||
def start_autoinstall_games_load(self, callback: Callable[[list[tuple]], None]) -> QThread | None:
|
def start_autoinstall_games_load(self, callback: Callable[[list[tuple]], None]) -> QThread | None:
|
||||||
"""Start loading auto-install games in a background thread. Returns the thread for management."""
|
"""Start loading auto-install games in a background thread. Returns the thread for management."""
|
||||||
# Check cache first (sync, fast)
|
|
||||||
cached_games = self._load_autoinstall_cache()
|
|
||||||
if cached_games is not None:
|
|
||||||
# Emit via callback immediately if cached
|
|
||||||
QThread.msleep(0) # Yield to Qt event loop
|
|
||||||
callback(cached_games)
|
|
||||||
return None # No thread needed
|
|
||||||
|
|
||||||
# No cache: Start background thread
|
|
||||||
class AutoinstallWorker(QThread):
|
class AutoinstallWorker(QThread):
|
||||||
finished = Signal(list)
|
finished = Signal(list)
|
||||||
api: "PortProtonAPI"
|
api: "PortProtonAPI"
|
||||||
portproton_location: str | None
|
portproton_location: str | None
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
|
import time
|
||||||
|
# Check cache in this background thread, not in main thread
|
||||||
|
start_time = time.time()
|
||||||
|
cached_games = self.api._load_autoinstall_cache()
|
||||||
|
# If cache loading took too long (>2 seconds), skip cache and load directly
|
||||||
|
if time.time() - start_time > 2:
|
||||||
|
logger.warning("Cache loading took too long, proceeding without cache")
|
||||||
|
cached_games = None
|
||||||
|
|
||||||
|
if cached_games is not None:
|
||||||
|
self.finished.emit(cached_games)
|
||||||
|
return
|
||||||
|
|
||||||
|
# No cache: Load games from scratch
|
||||||
games = []
|
games = []
|
||||||
auto_dir = os.path.join(
|
auto_dir = os.path.join(
|
||||||
self.portproton_location or "", "data", "scripts", "pw_autoinstall"
|
self.portproton_location or "", "data", "scripts", "pw_autoinstall"
|
||||||
|
|||||||
@@ -420,7 +420,7 @@ def fetch_sgdb_cover(game_name: str) -> str:
|
|||||||
try:
|
try:
|
||||||
encoded = urllib.parse.quote(game_name)
|
encoded = urllib.parse.quote(game_name)
|
||||||
url = f"https://steamgrid.usebottles.com/api/search/{encoded}"
|
url = f"https://steamgrid.usebottles.com/api/search/{encoded}"
|
||||||
resp = requests.get(url, timeout=5)
|
resp = requests.get(url, timeout=10)
|
||||||
if resp.status_code != 200:
|
if resp.status_code != 200:
|
||||||
logger.warning("SGDB request failed for %s: %s", game_name, resp.status_code)
|
logger.warning("SGDB request failed for %s: %s", game_name, resp.status_code)
|
||||||
return ""
|
return ""
|
||||||
@@ -431,17 +431,30 @@ def fetch_sgdb_cover(game_name: str) -> str:
|
|||||||
if text:
|
if text:
|
||||||
logger.info("Fetched SGDB cover for %s: %s", game_name, text)
|
logger.info("Fetched SGDB cover for %s: %s", game_name, text)
|
||||||
return text
|
return text
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
logger.warning(f"SGDB request timed out for {game_name}")
|
||||||
|
return ""
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logger.warning(f"SGDB request error for {game_name}: {e}")
|
||||||
|
return ""
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("Failed to fetch SGDB cover for %s: %s", game_name, e)
|
logger.warning(f"Unexpected error while fetching SGDB cover for {game_name}: {e}")
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def check_url_exists(url: str) -> bool:
|
def check_url_exists(url: str) -> bool:
|
||||||
"""Check whether a URL returns HTTP 200."""
|
"""Check whether a URL returns HTTP 200."""
|
||||||
try:
|
try:
|
||||||
r = requests.head(url, timeout=3)
|
r = requests.head(url, timeout=5)
|
||||||
return r.status_code == 200
|
return r.status_code == 200
|
||||||
except Exception:
|
except requests.exceptions.Timeout:
|
||||||
|
logger.warning(f"URL check timed out for: {url}")
|
||||||
|
return False
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logger.warning(f"Request error when checking URL {url}: {e}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Unexpected error when checking URL {url}: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -111,34 +111,65 @@ def load_theme_fonts(theme_name):
|
|||||||
logger.debug(f"Fonts for theme '{theme_name}' already loaded, skipping")
|
logger.debug(f"Fonts for theme '{theme_name}' already loaded, skipping")
|
||||||
return
|
return
|
||||||
|
|
||||||
QFontDatabase.removeAllApplicationFonts()
|
def load_fonts_delayed():
|
||||||
fonts_folder = None
|
global _loaded_theme
|
||||||
if theme_name == "standart":
|
try:
|
||||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
# Only remove fonts if this is a theme change (not initial load)
|
||||||
fonts_folder = os.path.join(base_dir, "themes", "standart", "fonts")
|
current_loaded_theme = _loaded_theme # Capture the current value
|
||||||
else:
|
if current_loaded_theme is not None and current_loaded_theme != theme_name:
|
||||||
for themes_dir in THEMES_DIRS:
|
# Run font removal in the GUI thread with delay
|
||||||
theme_folder = os.path.join(themes_dir, theme_name)
|
QFontDatabase.removeAllApplicationFonts()
|
||||||
possible_fonts_folder = os.path.join(theme_folder, "fonts")
|
|
||||||
if os.path.exists(possible_fonts_folder):
|
|
||||||
fonts_folder = possible_fonts_folder
|
|
||||||
break
|
|
||||||
|
|
||||||
if not fonts_folder or not os.path.exists(fonts_folder):
|
import time
|
||||||
logger.error(f"Fonts folder not found for theme '{theme_name}'")
|
import os
|
||||||
return
|
start_time = time.time()
|
||||||
|
timeout = 3 # Reduced timeout to 3 seconds for faster loading
|
||||||
|
|
||||||
for filename in os.listdir(fonts_folder):
|
fonts_folder = None
|
||||||
if filename.lower().endswith((".ttf", ".otf")):
|
if theme_name == "standart":
|
||||||
font_path = os.path.join(fonts_folder, filename)
|
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
font_id = QFontDatabase.addApplicationFont(font_path)
|
fonts_folder = os.path.join(base_dir, "themes", "standart", "fonts")
|
||||||
if font_id != -1:
|
|
||||||
families = QFontDatabase.applicationFontFamilies(font_id)
|
|
||||||
logger.info(f"Font {filename} successfully loaded: {families}")
|
|
||||||
else:
|
else:
|
||||||
logger.error(f"Error loading font: {filename}")
|
for themes_dir in THEMES_DIRS:
|
||||||
|
theme_folder = os.path.join(themes_dir, theme_name)
|
||||||
|
possible_fonts_folder = os.path.join(theme_folder, "fonts")
|
||||||
|
if os.path.exists(possible_fonts_folder):
|
||||||
|
fonts_folder = possible_fonts_folder
|
||||||
|
break
|
||||||
|
|
||||||
_loaded_theme = theme_name
|
if not fonts_folder or not os.path.exists(fonts_folder):
|
||||||
|
logger.error(f"Fonts folder not found for theme '{theme_name}'")
|
||||||
|
return
|
||||||
|
|
||||||
|
font_files = []
|
||||||
|
for filename in os.listdir(fonts_folder):
|
||||||
|
if filename.lower().endswith((".ttf", ".otf")):
|
||||||
|
font_files.append(filename)
|
||||||
|
|
||||||
|
# Limit number of fonts loaded to prevent too much blocking
|
||||||
|
font_files = font_files[:10] # Only load first 10 fonts to prevent too much blocking
|
||||||
|
|
||||||
|
for filename in font_files:
|
||||||
|
if time.time() - start_time > timeout:
|
||||||
|
logger.warning(f"Font loading timed out for theme '{theme_name}' after loading {len(font_files)} fonts")
|
||||||
|
break
|
||||||
|
|
||||||
|
font_path = os.path.join(fonts_folder, filename)
|
||||||
|
font_id = QFontDatabase.addApplicationFont(font_path)
|
||||||
|
if font_id != -1:
|
||||||
|
families = QFontDatabase.applicationFontFamilies(font_id)
|
||||||
|
logger.info(f"Font {filename} successfully loaded: {families}")
|
||||||
|
else:
|
||||||
|
logger.error(f"Error loading font: {filename}")
|
||||||
|
|
||||||
|
# Update the global variable in the main thread
|
||||||
|
_loaded_theme = theme_name
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error loading fonts for theme '{theme_name}': {e}")
|
||||||
|
|
||||||
|
# Use QTimer to delay font loading until after the UI is rendered
|
||||||
|
from PySide6.QtCore import QTimer
|
||||||
|
QTimer.singleShot(100, load_fonts_delayed) # Delay font loading by 100ms
|
||||||
|
|
||||||
class ThemeWrapper:
|
class ThemeWrapper:
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user