Add animated fade for status and controls
Introduce smooth UI transitions by adding opacity-based animations. Import QGraphicsOpacityEffect and add animate_widget_fade_in, animate_widget_fade_out, and set_status_message_animated to cross-fade status text and fade buttons in/out. Update signal handlers and download flow to use the new animated status setter and fade methods instead of direct setText/setVisible calls. Keep animation references on widgets to avoid premature GC and remove effects after fade-out.
This commit is contained in:
+107
-12
@@ -26,6 +26,7 @@ from PySide6.QtWidgets import (
|
||||
QTextEdit,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
QGraphicsOpacityEffect,
|
||||
)
|
||||
|
||||
from .. import __version__ as APP_VERSION
|
||||
@@ -505,7 +506,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin):
|
||||
|
||||
# Connect signals
|
||||
self.signals.update_formats.connect(self.update_format_table)
|
||||
self.signals.update_status.connect(self.status_label.setText)
|
||||
self.signals.update_status.connect(self.set_status_message_animated)
|
||||
self.signals.update_progress.connect(self.update_progress_bar)
|
||||
|
||||
# Connect new signals
|
||||
@@ -698,7 +699,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin):
|
||||
|
||||
# Connect signals
|
||||
self.download_thread.progress_signal.connect(self.update_progress_bar)
|
||||
self.download_thread.status_signal.connect(self.status_label.setText)
|
||||
self.download_thread.status_signal.connect(self.set_status_message_animated)
|
||||
self.download_thread.update_details.connect(self.download_details_label.setText)
|
||||
self.download_thread.finished_signal.connect(self.download_finished)
|
||||
self.download_thread.error_signal.connect(self.download_error)
|
||||
@@ -710,8 +711,8 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin):
|
||||
|
||||
# Show pause/cancel buttons
|
||||
self.pause_btn.setText(_("buttons.pause"))
|
||||
self.pause_btn.setVisible(True)
|
||||
self.cancel_btn.setVisible(True)
|
||||
self.animate_widget_fade_in(self.pause_btn)
|
||||
self.animate_widget_fade_in(self.cancel_btn)
|
||||
|
||||
# Start download thread
|
||||
self.current_download = self.download_thread
|
||||
@@ -720,8 +721,8 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin):
|
||||
|
||||
def download_finished(self) -> None:
|
||||
self.toggle_download_controls(True)
|
||||
self.pause_btn.setVisible(False)
|
||||
self.cancel_btn.setVisible(False)
|
||||
self.animate_widget_fade_out(self.pause_btn)
|
||||
self.animate_widget_fade_out(self.cancel_btn)
|
||||
self.progress_bar.setValue(10000) # 100% in 0-10000 range
|
||||
|
||||
# Set completion message based on the file type of last downloaded file
|
||||
@@ -731,19 +732,19 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin):
|
||||
|
||||
# Video file extensions
|
||||
if ext in VIDEO_EXTENSIONS:
|
||||
self.status_label.setText(_('download.video_completed'))
|
||||
self.set_status_message_animated(_('download.video_completed'))
|
||||
# Audio file extensions
|
||||
elif ext in AUDIO_EXTENSIONS:
|
||||
self.status_label.setText(_('download.audio_completed'))
|
||||
self.set_status_message_animated(_('download.audio_completed'))
|
||||
# Subtitle file extensions
|
||||
elif ext in SUBTITLE_EXTENSIONS:
|
||||
self.status_label.setText(_('download.subtitle_completed'))
|
||||
self.set_status_message_animated(_('download.subtitle_completed'))
|
||||
# Default case
|
||||
else:
|
||||
self.status_label.setText(_('download.completed'))
|
||||
self.set_status_message_animated(_('download.completed'))
|
||||
|
||||
# Show the open folder button
|
||||
self.open_folder_btn.setVisible(True)
|
||||
self.animate_widget_fade_in(self.open_folder_btn)
|
||||
|
||||
# Save to history
|
||||
try:
|
||||
@@ -1409,7 +1410,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin):
|
||||
def cancel_download(self) -> None:
|
||||
if self.current_download:
|
||||
self.current_download.cancelled = True
|
||||
self.status_label.setText(_("status.cancelling")) # Set status directly
|
||||
self.set_status_message_animated(_("status.cancelling")) # Set status directly
|
||||
self.download_details_label.setText("") # Clear details label on cancellation
|
||||
|
||||
def show_ffmpeg_dialog(self) -> None:
|
||||
@@ -1458,4 +1459,98 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin):
|
||||
success_dialog.setStyleSheet(StyleSheet.SETUP_SUCCESS_DIALOG)
|
||||
success_dialog.exec()
|
||||
|
||||
def animate_widget_fade_in(self, widget: QWidget, duration: int = 300) -> None:
|
||||
"""Fade in a widget using opacity animation."""
|
||||
if widget.isVisible():
|
||||
return
|
||||
|
||||
# Setup opacity effect if not present
|
||||
effect = QGraphicsOpacityEffect(widget)
|
||||
widget.setGraphicsEffect(effect)
|
||||
|
||||
# Determine start and end values
|
||||
start_val = 0.0
|
||||
end_val = 1.0
|
||||
|
||||
# Setup animation
|
||||
anim = QPropertyAnimation(effect, b"opacity", widget)
|
||||
anim.setDuration(duration)
|
||||
anim.setStartValue(start_val)
|
||||
anim.setEndValue(end_val)
|
||||
anim.setEasingCurve(QEasingCurve.Type.OutQuad)
|
||||
|
||||
# Show widget before animation
|
||||
widget.setVisible(True)
|
||||
anim.start(QPropertyAnimation.DeletionPolicy.DeleteWhenStopped)
|
||||
|
||||
# Keep reference to avoid garbage collection
|
||||
widget._fade_anim = anim
|
||||
|
||||
def animate_widget_fade_out(self, widget: QWidget, duration: int = 300) -> None:
|
||||
"""Fade out a widget and then hide it."""
|
||||
if not widget.isVisible():
|
||||
return
|
||||
|
||||
effect = QGraphicsOpacityEffect(widget)
|
||||
widget.setGraphicsEffect(effect)
|
||||
|
||||
anim = QPropertyAnimation(effect, b"opacity", widget)
|
||||
anim.setDuration(duration)
|
||||
anim.setStartValue(1.0)
|
||||
anim.setEndValue(0.0)
|
||||
anim.setEasingCurve(QEasingCurve.Type.InQuad)
|
||||
|
||||
def on_finished():
|
||||
widget.setVisible(False)
|
||||
widget.setGraphicsEffect(None) # Remove effect to restore normal painting
|
||||
|
||||
anim.finished.connect(on_finished)
|
||||
anim.start(QPropertyAnimation.DeletionPolicy.DeleteWhenStopped)
|
||||
|
||||
widget._fade_out_anim = anim # Keep reference
|
||||
|
||||
def set_status_message_animated(self, message: str) -> None:
|
||||
"""Update status label with a cross-fade animation."""
|
||||
if self.status_label.text() == message:
|
||||
return
|
||||
|
||||
# If previous animation is running, stop it
|
||||
if hasattr(self.status_label, '_status_anim'):
|
||||
try:
|
||||
if self.status_label._status_anim.state() == QPropertyAnimation.State.Running:
|
||||
self.status_label._status_anim.stop()
|
||||
except RuntimeError:
|
||||
pass # Animation object already deleted
|
||||
|
||||
# Create effect if needed
|
||||
effect = self.status_label.graphicsEffect()
|
||||
if not effect or not isinstance(effect, QGraphicsOpacityEffect):
|
||||
effect = QGraphicsOpacityEffect(self.status_label)
|
||||
self.status_label.setGraphicsEffect(effect)
|
||||
|
||||
# 1. Fade OUT
|
||||
anim1 = QPropertyAnimation(effect, b"opacity", self.status_label)
|
||||
anim1.setDuration(150)
|
||||
anim1.setStartValue(1.0)
|
||||
anim1.setEndValue(0.0)
|
||||
anim1.setEasingCurve(QEasingCurve.Type.OutQuad)
|
||||
|
||||
# 2. Change Text & Fade IN
|
||||
anim2 = QPropertyAnimation(effect, b"opacity", self.status_label)
|
||||
anim2.setDuration(150)
|
||||
anim2.setStartValue(0.0)
|
||||
anim2.setEndValue(1.0)
|
||||
anim2.setEasingCurve(QEasingCurve.Type.InQuad)
|
||||
|
||||
def on_fade_out_finished():
|
||||
self.status_label.setText(message)
|
||||
anim2.start(QPropertyAnimation.DeletionPolicy.DeleteWhenStopped)
|
||||
|
||||
anim1.finished.connect(on_fade_out_finished)
|
||||
anim1.start(QPropertyAnimation.DeletionPolicy.DeleteWhenStopped)
|
||||
|
||||
# Store ref
|
||||
self.status_label._status_anim = anim1
|
||||
self.status_label._status_anim2 = anim2
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user