Add comprehensive soundpack manager with security-first design
- **Soundpack Manager**: Full-featured package discovery, download, and installation system - Repository management with HTTPS enforcement and validation - Directory listing support (auto-discovers .zip files) + soundpacknames.txt fallback - Secure download with size limits, timeout protection, and path sanitization - Zip bomb protection: file count, individual size, and total extraction limits - Audio file validation using magic numbers (not just extensions) - Accessible UI with keyboard navigation and screen reader optimization - **Auto-refresh system**: Smart timeline updates respecting user activity - 300-second default interval + 10-second idle buffer - Keyboard activity tracking prevents interrupting active users - True new content detection using post IDs instead of counts - Preserves cursor position during background refreshes - **Enhanced notifications**: Fixed spam issues and improved targeting - Timeline switching now silent (no notifications for actively viewed content) - Initial app load notifications disabled for 2 seconds - Generic "New content in timeline" messages instead of misleading post counts - Separate handling for auto-refresh vs manual refresh scenarios - **Load more posts improvements**: Better positioning and user experience - New posts load below cursor position instead of above - Cursor automatically focuses on first new post for natural reading flow - Fixed widget hierarchy issues preventing activation - **Accessibility enhancements**: Workarounds for screen reader quirks - Added list headers to fix Orca single-item list reading issues - Improved accessible names and descriptions throughout - Non-selectable header items with dynamic counts - Better error messages and status updates - **Settings integration**: Corrected soundpack configuration management - Fixed inconsistent config keys (now uses [audio] sound_pack consistently) - Added soundpack manager to File menu (Ctrl+Shift+P) - Repository persistence and validation 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
674
src/widgets/soundpack_manager_dialog.py
Normal file
674
src/widgets/soundpack_manager_dialog.py
Normal file
@ -0,0 +1,674 @@
|
||||
"""
|
||||
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}")
|
Reference in New Issue
Block a user