""" Selection dialogs for YTSage application. Contains dialogs for selecting subtitles, playlist videos, and SponsorBlock categories. """ from PySide6.QtCore import Qt from PySide6.QtWidgets import ( QCheckBox, QDialog, QDialogButtonBox, QHBoxLayout, QLabel, QLineEdit, QPushButton, QScrollArea, QVBoxLayout, QWidget, ) from src.utils.ytsage_localization import _ class SubtitleSelectionDialog(QDialog): def __init__(self, available_manual, available_auto, previously_selected, parent=None) -> None: super().__init__(parent) self.setWindowTitle(_("dialogs.select_subtitles")) self.setMinimumWidth(400) self.setMinimumHeight(300) self.available_manual = available_manual self.available_auto = available_auto self.previously_selected = set(previously_selected) # Use a set for quick lookups self.selected_subtitles = list(previously_selected) # Initialize with previous selection layout = QVBoxLayout(self) layout.setSpacing(10) # Filter input self.filter_input = QLineEdit() self.filter_input.setPlaceholderText(_("dialogs.filter_languages_placeholder")) self.filter_input.textChanged.connect(self.filter_list) self.filter_input.setStyleSheet( """ QLineEdit { background-color: #363636; border: 2px solid #3d3d3d; border-radius: 4px; padding: 5px; min-height: 30px; color: white; } QLineEdit:focus { border-color: #ff0000; } """ ) layout.addWidget(self.filter_input) # Scroll Area for the list scroll_area = QScrollArea() scroll_area.setWidgetResizable(True) scroll_area.setStyleSheet("QScrollArea { border: none; }") # Remove border around scroll area layout.addWidget(scroll_area) # Container widget for list items (needed for scroll area) self.list_container = QWidget() self.list_layout = QVBoxLayout(self.list_container) self.list_layout.setContentsMargins(0, 0, 0, 0) self.list_layout.setSpacing(2) # Compact spacing self.list_layout.setAlignment(Qt.AlignmentFlag.AlignTop) # Align items to top scroll_area.setWidget(self.list_container) # Populate the list initially self.populate_list() # OK and Cancel buttons 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 for button in button_box.buttons(): button.setStyleSheet( """ QPushButton { background-color: #363636; border: 2px solid #3d3d3d; border-radius: 4px; padding: 5px 15px; /* Adjust padding */ min-height: 30px; /* Ensure consistent height */ color: white; } QPushButton:hover { background-color: #444444; } QPushButton:pressed { background-color: #555555; } """ ) # Style the OK button specifically if needed if button_box.buttonRole(button) == QDialogButtonBox.ButtonRole.AcceptRole: button.setStyleSheet( button.styleSheet() + "QPushButton { background-color: #ff0000; border-color: #cc0000; } QPushButton:hover { background-color: #cc0000; }" ) layout.addWidget(button_box) def populate_list(self, filter_text="") -> None: # Clear existing checkboxes from layout while self.list_layout.count(): item = self.list_layout.takeAt(0) widget = item.widget() if widget is not None: widget.deleteLater() filter_text = filter_text.lower() combined_subs = {} # Add manual subs for lang_code, sub_info in self.available_manual.items(): if not filter_text or filter_text in lang_code.lower(): combined_subs[lang_code] = f"{lang_code} - Manual" # Add auto subs (only if no manual exists and matches filter) for lang_code, sub_info in self.available_auto.items(): if lang_code not in combined_subs: # Don't overwrite manual if not filter_text or filter_text in lang_code.lower(): combined_subs[lang_code] = f"{lang_code} - Auto-generated" if not combined_subs: matching_text = _("dialogs.matching") if filter_text else "" no_subs_label = QLabel(_("dialogs.no_subtitles_available") + (f" {matching_text} '{filter_text}'" if filter_text else "")) no_subs_label.setStyleSheet("color: #aaaaaa; padding: 10px;") self.list_layout.addWidget(no_subs_label) return # Sort by language code sorted_lang_codes = sorted(combined_subs.keys()) for lang_code in sorted_lang_codes: item_text = combined_subs[lang_code] checkbox = QCheckBox(item_text) checkbox.setProperty("subtitle_id", item_text) # Store the identifier checkbox.setChecked(item_text in self.previously_selected) # Check if previously selected checkbox.stateChanged.connect(self.update_selection) checkbox.setStyleSheet( """ QCheckBox { color: #ffffff; padding: 5px; } QCheckBox::indicator { width: 18px; height: 18px; border-radius: 4px; /* Square checkboxes */ } QCheckBox::indicator:unchecked { border: 2px solid #666666; background: #2b2b2b; } QCheckBox::indicator:checked { border: 2px solid #ff0000; background: #ff0000; } """ ) self.list_layout.addWidget(checkbox) self.list_layout.addStretch() # Pushes items up if list is short def filter_list(self) -> None: self.populate_list(self.filter_input.text()) def update_selection(self, state) -> None: sender = self.sender() subtitle_id = sender.property("subtitle_id") if state == Qt.CheckState.Checked.value: if subtitle_id not in self.previously_selected: self.previously_selected.add(subtitle_id) else: if subtitle_id in self.previously_selected: self.previously_selected.remove(subtitle_id) def get_selected_subtitles(self) -> list: # Return the final set as a list return list(self.previously_selected) def accept(self) -> None: # Update the final list before closing self.selected_subtitles = self.get_selected_subtitles() super().accept() class PlaylistSelectionDialog(QDialog): def __init__(self, playlist_entries, previously_selected_string, parent=None) -> None: super().__init__(parent) self.setWindowTitle("Select Playlist Videos") self.setMinimumWidth(500) self.setMinimumHeight(400) # Allow more vertical space self.playlist_entries = playlist_entries self.checkboxes = [] # Main layout main_layout = QVBoxLayout(self) # Top buttons (Select/Deselect All) button_layout = QHBoxLayout() select_all_btn = QPushButton(_("buttons.select_all")) deselect_all_btn = QPushButton(_("buttons.deselect_all")) select_all_btn.clicked.connect(self._select_all) deselect_all_btn.clicked.connect(self._deselect_all) # Style the buttons to match the subtitle dialog select_all_btn.setStyleSheet( """ QPushButton { background-color: #363636; border: 2px solid #3d3d3d; border-radius: 4px; padding: 5px 15px; min-height: 30px; color: white; } QPushButton:hover { background-color: #444444; } QPushButton:pressed { background-color: #555555; } """ ) deselect_all_btn.setStyleSheet(select_all_btn.styleSheet()) button_layout.addWidget(select_all_btn) button_layout.addWidget(deselect_all_btn) button_layout.addStretch() main_layout.addLayout(button_layout) # Scrollable area for checkboxes scroll_area = QScrollArea() scroll_area.setWidgetResizable(True) scroll_area.setStyleSheet("QScrollArea { border: none; }") # Remove border around scroll area scroll_widget = QWidget() self.list_layout = QVBoxLayout(scroll_widget) # Layout for checkboxes self.list_layout.setContentsMargins(0, 0, 0, 0) self.list_layout.setSpacing(2) # Compact spacing self.list_layout.setAlignment(Qt.AlignmentFlag.AlignTop) # Align items to top scroll_area.setWidget(scroll_widget) main_layout.addWidget(scroll_area) # Populate checkboxes self._populate_list(previously_selected_string) # 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 match subtitle dialog for button in button_box.buttons(): button.setStyleSheet( """ QPushButton { background-color: #363636; border: 2px solid #3d3d3d; border-radius: 4px; padding: 5px 15px; min-height: 30px; color: white; } QPushButton:hover { background-color: #444444; } QPushButton:pressed { background-color: #555555; } """ ) # Style the OK button specifically if needed if button_box.buttonRole(button) == QDialogButtonBox.ButtonRole.AcceptRole: button.setStyleSheet( button.styleSheet() + "QPushButton { background-color: #ff0000; border-color: #cc0000; } QPushButton:hover { background-color: #cc0000; }" ) main_layout.addWidget(button_box) # Apply styling to match subtitle dialog self.setStyleSheet( """ QDialog { background-color: #15181b; } QCheckBox { color: #ffffff; padding: 5px; } QCheckBox::indicator { width: 18px; height: 18px; border-radius: 4px; } QCheckBox::indicator:unchecked { border: 2px solid #666666; background: #2b2b2b; } QCheckBox::indicator:checked { border: 2px solid #ff0000; background: #ff0000; } QWidget { background-color: #15181b; } """ ) def _parse_selection_string(self, selection_string) -> set: """Parses a yt-dlp playlist selection string (e.g., '1-3,5,7-9') into a set of 1-based indices.""" selected_indices = set() if not selection_string: # If no previous selection, assume all are selected initially return set(range(1, len(self.playlist_entries) + 1)) parts = selection_string.split(",") for part in parts: part = part.strip() if "-" in part: try: start, end = map(int, part.split("-")) if start <= end: selected_indices.update(range(start, end + 1)) except ValueError: pass # Ignore invalid ranges else: try: selected_indices.add(int(part)) except ValueError: pass # Ignore invalid numbers return selected_indices def _populate_list(self, previously_selected_string) -> None: """Populates the scroll area with checkboxes for each video.""" selected_indices = self._parse_selection_string(previously_selected_string) # Clear existing checkboxes if any (e.g., if repopulating) while self.list_layout.count(): child = self.list_layout.takeAt(0) if child.widget(): child.widget().deleteLater() self.checkboxes.clear() for index, entry in enumerate(self.playlist_entries): if not entry: continue # Skip None entries if yt-dlp returns them video_index = index + 1 # yt-dlp uses 1-based indexing title = entry.get("title", f"Video {video_index}") # Shorten title if too long display_title = (title[:70] + "...") if len(title) > 73 else title checkbox = QCheckBox(f"{video_index}. {display_title}") checkbox.setChecked(video_index in selected_indices) checkbox.setProperty("video_index", video_index) # Store index checkbox.setStyleSheet( """ QCheckBox { color: #ffffff; padding: 5px; } QCheckBox::indicator { width: 18px; height: 18px; border-radius: 4px; } QCheckBox::indicator:unchecked { border: 2px solid #666666; background: #2b2b2b; } QCheckBox::indicator:checked { border: 2px solid #ff0000; background: #ff0000; } """ ) self.list_layout.addWidget(checkbox) self.checkboxes.append(checkbox) self.list_layout.addStretch() # Push checkboxes to the top def _select_all(self) -> None: for checkbox in self.checkboxes: checkbox.setChecked(True) def _deselect_all(self) -> None: for checkbox in self.checkboxes: checkbox.setChecked(False) def _condense_indices(self, indices: list[int]) -> str: """Condenses a list of 1-based indices into a yt-dlp selection string.""" if not indices: return "" # Remove duplicates and sort in one step indices = sorted(set(indices)) ranges = [] start = end = indices[0] for num in indices[1:]: if num == end + 1: end = num else: ranges.append(f"{start}-{end}" if start != end else str(start)) start = end = num # Append the last range ranges.append(f"{start}-{end}" if start != end else str(start)) return ",".join(ranges) def get_selected_items_string(self) -> str | None: """Returns the selection string based on checked boxes.""" selected_indices = [cb.property("video_index") for cb in self.checkboxes if cb.isChecked()] # Check if all items are selected if len(selected_indices) == len(self.playlist_entries): return None # yt-dlp default is all items, so return None or empty string return self._condense_indices(selected_indices) class SponsorBlockCategoryDialog(QDialog): """Dialog for selecting SponsorBlock categories to remove from videos.""" # Default SponsorBlock categories with descriptions SPONSORBLOCK_CATEGORIES = { "sponsor": { "name_key": "sponsorblock.sponsor", "description_key": "sponsorblock.sponsor_desc", "default": True, }, "selfpromo": { "name_key": "sponsorblock.selfpromo", "description_key": "sponsorblock.selfpromo_desc", "default": True, }, "interaction": { "name_key": "sponsorblock.interaction", "description_key": "sponsorblock.interaction_desc", "default": True, }, "intro": { "name_key": "sponsorblock.intro", "description_key": "sponsorblock.intro_desc", "default": False, }, "outro": { "name_key": "sponsorblock.outro", "description_key": "sponsorblock.outro_desc", "default": False, }, "preview": { "name_key": "sponsorblock.preview", "description_key": "sponsorblock.preview_desc", "default": False, }, "music_offtopic": { "name_key": "sponsorblock.music_offtopic", "description_key": "sponsorblock.music_offtopic_desc", "default": False, }, "filler": { "name_key": "sponsorblock.filler", "description_key": "sponsorblock.filler_desc", "default": False, }, } def __init__(self, previously_selected=None, parent=None) -> None: super().__init__(parent) self.setWindowTitle(_("dialogs.sponsorblock_categories")) self.setMinimumWidth(500) self.setMinimumHeight(400) # Set the window icon to match the main app if parent: self.setWindowIcon(parent.windowIcon()) self.previously_selected = set(previously_selected) if previously_selected else set() self.checkboxes = {} self.init_ui() self.apply_styling() def init_ui(self) -> None: layout = QVBoxLayout(self) # Title and description title_label = QLabel(_("dialogs.sponsorblock_categories")) title_label.setAlignment(Qt.AlignmentFlag.AlignCenter) title_label.setStyleSheet("font-size: 16px; font-weight: bold; color: #ffffff; margin: 10px;") layout.addWidget(title_label) desc_label = QLabel(_("dialogs.sponsorblock_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) # Scroll area for categories scroll_area = QScrollArea() scroll_area.setWidgetResizable(True) scroll_area.setStyleSheet("QScrollArea { border: none; }") scroll_widget = QWidget() scroll_layout = QVBoxLayout(scroll_widget) scroll_layout.setContentsMargins(10, 0, 10, 0) scroll_layout.setSpacing(8) # Add category checkboxes for category_id, category_info in self.SPONSORBLOCK_CATEGORIES.items(): # Create a container widget for each category category_widget = QWidget() category_layout = QVBoxLayout(category_widget) category_layout.setContentsMargins(0, 0, 0, 0) category_layout.setSpacing(2) # Create checkbox with localized name checkbox = QCheckBox(_(category_info["name_key"])) checkbox.setProperty("category_id", category_id) # Determine if this category should be checked if self.previously_selected: # Use previously selected categories is_checked = category_id in self.previously_selected else: # Use default values for first time is_checked = category_info["default"] checkbox.setChecked(is_checked) checkbox.setStyleSheet( """ QCheckBox { color: #ffffff; padding: 4px; spacing: 10px; font-weight: bold; } QCheckBox::indicator { width: 18px; height: 18px; border-radius: 4px; } QCheckBox::indicator:unchecked { border: 2px solid #666666; background: #2b2b2b; } QCheckBox::indicator:checked { border: 2px solid #ff0000; background: #ff0000; } """ ) # Create description label with localized text desc_label = QLabel(_(category_info["description_key"])) desc_label.setStyleSheet("color: #aaaaaa; font-size: 11px; margin-left: 28px; margin-bottom: 8px;") desc_label.setWordWrap(True) category_layout.addWidget(checkbox) category_layout.addWidget(desc_label) self.checkboxes[category_id] = checkbox scroll_layout.addWidget(category_widget) scroll_layout.addStretch() scroll_area.setWidget(scroll_widget) layout.addWidget(scroll_area) # Quick selection buttons button_layout = QHBoxLayout() select_defaults_btn = QPushButton(_("buttons.select_defaults")) select_defaults_btn.clicked.connect(self.select_defaults) select_defaults_btn.setStyleSheet(self._get_button_style()) select_all_btn = QPushButton(_("buttons.select_all")) select_all_btn.clicked.connect(self.select_all) select_all_btn.setStyleSheet(self._get_button_style()) deselect_all_btn = QPushButton(_("buttons.deselect_all")) deselect_all_btn.clicked.connect(self.deselect_all) deselect_all_btn.setStyleSheet(self._get_button_style()) button_layout.addWidget(select_defaults_btn) button_layout.addWidget(select_all_btn) button_layout.addWidget(deselect_all_btn) button_layout.addStretch() layout.addLayout(button_layout) # Dialog buttons 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 dialog buttons for button in button_box.buttons(): button.setStyleSheet(self._get_button_style()) if button_box.buttonRole(button) == QDialogButtonBox.ButtonRole.AcceptRole: button.setStyleSheet( button.styleSheet() + "QPushButton { background-color: #ff0000; border-color: #cc0000; } " + "QPushButton:hover { background-color: #cc0000; }" ) layout.addWidget(button_box) def _get_button_style(self) -> str: """Returns the standard button style for this dialog.""" return """ QPushButton { background-color: #363636; border: 2px solid #3d3d3d; border-radius: 4px; padding: 5px 15px; min-height: 30px; color: white; } QPushButton:hover { background-color: #444444; } QPushButton:pressed { background-color: #555555; } """ def apply_styling(self) -> None: """Apply the dialog styling to match the rest of the application.""" self.setStyleSheet( """ QDialog { background-color: #15181b; color: #ffffff; } QLabel { color: #ffffff; } QWidget { background-color: #15181b; } """ ) def select_defaults(self) -> None: """Select only the default categories.""" for category_id, checkbox in self.checkboxes.items(): default_value = self.SPONSORBLOCK_CATEGORIES[category_id]["default"] checkbox.setChecked(default_value) def select_all(self) -> None: """Select all categories.""" for checkbox in self.checkboxes.values(): checkbox.setChecked(True) def deselect_all(self) -> None: """Deselect all categories.""" for checkbox in self.checkboxes.values(): checkbox.setChecked(False) def get_selected_categories(self) -> list: """Returns a list of selected category IDs.""" selected = [] for category_id, checkbox in self.checkboxes.items(): if checkbox.isChecked(): selected.append(category_id) return selected def get_selected_categories_string(self) -> str: """Returns a comma-separated string of selected categories for yt-dlp.""" selected = self.get_selected_categories() return ",".join(selected) if selected else ""