You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

184 lines
7.1 KiB

"""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