"""Modal editor for non-secret settings and the stored API key.""" from __future__ import annotations import os from pathlib import Path from PySide6.QtCore import Signal from PySide6.QtWidgets import ( QDialog, QDialogButtonBox, QCheckBox, QFileDialog, QFormLayout, QHBoxLayout, QLineEdit, QMessageBox, QPushButton, QSpinBox, QToolButton, QVBoxLayout, QWidget, ) from voice_transcriptor.models import AppSettings from voice_transcriptor.services.credentials import CredentialError, CredentialService from voice_transcriptor.services.settings import SettingsError, SettingsRepository class SettingsDialog(QDialog): """Edit application settings while keeping the API key out of normal storage.""" settings_saved = Signal(AppSettings) def __init__( self, settings: AppSettings, repository: SettingsRepository, credentials: CredentialService, parent=None, ) -> None: super().__init__(parent) self._repository = repository self._credentials = credentials self.setWindowTitle("Settings") self.model_input = QLineEdit(settings.model, self) self.model_input.setObjectName("modelInput") self.language_input = QLineEdit(settings.language, self) self.language_input.setObjectName("languageInput") self.api_key_input = QLineEdit(self) self.api_key_input.setObjectName("apiKeyInput") self.api_key_input.setEchoMode(QLineEdit.EchoMode.Password) self.output_directory_input = QLineEdit(str(settings.output_directory), self) self.output_directory_input.setObjectName("outputDirectoryInput") self.choose_directory_button = QPushButton("Browse…", self) self.choose_directory_button.setObjectName("chooseDirectoryButton") self.choose_directory_button.clicked.connect(self.choose_output_directory) output_directory_layout = QHBoxLayout() output_directory_layout.addWidget(self.output_directory_input) output_directory_layout.addWidget(self.choose_directory_button) form = QFormLayout() form.addRow("OpenAI API key", self.api_key_input) form.addRow("Model", self.model_input) form.addRow("Language", self.language_input) form.addRow("Output directory", output_directory_layout) self.advanced_toggle = QToolButton(self) self.advanced_toggle.setText("Advanced") self.advanced_toggle.setCheckable(True) self.advanced_toggle.setObjectName("advancedToggle") self.advanced_panel = QWidget(self) self.advanced_panel.setObjectName("advancedPanel") advanced_form = QFormLayout(self.advanced_panel) self.chunk_duration_input = QSpinBox(self.advanced_panel) self.chunk_duration_input.setObjectName("chunkDurationInput") self.chunk_duration_input.setRange(1, 86400) self.chunk_duration_input.setSuffix(" seconds") self.chunk_duration_input.setValue(settings.chunk_duration_seconds) self.chunk_overlap_input = QSpinBox(self.advanced_panel) self.chunk_overlap_input.setObjectName("chunkOverlapInput") self.chunk_overlap_input.setRange(0, 3600) self.chunk_overlap_input.setSuffix(" seconds") self.chunk_overlap_input.setValue(settings.chunk_overlap_seconds) self.retain_temporary_files_input = QCheckBox("Retain temporary files for debugging", self.advanced_panel) self.retain_temporary_files_input.setObjectName("retainTemporaryFilesInput") self.retain_temporary_files_input.setChecked(settings.retain_temporary_files) advanced_form.addRow("Chunk duration", self.chunk_duration_input) advanced_form.addRow("Overlap", self.chunk_overlap_input) advanced_form.addRow(self.retain_temporary_files_input) self.advanced_panel.hide() self.advanced_toggle.toggled.connect(self.advanced_panel.setVisible) self.button_box = QDialogButtonBox( QDialogButtonBox.StandardButton.Save | QDialogButtonBox.StandardButton.Cancel, parent=self, ) self.save_button = self.button_box.button(QDialogButtonBox.StandardButton.Save) self.save_button.setObjectName("saveButton") self.cancel_button = self.button_box.button(QDialogButtonBox.StandardButton.Cancel) self.cancel_button.setObjectName("cancelButton") self.button_box.accepted.connect(self.save) self.button_box.rejected.connect(self.reject) layout = QVBoxLayout(self) layout.addLayout(form) layout.addWidget(self.advanced_toggle) layout.addWidget(self.advanced_panel) layout.addWidget(self.button_box) def choose_output_directory(self) -> None: directory = QFileDialog.getExistingDirectory( self, "Choose output directory", self.output_directory_input.text(), ) if directory: self.output_directory_input.setText(directory) def save(self) -> None: """Persist valid settings and, only when supplied, replace the API key.""" api_key = self.api_key_input.text().strip() self.api_key_input.clear() output_directory = self._output_directory() if output_directory is None: QMessageBox.warning( self, "Invalid output directory", "Choose an accessible output directory.", ) return model = self.model_input.text().strip() language = self.language_input.text().strip() if not model or not language: QMessageBox.warning(self, "Invalid settings", "Model and language are required.") return chunk_duration = self.chunk_duration_input.value() overlap = self.chunk_overlap_input.value() if overlap >= chunk_duration: QMessageBox.warning(self, "Invalid settings", "Overlap must be smaller than chunk duration.") return updated_settings = AppSettings( model, language, output_directory, chunk_duration, overlap, self.retain_temporary_files_input.isChecked(), ) try: self._repository.save(updated_settings) except SettingsError: QMessageBox.warning(self, "Could not save settings", "Settings could not be saved.") return if api_key: try: self._credentials.set_api_key(api_key) except CredentialError: QMessageBox.warning( self, "Could not save settings", "The API key could not be saved.", ) return self.settings_saved.emit(updated_settings) self.accept() def _output_directory(self) -> Path | None: value = self.output_directory_input.text().strip() if not value: return None directory = Path(value) try: if not directory.is_dir(): return None if not os.access(directory, os.R_OK | os.W_OK): return None except OSError: return None return directory