Add widget shake animation for invalid input

Introduce a shake animation to visually indicate invalid inputs and wire it into validation/error flows. Changes: import QPoint; add animate_widget_shake(QWidget) implementation that uses QPropertyAnimation on widget.pos with keyframes; call the animation when URL/path/format validations fail and replace some direct status_label updates with set_status_message_animated. Updated files: ytsage/gui/ytsage_gui_main.py and ytsage/gui/ytsage_gui_analysis.py.
This commit is contained in:
oop7
2026-02-01 12:36:00 +02:00
parent 091c6c68c1
commit 36eff02b18
2 changed files with 45 additions and 5 deletions
+4
View File
@@ -271,12 +271,16 @@ class AnalysisMixin:
url = self.url_input.text().strip() url = self.url_input.text().strip()
if not url: if not url:
self.signals.update_status.emit(_("main_ui.invalid_url_or_enter")) self.signals.update_status.emit(_("main_ui.invalid_url_or_enter"))
if hasattr(self, "animate_widget_shake"):
self.animate_widget_shake(self.url_input)
return return
# Validate URL before processing # Validate URL before processing
is_valid, error_message = validate_video_url(url) is_valid, error_message = validate_video_url(url)
if not is_valid: if not is_valid:
QMessageBox.warning(self, _("main_ui.error_title"), error_message) QMessageBox.warning(self, _("main_ui.error_title"), error_message)
if hasattr(self, "animate_widget_shake"):
self.animate_widget_shake(self.url_input)
return return
# Cancel any existing analysis thread # Cancel any existing analysis thread
+41 -5
View File
@@ -7,7 +7,7 @@ from pathlib import Path
import markdown import markdown
import requests import requests
from packaging import version from packaging import version
from PySide6.QtCore import Q_ARG, QMetaObject, Qt, QTimer, Slot, QThread, Signal, QUrl, QPropertyAnimation, QEasingCurve from PySide6.QtCore import Q_ARG, QMetaObject, Qt, QTimer, Slot, QThread, Signal, QUrl, QPropertyAnimation, QEasingCurve, QPoint
from PySide6.QtGui import QIcon from PySide6.QtGui import QIcon
from PySide6.QtMultimedia import QAudioOutput, QMediaPlayer from PySide6.QtMultimedia import QAudioOutput, QMediaPlayer
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
@@ -598,11 +598,15 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin):
if not url or not path: if not url or not path:
# More specific error message if path is missing # More specific error message if path is missing
if not path: if not path:
self.status_label.setText(_('download.please_set_path')) self.set_status_message_animated(_('download.please_set_path'))
self.animate_widget_shake(self.settings_button)
elif not url: elif not url:
self.status_label.setText(_('download.please_enter_url')) self.set_status_message_animated(_('download.please_enter_url'))
self.animate_widget_shake(self.url_input)
else: else:
self.status_label.setText(_('download.please_enter_url_and_path')) self.set_status_message_animated(_('download.please_enter_url_and_path'))
self.animate_widget_shake(self.url_input)
self.animate_widget_shake(self.settings_button)
return return
# --- End Path Change --- # --- End Path Change ---
@@ -610,12 +614,14 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin):
is_valid, error_message = validate_video_url(url) is_valid, error_message = validate_video_url(url)
if not is_valid: if not is_valid:
QMessageBox.warning(self, _("main_ui.error_title"), error_message) QMessageBox.warning(self, _("main_ui.error_title"), error_message)
self.animate_widget_shake(self.url_input)
return return
# Get selected format # Get selected format
selected_format = self.get_selected_format() selected_format = self.get_selected_format()
if not selected_format: if not selected_format:
self.status_label.setText(_('download.please_select_format')) self.set_status_message_animated(_('download.please_select_format'))
self.animate_widget_shake(self.format_table)
return return
format_id = selected_format["format_id"] format_id = selected_format["format_id"]
is_audio_only = bool(selected_format.get("is_audio_only")) is_audio_only = bool(selected_format.get("is_audio_only"))
@@ -1539,6 +1545,36 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin):
else: else:
self.animate_widget_fade_out(widget) self.animate_widget_fade_out(widget)
def animate_widget_shake(self, widget: QWidget) -> None:
"""Shake a widget left and right to indicate an error or invalid input."""
# Stop shake if running
if hasattr(widget, '_shake_anim'):
try:
if widget._shake_anim.state() == QPropertyAnimation.State.Running:
return
except RuntimeError:
pass
# Use current position as baseline
pos = widget.pos()
x = pos.x()
y = pos.y()
anim = QPropertyAnimation(widget, b"pos", widget)
anim.setDuration(300)
anim.setLoopCount(1)
# Create keyframes for shake effect
anim.setKeyValueAt(0, QPoint(x, y))
anim.setKeyValueAt(0.2, QPoint(x - 5, y))
anim.setKeyValueAt(0.4, QPoint(x + 5, y))
anim.setKeyValueAt(0.6, QPoint(x - 5, y))
anim.setKeyValueAt(0.8, QPoint(x + 5, y))
anim.setKeyValueAt(1.0, QPoint(x, y))
widget._shake_anim = anim
anim.start(QPropertyAnimation.DeletionPolicy.DeleteWhenStopped)
def set_status_message_animated(self, message: str) -> None: def set_status_message_animated(self, message: str) -> None:
"""Update status label with a cross-fade animation.""" """Update status label with a cross-fade animation."""