Compare commits
14 Commits
9452bfda2e
...
4d6f32f053
| Author | SHA1 | Date | |
|---|---|---|---|
|
4d6f32f053
|
|||
|
a2f5141b20
|
|||
|
e3cb2857e7
|
|||
|
efe8a35832
|
|||
|
61fae97dad
|
|||
|
5442100f64
|
|||
|
2d6ef84798
|
|||
|
|
f4aee15b5d | ||
|
87a65108a5
|
|||
|
bb617708ac
|
|||
|
1cf332cd87
|
|||
|
577ad4d3a3
|
|||
|
ef3f2d6e96
|
|||
|
657d7728a6
|
@@ -16,7 +16,7 @@ repos:
|
||||
- id: uv-lock
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.13.2
|
||||
rev: v0.14.0
|
||||
hooks:
|
||||
- id: ruff-check
|
||||
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
- Импорт и экспорт бекапа префикса
|
||||
- Диалог для управление Winetricks
|
||||
- Кнопки для удаления префикса, wine или proton
|
||||
- Все настройки Wine с оригинального PortProton
|
||||
- Виртуальная клавиатура в диалог добавления игры и поиск по библиотеке
|
||||
- Вкладка автоустановок
|
||||
|
||||
### Changed
|
||||
- Проведён рефакторинг и оптимизация всего что связано с карточками и библиотекой игр
|
||||
@@ -22,6 +24,8 @@
|
||||
- Исправлено зависание при поиске игр
|
||||
- Исправлено ошибочное присвоение ID игры с названием "GAME", возникавшее, если исполняемый файл находился в подпапке `game/` (часто встречается у игр на Unity)
|
||||
- Исправлена ошибка из-за которой подсказки по управлению снизу и сверху могли не совпадать с друг другом, из-за чего возле вкладок были стрелки клавиатуры, а снизу кнопки геймпада
|
||||
- Исправлен выход из полноэкранного режима при отключении геймпада подключённого по USB даже если настройка "Режим полноэкранного отображения приложения при подключении геймпада" выключена
|
||||
- При сохранении настроек теперь не меняется размер окна
|
||||
|
||||
### Contributors
|
||||
|
||||
|
||||
@@ -21,9 +21,9 @@ Current translation status:
|
||||
|
||||
| Locale | Progress | Translated |
|
||||
| :----- | -------: | ---------: |
|
||||
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 of 233 |
|
||||
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 233 |
|
||||
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 233 of 233 |
|
||||
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 of 239 |
|
||||
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 239 |
|
||||
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 239 of 239 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -21,9 +21,9 @@
|
||||
|
||||
| Локаль | Прогресс | Переведено |
|
||||
| :----- | -------: | ---------: |
|
||||
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 из 233 |
|
||||
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 233 |
|
||||
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 233 из 233 |
|
||||
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 из 239 |
|
||||
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 239 |
|
||||
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 239 из 239 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import sys
|
||||
import os
|
||||
import subprocess
|
||||
from PySide6.QtCore import QLocale, QTranslator, QLibraryInfo
|
||||
from PySide6.QtWidgets import QApplication
|
||||
from PySide6.QtGui import QIcon
|
||||
from portprotonqt.main_window import MainWindow
|
||||
from portprotonqt.config_utils import save_fullscreen_config
|
||||
from portprotonqt.config_utils import save_fullscreen_config, get_portproton_location
|
||||
from portprotonqt.logger import get_logger, setup_logger
|
||||
from portprotonqt.cli import parse_args
|
||||
|
||||
@@ -12,6 +14,19 @@ __app_name__ = "PortProtonQt"
|
||||
__app_version__ = "0.1.6"
|
||||
|
||||
def main():
|
||||
|
||||
os.environ['PW_CLI'] = '1'
|
||||
os.environ['PROCESS_LOG'] = '1'
|
||||
os.environ['START_FROM_STEAM'] = '1'
|
||||
|
||||
portproton_path = get_portproton_location()
|
||||
|
||||
if portproton_path is None:
|
||||
return
|
||||
|
||||
script_path = os.path.join(portproton_path, 'data', 'scripts', 'start.sh')
|
||||
subprocess.run([script_path, 'cli', '--initial'])
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
app.setWindowIcon(QIcon.fromTheme(__app_id__))
|
||||
app.setDesktopFileName(__app_id__)
|
||||
|
||||
|
Before Width: | Height: | Size: 634 KiB After Width: | Height: | Size: 634 KiB |
|
Before Width: | Height: | Size: 315 KiB After Width: | Height: | Size: 315 KiB |
|
Before Width: | Height: | Size: 978 KiB After Width: | Height: | Size: 978 KiB |
|
Before Width: | Height: | Size: 650 KiB After Width: | Height: | Size: 650 KiB |
|
Before Width: | Height: | Size: 391 KiB After Width: | Height: | Size: 391 KiB |
|
Before Width: | Height: | Size: 710 KiB After Width: | Height: | Size: 710 KiB |
|
Before Width: | Height: | Size: 627 KiB After Width: | Height: | Size: 627 KiB |
@@ -453,3 +453,11 @@ class GameLibraryManager:
|
||||
def filter_games_delayed(self):
|
||||
"""Filters games based on search text and updates the grid."""
|
||||
self.update_game_grid(is_filter=True)
|
||||
|
||||
def calculate_columns(self, card_width: int) -> int:
|
||||
"""Calculate the number of columns based on card width and assumed container width."""
|
||||
# Assuming a typical container width; adjust as needed
|
||||
available_width = 1200 # Example width, can be dynamic if widget access is added
|
||||
spacing = 15 # Assumed spacing between cards
|
||||
columns = max(1, (available_width - spacing) // (card_width + spacing))
|
||||
return min(columns, 8) # Cap at reasonable max
|
||||
|
||||
@@ -38,6 +38,7 @@ class MainWindowProtocol(Protocol):
|
||||
stackedWidget: QStackedWidget
|
||||
tabButtons: dict[int, QWidget]
|
||||
gamesListWidget: QWidget
|
||||
autoInstallContainer: QWidget | None
|
||||
currentDetailPage: QWidget | None
|
||||
current_exec_line: str | None
|
||||
current_add_game_dialog: AddGameDialog | None
|
||||
@@ -91,6 +92,7 @@ class InputManager(QObject):
|
||||
self._parent.currentDetailPage = getattr(self._parent, 'currentDetailPage', None)
|
||||
self._parent.current_exec_line = getattr(self._parent, 'current_exec_line', None)
|
||||
self._parent.current_add_game_dialog = getattr(self._parent, 'current_add_game_dialog', None)
|
||||
self._parent.autoInstallContainer = getattr(self._parent, 'autoInstallContainer', None)
|
||||
self.axis_deadzone = axis_deadzone
|
||||
self.initial_axis_move_delay = initial_axis_move_delay
|
||||
self.repeat_axis_move_delay = repeat_axis_move_delay
|
||||
@@ -143,6 +145,132 @@ class InputManager(QObject):
|
||||
# Initialize evdev + hotplug
|
||||
self.init_gamepad()
|
||||
|
||||
def _navigate_game_cards(self, container, tab_index: int, code: int, value: int) -> None:
|
||||
"""Common navigation logic for game cards in a container."""
|
||||
if container is None:
|
||||
return
|
||||
focused = QApplication.focusWidget()
|
||||
game_cards = container.findChildren(GameCard)
|
||||
if not game_cards:
|
||||
return
|
||||
|
||||
scroll_area = container.parentWidget()
|
||||
while scroll_area and not isinstance(scroll_area, QScrollArea):
|
||||
scroll_area = scroll_area.parentWidget()
|
||||
|
||||
# If no focused widget or not a GameCard, focus the first card
|
||||
if not isinstance(focused, GameCard) or focused not in game_cards:
|
||||
game_cards[0].setFocus()
|
||||
if scroll_area:
|
||||
scroll_area.ensureWidgetVisible(game_cards[0], 50, 50)
|
||||
return
|
||||
|
||||
cards = container.findChildren(GameCard, options=Qt.FindChildOption.FindChildrenRecursively)
|
||||
if not cards:
|
||||
return
|
||||
# Group cards by rows with tolerance for y-position
|
||||
rows = {}
|
||||
y_tolerance = 10 # Allow slight variations in y-position
|
||||
for card in cards:
|
||||
y = card.pos().y()
|
||||
matched = False
|
||||
for row_y in rows:
|
||||
if abs(y - row_y) <= y_tolerance:
|
||||
rows[row_y].append(card)
|
||||
matched = True
|
||||
break
|
||||
if not matched:
|
||||
rows[y] = [card]
|
||||
sorted_rows = sorted(rows.items(), key=lambda x: x[0])
|
||||
if not sorted_rows:
|
||||
return
|
||||
current_row_idx = None
|
||||
current_col_idx = None
|
||||
for row_idx, (_y, row_cards) in enumerate(sorted_rows):
|
||||
for idx, card in enumerate(row_cards):
|
||||
if card == focused:
|
||||
current_row_idx = row_idx
|
||||
current_col_idx = idx
|
||||
break
|
||||
if current_row_idx is not None:
|
||||
break
|
||||
|
||||
# Fallback: if focused card not found, select closest row by y-position
|
||||
if current_row_idx is None:
|
||||
if not sorted_rows: # Additional safety check
|
||||
return
|
||||
focused_y = focused.pos().y()
|
||||
current_row_idx = min(range(len(sorted_rows)), key=lambda i: abs(sorted_rows[i][0] - focused_y))
|
||||
if current_row_idx >= len(sorted_rows): # Safety check
|
||||
return
|
||||
current_row = sorted_rows[current_row_idx][1]
|
||||
focused_x = focused.pos().x() + focused.width() / 2
|
||||
current_col_idx = min(range(len(current_row)), key=lambda i: abs((current_row[i].pos().x() + current_row[i].width() / 2) - focused_x), default=0) # type: ignore
|
||||
|
||||
# Add null checks before using current_row_idx and current_col_idx
|
||||
if current_row_idx is None or current_col_idx is None or current_row_idx >= len(sorted_rows):
|
||||
return
|
||||
|
||||
current_row = sorted_rows[current_row_idx][1]
|
||||
if code == ecodes.ABS_HAT0X and value != 0:
|
||||
if value < 0: # Left
|
||||
if current_col_idx > 0:
|
||||
next_card = current_row[current_col_idx - 1]
|
||||
next_card.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||
if scroll_area:
|
||||
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
||||
else:
|
||||
if current_row_idx > 0:
|
||||
prev_row = sorted_rows[current_row_idx - 1][1]
|
||||
next_card = prev_row[-1] if prev_row else None
|
||||
if next_card:
|
||||
next_card.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||
if scroll_area:
|
||||
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
||||
elif value > 0: # Right
|
||||
if current_col_idx < len(current_row) - 1:
|
||||
next_card = current_row[current_col_idx + 1]
|
||||
next_card.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||
if scroll_area:
|
||||
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
||||
else:
|
||||
if current_row_idx < len(sorted_rows) - 1:
|
||||
next_row = sorted_rows[current_row_idx + 1][1]
|
||||
next_card = next_row[0] if next_row else None
|
||||
if next_card:
|
||||
next_card.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||
if scroll_area:
|
||||
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
||||
elif code == ecodes.ABS_HAT0Y and value != 0:
|
||||
if value > 0: # Down
|
||||
if current_row_idx < len(sorted_rows) - 1:
|
||||
next_row = sorted_rows[current_row_idx + 1][1]
|
||||
current_x = focused.pos().x() + focused.width() / 2
|
||||
next_card = min(
|
||||
next_row,
|
||||
key=lambda c: abs((c.pos().x() + c.width() / 2) - current_x),
|
||||
default=None
|
||||
)
|
||||
if next_card:
|
||||
next_card.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||
if scroll_area:
|
||||
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
||||
elif value < 0: # Up
|
||||
if current_row_idx > 0:
|
||||
prev_row = sorted_rows[current_row_idx - 1][1]
|
||||
current_x = focused.pos().x() + focused.width() / 2
|
||||
next_card = min(
|
||||
prev_row,
|
||||
key=lambda c: abs((c.pos().x() + c.width() / 2) - current_x),
|
||||
default=None
|
||||
)
|
||||
if next_card:
|
||||
next_card.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||
if scroll_area:
|
||||
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
||||
elif current_row_idx == 0:
|
||||
self._parent.tabButtons[tab_index].setFocus(Qt.FocusReason.OtherFocusReason)
|
||||
|
||||
def detect_gamepad_type(self, device: InputDevice) -> GamepadType:
|
||||
"""
|
||||
Определяет тип геймпада по capabilities
|
||||
@@ -483,8 +611,14 @@ class InputManager(QObject):
|
||||
if not app or not active:
|
||||
return
|
||||
|
||||
current_tab_index = self._parent.stackedWidget.currentIndex()
|
||||
|
||||
if button_code in BUTTONS['confirm'] and isinstance(focused, QLineEdit):
|
||||
search_edit = None
|
||||
if current_tab_index == 0:
|
||||
search_edit = getattr(self._parent, 'searchEdit', None)
|
||||
elif current_tab_index == 1:
|
||||
search_edit = getattr(self._parent, 'autoInstallSearchLineEdit', None)
|
||||
if focused == search_edit:
|
||||
keyboard = getattr(self._parent, 'keyboard', None)
|
||||
if keyboard:
|
||||
@@ -493,7 +627,11 @@ class InputManager(QObject):
|
||||
|
||||
# Handle Y button to focus search
|
||||
if button_code in BUTTONS['prev_dir']: # Y button
|
||||
search_edit = None
|
||||
if current_tab_index == 0:
|
||||
search_edit = getattr(self._parent, 'searchEdit', None)
|
||||
elif current_tab_index == 1:
|
||||
search_edit = getattr(self._parent, 'autoInstallSearchLineEdit', None)
|
||||
if search_edit:
|
||||
search_edit.setFocus()
|
||||
return
|
||||
@@ -757,32 +895,6 @@ class InputManager(QObject):
|
||||
if not app or not active:
|
||||
return
|
||||
|
||||
# Новый код: обработка перехода на поле поиска
|
||||
if code == ecodes.ABS_HAT0Y and value < 0: # Only D-pad up
|
||||
if isinstance(focused, GameCard):
|
||||
# Get all visible game cards
|
||||
game_cards = self._parent.gamesListWidget.findChildren(GameCard)
|
||||
if not game_cards:
|
||||
return
|
||||
|
||||
# Find the current card's position
|
||||
current_card_pos = focused.pos()
|
||||
current_row_y = current_card_pos.y()
|
||||
|
||||
# Check if this is the first row (no cards above)
|
||||
is_first_row = True
|
||||
for card in game_cards:
|
||||
if card.pos().y() < current_row_y and card.isVisible():
|
||||
is_first_row = False
|
||||
break
|
||||
|
||||
# Only move to search if on first row
|
||||
if is_first_row:
|
||||
search_edit = getattr(self._parent, 'searchEdit', None)
|
||||
if search_edit:
|
||||
search_edit.setFocus()
|
||||
return
|
||||
|
||||
# Handle SystemOverlay, AddGameDialog, or QMessageBox navigation with D-pad
|
||||
if isinstance(active, QDialog) and code == ecodes.ABS_HAT0X and value != 0:
|
||||
if isinstance(active, QMessageBox): # Specific handling for QMessageBox
|
||||
@@ -898,132 +1010,43 @@ class InputManager(QObject):
|
||||
focused.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||
return
|
||||
|
||||
# Library tab navigation (index 0)
|
||||
if self._parent.stackedWidget.currentIndex() == 0 and code in (ecodes.ABS_HAT0X, ecodes.ABS_HAT0Y):
|
||||
# Search focus logic for tabs 0 and 1
|
||||
if code == ecodes.ABS_HAT0Y and value < 0:
|
||||
focused = QApplication.focusWidget()
|
||||
game_cards = self._parent.gamesListWidget.findChildren(GameCard)
|
||||
if not game_cards:
|
||||
return
|
||||
|
||||
scroll_area = self._parent.gamesListWidget.parentWidget()
|
||||
while scroll_area and not isinstance(scroll_area, QScrollArea):
|
||||
scroll_area = scroll_area.parentWidget()
|
||||
|
||||
# If no focused widget or not a GameCard, focus the first card
|
||||
if not isinstance(focused, GameCard) or focused not in game_cards:
|
||||
game_cards[0].setFocus()
|
||||
if scroll_area:
|
||||
scroll_area.ensureWidgetVisible(game_cards[0], 50, 50)
|
||||
return
|
||||
|
||||
cards = self._parent.gamesListWidget.findChildren(GameCard, options=Qt.FindChildOption.FindChildrenRecursively)
|
||||
if not cards:
|
||||
return
|
||||
# Group cards by rows with tolerance for y-position
|
||||
rows = {}
|
||||
y_tolerance = 10 # Allow slight variations in y-position
|
||||
for card in cards:
|
||||
y = card.pos().y()
|
||||
matched = False
|
||||
for row_y in rows:
|
||||
if abs(y - row_y) <= y_tolerance:
|
||||
rows[row_y].append(card)
|
||||
matched = True
|
||||
break
|
||||
if not matched:
|
||||
rows[y] = [card]
|
||||
sorted_rows = sorted(rows.items(), key=lambda x: x[0])
|
||||
if not sorted_rows:
|
||||
return
|
||||
current_row_idx = None
|
||||
current_col_idx = None
|
||||
for row_idx, (_y, row_cards) in enumerate(sorted_rows):
|
||||
for idx, card in enumerate(row_cards):
|
||||
if card == focused:
|
||||
current_row_idx = row_idx
|
||||
current_col_idx = idx
|
||||
break
|
||||
if current_row_idx is not None:
|
||||
break
|
||||
|
||||
# Fallback: if focused card not found, select closest row by y-position
|
||||
if current_row_idx is None:
|
||||
if not sorted_rows: # Additional safety check
|
||||
return
|
||||
focused_y = focused.pos().y()
|
||||
current_row_idx = min(range(len(sorted_rows)), key=lambda i: abs(sorted_rows[i][0] - focused_y))
|
||||
if current_row_idx >= len(sorted_rows): # Safety check
|
||||
return
|
||||
current_row = sorted_rows[current_row_idx][1]
|
||||
focused_x = focused.pos().x() + focused.width() / 2
|
||||
current_col_idx = min(range(len(current_row)), key=lambda i: abs((current_row[i].pos().x() + current_row[i].width() / 2) - focused_x), default=0) # type: ignore
|
||||
|
||||
# Add null checks before using current_row_idx and current_col_idx
|
||||
if current_row_idx is None or current_col_idx is None or current_row_idx >= len(sorted_rows):
|
||||
return
|
||||
|
||||
current_row = sorted_rows[current_row_idx][1]
|
||||
if code == ecodes.ABS_HAT0X and value != 0:
|
||||
if value < 0: # Left
|
||||
if current_col_idx > 0:
|
||||
next_card = current_row[current_col_idx - 1]
|
||||
next_card.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||
if scroll_area:
|
||||
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
||||
current_index = self._parent.stackedWidget.currentIndex()
|
||||
if current_index in (0, 1) and isinstance(focused, GameCard):
|
||||
if current_index == 0:
|
||||
container = self._parent.gamesListWidget
|
||||
search_edit = getattr(self._parent, 'searchEdit', None)
|
||||
else:
|
||||
if current_row_idx > 0:
|
||||
prev_row = sorted_rows[current_row_idx - 1][1]
|
||||
next_card = prev_row[-1] if prev_row else None
|
||||
if next_card:
|
||||
next_card.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||
if scroll_area:
|
||||
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
||||
elif value > 0: # Right
|
||||
if current_col_idx < len(current_row) - 1:
|
||||
next_card = current_row[current_col_idx + 1]
|
||||
next_card.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||
if scroll_area:
|
||||
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
||||
else:
|
||||
if current_row_idx < len(sorted_rows) - 1:
|
||||
next_row = sorted_rows[current_row_idx + 1][1]
|
||||
next_card = next_row[0] if next_row else None
|
||||
if next_card:
|
||||
next_card.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||
if scroll_area:
|
||||
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
||||
elif code == ecodes.ABS_HAT0Y and value != 0:
|
||||
if value > 0: # Down
|
||||
if current_row_idx < len(sorted_rows) - 1:
|
||||
next_row = sorted_rows[current_row_idx + 1][1]
|
||||
current_x = focused.pos().x() + focused.width() / 2
|
||||
next_card = min(
|
||||
next_row,
|
||||
key=lambda c: abs((c.pos().x() + c.width() / 2) - current_x),
|
||||
default=None
|
||||
)
|
||||
if next_card:
|
||||
next_card.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||
if scroll_area:
|
||||
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
||||
elif value < 0: # Up
|
||||
if current_row_idx > 0:
|
||||
prev_row = sorted_rows[current_row_idx - 1][1]
|
||||
current_x = focused.pos().x() + focused.width() / 2
|
||||
next_card = min(
|
||||
prev_row,
|
||||
key=lambda c: abs((c.pos().x() + c.width() / 2) - current_x),
|
||||
default=None
|
||||
)
|
||||
if next_card:
|
||||
next_card.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||
if scroll_area:
|
||||
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
||||
elif current_row_idx == 0:
|
||||
self._parent.tabButtons[0].setFocus(Qt.FocusReason.OtherFocusReason)
|
||||
container = self._parent.autoInstallContainer
|
||||
search_edit = getattr(self._parent, 'autoInstallSearchLineEdit', None)
|
||||
if container and search_edit:
|
||||
game_cards = container.findChildren(GameCard)
|
||||
if game_cards:
|
||||
current_card_pos = focused.pos()
|
||||
current_row_y = current_card_pos.y()
|
||||
is_first_row = True
|
||||
for card in game_cards:
|
||||
if card.pos().y() < current_row_y and card.isVisible():
|
||||
is_first_row = False
|
||||
break
|
||||
if is_first_row:
|
||||
search_edit.setFocus()
|
||||
return
|
||||
|
||||
# Game cards navigation for tabs 0 and 1
|
||||
if code in (ecodes.ABS_HAT0X, ecodes.ABS_HAT0Y):
|
||||
current_index = self._parent.stackedWidget.currentIndex()
|
||||
if current_index in (0, 1):
|
||||
container = self._parent.gamesListWidget if current_index == 0 else self._parent.autoInstallContainer
|
||||
if container is None:
|
||||
return
|
||||
self._navigate_game_cards(container, current_index, code, value)
|
||||
return
|
||||
|
||||
# Vertical navigation in other tabs
|
||||
elif code == ecodes.ABS_HAT0Y and value != 0:
|
||||
if code == ecodes.ABS_HAT0Y and value != 0:
|
||||
focused = QApplication.focusWidget()
|
||||
page = self._parent.stackedWidget.currentWidget()
|
||||
if value > 0: # Down
|
||||
@@ -1336,7 +1359,7 @@ class InputManager(QObject):
|
||||
self.gamepad = None
|
||||
if self.gamepad_thread:
|
||||
self.gamepad_thread.join()
|
||||
# Signal to exit fullscreen mode
|
||||
if read_auto_fullscreen_gamepad() and not read_fullscreen_config():
|
||||
self.toggle_fullscreen.emit(False)
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling udev event: {e}", exc_info=True)
|
||||
|
||||
@@ -9,7 +9,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2025-10-09 16:37+0500\n"
|
||||
"POT-Creation-Date: 2025-10-12 15:20+0500\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language: de_DE\n"
|
||||
@@ -395,9 +395,6 @@ msgstr ""
|
||||
msgid "Auto Install"
|
||||
msgstr ""
|
||||
|
||||
msgid "Emulators"
|
||||
msgstr ""
|
||||
|
||||
msgid "Wine Settings"
|
||||
msgstr ""
|
||||
|
||||
@@ -416,6 +413,21 @@ msgstr ""
|
||||
msgid "Search"
|
||||
msgstr ""
|
||||
|
||||
msgid "Installation already in progress."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start installation."
|
||||
msgstr ""
|
||||
|
||||
msgid "Installation completed successfully."
|
||||
msgstr ""
|
||||
|
||||
msgid "Installation failed."
|
||||
msgstr ""
|
||||
|
||||
msgid "Installation error."
|
||||
msgstr ""
|
||||
|
||||
msgid "Loading Steam games..."
|
||||
msgstr ""
|
||||
|
||||
@@ -432,12 +444,6 @@ msgstr ""
|
||||
msgid "Added '{name}'"
|
||||
msgstr ""
|
||||
|
||||
msgid "Here you can configure automatic game installation..."
|
||||
msgstr ""
|
||||
|
||||
msgid "List of available emulators and their configuration..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Compatibility tool:"
|
||||
msgstr ""
|
||||
|
||||
@@ -450,12 +456,6 @@ msgstr ""
|
||||
msgid "Registry Editor"
|
||||
msgstr ""
|
||||
|
||||
msgid "Control Panel"
|
||||
msgstr ""
|
||||
|
||||
msgid "Task Manager"
|
||||
msgstr ""
|
||||
|
||||
msgid "Command Prompt"
|
||||
msgstr ""
|
||||
|
||||
@@ -477,6 +477,29 @@ msgstr ""
|
||||
msgid "Clear Prefix"
|
||||
msgstr ""
|
||||
|
||||
msgid "Launching tool..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start process."
|
||||
msgstr ""
|
||||
|
||||
msgid "Confirm Clear"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Are you sure you want to clear prefix '{}'?"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Prefix '{}' cleared successfully."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"Prefix '{}' cleared with errors:\n"
|
||||
"{}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start backup process."
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2025-10-09 16:37+0500\n"
|
||||
"POT-Creation-Date: 2025-10-12 15:20+0500\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language: es_ES\n"
|
||||
@@ -395,9 +395,6 @@ msgstr ""
|
||||
msgid "Auto Install"
|
||||
msgstr ""
|
||||
|
||||
msgid "Emulators"
|
||||
msgstr ""
|
||||
|
||||
msgid "Wine Settings"
|
||||
msgstr ""
|
||||
|
||||
@@ -416,6 +413,21 @@ msgstr ""
|
||||
msgid "Search"
|
||||
msgstr ""
|
||||
|
||||
msgid "Installation already in progress."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start installation."
|
||||
msgstr ""
|
||||
|
||||
msgid "Installation completed successfully."
|
||||
msgstr ""
|
||||
|
||||
msgid "Installation failed."
|
||||
msgstr ""
|
||||
|
||||
msgid "Installation error."
|
||||
msgstr ""
|
||||
|
||||
msgid "Loading Steam games..."
|
||||
msgstr ""
|
||||
|
||||
@@ -432,12 +444,6 @@ msgstr ""
|
||||
msgid "Added '{name}'"
|
||||
msgstr ""
|
||||
|
||||
msgid "Here you can configure automatic game installation..."
|
||||
msgstr ""
|
||||
|
||||
msgid "List of available emulators and their configuration..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Compatibility tool:"
|
||||
msgstr ""
|
||||
|
||||
@@ -450,12 +456,6 @@ msgstr ""
|
||||
msgid "Registry Editor"
|
||||
msgstr ""
|
||||
|
||||
msgid "Control Panel"
|
||||
msgstr ""
|
||||
|
||||
msgid "Task Manager"
|
||||
msgstr ""
|
||||
|
||||
msgid "Command Prompt"
|
||||
msgstr ""
|
||||
|
||||
@@ -477,6 +477,29 @@ msgstr ""
|
||||
msgid "Clear Prefix"
|
||||
msgstr ""
|
||||
|
||||
msgid "Launching tool..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start process."
|
||||
msgstr ""
|
||||
|
||||
msgid "Confirm Clear"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Are you sure you want to clear prefix '{}'?"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Prefix '{}' cleared successfully."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"Prefix '{}' cleared with errors:\n"
|
||||
"{}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start backup process."
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PortProtonQt 0.1.1\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2025-10-09 16:37+0500\n"
|
||||
"POT-Creation-Date: 2025-10-12 15:20+0500\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -393,9 +393,6 @@ msgstr ""
|
||||
msgid "Auto Install"
|
||||
msgstr ""
|
||||
|
||||
msgid "Emulators"
|
||||
msgstr ""
|
||||
|
||||
msgid "Wine Settings"
|
||||
msgstr ""
|
||||
|
||||
@@ -414,6 +411,21 @@ msgstr ""
|
||||
msgid "Search"
|
||||
msgstr ""
|
||||
|
||||
msgid "Installation already in progress."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start installation."
|
||||
msgstr ""
|
||||
|
||||
msgid "Installation completed successfully."
|
||||
msgstr ""
|
||||
|
||||
msgid "Installation failed."
|
||||
msgstr ""
|
||||
|
||||
msgid "Installation error."
|
||||
msgstr ""
|
||||
|
||||
msgid "Loading Steam games..."
|
||||
msgstr ""
|
||||
|
||||
@@ -430,12 +442,6 @@ msgstr ""
|
||||
msgid "Added '{name}'"
|
||||
msgstr ""
|
||||
|
||||
msgid "Here you can configure automatic game installation..."
|
||||
msgstr ""
|
||||
|
||||
msgid "List of available emulators and their configuration..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Compatibility tool:"
|
||||
msgstr ""
|
||||
|
||||
@@ -448,12 +454,6 @@ msgstr ""
|
||||
msgid "Registry Editor"
|
||||
msgstr ""
|
||||
|
||||
msgid "Control Panel"
|
||||
msgstr ""
|
||||
|
||||
msgid "Task Manager"
|
||||
msgstr ""
|
||||
|
||||
msgid "Command Prompt"
|
||||
msgstr ""
|
||||
|
||||
@@ -475,6 +475,29 @@ msgstr ""
|
||||
msgid "Clear Prefix"
|
||||
msgstr ""
|
||||
|
||||
msgid "Launching tool..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start process."
|
||||
msgstr ""
|
||||
|
||||
msgid "Confirm Clear"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Are you sure you want to clear prefix '{}'?"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Prefix '{}' cleared successfully."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"Prefix '{}' cleared with errors:\n"
|
||||
"{}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start backup process."
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2025-10-09 16:37+0500\n"
|
||||
"PO-Revision-Date: 2025-10-09 16:37+0500\n"
|
||||
"POT-Creation-Date: 2025-10-12 15:20+0500\n"
|
||||
"PO-Revision-Date: 2025-10-12 15:20+0500\n"
|
||||
"Last-Translator: \n"
|
||||
"Language: ru_RU\n"
|
||||
"Language-Team: ru_RU <LL@li.org>\n"
|
||||
@@ -402,9 +402,6 @@ msgstr "Библиотека"
|
||||
msgid "Auto Install"
|
||||
msgstr "Автоустановка"
|
||||
|
||||
msgid "Emulators"
|
||||
msgstr "Эмуляторы"
|
||||
|
||||
msgid "Wine Settings"
|
||||
msgstr "Настройки wine"
|
||||
|
||||
@@ -423,6 +420,21 @@ msgstr "Полный экран"
|
||||
msgid "Search"
|
||||
msgstr "Поиск"
|
||||
|
||||
msgid "Installation already in progress."
|
||||
msgstr "Установка уже выполняется."
|
||||
|
||||
msgid "Failed to start installation."
|
||||
msgstr "Не удалось запустить установку."
|
||||
|
||||
msgid "Installation completed successfully."
|
||||
msgstr "Установка завершена успешно."
|
||||
|
||||
msgid "Installation failed."
|
||||
msgstr "Установка не удалась."
|
||||
|
||||
msgid "Installation error."
|
||||
msgstr "Ошибка установки."
|
||||
|
||||
msgid "Loading Steam games..."
|
||||
msgstr "Загрузка игр из Steam..."
|
||||
|
||||
@@ -439,12 +451,6 @@ msgstr "Найти игры..."
|
||||
msgid "Added '{name}'"
|
||||
msgstr "'{name}' добавлен(а)"
|
||||
|
||||
msgid "Here you can configure automatic game installation..."
|
||||
msgstr "Здесь можно настроить автоматическую установку игр..."
|
||||
|
||||
msgid "List of available emulators and their configuration..."
|
||||
msgstr "Список доступных эмуляторов и их настройка..."
|
||||
|
||||
msgid "Compatibility tool:"
|
||||
msgstr "Инструмент совместимости:"
|
||||
|
||||
@@ -457,12 +463,6 @@ msgstr "Конфигурация Wine"
|
||||
msgid "Registry Editor"
|
||||
msgstr "Редактор реестра"
|
||||
|
||||
msgid "Control Panel"
|
||||
msgstr "Панель управления"
|
||||
|
||||
msgid "Task Manager"
|
||||
msgstr "Диспетчер задач"
|
||||
|
||||
msgid "Command Prompt"
|
||||
msgstr "Командная строка"
|
||||
|
||||
@@ -484,6 +484,31 @@ msgstr "Удалить Префикс"
|
||||
msgid "Clear Prefix"
|
||||
msgstr "Очистить Префикс"
|
||||
|
||||
msgid "Launching tool..."
|
||||
msgstr "Запуск инструмента..."
|
||||
|
||||
msgid "Failed to start process."
|
||||
msgstr "Не удалось запустить процесс."
|
||||
|
||||
msgid "Confirm Clear"
|
||||
msgstr "Подтвердите очистку"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Are you sure you want to clear prefix '{}'?"
|
||||
msgstr "Вы уверены, что хотите очистить префикс «{}»?"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Prefix '{}' cleared successfully."
|
||||
msgstr "Префикс '{}' успешно удален."
|
||||
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"Prefix '{}' cleared with errors:\n"
|
||||
"{}"
|
||||
msgstr ""
|
||||
"Префикс '{}' очищен с ошибками:\n"
|
||||
"{}"
|
||||
|
||||
msgid "Failed to start backup process."
|
||||
msgstr "Не удалось запустить процесс резервного копирования."
|
||||
|
||||
|
||||
@@ -5,12 +5,13 @@ import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import psutil
|
||||
import re
|
||||
|
||||
from portprotonqt.logger import get_logger
|
||||
from portprotonqt.dialogs import AddGameDialog, FileExplorer, WinetricksDialog
|
||||
from portprotonqt.game_card import GameCard
|
||||
from portprotonqt.animations import DetailPageAnimations
|
||||
from portprotonqt.custom_widgets import ClickableLabel, AutoSizeButton, NavLabel
|
||||
from portprotonqt.custom_widgets import ClickableLabel, AutoSizeButton, NavLabel, FlowLayout
|
||||
from portprotonqt.portproton_api import PortProtonAPI
|
||||
from portprotonqt.input_manager import InputManager
|
||||
from portprotonqt.context_menu_manager import ContextMenuManager, CustomLineEdit
|
||||
@@ -38,7 +39,7 @@ from portprotonqt.game_library_manager import GameLibraryManager
|
||||
from portprotonqt.virtual_keyboard import VirtualKeyboard
|
||||
|
||||
from PySide6.QtWidgets import (QLineEdit, QMainWindow, QStatusBar, QWidget, QVBoxLayout, QLabel, QHBoxLayout, QStackedWidget, QComboBox,
|
||||
QDialog, QFormLayout, QFrame, QGraphicsDropShadowEffect, QMessageBox, QApplication, QPushButton, QProgressBar, QCheckBox, QSizePolicy, QGridLayout)
|
||||
QDialog, QFormLayout, QFrame, QGraphicsDropShadowEffect, QMessageBox, QApplication, QPushButton, QProgressBar, QCheckBox, QSizePolicy, QGridLayout, QScrollArea, QScroller)
|
||||
from PySide6.QtCore import Qt, QAbstractAnimation, QUrl, Signal, QTimer, Slot, QProcess
|
||||
from PySide6.QtGui import QIcon, QPixmap, QColor, QDesktopServices
|
||||
from typing import cast
|
||||
@@ -129,6 +130,11 @@ class MainWindow(QMainWindow):
|
||||
self.update_progress.connect(self.progress_bar.setValue)
|
||||
self.update_status_message.connect(self.statusBar().showMessage)
|
||||
|
||||
self.installing = False
|
||||
self.current_install_script = None
|
||||
self.install_process = None
|
||||
self.install_monitor_timer = None
|
||||
|
||||
# Центральный виджет и основной layout
|
||||
centralWidget = QWidget()
|
||||
self.setCentralWidget(centralWidget)
|
||||
@@ -166,7 +172,6 @@ class MainWindow(QMainWindow):
|
||||
tabs = [
|
||||
_("Library"),
|
||||
_("Auto Install"),
|
||||
_("Emulators"),
|
||||
_("Wine Settings"),
|
||||
_("PortProton Settings"),
|
||||
_("Themes")
|
||||
@@ -198,7 +203,6 @@ class MainWindow(QMainWindow):
|
||||
|
||||
self.createInstalledTab()
|
||||
self.createAutoInstallTab()
|
||||
self.createEmulatorsTab()
|
||||
self.createWineTab()
|
||||
self.createPortProtonTab()
|
||||
self.createThemeTab()
|
||||
@@ -439,6 +443,102 @@ class MainWindow(QMainWindow):
|
||||
# Update navigation buttons
|
||||
self.updateNavButtons()
|
||||
|
||||
def launch_autoinstall(self, script_name: str):
|
||||
"""Launch auto-install script."""
|
||||
if self.installing:
|
||||
QMessageBox.warning(self, _("Warning"), _("Installation already in progress."))
|
||||
return
|
||||
self.installing = True
|
||||
self.current_install_script = script_name
|
||||
self.seen_progress = False
|
||||
self.current_percent = 0.0
|
||||
start_sh = os.path.join(self.portproton_location or "", "data", "scripts", "start.sh") if self.portproton_location else ""
|
||||
if not os.path.exists(start_sh):
|
||||
self.installing = False
|
||||
return
|
||||
cmd = [start_sh, "cli", "--autoinstall", script_name]
|
||||
self.install_process = QProcess(self)
|
||||
self.install_process.finished.connect(self.on_install_finished)
|
||||
self.install_process.errorOccurred.connect(self.on_install_error)
|
||||
self.install_process.start(cmd[0], cmd[1:])
|
||||
if not self.install_process.waitForStarted(5000):
|
||||
self.installing = False
|
||||
QMessageBox.warning(self, _("Error"), _("Failed to start installation."))
|
||||
return
|
||||
self.progress_bar.setVisible(True)
|
||||
self.progress_bar.setRange(0, 0) # Indeterminate
|
||||
self.update_status_message.emit(f"Processed {script_name} installation...", 0)
|
||||
self.install_monitor_timer = QTimer(self)
|
||||
self.install_monitor_timer.timeout.connect(self.monitor_install_progress)
|
||||
self.install_monitor_timer.start(2000) # Start monitoring after 2s
|
||||
|
||||
def monitor_install_progress(self):
|
||||
"""Monitor /tmp/PortProton_$USER/process.log for progress."""
|
||||
user = os.getenv('USER', 'unknown')
|
||||
log_file = f"/tmp/PortProton_{user}/process.log"
|
||||
if not os.path.exists(log_file):
|
||||
return
|
||||
try:
|
||||
with open(log_file, encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
# Extract all percentage matches, including .0% as 0.0
|
||||
matches = re.findall(r'([0-9]*\.?[0-9]+)%', content)
|
||||
if matches:
|
||||
try:
|
||||
percent = float(matches[-1])
|
||||
if percent > 0:
|
||||
self.seen_progress = True
|
||||
self.current_percent = percent
|
||||
elif self.seen_progress and percent == 0:
|
||||
self.current_percent = 100.0
|
||||
if self.install_monitor_timer is not None:
|
||||
self.install_monitor_timer.stop()
|
||||
# Update progress bar to determinate if not already
|
||||
if self.progress_bar.maximum() == 0:
|
||||
self.progress_bar.setRange(0, 100)
|
||||
self.progress_bar.setFormat("%p") # Show percentage
|
||||
self.progress_bar.setValue(int(self.current_percent))
|
||||
if self.current_percent >= 100:
|
||||
if self.install_monitor_timer is not None:
|
||||
self.install_monitor_timer.stop()
|
||||
except ValueError:
|
||||
pass # Ignore invalid floats
|
||||
except Exception as e:
|
||||
logger.error(f"Error monitoring log: {e}")
|
||||
|
||||
@Slot(int, int)
|
||||
def on_install_finished(self, exit_code: int, exit_status: int):
|
||||
"""Handle installation finish."""
|
||||
self.installing = False
|
||||
if self.install_monitor_timer is not None:
|
||||
self.install_monitor_timer.stop()
|
||||
self.install_monitor_timer.deleteLater()
|
||||
self.install_monitor_timer = None
|
||||
self.progress_bar.setRange(0, 100)
|
||||
self.progress_bar.setValue(100)
|
||||
if exit_code == 0:
|
||||
self.update_status_message.emit(_("Installation completed successfully."), 5000)
|
||||
QTimer.singleShot(500, lambda: self.restart_application())
|
||||
else:
|
||||
self.update_status_message.emit(_("Installation failed."), 5000)
|
||||
QMessageBox.warning(self, _("Error"), f"Installation failed (code: {exit_code}).")
|
||||
self.progress_bar.setVisible(False)
|
||||
self.current_install_script = None
|
||||
if self.install_process:
|
||||
self.install_process.deleteLater()
|
||||
self.install_process = None
|
||||
|
||||
def on_install_error(self, error: QProcess.ProcessError):
|
||||
"""Handle installation error."""
|
||||
self.installing = False
|
||||
if self.install_monitor_timer is not None:
|
||||
self.install_monitor_timer.stop()
|
||||
self.install_monitor_timer.deleteLater()
|
||||
self.install_monitor_timer = None
|
||||
self.update_status_message.emit(_("Installation error."), 5000)
|
||||
QMessageBox.warning(self, _("Error"), f"Process error: {error}")
|
||||
self.progress_bar.setVisible(False)
|
||||
|
||||
@Slot(list)
|
||||
def on_games_loaded(self, games: list[tuple]):
|
||||
self.game_library_manager.set_games(games)
|
||||
@@ -960,52 +1060,167 @@ class MainWindow(QMainWindow):
|
||||
get_steam_game_info_async(final_name, exec_line, on_steam_info)
|
||||
|
||||
def createAutoInstallTab(self):
|
||||
"""Вкладка 'Auto Install'."""
|
||||
self.autoInstallWidget = QWidget()
|
||||
self.autoInstallWidget.setStyleSheet(self.theme.OTHER_PAGES_WIDGET_STYLE)
|
||||
self.autoInstallWidget.setObjectName("otherPage")
|
||||
layout = QVBoxLayout(self.autoInstallWidget)
|
||||
layout.setContentsMargins(10, 18, 10, 10)
|
||||
autoInstallPage = QWidget()
|
||||
autoInstallPage.setStyleSheet(self.theme.LIBRARY_WIDGET_STYLE)
|
||||
autoInstallLayout = QVBoxLayout(autoInstallPage)
|
||||
autoInstallLayout.setContentsMargins(0, 0, 0, 0)
|
||||
autoInstallLayout.setSpacing(0)
|
||||
|
||||
self.autoInstallTitle = QLabel(_("Auto Install"))
|
||||
self.autoInstallTitle.setStyleSheet(self.theme.TAB_TITLE_STYLE)
|
||||
self.autoInstallTitle.setObjectName("tabTitle")
|
||||
layout.addWidget(self.autoInstallTitle)
|
||||
# Верхняя панель с заголовком и поиском
|
||||
headerWidget = QWidget()
|
||||
headerLayout = QHBoxLayout(headerWidget)
|
||||
headerLayout.setContentsMargins(20, 10, 20, 10)
|
||||
headerLayout.setSpacing(10)
|
||||
|
||||
self.autoInstallContent = QLabel(_("Here you can configure automatic game installation..."))
|
||||
self.autoInstallContent.setStyleSheet(self.theme.CONTENT_STYLE)
|
||||
self.autoInstallContent.setObjectName("tabContent")
|
||||
layout.addWidget(self.autoInstallContent)
|
||||
layout.addStretch(1)
|
||||
# Заголовок
|
||||
titleLabel = QLabel(_("Auto Install"))
|
||||
titleLabel.setStyleSheet(self.theme.TAB_TITLE_STYLE)
|
||||
titleLabel.setAlignment(Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignLeft)
|
||||
headerLayout.addWidget(titleLabel)
|
||||
|
||||
self.stackedWidget.addWidget(self.autoInstallWidget)
|
||||
headerLayout.addStretch()
|
||||
|
||||
def createEmulatorsTab(self):
|
||||
"""Вкладка 'Emulators'."""
|
||||
self.emulatorsWidget = QWidget()
|
||||
self.emulatorsWidget.setStyleSheet(self.theme.OTHER_PAGES_WIDGET_STYLE)
|
||||
self.emulatorsWidget.setObjectName("otherPage")
|
||||
layout = QVBoxLayout(self.emulatorsWidget)
|
||||
layout.setContentsMargins(10, 18, 10, 10)
|
||||
# Поисковая строка
|
||||
self.autoInstallSearchLineEdit = CustomLineEdit(self, theme=self.theme)
|
||||
icon: QIcon = cast(QIcon, self.theme_manager.get_icon("search"))
|
||||
action_pos = QLineEdit.ActionPosition.LeadingPosition
|
||||
self.search_action = self.autoInstallSearchLineEdit.addAction(icon, action_pos)
|
||||
self.autoInstallSearchLineEdit.setMaximumWidth(200)
|
||||
self.autoInstallSearchLineEdit.setPlaceholderText(_("Find Games ..."))
|
||||
self.autoInstallSearchLineEdit.setClearButtonEnabled(True)
|
||||
self.autoInstallSearchLineEdit.setStyleSheet(self.theme.SEARCH_EDIT_STYLE)
|
||||
self.autoInstallSearchLineEdit.textChanged.connect(self.filterAutoInstallGames)
|
||||
headerLayout.addWidget(self.autoInstallSearchLineEdit)
|
||||
|
||||
self.emulatorsTitle = QLabel(_("Emulators"))
|
||||
self.emulatorsTitle.setStyleSheet(self.theme.TAB_TITLE_STYLE)
|
||||
self.emulatorsTitle.setObjectName("tabTitle")
|
||||
layout.addWidget(self.emulatorsTitle)
|
||||
autoInstallLayout.addWidget(headerWidget)
|
||||
|
||||
self.emulatorsContent = QLabel(_("List of available emulators and their configuration..."))
|
||||
self.emulatorsContent.setStyleSheet(self.theme.CONTENT_STYLE)
|
||||
self.emulatorsContent.setObjectName("tabContent")
|
||||
layout.addWidget(self.emulatorsContent)
|
||||
layout.addStretch(1)
|
||||
# Прогресс-бар
|
||||
self.autoInstallProgress = QProgressBar()
|
||||
self.autoInstallProgress.setStyleSheet(self.theme.PROGRESS_BAR_STYLE)
|
||||
self.autoInstallProgress.setVisible(False)
|
||||
autoInstallLayout.addWidget(self.autoInstallProgress)
|
||||
|
||||
self.stackedWidget.addWidget(self.emulatorsWidget)
|
||||
# Скролл
|
||||
self.autoInstallScrollArea = QScrollArea()
|
||||
self.autoInstallScrollArea.setWidgetResizable(True)
|
||||
self.autoInstallScrollArea.setStyleSheet(self.theme.SCROLL_AREA_STYLE)
|
||||
QScroller.grabGesture(self.autoInstallScrollArea.viewport(), QScroller.ScrollerGestureType.LeftMouseButtonGesture)
|
||||
|
||||
self.autoInstallContainer = QWidget()
|
||||
self.autoInstallContainerLayout = FlowLayout(self.autoInstallContainer)
|
||||
self.autoInstallContainer.setLayout(self.autoInstallContainerLayout)
|
||||
self.autoInstallScrollArea.setWidget(self.autoInstallContainer)
|
||||
|
||||
autoInstallLayout.addWidget(self.autoInstallScrollArea)
|
||||
|
||||
# Хранение карточек
|
||||
self.autoInstallGameCards = {}
|
||||
self.allAutoInstallCards = []
|
||||
|
||||
# Обновление обложки
|
||||
def on_autoinstall_cover_updated(exe_name, local_path):
|
||||
if exe_name in self.autoInstallGameCards and local_path:
|
||||
card = self.autoInstallGameCards[exe_name]
|
||||
card.cover_path = local_path
|
||||
load_pixmap_async(local_path, self.card_width, int(self.card_width * 1.5), card.on_cover_loaded)
|
||||
|
||||
# Загрузка игр
|
||||
def on_autoinstall_games_loaded(games: list[tuple]):
|
||||
self.autoInstallProgress.setVisible(False)
|
||||
|
||||
# Очистка
|
||||
while self.autoInstallContainerLayout.count():
|
||||
child = self.autoInstallContainerLayout.takeAt(0)
|
||||
if child:
|
||||
child.widget().deleteLater()
|
||||
|
||||
self.autoInstallGameCards.clear()
|
||||
self.allAutoInstallCards.clear()
|
||||
|
||||
if not games:
|
||||
return
|
||||
|
||||
# Callback для запуска установки
|
||||
def select_callback(name, description, cover_path, appid, exec_line, controller_support, *_):
|
||||
if not exec_line or not exec_line.startswith("autoinstall:"):
|
||||
logger.warning(f"Invalid exec_line for autoinstall: {exec_line}")
|
||||
return
|
||||
script_name = exec_line[11:].lstrip(':').strip()
|
||||
self.launch_autoinstall(script_name)
|
||||
|
||||
# Создаём карточки
|
||||
for game_tuple in games:
|
||||
name, description, cover_path, appid, controller_support, exec_line, *_ , game_source, exe_name = game_tuple
|
||||
|
||||
card = GameCard(
|
||||
name, description, cover_path, appid, controller_support,
|
||||
exec_line, None, None, None,
|
||||
None, None, None, game_source,
|
||||
select_callback=select_callback,
|
||||
theme=self.theme,
|
||||
card_width=self.card_width,
|
||||
parent=self.autoInstallContainer,
|
||||
)
|
||||
|
||||
# Hide badges and favorite button
|
||||
if hasattr(card, 'steamLabel'):
|
||||
card.steamLabel.setVisible(False)
|
||||
if hasattr(card, 'egsLabel'):
|
||||
card.egsLabel.setVisible(False)
|
||||
if hasattr(card, 'portprotonLabel'):
|
||||
card.portprotonLabel.setVisible(False)
|
||||
if hasattr(card, 'protondbLabel'):
|
||||
card.protondbLabel.setVisible(False)
|
||||
if hasattr(card, 'anticheatLabel'):
|
||||
card.anticheatLabel.setVisible(False)
|
||||
if hasattr(card, 'favoriteLabel'):
|
||||
card.favoriteLabel.setVisible(False)
|
||||
|
||||
self.autoInstallGameCards[exe_name] = card
|
||||
self.allAutoInstallCards.append(card)
|
||||
self.autoInstallContainerLayout.addWidget(card)
|
||||
|
||||
# Загружаем недостающие обложки
|
||||
for game_tuple in games:
|
||||
name, _, cover_path, *_ , game_source, exe_name = game_tuple
|
||||
if not cover_path:
|
||||
self.portproton_api.download_autoinstall_cover_async(
|
||||
exe_name, timeout=5,
|
||||
callback=lambda path, ex=exe_name: on_autoinstall_cover_updated(ex, path)
|
||||
)
|
||||
|
||||
self.autoInstallContainer.updateGeometry()
|
||||
self.autoInstallScrollArea.updateGeometry()
|
||||
self.filterAutoInstallGames()
|
||||
|
||||
# Показываем прогресс
|
||||
self.autoInstallProgress.setVisible(True)
|
||||
self.autoInstallProgress.setRange(0, 0)
|
||||
self.portproton_api.get_autoinstall_games_async(on_autoinstall_games_loaded)
|
||||
|
||||
self.stackedWidget.addWidget(autoInstallPage)
|
||||
|
||||
def filterAutoInstallGames(self):
|
||||
"""Filter auto install game cards based on search text."""
|
||||
search_text = self.autoInstallSearchLineEdit.text().lower().strip()
|
||||
visible_count = 0
|
||||
|
||||
for card in self.allAutoInstallCards:
|
||||
if search_text in card.name.lower():
|
||||
card.setVisible(True)
|
||||
visible_count += 1
|
||||
else:
|
||||
card.setVisible(False)
|
||||
|
||||
# Re-layout the container
|
||||
self.autoInstallContainerLayout.invalidate()
|
||||
self.autoInstallContainer.updateGeometry()
|
||||
self.autoInstallScrollArea.updateGeometry()
|
||||
|
||||
def createWineTab(self):
|
||||
"""Вкладка 'Wine Settings'."""
|
||||
self.wineWidget = QWidget()
|
||||
self.wineWidget.setStyleSheet(self.theme.OTHER_PAGES_WIDGET_STYLE)
|
||||
self.wineWidget.setObjectName("otherPage")
|
||||
layout = QVBoxLayout(self.wineWidget)
|
||||
layout.setContentsMargins(10, 18, 10, 10)
|
||||
|
||||
@@ -1061,21 +1276,20 @@ class MainWindow(QMainWindow):
|
||||
tools_grid.setSpacing(6)
|
||||
|
||||
tools = [
|
||||
("winecfg", _("Wine Configuration")),
|
||||
("regedit", _("Registry Editor")),
|
||||
("control", _("Control Panel")),
|
||||
("taskmgr", _("Task Manager")),
|
||||
("explorer", _("File Explorer")),
|
||||
("cmd", _("Command Prompt")),
|
||||
("uninstaller", _("Uninstaller")),
|
||||
("--winecfg", _("Wine Configuration")),
|
||||
("--winereg", _("Registry Editor")),
|
||||
("--winefile", _("File Explorer")),
|
||||
("--winecmd", _("Command Prompt")),
|
||||
("--wine_uninstaller", _("Uninstaller")),
|
||||
]
|
||||
|
||||
for i, (_tool_cmd, tool_name) in enumerate(tools):
|
||||
for i, (tool_cmd, tool_name) in enumerate(tools):
|
||||
row = i // 3
|
||||
col = i % 3
|
||||
btn = AutoSizeButton(tool_name, update_size=False)
|
||||
btn.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||
btn.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||
btn.clicked.connect(lambda checked, t=tool_cmd: self.launch_generic_tool(t))
|
||||
tools_grid.addWidget(btn, row, col)
|
||||
|
||||
for col in range(3):
|
||||
@@ -1093,7 +1307,7 @@ class MainWindow(QMainWindow):
|
||||
(_("Load Prefix Backup"), self.load_prefix_backup),
|
||||
(_("Delete Compatibility Tool"), self.delete_compat_tool),
|
||||
(_("Delete Prefix"), self.delete_prefix),
|
||||
(_("Clear Prefix"), None),
|
||||
(_("Clear Prefix"), self.clear_prefix),
|
||||
]
|
||||
|
||||
for i, (text, callback) in enumerate(additional_buttons):
|
||||
@@ -1114,8 +1328,220 @@ class MainWindow(QMainWindow):
|
||||
additional_grid.setContentsMargins(10, 6, 10, 0)
|
||||
layout.addStretch(1)
|
||||
|
||||
self.wine_progress_bar = QProgressBar(self.wineWidget)
|
||||
self.wine_progress_bar.setStyleSheet(self.theme.PROGRESS_BAR_STYLE)
|
||||
self.wine_progress_bar.setMaximumWidth(200)
|
||||
self.wine_progress_bar.setTextVisible(True)
|
||||
self.wine_progress_bar.setVisible(False)
|
||||
self.wine_progress_bar.setRange(0, 0)
|
||||
|
||||
wine_progress_layout = QHBoxLayout()
|
||||
wine_progress_layout.addStretch(1)
|
||||
wine_progress_layout.addWidget(self.wine_progress_bar)
|
||||
layout.addLayout(wine_progress_layout)
|
||||
|
||||
self.stackedWidget.addWidget(self.wineWidget)
|
||||
|
||||
def launch_generic_tool(self, cli_arg):
|
||||
wine = self.wineCombo.currentText()
|
||||
prefix = self.prefixCombo.currentText()
|
||||
if not wine or not prefix:
|
||||
return
|
||||
if not self.portproton_location:
|
||||
return
|
||||
start_sh = os.path.join(self.portproton_location, "data", "scripts", "start.sh")
|
||||
if not os.path.exists(start_sh):
|
||||
return
|
||||
cmd = [start_sh, "cli", cli_arg, wine, prefix]
|
||||
|
||||
# Показываем прогресс-бар перед запуском
|
||||
self.wine_progress_bar.setVisible(True)
|
||||
self.update_status_message.emit(_("Launching tool..."), 0)
|
||||
|
||||
proc = QProcess(self)
|
||||
proc.finished.connect(lambda exitCode, exitStatus: self._on_wine_tool_finished(exitCode, cli_arg))
|
||||
proc.errorOccurred.connect(lambda error: self._on_wine_tool_error(error, cli_arg))
|
||||
proc.start(cmd[0], cmd[1:])
|
||||
|
||||
if not proc.waitForStarted(5000):
|
||||
self.wine_progress_bar.setVisible(False)
|
||||
self.update_status_message.emit("", 0)
|
||||
QMessageBox.warning(self, _("Error"), _("Failed to start process."))
|
||||
return
|
||||
|
||||
self._start_wine_process_monitor(cli_arg)
|
||||
|
||||
def _start_wine_process_monitor(self, cli_arg):
|
||||
"""Запускает таймер для мониторинга запуска Wine утилиты."""
|
||||
self.wine_monitor_timer = QTimer(self)
|
||||
self.wine_monitor_timer.setInterval(500)
|
||||
self.wine_monitor_timer.timeout.connect(lambda: self._check_wine_process(cli_arg))
|
||||
self.wine_monitor_timer.start()
|
||||
|
||||
def _check_wine_process(self, cli_arg):
|
||||
"""Проверяет, запустился ли целевой .exe процесс."""
|
||||
exe_map = {
|
||||
"--winecfg": "winecfg.exe",
|
||||
"--winereg": "regedit.exe",
|
||||
"--winefile": "winefile.exe",
|
||||
"--winecmd": "cmd.exe",
|
||||
"--wine_uninstaller": "uninstaller.exe",
|
||||
}
|
||||
target_exe = exe_map.get(cli_arg, "")
|
||||
if not target_exe:
|
||||
return
|
||||
|
||||
# Проверяем процессы через psutil
|
||||
for proc in psutil.process_iter(attrs=["name"]):
|
||||
if proc.info["name"].lower() == target_exe.lower():
|
||||
# Процесс запустился — скрываем прогресс-бар и останавливаем мониторинг
|
||||
self.wine_progress_bar.setVisible(False)
|
||||
self.update_status_message.emit("", 0)
|
||||
if hasattr(self, 'wine_monitor_timer') and self.wine_monitor_timer is not None:
|
||||
self.wine_monitor_timer.stop()
|
||||
self.wine_monitor_timer.deleteLater()
|
||||
self.wine_monitor_timer = None
|
||||
logger.info(f"Wine tool {target_exe} started successfully")
|
||||
return
|
||||
|
||||
def _on_wine_tool_finished(self, exitCode, cli_arg):
|
||||
"""Обработчик завершения Wine утилиты."""
|
||||
self.wine_progress_bar.setVisible(False)
|
||||
self.update_status_message.emit("", 0)
|
||||
# Останавливаем мониторинг, если он активен
|
||||
if hasattr(self, 'wine_monitor_timer') and self.wine_monitor_timer is not None:
|
||||
self.wine_monitor_timer.stop()
|
||||
self.wine_monitor_timer.deleteLater()
|
||||
self.wine_monitor_timer = None
|
||||
if exitCode == 0:
|
||||
logger.info(f"Wine tool {cli_arg} finished successfully")
|
||||
else:
|
||||
logger.warning(f"Wine tool {cli_arg} finished with exit code {exitCode}")
|
||||
|
||||
def _on_wine_tool_error(self, error, cli_arg):
|
||||
"""Обработчик ошибки запуска Wine утилиты."""
|
||||
self.wine_progress_bar.setVisible(False)
|
||||
self.update_status_message.emit("", 0)
|
||||
# Останавливаем мониторинг, если он активен
|
||||
if hasattr(self, 'wine_monitor_timer') and self.wine_monitor_timer is not None:
|
||||
self.wine_monitor_timer.stop()
|
||||
self.wine_monitor_timer.deleteLater()
|
||||
self.wine_monitor_timer = None
|
||||
logger.error(f"Wine tool {cli_arg} error: {error}")
|
||||
QMessageBox.warning(self, _("Error"), f"Failed to launch tool: {error}")
|
||||
|
||||
def clear_prefix(self):
|
||||
"""Очистка префикса (позже удалить)."""
|
||||
selected_prefix = self.prefixCombo.currentText()
|
||||
selected_wine = self.wineCombo.currentText()
|
||||
if not selected_prefix or not selected_wine:
|
||||
return
|
||||
if not self.portproton_location:
|
||||
return
|
||||
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
_("Confirm Clear"),
|
||||
_("Are you sure you want to clear prefix '{}'?").format(selected_prefix),
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
QMessageBox.StandardButton.No
|
||||
)
|
||||
if reply != QMessageBox.StandardButton.Yes:
|
||||
return
|
||||
|
||||
prefix_dir = os.path.join(self.portproton_location, "data", "prefixes", selected_prefix)
|
||||
if not os.path.exists(prefix_dir):
|
||||
return
|
||||
|
||||
success = True
|
||||
errors = []
|
||||
|
||||
# Удаление файлов
|
||||
files_to_remove = [
|
||||
os.path.join(prefix_dir, "*.dot*"),
|
||||
os.path.join(prefix_dir, "*.prog*"),
|
||||
os.path.join(prefix_dir, ".wine_ver"),
|
||||
os.path.join(prefix_dir, "system.reg"),
|
||||
os.path.join(prefix_dir, "user.reg"),
|
||||
os.path.join(prefix_dir, "userdef.reg"),
|
||||
os.path.join(prefix_dir, "winetricks.log"),
|
||||
os.path.join(prefix_dir, ".update-timestamp"),
|
||||
os.path.join(prefix_dir, "drive_c", ".windows-serial"),
|
||||
]
|
||||
|
||||
import glob
|
||||
for pattern in files_to_remove:
|
||||
if "*" in pattern: # Глобальный паттерн
|
||||
matches = glob.glob(pattern)
|
||||
for file_path in matches:
|
||||
try:
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
except Exception as e:
|
||||
success = False
|
||||
errors.append(str(e))
|
||||
else: # Конкретный файл
|
||||
try:
|
||||
if os.path.exists(pattern):
|
||||
os.remove(pattern)
|
||||
except Exception as e:
|
||||
success = False
|
||||
errors.append(str(e))
|
||||
|
||||
# Удаление директорий
|
||||
dirs_to_remove = [
|
||||
os.path.join(prefix_dir, "drive_c", "windows"),
|
||||
os.path.join(prefix_dir, "drive_c", "ProgramData", "Setup"),
|
||||
os.path.join(prefix_dir, "drive_c", "ProgramData", "Windows"),
|
||||
os.path.join(prefix_dir, "drive_c", "ProgramData", "WindowsTask"),
|
||||
os.path.join(prefix_dir, "drive_c", "ProgramData", "Package Cache"),
|
||||
os.path.join(prefix_dir, "drive_c", "users", "Public", "Local Settings", "Application Data", "Microsoft"),
|
||||
os.path.join(prefix_dir, "drive_c", "users", "Public", "Local Settings", "Application Data", "Temp"),
|
||||
os.path.join(prefix_dir, "drive_c", "users", "Public", "Local Settings", "Temporary Internet Files"),
|
||||
os.path.join(prefix_dir, "drive_c", "users", "Public", "Application Data", "Microsoft"),
|
||||
os.path.join(prefix_dir, "drive_c", "users", "Public", "Application Data", "wine_gecko"),
|
||||
os.path.join(prefix_dir, "drive_c", "users", "Public", "Temp"),
|
||||
os.path.join(prefix_dir, "drive_c", "users", "steamuser", "Local Settings", "Application Data", "Microsoft"),
|
||||
os.path.join(prefix_dir, "drive_c", "users", "steamuser", "Local Settings", "Application Data", "Temp"),
|
||||
os.path.join(prefix_dir, "drive_c", "users", "steamuser", "Local Settings", "Temporary Internet Files"),
|
||||
os.path.join(prefix_dir, "drive_c", "users", "steamuser", "Application Data", "Microsoft"),
|
||||
os.path.join(prefix_dir, "drive_c", "users", "steamuser", "Application Data", "wine_gecko"),
|
||||
os.path.join(prefix_dir, "drive_c", "users", "steamuser", "Temp"),
|
||||
os.path.join(prefix_dir, "drive_c", "Program Files", "Internet Explorer"),
|
||||
os.path.join(prefix_dir, "drive_c", "Program Files", "Windows Media Player"),
|
||||
os.path.join(prefix_dir, "drive_c", "Program Files", "Windows NT"),
|
||||
os.path.join(prefix_dir, "drive_c", "Program Files (x86)", "Internet Explorer"),
|
||||
os.path.join(prefix_dir, "drive_c", "Program Files (x86)", "Windows Media Player"),
|
||||
os.path.join(prefix_dir, "drive_c", "Program Files (x86)", "Windows NT"),
|
||||
]
|
||||
|
||||
import shutil
|
||||
for dir_path in dirs_to_remove:
|
||||
try:
|
||||
if os.path.exists(dir_path):
|
||||
shutil.rmtree(dir_path)
|
||||
except Exception as e:
|
||||
success = False
|
||||
errors.append(str(e))
|
||||
|
||||
tmp_path = os.path.join(self.portproton_location, "data", "tmp")
|
||||
if os.path.exists(tmp_path):
|
||||
import glob
|
||||
bin_files = glob.glob(os.path.join(tmp_path, "*.bin"))
|
||||
foz_files = glob.glob(os.path.join(tmp_path, "*.foz"))
|
||||
for file_path in bin_files + foz_files:
|
||||
try:
|
||||
os.remove(file_path)
|
||||
except Exception as e:
|
||||
success = False
|
||||
errors.append(str(e))
|
||||
|
||||
if success:
|
||||
QMessageBox.information(self, _("Success"), _("Prefix '{}' cleared successfully.").format(selected_prefix))
|
||||
else:
|
||||
error_msg = _("Prefix '{}' cleared with errors:\n{}").format(selected_prefix, "\n".join(errors[:5]))
|
||||
QMessageBox.warning(self, _("Warning"), error_msg)
|
||||
|
||||
def create_prefix_backup(self):
|
||||
selected_prefix = self.prefixCombo.currentText()
|
||||
if not selected_prefix:
|
||||
@@ -1196,8 +1622,9 @@ class MainWindow(QMainWindow):
|
||||
QMessageBox.information(self, _("Success"), _("Prefix '{}' deleted.").format(selected_prefix))
|
||||
# обновляем список
|
||||
self.prefixCombo.clear()
|
||||
self.prefixes = [d for d in os.listdir(os.path.join(self.portproton_location, "data", "prefixes"))
|
||||
if os.path.isdir(os.path.join(self.portproton_location, "data", "prefixes", d))]
|
||||
prefixes_path = os.path.join(self.portproton_location, "data", "prefixes")
|
||||
self.prefixes = [d for d in os.listdir(prefixes_path)
|
||||
if os.path.isdir(os.path.join(prefixes_path, d))]
|
||||
self.prefixCombo.addItems(self.prefixes)
|
||||
except Exception as e:
|
||||
QMessageBox.warning(self, _("Error"), _("Failed to delete prefix: {}").format(str(e)))
|
||||
@@ -1407,7 +1834,7 @@ class MainWindow(QMainWindow):
|
||||
# # 8. Legendary Authentication
|
||||
# self.legendaryAuthButton = AutoSizeButton(
|
||||
# _("Open Legendary Login"),
|
||||
# icon=self.theme_manager.get_icon("login")
|
||||
# icon=self.theme_manager.get_icon("login")self.theme_manager.get_icon("login")
|
||||
# )
|
||||
# self.legendaryAuthButton.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||
# self.legendaryAuthButton.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||
@@ -1600,9 +2027,6 @@ class MainWindow(QMainWindow):
|
||||
gamepad_connected = self.input_manager.find_gamepad() is not None
|
||||
if fullscreen or (auto_fullscreen_gamepad and gamepad_connected):
|
||||
self.showFullScreen()
|
||||
else:
|
||||
self.showNormal()
|
||||
self.resize(*read_window_geometry())
|
||||
|
||||
self.statusBar().showMessage(_("Settings saved"), 3000)
|
||||
|
||||
@@ -2392,9 +2816,7 @@ class MainWindow(QMainWindow):
|
||||
else:
|
||||
# Запускаем игру через PortProton
|
||||
env_vars = os.environ.copy()
|
||||
env_vars['START_FROM_STEAM'] = '1'
|
||||
env_vars['LEGENDARY_CONFIG_PATH'] = self.legendary_config_path
|
||||
env_vars['PROCESS_LOG'] = '1'
|
||||
|
||||
wrapper = "flatpak run ru.linux_gaming.PortProton"
|
||||
if self.portproton_location is not None and ".var" not in self.portproton_location:
|
||||
@@ -2549,16 +2971,20 @@ class MainWindow(QMainWindow):
|
||||
self.game_processes = [] # Очищаем список процессов
|
||||
|
||||
# Очищаем таймеры
|
||||
if hasattr(self, 'games_load_timer') and self.games_load_timer.isActive():
|
||||
if hasattr(self, 'games_load_timer') and self.games_load_timer is not None and self.games_load_timer.isActive():
|
||||
self.games_load_timer.stop()
|
||||
if hasattr(self, 'settingsDebounceTimer') and self.settingsDebounceTimer.isActive():
|
||||
if hasattr(self, 'settingsDebounceTimer') and self.settingsDebounceTimer is not None and self.settingsDebounceTimer.isActive():
|
||||
self.settingsDebounceTimer.stop()
|
||||
if hasattr(self, 'searchDebounceTimer') and self.searchDebounceTimer.isActive():
|
||||
if hasattr(self, 'searchDebounceTimer') and self.searchDebounceTimer is not None and self.searchDebounceTimer.isActive():
|
||||
self.searchDebounceTimer.stop()
|
||||
if hasattr(self, 'checkProcessTimer') and self.checkProcessTimer is not None and self.checkProcessTimer.isActive():
|
||||
self.checkProcessTimer.stop()
|
||||
self.checkProcessTimer.deleteLater()
|
||||
self.checkProcessTimer = None
|
||||
if hasattr(self, 'wine_monitor_timer') and self.wine_monitor_timer is not None:
|
||||
self.wine_monitor_timer.stop()
|
||||
self.wine_monitor_timer.deleteLater()
|
||||
self.wine_monitor_timer = None
|
||||
|
||||
# Сохраняем настройки окна
|
||||
if not read_fullscreen_config():
|
||||
|
||||
@@ -4,9 +4,12 @@ import orjson
|
||||
import requests
|
||||
import urllib.parse
|
||||
import time
|
||||
import glob
|
||||
import re
|
||||
from collections.abc import Callable
|
||||
from portprotonqt.downloader import Downloader
|
||||
from portprotonqt.logger import get_logger
|
||||
from portprotonqt.config_utils import get_portproton_location
|
||||
|
||||
logger = get_logger(__name__)
|
||||
CACHE_DURATION = 30 * 24 * 60 * 60 # 30 days in seconds
|
||||
@@ -52,6 +55,9 @@ class PortProtonAPI:
|
||||
self.xdg_data_home = os.getenv("XDG_DATA_HOME", os.path.join(os.path.expanduser("~"), ".local", "share"))
|
||||
self.custom_data_dir = os.path.join(self.xdg_data_home, "PortProtonQt", "custom_data")
|
||||
os.makedirs(self.custom_data_dir, exist_ok=True)
|
||||
self.portproton_location = get_portproton_location()
|
||||
self.repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
self.builtin_custom_folder = os.path.join(self.repo_root, "custom_data")
|
||||
self._topics_data = None
|
||||
|
||||
def _get_game_dir(self, exe_name: str) -> str:
|
||||
@@ -68,40 +74,6 @@ class PortProtonAPI:
|
||||
logger.debug(f"Failed to check file at {url}: {e}")
|
||||
return False
|
||||
|
||||
def download_game_assets(self, exe_name: str, timeout: int = 5) -> dict[str, str | None]:
|
||||
game_dir = self._get_game_dir(exe_name)
|
||||
results: dict[str, str | None] = {"cover": None, "metadata": None}
|
||||
cover_extensions = [".png", ".jpg", ".jpeg", ".bmp"]
|
||||
cover_url_base = f"{self.base_url}/{exe_name}/cover"
|
||||
metadata_url = f"{self.base_url}/{exe_name}/metadata.txt"
|
||||
|
||||
for ext in cover_extensions:
|
||||
cover_url = f"{cover_url_base}{ext}"
|
||||
if self._check_file_exists(cover_url, timeout):
|
||||
local_cover_path = os.path.join(game_dir, f"cover{ext}")
|
||||
result = self.downloader.download(cover_url, local_cover_path, timeout=timeout)
|
||||
if result:
|
||||
results["cover"] = result
|
||||
logger.info(f"Downloaded cover for {exe_name} to {result}")
|
||||
break
|
||||
else:
|
||||
logger.error(f"Failed to download cover for {exe_name} from {cover_url}")
|
||||
else:
|
||||
logger.debug(f"No cover found for {exe_name} with extension {ext}")
|
||||
|
||||
if self._check_file_exists(metadata_url, timeout):
|
||||
local_metadata_path = os.path.join(game_dir, "metadata.txt")
|
||||
result = self.downloader.download(metadata_url, local_metadata_path, timeout=timeout)
|
||||
if result:
|
||||
results["metadata"] = result
|
||||
logger.info(f"Downloaded metadata for {exe_name} to {result}")
|
||||
else:
|
||||
logger.error(f"Failed to download metadata for {exe_name} from {metadata_url}")
|
||||
else:
|
||||
logger.debug(f"No metadata found for {exe_name}")
|
||||
|
||||
return results
|
||||
|
||||
def download_game_assets_async(self, exe_name: str, timeout: int = 5, callback: Callable[[dict[str, str | None]], None] | None = None) -> None:
|
||||
game_dir = self._get_game_dir(exe_name)
|
||||
cover_extensions = [".png", ".jpg", ".jpeg", ".bmp"]
|
||||
@@ -163,6 +135,164 @@ class PortProtonAPI:
|
||||
if callback:
|
||||
callback(results)
|
||||
|
||||
def download_autoinstall_cover_async(self, exe_name: str, timeout: int = 5, callback: Callable[[str | None], None] | None = None) -> None:
|
||||
"""Download only autoinstall cover image (PNG only, no metadata)."""
|
||||
xdg_data_home = os.getenv("XDG_DATA_HOME",
|
||||
os.path.join(os.path.expanduser("~"), ".local", "share"))
|
||||
autoinstall_root = os.path.join(xdg_data_home, "PortProtonQt", "custom_data", "autoinstall")
|
||||
user_game_folder = os.path.join(autoinstall_root, exe_name)
|
||||
|
||||
if not os.path.isdir(user_game_folder):
|
||||
try:
|
||||
os.mkdir(user_game_folder)
|
||||
except FileExistsError:
|
||||
pass
|
||||
|
||||
cover_url = f"{self.base_url}/{exe_name}/cover.png"
|
||||
local_cover_path = os.path.join(user_game_folder, "cover.png")
|
||||
|
||||
def on_cover_downloaded(local_path: str | None):
|
||||
if local_path:
|
||||
logger.info(f"Async autoinstall cover downloaded for {exe_name}: {local_path}")
|
||||
else:
|
||||
logger.debug(f"No autoinstall cover downloaded for {exe_name}")
|
||||
if callback:
|
||||
callback(local_path)
|
||||
|
||||
if self._check_file_exists(cover_url, timeout):
|
||||
self.downloader.download_async(
|
||||
cover_url,
|
||||
local_cover_path,
|
||||
timeout=timeout,
|
||||
callback=on_cover_downloaded
|
||||
)
|
||||
else:
|
||||
logger.debug(f"No autoinstall cover found for {exe_name}")
|
||||
if callback:
|
||||
callback(None)
|
||||
|
||||
def parse_autoinstall_script(self, file_path: str) -> tuple[str | None, str | None]:
|
||||
"""Extract display_name from # name comment and exe_name from autoinstall bash script."""
|
||||
try:
|
||||
with open(file_path, encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# Skip emulators
|
||||
if re.search(r'#\s*type\s*:\s*emulators', content, re.IGNORECASE):
|
||||
return None, None
|
||||
|
||||
display_name = None
|
||||
exe_name = None
|
||||
|
||||
# Extract display_name from "# name:" comment
|
||||
name_match = re.search(r'#\s*name\s*:\s*(.+)', content, re.IGNORECASE)
|
||||
if name_match:
|
||||
display_name = name_match.group(1).strip()
|
||||
|
||||
# --- pw_create_unique_exe ---
|
||||
pw_match = re.search(r'pw_create_unique_exe(?:\s+["\']([^"\']+)["\'])?', content)
|
||||
if pw_match:
|
||||
arg = pw_match.group(1)
|
||||
if arg:
|
||||
exe_name = arg.strip()
|
||||
if not exe_name.lower().endswith(".exe"):
|
||||
exe_name += ".exe"
|
||||
else:
|
||||
export_match = re.search(
|
||||
r'export\s+PORTWINE_CREATE_SHORTCUT_NAME\s*=\s*["\']([^"\']+)["\']',
|
||||
content, re.IGNORECASE)
|
||||
if export_match:
|
||||
exe_name = f"{export_match.group(1).strip()}.exe"
|
||||
|
||||
else:
|
||||
portwine_match = None
|
||||
for line in content.splitlines():
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("#"):
|
||||
continue
|
||||
if "portwine_exe" in stripped and "=" in stripped:
|
||||
portwine_match = stripped
|
||||
break
|
||||
|
||||
if portwine_match:
|
||||
exe_expr = portwine_match.split("=", 1)[1].strip().strip("'\" ")
|
||||
exe_candidates = re.findall(r'[-\w\s/\\\.]+\.exe', exe_expr)
|
||||
if exe_candidates:
|
||||
exe_name = os.path.basename(exe_candidates[-1].strip())
|
||||
|
||||
|
||||
# Fallback
|
||||
if not display_name and exe_name:
|
||||
display_name = exe_name
|
||||
|
||||
return display_name, exe_name
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to parse {file_path}: {e}")
|
||||
return None, None
|
||||
|
||||
def get_autoinstall_games_async(self, callback: Callable[[list[tuple]], None]) -> None:
|
||||
"""Load auto-install games with user/builtin covers (no async download here)."""
|
||||
games = []
|
||||
auto_dir = os.path.join(self.portproton_location or "", "data", "scripts", "pw_autoinstall") if self.portproton_location else ""
|
||||
if not os.path.exists(auto_dir):
|
||||
callback(games)
|
||||
return
|
||||
|
||||
scripts = sorted(glob.glob(os.path.join(auto_dir, "*")))
|
||||
if not scripts:
|
||||
callback(games)
|
||||
return
|
||||
|
||||
xdg_data_home = os.getenv("XDG_DATA_HOME",
|
||||
os.path.join(os.path.expanduser("~"), ".local", "share"))
|
||||
base_autoinstall_dir = os.path.join(xdg_data_home, "PortProtonQt", "custom_data", "autoinstall")
|
||||
os.makedirs(base_autoinstall_dir, exist_ok=True)
|
||||
|
||||
for script_path in scripts:
|
||||
display_name, exe_name = self.parse_autoinstall_script(script_path)
|
||||
script_name = os.path.splitext(os.path.basename(script_path))[0]
|
||||
|
||||
if not (display_name and exe_name):
|
||||
continue
|
||||
|
||||
exe_name = os.path.splitext(exe_name)[0] # Без .exe
|
||||
user_game_folder = os.path.join(base_autoinstall_dir, exe_name)
|
||||
os.makedirs(user_game_folder, exist_ok=True)
|
||||
|
||||
# Поиск обложки
|
||||
cover_path = ""
|
||||
user_files = set(os.listdir(user_game_folder)) if os.path.exists(user_game_folder) else set()
|
||||
for ext in [".jpg", ".png", ".jpeg", ".bmp"]:
|
||||
candidate = f"cover{ext}"
|
||||
if candidate in user_files:
|
||||
cover_path = os.path.join(user_game_folder, candidate)
|
||||
break
|
||||
|
||||
if not cover_path:
|
||||
logger.debug(f"No local cover found for autoinstall {exe_name}")
|
||||
|
||||
# Формируем кортеж игры (добавлен exe_name в конец)
|
||||
game_tuple = (
|
||||
display_name, # name
|
||||
"", # description
|
||||
cover_path, # cover
|
||||
"", # appid
|
||||
f"autoinstall:{script_name}", # exec_line
|
||||
"", # controller_support
|
||||
"Never", # last_launch
|
||||
"0h 0m", # formatted_playtime
|
||||
"", # protondb_tier
|
||||
"", # anticheat_status
|
||||
0, # last_played
|
||||
0, # playtime_seconds
|
||||
"autoinstall", # game_source
|
||||
exe_name # exe_name
|
||||
)
|
||||
games.append(game_tuple)
|
||||
|
||||
callback(games)
|
||||
|
||||
def _load_topics_data(self):
|
||||
"""Load and cache linux_gaming_topics_min.json from the archive."""
|
||||
if self._topics_data is not None:
|
||||
|
||||