forked from Boria138/PortProtonQt
feat: use alphabeth and number sort on prefixes and dist
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
@@ -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)
|
||||
|
||||
# Собираем метаданные в данных элемента
|
||||
|
||||
@@ -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)
|
||||
|
||||
93
portprotonqt/version_utils.py
Normal file
93
portprotonqt/version_utils.py
Normal 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())
|
||||
Reference in New Issue
Block a user