Add analysis-dependent control logic and tooltips

Introduces logic to enable or disable UI controls based on whether video analysis is complete, with appropriate tooltips for disabled states. Updates all language files to include new tooltip strings. Ensures controls like subtitle selection, SponsorBlock, and merge subtitles are only enabled after analysis, improving user guidance and preventing invalid actions.
This commit is contained in:
oop7
2025-11-03 21:33:25 +02:00
parent 3729d2941e
commit f2a6f3c3e6
16 changed files with 184 additions and 25 deletions
+4 -1
View File
@@ -320,7 +320,10 @@
"select_subtitles": "اختر الترجمات...", "select_subtitles": "اختر الترجمات...",
"sponsorblock_categories": "فئات SponsorBlock...", "sponsorblock_categories": "فئات SponsorBlock...",
"invalid_url_or_enter": "عنوان URL غير صالح أو الرجاء إدخال عنوان URL.", "invalid_url_or_enter": "عنوان URL غير صالح أو الرجاء إدخال عنوان URL.",
"zero_selected": "تم اختيار 0" "zero_selected": "تم اختيار 0",
"analyze_first_tooltip": "يرجى تحليل الفيديو أولاً",
"audio_mode_disabled": "غير متاح في وضع الصوت فقط",
"select_subtitles_first": "يرجى تحديد الترجمة أولاً"
}, },
"sponsorblock": { "sponsorblock": {
"sponsor": "الراعي", "sponsor": "الراعي",
+4 -1
View File
@@ -320,7 +320,10 @@
"select_subtitles": "Untertitel auswählen...", "select_subtitles": "Untertitel auswählen...",
"sponsorblock_categories": "SponsorBlock-Kategorien...", "sponsorblock_categories": "SponsorBlock-Kategorien...",
"invalid_url_or_enter": "Ungültige URL oder bitte geben Sie eine URL ein.", "invalid_url_or_enter": "Ungültige URL oder bitte geben Sie eine URL ein.",
"zero_selected": "0 ausgewählt" "zero_selected": "0 ausgewählt",
"analyze_first_tooltip": "Bitte analysieren Sie zuerst das Video",
"audio_mode_disabled": "Nicht verfügbar im Nur-Audio-Modus",
"select_subtitles_first": "Bitte wählen Sie zuerst Untertitel aus"
}, },
"sponsorblock": { "sponsorblock": {
"sponsor": "Sponsor", "sponsor": "Sponsor",
+4 -1
View File
@@ -320,7 +320,10 @@
"select_subtitles": "Select Subtitles...", "select_subtitles": "Select Subtitles...",
"sponsorblock_categories": "SponsorBlock Categories...", "sponsorblock_categories": "SponsorBlock Categories...",
"invalid_url_or_enter": "Invalid URL or please enter a URL.", "invalid_url_or_enter": "Invalid URL or please enter a URL.",
"zero_selected": "0 selected" "zero_selected": "0 selected",
"analyze_first_tooltip": "Please analyze the video first",
"audio_mode_disabled": "Not available in audio-only mode",
"select_subtitles_first": "Please select subtitles first"
}, },
"sponsorblock": { "sponsorblock": {
"sponsor": "Sponsor", "sponsor": "Sponsor",
+4 -1
View File
@@ -303,7 +303,10 @@
"select_subtitles": "Seleccionar Subtítulos...", "select_subtitles": "Seleccionar Subtítulos...",
"sponsorblock_categories": "Categorías SponsorBlock...", "sponsorblock_categories": "Categorías SponsorBlock...",
"invalid_url_or_enter": "URL inválida o por favor ingresa una URL.", "invalid_url_or_enter": "URL inválida o por favor ingresa una URL.",
"zero_selected": "0 seleccionados" "zero_selected": "0 seleccionados",
"analyze_first_tooltip": "Por favor analiza el video primero",
"audio_mode_disabled": "No disponible en modo solo audio",
"select_subtitles_first": "Por favor selecciona subtítulos primero"
}, },
"sponsorblock": { "sponsorblock": {
"sponsor": "Patrocinador", "sponsor": "Patrocinador",
+4 -1
View File
@@ -320,7 +320,10 @@
"select_subtitles": "Sélectionner les sous-titres...", "select_subtitles": "Sélectionner les sous-titres...",
"sponsorblock_categories": "Catégories SponsorBlock...", "sponsorblock_categories": "Catégories SponsorBlock...",
"invalid_url_or_enter": "URL invalide ou veuillez entrer une URL.", "invalid_url_or_enter": "URL invalide ou veuillez entrer une URL.",
"zero_selected": "0 sélectionné" "zero_selected": "0 sélectionné",
"analyze_first_tooltip": "Veuillez d'abord analyser la vidéo",
"audio_mode_disabled": "Non disponible en mode audio uniquement",
"select_subtitles_first": "Veuillez d'abord sélectionner les sous-titres"
}, },
"sponsorblock": { "sponsorblock": {
"sponsor": "Sponsor", "sponsor": "Sponsor",
+4 -1
View File
@@ -320,7 +320,10 @@
"select_subtitles": "उपशीर्षक चुनें...", "select_subtitles": "उपशीर्षक चुनें...",
"sponsorblock_categories": "SponsorBlock श्रेणियां...", "sponsorblock_categories": "SponsorBlock श्रेणियां...",
"invalid_url_or_enter": "अमान्य URL या कृपया URL दर्ज करें।", "invalid_url_or_enter": "अमान्य URL या कृपया URL दर्ज करें।",
"zero_selected": "0 चयनित" "zero_selected": "0 चयनित",
"analyze_first_tooltip": "कृपया पहले वीडियो का विश्लेषण करें",
"audio_mode_disabled": "केवल ऑडियो मोड में उपलब्ध नहीं",
"select_subtitles_first": "कृपया पहले उपशीर्षक चुनें"
}, },
"sponsorblock": { "sponsorblock": {
"sponsor": "प्रायोजक", "sponsor": "प्रायोजक",
+4 -1
View File
@@ -320,7 +320,10 @@
"select_subtitles": "Pilih subtitle...", "select_subtitles": "Pilih subtitle...",
"sponsorblock_categories": "Kategori SponsorBlock...", "sponsorblock_categories": "Kategori SponsorBlock...",
"invalid_url_or_enter": "URL tidak valid atau silakan masukkan URL.", "invalid_url_or_enter": "URL tidak valid atau silakan masukkan URL.",
"zero_selected": "0 dipilih" "zero_selected": "0 dipilih",
"analyze_first_tooltip": "Silakan analisis video terlebih dahulu",
"audio_mode_disabled": "Tidak tersedia dalam mode audio saja",
"select_subtitles_first": "Silakan pilih subtitle terlebih dahulu"
}, },
"sponsorblock": { "sponsorblock": {
"sponsor": "Sponsor", "sponsor": "Sponsor",
+4 -1
View File
@@ -320,7 +320,10 @@
"select_subtitles": "Seleziona sottotitoli...", "select_subtitles": "Seleziona sottotitoli...",
"sponsorblock_categories": "Categorie SponsorBlock...", "sponsorblock_categories": "Categorie SponsorBlock...",
"invalid_url_or_enter": "URL non valido o inserisci un URL.", "invalid_url_or_enter": "URL non valido o inserisci un URL.",
"zero_selected": "0 selezionati" "zero_selected": "0 selezionati",
"analyze_first_tooltip": "Si prega di analizzare prima il video",
"audio_mode_disabled": "Non disponibile in modalità solo audio",
"select_subtitles_first": "Si prega di selezionare prima i sottotitoli"
}, },
"sponsorblock": { "sponsorblock": {
"sponsor": "Sponsor", "sponsor": "Sponsor",
+4 -1
View File
@@ -320,7 +320,10 @@
"select_subtitles": "字幕を選択...", "select_subtitles": "字幕を選択...",
"sponsorblock_categories": "SponsorBlockカテゴリ...", "sponsorblock_categories": "SponsorBlockカテゴリ...",
"invalid_url_or_enter": "無効なURLまたはURLを入力してください。", "invalid_url_or_enter": "無効なURLまたはURLを入力してください。",
"zero_selected": "0個選択" "zero_selected": "0個選択",
"analyze_first_tooltip": "最初に動画を分析してください",
"audio_mode_disabled": "音声のみモードでは利用できません",
"select_subtitles_first": "最初に字幕を選択してください"
}, },
"sponsorblock": { "sponsorblock": {
"sponsor": "スポンサー", "sponsor": "スポンサー",
+4 -1
View File
@@ -320,7 +320,10 @@
"select_subtitles": "Wybierz napisy...", "select_subtitles": "Wybierz napisy...",
"sponsorblock_categories": "Kategorie SponsorBlock...", "sponsorblock_categories": "Kategorie SponsorBlock...",
"invalid_url_or_enter": "Nieprawidłowy URL lub wprowadź URL.", "invalid_url_or_enter": "Nieprawidłowy URL lub wprowadź URL.",
"zero_selected": "0 wybranych" "zero_selected": "0 wybranych",
"analyze_first_tooltip": "Najpierw przeanalizuj wideo",
"audio_mode_disabled": "Niedostępne w trybie tylko audio",
"select_subtitles_first": "Najpierw wybierz napisy"
}, },
"sponsorblock": { "sponsorblock": {
"sponsor": "Sponsor", "sponsor": "Sponsor",
+4 -1
View File
@@ -320,7 +320,10 @@
"select_subtitles": "Selecionar Legendas...", "select_subtitles": "Selecionar Legendas...",
"sponsorblock_categories": "Categorias SponsorBlock...", "sponsorblock_categories": "Categorias SponsorBlock...",
"invalid_url_or_enter": "URL inválido ou por favor insira um URL.", "invalid_url_or_enter": "URL inválido ou por favor insira um URL.",
"zero_selected": "0 selecionados" "zero_selected": "0 selecionados",
"analyze_first_tooltip": "Por favor analise o vídeo primeiro",
"audio_mode_disabled": "Não disponível no modo apenas áudio",
"select_subtitles_first": "Por favor selecione legendas primeiro"
}, },
"sponsorblock": { "sponsorblock": {
"sponsor": "Patrocinador", "sponsor": "Patrocinador",
+4 -1
View File
@@ -320,7 +320,10 @@
"select_subtitles": "Выбрать субтитры...", "select_subtitles": "Выбрать субтитры...",
"sponsorblock_categories": "Категории SponsorBlock...", "sponsorblock_categories": "Категории SponsorBlock...",
"invalid_url_or_enter": "Неверный URL или пожалуйста введите URL.", "invalid_url_or_enter": "Неверный URL или пожалуйста введите URL.",
"zero_selected": "0 выбрано" "zero_selected": "0 выбрано",
"analyze_first_tooltip": "Пожалуйста, сначала проанализируйте видео",
"audio_mode_disabled": "Недоступно в режиме только аудио",
"select_subtitles_first": "Пожалуйста, сначала выберите субтитры"
}, },
"sponsorblock": { "sponsorblock": {
"sponsor": "Спонсор", "sponsor": "Спонсор",
+4 -1
View File
@@ -320,7 +320,10 @@
"select_subtitles": "Altyazı seç...", "select_subtitles": "Altyazı seç...",
"sponsorblock_categories": "SponsorBlock kategorileri...", "sponsorblock_categories": "SponsorBlock kategorileri...",
"invalid_url_or_enter": "Geçersiz URL veya lütfen bir URL girin.", "invalid_url_or_enter": "Geçersiz URL veya lütfen bir URL girin.",
"zero_selected": "0 seçildi" "zero_selected": "0 seçildi",
"analyze_first_tooltip": "Lütfen önce videoyu analiz edin",
"audio_mode_disabled": "Yalnızca ses modunda kullanılamaz",
"select_subtitles_first": "Lütfen önce altyazıları seçin"
}, },
"sponsorblock": { "sponsorblock": {
"sponsor": "Sponsor", "sponsor": "Sponsor",
+4 -1
View File
@@ -320,7 +320,10 @@
"select_subtitles": "选择字幕...", "select_subtitles": "选择字幕...",
"sponsorblock_categories": "SponsorBlock 分类...", "sponsorblock_categories": "SponsorBlock 分类...",
"invalid_url_or_enter": "无效的URL或请输入URL。", "invalid_url_or_enter": "无效的URL或请输入URL。",
"zero_selected": "已选择 0 个" "zero_selected": "已选择 0 个",
"analyze_first_tooltip": "请先分析视频",
"audio_mode_disabled": "在纯音频模式下不可用",
"select_subtitles_first": "请先选择字幕"
}, },
"sponsorblock": { "sponsorblock": {
"sponsor": "赞助商", "sponsor": "赞助商",
+118 -11
View File
@@ -8,7 +8,7 @@ import markdown
import pyglet import pyglet
import requests import requests
from packaging import version from packaging import version
from PySide6.QtCore import Q_ARG, QMetaObject, Qt, QTimer from PySide6.QtCore import Q_ARG, QMetaObject, Qt, QTimer, Slot
from PySide6.QtGui import QIcon from PySide6.QtGui import QIcon
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QApplication, QApplication,
@@ -127,6 +127,8 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
# Initialize output format settings # Initialize output format settings
self.force_output_format = ConfigManager.get("force_output_format") or False self.force_output_format = ConfigManager.get("force_output_format") or False
self.preferred_output_format = ConfigManager.get("preferred_output_format") or "mp4" self.preferred_output_format = ConfigManager.get("preferred_output_format") or "mp4"
# Track if video analysis is completed
self.analysis_completed = False
self.init_ui() self.init_ui()
self.setStyleSheet( self.setStyleSheet(
@@ -708,6 +710,9 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
self.signals.playlist_select_btn_visible.connect(self.playlist_select_btn.setVisible) self.signals.playlist_select_btn_visible.connect(self.playlist_select_btn.setVisible)
self.signals.playlist_select_btn_text.connect(self.playlist_select_btn.setText) self.signals.playlist_select_btn_text.connect(self.playlist_select_btn.setText)
# Disable analysis-dependent controls until video is analyzed
self.toggle_analysis_dependent_controls(enabled=False)
def analyze_url(self) -> None: def analyze_url(self) -> None:
url = self.url_input.text().strip() url = self.url_input.text().strip()
if not url: if not url:
@@ -720,6 +725,10 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
QMessageBox.warning(self, _("main_ui.error_title"), error_message) QMessageBox.warning(self, _("main_ui.error_title"), error_message)
return return
# Reset analysis state and disable controls
self.analysis_completed = False
self.toggle_analysis_dependent_controls(enabled=False)
self.signals.update_status.emit(_("main_ui.analyzing_preparing")) self.signals.update_status.emit(_("main_ui.analyzing_preparing"))
threading.Thread(target=self._analyze_url_thread, args=(url,), daemon=True).start() threading.Thread(target=self._analyze_url_thread, args=(url,), daemon=True).start()
@@ -1532,39 +1541,128 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
self.status_label.setText(_("status.ready")) self.status_label.setText(_("status.ready"))
self.download_details_label.setText("") # Clear details label self.download_details_label.setText("") # Clear details label
@Slot(bool)
def toggle_analysis_dependent_controls(self, enabled=True) -> None:
"""Enable or disable controls that require video analysis to be completed"""
# Determine tooltip text for disabled state
tooltip_text = "" if enabled else _("main_ui.analyze_first_tooltip")
# Subtitle selection
if hasattr(self, "subtitle_select_btn"):
self.subtitle_select_btn.setEnabled(enabled)
if not enabled:
self.subtitle_select_btn.setToolTip(tooltip_text)
else:
self.subtitle_select_btn.setToolTip("")
# SponsorBlock (only if not in audio mode)
if hasattr(self, "sponsorblock_select_btn"):
is_audio_mode = self.audio_button.isChecked()
self.sponsorblock_select_btn.setEnabled(enabled and not is_audio_mode)
if not enabled or is_audio_mode:
self.sponsorblock_select_btn.setToolTip(tooltip_text if not enabled else _("main_ui.audio_mode_disabled"))
else:
self.sponsorblock_select_btn.setToolTip("")
# Save Thumbnail checkbox
if hasattr(self, "save_thumbnail_checkbox"):
self.save_thumbnail_checkbox.setEnabled(enabled)
if not enabled:
self.save_thumbnail_checkbox.setToolTip(tooltip_text)
else:
self.save_thumbnail_checkbox.setToolTip("")
# Save Description checkbox
if hasattr(self, "save_description_checkbox"):
self.save_description_checkbox.setEnabled(enabled)
if not enabled:
self.save_description_checkbox.setToolTip(tooltip_text)
else:
self.save_description_checkbox.setToolTip("")
# Embed Chapters checkbox
if hasattr(self, "embed_chapters_checkbox"):
self.embed_chapters_checkbox.setEnabled(enabled)
if not enabled:
self.embed_chapters_checkbox.setToolTip(tooltip_text)
else:
self.embed_chapters_checkbox.setToolTip("")
# Merge Subtitles (only if subtitles are selected and not in audio mode)
if hasattr(self, "merge_subs_checkbox"):
has_subs = len(getattr(self, "selected_subtitles", [])) > 0
is_audio_mode = self.audio_button.isChecked()
should_enable = enabled and has_subs and not is_audio_mode
self.merge_subs_checkbox.setEnabled(should_enable)
if not enabled:
self.merge_subs_checkbox.setToolTip(tooltip_text)
elif not has_subs:
self.merge_subs_checkbox.setToolTip(_("main_ui.select_subtitles_first"))
elif is_audio_mode:
self.merge_subs_checkbox.setToolTip(_("main_ui.audio_mode_disabled"))
else:
self.merge_subs_checkbox.setToolTip("")
def handle_format_selection(self, button) -> None: def handle_format_selection(self, button) -> None:
# Update formats # Update formats
self.filter_formats() self.filter_formats()
def handle_mode_change(self) -> None: def handle_mode_change(self) -> None:
"""Enable or disable features based on video/audio mode""" """Enable or disable features based on video/audio mode"""
# Only allow enabling if analysis is complete
can_enable = self.analysis_completed
if self.audio_button.isChecked(): if self.audio_button.isChecked():
# In Audio Only mode, disable video-specific features # In Audio Only mode, disable video-specific features
if hasattr(self, "sponsorblock_select_btn"): if hasattr(self, "sponsorblock_select_btn"):
self.sponsorblock_select_btn.setEnabled(False) self.sponsorblock_select_btn.setEnabled(False)
self.sponsorblock_select_btn.setToolTip(_("main_ui.audio_mode_disabled"))
if hasattr(self, "selected_sponsorblock_categories"): if hasattr(self, "selected_sponsorblock_categories"):
self.selected_sponsorblock_categories = [] # Clear selection when disabled self.selected_sponsorblock_categories = [] # Clear selection when disabled
if hasattr(self, "_update_sponsorblock_display"): if hasattr(self, "_update_sponsorblock_display"):
self._update_sponsorblock_display() self._update_sponsorblock_display()
self.merge_subs_checkbox.setEnabled(False) self.merge_subs_checkbox.setEnabled(False)
self.merge_subs_checkbox.setChecked(False) # Uncheck when disabled self.merge_subs_checkbox.setChecked(False) # Uncheck when disabled
if not can_enable:
# Allow subtitle selection in Audio Only mode too self.merge_subs_checkbox.setToolTip(_("main_ui.analyze_first_tooltip"))
if hasattr(self, "subtitle_select_btn"):
self.subtitle_select_btn.setEnabled(True)
else: else:
# In Video mode, enable video-specific features self.merge_subs_checkbox.setToolTip(_("main_ui.audio_mode_disabled"))
# Allow subtitle selection in Audio Only mode if analysis is complete
if hasattr(self, "subtitle_select_btn"):
self.subtitle_select_btn.setEnabled(can_enable)
if not can_enable:
self.subtitle_select_btn.setToolTip(_("main_ui.analyze_first_tooltip"))
else:
self.subtitle_select_btn.setToolTip("")
else:
# In Video mode, enable video-specific features (if analysis complete)
if hasattr(self, "sponsorblock_select_btn"): if hasattr(self, "sponsorblock_select_btn"):
self.sponsorblock_select_btn.setEnabled(True) self.sponsorblock_select_btn.setEnabled(can_enable)
if not can_enable:
self.sponsorblock_select_btn.setToolTip(_("main_ui.analyze_first_tooltip"))
else:
self.sponsorblock_select_btn.setToolTip("")
# Don't automatically restore categories - let user choose when they open the dialog # Don't automatically restore categories - let user choose when they open the dialog
# Enable merge_subs only if subtitles are selected # Enable merge_subs only if subtitles are selected and analysis is complete
has_subs_selected = len(getattr(self, "selected_subtitles", [])) > 0 has_subs_selected = len(getattr(self, "selected_subtitles", [])) > 0
self.merge_subs_checkbox.setEnabled(has_subs_selected) should_enable_merge = can_enable and has_subs_selected
self.merge_subs_checkbox.setEnabled(should_enable_merge)
if not can_enable:
self.merge_subs_checkbox.setToolTip(_("main_ui.analyze_first_tooltip"))
elif not has_subs_selected:
self.merge_subs_checkbox.setToolTip(_("main_ui.select_subtitles_first"))
else:
self.merge_subs_checkbox.setToolTip("")
# Re-enable subtitle selection button in Video mode # Re-enable subtitle selection button in Video mode (if analysis complete)
if hasattr(self, "subtitle_select_btn"): if hasattr(self, "subtitle_select_btn"):
self.subtitle_select_btn.setEnabled(True) self.subtitle_select_btn.setEnabled(can_enable)
if not can_enable:
self.subtitle_select_btn.setToolTip(_("main_ui.analyze_first_tooltip"))
else:
self.subtitle_select_btn.setToolTip("")
# Keep these methods for backwards compatibility - they just call the new dialog now # Keep these methods for backwards compatibility - they just call the new dialog now
def show_custom_command(self) -> None: def show_custom_command(self) -> None:
@@ -1843,6 +1941,15 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
self.signals.update_status.emit(_("main_ui.analysis_complete")) self.signals.update_status.emit(_("main_ui.analysis_complete"))
# Mark analysis as complete and enable analysis-dependent controls
self.analysis_completed = True
QMetaObject.invokeMethod(
self,
"toggle_analysis_dependent_controls",
Qt.ConnectionType.QueuedConnection,
Q_ARG(bool, True),
)
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
logger.error("Analysis timed out. Please try again.") logger.error("Analysis timed out. Please try again.")
self.signals.update_status.emit(_("errors.analysis_timeout")) self.signals.update_status.emit(_("errors.analysis_timeout"))
+12 -2
View File
@@ -300,11 +300,21 @@ class VideoInfoMixin:
# Enable/disable the merge checkbox in the parent window # Enable/disable the merge checkbox in the parent window
if merge_checkbox: if merge_checkbox:
# Only enable merge checkbox if we're not in Audio Only mode # Only enable merge checkbox if we're not in Audio Only mode and analysis is complete
is_audio_only = hasattr(self, "audio_button") and self.audio_button.isChecked() is_audio_only = hasattr(self, "audio_button") and self.audio_button.isChecked()
has_analysis = getattr(self, "analysis_completed", False)
# In audio-only mode, we still allow subtitle selection but not merging # In audio-only mode, we still allow subtitle selection but not merging
should_enable = count > 0 and not is_audio_only should_enable = count > 0 and not is_audio_only and has_analysis
merge_checkbox.setEnabled(should_enable) merge_checkbox.setEnabled(should_enable)
# Update tooltip
if not has_analysis:
merge_checkbox.setToolTip(_("main_ui.analyze_first_tooltip"))
elif is_audio_only:
merge_checkbox.setToolTip(_("main_ui.audio_mode_disabled"))
elif count == 0:
merge_checkbox.setToolTip(_("main_ui.select_subtitles_first"))
else:
merge_checkbox.setToolTip("")
else: else:
logger.warning("merge_subs_checkbox not found on parent window.") logger.warning("merge_subs_checkbox not found on parent window.")