diff --git a/resources/translations/de.json b/resources/translations/de.json index e3162e4..e70acca 100644 --- a/resources/translations/de.json +++ b/resources/translations/de.json @@ -94,6 +94,13 @@ "settings.branding.select_label": "Branding-Vorlage:", "settings.branding.select_tooltip": "Wählen Sie die Branding-Vorlage, die beim Start automatisch geladen werden soll.", "settings.branding.help_text": "Branding steuert die visuelle Identität der App, zum Beispiel Name und Icons. Es ist klar von den gespeicherten Setups getrennt.", + "settings.branding.display_name_label": "Anzeigename:", + "settings.branding.app_name_label": "Anwendungsname:", + "settings.branding.window_title_label": "Fenstertitel (optional):", + "settings.branding.logo_path_label": "Logopfad (optional):", + "settings.branding.save_as_btn": "Als Vorlage speichern", + "settings.branding.save_as_title": "Branding-Vorlage speichern", + "settings.branding.save_as_prompt": "Vorlagen-ID eingeben (z.B. kunde_a):", "settings.branding.restart_note": "Branding-Änderungen werden persistent gespeichert und nach einem Neustart vollständig angewendet.", "settings.web_url.label": "Web-Anwendungs-URL:", "settings.web_url.placeholder": "z.B. http://localhost:8080 oder file:///./webapp/index.html", diff --git a/resources/translations/en.json b/resources/translations/en.json index d92c546..9e0b46e 100644 --- a/resources/translations/en.json +++ b/resources/translations/en.json @@ -94,6 +94,13 @@ "settings.branding.select_label": "Branding template:", "settings.branding.select_tooltip": "Choose the branding template that should be loaded automatically on startup.", "settings.branding.help_text": "Branding controls the visual identity of the app, such as name and icons. It is kept separate from your saved setups.", + "settings.branding.display_name_label": "Display name:", + "settings.branding.app_name_label": "Application name:", + "settings.branding.window_title_label": "Window title (optional):", + "settings.branding.logo_path_label": "Logo path (optional):", + "settings.branding.save_as_btn": "Save as Template", + "settings.branding.save_as_title": "Save Branding Template", + "settings.branding.save_as_prompt": "Enter a template ID (e.g. customer_a):", "settings.branding.restart_note": "Branding changes are persisted immediately and are fully applied after restarting the app.", "settings.web_url.label": "Web Application URL:", "settings.web_url.placeholder": "e.g., http://localhost:8080 or file:///./webapp/index.html", diff --git a/resources/translations/fr.json b/resources/translations/fr.json index 92c970a..3032138 100644 --- a/resources/translations/fr.json +++ b/resources/translations/fr.json @@ -94,6 +94,13 @@ "settings.branding.select_label": "Modèle de branding :", "settings.branding.select_tooltip": "Choisissez le modèle de branding qui doit être chargé automatiquement au démarrage.", "settings.branding.help_text": "Le branding contrôle l’identité visuelle de l’application, comme le nom et les icônes. Il reste séparé de vos configurations enregistrées.", + "settings.branding.display_name_label": "Nom d’affichage :", + "settings.branding.app_name_label": "Nom de l’application :", + "settings.branding.window_title_label": "Titre de la fenêtre (facultatif) :", + "settings.branding.logo_path_label": "Chemin du logo (facultatif) :", + "settings.branding.save_as_btn": "Enregistrer comme modèle", + "settings.branding.save_as_title": "Enregistrer le modèle de branding", + "settings.branding.save_as_prompt": "Entrez un identifiant de modèle (par ex. client_a) :", "settings.branding.restart_note": "Les changements de branding sont enregistrés de façon persistante et seront entièrement appliqués après le redémarrage de l’application.", "settings.web_url.label": "URL de l'application web\u00a0:", "settings.web_url.placeholder": "p.ex. http://localhost:8080 ou file:///./webapp/index.html", diff --git a/resources/translations/it.json b/resources/translations/it.json index 58e54f7..8046651 100644 --- a/resources/translations/it.json +++ b/resources/translations/it.json @@ -94,6 +94,13 @@ "settings.branding.select_label": "Modello di branding:", "settings.branding.select_tooltip": "Scegli il modello di branding da caricare automaticamente all’avvio.", "settings.branding.help_text": "Il branding controlla l’identità visiva dell’app, come nome e icone. Rimane separato dalle configurazioni salvate.", + "settings.branding.display_name_label": "Nome visualizzato:", + "settings.branding.app_name_label": "Nome applicazione:", + "settings.branding.window_title_label": "Titolo finestra (opzionale):", + "settings.branding.logo_path_label": "Percorso logo (opzionale):", + "settings.branding.save_as_btn": "Salva come modello", + "settings.branding.save_as_title": "Salva modello di branding", + "settings.branding.save_as_prompt": "Inserisci un ID modello (es. cliente_a):", "settings.branding.restart_note": "Le modifiche al branding vengono salvate in modo persistente e saranno applicate completamente dopo il riavvio dell’applicazione.", "settings.web_url.label": "URL applicazione web:", "settings.web_url.placeholder": "es. http://localhost:8080 o file:///./webapp/index.html", diff --git a/resources/translations/ru.json b/resources/translations/ru.json index 93e82dc..7e4d462 100644 --- a/resources/translations/ru.json +++ b/resources/translations/ru.json @@ -94,6 +94,13 @@ "settings.branding.select_label": "Шаблон брендинга:", "settings.branding.select_tooltip": "Выберите шаблон брендинга, который должен автоматически загружаться при запуске.", "settings.branding.help_text": "Брендинг управляет визуальной идентичностью приложения, например названием и иконками. Он отделен от сохраненных наборов настроек.", + "settings.branding.display_name_label": "Отображаемое имя:", + "settings.branding.app_name_label": "Имя приложения:", + "settings.branding.window_title_label": "Заголовок окна (необязательно):", + "settings.branding.logo_path_label": "Путь к логотипу (необязательно):", + "settings.branding.save_as_btn": "Сохранить как шаблон", + "settings.branding.save_as_title": "Сохранить шаблон брендинга", + "settings.branding.save_as_prompt": "Введите ID шаблона (например, client_a):", "settings.branding.restart_note": "Изменения брендинга сохраняются постоянно и будут полностью применены после перезапуска приложения.", "settings.web_url.label": "URL веб-приложения:", "settings.web_url.placeholder": "например, http://localhost:8080 или file:///./webapp/index.html", diff --git a/resources/translations/zh.json b/resources/translations/zh.json index f54d1fb..ea3ce6b 100644 --- a/resources/translations/zh.json +++ b/resources/translations/zh.json @@ -94,6 +94,13 @@ "settings.branding.select_label": "品牌模板:", "settings.branding.select_tooltip": "选择应用启动时应自动加载的品牌模板。", "settings.branding.help_text": "品牌控制应用的视觉标识,例如名称和图标,并与已保存的设置保持分离。", + "settings.branding.display_name_label": "显示名称:", + "settings.branding.app_name_label": "应用名称:", + "settings.branding.window_title_label": "窗口标题(可选):", + "settings.branding.logo_path_label": "Logo 路径(可选):", + "settings.branding.save_as_btn": "另存为模板", + "settings.branding.save_as_title": "保存品牌模板", + "settings.branding.save_as_prompt": "输入模板 ID(例如 customer_a):", "settings.branding.restart_note": "品牌更改会被持久保存,并将在应用重启后完整生效。", "settings.web_url.label": "Web 应用 URL:", "settings.web_url.placeholder": "例如: http://localhost:8080 或 file:///./webapp/index.html", diff --git a/src/webdrop_bridge/core/branding_manager.py b/src/webdrop_bridge/core/branding_manager.py index 6fad03e..8408f33 100644 --- a/src/webdrop_bridge/core/branding_manager.py +++ b/src/webdrop_bridge/core/branding_manager.py @@ -168,6 +168,24 @@ class BrandingManager: logger.info("Branding template saved: %s", template.template_id) return template_path + def build_template( + self, + *, + template_id: str, + display_name: str, + app_name: str, + window_title: str = "", + logo_path: str = "", + ) -> BrandingTemplate: + """Build a validated branding template from editable UI fields.""" + return BrandingTemplate( + template_id=template_id.strip(), + display_name=display_name.strip(), + app_name=app_name.strip(), + window_title=window_title.strip(), + logo_path=logo_path.strip(), + ) + def delete_template(self, template_id: str) -> None: """Delete a user template while protecting built-ins.""" if template_id in BUILTIN_BRANDING_TEMPLATES: diff --git a/src/webdrop_bridge/ui/settings_dialog.py b/src/webdrop_bridge/ui/settings_dialog.py index c83d65a..201349e 100644 --- a/src/webdrop_bridge/ui/settings_dialog.py +++ b/src/webdrop_bridge/ui/settings_dialog.py @@ -10,6 +10,7 @@ from PySide6.QtWidgets import ( QDialogButtonBox, QFileDialog, QHBoxLayout, + QInputDialog, QLabel, QLineEdit, QListWidget, @@ -184,16 +185,42 @@ class SettingsDialog(QDialog): self.branding_combo = QComboBox() self.branding_combo.setToolTip(tr("settings.branding.select_tooltip")) - for template in self.branding_manager.list_templates(): - self.branding_combo.addItem(template.display_name, template.template_id) - - idx = self.branding_combo.findData(self.config.active_branding_id) - if idx < 0: - idx = self.branding_combo.findData("default") - if idx >= 0: - self.branding_combo.setCurrentIndex(idx) + self._refresh_branding_combo() + self.branding_combo.currentIndexChanged.connect(self._on_branding_selection_changed) layout.addWidget(self.branding_combo) + self.branding_display_name_input = QLineEdit() + self.branding_display_name_input.setPlaceholderText( + tr("settings.branding.display_name_label") + ) + layout.addWidget(QLabel(tr("settings.branding.display_name_label"))) + layout.addWidget(self.branding_display_name_input) + + self.branding_app_name_input = QLineEdit() + self.branding_app_name_input.setPlaceholderText(tr("settings.branding.app_name_label")) + layout.addWidget(QLabel(tr("settings.branding.app_name_label"))) + layout.addWidget(self.branding_app_name_input) + + self.branding_window_title_input = QLineEdit() + self.branding_window_title_input.setPlaceholderText( + tr("settings.branding.window_title_label") + ) + layout.addWidget(QLabel(tr("settings.branding.window_title_label"))) + layout.addWidget(self.branding_window_title_input) + + self.branding_logo_path_input = QLineEdit() + self.branding_logo_path_input.setPlaceholderText(tr("settings.branding.logo_path_label")) + layout.addWidget(QLabel(tr("settings.branding.logo_path_label"))) + layout.addWidget(self.branding_logo_path_input) + + branding_button_layout = QHBoxLayout() + self.save_branding_as_btn = QPushButton(tr("settings.branding.save_as_btn")) + self.save_branding_as_btn.clicked.connect(self._save_branding_as) + branding_button_layout.addWidget(self.save_branding_as_btn) + layout.addLayout(branding_button_layout) + + self._load_branding_into_editor(self.branding_combo.currentData() or "default") + note = QLabel(tr("settings.branding.restart_note")) note.setWordWrap(True) note.setStyleSheet("color: gray; font-size: 11px;") @@ -203,6 +230,60 @@ class SettingsDialog(QDialog): widget.setLayout(layout) return widget + def _refresh_branding_combo(self, selected_template_id: Optional[str] = None) -> None: + """Refresh the branding template selector.""" + current = selected_template_id or self.config.active_branding_id or "default" + self.branding_combo.blockSignals(True) + self.branding_combo.clear() + for template in self.branding_manager.list_templates(): + self.branding_combo.addItem(template.display_name, template.template_id) + + idx = self.branding_combo.findData(current) + if idx < 0: + idx = self.branding_combo.findData("default") + if idx >= 0: + self.branding_combo.setCurrentIndex(idx) + self.branding_combo.blockSignals(False) + + def _load_branding_into_editor(self, template_id: str) -> None: + """Load the selected branding template into the editable fields.""" + template = self.branding_manager.load_template(template_id) + self.branding_display_name_input.setText(template.display_name) + self.branding_app_name_input.setText(template.app_name) + self.branding_window_title_input.setText(template.window_title) + self.branding_logo_path_input.setText(template.logo_path) + + def _on_branding_selection_changed(self) -> None: + """Update editable branding fields when a different template is selected.""" + template_id = self.branding_combo.currentData() + if template_id: + self._load_branding_into_editor(template_id) + + def _save_branding_as(self) -> None: + """Save the edited branding as a new reusable template.""" + template_id, ok = QInputDialog.getText( + self, + tr("settings.branding.save_as_title"), + tr("settings.branding.save_as_prompt"), + ) + + if not ok or not template_id: + return + + try: + template = self.branding_manager.build_template( + template_id=template_id, + display_name=self.branding_display_name_input.text(), + app_name=self.branding_app_name_input.text(), + window_title=self.branding_window_title_input.text(), + logo_path=self.branding_logo_path_input.text(), + ) + self.branding_manager.save_template(template) + self._refresh_branding_combo(template.template_id) + self._load_branding_into_editor(template.template_id) + except ConfigurationError as e: + self._show_error(f"Failed to save branding: {e}") + def _create_web_source_tab(self) -> QWidget: """Create web source configuration tab.""" widget = QWidget() diff --git a/tests/unit/test_settings_dialog.py b/tests/unit/test_settings_dialog.py index b63796e..80df965 100644 --- a/tests/unit/test_settings_dialog.py +++ b/tests/unit/test_settings_dialog.py @@ -1,6 +1,7 @@ """Tests for settings dialog.""" from pathlib import Path +from unittest.mock import patch import pytest @@ -110,6 +111,34 @@ class TestSettingsDialogInitialization: assert "backup" in dialog.export_btn.toolTip().lower() assert "json" in dialog.import_btn.toolTip().lower() + def test_branding_editor_fields_are_initialized( + self, qtbot, sample_config, monkeypatch, tmp_path + ): + """Test branding tab exposes editable fields for the selected template.""" + monkeypatch.setenv("WEBDROP_BRANDING_DIR", str(tmp_path / "branding")) + dialog = SettingsDialog(sample_config) + qtbot.addWidget(dialog) + + assert dialog.branding_display_name_input.text() == "Default" + assert dialog.branding_app_name_input.text() == "WebDrop Bridge" + + def test_save_branding_as_creates_custom_template( + self, qtbot, sample_config, monkeypatch, tmp_path + ): + """Test edited branding can be saved as a new reusable template.""" + monkeypatch.setenv("WEBDROP_BRANDING_DIR", str(tmp_path / "branding")) + dialog = SettingsDialog(sample_config) + qtbot.addWidget(dialog) + + dialog.branding_display_name_input.setText("Customer A") + dialog.branding_app_name_input.setText("Customer A Bridge") + + with patch("PySide6.QtWidgets.QInputDialog.getText", return_value=("customer_a", True)): + dialog._save_branding_as() + + assert dialog.branding_manager.has_template("customer_a") + assert dialog.branding_combo.findData("customer_a") >= 0 + class TestPathsTab: """Test Paths configuration tab."""