Compare commits
2 Commits
7dfaee6831
...
80a2c06b5a
| Author | SHA1 | Date | |
|---|---|---|---|
|
80a2c06b5a
|
|||
|
f0a4ace735
|
@@ -9,6 +9,10 @@ logger = get_logger(__name__)
|
||||
_portproton_location = None
|
||||
_portproton_start_sh = None
|
||||
|
||||
# Configuration cache for performance optimization
|
||||
_config_cache = {}
|
||||
_config_last_modified = {}
|
||||
|
||||
# Paths to configuration files
|
||||
CONFIG_FILE = os.path.join(
|
||||
os.getenv("XDG_CONFIG_HOME", os.path.join(os.path.expanduser("~"), ".config")),
|
||||
@@ -28,13 +32,35 @@ THEMES_DIRS = [
|
||||
]
|
||||
|
||||
def read_config_safely(config_file: str) -> configparser.ConfigParser | None:
|
||||
"""Safely reads a configuration file and returns a ConfigParser object or None if reading fails."""
|
||||
cp = configparser.ConfigParser()
|
||||
"""Safely reads a configuration file and returns a ConfigParser object or None if reading fails.
|
||||
Uses caching to avoid repeated file reads for better performance.
|
||||
"""
|
||||
# Check if file exists
|
||||
if not os.path.exists(config_file):
|
||||
logger.debug(f"Configuration file {config_file} not found")
|
||||
return None
|
||||
|
||||
# Get file modification time
|
||||
try:
|
||||
current_mtime = os.path.getmtime(config_file)
|
||||
except OSError:
|
||||
logger.warning(f"Failed to get modification time for {config_file}")
|
||||
return None
|
||||
|
||||
# Check if we have a cached version that's still valid
|
||||
if config_file in _config_cache and config_file in _config_last_modified:
|
||||
if _config_last_modified[config_file] == current_mtime:
|
||||
logger.debug(f"Using cached config for {config_file}")
|
||||
return _config_cache[config_file]
|
||||
|
||||
# Read and parse the config file
|
||||
cp = configparser.ConfigParser()
|
||||
try:
|
||||
cp.read(config_file, encoding="utf-8")
|
||||
# Update cache
|
||||
_config_cache[config_file] = cp
|
||||
_config_last_modified[config_file] = current_mtime
|
||||
logger.debug(f"Config file {config_file} loaded and cached")
|
||||
return cp
|
||||
except (configparser.DuplicateSectionError, configparser.DuplicateOptionError) as e:
|
||||
logger.warning(f"Invalid configuration file format: {e}")
|
||||
@@ -43,6 +69,14 @@ def read_config_safely(config_file: str) -> configparser.ConfigParser | None:
|
||||
logger.warning(f"Failed to read configuration file: {e}")
|
||||
return None
|
||||
|
||||
def invalidate_config_cache(config_file: str = CONFIG_FILE):
|
||||
"""Invalidates the cached configuration for the specified file."""
|
||||
if config_file in _config_cache:
|
||||
del _config_cache[config_file]
|
||||
if config_file in _config_last_modified:
|
||||
del _config_last_modified[config_file]
|
||||
logger.debug(f"Config cache invalidated for {config_file}")
|
||||
|
||||
def read_config():
|
||||
"""Reads the configuration file and returns a dictionary of parameters.
|
||||
Example line in config (no sections):
|
||||
@@ -77,6 +111,8 @@ def save_theme_to_config(theme_name):
|
||||
cp["Appearance"]["theme"] = theme_name
|
||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||
cp.write(configfile)
|
||||
# Invalidate cache after saving
|
||||
invalidate_config_cache()
|
||||
|
||||
def read_time_config():
|
||||
"""Reads time settings from the [Time] section of the configuration file.
|
||||
@@ -96,6 +132,8 @@ def save_time_config(detail_level):
|
||||
cp["Time"]["detail_level"] = detail_level
|
||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||
cp.write(configfile)
|
||||
# Invalidate cache after saving
|
||||
invalidate_config_cache()
|
||||
|
||||
def read_file_content(file_path):
|
||||
"""Reads the content of a file and returns it as a string."""
|
||||
@@ -205,6 +243,8 @@ def save_card_size(card_width):
|
||||
cp["Cards"]["card_width"] = str(card_width)
|
||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||
cp.write(configfile)
|
||||
# Invalidate cache after saving
|
||||
invalidate_config_cache()
|
||||
|
||||
def read_auto_card_size():
|
||||
"""Reads the card size (width) for Auto Install from the [Cards] section.
|
||||
@@ -224,6 +264,8 @@ def save_auto_card_size(card_width):
|
||||
cp["Cards"]["auto_card_width"] = str(card_width)
|
||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||
cp.write(configfile)
|
||||
# Invalidate cache after saving
|
||||
invalidate_config_cache()
|
||||
|
||||
|
||||
def read_sort_method():
|
||||
@@ -244,6 +286,8 @@ def save_sort_method(sort_method):
|
||||
cp["Games"]["sort_method"] = sort_method
|
||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||
cp.write(configfile)
|
||||
# Invalidate cache after saving
|
||||
invalidate_config_cache()
|
||||
|
||||
def read_display_filter():
|
||||
"""Reads the display_filter parameter from the [Games] section.
|
||||
@@ -263,6 +307,8 @@ def save_display_filter(filter_value):
|
||||
cp["Games"]["display_filter"] = filter_value
|
||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||
cp.write(configfile)
|
||||
# Invalidate cache after saving
|
||||
invalidate_config_cache()
|
||||
|
||||
def read_favorites():
|
||||
"""Reads the list of favorite games from the [Favorites] section.
|
||||
@@ -288,6 +334,8 @@ def save_favorites(favorites):
|
||||
cp["Favorites"]["games"] = f'"{fav_str}"'
|
||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||
cp.write(configfile)
|
||||
# Invalidate cache after saving
|
||||
invalidate_config_cache()
|
||||
|
||||
def read_rumble_config():
|
||||
"""Reads the gamepad rumble setting from the [Gamepad] section.
|
||||
@@ -307,6 +355,8 @@ def save_rumble_config(rumble_enabled):
|
||||
cp["Gamepad"]["rumble_enabled"] = str(rumble_enabled)
|
||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||
cp.write(configfile)
|
||||
# Invalidate cache after saving
|
||||
invalidate_config_cache()
|
||||
|
||||
def read_gamepad_type():
|
||||
"""Reads the gamepad type from the [Gamepad] section.
|
||||
@@ -326,6 +376,8 @@ def save_gamepad_type(gpad_type):
|
||||
cp["Gamepad"]["type"] = gpad_type
|
||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||
cp.write(configfile)
|
||||
# Invalidate cache after saving
|
||||
invalidate_config_cache()
|
||||
|
||||
def ensure_default_proxy_config():
|
||||
"""Ensures the [Proxy] section exists in the configuration file.
|
||||
@@ -370,6 +422,8 @@ def save_proxy_config(proxy_url="", proxy_user="", proxy_password=""):
|
||||
cp["Proxy"]["proxy_password"] = proxy_password
|
||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||
cp.write(configfile)
|
||||
# Invalidate cache after saving
|
||||
invalidate_config_cache()
|
||||
|
||||
def read_fullscreen_config():
|
||||
"""Reads the fullscreen mode setting from the [Display] section.
|
||||
@@ -389,6 +443,8 @@ def save_fullscreen_config(fullscreen):
|
||||
cp["Display"]["fullscreen"] = str(fullscreen)
|
||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||
cp.write(configfile)
|
||||
# Invalidate cache after saving
|
||||
invalidate_config_cache()
|
||||
|
||||
def read_window_geometry() -> tuple[int, int]:
|
||||
"""Reads the window width and height from the [MainWindow] section.
|
||||
@@ -410,6 +466,8 @@ def save_window_geometry(width: int, height: int):
|
||||
cp["MainWindow"]["height"] = str(height)
|
||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||
cp.write(configfile)
|
||||
# Invalidate cache after saving
|
||||
invalidate_config_cache()
|
||||
|
||||
def reset_config():
|
||||
"""Resets the configuration file by deleting it.
|
||||
@@ -419,6 +477,8 @@ def reset_config():
|
||||
try:
|
||||
os.remove(CONFIG_FILE)
|
||||
logger.info("Configuration file %s deleted", CONFIG_FILE)
|
||||
# Invalidate cache after deletion
|
||||
invalidate_config_cache()
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to delete configuration file: {e}")
|
||||
|
||||
@@ -433,6 +493,9 @@ def clear_cache():
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to delete cache: {e}")
|
||||
|
||||
# Also clear our internal config cache
|
||||
invalidate_config_cache()
|
||||
|
||||
def read_auto_fullscreen_gamepad():
|
||||
"""Reads the auto-fullscreen setting for gamepad from the [Display] section.
|
||||
Returns False if the parameter is missing.
|
||||
@@ -451,6 +514,8 @@ def save_auto_fullscreen_gamepad(auto_fullscreen):
|
||||
cp["Display"]["auto_fullscreen_gamepad"] = str(auto_fullscreen)
|
||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||
cp.write(configfile)
|
||||
# Invalidate cache after saving
|
||||
invalidate_config_cache()
|
||||
|
||||
def read_favorite_folders():
|
||||
"""Reads the list of favorite folders from the [FavoritesFolders] section.
|
||||
@@ -476,6 +541,8 @@ def save_favorite_folders(folders):
|
||||
cp["FavoritesFolders"]["folders"] = f'"{fav_str}"'
|
||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||
cp.write(configfile)
|
||||
# Invalidate cache after saving
|
||||
invalidate_config_cache()
|
||||
|
||||
def read_minimize_to_tray():
|
||||
"""Reads the minimize-to-tray setting from the [Display] section.
|
||||
@@ -495,3 +562,5 @@ def save_minimize_to_tray(minimize_to_tray):
|
||||
cp["Display"]["minimize_to_tray"] = str(minimize_to_tray)
|
||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||
cp.write(configfile)
|
||||
# Invalidate cache after saving
|
||||
invalidate_config_cache()
|
||||
|
||||
@@ -565,6 +565,16 @@ class MainWindow(QMainWindow):
|
||||
self.game_library_manager.set_games(games)
|
||||
self.progress_bar.setVisible(False)
|
||||
|
||||
# Clear the refresh in progress flag
|
||||
if hasattr(self, '_refresh_in_progress'):
|
||||
self._refresh_in_progress = False
|
||||
|
||||
# Re-enable the refresh button if it exists
|
||||
if hasattr(self, 'refreshButton'):
|
||||
self.refreshButton.setEnabled(True)
|
||||
self.refreshButton.setText(_("Refresh Grid"))
|
||||
self.update_status_message.emit(_("Game library refreshed"), 3000)
|
||||
|
||||
def open_portproton_forum_topic(self, topic_name: str):
|
||||
"""Open the PortProton forum topic or search page for this game."""
|
||||
result = self.portproton_api.get_forum_topic_slug(topic_name)
|
||||
@@ -881,7 +891,16 @@ class MainWindow(QMainWindow):
|
||||
self.addGameButton.setStyleSheet(self.theme.ADDGAME_BACK_BUTTON_STYLE)
|
||||
self.addGameButton.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
||||
self.addGameButton.clicked.connect(self.openAddGameDialog)
|
||||
layout.addWidget(self.addGameButton, alignment=Qt.AlignmentFlag.AlignRight)
|
||||
layout.addWidget(self.addGameButton)
|
||||
|
||||
# Refresh button
|
||||
self.refreshButton = AutoSizeButton(_("Refresh Grid"), icon=self.theme_manager.get_icon("update"))
|
||||
self.refreshButton.setStyleSheet(self.theme.ADDGAME_BACK_BUTTON_STYLE)
|
||||
self.refreshButton.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
||||
self.refreshButton.clicked.connect(self.refreshGames)
|
||||
layout.addWidget(self.refreshButton)
|
||||
|
||||
layout.addStretch() # Add stretch to push search to the right
|
||||
|
||||
self.searchEdit = CustomLineEdit(self, theme=self.theme)
|
||||
icon: QIcon = cast(QIcon, self.theme_manager.get_icon("search"))
|
||||
@@ -901,6 +920,32 @@ class MainWindow(QMainWindow):
|
||||
layout.addWidget(self.searchEdit)
|
||||
return self.container, self.searchEdit
|
||||
|
||||
def refreshGames(self):
|
||||
"""Refresh the game grid by reloading all games without restarting the application."""
|
||||
# Prevent multiple refreshes at once
|
||||
if hasattr(self, '_refresh_in_progress') and self._refresh_in_progress:
|
||||
# If refresh is already in progress, just update the status
|
||||
self.update_status_message.emit(_("A refresh is already in progress..."), 3000)
|
||||
return
|
||||
|
||||
# Mark that a refresh is in progress
|
||||
self._refresh_in_progress = True
|
||||
|
||||
# Clear the search field to ensure all games are shown after refresh
|
||||
self.searchEdit.clear()
|
||||
|
||||
# Disable the refresh button during refresh to prevent multiple clicks
|
||||
self.refreshButton.setEnabled(False)
|
||||
self.refreshButton.setText(_("Refreshing..."))
|
||||
|
||||
# Show progress bar
|
||||
self.progress_bar.setVisible(True)
|
||||
self.progress_bar.setRange(0, 0) # Indeterminate
|
||||
self.update_status_message.emit(_("Refreshing game library..."), 0)
|
||||
|
||||
# Reload games using the existing loadGames functionality
|
||||
QTimer.singleShot(0, self.loadGames)
|
||||
|
||||
def on_search_text_changed(self, text: str):
|
||||
"""Search text change handler with debounce."""
|
||||
self.searchDebounceTimer.stop()
|
||||
|
||||
@@ -5,6 +5,9 @@ from portprotonqt.logger import get_logger
|
||||
from PySide6.QtGui import QIcon, QFontDatabase, QPixmap
|
||||
from portprotonqt.config_utils import save_theme_to_config, load_theme_metainfo
|
||||
|
||||
# Icon caching for performance optimization
|
||||
_icon_cache = {}
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# Папка, где располагаются все дополнительные темы
|
||||
@@ -232,6 +235,14 @@ class ThemeManager:
|
||||
а если файл не найден, то из стандартной темы.
|
||||
Если as_path=True, возвращает путь к иконке вместо QIcon.
|
||||
"""
|
||||
# Create cache key
|
||||
cache_key = f"{icon_name}_{theme_name or self.current_theme_name}_{as_path}"
|
||||
|
||||
# Check if we already have this icon cached
|
||||
if cache_key in _icon_cache:
|
||||
logger.debug(f"Using cached icon for {icon_name}")
|
||||
return _icon_cache[cache_key]
|
||||
|
||||
icon_path = None
|
||||
theme_name = theme_name or self.current_theme_name
|
||||
supported_extensions = ['.svg', '.png', '.jpg', '.jpeg']
|
||||
@@ -279,12 +290,20 @@ class ThemeManager:
|
||||
# Если иконка всё равно не найдена
|
||||
if not icon_path or not os.path.exists(icon_path):
|
||||
logger.error(f"Warning: icon '{icon_name}' not found")
|
||||
return QIcon() if not as_path else None
|
||||
result = QIcon() if not as_path else None
|
||||
# Cache the result even if it's None
|
||||
_icon_cache[cache_key] = result
|
||||
return result
|
||||
|
||||
if as_path:
|
||||
# Cache the path
|
||||
_icon_cache[cache_key] = icon_path
|
||||
return icon_path
|
||||
|
||||
return QIcon(icon_path)
|
||||
# Create QIcon and cache it
|
||||
icon = QIcon(icon_path)
|
||||
_icon_cache[cache_key] = icon
|
||||
return icon
|
||||
|
||||
def get_theme_image(self, image_name, theme_name=None):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user