Refactor SettingsDialog for improved readability and maintainability

- Cleaned up whitespace and formatting throughout the settings_dialog.py file.
- Enhanced type hints for better clarity and type checking.
- Consolidated URL mapping handling in get_config_data method.
- Improved error handling and logging for configuration operations.
- Added comments for better understanding of the code structure and functionality.
This commit is contained in:
claudi 2026-02-25 13:26:46 +01:00
parent 03991fdea5
commit 986793632e
4 changed files with 468 additions and 480 deletions

View file

@ -12,7 +12,7 @@ logger = logging.getLogger(__name__)
class ConfigValidator: class ConfigValidator:
"""Validates configuration values against schema. """Validates configuration values against schema.
Provides detailed error messages for invalid configurations. Provides detailed error messages for invalid configurations.
""" """
@ -33,10 +33,10 @@ class ConfigValidator:
@staticmethod @staticmethod
def validate(config_dict: Dict[str, Any]) -> List[str]: def validate(config_dict: Dict[str, Any]) -> List[str]:
"""Validate configuration dictionary. """Validate configuration dictionary.
Args: Args:
config_dict: Configuration dictionary to validate config_dict: Configuration dictionary to validate
Returns: Returns:
List of validation error messages (empty if valid) List of validation error messages (empty if valid)
""" """
@ -53,7 +53,9 @@ class ConfigValidator:
# Check type # Check type
expected_type = rules.get("type") expected_type = rules.get("type")
if expected_type and not isinstance(value, expected_type): if expected_type and not isinstance(value, expected_type):
errors.append(f"{field}: expected {expected_type.__name__}, got {type(value).__name__}") errors.append(
f"{field}: expected {expected_type.__name__}, got {type(value).__name__}"
)
continue continue
# Check allowed values # Check allowed values
@ -84,10 +86,10 @@ class ConfigValidator:
@staticmethod @staticmethod
def validate_or_raise(config_dict: Dict[str, Any]) -> None: def validate_or_raise(config_dict: Dict[str, Any]) -> None:
"""Validate configuration and raise error if invalid. """Validate configuration and raise error if invalid.
Args: Args:
config_dict: Configuration dictionary to validate config_dict: Configuration dictionary to validate
Raises: Raises:
ConfigurationError: If configuration is invalid ConfigurationError: If configuration is invalid
""" """
@ -98,26 +100,26 @@ class ConfigValidator:
class ConfigProfile: class ConfigProfile:
"""Manages named configuration profiles. """Manages named configuration profiles.
Profiles are stored in ~/.webdrop-bridge/profiles/ directory as JSON files. Profiles are stored in ~/.webdrop-bridge/profiles/ directory as JSON files.
""" """
PROFILES_DIR = Path.home() / ".webdrop-bridge" / "profiles" PROFILES_DIR = Path.home() / ".webdrop-bridge" / "profiles"
def __init__(self): def __init__(self) -> None:
"""Initialize profile manager.""" """Initialize profile manager."""
self.PROFILES_DIR.mkdir(parents=True, exist_ok=True) self.PROFILES_DIR.mkdir(parents=True, exist_ok=True)
def save_profile(self, profile_name: str, config: Config) -> Path: def save_profile(self, profile_name: str, config: Config) -> Path:
"""Save configuration as a named profile. """Save configuration as a named profile.
Args: Args:
profile_name: Name of the profile (e.g., "work", "personal") profile_name: Name of the profile (e.g., "work", "personal")
config: Config object to save config: Config object to save
Returns: Returns:
Path to the saved profile file Path to the saved profile file
Raises: Raises:
ConfigurationError: If profile name is invalid ConfigurationError: If profile name is invalid
""" """
@ -148,13 +150,13 @@ class ConfigProfile:
def load_profile(self, profile_name: str) -> Dict[str, Any]: def load_profile(self, profile_name: str) -> Dict[str, Any]:
"""Load configuration from a named profile. """Load configuration from a named profile.
Args: Args:
profile_name: Name of the profile to load profile_name: Name of the profile to load
Returns: Returns:
Configuration dictionary Configuration dictionary
Raises: Raises:
ConfigurationError: If profile not found or invalid ConfigurationError: If profile not found or invalid
""" """
@ -173,7 +175,7 @@ class ConfigProfile:
def list_profiles(self) -> List[str]: def list_profiles(self) -> List[str]:
"""List all available profiles. """List all available profiles.
Returns: Returns:
List of profile names (without .json extension) List of profile names (without .json extension)
""" """
@ -184,10 +186,10 @@ class ConfigProfile:
def delete_profile(self, profile_name: str) -> None: def delete_profile(self, profile_name: str) -> None:
"""Delete a profile. """Delete a profile.
Args: Args:
profile_name: Name of the profile to delete profile_name: Name of the profile to delete
Raises: Raises:
ConfigurationError: If profile not found ConfigurationError: If profile not found
""" """
@ -209,11 +211,11 @@ class ConfigExporter:
@staticmethod @staticmethod
def export_to_json(config: Config, output_path: Path) -> None: def export_to_json(config: Config, output_path: Path) -> None:
"""Export configuration to JSON file. """Export configuration to JSON file.
Args: Args:
config: Config object to export config: Config object to export
output_path: Path to write JSON file output_path: Path to write JSON file
Raises: Raises:
ConfigurationError: If export fails ConfigurationError: If export fails
""" """
@ -240,13 +242,13 @@ class ConfigExporter:
@staticmethod @staticmethod
def import_from_json(input_path: Path) -> Dict[str, Any]: def import_from_json(input_path: Path) -> Dict[str, Any]:
"""Import configuration from JSON file. """Import configuration from JSON file.
Args: Args:
input_path: Path to JSON file to import input_path: Path to JSON file to import
Returns: Returns:
Configuration dictionary Configuration dictionary
Raises: Raises:
ConfigurationError: If import fails or validation fails ConfigurationError: If import fails or validation fails
""" """

