feat: Enhance branding management with editable fields and save functionality in settings dialog

This commit is contained in:
claudi 2026-04-15 13:58:36 +02:00
parent fe341163e8
commit e52c09857f
9 changed files with 178 additions and 8 deletions

View file

@ -94,6 +94,13 @@
"settings.branding.select_label": "Branding-Vorlage:", "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.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.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.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.label": "Web-Anwendungs-URL:",
"settings.web_url.placeholder": "z.B. http://localhost:8080 oder file:///./webapp/index.html", "settings.web_url.placeholder": "z.B. http://localhost:8080 oder file:///./webapp/index.html",

View file

@ -94,6 +94,13 @@
"settings.branding.select_label": "Branding template:", "settings.branding.select_label": "Branding template:",
"settings.branding.select_tooltip": "Choose the branding template that should be loaded automatically on startup.", "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.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.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.label": "Web Application URL:",
"settings.web_url.placeholder": "e.g., http://localhost:8080 or file:///./webapp/index.html", "settings.web_url.placeholder": "e.g., http://localhost:8080 or file:///./webapp/index.html",

View file

@ -94,6 +94,13 @@
"settings.branding.select_label": "Modèle de branding :", "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.select_tooltip": "Choisissez le modèle de branding qui doit être chargé automatiquement au démarrage.",
"settings.branding.help_text": "Le branding contrôle lidentité visuelle de lapplication, comme le nom et les icônes. Il reste séparé de vos configurations enregistrées.", "settings.branding.help_text": "Le branding contrôle lidentité visuelle de lapplication, comme le nom et les icônes. Il reste séparé de vos configurations enregistrées.",
"settings.branding.display_name_label": "Nom daffichage :",
"settings.branding.app_name_label": "Nom de lapplication :",
"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 lapplication.", "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 lapplication.",
"settings.web_url.label": "URL de l'application web\u00a0:", "settings.web_url.label": "URL de l'application web\u00a0:",
"settings.web_url.placeholder": "p.ex. http://localhost:8080 ou file:///./webapp/index.html", "settings.web_url.placeholder": "p.ex. http://localhost:8080 ou file:///./webapp/index.html",

View file

@ -94,6 +94,13 @@
"settings.branding.select_label": "Modello di branding:", "settings.branding.select_label": "Modello di branding:",
"settings.branding.select_tooltip": "Scegli il modello di branding da caricare automaticamente allavvio.", "settings.branding.select_tooltip": "Scegli il modello di branding da caricare automaticamente allavvio.",
"settings.branding.help_text": "Il branding controlla lidentità visiva dellapp, come nome e icone. Rimane separato dalle configurazioni salvate.", "settings.branding.help_text": "Il branding controlla lidentità visiva dellapp, 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 dellapplicazione.", "settings.branding.restart_note": "Le modifiche al branding vengono salvate in modo persistente e saranno applicate completamente dopo il riavvio dellapplicazione.",
"settings.web_url.label": "URL applicazione web:", "settings.web_url.label": "URL applicazione web:",
"settings.web_url.placeholder": "es. http://localhost:8080 o file:///./webapp/index.html", "settings.web_url.placeholder": "es. http://localhost:8080 o file:///./webapp/index.html",

View file

@ -94,6 +94,13 @@
"settings.branding.select_label": "Шаблон брендинга:", "settings.branding.select_label": "Шаблон брендинга:",
"settings.branding.select_tooltip": "Выберите шаблон брендинга, который должен автоматически загружаться при запуске.", "settings.branding.select_tooltip": "Выберите шаблон брендинга, который должен автоматически загружаться при запуске.",
"settings.branding.help_text": "Брендинг управляет визуальной идентичностью приложения, например названием и иконками. Он отделен от сохраненных наборов настроек.", "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.branding.restart_note": "Изменения брендинга сохраняются постоянно и будут полностью применены после перезапуска приложения.",
"settings.web_url.label": "URL веб-приложения:", "settings.web_url.label": "URL веб-приложения:",
"settings.web_url.placeholder": "например, http://localhost:8080 или file:///./webapp/index.html", "settings.web_url.placeholder": "например, http://localhost:8080 или file:///./webapp/index.html",

View file

@ -94,6 +94,13 @@
"settings.branding.select_label": "品牌模板:", "settings.branding.select_label": "品牌模板:",
"settings.branding.select_tooltip": "选择应用启动时应自动加载的品牌模板。", "settings.branding.select_tooltip": "选择应用启动时应自动加载的品牌模板。",
"settings.branding.help_text": "品牌控制应用的视觉标识,例如名称和图标,并与已保存的设置保持分离。", "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.branding.restart_note": "品牌更改会被持久保存,并将在应用重启后完整生效。",
"settings.web_url.label": "Web 应用 URL:", "settings.web_url.label": "Web 应用 URL:",
"settings.web_url.placeholder": "例如: http://localhost:8080 或 file:///./webapp/index.html", "settings.web_url.placeholder": "例如: http://localhost:8080 或 file:///./webapp/index.html",

View file

@ -168,6 +168,24 @@ class BrandingManager:
logger.info("Branding template saved: %s", template.template_id) logger.info("Branding template saved: %s", template.template_id)
return template_path 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: def delete_template(self, template_id: str) -> None:
"""Delete a user template while protecting built-ins.""" """Delete a user template while protecting built-ins."""
if template_id in BUILTIN_BRANDING_TEMPLATES: if template_id in BUILTIN_BRANDING_TEMPLATES:

View file

@ -10,6 +10,7 @@ from PySide6.QtWidgets import (
QDialogButtonBox, QDialogButtonBox,
QFileDialog, QFileDialog,
QHBoxLayout, QHBoxLayout,
QInputDialog,
QLabel, QLabel,
QLineEdit, QLineEdit,
QListWidget, QListWidget,
@ -184,16 +185,42 @@ class SettingsDialog(QDialog):
self.branding_combo = QComboBox() self.branding_combo = QComboBox()
self.branding_combo.setToolTip(tr("settings.branding.select_tooltip")) self.branding_combo.setToolTip(tr("settings.branding.select_tooltip"))
for template in self.branding_manager.list_templates(): self._refresh_branding_combo()
self.branding_combo.addItem(template.display_name, template.template_id) self.branding_combo.currentIndexChanged.connect(self._on_branding_selection_changed)
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)
layout.addWidget(self.branding_combo) 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 = QLabel(tr("settings.branding.restart_note"))
note.setWordWrap(True) note.setWordWrap(True)
note.setStyleSheet("color: gray; font-size: 11px;") note.setStyleSheet("color: gray; font-size: 11px;")
@ -203,6 +230,60 @@ class SettingsDialog(QDialog):
widget.setLayout(layout) widget.setLayout(layout)
return widget 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: def _create_web_source_tab(self) -> QWidget:
"""Create web source configuration tab.""" """Create web source configuration tab."""
widget = QWidget() widget = QWidget()

View file

@ -1,6 +1,7 @@
"""Tests for settings dialog.""" """Tests for settings dialog."""
from pathlib import Path from pathlib import Path
from unittest.mock import patch
import pytest import pytest
@ -110,6 +111,34 @@ class TestSettingsDialogInitialization:
assert "backup" in dialog.export_btn.toolTip().lower() assert "backup" in dialog.export_btn.toolTip().lower()
assert "json" in dialog.import_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: class TestPathsTab:
"""Test Paths configuration tab.""" """Test Paths configuration tab."""