From 2753e53a4dc4eff8b12b99cff4fb73eb87ad16d2 Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Thu, 7 Aug 2025 10:29:13 +0500 Subject: [PATCH] refactor: move animations to separate module Signed-off-by: Boris Yumankulov --- portprotonqt/animations.py | 250 ++++++++++++++++++++++++++++++++++++ portprotonqt/game_card.py | 136 ++------------------ portprotonqt/main_window.py | 98 +------------- 3 files changed, 266 insertions(+), 218 deletions(-) create mode 100644 portprotonqt/animations.py diff --git a/portprotonqt/animations.py b/portprotonqt/animations.py new file mode 100644 index 0000000..9f84a93 --- /dev/null +++ b/portprotonqt/animations.py @@ -0,0 +1,250 @@ +from PySide6.QtCore import QPropertyAnimation, QByteArray, QEasingCurve, QAbstractAnimation, QParallelAnimationGroup, QRect, Qt +from PySide6.QtGui import QPainter, QPen, QColor, QConicalGradient, QBrush +from PySide6.QtWidgets import QWidget, QGraphicsOpacityEffect +from collections.abc import Callable +import portprotonqt.themes.standart.styles as default_styles + +class GameCardAnimations: + def __init__(self, game_card, theme=None): + self.game_card = game_card + self.theme = theme if theme is not None else default_styles + self.thickness_anim: QPropertyAnimation | None = None + self.gradient_anim: QPropertyAnimation | None = None + self.pulse_anim: QPropertyAnimation | None = None + self._isPulseAnimationConnected = False + + def setup_animations(self): + """Initialize animation properties.""" + self.thickness_anim = QPropertyAnimation(self.game_card, QByteArray(b"borderWidth")) + self.thickness_anim.setDuration(self.theme.GAME_CARD_ANIMATION["thickness_anim_duration"]) + + def start_pulse_animation(self): + """Start pulse animation for border width when hovered or focused.""" + if not (self.game_card._hovered or self.game_card._focused): + return + if self.pulse_anim: + self.pulse_anim.stop() + self.pulse_anim = QPropertyAnimation(self.game_card, QByteArray(b"borderWidth")) + self.pulse_anim.setDuration(self.theme.GAME_CARD_ANIMATION["pulse_anim_duration"]) + self.pulse_anim.setLoopCount(0) + self.pulse_anim.setKeyValueAt(0, self.theme.GAME_CARD_ANIMATION["pulse_min_border_width"]) + self.pulse_anim.setKeyValueAt(0.5, self.theme.GAME_CARD_ANIMATION["pulse_max_border_width"]) + self.pulse_anim.setKeyValueAt(1, self.theme.GAME_CARD_ANIMATION["pulse_min_border_width"]) + self.pulse_anim.start() + + def handle_enter_event(self): + """Handle mouse enter event animations.""" + self.game_card._hovered = True + self.game_card.hoverChanged.emit(self.game_card.name, True) + self.game_card.setFocus(Qt.FocusReason.MouseFocusReason) + + # Ensure thickness_anim is initialized + if not self.thickness_anim: + self.setup_animations() + + if self.thickness_anim: + self.thickness_anim.stop() + if self._isPulseAnimationConnected: + self.thickness_anim.finished.disconnect(self.start_pulse_animation) + self._isPulseAnimationConnected = False + self.thickness_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION["thickness_easing_curve"]])) + self.thickness_anim.setStartValue(self.game_card._borderWidth) + self.thickness_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["hover_border_width"]) + self.thickness_anim.finished.connect(self.start_pulse_animation) + self._isPulseAnimationConnected = True + self.thickness_anim.start() + + if self.gradient_anim: + self.gradient_anim.stop() + self.gradient_anim = QPropertyAnimation(self.game_card, QByteArray(b"gradientAngle")) + self.gradient_anim.setDuration(self.theme.GAME_CARD_ANIMATION["gradient_anim_duration"]) + self.gradient_anim.setStartValue(self.theme.GAME_CARD_ANIMATION["gradient_start_angle"]) + self.gradient_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["gradient_end_angle"]) + self.gradient_anim.setLoopCount(-1) + self.gradient_anim.start() + + def handle_leave_event(self): + """Handle mouse leave event animations.""" + self.game_card._hovered = False + self.game_card.hoverChanged.emit(self.game_card.name, False) + if not self.game_card._focused: + if self.gradient_anim: + self.gradient_anim.stop() + self.gradient_anim = None + if self.pulse_anim: + self.pulse_anim.stop() + self.pulse_anim = None + if self.thickness_anim: + self.thickness_anim.stop() + if self._isPulseAnimationConnected: + self.thickness_anim.finished.disconnect(self.start_pulse_animation) + self._isPulseAnimationConnected = False + self.thickness_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION["thickness_easing_curve_out"]])) + self.thickness_anim.setStartValue(self.game_card._borderWidth) + self.thickness_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["default_border_width"]) + self.thickness_anim.start() + + def handle_focus_in_event(self): + """Handle focus in event animations.""" + if not self.game_card._hovered: + self.game_card._focused = True + self.game_card.focusChanged.emit(self.game_card.name, True) + + # Ensure thickness_anim is initialized + if not self.thickness_anim: + self.setup_animations() + + if self.thickness_anim: + self.thickness_anim.stop() + if self._isPulseAnimationConnected: + self.thickness_anim.finished.disconnect(self.start_pulse_animation) + self._isPulseAnimationConnected = False + self.thickness_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION["thickness_easing_curve"]])) + self.thickness_anim.setStartValue(self.game_card._borderWidth) + self.thickness_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["focus_border_width"]) + self.thickness_anim.finished.connect(self.start_pulse_animation) + self._isPulseAnimationConnected = True + self.thickness_anim.start() + + if self.gradient_anim: + self.gradient_anim.stop() + self.gradient_anim = QPropertyAnimation(self.game_card, QByteArray(b"gradientAngle")) + self.gradient_anim.setDuration(self.theme.GAME_CARD_ANIMATION["gradient_anim_duration"]) + self.gradient_anim.setStartValue(self.theme.GAME_CARD_ANIMATION["gradient_start_angle"]) + self.gradient_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["gradient_end_angle"]) + self.gradient_anim.setLoopCount(-1) + self.gradient_anim.start() + + def handle_focus_out_event(self): + """Handle focus out event animations.""" + self.game_card._focused = False + self.game_card.focusChanged.emit(self.game_card.name, False) + if not self.game_card._hovered: + if self.gradient_anim: + self.gradient_anim.stop() + self.gradient_anim = None + if self.pulse_anim: + self.pulse_anim.stop() + self.pulse_anim = None + if self.thickness_anim: + self.thickness_anim.stop() + if self._isPulseAnimationConnected: + self.thickness_anim.finished.disconnect(self.start_pulse_animation) + self._isPulseAnimationConnected = False + self.thickness_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION["thickness_easing_curve_out"]])) + self.thickness_anim.setStartValue(self.game_card._borderWidth) + self.thickness_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["default_border_width"]) + self.thickness_anim.start() + + def paint_border(self, painter: QPainter): + """Paint the animated border for the GameCard.""" + painter.setRenderHint(QPainter.RenderHint.Antialiasing) + pen = QPen() + pen.setWidth(self.game_card._borderWidth) + if self.game_card._hovered or self.game_card._focused: + center = self.game_card.rect().center() + gradient = QConicalGradient(center, self.game_card._gradientAngle) + for stop in self.theme.GAME_CARD_ANIMATION["gradient_colors"]: + gradient.setColorAt(stop["position"], QColor(stop["color"])) + pen.setBrush(QBrush(gradient)) + else: + pen.setColor(QColor(0, 0, 0, 0)) + painter.setPen(pen) + radius = 18 + bw = round(self.game_card._borderWidth / 2) + rect = self.game_card.rect().adjusted(bw, bw, -bw, -bw) + painter.drawRoundedRect(rect, radius, radius) + +class DetailPageAnimations: + def __init__(self, main_window, theme=None): + self.main_window = main_window + self.theme = theme if theme is not None else default_styles + self.animations = main_window._animations if hasattr(main_window, '_animations') else {} + + def animate_detail_page(self, detail_page: QWidget, load_image_and_restore_effect: Callable, cleanup_animation: Callable): + """Animate the detail page based on theme settings.""" + shadow = detail_page.graphicsEffect() + animation_type = self.theme.GAME_CARD_ANIMATION.get("detail_page_animation_type", "fade") + duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_fade_duration", 350) + + if animation_type == "fade": + opacity_effect = QGraphicsOpacityEffect(detail_page) + detail_page.setGraphicsEffect(opacity_effect) + animation = QPropertyAnimation(opacity_effect, QByteArray(b"opacity")) + animation.setDuration(duration) + animation.setStartValue(0) + animation.setEndValue(1) + animation.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped) + self.animations[detail_page] = animation + animation.finished.connect(lambda: detail_page.setGraphicsEffect(shadow) if shadow is not None else detail_page.setGraphicsEffect(None)) # type: ignore + animation.finished.connect(load_image_and_restore_effect) + elif animation_type == "slide_left": + duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_slide_duration", 500) + detail_page.move(self.main_window.width(), 0) + animation = QPropertyAnimation(detail_page, QByteArray(b"pos")) + animation.setDuration(duration) + animation.setStartValue(detail_page.pos()) + animation.setEndValue(self.main_window.stackedWidget.rect().topLeft()) + animation.setEasingCurve(QEasingCurve.Type.OutCubic) + animation.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped) + self.animations[detail_page] = animation + animation.finished.connect(cleanup_animation) + animation.finished.connect(load_image_and_restore_effect) + elif animation_type == "slide_right": + duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_slide_duration", 500) + detail_page.move(-self.main_window.width(), 0) + animation = QPropertyAnimation(detail_page, QByteArray(b"pos")) + animation.setDuration(duration) + animation.setStartValue(detail_page.pos()) + animation.setEndValue(self.main_window.stackedWidget.rect().topLeft()) + animation.setEasingCurve(QEasingCurve.Type.OutCubic) + animation.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped) + self.animations[detail_page] = animation + animation.finished.connect(cleanup_animation) + animation.finished.connect(load_image_and_restore_effect) + elif animation_type == "slide_up": + duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_slide_duration", 500) + detail_page.move(0, self.main_window.height()) + animation = QPropertyAnimation(detail_page, QByteArray(b"pos")) + animation.setDuration(duration) + animation.setStartValue(detail_page.pos()) + animation.setEndValue(self.main_window.stackedWidget.rect().topLeft()) + animation.setEasingCurve(QEasingCurve.Type.OutCubic) + animation.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped) + self.animations[detail_page] = animation + animation.finished.connect(cleanup_animation) + animation.finished.connect(load_image_and_restore_effect) + elif animation_type == "slide_down": + duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_slide_duration", 500) + detail_page.move(0, -self.main_window.height()) + animation = QPropertyAnimation(detail_page, QByteArray(b"pos")) + animation.setDuration(duration) + animation.setStartValue(detail_page.pos()) + animation.setEndValue(self.main_window.stackedWidget.rect().topLeft()) + animation.setEasingCurve(QEasingCurve.Type.OutCubic) + animation.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped) + self.animations[detail_page] = animation + animation.finished.connect(cleanup_animation) + animation.finished.connect(load_image_and_restore_effect) + elif animation_type == "bounce": + duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_zoom_duration", 400) + detail_page.setWindowOpacity(0.0) + opacity_anim = QPropertyAnimation(detail_page, QByteArray(b"windowOpacity")) + opacity_anim.setDuration(duration) + opacity_anim.setStartValue(0.0) + opacity_anim.setEndValue(1.0) + initial_rect = QRect(detail_page.x() + detail_page.width() // 4, detail_page.y() + detail_page.height() // 4, + detail_page.width() // 2, detail_page.height() // 2) + final_rect = detail_page.geometry() + geometry_anim = QPropertyAnimation(detail_page, QByteArray(b"geometry")) + geometry_anim.setDuration(duration) + geometry_anim.setStartValue(initial_rect) + geometry_anim.setEndValue(final_rect) + geometry_anim.setEasingCurve(QEasingCurve.Type.OutBack) + group_anim = QParallelAnimationGroup() + group_anim.addAnimation(opacity_anim) + group_anim.addAnimation(geometry_anim) + group_anim.finished.connect(load_image_and_restore_effect) + group_anim.finished.connect(cleanup_animation) + group_anim.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped) + self.animations[detail_page] = group_anim diff --git a/portprotonqt/game_card.py b/portprotonqt/game_card.py index b5e9d52..3c526de 100644 --- a/portprotonqt/game_card.py +++ b/portprotonqt/game_card.py @@ -1,5 +1,5 @@ -from PySide6.QtGui import QPainter, QPen, QColor, QConicalGradient, QBrush, QDesktopServices -from PySide6.QtCore import QEasingCurve, Signal, Property, Qt, QPropertyAnimation, QByteArray, QUrl +from PySide6.QtGui import QPainter, QColor, QDesktopServices +from PySide6.QtCore import Signal, Property, Qt, QUrl from PySide6.QtWidgets import QFrame, QGraphicsDropShadowEffect, QVBoxLayout, QWidget, QStackedLayout, QLabel from collections.abc import Callable import portprotonqt.themes.standart.styles as default_styles @@ -11,9 +11,11 @@ from portprotonqt.config_utils import read_theme_from_config from portprotonqt.custom_widgets import ClickableLabel from portprotonqt.portproton_api import PortProtonAPI from portprotonqt.downloader import Downloader +from portprotonqt.animations import GameCardAnimations import weakref from typing import cast + class GameCard(QFrame): borderWidthChanged = Signal() gradientAngleChanged = Signal() @@ -78,13 +80,8 @@ class GameCard(QFrame): self._focused = False # Анимации - self.thickness_anim = QPropertyAnimation(self, QByteArray(b"borderWidth")) - self.thickness_anim.setDuration(self.theme.GAME_CARD_ANIMATION["thickness_anim_duration"]) - self.gradient_anim = None - self.pulse_anim = None - - # Флаг для отслеживания подключения слота startPulseAnimation - self._isPulseAnimationConnected = False + self.animations = GameCardAnimations(self, self.theme) + self.animations.setup_animations() # Тень shadow = QGraphicsDropShadowEffect(self) @@ -455,133 +452,22 @@ class GameCard(QFrame): def paintEvent(self, event): super().paintEvent(event) - painter = QPainter(self) - painter.setRenderHint(QPainter.RenderHint.Antialiasing) - - pen = QPen() - pen.setWidth(self._borderWidth) - if self._hovered or self._focused: - center = self.rect().center() - gradient = QConicalGradient(center, self._gradientAngle) - for stop in self.theme.GAME_CARD_ANIMATION["gradient_colors"]: - gradient.setColorAt(stop["position"], QColor(stop["color"])) - pen.setBrush(QBrush(gradient)) - else: - pen.setColor(QColor(0, 0, 0, 0)) - - painter.setPen(pen) - radius = 18 - bw = round(self._borderWidth / 2) - rect = self.rect().adjusted(bw, bw, -bw, -bw) - painter.drawRoundedRect(rect, radius, radius) - - def startPulseAnimation(self): - if not (self._hovered or self._focused): - return - if self.pulse_anim: - self.pulse_anim.stop() - self.pulse_anim = QPropertyAnimation(self, QByteArray(b"borderWidth")) - self.pulse_anim.setDuration(self.theme.GAME_CARD_ANIMATION["pulse_anim_duration"]) - self.pulse_anim.setLoopCount(0) - self.pulse_anim.setKeyValueAt(0, self.theme.GAME_CARD_ANIMATION["pulse_min_border_width"]) - self.pulse_anim.setKeyValueAt(0.5, self.theme.GAME_CARD_ANIMATION["pulse_max_border_width"]) - self.pulse_anim.setKeyValueAt(1, self.theme.GAME_CARD_ANIMATION["pulse_min_border_width"]) - self.pulse_anim.start() + self.animations.paint_border(QPainter(self)) def enterEvent(self, event): - self._hovered = True - self.hoverChanged.emit(self.name, True) - self.setFocus(Qt.FocusReason.MouseFocusReason) - - self.thickness_anim.stop() - if self._isPulseAnimationConnected: - self.thickness_anim.finished.disconnect(self.startPulseAnimation) - self._isPulseAnimationConnected = False - self.thickness_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION["thickness_easing_curve"]])) - self.thickness_anim.setStartValue(self._borderWidth) - self.thickness_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["hover_border_width"]) - self.thickness_anim.finished.connect(self.startPulseAnimation) - self._isPulseAnimationConnected = True - self.thickness_anim.start() - - if self.gradient_anim: - self.gradient_anim.stop() - self.gradient_anim = QPropertyAnimation(self, QByteArray(b"gradientAngle")) - self.gradient_anim.setDuration(self.theme.GAME_CARD_ANIMATION["gradient_anim_duration"]) - self.gradient_anim.setStartValue(self.theme.GAME_CARD_ANIMATION["gradient_start_angle"]) - self.gradient_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["gradient_end_angle"]) - self.gradient_anim.setLoopCount(-1) - self.gradient_anim.start() - + self.animations.handle_enter_event() super().enterEvent(event) def leaveEvent(self, event): - self._hovered = False - self.hoverChanged.emit(self.name, False) - if not self._focused: - if self.gradient_anim: - self.gradient_anim.stop() - self.gradient_anim = None - if self.pulse_anim: - self.pulse_anim.stop() - self.pulse_anim = None - if self.thickness_anim: - self.thickness_anim.stop() - if self._isPulseAnimationConnected: - self.thickness_anim.finished.disconnect(self.startPulseAnimation) - self._isPulseAnimationConnected = False - self.thickness_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION["thickness_easing_curve_out"]])) - self.thickness_anim.setStartValue(self._borderWidth) - self.thickness_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["default_border_width"]) - self.thickness_anim.start() + self.animations.handle_leave_event() super().leaveEvent(event) def focusInEvent(self, event): - if not self._hovered: - self._focused = True - self.focusChanged.emit(self.name, True) - - self.thickness_anim.stop() - if self._isPulseAnimationConnected: - self.thickness_anim.finished.disconnect(self.startPulseAnimation) - self._isPulseAnimationConnected = False - self.thickness_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION["thickness_easing_curve"]])) - self.thickness_anim.setStartValue(self._borderWidth) - self.thickness_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["focus_border_width"]) - self.thickness_anim.finished.connect(self.startPulseAnimation) - self._isPulseAnimationConnected = True - self.thickness_anim.start() - - if self.gradient_anim: - self.gradient_anim.stop() - self.gradient_anim = QPropertyAnimation(self, QByteArray(b"gradientAngle")) - self.gradient_anim.setDuration(self.theme.GAME_CARD_ANIMATION["gradient_anim_duration"]) - self.gradient_anim.setStartValue(self.theme.GAME_CARD_ANIMATION["gradient_start_angle"]) - self.gradient_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["gradient_end_angle"]) - self.gradient_anim.setLoopCount(-1) - self.gradient_anim.start() - + self.animations.handle_focus_in_event() super().focusInEvent(event) def focusOutEvent(self, event): - self._focused = False - self.focusChanged.emit(self.name, False) - if not self._hovered: - if self.gradient_anim: - self.gradient_anim.stop() - self.gradient_anim = None - if self.pulse_anim: - self.pulse_anim.stop() - self.pulse_anim = None - if self.thickness_anim: - self.thickness_anim.stop() - if self._isPulseAnimationConnected: - self.thickness_anim.finished.disconnect(self.startPulseAnimation) - self._isPulseAnimationConnected = False - self.thickness_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION["thickness_easing_curve_out"]])) - self.thickness_anim.setStartValue(self._borderWidth) - self.thickness_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["default_border_width"]) - self.thickness_anim.start() + self.animations.handle_focus_out_event() super().focusOutEvent(event) def mousePressEvent(self, event): diff --git a/portprotonqt/main_window.py b/portprotonqt/main_window.py index 099b766..c49a13e 100644 --- a/portprotonqt/main_window.py +++ b/portprotonqt/main_window.py @@ -10,6 +10,7 @@ import psutil from portprotonqt.dialogs import AddGameDialog, FileExplorer from portprotonqt.game_card import GameCard +from portprotonqt.animations import DetailPageAnimations from portprotonqt.custom_widgets import FlowLayout, ClickableLabel, AutoSizeButton, NavLabel from portprotonqt.portproton_api import PortProtonAPI from portprotonqt.input_manager import InputManager @@ -35,8 +36,8 @@ from portprotonqt.howlongtobeat_api import HowLongToBeat from portprotonqt.downloader import Downloader from PySide6.QtWidgets import (QLineEdit, QMainWindow, QStatusBar, QWidget, QVBoxLayout, QLabel, QHBoxLayout, QStackedWidget, QComboBox, QScrollArea, QSlider, - QDialog, QFormLayout, QFrame, QGraphicsDropShadowEffect, QMessageBox, QGraphicsOpacityEffect, QApplication, QPushButton, QProgressBar, QCheckBox, QSizePolicy) -from PySide6.QtCore import Qt, QAbstractAnimation, QPropertyAnimation, QByteArray, QUrl, Signal, QTimer, Slot, QEasingCurve, QParallelAnimationGroup, QRect + QDialog, QFormLayout, QFrame, QGraphicsDropShadowEffect, QMessageBox, QApplication, QPushButton, QProgressBar, QCheckBox, QSizePolicy) +from PySide6.QtCore import Qt, QAbstractAnimation, QUrl, Signal, QTimer, Slot from PySide6.QtGui import QIcon, QPixmap, QColor, QDesktopServices from typing import cast from collections.abc import Callable @@ -209,6 +210,7 @@ class MainWindow(QMainWindow): self.restore_state() self.input_manager = InputManager(self) + self.detail_animations = DetailPageAnimations(self, self.theme) QTimer.singleShot(0, self.loadGames) if read_fullscreen_config(): @@ -1909,97 +1911,7 @@ class MainWindow(QMainWindow): self.current_play_button = playButton # Анимация - animation_type = self.theme.GAME_CARD_ANIMATION.get("detail_page_animation_type", "fade") - duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_fade_duration", 350) - - if animation_type == "fade": - opacity_effect = QGraphicsOpacityEffect(detailPage) - detailPage.setGraphicsEffect(opacity_effect) - animation = QPropertyAnimation(opacity_effect, QByteArray(b"opacity")) - animation.setDuration(duration) - animation.setStartValue(0) - animation.setEndValue(1) - animation.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped) - self._animations[detailPage] = animation - # Очистка эффекта и загрузка изображения после завершения анимации - animation.finished.connect(lambda: detailPage.setGraphicsEffect(shadow)) - animation.finished.connect(load_image_and_restore_effect) - elif animation_type == "slide_left": - duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_slide_duration", 500) - detailPage.move(self.width(), 0) - animation = QPropertyAnimation(detailPage, QByteArray(b"pos")) - animation.setDuration(duration) - animation.setStartValue(detailPage.pos()) - animation.setEndValue(self.stackedWidget.rect().topLeft()) - animation.setEasingCurve(QEasingCurve.Type.OutCubic) - animation.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped) - self._animations[detailPage] = animation - animation.finished.connect(cleanup_animation) - animation.finished.connect(load_image_and_restore_effect) - elif animation_type == "slide_right": - duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_slide_duration", 500) - detailPage.move(-self.width(), 0) - animation = QPropertyAnimation(detailPage, QByteArray(b"pos")) - animation.setDuration(duration) - animation.setStartValue(detailPage.pos()) - animation.setEndValue(self.stackedWidget.rect().topLeft()) - animation.setEasingCurve(QEasingCurve.Type.OutCubic) - animation.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped) - self._animations[detailPage] = animation - animation.finished.connect(cleanup_animation) - animation.finished.connect(load_image_and_restore_effect) - elif animation_type == "slide_up": - duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_slide_duration", 500) - detailPage.move(0, self.height()) - animation = QPropertyAnimation(detailPage, QByteArray(b"pos")) - animation.setDuration(duration) - animation.setStartValue(detailPage.pos()) - animation.setEndValue(self.stackedWidget.rect().topLeft()) - animation.setEasingCurve(QEasingCurve.Type.OutCubic) - animation.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped) - self._animations[detailPage] = animation - animation.finished.connect(cleanup_animation) - animation.finished.connect(load_image_and_restore_effect) - elif animation_type == "slide_down": - duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_slide_duration", 500) - detailPage.move(0, -self.height()) - animation = QPropertyAnimation(detailPage, QByteArray(b"pos")) - animation.setDuration(duration) - animation.setStartValue(detailPage.pos()) - animation.setEndValue(self.stackedWidget.rect().topLeft()) - animation.setEasingCurve(QEasingCurve.Type.OutCubic) - animation.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped) - self._animations[detailPage] = animation - animation.finished.connect(cleanup_animation) - animation.finished.connect(load_image_and_restore_effect) - elif animation_type == "bounce": - duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_zoom_duration", 400) - - detailPage.setWindowOpacity(0.0) - - opacity_anim = QPropertyAnimation(detailPage, QByteArray(b"windowOpacity")) - opacity_anim.setDuration(duration) - opacity_anim.setStartValue(0.0) - opacity_anim.setEndValue(1.0) - - # Animate geometry - initial_rect = QRect(detailPage.x() + detailPage.width() // 4, detailPage.y() + detailPage.height() // 4, - detailPage.width() // 2, detailPage.height() // 2) - final_rect = detailPage.geometry() - geometry_anim = QPropertyAnimation(detailPage, QByteArray(b"geometry")) - geometry_anim.setDuration(duration) - geometry_anim.setStartValue(initial_rect) - geometry_anim.setEndValue(final_rect) - geometry_anim.setEasingCurve(QEasingCurve.Type.OutBack) - - group_anim = QParallelAnimationGroup() - group_anim.addAnimation(opacity_anim) - group_anim.addAnimation(geometry_anim) - - group_anim.finished.connect(load_image_and_restore_effect) - group_anim.finished.connect(cleanup_animation) - group_anim.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped) - self._animations[detailPage] = group_anim + self.detail_animations.animate_detail_page(detailPage, load_image_and_restore_effect, cleanup_animation) def toggleFavoriteInDetailPage(self, game_name, label): favorites = read_favorites()