diff --git a/portprotonqt/delete_wine_module.py b/portprotonqt/delete_wine_module.py index 2bb0141..07e5597 100644 --- a/portprotonqt/delete_wine_module.py +++ b/portprotonqt/delete_wine_module.py @@ -6,6 +6,7 @@ from PySide6.QtWidgets import (QDialog, QVBoxLayout, QWidget, QCheckBox, QListWidget, QListWidgetItem) from PySide6.QtCore import Qt from portprotonqt.localization import _ +from portprotonqt.version_utils import version_sort_key class WineDeleteManager(QDialog): @@ -70,8 +71,8 @@ class WineDeleteManager(QDialog): if not os.path.exists(dist_path): return - # Get all wine directories - wine_dirs = [d for d in os.listdir(dist_path) if os.path.isdir(os.path.join(dist_path, d))] + # Get all wine directories and sort them by version + wine_dirs = sorted([d for d in os.listdir(dist_path) if os.path.isdir(os.path.join(dist_path, d))], key=version_sort_key) # Add each wine to the list for wine_name in wine_dirs: diff --git a/portprotonqt/dialogs.py b/portprotonqt/dialogs.py index 040558b..414a90a 100644 --- a/portprotonqt/dialogs.py +++ b/portprotonqt/dialogs.py @@ -19,6 +19,7 @@ from portprotonqt.downloader import Downloader from portprotonqt.virtual_keyboard import VirtualKeyboard from portprotonqt.preloader import Preloader from portprotonqt.settings_manager import get_toggle_settings, get_advanced_settings, ADVANCED_SETTING_KEYS +from portprotonqt.version_utils import version_sort_key import psutil if TYPE_CHECKING: @@ -1738,10 +1739,10 @@ class ExeSettingsDialog(QDialog): if self.portproton_path: dist_dir = os.path.join(self.portproton_path, "data", 'dist') if os.path.exists(dist_dir): - self.dist_options = [f for f in os.listdir(dist_dir) if os.path.isdir(os.path.join(dist_dir, f))] + self.dist_options = sorted([f for f in os.listdir(dist_dir) if os.path.isdir(os.path.join(dist_dir, f))], key=version_sort_key) prefixes_dir = os.path.join(self.portproton_path, 'prefixes') if os.path.exists(prefixes_dir): - self.prefix_options = [f for f in os.listdir(prefixes_dir) if os.path.isdir(os.path.join(prefixes_dir, f))] + self.prefix_options = sorted([f for f in os.listdir(prefixes_dir) if os.path.isdir(os.path.join(prefixes_dir, f))], key=version_sort_key) self.current_settings = {} self.value_widgets = {} diff --git a/portprotonqt/get_wine_module.py b/portprotonqt/get_wine_module.py index cdcaf36..30bd97a 100644 --- a/portprotonqt/get_wine_module.py +++ b/portprotonqt/get_wine_module.py @@ -15,6 +15,7 @@ import urllib.parse from portprotonqt.config_utils import read_proxy_config, get_portproton_start_command from portprotonqt.logger import get_logger from portprotonqt.localization import _ +from portprotonqt.version_utils import version_sort_key logger = get_logger(__name__) @@ -463,8 +464,9 @@ class ProtonManager(QDialog): successful_tabs += 1 del tabs_dict['proton_lg'] - # Остальные табы после Proton_LG - for source_key, entries in tabs_dict.items(): + # Остальные табы после Proton_LG, сортируем по алфавиту + for source_key in sorted(tabs_dict.keys()): + entries = tabs_dict[source_key] if self.create_tab_from_entries(source_key, entries): successful_tabs += 1 @@ -506,6 +508,7 @@ class ProtonManager(QDialog): logger.info(f"Filtered {len(entries)} -> {len(filtered_entries)} entries for {source_name} based on CPU level {self.cpu_level}") return filtered_entries + def create_tab_from_entries(self, source_name, entries): """Создаем вкладку с таблицей для источника Proton из записей JSON""" @@ -529,33 +532,31 @@ class ProtonManager(QDialog): header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch) - # Filter out installed entries before setting row count - non_installed_entries = [] + # Include all entries (both installed and non-installed) + all_entries = [] for entry in entries: # Извлекаем имя файла из URL url = entry.get('url', '') - filename = entry.get('name', '') if url: parsed_url = urllib.parse.urlparse(url) url_filename = os.path.basename(parsed_url.path) if url_filename: - filename = url_filename + entry['filename'] = url_filename - uppercase_filename = filename.upper() # Преобразование имени WINE в верхний регистр - is_installed = self.is_asset_installed(uppercase_filename, source_name) + all_entries.append(entry) - if not is_installed: - non_installed_entries.append(entry) + # Sort entries by version before displaying + all_entries.sort(key=version_sort_key) - table.setRowCount(len(non_installed_entries)) + table.setRowCount(len(all_entries)) table.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows) table.setSelectionMode(QTableWidget.SelectionMode.SingleSelection) table.cellClicked.connect(self.on_cell_clicked) - for row_index, entry in enumerate(non_installed_entries): + for row_index, entry in enumerate(all_entries): self.add_asset_row_from_json(table, row_index, entry, source_name) layout.addWidget(table, 1) @@ -563,7 +564,7 @@ class ProtonManager(QDialog): tab_name = (self.get_short_source_name(source_name) or "UNKNOWN").upper() # Название для Таба в верхний регистр self.tab_widget.addTab(tab, tab_name) - logger.info(f"Successfully created tab for {source_name} with {len(non_installed_entries)} assets (filtered from {len(entries)})") + logger.info(f"Successfully created tab for {source_name} with {len(all_entries)} assets (filtered from {len(entries)})") return True except Exception as e: @@ -610,10 +611,6 @@ class ProtonManager(QDialog): uppercase_filename = filename.upper() # Преобразование имени WINE в верхний регистр is_installed = self.is_asset_installed(uppercase_filename, source_name) - # Если ассет уже установлен, не показываем его вообще - if is_installed: - return - checkbox_widget = QWidget() checkbox_layout = QHBoxLayout(checkbox_widget) checkbox_layout.setAlignment(Qt.AlignmentFlag.AlignCenter) @@ -627,9 +624,16 @@ class ProtonManager(QDialog): 'browser_download_url': url, } - checkbox.stateChanged.connect(lambda state, a=asset_data, v=version_from_name, - s=source_name: - self.on_asset_toggled_json(state, a, v, s)) + if is_installed: + # If asset is already installed, disable the checkbox + checkbox.setEnabled(False) + checkbox.setChecked(False) # Ensure it's not checked + else: + # Only connect the signal if the asset is not installed + checkbox.stateChanged.connect(lambda state, a=asset_data, v=version_from_name, + s=source_name: + self.on_asset_toggled_json(state, a, v, s)) + checkbox_layout.addWidget(checkbox) table.setCellWidget(row_index, 0, checkbox_widget) @@ -640,6 +644,13 @@ class ProtonManager(QDialog): display_name = filename[:-7] asset_name_item = QTableWidgetItem(display_name) + + if is_installed: + # Make the item disabled and add "(installed)" suffix + asset_name_item.setFlags(asset_name_item.flags() & ~Qt.ItemFlag.ItemIsEnabled) + # Add "(installed)" suffix to indicate it's already installed + asset_name_item.setText(_('{display_name} (installed)').format(display_name=display_name)) + table.setItem(row_index, 1, asset_name_item) # Собираем метаданные в данных элемента diff --git a/portprotonqt/main_window.py b/portprotonqt/main_window.py index 19f6eda..86dfcfc 100644 --- a/portprotonqt/main_window.py +++ b/portprotonqt/main_window.py @@ -32,6 +32,7 @@ from portprotonqt.config_utils import ( clear_cache, read_auto_fullscreen_gamepad, save_auto_fullscreen_gamepad, read_rumble_config, save_rumble_config, read_gamepad_type, save_gamepad_type, read_minimize_to_tray, save_minimize_to_tray, read_auto_card_size, save_auto_card_size, get_portproton_start_command ) +from portprotonqt.version_utils import version_sort_key from portprotonqt.localization import _, get_egs_language, read_metadata_translations from portprotonqt.downloader import Downloader from portprotonqt.tray_manager import TrayManager @@ -1700,7 +1701,7 @@ class MainWindow(QMainWindow): formLayout.setSpacing(10) formLayout.setLabelAlignment(Qt.AlignmentFlag.AlignLeft) - self.wine_versions = [d for d in os.listdir(dist_path) if os.path.isdir(os.path.join(dist_path, d))] + self.wine_versions = sorted([d for d in os.listdir(dist_path) if os.path.isdir(os.path.join(dist_path, d))], key=version_sort_key) self.wineCombo = QComboBox() self.wineCombo.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) self.wineCombo.addItems(self.wine_versions) @@ -1713,7 +1714,7 @@ class MainWindow(QMainWindow): self.wineCombo.setCurrentIndex(0) formLayout.addRow(self.wineTitleLabel, self.wineCombo) - self.prefixes = [d for d in os.listdir(prefixes_path) if os.path.isdir(os.path.join(prefixes_path, d))] if os.path.exists(prefixes_path) else [] + self.prefixes = sorted([d for d in os.listdir(prefixes_path) if os.path.isdir(os.path.join(prefixes_path, d))], key=version_sort_key) if os.path.exists(prefixes_path) else [] self.prefixCombo = QComboBox() self.prefixCombo.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) self.prefixCombo.addItems(self.prefixes) diff --git a/portprotonqt/version_utils.py b/portprotonqt/version_utils.py new file mode 100644 index 0000000..204bfe7 --- /dev/null +++ b/portprotonqt/version_utils.py @@ -0,0 +1,93 @@ +import os +import urllib.parse + + +def version_sort_key(entry): + """ + Create a sort key for version-aware sorting of Proton/Wine entries. + This sorts alphabetically first, but within entries with the same prefix (like "wine-"), + it sorts by version number (descending). + """ + if isinstance(entry, dict): + # If entry is a dict (from JSON metadata), extract name + name = entry.get('name', '') + + # Извлекаем имя файла из URL если нет имени + url = entry.get('url', '') + if url and not name: + parsed_url = urllib.parse.urlparse(url) + name = os.path.basename(parsed_url.path) + else: + # If entry is a string (directory name), use it directly + name = entry + + # Remove extensions to get clean name + for ext in ['.tar.gz', '.tar.xz', '.zip']: + if name.lower().endswith(ext): + name = name[:-len(ext)] + break + + # Determine the prefix (e.g., "wine", "GE-Proton", "proton") for grouping + prefix = name.lower() + version_part = name + + # Extract version part and prefix for different naming patterns + if name.startswith('GE-Proton-'): + # For "GE-Proton-9-25", prefix is "ge-proton", version is "9-25" + parts = name.split('-', 2) + if len(parts) >= 3: + prefix = f"{parts[0]}-{parts[1]}".lower() # "ge-proton" + version_part = parts[2] # "9-25" + elif len(parts) >= 2: + prefix = parts[0].lower() + version_part = parts[1] + elif name.lower().startswith('wine-'): + # For "wine-8.0-rc1", prefix is "wine", version is "8.0-rc1" + parts = name.split('-', 2) + if len(parts) >= 2: + prefix = parts[0].lower() # "wine" + if len(parts) >= 3: + version_part = f"{parts[1]}-{parts[2]}" # "8.0-rc1" + elif len(parts) >= 2: + version_part = parts[1] # "8.0" + elif name.lower().startswith('proton-'): + # For "proton-8.0-rc1", prefix is "proton", version is "8.0-rc1" + parts = name.split('-', 2) + if len(parts) >= 2: + prefix = parts[0].lower() # "proton" + if len(parts) >= 3: + version_part = f"{parts[1]}-{parts[2]}" # "8.0-rc1" + elif len(parts) >= 2: + version_part = parts[1] # "8.0" + else: + # For entries without standard prefixes, use the first part as prefix + if '-' in name: + prefix = name.split('-', 1)[0].lower() + else: + prefix = name.lower() + version_part = name + + # Handle different version formats - create a proper version tuple for descending sorting + if '-' in version_part: + # Split on '-' and convert numeric parts for proper sorting (inverted for descending) + parts = version_part.split('-') + numeric_parts = [] + for part in parts: + try: + # Convert to negative integer for descending numeric sorting + numeric_parts.append(-int(part)) + except ValueError: + # For non-numeric parts, use a large number to sort them after numeric parts + # and append the lowercase string for consistent ordering + numeric_parts.append(float('inf')) + numeric_parts.append(part.lower()) + # Return tuple: (prefix for alphabetical sort, version for version sort, original name for tie-breaker) + return (prefix, numeric_parts, name.lower()) + else: + # If no dash in version part, try to parse as a simple version + try: + # Return tuple with prefix for alphabetical sort, version for version sort, and name for tie-breaker + return (prefix, [-int(version_part)], name.lower()) # Negative for descending order + except ValueError: + # For non-numeric versions, use prefix for alphabetical sort and version part for secondary sort + return (prefix, [float('inf'), version_part.lower()], name.lower())