fda6a4059d
Add a "Concurrent Connections" section to DownloadSettingsDialog with a QComboBox (1–20) and help text; initialize from ConfigManager("concurrent_fragments"). Introduce get_concurrent_fragments() to return the chosen value (with a safe fallback to 1) and persist the setting on dialog accept. Uses localized labels and minor styling for the help label.
950 lines
36 KiB
Python
950 lines
36 KiB
Python
"""
|
|
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,
|
|
QWidget,
|
|
)
|
|
|
|
from ..ytsage_smooth_tab_widget import SmoothTabWidget
|
|
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(550)
|
|
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;
|
|
}
|
|
QFrame#tabContent {
|
|
border: 1px solid #3d3d3d;
|
|
background-color: #15181b;
|
|
}
|
|
QTabBar::tab {
|
|
background-color: #1d1e22;
|
|
color: #ffffff;
|
|
padding: 8px 12px;
|
|
border: 1px solid #3d3d3d;
|
|
border-bottom: none;
|
|
border-top-left-radius: 4px;
|
|
border-top-right-radius: 4px;
|
|
}
|
|
QTabBar::tab:selected {
|
|
background-color: #c90000;
|
|
}
|
|
QTabBar::tab:hover:!selected {
|
|
background-color: #2a2d36;
|
|
}
|
|
QWidget {
|
|
background-color: #15181b;
|
|
color: #ffffff;
|
|
}
|
|
QLabel {
|
|
color: #ffffff;
|
|
}
|
|
QGroupBox {
|
|
border: 1px solid #3d3d3d;
|
|
border-radius: 4px;
|
|
margin-top: 1.5ex;
|
|
color: #ffffff;
|
|
padding: 10px;
|
|
font-weight: bold;
|
|
}
|
|
QGroupBox::title {
|
|
subcontrol-origin: margin;
|
|
subcontrol-position: top left;
|
|
padding: 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: #1d1e22;
|
|
border-radius: 9px;
|
|
}
|
|
QCheckBox::indicator:checked {
|
|
border: 2px solid #c90000;
|
|
background: #c90000;
|
|
border-radius: 9px;
|
|
}
|
|
QRadioButton {
|
|
spacing: 5px;
|
|
color: #ffffff;
|
|
}
|
|
QRadioButton::indicator {
|
|
width: 18px;
|
|
height: 18px;
|
|
border-radius: 9px;
|
|
}
|
|
QRadioButton::indicator:unchecked {
|
|
border: 2px solid #666666;
|
|
background: #1d1e22;
|
|
border-radius: 9px;
|
|
}
|
|
QRadioButton::indicator:checked {
|
|
border: 2px solid #c90000;
|
|
background: #c90000;
|
|
border-radius: 9px;
|
|
}
|
|
QComboBox {
|
|
padding: 8px;
|
|
border: 2px solid #1b2021;
|
|
border-radius: 4px;
|
|
background-color: #1b2021;
|
|
color: #ffffff;
|
|
min-width: 150px;
|
|
}
|
|
QComboBox::drop-down {
|
|
border: none;
|
|
width: 20px;
|
|
}
|
|
QComboBox::down-arrow {
|
|
border: none;
|
|
width: 12px;
|
|
height: 12px;
|
|
}
|
|
QComboBox QAbstractItemView {
|
|
background-color: #1d1e22;
|
|
color: #ffffff;
|
|
border: 1px solid #3d3d3d;
|
|
selection-background-color: #c90000;
|
|
}
|
|
"""
|
|
)
|
|
|
|
layout = QVBoxLayout(self)
|
|
|
|
# Create tab widget
|
|
self.tab_widget = SmoothTabWidget()
|
|
layout.addWidget(self.tab_widget)
|
|
|
|
# === General Tab ===
|
|
general_tab = QWidget()
|
|
general_layout = QVBoxLayout(general_tab)
|
|
|
|
# --- 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)
|
|
general_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)
|
|
general_layout.addWidget(speed_group_box)
|
|
|
|
# --- Connections Section ---
|
|
connections_group_box = QGroupBox(_("settings.concurrent_fragments", default="Concurrent Connections"))
|
|
connections_layout = QVBoxLayout()
|
|
|
|
self.connections_enabled = ConfigManager.get("concurrent_fragments") or 1
|
|
|
|
connections_spin_layout = QHBoxLayout()
|
|
self.connections_input = QComboBox()
|
|
self.connections_input.addItems([str(i) for i in range(1, 21)])
|
|
self.connections_input.setCurrentText(str(self.connections_enabled))
|
|
connections_spin_layout.addWidget(self.connections_input)
|
|
connections_spin_layout.addStretch()
|
|
connections_layout.addLayout(connections_spin_layout)
|
|
|
|
connections_help_label = QLabel(_("settings.concurrent_fragments_help", default="Number of connections per download. Higher values bypass throttling but may cause temporary blocks if set too high. Default: 1."))
|
|
connections_help_label.setWordWrap(True)
|
|
connections_help_label.setStyleSheet("color: #cccccc; margin: 5px; font-size: 11px;")
|
|
connections_layout.addWidget(connections_help_label)
|
|
|
|
connections_group_box.setLayout(connections_layout)
|
|
general_layout.addWidget(connections_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)
|
|
general_layout.addWidget(generic_mode_group_box)
|
|
general_layout.addStretch()
|
|
|
|
# === Format Tab ===
|
|
format_tab = QWidget()
|
|
format_layout = QVBoxLayout(format_tab)
|
|
|
|
# --- 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)
|
|
format_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)
|
|
format_layout.addWidget(audio_format_group_box)
|
|
format_layout.addStretch()
|
|
|
|
# === File Tab ===
|
|
file_tab = QWidget()
|
|
file_layout = QVBoxLayout(file_tab)
|
|
|
|
# --- 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)
|
|
file_layout.addWidget(filename_format_group_box)
|
|
file_layout.addStretch()
|
|
|
|
# Add tabs to tab widget
|
|
self.tab_widget.addTab(general_tab, _("settings.tab_general", default="General"))
|
|
self.tab_widget.addTab(format_tab, _("settings.tab_format", default="Format"))
|
|
self.tab_widget.addTab(file_tab, _("settings.tab_file", default="File"))
|
|
|
|
# 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)
|
|
|
|
# Style the buttons to look identical to Custom Options dialog
|
|
for btn in [ok_button, cancel_button]:
|
|
btn.setMinimumHeight(35)
|
|
btn.setMinimumWidth(80)
|
|
btn.setStyleSheet(
|
|
"""
|
|
QPushButton {
|
|
padding: 8px 20px;
|
|
background-color: #c90000;
|
|
border: none;
|
|
border-radius: 4px;
|
|
color: white;
|
|
font-weight: bold;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #a50000;
|
|
}
|
|
"""
|
|
)
|
|
|
|
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_concurrent_fragments(self) -> int:
|
|
"""Returns the number of concurrent fragments."""
|
|
try:
|
|
return int(self.connections_input.currentText())
|
|
except ValueError:
|
|
return 1
|
|
|
|
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())
|
|
ConfigManager.set("concurrent_fragments", self.get_concurrent_fragments())
|
|
|
|
# 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"<h2>{_("settings.auto_update_header")}</h2>")
|
|
title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
layout.addWidget(title_label)
|
|
|
|
# Description
|
|
desc_label = QLabel(_("settings.auto_update_description"))
|
|
desc_label.setWordWrap(True)
|
|
desc_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
desc_label.setStyleSheet("color: #cccccc; margin: 10px; font-size: 11px;")
|
|
layout.addWidget(desc_label)
|
|
|
|
# Enable/Disable auto-update
|
|
self.enable_checkbox = QCheckBox(_("settings.enable_auto_updates"))
|
|
self.enable_checkbox.setChecked(True) # Default enabled
|
|
self.enable_checkbox.toggled.connect(self.on_enable_toggled)
|
|
layout.addWidget(self.enable_checkbox)
|
|
|
|
# Frequency options
|
|
frequency_group = QGroupBox(_("settings.update_frequency_group"))
|
|
frequency_layout = QVBoxLayout()
|
|
|
|
self.frequency_group = QButtonGroup(self)
|
|
|
|
self.startup_radio = QRadioButton(_("settings.check_startup"))
|
|
self.daily_radio = QRadioButton(_("settings.check_daily"))
|
|
self.weekly_radio = QRadioButton(_("settings.check_weekly"))
|
|
|
|
self.daily_radio.setChecked(True) # Default to daily
|
|
|
|
self.frequency_group.addButton(self.startup_radio, 0)
|
|
self.frequency_group.addButton(self.daily_radio, 1)
|
|
self.frequency_group.addButton(self.weekly_radio, 2)
|
|
|
|
frequency_layout.addWidget(self.startup_radio)
|
|
frequency_layout.addWidget(self.daily_radio)
|
|
frequency_layout.addWidget(self.weekly_radio)
|
|
frequency_group.setLayout(frequency_layout)
|
|
|
|
layout.addWidget(frequency_group)
|
|
|
|
# Current status
|
|
status_group = QGroupBox(_("settings.current_status"))
|
|
status_layout = QVBoxLayout()
|
|
|
|
self.current_version_label = QLabel(_("settings.current_version_label"))
|
|
self.last_check_label = QLabel(_("settings.last_check_label"))
|
|
self.next_check_label = QLabel(_("settings.next_check_label"))
|
|
|
|
status_layout.addWidget(self.current_version_label)
|
|
status_layout.addWidget(self.last_check_label)
|
|
status_layout.addWidget(self.next_check_label)
|
|
status_group.setLayout(status_layout)
|
|
|
|
layout.addWidget(status_group)
|
|
|
|
# Manual check button
|
|
self.manual_check_btn = QPushButton(_("settings.manual_check_button"))
|
|
self.manual_check_btn.clicked.connect(self.manual_check)
|
|
layout.addWidget(self.manual_check_btn)
|
|
|
|
# Buttons
|
|
button_layout = QHBoxLayout()
|
|
|
|
self.save_btn = QPushButton(_("settings.save_settings"))
|
|
self.save_btn.clicked.connect(self.save_settings)
|
|
|
|
self.cancel_btn = QPushButton(_("buttons.cancel"))
|
|
self.cancel_btn.clicked.connect(self.reject)
|
|
|
|
button_layout.addWidget(self.save_btn)
|
|
button_layout.addWidget(self.cancel_btn)
|
|
|
|
layout.addLayout(button_layout)
|
|
|
|
def apply_styling(self) -> None:
|
|
self.setStyleSheet(
|
|
"""
|
|
QDialog {
|
|
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;
|
|
}
|
|
QCheckBox, QRadioButton {
|
|
color: #ffffff;
|
|
spacing: 5px;
|
|
margin: 5px;
|
|
}
|
|
QCheckBox::indicator, QRadioButton::indicator {
|
|
width: 18px;
|
|
height: 18px;
|
|
border-radius: 9px;
|
|
}
|
|
QCheckBox::indicator:unchecked, QRadioButton::indicator:unchecked {
|
|
border: 2px solid #666666;
|
|
background: #15181b;
|
|
}
|
|
QCheckBox::indicator:checked, QRadioButton::indicator:checked {
|
|
border: 2px solid #c90000;
|
|
background: #c90000;
|
|
}
|
|
QPushButton {
|
|
padding: 8px 15px;
|
|
background-color: #c90000;
|
|
border: none;
|
|
border-radius: 4px;
|
|
color: white;
|
|
font-weight: bold;
|
|
margin: 5px;
|
|
min-width: 100px;
|
|
min-height: 20px;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #a50000;
|
|
}
|
|
QPushButton:pressed {
|
|
background-color: #800000;
|
|
}
|
|
QPushButton:disabled {
|
|
background-color: #666666;
|
|
color: #999999;
|
|
}
|
|
"""
|
|
)
|
|
|
|
def load_current_settings(self) -> None:
|
|
"""Load current auto-update settings from config."""
|
|
try:
|
|
settings = get_auto_update_settings()
|
|
|
|
# Set checkbox
|
|
self.enable_checkbox.setChecked(settings["enabled"])
|
|
|
|
# Set frequency
|
|
frequency = settings["frequency"]
|
|
if frequency == "startup":
|
|
self.startup_radio.setChecked(True)
|
|
elif frequency == "weekly":
|
|
self.weekly_radio.setChecked(True)
|
|
else: # daily
|
|
self.daily_radio.setChecked(True)
|
|
|
|
# Update status labels
|
|
current_version = get_ytdlp_version()
|
|
self.current_version_label.setText(_("auto_update.current_version", version=current_version))
|
|
|
|
last_check = settings["last_check"]
|
|
if last_check > 0:
|
|
last_check_time = datetime.fromtimestamp(last_check).strftime("%Y-%m-%d %H:%M:%S")
|
|
self.last_check_label.setText(_("auto_update.last_check", time=last_check_time))
|
|
else:
|
|
self.last_check_label.setText(_("auto_update.last_check_never"))
|
|
|
|
# Calculate next check time
|
|
self.update_next_check_label()
|
|
|
|
# Update UI state
|
|
self.on_enable_toggled(settings["enabled"])
|
|
|
|
except Exception as e:
|
|
logger.exception(f"Error loading auto-update settings: {e}")
|
|
|
|
def update_next_check_label(self) -> None:
|
|
"""Update the next check label based on current settings."""
|
|
try:
|
|
if not self.enable_checkbox.isChecked():
|
|
self.next_check_label.setText(_("auto_update.next_check_disabled"))
|
|
return
|
|
|
|
settings = get_auto_update_settings()
|
|
last_check = settings["last_check"]
|
|
frequency = self.get_selected_frequency()
|
|
|
|
if last_check == 0:
|
|
self.next_check_label.setText(_("auto_update.next_check_startup"))
|
|
return
|
|
|
|
next_check_time = last_check
|
|
if frequency == "startup":
|
|
next_check_time += 3600 # 1 hour
|
|
elif frequency == "daily":
|
|
next_check_time += 86400 # 24 hours
|
|
elif frequency == "weekly":
|
|
next_check_time += 604800 # 7 days
|
|
|
|
current_time = time.time()
|
|
if next_check_time <= current_time:
|
|
self.next_check_label.setText(_("auto_update.next_check_overdue"))
|
|
else:
|
|
next_check_datetime = datetime.fromtimestamp(next_check_time)
|
|
self.next_check_label.setText(_("auto_update.next_check", time=next_check_datetime.strftime('%Y-%m-%d %H:%M:%S')))
|
|
|
|
except Exception as e:
|
|
self.next_check_label.setText(_("auto_update.next_check_error"))
|
|
logger.exception(f"Error calculating next check time: {e}")
|
|
|
|
def on_enable_toggled(self, enabled) -> None:
|
|
"""Handle enable/disable checkbox toggle."""
|
|
# Enable/disable frequency options
|
|
for i in range(self.frequency_group.buttons().__len__()):
|
|
self.frequency_group.button(i).setEnabled(enabled)
|
|
|
|
self.update_next_check_label()
|
|
|
|
def get_selected_frequency(self) -> str:
|
|
"""Get the selected frequency setting."""
|
|
if self.startup_radio.isChecked():
|
|
return "startup"
|
|
elif self.weekly_radio.isChecked():
|
|
return "weekly"
|
|
else:
|
|
return "daily"
|
|
|
|
def manual_check(self) -> None:
|
|
"""Perform a manual update check."""
|
|
self.manual_check_btn.setEnabled(False)
|
|
self.manual_check_btn.setText(_("auto_update.checking"))
|
|
|
|
# Force an immediate update check
|
|
def check_in_thread() -> None:
|
|
try:
|
|
result = check_and_update_ytdlp_auto()
|
|
|
|
# Update UI in main thread
|
|
QTimer.singleShot(0, lambda: self.manual_check_finished(result))
|
|
except Exception as e:
|
|
logger.exception(f"Error during manual check: {e}")
|
|
QTimer.singleShot(0, lambda: self.manual_check_finished(False))
|
|
|
|
# Run in separate thread to avoid blocking UI
|
|
threading.Thread(target=check_in_thread, daemon=True).start()
|
|
|
|
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 manual_check_finished(self, success) -> None:
|
|
"""Handle completion of manual update check."""
|
|
self.manual_check_btn.setEnabled(True)
|
|
self.manual_check_btn.setText(_("auto_update.check_now"))
|
|
|
|
if success:
|
|
msg_box = self._create_styled_message_box(
|
|
QMessageBox.Icon.Information,
|
|
"Update Check",
|
|
"✅ Update check completed successfully!\nCheck the console for details.",
|
|
)
|
|
msg_box.exec()
|
|
else:
|
|
msg_box = self._create_styled_message_box(
|
|
QMessageBox.Icon.Warning,
|
|
"Update Check",
|
|
"❌ Update check failed.\nCheck the console for error details.",
|
|
)
|
|
msg_box.exec()
|
|
|
|
# Refresh the current settings display
|
|
self.load_current_settings()
|
|
|
|
def save_settings(self) -> None:
|
|
"""Save the auto-update settings."""
|
|
try:
|
|
enabled = self.enable_checkbox.isChecked()
|
|
frequency = self.get_selected_frequency()
|
|
|
|
if update_auto_update_settings(enabled, frequency):
|
|
msg_box = self._create_styled_message_box(
|
|
QMessageBox.Icon.Information,
|
|
_("settings.settings_saved_title"),
|
|
_("settings.settings_saved_successfully"),
|
|
)
|
|
msg_box.exec()
|
|
self.accept()
|
|
else:
|
|
msg_box = self._create_styled_message_box(
|
|
QMessageBox.Icon.Warning,
|
|
_("settings.error_title"),
|
|
_("settings.failed_save_settings"),
|
|
)
|
|
msg_box.exec()
|
|
except Exception as e:
|
|
logger.exception(f"Error saving auto-update settings: {e}")
|
|
msg_box = self._create_styled_message_box(QMessageBox.Icon.Critical, _("settings.error_title"), _("settings", "error_saving", error=str(e)))
|
|
msg_box.exec()
|