View file

@ -231,9 +231,6 @@ class UpdateManager:
except socket.timeout as e: except socket.timeout as e:
logger.error(f"Socket timeout (5s) connecting to {self.api_endpoint}") logger.error(f"Socket timeout (5s) connecting to {self.api_endpoint}")
return None return None
except TimeoutError as e:
logger.error(f"Timeout error: {e}")
return None
except Exception as e: except Exception as e:
logger.error(f"Failed to fetch release: {type(e).__name__}: {e}") logger.error(f"Failed to fetch release: {type(e).__name__}: {e}")
import traceback import traceback

File diff suppressed because it is too large Load diff

View file

@ -2,10 +2,11 @@
import logging import logging
from pathlib import Path from pathlib import Path
from typing import List, Optional from typing import Any, Dict, List, Optional
from PySide6.QtCore import Qt from PySide6.QtCore import Qt
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QComboBox,
QDialog, QDialog,
QDialogButtonBox, QDialogButtonBox,
QFileDialog, QFileDialog,
@ -32,7 +33,7 @@ logger = logging.getLogger(__name__)
class SettingsDialog(QDialog): class SettingsDialog(QDialog):
"""Dialog for managing application settings and configuration. """Dialog for managing application settings and configuration.
Provides tabs for: Provides tabs for:
- Paths: Manage allowed root directories - Paths: Manage allowed root directories
- URLs: Manage allowed web URLs - URLs: Manage allowed web URLs
@ -41,9 +42,9 @@ class SettingsDialog(QDialog):
- Profiles: Save/load/delete configuration profiles - Profiles: Save/load/delete configuration profiles
""" """
def __init__(self, config: Config, parent=None): def __init__(self, config: Config, parent: Optional[QWidget] = None):
"""Initialize the settings dialog. """Initialize the settings dialog.
Args: Args:
config: Current application configuration config: Current application configuration
parent: Parent widget parent: Parent widget
@ -53,16 +54,16 @@ class SettingsDialog(QDialog):
self.profile_manager = ConfigProfile() self.profile_manager = ConfigProfile()
self.setWindowTitle("Settings") self.setWindowTitle("Settings")
self.setGeometry(100, 100, 600, 500) self.setGeometry(100, 100, 600, 500)
self.setup_ui() self.setup_ui()
def setup_ui(self) -> None: def setup_ui(self) -> None:
"""Set up the dialog UI with tabs.""" """Set up the dialog UI with tabs."""
layout = QVBoxLayout() layout = QVBoxLayout()
# Create tab widget # Create tab widget
self.tabs = QTabWidget() self.tabs = QTabWidget()
# Add tabs # Add tabs
self.tabs.addTab(self._create_web_source_tab(), "Web Source") self.tabs.addTab(self._create_web_source_tab(), "Web Source")
self.tabs.addTab(self._create_paths_tab(), "Paths") self.tabs.addTab(self._create_paths_tab(), "Paths")
@ -70,9 +71,9 @@ class SettingsDialog(QDialog):
self.tabs.addTab(self._create_logging_tab(), "Logging") self.tabs.addTab(self._create_logging_tab(), "Logging")
self.tabs.addTab(self._create_window_tab(), "Window") self.tabs.addTab(self._create_window_tab(), "Window")
self.tabs.addTab(self._create_profiles_tab(), "Profiles") self.tabs.addTab(self._create_profiles_tab(), "Profiles")
layout.addWidget(self.tabs) layout.addWidget(self.tabs)
# Add buttons # Add buttons
button_box = QDialogButtonBox( button_box = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
@ -80,12 +81,12 @@ class SettingsDialog(QDialog):
button_box.accepted.connect(self.accept) button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject) button_box.rejected.connect(self.reject)
layout.addWidget(button_box) layout.addWidget(button_box)
self.setLayout(layout) self.setLayout(layout)
def accept(self) -> None: def accept(self) -> None:
"""Handle OK button - save configuration changes to file. """Handle OK button - save configuration changes to file.
Validates configuration and saves to the default config path. Validates configuration and saves to the default config path.
Applies log level changes immediately in the running application. Applies log level changes immediately in the running application.
If validation or save fails, shows error and stays in dialog. If validation or save fails, shows error and stays in dialog.
@ -93,50 +94,49 @@ class SettingsDialog(QDialog):
try: try:
# Get updated configuration data from UI # Get updated configuration data from UI
config_data = self.get_config_data() config_data = self.get_config_data()
# Convert URL mappings from dict to URLMapping objects # Convert URL mappings from dict to URLMapping objects
from webdrop_bridge.config import URLMapping from webdrop_bridge.config import URLMapping
url_mappings = [ url_mappings = [
URLMapping( URLMapping(url_prefix=m["url_prefix"], local_path=m["local_path"])
url_prefix=m["url_prefix"],
local_path=m["local_path"]
)
for m in config_data["url_mappings"] for m in config_data["url_mappings"]
] ]
# Update the config object with new values # Update the config object with new values
old_log_level = self.config.log_level old_log_level = self.config.log_level
self.config.log_level = config_data["log_level"] self.config.log_level = config_data["log_level"]
self.config.log_file = Path(config_data["log_file"]) if config_data["log_file"] else None self.config.log_file = (
Path(config_data["log_file"]) if config_data["log_file"] else None
)
self.config.allowed_roots = [Path(r).resolve() for r in config_data["allowed_roots"]] self.config.allowed_roots = [Path(r).resolve() for r in config_data["allowed_roots"]]
self.config.allowed_urls = config_data["allowed_urls"] self.config.allowed_urls = config_data["allowed_urls"]
self.config.webapp_url = config_data["webapp_url"] self.config.webapp_url = config_data["webapp_url"]
self.config.url_mappings = url_mappings self.config.url_mappings = url_mappings
self.config.window_width = config_data["window_width"] self.config.window_width = config_data["window_width"]
self.config.window_height = config_data["window_height"] self.config.window_height = config_data["window_height"]
# Save to file (creates parent dirs if needed) # Save to file (creates parent dirs if needed)
config_path = Config.get_default_config_path() config_path = Config.get_default_config_path()
self.config.to_file(config_path) self.config.to_file(config_path)
logger.info(f"Configuration saved to {config_path}") logger.info(f"Configuration saved to {config_path}")
logger.info(f" Log level: {self.config.log_level} (was: {old_log_level})") logger.info(f" Log level: {self.config.log_level} (was: {old_log_level})")
logger.info(f" Window size: {self.config.window_width}x{self.config.window_height}") logger.info(f" Window size: {self.config.window_width}x{self.config.window_height}")
# Apply log level change immediately to running application # Apply log level change immediately to running application
if old_log_level != self.config.log_level: if old_log_level != self.config.log_level:
logger.info(f"🔄 Updating log level: {old_log_level}{self.config.log_level}") logger.info(f"🔄 Updating log level: {old_log_level}{self.config.log_level}")
reconfigure_logging( reconfigure_logging(
logger_name="webdrop_bridge", logger_name="webdrop_bridge",
level=self.config.log_level, level=self.config.log_level,
log_file=self.config.log_file log_file=self.config.log_file,
) )
logger.info(f"✅ Log level updated to {self.config.log_level}") logger.info(f"✅ Log level updated to {self.config.log_level}")
# Call parent accept to close dialog # Call parent accept to close dialog
super().accept() super().accept()
except ConfigurationError as e: except ConfigurationError as e:
logger.error(f"Configuration error: {e}") logger.error(f"Configuration error: {e}")
self._show_error(f"Configuration Error:\n\n{e}") self._show_error(f"Configuration Error:\n\n{e}")
@ -147,67 +147,70 @@ class SettingsDialog(QDialog):
def _create_web_source_tab(self) -> QWidget: def _create_web_source_tab(self) -> QWidget:
"""Create web source configuration tab.""" """Create web source configuration tab."""
from PySide6.QtWidgets import QTableWidget, QTableWidgetItem from PySide6.QtWidgets import QTableWidget, QTableWidgetItem
widget = QWidget() widget = QWidget()
layout = QVBoxLayout() layout = QVBoxLayout()
# Webapp URL configuration # Webapp URL configuration
layout.addWidget(QLabel("Web Application URL:")) layout.addWidget(QLabel("Web Application URL:"))
url_layout = QHBoxLayout() url_layout = QHBoxLayout()
self.webapp_url_input = QLineEdit() self.webapp_url_input = QLineEdit()
self.webapp_url_input.setText(self.config.webapp_url) self.webapp_url_input.setText(self.config.webapp_url)
self.webapp_url_input.setPlaceholderText("e.g., http://localhost:8080 or file:///./webapp/index.html") self.webapp_url_input.setPlaceholderText(
"e.g., http://localhost:8080 or file:///./webapp/index.html"
)
url_layout.addWidget(self.webapp_url_input) url_layout.addWidget(self.webapp_url_input)
open_btn = QPushButton("Open") open_btn = QPushButton("Open")
open_btn.clicked.connect(self._open_webapp_url) open_btn.clicked.connect(self._open_webapp_url)
url_layout.addWidget(open_btn) url_layout.addWidget(open_btn)
layout.addLayout(url_layout) layout.addLayout(url_layout)
# URL Mappings (Azure Blob URL → Local Path) # URL Mappings (Azure Blob URL → Local Path)
layout.addWidget(QLabel("URL Mappings (Azure Blob Storage → Local Paths):")) layout.addWidget(QLabel("URL Mappings (Azure Blob Storage → Local Paths):"))
# Create table for URL mappings # Create table for URL mappings
self.url_mappings_table = QTableWidget() self.url_mappings_table = QTableWidget()
self.url_mappings_table.setColumnCount(2) self.url_mappings_table.setColumnCount(2)
self.url_mappings_table.setHorizontalHeaderLabels(["URL Prefix", "Local Path"]) self.url_mappings_table.setHorizontalHeaderLabels(["URL Prefix", "Local Path"])
self.url_mappings_table.horizontalHeader().setStretchLastSection(True) self.url_mappings_table.horizontalHeader().setStretchLastSection(True)
# Populate from config # Populate from config
for mapping in self.config.url_mappings: for mapping in self.config.url_mappings:
row = self.url_mappings_table.rowCount() row = self.url_mappings_table.rowCount()
self.url_mappings_table.insertRow(row) self.url_mappings_table.insertRow(row)
self.url_mappings_table.setItem(row, 0, QTableWidgetItem(mapping.url_prefix)) self.url_mappings_table.setItem(row, 0, QTableWidgetItem(mapping.url_prefix))
self.url_mappings_table.setItem(row, 1, QTableWidgetItem(mapping.local_path)) self.url_mappings_table.setItem(row, 1, QTableWidgetItem(mapping.local_path))
layout.addWidget(self.url_mappings_table) layout.addWidget(self.url_mappings_table)
# Buttons for URL mapping management # Buttons for URL mapping management
button_layout = QHBoxLayout() button_layout = QHBoxLayout()
add_mapping_btn = QPushButton("Add Mapping") add_mapping_btn = QPushButton("Add Mapping")
add_mapping_btn.clicked.connect(self._add_url_mapping) add_mapping_btn.clicked.connect(self._add_url_mapping)
button_layout.addWidget(add_mapping_btn) button_layout.addWidget(add_mapping_btn)
edit_mapping_btn = QPushButton("Edit Selected") edit_mapping_btn = QPushButton("Edit Selected")
edit_mapping_btn.clicked.connect(self._edit_url_mapping) edit_mapping_btn.clicked.connect(self._edit_url_mapping)
button_layout.addWidget(edit_mapping_btn) button_layout.addWidget(edit_mapping_btn)
remove_mapping_btn = QPushButton("Remove Selected") remove_mapping_btn = QPushButton("Remove Selected")
remove_mapping_btn.clicked.connect(self._remove_url_mapping) remove_mapping_btn.clicked.connect(self._remove_url_mapping)
button_layout.addWidget(remove_mapping_btn) button_layout.addWidget(remove_mapping_btn)
layout.addLayout(button_layout) layout.addLayout(button_layout)
layout.addStretch() layout.addStretch()
widget.setLayout(layout) widget.setLayout(layout)
return widget return widget
def _open_webapp_url(self) -> None: def _open_webapp_url(self) -> None:
"""Open the webapp URL in the default browser.""" """Open the webapp URL in the default browser."""
import webbrowser import webbrowser
url = self.webapp_url_input.text().strip() url = self.webapp_url_input.text().strip()
if url: if url:
# Handle file:// URLs # Handle file:// URLs
@ -216,61 +219,55 @@ class SettingsDialog(QDialog):
except Exception as e: except Exception as e:
logger.error(f"Failed to open URL: {e}") logger.error(f"Failed to open URL: {e}")
self._show_error(f"Failed to open URL:\n\n{e}") self._show_error(f"Failed to open URL:\n\n{e}")
def _add_url_mapping(self) -> None: def _add_url_mapping(self) -> None:
"""Add new URL mapping.""" """Add new URL mapping."""
from PySide6.QtWidgets import QInputDialog from PySide6.QtWidgets import QInputDialog
url_prefix, ok1 = QInputDialog.getText( url_prefix, ok1 = QInputDialog.getText(
self, self,
"Add URL Mapping", "Add URL Mapping",
"Enter Azure Blob Storage URL prefix:\n(e.g., https://myblob.blob.core.windows.net/container/)" "Enter Azure Blob Storage URL prefix:\n(e.g., https://myblob.blob.core.windows.net/container/)",
) )
if ok1 and url_prefix: if ok1 and url_prefix:
local_path, ok2 = QInputDialog.getText( local_path, ok2 = QInputDialog.getText(
self, self,
"Add URL Mapping", "Add URL Mapping",
"Enter local file system path:\n(e.g., C:\\Share or /mnt/share)" "Enter local file system path:\n(e.g., C:\\Share or /mnt/share)",
) )
if ok2 and local_path: if ok2 and local_path:
row = self.url_mappings_table.rowCount() row = self.url_mappings_table.rowCount()
self.url_mappings_table.insertRow(row) self.url_mappings_table.insertRow(row)
self.url_mappings_table.setItem(row, 0, QTableWidgetItem(url_prefix)) self.url_mappings_table.setItem(row, 0, QTableWidgetItem(url_prefix))
self.url_mappings_table.setItem(row, 1, QTableWidgetItem(local_path)) self.url_mappings_table.setItem(row, 1, QTableWidgetItem(local_path))
def _edit_url_mapping(self) -> None: def _edit_url_mapping(self) -> None:
"""Edit selected URL mapping.""" """Edit selected URL mapping."""
from PySide6.QtWidgets import QInputDialog from PySide6.QtWidgets import QInputDialog
current_row = self.url_mappings_table.currentRow() current_row = self.url_mappings_table.currentRow()
if current_row < 0: if current_row < 0:
self._show_error("Please select a mapping to edit") self._show_error("Please select a mapping to edit")
return return
url_prefix = self.url_mappings_table.item(current_row, 0).text() url_prefix = self.url_mappings_table.item(current_row, 0).text() # type: ignore
local_path = self.url_mappings_table.item(current_row, 1).text() local_path = self.url_mappings_table.item(current_row, 1).text() # type: ignore
new_url_prefix, ok1 = QInputDialog.getText( new_url_prefix, ok1 = QInputDialog.getText(
self, self, "Edit URL Mapping", "Enter Azure Blob Storage URL prefix:", text=url_prefix
"Edit URL Mapping",
"Enter Azure Blob Storage URL prefix:",
text=url_prefix
) )
if ok1 and new_url_prefix: if ok1 and new_url_prefix:
new_local_path, ok2 = QInputDialog.getText( new_local_path, ok2 = QInputDialog.getText(
self, self, "Edit URL Mapping", "Enter local file system path:", text=local_path
"Edit URL Mapping",
"Enter local file system path:",
text=local_path
) )
if ok2 and new_local_path: if ok2 and new_local_path:
self.url_mappings_table.setItem(current_row, 0, QTableWidgetItem(new_url_prefix)) self.url_mappings_table.setItem(current_row, 0, QTableWidgetItem(new_url_prefix))
self.url_mappings_table.setItem(current_row, 1, QTableWidgetItem(new_local_path)) self.url_mappings_table.setItem(current_row, 1, QTableWidgetItem(new_local_path))
def _remove_url_mapping(self) -> None: def _remove_url_mapping(self) -> None:
"""Remove selected URL mapping.""" """Remove selected URL mapping."""
current_row = self.url_mappings_table.currentRow() current_row = self.url_mappings_table.currentRow()
@ -281,29 +278,29 @@ class SettingsDialog(QDialog):
"""Create paths configuration tab.""" """Create paths configuration tab."""
widget = QWidget() widget = QWidget()
layout = QVBoxLayout() layout = QVBoxLayout()
layout.addWidget(QLabel("Allowed root directories for file access:")) layout.addWidget(QLabel("Allowed root directories for file access:"))
# List widget for paths # List widget for paths
self.paths_list = QListWidget() self.paths_list = QListWidget()
for path in self.config.allowed_roots: for path in self.config.allowed_roots:
self.paths_list.addItem(str(path)) self.paths_list.addItem(str(path))
layout.addWidget(self.paths_list) layout.addWidget(self.paths_list)
# Buttons for path management # Buttons for path management
button_layout = QHBoxLayout() button_layout = QHBoxLayout()
add_path_btn = QPushButton("Add Path") add_path_btn = QPushButton("Add Path")
add_path_btn.clicked.connect(self._add_path) add_path_btn.clicked.connect(self._add_path)
button_layout.addWidget(add_path_btn) button_layout.addWidget(add_path_btn)
remove_path_btn = QPushButton("Remove Selected") remove_path_btn = QPushButton("Remove Selected")
remove_path_btn.clicked.connect(self._remove_path) remove_path_btn.clicked.connect(self._remove_path)
button_layout.addWidget(remove_path_btn) button_layout.addWidget(remove_path_btn)
layout.addLayout(button_layout) layout.addLayout(button_layout)
layout.addStretch() layout.addStretch()
widget.setLayout(layout) widget.setLayout(layout)
return widget return widget
@ -311,29 +308,29 @@ class SettingsDialog(QDialog):
"""Create URLs configuration tab.""" """Create URLs configuration tab."""
widget = QWidget() widget = QWidget()
layout = QVBoxLayout() layout = QVBoxLayout()
layout.addWidget(QLabel("Allowed web URLs (supports wildcards like http://*.example.com):")) layout.addWidget(QLabel("Allowed web URLs (supports wildcards like http://*.example.com):"))
# List widget for URLs # List widget for URLs
self.urls_list = QListWidget() self.urls_list = QListWidget()
for url in self.config.allowed_urls: for url in self.config.allowed_urls:
self.urls_list.addItem(url) self.urls_list.addItem(url)
layout.addWidget(self.urls_list) layout.addWidget(self.urls_list)
# Buttons for URL management # Buttons for URL management
button_layout = QHBoxLayout() button_layout = QHBoxLayout()
add_url_btn = QPushButton("Add URL") add_url_btn = QPushButton("Add URL")
add_url_btn.clicked.connect(self._add_url) add_url_btn.clicked.connect(self._add_url)
button_layout.addWidget(add_url_btn) button_layout.addWidget(add_url_btn)
remove_url_btn = QPushButton("Remove Selected") remove_url_btn = QPushButton("Remove Selected")
remove_url_btn.clicked.connect(self._remove_url) remove_url_btn.clicked.connect(self._remove_url)
button_layout.addWidget(remove_url_btn) button_layout.addWidget(remove_url_btn)
layout.addLayout(button_layout) layout.addLayout(button_layout)
layout.addStretch() layout.addStretch()
widget.setLayout(layout) widget.setLayout(layout)
return widget return widget
@ -341,27 +338,28 @@ class SettingsDialog(QDialog):
"""Create logging configuration tab.""" """Create logging configuration tab."""
widget = QWidget() widget = QWidget()
layout = QVBoxLayout() layout = QVBoxLayout()
# Log level selection # Log level selection
layout.addWidget(QLabel("Log Level:")) layout.addWidget(QLabel("Log Level:"))
from PySide6.QtWidgets import QComboBox from PySide6.QtWidgets import QComboBox
self.log_level_combo: QComboBox = self._create_log_level_widget() self.log_level_combo: QComboBox = self._create_log_level_widget()
layout.addWidget(self.log_level_combo) layout.addWidget(self.log_level_combo)
# Log file path # Log file path
layout.addWidget(QLabel("Log File (optional):")) layout.addWidget(QLabel("Log File (optional):"))
log_file_layout = QHBoxLayout() log_file_layout = QHBoxLayout()
self.log_file_input = QLineEdit() self.log_file_input = QLineEdit()
self.log_file_input.setText(str(self.config.log_file) if self.config.log_file else "") self.log_file_input.setText(str(self.config.log_file) if self.config.log_file else "")
log_file_layout.addWidget(self.log_file_input) log_file_layout.addWidget(self.log_file_input)
browse_btn = QPushButton("Browse...") browse_btn = QPushButton("Browse...")
browse_btn.clicked.connect(self._browse_log_file) browse_btn.clicked.connect(self._browse_log_file)
log_file_layout.addWidget(browse_btn) log_file_layout.addWidget(browse_btn)
layout.addLayout(log_file_layout) layout.addLayout(log_file_layout)
layout.addStretch() layout.addStretch()
widget.setLayout(layout) widget.setLayout(layout)
return widget return widget
@ -370,7 +368,7 @@ class SettingsDialog(QDialog):
"""Create window settings tab.""" """Create window settings tab."""
widget = QWidget() widget = QWidget()
layout = QVBoxLayout() layout = QVBoxLayout()
# Window width # Window width
width_layout = QHBoxLayout() width_layout = QHBoxLayout()
width_layout.addWidget(QLabel("Window Width:")) width_layout.addWidget(QLabel("Window Width:"))
@ -381,7 +379,7 @@ class SettingsDialog(QDialog):
width_layout.addWidget(self.width_spin) width_layout.addWidget(self.width_spin)
width_layout.addStretch() width_layout.addStretch()
layout.addLayout(width_layout) layout.addLayout(width_layout)
# Window height # Window height
height_layout = QHBoxLayout() height_layout = QHBoxLayout()
height_layout.addWidget(QLabel("Window Height:")) height_layout.addWidget(QLabel("Window Height:"))
@ -392,7 +390,7 @@ class SettingsDialog(QDialog):
height_layout.addWidget(self.height_spin) height_layout.addWidget(self.height_spin)
height_layout.addStretch() height_layout.addStretch()
layout.addLayout(height_layout) layout.addLayout(height_layout)
layout.addStretch() layout.addStretch()
widget.setLayout(layout) widget.setLayout(layout)
return widget return widget
@ -401,52 +399,50 @@ class SettingsDialog(QDialog):
"""Create profiles management tab.""" """Create profiles management tab."""
widget = QWidget() widget = QWidget()
layout = QVBoxLayout() layout = QVBoxLayout()
layout.addWidget(QLabel("Saved Configuration Profiles:")) layout.addWidget(QLabel("Saved Configuration Profiles:"))
# List of profiles # List of profiles
self.profiles_list = QListWidget() self.profiles_list = QListWidget()
self._refresh_profiles_list() self._refresh_profiles_list()
layout.addWidget(self.profiles_list) layout.addWidget(self.profiles_list)
# Profile management buttons # Profile management buttons
button_layout = QHBoxLayout() button_layout = QHBoxLayout()
save_profile_btn = QPushButton("Save as Profile") save_profile_btn = QPushButton("Save as Profile")
save_profile_btn.clicked.connect(self._save_profile) save_profile_btn.clicked.connect(self._save_profile)
button_layout.addWidget(save_profile_btn) button_layout.addWidget(save_profile_btn)
load_profile_btn = QPushButton("Load Profile") load_profile_btn = QPushButton("Load Profile")
load_profile_btn.clicked.connect(self._load_profile) load_profile_btn.clicked.connect(self._load_profile)
button_layout.addWidget(load_profile_btn) button_layout.addWidget(load_profile_btn)
delete_profile_btn = QPushButton("Delete Profile") delete_profile_btn = QPushButton("Delete Profile")
delete_profile_btn.clicked.connect(self._delete_profile) delete_profile_btn.clicked.connect(self._delete_profile)
button_layout.addWidget(delete_profile_btn) button_layout.addWidget(delete_profile_btn)
layout.addLayout(button_layout) layout.addLayout(button_layout)
# Export/Import buttons # Export/Import buttons
export_layout = QHBoxLayout() export_layout = QHBoxLayout()
export_btn = QPushButton("Export Configuration") export_btn = QPushButton("Export Configuration")
export_btn.clicked.connect(self._export_config) export_btn.clicked.connect(self._export_config)
export_layout.addWidget(export_btn) export_layout.addWidget(export_btn)
import_btn = QPushButton("Import Configuration") import_btn = QPushButton("Import Configuration")
import_btn.clicked.connect(self._import_config) import_btn.clicked.connect(self._import_config)
export_layout.addWidget(import_btn) export_layout.addWidget(import_btn)
layout.addLayout(export_layout) layout.addLayout(export_layout)
layout.addStretch() layout.addStretch()
widget.setLayout(layout) widget.setLayout(layout)
return widget return widget
def _create_log_level_widget(self): def _create_log_level_widget(self) -> QComboBox:
"""Create log level selection widget.""" """Create log level selection widget."""
from PySide6.QtWidgets import QComboBox
combo = QComboBox() combo = QComboBox()
levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
combo.addItems(levels) combo.addItems(levels)
@ -467,11 +463,9 @@ class SettingsDialog(QDialog):
def _add_url(self) -> None: def _add_url(self) -> None:
"""Add a new allowed URL.""" """Add a new allowed URL."""
from PySide6.QtWidgets import QInputDialog from PySide6.QtWidgets import QInputDialog
url, ok = QInputDialog.getText( url, ok = QInputDialog.getText(
self, self, "Add URL", "Enter URL pattern (e.g., http://example.com or http://*.example.com):"
"Add URL",
"Enter URL pattern (e.g., http://example.com or http://*.example.com):"
) )
if ok and url: if ok and url:
self.urls_list.addItem(url) self.urls_list.addItem(url)
@ -484,10 +478,7 @@ class SettingsDialog(QDialog):
def _browse_log_file(self) -> None: def _browse_log_file(self) -> None:
"""Browse for log file location.""" """Browse for log file location."""
file_path, _ = QFileDialog.getSaveFileName( file_path, _ = QFileDialog.getSaveFileName(
self, self, "Select Log File", str(Path.home()), "Log Files (*.log);;All Files (*)"
"Select Log File",
str(Path.home()),
"Log Files (*.log);;All Files (*)"
) )
if file_path: if file_path:
self.log_file_input.setText(file_path) self.log_file_input.setText(file_path)
@ -501,13 +492,11 @@ class SettingsDialog(QDialog):
def _save_profile(self) -> None: def _save_profile(self) -> None:
"""Save current configuration as a profile.""" """Save current configuration as a profile."""
from PySide6.QtWidgets import QInputDialog from PySide6.QtWidgets import QInputDialog
profile_name, ok = QInputDialog.getText( profile_name, ok = QInputDialog.getText(
self, self, "Save Profile", "Enter profile name (e.g., work, personal):"
"Save Profile",
"Enter profile name (e.g., work, personal):"
) )
if ok and profile_name: if ok and profile_name:
try: try:
self.profile_manager.save_profile(profile_name, self.config) self.profile_manager.save_profile(profile_name, self.config)
@ -521,7 +510,7 @@ class SettingsDialog(QDialog):
if not current_item: if not current_item:
self._show_error("Please select a profile to load") self._show_error("Please select a profile to load")
return return
profile_name = current_item.text() profile_name = current_item.text()
try: try:
config_data = self.profile_manager.load_profile(profile_name) config_data = self.profile_manager.load_profile(profile_name)
@ -535,7 +524,7 @@ class SettingsDialog(QDialog):
if not current_item: if not current_item:
self._show_error("Please select a profile to delete") self._show_error("Please select a profile to delete")
return return
profile_name = current_item.text() profile_name = current_item.text()
try: try:
self.profile_manager.delete_profile(profile_name) self.profile_manager.delete_profile(profile_name)
@ -546,12 +535,9 @@ class SettingsDialog(QDialog):
def _export_config(self) -> None: def _export_config(self) -> None:
"""Export configuration to file.""" """Export configuration to file."""
file_path, _ = QFileDialog.getSaveFileName( file_path, _ = QFileDialog.getSaveFileName(
self, self, "Export Configuration", str(Path.home()), "JSON Files (*.json);;All Files (*)"
"Export Configuration",
str(Path.home()),
"JSON Files (*.json);;All Files (*)"
) )
if file_path: if file_path:
try: try:
ConfigExporter.export_to_json(self.config, Path(file_path)) ConfigExporter.export_to_json(self.config, Path(file_path))
@ -561,12 +547,9 @@ class SettingsDialog(QDialog):
def _import_config(self) -> None: def _import_config(self) -> None:
"""Import configuration from file.""" """Import configuration from file."""
file_path, _ = QFileDialog.getOpenFileName( file_path, _ = QFileDialog.getOpenFileName(
self, self, "Import Configuration", str(Path.home()), "JSON Files (*.json);;All Files (*)"
"Import Configuration",
str(Path.home()),
"JSON Files (*.json);;All Files (*)"
) )
if file_path: if file_path:
try: try:
config_data = ConfigExporter.import_from_json(Path(file_path)) config_data = ConfigExporter.import_from_json(Path(file_path))
@ -574,9 +557,9 @@ class SettingsDialog(QDialog):
except ConfigurationError as e: except ConfigurationError as e:
self._show_error(f"Failed to import configuration: {e}") self._show_error(f"Failed to import configuration: {e}")
def _apply_config_data(self, config_data: dict) -> None: def _apply_config_data(self, config_data: Dict[str, Any]) -> None:
"""Apply imported configuration data to UI. """Apply imported configuration data to UI.
Args: Args:
config_data: Configuration dictionary config_data: Configuration dictionary
""" """
@ -584,60 +567,67 @@ class SettingsDialog(QDialog):
self.paths_list.clear() self.paths_list.clear()
for path in config_data.get("allowed_roots", []): for path in config_data.get("allowed_roots", []):
self.paths_list.addItem(str(path)) self.paths_list.addItem(str(path))
# Apply URLs # Apply URLs
self.urls_list.clear() self.urls_list.clear()
for url in config_data.get("allowed_urls", []): for url in config_data.get("allowed_urls", []):
self.urls_list.addItem(url) self.urls_list.addItem(url)
# Apply logging settings # Apply logging settings
self.log_level_combo.setCurrentText(config_data.get("log_level", "INFO")) self.log_level_combo.setCurrentText(config_data.get("log_level", "INFO"))
log_file = config_data.get("log_file") log_file = config_data.get("log_file")
self.log_file_input.setText(str(log_file) if log_file else "") self.log_file_input.setText(str(log_file) if log_file else "")
# Apply window settings # Apply window settings
self.width_spin.setValue(config_data.get("window_width", 800)) self.width_spin.setValue(config_data.get("window_width", 800))
self.height_spin.setValue(config_data.get("window_height", 600)) self.height_spin.setValue(config_data.get("window_height", 600))
def get_config_data(self) -> dict: def get_config_data(self) -> Dict[str, Any]:
"""Get updated configuration data from dialog. """Get updated configuration data from dialog.
Returns: Returns:
Configuration dictionary Configuration dictionary
Raises: Raises:
ConfigurationError: If configuration is invalid ConfigurationError: If configuration is invalid
""" """
if self.url_mappings_table:
url_mappings_table_count = self.url_mappings_table.rowCount() or 0
else:
url_mappings_table_count = 0
config_data = { config_data = {
"app_name": self.config.app_name, "app_name": self.config.app_name,
"app_version": self.config.app_version, "app_version": self.config.app_version,
"log_level": self.log_level_combo.currentText(), "log_level": self.log_level_combo.currentText(),
"log_file": self.log_file_input.text() or None, "log_file": self.log_file_input.text() or None,
"allowed_roots": [self.paths_list.item(i).text() for i in range(self.paths_list.count())], "allowed_roots": [
self.paths_list.item(i).text() for i in range(self.paths_list.count())
],
"allowed_urls": [self.urls_list.item(i).text() for i in range(self.urls_list.count())], "allowed_urls": [self.urls_list.item(i).text() for i in range(self.urls_list.count())],
"webapp_url": self.webapp_url_input.text().strip(), "webapp_url": self.webapp_url_input.text().strip(),
"url_mappings": [ "url_mappings": [
{ {
"url_prefix": self.url_mappings_table.item(i, 0).text(), "url_prefix": self.url_mappings_table.item(i, 0).text() if self.url_mappings_table.item(i, 0) else "", # type: ignore
"local_path": self.url_mappings_table.item(i, 1).text() "local_path": self.url_mappings_table.item(i, 1).text() if self.url_mappings_table.item(i, 1) else "", # type: ignore
} }
for i in range(self.url_mappings_table.rowCount()) for i in range(url_mappings_table_count)
], ],
"window_width": self.width_spin.value(), "window_width": self.width_spin.value(),
"window_height": self.height_spin.value(), "window_height": self.height_spin.value(),
"enable_logging": self.config.enable_logging, "enable_logging": self.config.enable_logging,
} }
# Validate # Validate
ConfigValidator.validate_or_raise(config_data) ConfigValidator.validate_or_raise(config_data)
return config_data return config_data
def _show_error(self, message: str) -> None: def _show_error(self, message: str) -> None:
"""Show error message to user. """Show error message to user.
Args: Args:
message: Error message message: Error message
""" """
from PySide6.QtWidgets import QMessageBox from PySide6.QtWidgets import QMessageBox
QMessageBox.critical(self, "Error", message) QMessageBox.critical(self, "Error", message)