feat: use alphabeth and number sort on prefixes and dist

Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
This commit is contained in:
2026-01-05 21:54:31 +05:00
parent 59093f743c
commit 9bb7e45b27
5 changed files with 133 additions and 26 deletions

View File

@@ -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:

View File

@@ -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 = {}

View File

@@ -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)
# Собираем метаданные в данных элемента

View File

@@ -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)

View File

@@ -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())