diff --git a/jlauncher/settings.py b/jlauncher/settings.py index c931de9..1693ad9 100644 --- a/jlauncher/settings.py +++ b/jlauncher/settings.py @@ -26,6 +26,14 @@ def _valid_console_mode(value) -> str: return value if value in _CONSOLE_MODES else "auto" +_SORT_ORDERS = ("default", "name") + + +def _valid_sort_order(value) -> str: + """Normaliza el orden de la lista; cae a 'default' (orden del TOML) si es desconocido.""" + return value if value in _SORT_ORDERS else "default" + + @dataclass class Settings: hide_not_downloaded: bool = False @@ -34,6 +42,7 @@ class Settings: check_updates_on_start: bool = False # comprobar updates automáticamente al iniciar theme: str = "system" # tema de la UI: "system" | "light" | "dark" console_mode: str = "auto" # consola de log: "show" | "auto" | "hide" + sort_order: str = "default" # orden de la lista: "default" (TOML) | "name" # Tolerancia a repos offline/inalcanzables (segundos, salvo stall_limit en bytes/s). git_fetch_timeout: int = 60 # techo para fetch / comprobar update git_clone_timeout: int = 900 # techo para clone (repo grande) @@ -61,6 +70,7 @@ def load_settings() -> Settings: check_updates_on_start=bool(data.get("check_updates_on_start", False)), theme=_valid_theme(data.get("theme", "system")), console_mode=_valid_console_mode(data.get("console_mode", "auto")), + sort_order=_valid_sort_order(data.get("sort_order", "default")), git_fetch_timeout=int(data.get("git_fetch_timeout", 60)), git_clone_timeout=int(data.get("git_clone_timeout", 900)), http_timeout=int(data.get("http_timeout", 15)), diff --git a/jlauncher/ui/main_window.py b/jlauncher/ui/main_window.py index 7bbcea0..9886241 100644 --- a/jlauncher/ui/main_window.py +++ b/jlauncher/ui/main_window.py @@ -42,6 +42,9 @@ CONSOLE_SHOW = "show" CONSOLE_AUTO = "auto" CONSOLE_HIDE = "hide" +SORT_DEFAULT = "default" # ordre del games.toml +SORT_NAME = "name" # alfabètic pel nom + class MainWindow(QMainWindow): def __init__(self, config: Config, root: Path, parent=None) -> None: @@ -76,16 +79,15 @@ class MainWindow(QMainWindow): # --- Lista de juegos con scroll --- list_container = QWidget() - list_layout = QVBoxLayout(list_container) - list_layout.setContentsMargins(6, 6, 6, 6) - list_layout.setSpacing(6) + self.list_layout = QVBoxLayout(list_container) + self.list_layout.setContentsMargins(6, 6, 6, 6) + self.list_layout.setSpacing(6) for game in config.games: row = GameRow(game, root) row.activated.connect(self._on_activate) row.delete_requested.connect(self._on_delete) self.rows[game.id] = row - list_layout.addWidget(row) - list_layout.addStretch(1) + self._populate_list() scroll = QScrollArea() scroll.setWidgetResizable(True) @@ -238,6 +240,7 @@ class MainWindow(QMainWindow): menu.addAction(self.action_check_on_start) menu.addSeparator() + self._build_sort_menu(menu) self._build_theme_menu(menu) self._build_console_menu(menu) @@ -329,6 +332,27 @@ class MainWindow(QMainWindow): dlg.exec() + def _build_sort_menu(self, parent_menu) -> None: + """Submenú Ordena amb dues opcions exclusives: Per defecte / Per nom.""" + submenu = parent_menu.addMenu("Ordena") + group = QActionGroup(self) + group.setExclusive(True) + options = [ + ("Per defecte", SORT_DEFAULT), + ("Per nom", SORT_NAME), + ] + for label, mode in options: + action = QAction(label, self, checkable=True) + action.setChecked(self.settings.sort_order == mode) + action.triggered.connect(lambda _checked, m=mode: self._on_sort_selected(m)) + group.addAction(action) + submenu.addAction(action) + + def _on_sort_selected(self, mode: str) -> None: + self.settings.sort_order = mode + save_settings(self.settings) + self._populate_list() + def _build_theme_menu(self, parent_menu) -> None: """Submenú Tema amb tres opcions exclusives: Sistema / Clar / Fosc.""" submenu = parent_menu.addMenu("Tema") @@ -412,6 +436,23 @@ class MainWindow(QMainWindow): self.settings.check_updates_on_start = checked save_settings(self.settings) + def _ordered_games(self) -> list[Game]: + """Jocs en l'ordre triat: alfabètic pel nom, o l'ordre original del games.toml.""" + if self.settings.sort_order == SORT_NAME: + return sorted(self.config.games, key=lambda g: g.name.casefold()) + return list(self.config.games) + + def _populate_list(self) -> None: + """(Re)col·loca les files al layout segons l'ordre triat, sense destruir-les.""" + while self.list_layout.count(): + item = self.list_layout.takeAt(0) + w = item.widget() if item else None + if w is not None: + w.setParent(None) # treu del layout però conserva la fila (viu a self.rows) + for game in self._ordered_games(): + self.list_layout.addWidget(self.rows[game.id]) + self.list_layout.addStretch(1) + def _apply_filter(self) -> None: hide = self.action_hide.isChecked() for row in self.rows.values():