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:
@@ -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
|
||||||
|
|||||||
@@ -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."""
|
||||||
|
|||||||
Reference in New Issue
Block a user