feat: added scale animation to game card hover and focus
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
This commit is contained in:
@@ -26,14 +26,23 @@ class GameCardAnimations:
|
|||||||
self.theme = theme if theme is not None else default_styles
|
self.theme = theme if theme is not None else default_styles
|
||||||
self.thickness_anim: QPropertyAnimation | None = None
|
self.thickness_anim: QPropertyAnimation | None = None
|
||||||
self.gradient_anim: QPropertyAnimation | None = None
|
self.gradient_anim: QPropertyAnimation | None = None
|
||||||
|
self.scale_anim: QPropertyAnimation | None = None
|
||||||
self.pulse_anim: QPropertyAnimation | None = None
|
self.pulse_anim: QPropertyAnimation | None = None
|
||||||
self._isPulseAnimationConnected = False
|
self._isPulseAnimationConnected = False
|
||||||
|
|
||||||
def setup_animations(self):
|
def setup_animations(self):
|
||||||
"""Initialize animation properties."""
|
"""Initialize animation properties based on theme."""
|
||||||
self.thickness_anim = QPropertyAnimation(self.game_card, QByteArray(b"borderWidth"))
|
self.thickness_anim = QPropertyAnimation(self.game_card, QByteArray(b"borderWidth"))
|
||||||
self.thickness_anim.setDuration(self.theme.GAME_CARD_ANIMATION["thickness_anim_duration"])
|
self.thickness_anim.setDuration(self.theme.GAME_CARD_ANIMATION["thickness_anim_duration"])
|
||||||
|
|
||||||
|
animation_type = self.theme.GAME_CARD_ANIMATION.get("card_animation_type", "gradient")
|
||||||
|
if animation_type == "gradient":
|
||||||
|
self.gradient_anim = QPropertyAnimation(self.game_card, QByteArray(b"gradientAngle"))
|
||||||
|
self.gradient_anim.setDuration(self.theme.GAME_CARD_ANIMATION["gradient_anim_duration"])
|
||||||
|
elif animation_type == "scale":
|
||||||
|
self.scale_anim = QPropertyAnimation(self.game_card, QByteArray(b"scale"))
|
||||||
|
self.scale_anim.setDuration(self.theme.GAME_CARD_ANIMATION["scale_anim_duration"])
|
||||||
|
|
||||||
def start_pulse_animation(self):
|
def start_pulse_animation(self):
|
||||||
"""Start pulse animation for border width when hovered or focused."""
|
"""Start pulse animation for border width when hovered or focused."""
|
||||||
if not (self.game_card._hovered or self.game_card._focused):
|
if not (self.game_card._hovered or self.game_card._focused):
|
||||||
@@ -57,6 +66,8 @@ class GameCardAnimations:
|
|||||||
if not self.thickness_anim:
|
if not self.thickness_anim:
|
||||||
self.setup_animations()
|
self.setup_animations()
|
||||||
|
|
||||||
|
animation_type = self.theme.GAME_CARD_ANIMATION.get("card_animation_type", "gradient")
|
||||||
|
|
||||||
if self.thickness_anim:
|
if self.thickness_anim:
|
||||||
self.thickness_anim.stop()
|
self.thickness_anim.stop()
|
||||||
if self._isPulseAnimationConnected:
|
if self._isPulseAnimationConnected:
|
||||||
@@ -69,6 +80,7 @@ class GameCardAnimations:
|
|||||||
self._isPulseAnimationConnected = True
|
self._isPulseAnimationConnected = True
|
||||||
self.thickness_anim.start()
|
self.thickness_anim.start()
|
||||||
|
|
||||||
|
if animation_type == "gradient":
|
||||||
if self.gradient_anim:
|
if self.gradient_anim:
|
||||||
self.gradient_anim.stop()
|
self.gradient_anim.stop()
|
||||||
self.gradient_anim = QPropertyAnimation(self.game_card, QByteArray(b"gradientAngle"))
|
self.gradient_anim = QPropertyAnimation(self.game_card, QByteArray(b"gradientAngle"))
|
||||||
@@ -77,15 +89,35 @@ class GameCardAnimations:
|
|||||||
self.gradient_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["gradient_end_angle"])
|
self.gradient_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["gradient_end_angle"])
|
||||||
self.gradient_anim.setLoopCount(-1)
|
self.gradient_anim.setLoopCount(-1)
|
||||||
self.gradient_anim.start()
|
self.gradient_anim.start()
|
||||||
|
elif animation_type == "scale":
|
||||||
|
if self.scale_anim:
|
||||||
|
self.scale_anim.stop()
|
||||||
|
self.scale_anim = QPropertyAnimation(self.game_card, QByteArray(b"scale"))
|
||||||
|
self.scale_anim.setDuration(self.theme.GAME_CARD_ANIMATION["scale_anim_duration"])
|
||||||
|
self.scale_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION["scale_easing_curve"]]))
|
||||||
|
self.scale_anim.setStartValue(self.game_card._scale)
|
||||||
|
self.scale_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["hover_scale"])
|
||||||
|
self.scale_anim.start()
|
||||||
|
|
||||||
def handle_leave_event(self):
|
def handle_leave_event(self):
|
||||||
"""Handle mouse leave event animations."""
|
"""Handle mouse leave event animations."""
|
||||||
self.game_card._hovered = False
|
self.game_card._hovered = False
|
||||||
self.game_card.hoverChanged.emit(self.game_card.name, False)
|
self.game_card.hoverChanged.emit(self.game_card.name, False)
|
||||||
if not self.game_card._focused:
|
if not self.game_card._focused:
|
||||||
|
animation_type = self.theme.GAME_CARD_ANIMATION.get("card_animation_type", "gradient")
|
||||||
|
if animation_type == "gradient":
|
||||||
if self.gradient_anim:
|
if self.gradient_anim:
|
||||||
self.gradient_anim.stop()
|
self.gradient_anim.stop()
|
||||||
self.gradient_anim = None
|
self.gradient_anim = None
|
||||||
|
elif animation_type == "scale":
|
||||||
|
if self.scale_anim:
|
||||||
|
self.scale_anim.stop()
|
||||||
|
self.scale_anim = QPropertyAnimation(self.game_card, QByteArray(b"scale"))
|
||||||
|
self.scale_anim.setDuration(self.theme.GAME_CARD_ANIMATION["scale_anim_duration"])
|
||||||
|
self.scale_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION["scale_easing_curve_out"]]))
|
||||||
|
self.scale_anim.setStartValue(self.game_card._scale)
|
||||||
|
self.scale_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["default_scale"])
|
||||||
|
self.scale_anim.start()
|
||||||
if self.pulse_anim:
|
if self.pulse_anim:
|
||||||
self.pulse_anim.stop()
|
self.pulse_anim.stop()
|
||||||
self.pulse_anim = None
|
self.pulse_anim = None
|
||||||
@@ -108,6 +140,8 @@ class GameCardAnimations:
|
|||||||
if not self.thickness_anim:
|
if not self.thickness_anim:
|
||||||
self.setup_animations()
|
self.setup_animations()
|
||||||
|
|
||||||
|
animation_type = self.theme.GAME_CARD_ANIMATION.get("card_animation_type", "gradient")
|
||||||
|
|
||||||
if self.thickness_anim:
|
if self.thickness_anim:
|
||||||
self.thickness_anim.stop()
|
self.thickness_anim.stop()
|
||||||
if self._isPulseAnimationConnected:
|
if self._isPulseAnimationConnected:
|
||||||
@@ -120,6 +154,7 @@ class GameCardAnimations:
|
|||||||
self._isPulseAnimationConnected = True
|
self._isPulseAnimationConnected = True
|
||||||
self.thickness_anim.start()
|
self.thickness_anim.start()
|
||||||
|
|
||||||
|
if animation_type == "gradient":
|
||||||
if self.gradient_anim:
|
if self.gradient_anim:
|
||||||
self.gradient_anim.stop()
|
self.gradient_anim.stop()
|
||||||
self.gradient_anim = QPropertyAnimation(self.game_card, QByteArray(b"gradientAngle"))
|
self.gradient_anim = QPropertyAnimation(self.game_card, QByteArray(b"gradientAngle"))
|
||||||
@@ -128,15 +163,35 @@ class GameCardAnimations:
|
|||||||
self.gradient_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["gradient_end_angle"])
|
self.gradient_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["gradient_end_angle"])
|
||||||
self.gradient_anim.setLoopCount(-1)
|
self.gradient_anim.setLoopCount(-1)
|
||||||
self.gradient_anim.start()
|
self.gradient_anim.start()
|
||||||
|
elif animation_type == "scale":
|
||||||
|
if self.scale_anim:
|
||||||
|
self.scale_anim.stop()
|
||||||
|
self.scale_anim = QPropertyAnimation(self.game_card, QByteArray(b"scale"))
|
||||||
|
self.scale_anim.setDuration(self.theme.GAME_CARD_ANIMATION["scale_anim_duration"])
|
||||||
|
self.scale_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION["scale_easing_curve"]]))
|
||||||
|
self.scale_anim.setStartValue(self.game_card._scale)
|
||||||
|
self.scale_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["focus_scale"])
|
||||||
|
self.scale_anim.start()
|
||||||
|
|
||||||
def handle_focus_out_event(self):
|
def handle_focus_out_event(self):
|
||||||
"""Handle focus out event animations."""
|
"""Handle focus out event animations."""
|
||||||
self.game_card._focused = False
|
self.game_card._focused = False
|
||||||
self.game_card.focusChanged.emit(self.game_card.name, False)
|
self.game_card.focusChanged.emit(self.game_card.name, False)
|
||||||
if not self.game_card._hovered:
|
if not self.game_card._hovered:
|
||||||
|
animation_type = self.theme.GAME_CARD_ANIMATION.get("card_animation_type", "gradient")
|
||||||
|
if animation_type == "gradient":
|
||||||
if self.gradient_anim:
|
if self.gradient_anim:
|
||||||
self.gradient_anim.stop()
|
self.gradient_anim.stop()
|
||||||
self.gradient_anim = None
|
self.gradient_anim = None
|
||||||
|
elif animation_type == "scale":
|
||||||
|
if self.scale_anim:
|
||||||
|
self.scale_anim.stop()
|
||||||
|
self.scale_anim = QPropertyAnimation(self.game_card, QByteArray(b"scale"))
|
||||||
|
self.scale_anim.setDuration(self.theme.GAME_CARD_ANIMATION["scale_anim_duration"])
|
||||||
|
self.scale_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION["scale_easing_curve_out"]]))
|
||||||
|
self.scale_anim.setStartValue(self.game_card._scale)
|
||||||
|
self.scale_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["default_scale"])
|
||||||
|
self.scale_anim.start()
|
||||||
if self.pulse_anim:
|
if self.pulse_anim:
|
||||||
self.pulse_anim.stop()
|
self.pulse_anim.stop()
|
||||||
self.pulse_anim = None
|
self.pulse_anim = None
|
||||||
@@ -157,7 +212,8 @@ class GameCardAnimations:
|
|||||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||||
pen = QPen()
|
pen = QPen()
|
||||||
pen.setWidth(self.game_card._borderWidth)
|
pen.setWidth(self.game_card._borderWidth)
|
||||||
if self.game_card._hovered or self.game_card._focused:
|
animation_type = self.theme.GAME_CARD_ANIMATION.get("card_animation_type", "gradient")
|
||||||
|
if (self.game_card._hovered or self.game_card._focused) and animation_type == "gradient":
|
||||||
center = self.game_card.rect().center()
|
center = self.game_card.rect().center()
|
||||||
gradient = QConicalGradient(center, self.game_card._gradientAngle)
|
gradient = QConicalGradient(center, self.game_card._gradientAngle)
|
||||||
for stop in self.theme.GAME_CARD_ANIMATION["gradient_colors"]:
|
for stop in self.theme.GAME_CARD_ANIMATION["gradient_colors"]:
|
||||||
@@ -166,11 +222,11 @@ class GameCardAnimations:
|
|||||||
else:
|
else:
|
||||||
pen.setColor(QColor(0, 0, 0, 0))
|
pen.setColor(QColor(0, 0, 0, 0))
|
||||||
painter.setPen(pen)
|
painter.setPen(pen)
|
||||||
radius = 18
|
radius = 18 * self.game_card._scale
|
||||||
bw = round(self.game_card._borderWidth / 2)
|
bw = round(self.game_card._borderWidth / 2)
|
||||||
rect = self.game_card.rect().adjusted(bw, bw, -bw, -bw)
|
rect = self.game_card.rect().adjusted(bw, bw, -bw, -bw)
|
||||||
if rect.isEmpty():
|
if rect.isEmpty():
|
||||||
return # Avoid drawing invalid rect
|
return
|
||||||
painter.drawRoundedRect(rect, radius, radius)
|
painter.drawRoundedRect(rect, radius, radius)
|
||||||
|
|
||||||
class DetailPageAnimations:
|
class DetailPageAnimations:
|
||||||
@@ -284,15 +340,15 @@ class DetailPageAnimations:
|
|||||||
logger.debug("Original effect already deleted")
|
logger.debug("Original effect already deleted")
|
||||||
cleanup_callback()
|
cleanup_callback()
|
||||||
animation.finished.connect(restore_and_cleanup)
|
animation.finished.connect(restore_and_cleanup)
|
||||||
animation.finished.connect(opacity_effect.deleteLater) # Clean up effect
|
animation.finished.connect(opacity_effect.deleteLater)
|
||||||
elif animation_type in ["slide_left", "slide_right", "slide_up", "slide_down"]:
|
elif animation_type in ["slide_left", "slide_right", "slide_up", "slide_down"]:
|
||||||
duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_slide_duration_exit", 500)
|
duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_slide_duration_exit", 500)
|
||||||
easing_curve = QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION.get("detail_page_easing_curve_exit", "InCubic")])
|
easing_curve = QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION.get("detail_page_easing_curve_exit", "InCubic")])
|
||||||
end_pos = {
|
end_pos = {
|
||||||
"slide_left": QPoint(-self.main_window.width(), 0), # Exit to left (opposite of entry)
|
"slide_left": QPoint(-self.main_window.width(), 0),
|
||||||
"slide_right": QPoint(self.main_window.width(), 0), # Exit to right
|
"slide_right": QPoint(self.main_window.width(), 0),
|
||||||
"slide_up": QPoint(0, self.main_window.height()), # Exit downward
|
"slide_up": QPoint(0, self.main_window.height()),
|
||||||
"slide_down": QPoint(0, -self.main_window.height()) # Exit upward
|
"slide_down": QPoint(0, -self.main_window.height())
|
||||||
}[animation_type]
|
}[animation_type]
|
||||||
animation = QPropertyAnimation(detail_page, QByteArray(b"pos"))
|
animation = QPropertyAnimation(detail_page, QByteArray(b"pos"))
|
||||||
animation.setDuration(duration)
|
animation.setDuration(duration)
|
||||||
@@ -325,4 +381,4 @@ class DetailPageAnimations:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error in animate_detail_page_exit: {e}", exc_info=True)
|
logger.error(f"Error in animate_detail_page_exit: {e}", exc_info=True)
|
||||||
self.animations.pop(detail_page, None)
|
self.animations.pop(detail_page, None)
|
||||||
cleanup_callback() # Fallback to cleanup if animation setup fails
|
cleanup_callback()
|
||||||
|
@@ -12,23 +12,21 @@ from portprotonqt.custom_widgets import ClickableLabel
|
|||||||
from portprotonqt.portproton_api import PortProtonAPI
|
from portprotonqt.portproton_api import PortProtonAPI
|
||||||
from portprotonqt.downloader import Downloader
|
from portprotonqt.downloader import Downloader
|
||||||
from portprotonqt.animations import GameCardAnimations
|
from portprotonqt.animations import GameCardAnimations
|
||||||
import weakref
|
|
||||||
from typing import cast
|
from typing import cast
|
||||||
|
|
||||||
|
|
||||||
class GameCard(QFrame):
|
class GameCard(QFrame):
|
||||||
borderWidthChanged = Signal()
|
borderWidthChanged = Signal()
|
||||||
gradientAngleChanged = Signal()
|
gradientAngleChanged = Signal()
|
||||||
# Signals for context menu actions
|
scaleChanged = Signal()
|
||||||
editShortcutRequested = Signal(str, str, str) # name, exec_line, cover_path
|
editShortcutRequested = Signal(str, str, str)
|
||||||
deleteGameRequested = Signal(str, str) # name, exec_line
|
deleteGameRequested = Signal(str, str)
|
||||||
addToMenuRequested = Signal(str, str) # name, exec_line
|
addToMenuRequested = Signal(str, str)
|
||||||
removeFromMenuRequested = Signal(str) # name
|
removeFromMenuRequested = Signal(str)
|
||||||
addToDesktopRequested = Signal(str, str) # name, exec_line
|
addToDesktopRequested = Signal(str, str)
|
||||||
removeFromDesktopRequested = Signal(str) # name
|
removeFromDesktopRequested = Signal(str)
|
||||||
addToSteamRequested = Signal(str, str, str) # name, exec_line, cover_path
|
addToSteamRequested = Signal(str, str, str)
|
||||||
removeFromSteamRequested = Signal(str, str) # name, exec_line
|
removeFromSteamRequested = Signal(str, str)
|
||||||
openGameFolderRequested = Signal(str, str) # name, exec_line
|
openGameFolderRequested = Signal(str, str)
|
||||||
hoverChanged = Signal(str, bool)
|
hoverChanged = Signal(str, bool)
|
||||||
focusChanged = Signal(str, bool)
|
focusChanged = Signal(str, bool)
|
||||||
|
|
||||||
@@ -49,7 +47,9 @@ class GameCard(QFrame):
|
|||||||
self.game_source = game_source
|
self.game_source = game_source
|
||||||
self.last_launch_ts = last_launch_ts
|
self.last_launch_ts = last_launch_ts
|
||||||
self.playtime_seconds = playtime_seconds
|
self.playtime_seconds = playtime_seconds
|
||||||
self.card_width = card_width
|
self.base_card_width = card_width
|
||||||
|
self.base_pixmap = None
|
||||||
|
self.base_font_size = None
|
||||||
|
|
||||||
self.select_callback = select_callback
|
self.select_callback = select_callback
|
||||||
self.context_menu_manager = context_menu_manager
|
self.context_menu_manager = context_menu_manager
|
||||||
@@ -67,75 +67,46 @@ class GameCard(QFrame):
|
|||||||
self.egs_visible = (str(game_source).lower() == "epic" and self.display_filter in ("all", "favorites"))
|
self.egs_visible = (str(game_source).lower() == "epic" and self.display_filter in ("all", "favorites"))
|
||||||
self.portproton_visible = (str(game_source).lower() == "portproton" and self.display_filter in ("all", "favorites"))
|
self.portproton_visible = (str(game_source).lower() == "portproton" and self.display_filter in ("all", "favorites"))
|
||||||
|
|
||||||
# Дополнительное пространство для анимации
|
self.base_extra_margin = 20
|
||||||
extra_margin = 20
|
|
||||||
self.setFixedSize(card_width + extra_margin, int(card_width * 1.6) + extra_margin)
|
|
||||||
self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||||
self.setStyleSheet(self.theme.GAME_CARD_WINDOW_STYLE)
|
self.setStyleSheet(self.theme.GAME_CARD_WINDOW_STYLE)
|
||||||
|
|
||||||
# Параметры анимации обводки
|
|
||||||
self._borderWidth = self.theme.GAME_CARD_ANIMATION["default_border_width"]
|
self._borderWidth = self.theme.GAME_CARD_ANIMATION["default_border_width"]
|
||||||
self._gradientAngle = self.theme.GAME_CARD_ANIMATION["gradient_start_angle"]
|
self._gradientAngle = self.theme.GAME_CARD_ANIMATION["gradient_start_angle"]
|
||||||
|
self._scale = self.theme.GAME_CARD_ANIMATION["default_scale"]
|
||||||
self._hovered = False
|
self._hovered = False
|
||||||
self._focused = False
|
self._focused = False
|
||||||
|
|
||||||
# Анимации
|
|
||||||
self.animations = GameCardAnimations(self, self.theme)
|
self.animations = GameCardAnimations(self, self.theme)
|
||||||
self.animations.setup_animations()
|
self.animations.setup_animations()
|
||||||
|
|
||||||
# Тень
|
self.shadow = QGraphicsDropShadowEffect(self)
|
||||||
shadow = QGraphicsDropShadowEffect(self)
|
self.shadow.setBlurRadius(20)
|
||||||
shadow.setBlurRadius(20)
|
self.shadow.setColor(QColor(0, 0, 0, 150))
|
||||||
shadow.setColor(QColor(0, 0, 0, 150))
|
self.shadow.setOffset(0, 0)
|
||||||
shadow.setOffset(0, 0)
|
self.setGraphicsEffect(self.shadow)
|
||||||
self.setGraphicsEffect(shadow)
|
|
||||||
|
|
||||||
# Отступы
|
self.layout_ = QVBoxLayout(self)
|
||||||
layout = QVBoxLayout(self)
|
self.layout_.setSpacing(5)
|
||||||
layout.setContentsMargins(extra_margin // 2, extra_margin // 2, extra_margin // 2, extra_margin // 2)
|
self.layout_.setContentsMargins(self.base_extra_margin // 2, self.base_extra_margin // 2, self.base_extra_margin // 2, self.base_extra_margin // 2)
|
||||||
layout.setSpacing(5)
|
|
||||||
|
|
||||||
# Контейнер обложки
|
self.coverWidget = QWidget()
|
||||||
coverWidget = QWidget()
|
coverLayout = QStackedLayout(self.coverWidget)
|
||||||
coverWidget.setFixedSize(card_width, int(card_width * 1.2))
|
|
||||||
coverLayout = QStackedLayout(coverWidget)
|
|
||||||
coverLayout.setContentsMargins(0, 0, 0, 0)
|
coverLayout.setContentsMargins(0, 0, 0, 0)
|
||||||
coverLayout.setStackingMode(QStackedLayout.StackingMode.StackAll)
|
coverLayout.setStackingMode(QStackedLayout.StackingMode.StackAll)
|
||||||
|
|
||||||
# Обложка
|
|
||||||
self.coverLabel = QLabel()
|
self.coverLabel = QLabel()
|
||||||
self.coverLabel.setFixedSize(card_width, int(card_width * 1.2))
|
|
||||||
self.coverLabel.setStyleSheet(self.theme.COVER_LABEL_STYLE)
|
self.coverLabel.setStyleSheet(self.theme.COVER_LABEL_STYLE)
|
||||||
coverLayout.addWidget(self.coverLabel)
|
coverLayout.addWidget(self.coverLabel)
|
||||||
|
|
||||||
# создаём слабую ссылку на label
|
load_pixmap_async(cover_path or "", self.base_card_width, int(self.base_card_width * 1.2), self.on_cover_loaded)
|
||||||
label_ref = weakref.ref(self.coverLabel)
|
|
||||||
|
|
||||||
def on_cover_loaded(pixmap):
|
self.favoriteLabel = ClickableLabel(self.coverWidget)
|
||||||
label = label_ref()
|
|
||||||
if label is None:
|
|
||||||
return
|
|
||||||
label.setPixmap(round_corners(pixmap, 15))
|
|
||||||
|
|
||||||
# асинхронная загрузка обложки (пустая строка даст placeholder внутри load_pixmap_async)
|
|
||||||
load_pixmap_async(cover_path or "", card_width, int(card_width * 1.2), on_cover_loaded)
|
|
||||||
|
|
||||||
# Значок избранного (звёздочка) в левом верхнем углу обложки
|
|
||||||
self.favoriteLabel = ClickableLabel(coverWidget)
|
|
||||||
self.favoriteLabel.setFixedSize(*self.theme.favoriteLabelSize)
|
|
||||||
self.favoriteLabel.move(8, 8)
|
|
||||||
self.favoriteLabel.clicked.connect(self.toggle_favorite)
|
self.favoriteLabel.clicked.connect(self.toggle_favorite)
|
||||||
self.is_favorite = self.name in read_favorites()
|
self.is_favorite = self.name in read_favorites()
|
||||||
self.update_favorite_icon()
|
self.update_favorite_icon()
|
||||||
self.favoriteLabel.raise_()
|
self.favoriteLabel.raise_()
|
||||||
|
|
||||||
# Определяем общие параметры для бейджей
|
|
||||||
badge_width = int(card_width * 2/3)
|
|
||||||
icon_size = int(card_width * 0.06) # 6% от ширины карточки
|
|
||||||
icon_space = int(card_width * 0.012) # 1.2% от ширины карточки
|
|
||||||
font_scale_factor = 0.06 # Шрифт будет 6% от card_width
|
|
||||||
|
|
||||||
# ProtonDB бейдж
|
|
||||||
tier_text = self.getProtonDBText(protondb_tier)
|
tier_text = self.getProtonDBText(protondb_tier)
|
||||||
if tier_text:
|
if tier_text:
|
||||||
icon_filename = self.getProtonDBIconFilename(protondb_tier)
|
icon_filename = self.getProtonDBIconFilename(protondb_tier)
|
||||||
@@ -143,67 +114,50 @@ class GameCard(QFrame):
|
|||||||
self.protondbLabel = ClickableLabel(
|
self.protondbLabel = ClickableLabel(
|
||||||
tier_text,
|
tier_text,
|
||||||
icon=icon,
|
icon=icon,
|
||||||
parent=coverWidget,
|
parent=self.coverWidget,
|
||||||
icon_size=icon_size,
|
font_scale_factor=0.06
|
||||||
icon_space=icon_space,
|
|
||||||
font_scale_factor=font_scale_factor
|
|
||||||
)
|
)
|
||||||
self.protondbLabel.setStyleSheet(self.theme.get_protondb_badge_style(protondb_tier))
|
self.protondbLabel.setStyleSheet(self.theme.get_protondb_badge_style(protondb_tier))
|
||||||
self.protondbLabel.setFixedWidth(badge_width)
|
|
||||||
self.protondbLabel.setCardWidth(card_width)
|
self.protondbLabel.setCardWidth(card_width)
|
||||||
else:
|
else:
|
||||||
self.protondbLabel = ClickableLabel("", parent=coverWidget, icon_size=icon_size, icon_space=icon_space)
|
self.protondbLabel = ClickableLabel("", parent=self.coverWidget)
|
||||||
self.protondbLabel.setFixedWidth(badge_width)
|
|
||||||
self.protondbLabel.setVisible(False)
|
self.protondbLabel.setVisible(False)
|
||||||
|
|
||||||
# Steam бейдж
|
|
||||||
steam_icon = self.theme_manager.get_icon("steam")
|
steam_icon = self.theme_manager.get_icon("steam")
|
||||||
self.steamLabel = ClickableLabel(
|
self.steamLabel = ClickableLabel(
|
||||||
"Steam",
|
"Steam",
|
||||||
icon=steam_icon,
|
icon=steam_icon,
|
||||||
parent=coverWidget,
|
parent=self.coverWidget,
|
||||||
icon_size=icon_size,
|
font_scale_factor=0.06
|
||||||
icon_space=icon_space,
|
|
||||||
font_scale_factor=font_scale_factor
|
|
||||||
)
|
)
|
||||||
self.steamLabel.setStyleSheet(self.theme.STEAM_BADGE_STYLE)
|
self.steamLabel.setStyleSheet(self.theme.STEAM_BADGE_STYLE)
|
||||||
self.steamLabel.setFixedWidth(badge_width)
|
|
||||||
self.steamLabel.setCardWidth(card_width)
|
self.steamLabel.setCardWidth(card_width)
|
||||||
self.steamLabel.setVisible(self.steam_visible)
|
self.steamLabel.setVisible(self.steam_visible)
|
||||||
|
|
||||||
# Epic Games Store бейдж
|
|
||||||
egs_icon = self.theme_manager.get_icon("epic_games")
|
egs_icon = self.theme_manager.get_icon("epic_games")
|
||||||
self.egsLabel = ClickableLabel(
|
self.egsLabel = ClickableLabel(
|
||||||
"Epic Games",
|
"Epic Games",
|
||||||
icon=egs_icon,
|
icon=egs_icon,
|
||||||
parent=coverWidget,
|
parent=self.coverWidget,
|
||||||
icon_size=icon_size,
|
font_scale_factor=0.06,
|
||||||
icon_space=icon_space,
|
|
||||||
font_scale_factor=font_scale_factor,
|
|
||||||
change_cursor=False
|
change_cursor=False
|
||||||
)
|
)
|
||||||
self.egsLabel.setStyleSheet(self.theme.STEAM_BADGE_STYLE)
|
self.egsLabel.setStyleSheet(self.theme.STEAM_BADGE_STYLE)
|
||||||
self.egsLabel.setFixedWidth(badge_width)
|
|
||||||
self.egsLabel.setCardWidth(card_width)
|
self.egsLabel.setCardWidth(card_width)
|
||||||
self.egsLabel.setVisible(self.egs_visible)
|
self.egsLabel.setVisible(self.egs_visible)
|
||||||
|
|
||||||
# PortProton бейдж
|
|
||||||
portproton_icon = self.theme_manager.get_icon("portproton")
|
portproton_icon = self.theme_manager.get_icon("portproton")
|
||||||
self.portprotonLabel = ClickableLabel(
|
self.portprotonLabel = ClickableLabel(
|
||||||
"PortProton",
|
"PortProton",
|
||||||
icon=portproton_icon,
|
icon=portproton_icon,
|
||||||
parent=coverWidget,
|
parent=self.coverWidget,
|
||||||
icon_size=icon_size,
|
font_scale_factor=0.06
|
||||||
icon_space=icon_space,
|
|
||||||
font_scale_factor=font_scale_factor
|
|
||||||
)
|
)
|
||||||
self.portprotonLabel.setStyleSheet(self.theme.STEAM_BADGE_STYLE)
|
self.portprotonLabel.setStyleSheet(self.theme.STEAM_BADGE_STYLE)
|
||||||
self.portprotonLabel.setFixedWidth(badge_width)
|
|
||||||
self.portprotonLabel.setCardWidth(card_width)
|
self.portprotonLabel.setCardWidth(card_width)
|
||||||
self.portprotonLabel.setVisible(self.portproton_visible)
|
self.portprotonLabel.setVisible(self.portproton_visible)
|
||||||
self.portprotonLabel.clicked.connect(self.open_portproton_forum_topic)
|
self.portprotonLabel.clicked.connect(self.open_portproton_forum_topic)
|
||||||
|
|
||||||
# WeAntiCheatYet бейдж
|
|
||||||
anticheat_text = self.getAntiCheatText(anticheat_status)
|
anticheat_text = self.getAntiCheatText(anticheat_status)
|
||||||
if anticheat_text:
|
if anticheat_text:
|
||||||
icon_filename = self.getAntiCheatIconFilename(anticheat_status)
|
icon_filename = self.getAntiCheatIconFilename(anticheat_status)
|
||||||
@@ -211,40 +165,57 @@ class GameCard(QFrame):
|
|||||||
self.anticheatLabel = ClickableLabel(
|
self.anticheatLabel = ClickableLabel(
|
||||||
anticheat_text,
|
anticheat_text,
|
||||||
icon=icon,
|
icon=icon,
|
||||||
parent=coverWidget,
|
parent=self.coverWidget,
|
||||||
icon_size=icon_size,
|
font_scale_factor=0.06
|
||||||
icon_space=icon_space,
|
|
||||||
font_scale_factor=font_scale_factor
|
|
||||||
)
|
)
|
||||||
self.anticheatLabel.setStyleSheet(self.theme.get_anticheat_badge_style(anticheat_status))
|
self.anticheatLabel.setStyleSheet(self.theme.get_anticheat_badge_style(anticheat_status))
|
||||||
self.anticheatLabel.setFixedWidth(badge_width)
|
|
||||||
self.anticheatLabel.setCardWidth(card_width)
|
self.anticheatLabel.setCardWidth(card_width)
|
||||||
else:
|
else:
|
||||||
self.anticheatLabel = ClickableLabel("", parent=coverWidget, icon_size=icon_size, icon_space=icon_space)
|
self.anticheatLabel = ClickableLabel("", parent=self.coverWidget)
|
||||||
self.anticheatLabel.setFixedWidth(badge_width)
|
|
||||||
self.anticheatLabel.setVisible(False)
|
self.anticheatLabel.setVisible(False)
|
||||||
|
|
||||||
# Расположение бейджей
|
|
||||||
self._position_badges(card_width)
|
|
||||||
self.protondbLabel.clicked.connect(self.open_protondb_report)
|
self.protondbLabel.clicked.connect(self.open_protondb_report)
|
||||||
self.steamLabel.clicked.connect(self.open_steam_page)
|
self.steamLabel.clicked.connect(self.open_steam_page)
|
||||||
self.anticheatLabel.clicked.connect(self.open_weanticheatyet_page)
|
self.anticheatLabel.clicked.connect(self.open_weanticheatyet_page)
|
||||||
|
|
||||||
layout.addWidget(coverWidget)
|
self.layout_.addWidget(self.coverWidget)
|
||||||
|
|
||||||
# Название игры
|
self.nameLabel = QLabel(name)
|
||||||
nameLabel = QLabel(name)
|
self.nameLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
nameLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
self.nameLabel.setStyleSheet(self.theme.GAME_CARD_NAME_LABEL_STYLE)
|
||||||
nameLabel.setStyleSheet(self.theme.GAME_CARD_NAME_LABEL_STYLE)
|
self.layout_.addWidget(self.nameLabel)
|
||||||
layout.addWidget(nameLabel)
|
|
||||||
|
|
||||||
def _position_badges(self, card_width):
|
font_size = self.nameLabel.font().pointSizeF()
|
||||||
"""Позиционирует бейджи на основе ширины карточки."""
|
self.base_font_size = font_size if font_size > 0 else 10.0
|
||||||
right_margin = 8
|
|
||||||
badge_spacing = int(card_width * 0.02) # 2% от ширины карточки
|
self.update_scale()
|
||||||
top_y = 10
|
|
||||||
|
# Force initial layout update to ensure correct geometry
|
||||||
|
self.updateGeometry()
|
||||||
|
parent = self.parentWidget()
|
||||||
|
if parent:
|
||||||
|
layout = parent.layout()
|
||||||
|
if layout:
|
||||||
|
layout.invalidate()
|
||||||
|
parent.updateGeometry()
|
||||||
|
|
||||||
|
def on_cover_loaded(self, pixmap):
|
||||||
|
self.base_pixmap = pixmap
|
||||||
|
self.update_cover_pixmap()
|
||||||
|
|
||||||
|
def update_cover_pixmap(self):
|
||||||
|
if self.base_pixmap:
|
||||||
|
scaled_width = int(self.base_card_width * self._scale)
|
||||||
|
scaled_pixmap = self.base_pixmap.scaled(scaled_width, int(scaled_width * 1.2), Qt.AspectRatioMode.KeepAspectRatioByExpanding, Qt.TransformationMode.SmoothTransformation)
|
||||||
|
rounded_pixmap = round_corners(scaled_pixmap, int(15 * self._scale))
|
||||||
|
self.coverLabel.setPixmap(rounded_pixmap)
|
||||||
|
|
||||||
|
def _position_badges(self, current_width):
|
||||||
|
right_margin = int(8 * self._scale)
|
||||||
|
badge_spacing = int(current_width * 0.02)
|
||||||
|
top_y = int(10 * self._scale)
|
||||||
badge_y_positions = []
|
badge_y_positions = []
|
||||||
badge_width = int(card_width * 2/3)
|
badge_width = int(current_width * 2/3)
|
||||||
|
|
||||||
badges = [
|
badges = [
|
||||||
(self.steam_visible, self.steamLabel),
|
(self.steam_visible, self.steamLabel),
|
||||||
@@ -256,80 +227,99 @@ class GameCard(QFrame):
|
|||||||
|
|
||||||
for is_visible, badge in badges:
|
for is_visible, badge in badges:
|
||||||
if is_visible:
|
if is_visible:
|
||||||
badge_x = card_width - badge_width - right_margin
|
badge_x = current_width - badge_width - right_margin
|
||||||
badge_y = badge_y_positions[-1] + badge_spacing if badge_y_positions else top_y
|
badge_y = badge_y_positions[-1] + badge_spacing if badge_y_positions else top_y
|
||||||
badge.move(badge_x, badge_y)
|
badge.move(int(badge_x), int(badge_y))
|
||||||
badge_y_positions.append(badge_y + badge.height())
|
badge_y_positions.append(badge_y + badge.height())
|
||||||
|
|
||||||
# Поднимаем бейджи в правильном порядке (от нижнего к верхнему)
|
|
||||||
self.anticheatLabel.raise_()
|
self.anticheatLabel.raise_()
|
||||||
self.protondbLabel.raise_()
|
self.protondbLabel.raise_()
|
||||||
self.portprotonLabel.raise_()
|
self.portprotonLabel.raise_()
|
||||||
self.egsLabel.raise_()
|
self.egsLabel.raise_()
|
||||||
self.steamLabel.raise_()
|
self.steamLabel.raise_()
|
||||||
|
|
||||||
def update_card_size(self, new_width: int):
|
def update_scale(self):
|
||||||
"""Обновляет размер карточки, обложки и бейджей."""
|
scaled_width = int(self.base_card_width * self._scale)
|
||||||
self.card_width = new_width
|
scaled_height = int(self.base_card_width * 1.6 * self._scale)
|
||||||
extra_margin = 20
|
scaled_extra = int(self.base_extra_margin * self._scale)
|
||||||
self.setFixedSize(new_width + extra_margin, int(new_width * 1.6) + extra_margin)
|
self.setFixedSize(scaled_width + scaled_extra, scaled_height + scaled_extra)
|
||||||
|
self.layout_.setContentsMargins(scaled_extra // 2, scaled_extra // 2, scaled_extra // 2, scaled_extra // 2)
|
||||||
|
|
||||||
if self.coverLabel is None:
|
self.coverWidget.setFixedSize(scaled_width, int(scaled_width * 1.2))
|
||||||
return
|
self.coverLabel.setFixedSize(scaled_width, int(scaled_width * 1.2))
|
||||||
|
|
||||||
coverWidget = self.coverLabel.parentWidget()
|
self.update_cover_pixmap()
|
||||||
if coverWidget is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
coverWidget.setFixedSize(new_width, int(new_width * 1.2))
|
favorite_size = (int(self.theme.favoriteLabelSize[0] * self._scale), int(self.theme.favoriteLabelSize[1] * self._scale))
|
||||||
self.coverLabel.setFixedSize(new_width, int(new_width * 1.2))
|
self.favoriteLabel.setFixedSize(*favorite_size)
|
||||||
|
self.favoriteLabel.move(int(8 * self._scale), int(8 * self._scale))
|
||||||
|
|
||||||
label_ref = weakref.ref(self.coverLabel)
|
badge_width = int(scaled_width * 2/3)
|
||||||
def on_cover_loaded(pixmap):
|
icon_size = int(scaled_width * 0.06)
|
||||||
label = label_ref()
|
icon_space = int(scaled_width * 0.012)
|
||||||
if label:
|
|
||||||
scaled_pixmap = pixmap.scaled(new_width, int(new_width * 1.2), Qt.AspectRatioMode.KeepAspectRatioByExpanding, Qt.TransformationMode.SmoothTransformation)
|
|
||||||
rounded_pixmap = round_corners(scaled_pixmap, 15)
|
|
||||||
label.setPixmap(rounded_pixmap)
|
|
||||||
|
|
||||||
load_pixmap_async(self.cover_path or "", new_width, int(new_width * 1.2), on_cover_loaded)
|
|
||||||
|
|
||||||
# Обновляем размеры и шрифты бейджей
|
|
||||||
badge_width = int(new_width * 2/3)
|
|
||||||
icon_size = int(new_width * 0.06)
|
|
||||||
icon_space = int(new_width * 0.012)
|
|
||||||
for label in [self.steamLabel, self.egsLabel, self.portprotonLabel, self.protondbLabel, self.anticheatLabel]:
|
for label in [self.steamLabel, self.egsLabel, self.portprotonLabel, self.protondbLabel, self.anticheatLabel]:
|
||||||
if label is not None:
|
if label is not None:
|
||||||
label.setFixedWidth(badge_width)
|
label.setFixedWidth(badge_width)
|
||||||
label.setIconSize(icon_size, icon_space)
|
label.setIconSize(icon_size, icon_space)
|
||||||
label.setCardWidth(new_width) # Пересчитываем размер шрифта
|
label.setCardWidth(scaled_width)
|
||||||
|
|
||||||
# Перепозиционируем бейджи
|
self._position_badges(scaled_width)
|
||||||
self._position_badges(new_width)
|
|
||||||
|
|
||||||
|
if self.base_font_size is not None:
|
||||||
|
font = self.nameLabel.font()
|
||||||
|
new_font_size = self.base_font_size * self._scale
|
||||||
|
if new_font_size > 0:
|
||||||
|
font.setPointSizeF(new_font_size)
|
||||||
|
self.nameLabel.setFont(font)
|
||||||
|
|
||||||
|
self.shadow.setBlurRadius(int(20 * self._scale))
|
||||||
|
|
||||||
|
self.updateGeometry()
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
|
# Ensure parent layout is updated safely
|
||||||
|
parent = self.parentWidget()
|
||||||
|
if parent:
|
||||||
|
layout = parent.layout()
|
||||||
|
if layout:
|
||||||
|
layout.invalidate()
|
||||||
|
layout.activate()
|
||||||
|
layout.update()
|
||||||
|
parent.updateGeometry()
|
||||||
|
|
||||||
|
def update_card_size(self, new_width: int):
|
||||||
|
self.base_card_width = new_width
|
||||||
|
load_pixmap_async(self.cover_path or "", new_width, int(new_width * 1.2), self.on_cover_loaded)
|
||||||
|
self.update_scale()
|
||||||
|
|
||||||
def update_badge_visibility(self, display_filter: str):
|
def update_badge_visibility(self, display_filter: str):
|
||||||
"""Обновляет видимость бейджей на основе display_filter."""
|
|
||||||
self.display_filter = display_filter
|
self.display_filter = display_filter
|
||||||
self.steam_visible = (str(self.game_source).lower() == "steam" and display_filter in ("all", "favorites"))
|
self.steam_visible = (str(self.game_source).lower() == "steam" and self.display_filter in ("all", "favorites"))
|
||||||
self.egs_visible = (str(self.game_source).lower() == "epic" and display_filter in ("all", "favorites"))
|
self.egs_visible = (str(self.game_source).lower() == "epic" and self.display_filter in ("all", "favorites"))
|
||||||
self.portproton_visible = (str(self.game_source).lower() == "portproton" and display_filter in ("all", "favorites"))
|
self.portproton_visible = (str(self.game_source).lower() == "portproton" and self.display_filter in ("all", "favorites"))
|
||||||
protondb_visible = bool(self.getProtonDBText(self.protondb_tier))
|
protondb_visible = bool(self.getProtonDBText(self.protondb_tier))
|
||||||
anticheat_visible = bool(self.getAntiCheatText(self.anticheat_status))
|
anticheat_visible = bool(self.getAntiCheatText(self.anticheat_status))
|
||||||
|
|
||||||
# Обновляем видимость бейджей
|
|
||||||
self.steamLabel.setVisible(self.steam_visible)
|
self.steamLabel.setVisible(self.steam_visible)
|
||||||
self.egsLabel.setVisible(self.egs_visible)
|
self.egsLabel.setVisible(self.egs_visible)
|
||||||
self.portprotonLabel.setVisible(self.portproton_visible)
|
self.portprotonLabel.setVisible(self.portproton_visible)
|
||||||
self.protondbLabel.setVisible(protondb_visible)
|
self.protondbLabel.setVisible(protondb_visible)
|
||||||
self.anticheatLabel.setVisible(anticheat_visible)
|
self.anticheatLabel.setVisible(anticheat_visible)
|
||||||
|
|
||||||
# Перепозиционируем бейджи
|
scaled_width = int(self.base_card_width * self._scale)
|
||||||
self._position_badges(self.card_width)
|
self._position_badges(scaled_width)
|
||||||
|
|
||||||
|
# Update layout after visibility changes
|
||||||
|
self.updateGeometry()
|
||||||
|
parent = self.parentWidget()
|
||||||
|
if parent:
|
||||||
|
layout = parent.layout()
|
||||||
|
if layout:
|
||||||
|
layout.invalidate()
|
||||||
|
layout.update()
|
||||||
|
parent.updateGeometry()
|
||||||
|
|
||||||
def _show_context_menu(self, pos):
|
def _show_context_menu(self, pos):
|
||||||
"""Delegate context menu display to ContextMenuManager."""
|
|
||||||
if self.context_menu_manager:
|
if self.context_menu_manager:
|
||||||
self.context_menu_manager.show_context_menu(self, pos)
|
self.context_menu_manager.show_context_menu(self, pos)
|
||||||
|
|
||||||
@@ -387,7 +377,6 @@ class GameCard(QFrame):
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
def open_portproton_forum_topic(self):
|
def open_portproton_forum_topic(self):
|
||||||
"""Open the PortProton forum topic or search page for this game."""
|
|
||||||
result = self.portproton_api.get_forum_topic_slug(self.name)
|
result = self.portproton_api.get_forum_topic_slug(self.name)
|
||||||
base_url = "https://linux-gaming.ru/"
|
base_url = "https://linux-gaming.ru/"
|
||||||
if result.startswith("search?q="):
|
if result.startswith("search?q="):
|
||||||
@@ -447,8 +436,18 @@ class GameCard(QFrame):
|
|||||||
self.gradientAngleChanged.emit()
|
self.gradientAngleChanged.emit()
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
|
def getScale(self) -> float:
|
||||||
|
return self._scale
|
||||||
|
|
||||||
|
def setScale(self, value: float):
|
||||||
|
if self._scale != value:
|
||||||
|
self._scale = value
|
||||||
|
self.update_scale()
|
||||||
|
self.scaleChanged.emit()
|
||||||
|
|
||||||
borderWidth = Property(int, getBorderWidth, setBorderWidth, None, "", notify=cast(Callable[[], None], borderWidthChanged))
|
borderWidth = Property(int, getBorderWidth, setBorderWidth, None, "", notify=cast(Callable[[], None], borderWidthChanged))
|
||||||
gradientAngle = Property(float, getGradientAngle, setGradientAngle, None, "", notify=cast(Callable[[], None], gradientAngleChanged))
|
gradientAngle = Property(float, getGradientAngle, setGradientAngle, None, "", notify=cast(Callable[[], None], gradientAngleChanged))
|
||||||
|
scale = Property(float, getScale, setScale, None, "", notify=cast(Callable[[], None], scaleChanged))
|
||||||
|
|
||||||
def paintEvent(self, event):
|
def paintEvent(self, event):
|
||||||
super().paintEvent(event)
|
super().paintEvent(event)
|
||||||
@@ -487,6 +486,7 @@ class GameCard(QFrame):
|
|||||||
)
|
)
|
||||||
super().mousePressEvent(event)
|
super().mousePressEvent(event)
|
||||||
|
|
||||||
|
|
||||||
def keyPressEvent(self, event):
|
def keyPressEvent(self, event):
|
||||||
if event.key() in (Qt.Key.Key_Return, Qt.Key.Key_Enter):
|
if event.key() in (Qt.Key.Key_Return, Qt.Key.Key_Enter):
|
||||||
self.select_callback(
|
self.select_callback(
|
||||||
|
@@ -658,8 +658,9 @@ class InputManager(QObject):
|
|||||||
# Library tab navigation (index 0)
|
# Library tab navigation (index 0)
|
||||||
if self._parent.stackedWidget.currentIndex() == 0 and code in (ecodes.ABS_HAT0X, ecodes.ABS_HAT0Y):
|
if self._parent.stackedWidget.currentIndex() == 0 and code in (ecodes.ABS_HAT0X, ecodes.ABS_HAT0Y):
|
||||||
focused = QApplication.focusWidget()
|
focused = QApplication.focusWidget()
|
||||||
game_cards = self._parent.gamesListWidget.findChildren(GameCard)
|
game_cards = [card for card in self._parent.gamesListWidget.findChildren(GameCard) if card.isVisible()]
|
||||||
if not game_cards:
|
if not game_cards:
|
||||||
|
logger.debug("No visible GameCards found")
|
||||||
return
|
return
|
||||||
|
|
||||||
scroll_area = self._parent.gamesListWidget.parentWidget()
|
scroll_area = self._parent.gamesListWidget.parentWidget()
|
||||||
@@ -668,27 +669,48 @@ class InputManager(QObject):
|
|||||||
|
|
||||||
# If no focused widget or not a GameCard, focus the first card
|
# If no focused widget or not a GameCard, focus the first card
|
||||||
if not isinstance(focused, GameCard) or focused not in game_cards:
|
if not isinstance(focused, GameCard) or focused not in game_cards:
|
||||||
game_cards[0].setFocus()
|
game_cards[0].setFocus(Qt.FocusReason.OtherFocusReason)
|
||||||
if scroll_area:
|
if scroll_area:
|
||||||
scroll_area.ensureWidgetVisible(game_cards[0], 50, 50)
|
scroll_area.ensureWidgetVisible(game_cards[0], 50, 50)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Group cards by rows based on y-coordinate
|
# Group cards by rows based on normalized y-coordinate
|
||||||
rows = {}
|
rows = {}
|
||||||
|
tolerance = 50 # Increased tolerance for scaling effects
|
||||||
for card in game_cards:
|
for card in game_cards:
|
||||||
y = card.pos().y()
|
y = card.pos().y() / card.getScale() # Normalize y-coordinate
|
||||||
if y not in rows:
|
found = False
|
||||||
rows[y] = []
|
for row_y in rows:
|
||||||
rows[y].append(card)
|
if abs(y - row_y) < tolerance:
|
||||||
# Sort cards in each row by x-coordinate
|
rows[row_y].append(card)
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
if not found:
|
||||||
|
rows[y] = [card]
|
||||||
|
# Sort cards in each row by normalized x-coordinate
|
||||||
for y in rows:
|
for y in rows:
|
||||||
rows[y].sort(key=lambda c: c.pos().x())
|
rows[y].sort(key=lambda c: c.pos().x() / c.getScale())
|
||||||
# Sort rows by y-coordinate
|
# Sort rows by y-coordinate
|
||||||
sorted_rows = sorted(rows.items(), key=lambda x: x[0])
|
sorted_rows = sorted(rows.items(), key=lambda x: x[0])
|
||||||
|
|
||||||
# Find current row and column
|
# Find current row with normalized y-coordinate
|
||||||
current_y = focused.pos().y()
|
current_y = focused.pos().y() / focused.getScale()
|
||||||
current_row_idx = next(i for i, (y, _) in enumerate(sorted_rows) if y == current_y)
|
current_row_idx = None
|
||||||
|
min_diff = float('inf')
|
||||||
|
for i, (y, _) in enumerate(sorted_rows):
|
||||||
|
diff = abs(y - current_y)
|
||||||
|
if diff < tolerance and diff < min_diff:
|
||||||
|
current_row_idx = i
|
||||||
|
min_diff = diff
|
||||||
|
|
||||||
|
if current_row_idx is None:
|
||||||
|
logger.warning("No row found for current_y: %s, falling back to closest row", current_y)
|
||||||
|
if sorted_rows:
|
||||||
|
current_row_idx = min(range(len(sorted_rows)), key=lambda i: abs(sorted_rows[i][0] - current_y))
|
||||||
|
else:
|
||||||
|
logger.error("No rows available")
|
||||||
|
return
|
||||||
|
|
||||||
current_row = sorted_rows[current_row_idx][1]
|
current_row = sorted_rows[current_row_idx][1]
|
||||||
current_col_idx = current_row.index(focused)
|
current_col_idx = current_row.index(focused)
|
||||||
|
|
||||||
@@ -697,7 +719,7 @@ class InputManager(QObject):
|
|||||||
next_col_idx = current_col_idx - 1
|
next_col_idx = current_col_idx - 1
|
||||||
if next_col_idx >= 0:
|
if next_col_idx >= 0:
|
||||||
next_card = current_row[next_col_idx]
|
next_card = current_row[next_col_idx]
|
||||||
next_card.setFocus()
|
next_card.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||||
if scroll_area:
|
if scroll_area:
|
||||||
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
||||||
else:
|
else:
|
||||||
@@ -706,14 +728,14 @@ class InputManager(QObject):
|
|||||||
prev_row = sorted_rows[current_row_idx - 1][1]
|
prev_row = sorted_rows[current_row_idx - 1][1]
|
||||||
next_card = prev_row[-1] if prev_row else None
|
next_card = prev_row[-1] if prev_row else None
|
||||||
if next_card:
|
if next_card:
|
||||||
next_card.setFocus()
|
next_card.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||||
if scroll_area:
|
if scroll_area:
|
||||||
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
||||||
elif value > 0: # Right
|
elif value > 0: # Right
|
||||||
next_col_idx = current_col_idx + 1
|
next_col_idx = current_col_idx + 1
|
||||||
if next_col_idx < len(current_row):
|
if next_col_idx < len(current_row):
|
||||||
next_card = current_row[next_col_idx]
|
next_card = current_row[next_col_idx]
|
||||||
next_card.setFocus()
|
next_card.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||||
if scroll_area:
|
if scroll_area:
|
||||||
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
||||||
else:
|
else:
|
||||||
@@ -722,7 +744,7 @@ class InputManager(QObject):
|
|||||||
next_row = sorted_rows[current_row_idx + 1][1]
|
next_row = sorted_rows[current_row_idx + 1][1]
|
||||||
next_card = next_row[0] if next_row else None
|
next_card = next_row[0] if next_row else None
|
||||||
if next_card:
|
if next_card:
|
||||||
next_card.setFocus()
|
next_card.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||||
if scroll_area:
|
if scroll_area:
|
||||||
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
||||||
elif code == ecodes.ABS_HAT0Y and value != 0: # Up/Down
|
elif code == ecodes.ABS_HAT0Y and value != 0: # Up/Down
|
||||||
@@ -730,30 +752,30 @@ class InputManager(QObject):
|
|||||||
next_row_idx = current_row_idx + 1
|
next_row_idx = current_row_idx + 1
|
||||||
if next_row_idx < len(sorted_rows):
|
if next_row_idx < len(sorted_rows):
|
||||||
next_row = sorted_rows[next_row_idx][1]
|
next_row = sorted_rows[next_row_idx][1]
|
||||||
# Find card in same column or closest
|
# Find card in same column or closest based on normalized x
|
||||||
target_x = focused.pos().x()
|
target_x = focused.pos().x() / focused.getScale()
|
||||||
next_card = min(
|
next_card = min(
|
||||||
next_row,
|
next_row,
|
||||||
key=lambda c: abs(c.pos().x() - target_x),
|
key=lambda c: abs(c.pos().x() / c.getScale() - target_x),
|
||||||
default=None
|
default=None
|
||||||
)
|
)
|
||||||
if next_card:
|
if next_card:
|
||||||
next_card.setFocus()
|
next_card.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||||
if scroll_area:
|
if scroll_area:
|
||||||
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
||||||
elif value < 0: # Up
|
elif value < 0: # Up
|
||||||
next_row_idx = current_row_idx - 1
|
next_row_idx = current_row_idx - 1
|
||||||
if next_row_idx >= 0:
|
if next_row_idx >= 0:
|
||||||
next_row = sorted_rows[next_row_idx][1]
|
next_row = sorted_rows[next_row_idx][1]
|
||||||
# Find card in same column or closest
|
# Find card in same column or closest based on normalized x
|
||||||
target_x = focused.pos().x()
|
target_x = focused.pos().x() / focused.getScale()
|
||||||
next_card = min(
|
next_card = min(
|
||||||
next_row,
|
next_row,
|
||||||
key=lambda c: abs(c.pos().x() - target_x),
|
key=lambda c: abs(c.pos().x() / c.getScale() - target_x),
|
||||||
default=None
|
default=None
|
||||||
)
|
)
|
||||||
if next_card:
|
if next_card:
|
||||||
next_card.setFocus()
|
next_card.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||||
if scroll_area:
|
if scroll_area:
|
||||||
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
||||||
elif current_row_idx == 0:
|
elif current_row_idx == 0:
|
||||||
@@ -768,7 +790,7 @@ class InputManager(QObject):
|
|||||||
focusables = page.findChildren(QWidget, options=Qt.FindChildOption.FindChildrenRecursively)
|
focusables = page.findChildren(QWidget, options=Qt.FindChildOption.FindChildrenRecursively)
|
||||||
focusables = [w for w in focusables if w.focusPolicy() & Qt.FocusPolicy.StrongFocus]
|
focusables = [w for w in focusables if w.focusPolicy() & Qt.FocusPolicy.StrongFocus]
|
||||||
if focusables:
|
if focusables:
|
||||||
focusables[0].setFocus()
|
focusables[0].setFocus(Qt.FocusReason.OtherFocusReason)
|
||||||
return
|
return
|
||||||
elif focused:
|
elif focused:
|
||||||
focused.focusNextChild()
|
focused.focusNextChild()
|
||||||
|
@@ -29,69 +29,105 @@ color_h = "transparent"
|
|||||||
GAME_CARD_ANIMATION = {
|
GAME_CARD_ANIMATION = {
|
||||||
# Тип анимации при входе и выходе на детальную страницу
|
# Тип анимации при входе и выходе на детальную страницу
|
||||||
# Возможные значения: "fade", "slide_left", "slide_right", "slide_up", "slide_down", "bounce"
|
# Возможные значения: "fade", "slide_left", "slide_right", "slide_up", "slide_down", "bounce"
|
||||||
|
# Определяет, как детальная страница появляется и исчезает
|
||||||
"detail_page_animation_type": "fade",
|
"detail_page_animation_type": "fade",
|
||||||
|
|
||||||
# Ширина обводки карточки в состоянии покоя (без наведения или фокуса).
|
# Ширина обводки карточки в состоянии покоя (без наведения или фокуса)
|
||||||
# Влияет на толщину рамки вокруг карточки, когда она не выделена.
|
# Влияет на толщину рамки вокруг карточки, когда она не выделена
|
||||||
# Значение в пикселях.
|
# Значение в пикселях
|
||||||
"default_border_width": 2,
|
"default_border_width": 2,
|
||||||
|
|
||||||
# Ширина обводки при наведении курсора.
|
# Ширина обводки при наведении курсора
|
||||||
# Увеличивает толщину рамки, когда курсор находится над карточкой.
|
# Увеличивает толщину рамки, когда курсор находится над карточкой
|
||||||
# Значение в пикселях.
|
# Значение в пикселях
|
||||||
"hover_border_width": 8,
|
"hover_border_width": 8,
|
||||||
|
|
||||||
# Ширина обводки при фокусе (например, при выборе с клавиатуры).
|
# Ширина обводки при фокусе (например, при выборе с клавиатуры)
|
||||||
# Увеличивает толщину рамки, когда карточка в фокусе.
|
# Увеличивает толщину рамки, когда карточка в фокусе
|
||||||
# Значение в пикселях.
|
# Значение в пикселях
|
||||||
"focus_border_width": 12,
|
"focus_border_width": 12,
|
||||||
|
|
||||||
# Минимальная ширина обводки во время пульсирующей анимации.
|
# Минимальная ширина обводки во время пульсирующей анимации
|
||||||
# Определяет минимальную толщину рамки при пульсации (анимация "дыхания").
|
# Определяет минимальную толщину рамки при пульсации (анимация "дыхания")
|
||||||
# Значение в пикселях.
|
# Значение в пикселях
|
||||||
"pulse_min_border_width": 8,
|
"pulse_min_border_width": 8,
|
||||||
|
|
||||||
# Максимальная ширина обводки во время пульсирующей анимации.
|
# Максимальная ширина обводки во время пульсирующей анимации
|
||||||
# Определяет максимальную толщину рамки при пульсации.
|
# Определяет максимальную толщину рамки при пульсации
|
||||||
# Значение в пикселях.
|
# Значение в пикселях
|
||||||
"pulse_max_border_width": 10,
|
"pulse_max_border_width": 10,
|
||||||
|
|
||||||
# Длительность анимации изменения толщины обводки (например, при наведении или фокусе).
|
# Длительность анимации изменения толщины обводки (например, при наведении или фокусе)
|
||||||
# Влияет на скорость перехода от одной ширины обводки к другой.
|
# Влияет на скорость перехода от одной ширины обводки к другой
|
||||||
# Значение в миллисекундах.
|
# Значение в миллисекундах
|
||||||
"thickness_anim_duration": 300,
|
"thickness_anim_duration": 300,
|
||||||
|
|
||||||
# Длительность одного цикла пульсирующей анимации.
|
# Длительность одного цикла пульсирующей анимации
|
||||||
# Определяет, как быстро рамка "пульсирует" между min и max значениями.
|
# Определяет, как быстро рамка "пульсирует" между min и max значениями
|
||||||
# Значение в миллисекундах.
|
# Значение в миллисекундах
|
||||||
"pulse_anim_duration": 800,
|
"pulse_anim_duration": 800,
|
||||||
|
|
||||||
# Длительность анимации вращения градиента.
|
# Длительность анимации вращения градиента
|
||||||
# Влияет на скорость, с которой градиентная обводка вращается вокруг карточки.
|
# Влияет на скорость, с которой градиентная обводка вращается вокруг карточки
|
||||||
# Значение в миллисекундах.
|
# Значение в миллисекундах
|
||||||
"gradient_anim_duration": 3000,
|
"gradient_anim_duration": 3000,
|
||||||
|
|
||||||
# Начальный угол градиента (в градусах).
|
# Начальный угол градиента (в градусах)
|
||||||
# Определяет начальную точку вращения градиента при старте анимации.
|
# Определяет начальную точку вращения градиента при старте анимации
|
||||||
"gradient_start_angle": 360,
|
"gradient_start_angle": 360,
|
||||||
|
|
||||||
# Конечный угол градиента (в градусах).
|
# Конечный угол градиента (в градусах)
|
||||||
# Определяет конечную точку вращения градиента.
|
# Определяет конечную точку вращения градиента
|
||||||
# Значение 0 означает полный поворот на 360 градусов.
|
# Значение 0 означает полный поворот на 360 градусов
|
||||||
"gradient_end_angle": 0,
|
"gradient_end_angle": 0,
|
||||||
|
|
||||||
# Тип кривой сглаживания для анимации увеличения обводки (при наведении/фокусе).
|
# Тип анимации для карточки при наведении или фокусе
|
||||||
# Влияет на "чувство" анимации (например, плавное ускорение или замедление).
|
# Возможные значения: "gradient", "scale"
|
||||||
# Возможные значения: строки, соответствующие QEasingCurve.Type (например, "OutBack", "InOutQuad").
|
# scale крайне нестабилен и требует доработки (используйте на свой страх и риск)
|
||||||
|
# "gradient" включает вращающийся градиент для обводки, "scale" увеличивает размер карточки
|
||||||
|
"card_animation_type": "gradient",
|
||||||
|
|
||||||
|
# Масштаб карточки в состоянии покоя
|
||||||
|
# Определяет базовый размер карточки (1.0 = 100% от исходного размера)
|
||||||
|
# Значение в долях (например, 1.0 для нормального размера)
|
||||||
|
"default_scale": 1.0,
|
||||||
|
|
||||||
|
# Масштаб карточки при наведении курсора
|
||||||
|
# Увеличивает размер карточки при наведении
|
||||||
|
# Значение в долях (например, 1.1 = 110% от исходного размера)
|
||||||
|
"hover_scale": 1.1,
|
||||||
|
|
||||||
|
# Масштаб карточки при фокусе (например, при выборе с клавиатуры)
|
||||||
|
# Увеличивает размер карточки при фокусе
|
||||||
|
# Значение в долях (например, 1.05 = 105% от исходного размера)
|
||||||
|
"focus_scale": 1.05,
|
||||||
|
|
||||||
|
# Длительность анимации масштабирования
|
||||||
|
# Влияет на скорость изменения размера карточки при наведении или фокусе
|
||||||
|
# Значение в миллисекундах
|
||||||
|
"scale_anim_duration": 200,
|
||||||
|
|
||||||
|
# Тип кривой сглаживания для анимации увеличения обводки (при наведении/фокусе)
|
||||||
|
# Влияет на "чувство" анимации (например, плавное ускорение или замедление)
|
||||||
|
# Возможные значения: строки, соответствующие QEasingCurve.Type (например, "OutBack", "InOutQuad")
|
||||||
"thickness_easing_curve": "OutBack",
|
"thickness_easing_curve": "OutBack",
|
||||||
|
|
||||||
# Тип кривой сглаживания для анимации уменьшения обводки (при уходе курсора/потере фокуса).
|
# Тип кривой сглаживания для анимации уменьшения обводки (при уходе курсора/потере фокуса)
|
||||||
# Влияет на "чувство" возврата к исходной ширине обводки.
|
# Влияет на "чувство" возврата к исходной ширине обводки
|
||||||
"thickness_easing_curve_out": "InBack",
|
"thickness_easing_curve_out": "InBack",
|
||||||
|
|
||||||
# Цвета градиента для анимированной обводки.
|
# Тип кривой сглаживания для анимации увеличения масштаба (при наведении/фокусе)
|
||||||
# Список словарей, где каждый словарь задает позицию (0.0–1.0) и цвет в формате hex.
|
# Влияет на "чувство" анимации масштабирования (например, с эффектом "отскока")
|
||||||
# Влияет на внешний вид обводки при наведении или фокусе.
|
# Возможные значения: строки, соответствующие QEasingCurve.Type
|
||||||
|
"scale_easing_curve": "OutBack",
|
||||||
|
|
||||||
|
# Тип кривой сглаживания для анимации уменьшения масштаба (при уходе курсора/потере фокуса)
|
||||||
|
# Влияет на "чувство" возврата к исходному масштабу
|
||||||
|
"scale_easing_curve_out": "InBack",
|
||||||
|
|
||||||
|
# Цвета градиента для анимированной обводки
|
||||||
|
# Список словарей, где каждый словарь задает позицию (0.0–1.0) и цвет в формате hex
|
||||||
|
# Влияет на внешний вид обводки при наведении или фокусе, если card_animation_type="gradient"
|
||||||
"gradient_colors": [
|
"gradient_colors": [
|
||||||
{"position": 0, "color": "#00fff5"}, # Начальный цвет (циан)
|
{"position": 0, "color": "#00fff5"}, # Начальный цвет (циан)
|
||||||
{"position": 0.33, "color": "#FF5733"}, # Цвет на 33% (оранжевый)
|
{"position": 0.33, "color": "#FF5733"}, # Цвет на 33% (оранжевый)
|
||||||
@@ -100,29 +136,43 @@ GAME_CARD_ANIMATION = {
|
|||||||
],
|
],
|
||||||
|
|
||||||
# Длительность анимации fade при входе на детальную страницу
|
# Длительность анимации fade при входе на детальную страницу
|
||||||
|
# Влияет на скорость появления страницы при fade-анимации
|
||||||
|
# Значение в миллисекундах
|
||||||
"detail_page_fade_duration": 350,
|
"detail_page_fade_duration": 350,
|
||||||
|
|
||||||
# Длительность анимации slide при входе на детальную страницу
|
# Длительность анимации slide при входе на детальную страницу
|
||||||
|
# Влияет на скорость скольжения страницы при slide-анимации
|
||||||
|
# Значение в миллисекундах
|
||||||
"detail_page_slide_duration": 500,
|
"detail_page_slide_duration": 500,
|
||||||
|
|
||||||
# Длительность анимации bounce при входе на детальную страницу
|
# Длительность анимации bounce при входе на детальную страницу
|
||||||
|
# Влияет на скорость "прыжка" страницы при bounce-анимации
|
||||||
|
# Значение в миллисекундах
|
||||||
"detail_page_bounce_duration": 400,
|
"detail_page_bounce_duration": 400,
|
||||||
|
|
||||||
# Длительность анимации fade при выходе из детальной страницы
|
# Длительность анимации fade при выходе из детальной страницы
|
||||||
|
# Влияет на скорость исчезновения страницы при fade-анимации
|
||||||
|
# Значение в миллисекундах
|
||||||
"detail_page_fade_duration_exit": 350,
|
"detail_page_fade_duration_exit": 350,
|
||||||
|
|
||||||
# Длительность анимации slide при выходе из детальной страницы
|
# Длительность анимации slide при выходе из детальной страницы
|
||||||
|
# Влияет на скорость скольжения страницы при slide-анимации
|
||||||
|
# Значение в миллисекундах
|
||||||
"detail_page_slide_duration_exit": 500,
|
"detail_page_slide_duration_exit": 500,
|
||||||
|
|
||||||
# Длительность анимации bounce при выходе из детальной страницы
|
# Длительность анимации bounce при выходе из детальной страницы
|
||||||
|
# Влияет на скорость "сжатия" страницы при bounce-анимации
|
||||||
|
# Значение в миллисекундах
|
||||||
"detail_page_bounce_duration_exit": 400,
|
"detail_page_bounce_duration_exit": 400,
|
||||||
|
|
||||||
# Тип кривой сглаживания для анимации при входе на детальную страницу
|
# Тип кривой сглаживания для анимации при входе на детальную страницу
|
||||||
# Применяется к slide и bounce анимациям
|
# Применяется к slide и bounce анимациям, влияет на "чувство" движения
|
||||||
|
# Возможные значения: строки, соответствующие QEasingCurve.Type
|
||||||
"detail_page_easing_curve": "OutCubic",
|
"detail_page_easing_curve": "OutCubic",
|
||||||
|
|
||||||
# Тип кривой сглаживания для анимации при выходе из детальной страницы
|
# Тип кривой сглаживания для анимации при выходе из детальной страницы
|
||||||
# Применяется к slide и bounce анимациям
|
# Применяется к slide и bounce анимациям, влияет на "чувство" движения
|
||||||
|
# Возможные значения: строки, соответствующие QEasingCurve.Type
|
||||||
"detail_page_easing_curve_exit": "InCubic"
|
"detail_page_easing_curve_exit": "InCubic"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user