""" Settings-related dialogs for YTSage application. Contains dialogs for configuring download settings. """ import threading import time from datetime import datetime import requests from packaging import version as version_parser from PySide6.QtCore import Qt, QTimer, QUrl from PySide6.QtGui import QDesktopServices from PySide6.QtWidgets import ( QButtonGroup, QCheckBox, QComboBox, QDialog, QDialogButtonBox, QFileDialog, QGroupBox, QHBoxLayout, QLabel, QLineEdit, QMessageBox, QPushButton, QRadioButton, QVBoxLayout, ) from ...utils.ytsage_logger import logger from ...utils.ytsage_localization import _ from ...utils.ytsage_config_manager import ConfigManager from ...utils.ytsage_constants import APP_LOG_DIR class DownloadSettingsDialog(QDialog): def __init__(self, current_path, current_limit, current_unit_index, parent=None) -> None: super().__init__(parent) self.setWindowTitle(_("settings.title")) self.setMinimumWidth(450) self.setMinimumHeight(400) self.current_path = current_path self.current_limit = current_limit if current_limit is not None else "" self.current_unit_index = current_unit_index # Apply main app styling self.setStyleSheet( """ QDialog { background-color: #15181b; color: #ffffff; } QWidget { background-color: #15181b; color: #ffffff; } QLabel { color: #ffffff; } QGroupBox { color: #ffffff; border: 2px solid #1b2021; border-radius: 4px; margin-top: 10px; padding-top: 10px; font-weight: bold; background-color: #15181b; } QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 5px 0 5px; color: #ffffff; } QLineEdit { padding: 8px; border: 2px solid #1b2021; border-radius: 4px; background-color: #1b2021; color: #ffffff; selection-background-color: #c90000; selection-color: #ffffff; } QPushButton { padding: 8px 15px; background-color: #c90000; border: none; border-radius: 4px; color: white; font-weight: bold; min-height: 20px; } QPushButton:hover { background-color: #a50000; } QPushButton:pressed { background-color: #800000; } QPushButton:disabled { background-color: #666666; color: #999999; } QCheckBox { spacing: 5px; color: #ffffff; } QCheckBox::indicator { width: 18px; height: 18px; border-radius: 9px; } QCheckBox::indicator:unchecked { border: 2px solid #666666; background: #15181b; } QCheckBox::indicator:checked { border: 2px solid #c90000; background: #c90000; } QRadioButton { spacing: 5px; color: #ffffff; } QRadioButton::indicator { width: 18px; height: 18px; border-radius: 9px; } QRadioButton::indicator:unchecked { border: 2px solid #666666; background: #15181b; } QRadioButton::indicator:checked { border: 2px solid #c90000; background: #c90000; } QComboBox { padding: 5px; border: 2px solid #1b2021; border-radius: 4px; background-color: #1b2021; color: #ffffff; min-height: 20px; } QComboBox::drop-down { border: none; width: 20px; } QComboBox QAbstractItemView { border: 2px solid #1b2021; border-radius: 4px; background-color: #15181b; color: #ffffff; selection-background-color: #c90000; selection-color: #ffffff; } """ ) layout = QVBoxLayout(self) # --- Download Path Section --- path_group_box = QGroupBox(_("settings.download_path")) path_layout = QVBoxLayout() self.path_display = QLabel(str(self.current_path)) self.path_display.setWordWrap(True) self.path_display.setStyleSheet( "QLabel { color: #ffffff; padding: 5px; border: 1px solid #1b2021; border-radius: 4px; background-color: #1b2021; }" ) path_layout.addWidget(self.path_display) browse_button = QPushButton(_("settings.browse")) browse_button.clicked.connect(self.browse_new_path) path_layout.addWidget(browse_button) path_group_box.setLayout(path_layout) layout.addWidget(path_group_box) # --- Speed Limit Section --- speed_group_box = QGroupBox(_("settings.speed_limit")) speed_layout = QHBoxLayout() self.speed_limit_input = QLineEdit(str(self.current_limit)) self.speed_limit_input.setPlaceholderText(_("settings.speed_limit_placeholder")) speed_layout.addWidget(self.speed_limit_input) self.speed_limit_unit = QComboBox() self.speed_limit_unit.addItems(["KB/s", "MB/s"]) self.speed_limit_unit.setCurrentIndex(self.current_unit_index) speed_layout.addWidget(self.speed_limit_unit) speed_group_box.setLayout(speed_layout) layout.addWidget(speed_group_box) # --- Generic Mode Section --- generic_mode_group_box = QGroupBox(_("settings.generic_mode")) generic_mode_layout = QVBoxLayout() self.generic_mode_enabled = ConfigManager.get("generic_mode") or False self.generic_mode_checkbox = QCheckBox(_("settings.enable_generic_mode")) self.generic_mode_checkbox.setChecked(self.generic_mode_enabled) generic_mode_layout.addWidget(self.generic_mode_checkbox) generic_mode_help_label = QLabel(_("settings.generic_mode_help")) generic_mode_help_label.setWordWrap(True) generic_mode_help_label.setStyleSheet("color: #cccccc; margin: 5px; font-size: 11px;") generic_mode_layout.addWidget(generic_mode_help_label) generic_mode_group_box.setLayout(generic_mode_layout) layout.addWidget(generic_mode_group_box) # --- Output Format Settings Section --- output_format_group_box = QGroupBox(_("settings.output_format_settings")) output_format_layout = QVBoxLayout() # Load current format settings from ConfigManager self.force_format_enabled = ConfigManager.get("force_output_format") or False self.preferred_format_value = ConfigManager.get("preferred_output_format") or "mp4" # Enable/Disable force output format checkbox self.force_format_checkbox = QCheckBox(_("settings.force_output_format")) self.force_format_checkbox.setChecked(self.force_format_enabled) output_format_layout.addWidget(self.force_format_checkbox) # Format selection layout format_select_layout = QHBoxLayout() format_label = QLabel(_("settings.preferred_format")) format_label.setStyleSheet("color: #ffffff; margin-top: 5px;") format_select_layout.addWidget(format_label) self.format_combo = QComboBox() self.format_combo.addItems([ _("settings.format_mp4"), _("settings.format_webm"), _("settings.format_mkv") ]) # Set current selection based on saved format format_index_map = {"mp4": 0, "webm": 1, "mkv": 2} self.format_combo.setCurrentIndex(format_index_map.get(self.preferred_format_value, 0)) format_select_layout.addWidget(self.format_combo) format_select_layout.addStretch() output_format_layout.addLayout(format_select_layout) # Help text help_label = QLabel(_("settings.force_format_help")) help_label.setWordWrap(True) help_label.setStyleSheet("color: #cccccc; margin: 5px; font-size: 11px;") output_format_layout.addWidget(help_label) output_format_group_box.setLayout(output_format_layout) layout.addWidget(output_format_group_box) # --- Audio Format Settings Section (for audio-only downloads) --- audio_format_group_box = QGroupBox(_("settings.audio_format_settings")) audio_format_layout = QVBoxLayout() # Load current audio format settings from ConfigManager self.force_audio_format_enabled = ConfigManager.get("force_audio_format") or False self.preferred_audio_format_value = ConfigManager.get("preferred_audio_format") or "best" self.audio_normalization_enabled = ConfigManager.get("audio_normalization") or False # Enable/Disable force audio format checkbox self.force_audio_format_checkbox = QCheckBox(_("settings.force_audio_format")) self.force_audio_format_checkbox.setChecked(self.force_audio_format_enabled) audio_format_layout.addWidget(self.force_audio_format_checkbox) # Enable/Disable audio normalization checkbox self.audio_normalization_checkbox = QCheckBox(_("settings.audio_normalization", default="Audio Normalization")) self.audio_normalization_checkbox.setChecked(self.audio_normalization_enabled) audio_format_layout.addWidget(self.audio_normalization_checkbox) # Audio normalization help text audio_norm_help_label = QLabel(_("settings.audio_normalization_help", default="When enabled, audio will be normalized using EBU R128 standard.")) audio_norm_help_label.setWordWrap(True) audio_norm_help_label.setStyleSheet("color: #cccccc; margin: 5px; font-size: 11px;") audio_format_layout.addWidget(audio_norm_help_label) # Audio format selection layout audio_format_select_layout = QHBoxLayout() audio_format_label = QLabel(_("settings.preferred_audio_format")) audio_format_label.setStyleSheet("color: #ffffff; margin-top: 5px;") audio_format_select_layout.addWidget(audio_format_label) self.audio_format_combo = QComboBox() self.audio_format_combo.addItems([ _("settings.audio_format_best"), _("settings.audio_format_aac"), _("settings.audio_format_mp3"), _("settings.audio_format_flac"), _("settings.audio_format_wav"), _("settings.audio_format_opus"), _("settings.audio_format_m4a"), _("settings.audio_format_vorbis") ]) # Set current selection based on saved format audio_format_index_map = {"best": 0, "aac": 1, "mp3": 2, "flac": 3, "wav": 4, "opus": 5, "m4a": 6, "vorbis": 7} self.audio_format_combo.setCurrentIndex(audio_format_index_map.get(self.preferred_audio_format_value, 0)) audio_format_select_layout.addWidget(self.audio_format_combo) audio_format_select_layout.addStretch() audio_format_layout.addLayout(audio_format_select_layout) # Help text for audio format audio_help_label = QLabel(_("settings.force_audio_format_help")) audio_help_label.setWordWrap(True) audio_help_label.setStyleSheet("color: #cccccc; margin: 5px; font-size: 11px;") audio_format_layout.addWidget(audio_help_label) # Connect signals self.audio_normalization_checkbox.stateChanged.connect(self._on_audio_normalization_toggled) self.force_audio_format_checkbox.stateChanged.connect(self._on_force_audio_format_toggled) audio_format_group_box.setLayout(audio_format_layout) layout.addWidget(audio_format_group_box) # --- Filename Format Section --- filename_format_group_box = QGroupBox(_("settings.filename_format")) filename_layout = QVBoxLayout() # Load current filename format from ConfigManager self.filename_format_value = ConfigManager.get("filename_format") or "%(title)s_%(resolution)s.%(ext)s" # Input and Reset Button Layout filename_input_layout = QHBoxLayout() self.filename_format_input = QLineEdit(self.filename_format_value) self.filename_format_input.setPlaceholderText("%(title)s_%(resolution)s.%(ext)s") filename_input_layout.addWidget(self.filename_format_input) self.reset_format_button = QPushButton(_("buttons.reset")) self.reset_format_button.setFixedWidth(70) self.reset_format_button.clicked.connect(lambda: self.filename_format_input.setText("%(title)s_%(resolution)s.%(ext)s")) filename_input_layout.addWidget(self.reset_format_button) filename_layout.addLayout(filename_input_layout) filename_help_label = QLabel(_("settings.filename_format_help")) filename_help_label.setWordWrap(True) filename_help_label.setStyleSheet("color: #cccccc; margin: 5px; font-size: 11px;") filename_layout.addWidget(filename_help_label) filename_format_group_box.setLayout(filename_layout) layout.addWidget(filename_format_group_box) # Dialog buttons (OK/Cancel) button_box = QDialogButtonBox() ok_button = button_box.addButton(_("buttons.ok"), QDialogButtonBox.ButtonRole.AcceptRole) cancel_button = button_box.addButton(_("buttons.cancel"), QDialogButtonBox.ButtonRole.RejectRole) button_box.accepted.connect(self.accept) button_box.rejected.connect(self.reject) layout.addWidget(button_box) def _on_audio_normalization_toggled(self, state: int) -> None: """Handle logic when audio normalization is toggled.""" if state == Qt.CheckState.Checked.value: # Normalization requires re-encoding, so we must force an audio format self.force_audio_format_checkbox.setChecked(True) # If 'Best (No conversion)' is selected, change it to MP3 to ensure re-encoding if self.audio_format_combo.currentIndex() == 0: self.audio_format_combo.setCurrentIndex(2) # Index 2 is typically MP3 def _on_force_audio_format_toggled(self, state: int) -> None: """Handle logic when force audio format is toggled.""" if state == Qt.CheckState.Unchecked.value: # If re-encoding is disabled, normalization cannot happen self.audio_normalization_checkbox.setChecked(False) def browse_new_path(self) -> None: new_path = QFileDialog.getExistingDirectory(self, _("dialogs.select_folder"), str(self.current_path)) if new_path: self.current_path = new_path self.path_display.setText(self.current_path) def get_selected_path(self) -> str: """Returns the confirmed path after the dialog is accepted.""" return self.current_path def get_selected_speed_limit(self) -> str | None: """Returns the entered speed limit value (as string or None).""" limit_str = self.speed_limit_input.text().strip() if not limit_str: return None try: float(limit_str) # Check if convertible to float return limit_str except ValueError: logger.info("Invalid speed limit input in dialog") return None def get_selected_unit_index(self) -> int: """Returns the index of the selected speed limit unit.""" return self.speed_limit_unit.currentIndex() def get_force_format_enabled(self) -> bool: """Returns whether force output format is enabled.""" return self.force_format_checkbox.isChecked() def get_generic_mode_enabled(self) -> bool: """Returns whether generic mode is enabled.""" return self.generic_mode_checkbox.isChecked() def get_preferred_format(self) -> str: """Returns the selected preferred format (lowercase).""" format_map = {0: "mp4", 1: "webm", 2: "mkv"} return format_map.get(self.format_combo.currentIndex(), "mp4") def get_force_audio_format_enabled(self) -> bool: """Returns whether force audio format is enabled.""" return self.force_audio_format_checkbox.isChecked() def get_preferred_audio_format(self) -> str: """Returns the selected preferred audio format (lowercase).""" audio_format_map = {0: "best", 1: "aac", 2: "mp3", 3: "flac", 4: "wav", 5: "opus", 6: "m4a", 7: "vorbis"} return audio_format_map.get(self.audio_format_combo.currentIndex(), "best") def get_audio_normalization_enabled(self) -> bool: """Returns whether audio normalization is enabled.""" return self.audio_normalization_checkbox.isChecked() def get_filename_format(self) -> str: """Returns the filename format string.""" return self.filename_format_input.text().strip() def _create_styled_message_box(self, icon, title, text) -> QMessageBox: """Create a styled QMessageBox that matches the app theme.""" msg_box = QMessageBox(self) msg_box.setIcon(icon) msg_box.setWindowTitle(title) msg_box.setText(text) msg_box.setWindowIcon(self.windowIcon()) msg_box.setStyleSheet( """ QMessageBox { background-color: #15181b; color: #ffffff; } QMessageBox QLabel { color: #ffffff; } QMessageBox QPushButton { padding: 8px 15px; background-color: #c90000; border: none; border-radius: 4px; color: white; font-weight: bold; min-width: 80px; } QMessageBox QPushButton:hover { background-color: #a50000; } QMessageBox QPushButton:pressed { background-color: #800000; } """ ) return msg_box def accept(self) -> None: """Override accept to save format settings.""" try: ConfigManager.set("generic_mode", self.get_generic_mode_enabled()) # Save output format settings force_format = self.get_force_format_enabled() preferred_format = self.get_preferred_format() ConfigManager.set("force_output_format", force_format) ConfigManager.set("preferred_output_format", preferred_format) # Save audio format settings force_audio_format = self.get_force_audio_format_enabled() preferred_audio_format = self.get_preferred_audio_format() audio_normalization = self.get_audio_normalization_enabled() ConfigManager.set("force_audio_format", force_audio_format) ConfigManager.set("preferred_audio_format", preferred_audio_format) ConfigManager.set("audio_normalization", audio_normalization) # Save filename format filename_format = self.get_filename_format() if filename_format: ConfigManager.set("filename_format", filename_format) QMessageBox.information( self, _("settings.settings_saved_title"), _("settings.settings_saved_message"), ) except Exception as e: QMessageBox.critical(self, _("settings.error_title"), _("settings.error_saving_settings", error=str(e))) # Call the parent accept method to close the dialog super().accept() class AutoUpdateSettingsDialog(QDialog): def __init__(self, parent=None) -> None: super().__init__(parent) self.setWindowTitle(_("settings.auto_update_title")) self.setMinimumWidth(400) self.setMinimumHeight(300) # Set the window icon to match the main app if parent: self.setWindowIcon(parent.windowIcon()) self.init_ui() self.load_current_settings() self.apply_styling() def init_ui(self) -> None: layout = QVBoxLayout(self) # Title title_label = QLabel(f"