""" Soundpack Manager Dialog - UI for managing soundpack repositories and installations """ from PySide6.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QTabWidget, QWidget, QListWidget, QListWidgetItem, QPushButton, QLabel, QLineEdit, QTextEdit, QGroupBox, QMessageBox, QProgressDialog, QDialogButtonBox, QFormLayout, QCheckBox, QSplitter ) from PySide6.QtCore import Qt, QThread, Signal, QTimer from PySide6.QtGui import QFont from typing import List, Optional from config.settings import SettingsManager from audio.soundpack_manager import SoundpackManager, SoundpackRepository, SoundpackInfo class SoundpackOperationThread(QThread): """Thread for soundpack operations to prevent UI blocking""" operation_complete = Signal(bool, str) # success, message progress_update = Signal(str) # status message def __init__(self, operation, *args): super().__init__() self.operation = operation self.args = args def run(self): try: if self.operation == 'discover': self.discover_soundpacks() elif self.operation == 'install': self.install_soundpack() elif self.operation == 'validate_repo': self.validate_repository() except Exception as e: self.operation_complete.emit(False, f"Operation failed: {str(e)}") def discover_soundpacks(self): manager = self.args[0] self.progress_update.emit("Discovering soundpacks...") soundpacks = manager.discover_soundpacks() self.operation_complete.emit(True, f"Found {len(soundpacks)} soundpacks") def install_soundpack(self): manager, soundpack = self.args self.progress_update.emit(f"Installing {soundpack.name}...") success, message = manager.install_soundpack(soundpack) self.operation_complete.emit(success, message) def validate_repository(self): manager, url = self.args self.progress_update.emit("Validating repository...") success, message = manager.validate_repository(url) self.operation_complete.emit(success, message) class SoundpackManagerDialog(QDialog): """Dialog for managing soundpacks and repositories""" def __init__(self, settings: SettingsManager, parent=None): super().__init__(parent) self.settings = settings self.manager = SoundpackManager(settings) self.soundpacks = [] self.current_thread = None self.setWindowTitle("Soundpack Manager") self.setMinimumSize(800, 600) self.setModal(True) self.setup_ui() self.load_data() def setup_ui(self): """Initialize the user interface""" layout = QVBoxLayout(self) # Create tab widget self.tab_widget = QTabWidget() layout.addWidget(self.tab_widget) # Soundpacks tab self.setup_soundpacks_tab() # Repositories tab self.setup_repositories_tab() # Installed tab self.setup_installed_tab() # Button box button_box = QDialogButtonBox(QDialogButtonBox.Close) button_box.rejected.connect(self.close) layout.addWidget(button_box) def setup_soundpacks_tab(self): """Setup the available soundpacks tab""" tab = QWidget() layout = QVBoxLayout(tab) # Header header_label = QLabel("Available Soundpacks") header_label.setFont(QFont("", 12, QFont.Bold)) header_label.setAccessibleName("Available Soundpacks") layout.addWidget(header_label) # Refresh button refresh_layout = QHBoxLayout() self.refresh_button = QPushButton("&Refresh Soundpack List") self.refresh_button.setAccessibleName("Refresh soundpack list from repositories") self.refresh_button.clicked.connect(self.refresh_soundpacks) refresh_layout.addWidget(self.refresh_button) refresh_layout.addStretch() layout.addLayout(refresh_layout) # Splitter for list and details splitter = QSplitter(Qt.Horizontal) layout.addWidget(splitter) # Soundpack list self.soundpack_list = QListWidget() self.soundpack_list.setAccessibleName("Available soundpacks") self.soundpack_list.itemSelectionChanged.connect(self.on_soundpack_selected) splitter.addWidget(self.soundpack_list) # Details panel details_widget = QWidget() details_layout = QVBoxLayout(details_widget) self.details_label = QLabel("Select a soundpack to view details") self.details_label.setAccessibleName("Soundpack details") self.details_label.setWordWrap(True) details_layout.addWidget(self.details_label) self.description_text = QTextEdit() self.description_text.setAccessibleName("Soundpack description") self.description_text.setReadOnly(True) self.description_text.setMaximumHeight(150) details_layout.addWidget(self.description_text) # Action buttons action_layout = QHBoxLayout() self.install_button = QPushButton("&Install") self.install_button.setAccessibleName("Install selected soundpack") self.install_button.clicked.connect(self.install_selected) self.install_button.setEnabled(False) action_layout.addWidget(self.install_button) self.switch_button = QPushButton("&Switch To") self.switch_button.setAccessibleName("Switch to selected soundpack") self.switch_button.clicked.connect(self.switch_to_selected) self.switch_button.setEnabled(False) action_layout.addWidget(self.switch_button) action_layout.addStretch() details_layout.addLayout(action_layout) details_layout.addStretch() splitter.addWidget(details_widget) # Set splitter proportions splitter.setSizes([400, 400]) self.tab_widget.addTab(tab, "&Available") def setup_repositories_tab(self): """Setup the repositories management tab""" tab = QWidget() layout = QVBoxLayout(tab) # Header header_label = QLabel("Soundpack Repositories") header_label.setFont(QFont("", 12, QFont.Bold)) header_label.setAccessibleName("Soundpack repositories") layout.addWidget(header_label) # Repository list self.repo_list = QListWidget() self.repo_list.setAccessibleName("Repository list") self.repo_list.itemSelectionChanged.connect(self.on_repository_selected) layout.addWidget(self.repo_list) # Repository management buttons repo_button_layout = QHBoxLayout() self.add_repo_button = QPushButton("&Add Repository") self.add_repo_button.setAccessibleName("Add new repository") self.add_repo_button.clicked.connect(self.add_repository) repo_button_layout.addWidget(self.add_repo_button) self.remove_repo_button = QPushButton("&Remove Repository") self.remove_repo_button.setAccessibleName("Remove selected repository") self.remove_repo_button.clicked.connect(self.remove_repository) self.remove_repo_button.setEnabled(False) repo_button_layout.addWidget(self.remove_repo_button) repo_button_layout.addStretch() layout.addLayout(repo_button_layout) self.tab_widget.addTab(tab, "&Repositories") def setup_installed_tab(self): """Setup the installed soundpacks tab""" tab = QWidget() layout = QVBoxLayout(tab) # Header header_label = QLabel("Installed Soundpacks") header_label.setFont(QFont("", 12, QFont.Bold)) header_label.setAccessibleName("Installed soundpacks") layout.addWidget(header_label) # Current soundpack info current_group = QGroupBox("Current Soundpack") current_layout = QVBoxLayout(current_group) self.current_pack_label = QLabel("Loading...") self.current_pack_label.setAccessibleName("Current soundpack") current_layout.addWidget(self.current_pack_label) layout.addWidget(current_group) # Installed soundpacks list self.installed_list = QListWidget() self.installed_list.setAccessibleName("Installed soundpacks") self.installed_list.itemSelectionChanged.connect(self.on_installed_selected) layout.addWidget(self.installed_list) # Management buttons installed_button_layout = QHBoxLayout() self.switch_installed_button = QPushButton("&Switch To") self.switch_installed_button.setAccessibleName("Switch to selected installed soundpack") self.switch_installed_button.clicked.connect(self.switch_to_installed) self.switch_installed_button.setEnabled(False) installed_button_layout.addWidget(self.switch_installed_button) self.uninstall_button = QPushButton("&Uninstall") self.uninstall_button.setAccessibleName("Uninstall selected soundpack") self.uninstall_button.clicked.connect(self.uninstall_selected) self.uninstall_button.setEnabled(False) installed_button_layout.addWidget(self.uninstall_button) installed_button_layout.addStretch() layout.addLayout(installed_button_layout) self.tab_widget.addTab(tab, "&Installed") def load_data(self): """Load initial data""" self.load_repositories() self.load_installed_soundpacks() self.update_current_soundpack() self.refresh_soundpacks() def load_repositories(self): """Load repository list""" self.repo_list.clear() repositories = self.manager.get_repositories() # Add header item to help Orca read single-item lists header_item = QListWidgetItem(f"Repositories ({len(repositories)} configured):") header_item.setFlags(header_item.flags() & ~Qt.ItemIsSelectable) # Make it non-selectable header_item.setData(Qt.AccessibleDescriptionRole, "Repository list header") self.repo_list.addItem(header_item) for repo in repositories: # Format for screen readers - use single line with clear separators item_text = f"{repo.description} - {repo.url}" if not repo.enabled: item_text += " (Disabled)" item = QListWidgetItem(item_text) item.setData(Qt.UserRole, repo) # Set accessible description with more detail item.setData(Qt.AccessibleDescriptionRole, f"Repository: {repo.description}, URL: {repo.url}") self.repo_list.addItem(item) def load_installed_soundpacks(self): """Load installed soundpacks list""" self.installed_list.clear() installed = self.manager._get_installed_soundpacks() current_pack = self.settings.get('audio', 'sound_pack', 'default') for pack_name in installed: item_text = pack_name if pack_name == current_pack: item_text += " (Current)" item = QListWidgetItem(item_text) item.setData(Qt.UserRole, pack_name) self.installed_list.addItem(item) def update_current_soundpack(self): """Update current soundpack display""" # Check both possible keys - audio.sound_pack is the real one being used current_pack = self.settings.get('audio', 'sound_pack', None) if not current_pack: current_pack = self.settings.get('audio', 'sound_pack', 'default') print(f"Debug: Found soundpack setting = '{current_pack}'") self.current_pack_label.setText(f"Current soundpack: {current_pack}") def refresh_soundpacks(self): """Refresh available soundpacks from repositories""" if self.current_thread and self.current_thread.isRunning(): return self.refresh_button.setEnabled(False) self.refresh_button.setText("Refreshing...") # Start discovery in background thread self.current_thread = SoundpackOperationThread('discover', self.manager) self.current_thread.operation_complete.connect(self.on_discover_complete) self.current_thread.start() def on_discover_complete(self, success: bool, message: str): """Handle soundpack discovery completion""" self.refresh_button.setEnabled(True) self.refresh_button.setText("&Refresh Soundpack List") if success: self.soundpacks = self.manager.discover_soundpacks() self.update_soundpack_list() else: QMessageBox.warning(self, "Discovery Failed", f"Failed to discover soundpacks: {message}") def update_soundpack_list(self): """Update the soundpack list display""" self.soundpack_list.clear() # Add header item to help Orca read single-item lists header_item = QListWidgetItem(f"Available Soundpacks ({len(self.soundpacks)} found):") header_item.setFlags(header_item.flags() & ~Qt.ItemIsSelectable) # Make it non-selectable header_item.setData(Qt.AccessibleDescriptionRole, "Available soundpacks list header") self.soundpack_list.addItem(header_item) for soundpack in self.soundpacks: item_text = soundpack.name if soundpack.installed: item_text += " (Installed)" item_text += f" - {soundpack.description}" item = QListWidgetItem(item_text) item.setData(Qt.UserRole, soundpack) self.soundpack_list.addItem(item) def on_soundpack_selected(self): """Handle soundpack selection""" current = self.soundpack_list.currentItem() if not current: self.details_label.setText("Select a soundpack to view details") self.description_text.clear() self.install_button.setEnabled(False) self.switch_button.setEnabled(False) return soundpack = current.data(Qt.UserRole) # Skip header items (they don't have soundpack data) if not soundpack: self.details_label.setText("Select a soundpack to view details") self.description_text.clear() self.install_button.setEnabled(False) self.switch_button.setEnabled(False) return # Update details status = "Installed" if soundpack.installed else "Not installed" self.details_label.setText(f"Name: {soundpack.name}\nStatus: {status}\nRepository: {soundpack.repository_url}") self.description_text.setText(soundpack.description) # Update buttons self.install_button.setEnabled(not soundpack.installed) self.switch_button.setEnabled(soundpack.installed) def install_selected(self): """Install the selected soundpack""" current = self.soundpack_list.currentItem() if not current: return soundpack = current.data(Qt.UserRole) # Confirm installation reply = QMessageBox.question( self, "Install Soundpack", f"Install soundpack '{soundpack.name}'?\n\nThis will download and extract the soundpack to your soundpacks directory.", QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes ) if reply != QMessageBox.Yes: return # Start installation self.install_button.setEnabled(False) self.install_button.setText("Installing...") self.current_thread = SoundpackOperationThread('install', self.manager, soundpack) self.current_thread.operation_complete.connect(self.on_install_complete) self.current_thread.progress_update.connect(self.on_progress_update) self.current_thread.start() # Show progress dialog self.progress_dialog = QProgressDialog("Installing soundpack...", "Cancel", 0, 0, self) self.progress_dialog.setWindowModality(Qt.WindowModal) self.progress_dialog.show() def on_install_complete(self, success: bool, message: str): """Handle installation completion""" self.install_button.setEnabled(True) self.install_button.setText("&Install") if hasattr(self, 'progress_dialog'): self.progress_dialog.close() if success: QMessageBox.information(self, "Installation Successful", message) # Ask if user wants to switch to the new soundpack reply = QMessageBox.question( self, "Switch Soundpack", "Soundpack installed successfully!\n\nWould you like to switch to this soundpack now?", QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes ) if reply == QMessageBox.Yes: current = self.soundpack_list.currentItem() if current: soundpack = current.data(Qt.UserRole) self.manager.switch_soundpack(soundpack.name) self.update_current_soundpack() # Refresh lists self.refresh_soundpacks() self.load_installed_soundpacks() else: QMessageBox.critical(self, "Installation Failed", f"Failed to install soundpack: {message}") def on_progress_update(self, message: str): """Handle progress updates""" if hasattr(self, 'progress_dialog'): self.progress_dialog.setLabelText(message) def switch_to_selected(self): """Switch to the selected soundpack""" current = self.soundpack_list.currentItem() if not current: return soundpack = current.data(Qt.UserRole) success, message = self.manager.switch_soundpack(soundpack.name) if success: QMessageBox.information(self, "Soundpack Switched", message) self.update_current_soundpack() self.load_installed_soundpacks() else: QMessageBox.critical(self, "Switch Failed", f"Failed to switch soundpack: {message}") def add_repository(self): """Add a new repository""" dialog = AddRepositoryDialog(self.manager, self) if dialog.exec() == QDialog.Accepted: self.load_repositories() def remove_repository(self): """Remove selected repository""" current = self.repo_list.currentItem() if not current: return repo = current.data(Qt.UserRole) if not repo: # Skip header items return reply = QMessageBox.question( self, "Remove Repository", f"Remove repository '{repo.description}'?\n\nThis will not affect installed soundpacks.", QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply == QMessageBox.Yes: self.manager.remove_repository(repo.url) self.load_repositories() def on_repository_selected(self): """Handle repository selection""" current = self.repo_list.currentItem() # Enable remove button only if it's not the header item and has repo data has_repo_data = current and current.data(Qt.UserRole) is not None self.remove_repo_button.setEnabled(has_repo_data) def on_installed_selected(self): """Handle installed soundpack selection""" current = self.installed_list.currentItem() if current: pack_name = current.data(Qt.UserRole) current_pack = self.settings.get('audio', 'sound_pack', 'default') self.switch_installed_button.setEnabled(pack_name != current_pack) self.uninstall_button.setEnabled(pack_name != 'default') # Can't uninstall default else: self.switch_installed_button.setEnabled(False) self.uninstall_button.setEnabled(False) def switch_to_installed(self): """Switch to selected installed soundpack""" current = self.installed_list.currentItem() if not current: return pack_name = current.data(Qt.UserRole) success, message = self.manager.switch_soundpack(pack_name) if success: QMessageBox.information(self, "Soundpack Switched", message) self.update_current_soundpack() self.load_installed_soundpacks() else: QMessageBox.critical(self, "Switch Failed", f"Failed to switch soundpack: {message}") def uninstall_selected(self): """Uninstall selected soundpack""" current = self.installed_list.currentItem() if not current: return pack_name = current.data(Qt.UserRole) reply = QMessageBox.question( self, "Uninstall Soundpack", f"Uninstall soundpack '{pack_name}'?\n\nThis will permanently remove the soundpack files.", QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply == QMessageBox.Yes: success, message = self.manager.uninstall_soundpack(pack_name) if success: QMessageBox.information(self, "Uninstall Successful", message) self.load_installed_soundpacks() self.update_current_soundpack() self.refresh_soundpacks() # Update available list else: QMessageBox.critical(self, "Uninstall Failed", f"Failed to uninstall soundpack: {message}") class AddRepositoryDialog(QDialog): """Dialog for adding a new repository""" def __init__(self, manager: SoundpackManager, parent=None): super().__init__(parent) self.manager = manager self.setWindowTitle("Add Repository") self.setModal(True) self.resize(500, 200) self.setup_ui() def setup_ui(self): """Setup the UI""" layout = QVBoxLayout(self) # Form layout form_layout = QFormLayout() self.url_edit = QLineEdit() self.url_edit.setAccessibleName("Repository URL") self.url_edit.setPlaceholderText("https://example.com/soundpacks") self.url_edit.textChanged.connect(self.validate_form) form_layout.addRow("&URL:", self.url_edit) self.description_edit = QLineEdit() self.description_edit.setAccessibleName("Repository description") self.description_edit.setPlaceholderText("Description of this repository") self.description_edit.textChanged.connect(self.validate_form) form_layout.addRow("&Description:", self.description_edit) layout.addLayout(form_layout) # Validation button self.validate_button = QPushButton("&Validate Repository") self.validate_button.setAccessibleName("Validate repository URL") self.validate_button.clicked.connect(self.validate_repository) self.validate_button.setEnabled(False) layout.addWidget(self.validate_button) # Status label self.status_label = QLabel("") self.status_label.setAccessibleName("Validation status") self.status_label.setWordWrap(True) layout.addWidget(self.status_label) # Button box self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) self.button_box.accepted.connect(self.accept_repository) self.button_box.rejected.connect(self.reject) self.button_box.button(QDialogButtonBox.Ok).setEnabled(False) layout.addWidget(self.button_box) def validate_form(self): """Validate form inputs""" url = self.url_edit.text().strip() description = self.description_edit.text().strip() has_content = bool(url and description) self.validate_button.setEnabled(has_content) if has_content: # Basic URL validation valid_format, message = self.manager.validate_url(url) if not valid_format: self.status_label.setText(f"URL Error: {message}") self.status_label.setStyleSheet("color: red;") else: self.status_label.setText("Click 'Validate Repository' to check if this URL contains soundpacks") self.status_label.setStyleSheet("color: blue;") else: self.status_label.clear() def validate_repository(self): """Validate the repository URL""" url = self.url_edit.text().strip() self.validate_button.setEnabled(False) self.validate_button.setText("Validating...") self.status_label.setText("Checking repository...") self.status_label.setStyleSheet("color: blue;") # Start validation in background self.validation_thread = SoundpackOperationThread('validate_repo', self.manager, url) self.validation_thread.operation_complete.connect(self.on_validation_complete) self.validation_thread.start() def on_validation_complete(self, success: bool, message: str): """Handle validation completion""" self.validate_button.setEnabled(True) self.validate_button.setText("&Validate Repository") if success: self.status_label.setText(f"✓ {message}") self.status_label.setStyleSheet("color: green;") self.button_box.button(QDialogButtonBox.Ok).setEnabled(True) else: self.status_label.setText(f"✗ {message}") self.status_label.setStyleSheet("color: red;") self.button_box.button(QDialogButtonBox.Ok).setEnabled(False) def accept_repository(self): """Add the repository""" url = self.url_edit.text().strip() description = self.description_edit.text().strip() success, message = self.manager.add_repository(url, description) if success: self.accept() else: QMessageBox.critical(self, "Add Repository Failed", f"Failed to add repository: {message}")