feat: Update branding terminology and improve settings dialog for logo management
This commit is contained in:
parent
e52c09857f
commit
e1dbc2ee84
10 changed files with 104 additions and 57 deletions
|
|
@ -91,16 +91,16 @@
|
||||||
"settings.tab.profiles": "Setups",
|
"settings.tab.profiles": "Setups",
|
||||||
"settings.tab.general": "Allgemein",
|
"settings.tab.general": "Allgemein",
|
||||||
"settings.tab.branding": "Branding",
|
"settings.tab.branding": "Branding",
|
||||||
"settings.branding.select_label": "Branding-Vorlage:",
|
"settings.branding.select_label": "Branding:",
|
||||||
"settings.branding.select_tooltip": "Wählen Sie die Branding-Vorlage, die beim Start automatisch geladen werden soll.",
|
"settings.branding.select_tooltip": "Wählen Sie das Branding, das 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 Name sowie Logo/Icon der App. Änderungen sind klar von den gespeicherten Setups getrennt.",
|
||||||
"settings.branding.display_name_label": "Anzeigename:",
|
"settings.branding.display_name_label": "Name:",
|
||||||
"settings.branding.app_name_label": "Anwendungsname:",
|
"settings.branding.app_name_label": "Anwendungsname:",
|
||||||
"settings.branding.window_title_label": "Fenstertitel (optional):",
|
"settings.branding.window_title_label": "Fenstertitel (optional):",
|
||||||
"settings.branding.logo_path_label": "Logopfad (optional):",
|
"settings.branding.logo_path_label": "Logo/Icon-Datei (optional):",
|
||||||
"settings.branding.save_as_btn": "Als Vorlage speichern",
|
"settings.branding.save_as_btn": "Branding speichern",
|
||||||
"settings.branding.save_as_title": "Branding-Vorlage speichern",
|
"settings.branding.save_as_title": "Branding speichern",
|
||||||
"settings.branding.save_as_prompt": "Vorlagen-ID eingeben (z.B. kunde_a):",
|
"settings.branding.save_as_prompt": "Name für das Branding eingeben:",
|
||||||
"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",
|
||||||
|
|
|
||||||
|
|
@ -91,16 +91,16 @@
|
||||||
"settings.tab.profiles": "Setups",
|
"settings.tab.profiles": "Setups",
|
||||||
"settings.tab.general": "General",
|
"settings.tab.general": "General",
|
||||||
"settings.tab.branding": "Branding",
|
"settings.tab.branding": "Branding",
|
||||||
"settings.branding.select_label": "Branding template:",
|
"settings.branding.select_label": "Branding:",
|
||||||
"settings.branding.select_tooltip": "Choose the branding template that should be loaded automatically on startup.",
|
"settings.branding.select_tooltip": "Choose the branding 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 app name and logo/icon. It stays clearly separated from your saved setups.",
|
||||||
"settings.branding.display_name_label": "Display name:",
|
"settings.branding.display_name_label": "Name:",
|
||||||
"settings.branding.app_name_label": "Application name:",
|
"settings.branding.app_name_label": "Application name:",
|
||||||
"settings.branding.window_title_label": "Window title (optional):",
|
"settings.branding.window_title_label": "Window title (optional):",
|
||||||
"settings.branding.logo_path_label": "Logo path (optional):",
|
"settings.branding.logo_path_label": "Logo/Icon file (optional):",
|
||||||
"settings.branding.save_as_btn": "Save as Template",
|
"settings.branding.save_as_btn": "Save Branding",
|
||||||
"settings.branding.save_as_title": "Save Branding Template",
|
"settings.branding.save_as_title": "Save Branding",
|
||||||
"settings.branding.save_as_prompt": "Enter a template ID (e.g. customer_a):",
|
"settings.branding.save_as_prompt": "Enter a name for the branding:",
|
||||||
"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",
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@
|
||||||
"settings.tab.profiles": "Configs",
|
"settings.tab.profiles": "Configs",
|
||||||
"settings.tab.general": "G\u00e9n\u00e9ral",
|
"settings.tab.general": "G\u00e9n\u00e9ral",
|
||||||
"settings.tab.branding": "Branding",
|
"settings.tab.branding": "Branding",
|
||||||
"settings.branding.select_label": "Modèle de branding :",
|
"settings.branding.select_label": "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 l’identité visuelle de l’application, comme le nom et les icônes. Il reste séparé de vos configurations enregistrées.",
|
"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.display_name_label": "Nom d’affichage :",
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@
|
||||||
"settings.tab.profiles": "Config",
|
"settings.tab.profiles": "Config",
|
||||||
"settings.tab.general": "Generale",
|
"settings.tab.general": "Generale",
|
||||||
"settings.tab.branding": "Branding",
|
"settings.tab.branding": "Branding",
|
||||||
"settings.branding.select_label": "Modello di branding:",
|
"settings.branding.select_label": "Branding:",
|
||||||
"settings.branding.select_tooltip": "Scegli il modello di branding da caricare automaticamente all’avvio.",
|
"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.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.display_name_label": "Nome visualizzato:",
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@
|
||||||
"settings.tab.profiles": "Наборы",
|
"settings.tab.profiles": "Наборы",
|
||||||
"settings.tab.general": "Общие настройки",
|
"settings.tab.general": "Общие настройки",
|
||||||
"settings.tab.branding": "Брендинг",
|
"settings.tab.branding": "Брендинг",
|
||||||
"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.display_name_label": "Отображаемое имя:",
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@
|
||||||
"settings.tab.profiles": "设置",
|
"settings.tab.profiles": "设置",
|
||||||
"settings.tab.general": "通用",
|
"settings.tab.general": "通用",
|
||||||
"settings.tab.branding": "品牌",
|
"settings.tab.branding": "品牌",
|
||||||
"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.display_name_label": "显示名称:",
|
||||||
|
|
|
||||||
|
|
@ -161,6 +161,8 @@ class BrandingManager:
|
||||||
"""Save or update a branding template on disk."""
|
"""Save or update a branding template on disk."""
|
||||||
if not template.template_id:
|
if not template.template_id:
|
||||||
raise ConfigurationError("Branding template requires a template_id")
|
raise ConfigurationError("Branding template requires a template_id")
|
||||||
|
if template.template_id in BUILTIN_BRANDING_TEMPLATES:
|
||||||
|
raise ConfigurationError(f"Cannot overwrite built-in branding: {template.template_id}")
|
||||||
|
|
||||||
self.templates_dir.mkdir(parents=True, exist_ok=True)
|
self.templates_dir.mkdir(parents=True, exist_ok=True)
|
||||||
template_path = self.templates_dir / f"{template.template_id}.json"
|
template_path = self.templates_dir / f"{template.template_id}.json"
|
||||||
|
|
@ -173,19 +175,31 @@ class BrandingManager:
|
||||||
*,
|
*,
|
||||||
template_id: str,
|
template_id: str,
|
||||||
display_name: str,
|
display_name: str,
|
||||||
app_name: str,
|
app_name: str = "",
|
||||||
window_title: str = "",
|
window_title: str = "",
|
||||||
logo_path: str = "",
|
logo_path: str = "",
|
||||||
) -> BrandingTemplate:
|
) -> BrandingTemplate:
|
||||||
"""Build a validated branding template from editable UI fields."""
|
"""Build a validated branding template from editable UI fields."""
|
||||||
|
safe_id = self._slugify(template_id.strip() or display_name)
|
||||||
|
safe_name = display_name.strip()
|
||||||
|
logo = logo_path.strip()
|
||||||
|
resolved_app_name = (app_name or display_name).strip()
|
||||||
|
|
||||||
return BrandingTemplate(
|
return BrandingTemplate(
|
||||||
template_id=template_id.strip(),
|
template_id=safe_id,
|
||||||
display_name=display_name.strip(),
|
display_name=safe_name,
|
||||||
app_name=app_name.strip(),
|
app_name=resolved_app_name,
|
||||||
window_title=window_title.strip(),
|
window_title=window_title.strip(),
|
||||||
logo_path=logo_path.strip(),
|
logo_path=logo,
|
||||||
|
app_icon_path_windows=logo or "resources/icons/app.ico",
|
||||||
|
app_icon_path_macos=logo or "resources/icons/app.icns",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _slugify(value: str) -> str:
|
||||||
|
"""Convert a human-readable branding name into a stable id."""
|
||||||
|
return "".join(c.lower() if c.isalnum() else "_" for c in value).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:
|
||||||
|
|
@ -229,20 +243,24 @@ class BrandingManager:
|
||||||
requested_id = self.get_active_branding_id()
|
requested_id = self.get_active_branding_id()
|
||||||
|
|
||||||
template = self.load_template(requested_id)
|
template = self.load_template(requested_id)
|
||||||
is_non_default_branding = template.template_id != DEFAULT_BRANDING_TEMPLATE_ID
|
|
||||||
default_app_name = BUILTIN_BRANDING_TEMPLATES[DEFAULT_BRANDING_TEMPLATE_ID].app_name
|
default_app_name = BUILTIN_BRANDING_TEMPLATES[DEFAULT_BRANDING_TEMPLATE_ID].app_name
|
||||||
|
known_app_names = {known_template.app_name for known_template in self.list_templates()}
|
||||||
|
known_title_prefixes = {f"{app_name} v" for app_name in known_app_names}
|
||||||
|
|
||||||
config.active_branding_id = template.template_id
|
config.active_branding_id = template.template_id
|
||||||
config.branding_display_name = template.display_name
|
config.branding_display_name = template.display_name
|
||||||
|
|
||||||
if is_non_default_branding or not config.app_name or config.app_name == default_app_name:
|
if (
|
||||||
config.app_name = template.app_name
|
template.template_id != DEFAULT_BRANDING_TEMPLATE_ID
|
||||||
|
or not config.app_name
|
||||||
|
or config.app_name in known_app_names
|
||||||
|
):
|
||||||
|
config.app_name = template.app_name or default_app_name
|
||||||
|
|
||||||
if (
|
if (
|
||||||
is_non_default_branding
|
template.template_id != DEFAULT_BRANDING_TEMPLATE_ID
|
||||||
or not config.window_title
|
or not config.window_title
|
||||||
or config.window_title.startswith(f"{default_app_name} v")
|
or any(config.window_title.startswith(prefix) for prefix in known_title_prefixes)
|
||||||
or config.window_title.startswith(f"{config.app_name} v")
|
|
||||||
):
|
):
|
||||||
config.window_title = (
|
config.window_title = (
|
||||||
template.window_title or f"{config.app_name} v{config.app_version}"
|
template.window_title or f"{config.app_name} v{config.app_version}"
|
||||||
|
|
|
||||||
|
|
@ -196,22 +196,16 @@ class SettingsDialog(QDialog):
|
||||||
layout.addWidget(QLabel(tr("settings.branding.display_name_label")))
|
layout.addWidget(QLabel(tr("settings.branding.display_name_label")))
|
||||||
layout.addWidget(self.branding_display_name_input)
|
layout.addWidget(self.branding_display_name_input)
|
||||||
|
|
||||||
self.branding_app_name_input = QLineEdit()
|
layout.addWidget(QLabel(tr("settings.branding.logo_path_label")))
|
||||||
self.branding_app_name_input.setPlaceholderText(tr("settings.branding.app_name_label"))
|
logo_layout = QHBoxLayout()
|
||||||
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 = QLineEdit()
|
||||||
self.branding_logo_path_input.setPlaceholderText(tr("settings.branding.logo_path_label"))
|
self.branding_logo_path_input.setPlaceholderText(tr("settings.branding.logo_path_label"))
|
||||||
layout.addWidget(QLabel(tr("settings.branding.logo_path_label")))
|
logo_layout.addWidget(self.branding_logo_path_input)
|
||||||
layout.addWidget(self.branding_logo_path_input)
|
|
||||||
|
self.browse_branding_logo_btn = QPushButton(tr("settings.log_file.browse_btn"))
|
||||||
|
self.browse_branding_logo_btn.clicked.connect(self._browse_branding_logo)
|
||||||
|
logo_layout.addWidget(self.browse_branding_logo_btn)
|
||||||
|
layout.addLayout(logo_layout)
|
||||||
|
|
||||||
branding_button_layout = QHBoxLayout()
|
branding_button_layout = QHBoxLayout()
|
||||||
self.save_branding_as_btn = QPushButton(tr("settings.branding.save_as_btn"))
|
self.save_branding_as_btn = QPushButton(tr("settings.branding.save_as_btn"))
|
||||||
|
|
@ -249,8 +243,6 @@ class SettingsDialog(QDialog):
|
||||||
"""Load the selected branding template into the editable fields."""
|
"""Load the selected branding template into the editable fields."""
|
||||||
template = self.branding_manager.load_template(template_id)
|
template = self.branding_manager.load_template(template_id)
|
||||||
self.branding_display_name_input.setText(template.display_name)
|
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)
|
self.branding_logo_path_input.setText(template.logo_path)
|
||||||
|
|
||||||
def _on_branding_selection_changed(self) -> None:
|
def _on_branding_selection_changed(self) -> None:
|
||||||
|
|
@ -259,23 +251,34 @@ class SettingsDialog(QDialog):
|
||||||
if template_id:
|
if template_id:
|
||||||
self._load_branding_into_editor(template_id)
|
self._load_branding_into_editor(template_id)
|
||||||
|
|
||||||
|
def _browse_branding_logo(self) -> None:
|
||||||
|
"""Select an external logo or icon file for the current branding."""
|
||||||
|
file_path, _ = QFileDialog.getOpenFileName(
|
||||||
|
self,
|
||||||
|
tr("settings.branding.logo_path_label"),
|
||||||
|
str(Path.home()),
|
||||||
|
"Image Files (*.png *.jpg *.jpeg *.svg *.ico *.icns *.bmp);;All Files (*)",
|
||||||
|
)
|
||||||
|
if file_path:
|
||||||
|
self.branding_logo_path_input.setText(file_path)
|
||||||
|
|
||||||
def _save_branding_as(self) -> None:
|
def _save_branding_as(self) -> None:
|
||||||
"""Save the edited branding as a new reusable template."""
|
"""Save the edited branding as a new reusable branding entry."""
|
||||||
template_id, ok = QInputDialog.getText(
|
branding_name, ok = QInputDialog.getText(
|
||||||
self,
|
self,
|
||||||
tr("settings.branding.save_as_title"),
|
tr("settings.branding.save_as_title"),
|
||||||
tr("settings.branding.save_as_prompt"),
|
tr("settings.branding.save_as_prompt"),
|
||||||
|
text=self.branding_display_name_input.text().strip(),
|
||||||
)
|
)
|
||||||
|
|
||||||
if not ok or not template_id:
|
if not ok or not branding_name:
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
template = self.branding_manager.build_template(
|
template = self.branding_manager.build_template(
|
||||||
template_id=template_id,
|
template_id=branding_name,
|
||||||
display_name=self.branding_display_name_input.text(),
|
display_name=branding_name,
|
||||||
app_name=self.branding_app_name_input.text(),
|
app_name=branding_name,
|
||||||
window_title=self.branding_window_title_input.text(),
|
|
||||||
logo_path=self.branding_logo_path_input.text(),
|
logo_path=self.branding_logo_path_input.text(),
|
||||||
)
|
)
|
||||||
self.branding_manager.save_template(template)
|
self.branding_manager.save_template(template)
|
||||||
|
|
|
||||||
|
|
@ -72,3 +72,28 @@ def test_config_from_env_uses_persisted_active_branding(tmp_path, monkeypatch):
|
||||||
assert config.active_branding_id == "agravity"
|
assert config.active_branding_id == "agravity"
|
||||||
assert config.app_name == "Agravity Bridge"
|
assert config.app_name == "Agravity Bridge"
|
||||||
assert config.get_config_path().name == "config.json"
|
assert config.get_config_path().name == "config.json"
|
||||||
|
|
||||||
|
|
||||||
|
def test_switching_back_to_default_restores_default_branding(tmp_path):
|
||||||
|
"""Switching from a custom branding back to default should restore the default name."""
|
||||||
|
manager = BrandingManager(base_dir=tmp_path)
|
||||||
|
config = Config(
|
||||||
|
app_name="WebDrop Bridge",
|
||||||
|
app_version="1.0.0",
|
||||||
|
log_level="INFO",
|
||||||
|
log_file=None,
|
||||||
|
allowed_roots=[],
|
||||||
|
allowed_urls=[],
|
||||||
|
webapp_url="http://localhost:8080",
|
||||||
|
enable_logging=True,
|
||||||
|
active_branding_id="agravity",
|
||||||
|
)
|
||||||
|
|
||||||
|
manager.apply_to_config(config)
|
||||||
|
assert config.app_name == "Agravity Bridge"
|
||||||
|
|
||||||
|
config.active_branding_id = "default"
|
||||||
|
manager.apply_to_config(config)
|
||||||
|
|
||||||
|
assert config.app_name == "WebDrop Bridge"
|
||||||
|
assert config.branding_display_name == "Default"
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,8 @@ class TestSettingsDialogInitialization:
|
||||||
qtbot.addWidget(dialog)
|
qtbot.addWidget(dialog)
|
||||||
|
|
||||||
assert dialog.branding_display_name_input.text() == "Default"
|
assert dialog.branding_display_name_input.text() == "Default"
|
||||||
assert dialog.branding_app_name_input.text() == "WebDrop Bridge"
|
assert dialog.branding_logo_path_input is not None
|
||||||
|
assert dialog.browse_branding_logo_btn is not None
|
||||||
|
|
||||||
def test_save_branding_as_creates_custom_template(
|
def test_save_branding_as_creates_custom_template(
|
||||||
self, qtbot, sample_config, monkeypatch, tmp_path
|
self, qtbot, sample_config, monkeypatch, tmp_path
|
||||||
|
|
@ -131,9 +132,9 @@ class TestSettingsDialogInitialization:
|
||||||
qtbot.addWidget(dialog)
|
qtbot.addWidget(dialog)
|
||||||
|
|
||||||
dialog.branding_display_name_input.setText("Customer A")
|
dialog.branding_display_name_input.setText("Customer A")
|
||||||
dialog.branding_app_name_input.setText("Customer A Bridge")
|
dialog.branding_logo_path_input.setText("/tmp/customer-logo.png")
|
||||||
|
|
||||||
with patch("PySide6.QtWidgets.QInputDialog.getText", return_value=("customer_a", True)):
|
with patch("PySide6.QtWidgets.QInputDialog.getText", return_value=("Customer A", True)):
|
||||||
dialog._save_branding_as()
|
dialog._save_branding_as()
|
||||||
|
|
||||||
assert dialog.branding_manager.has_template("customer_a")
|
assert dialog.branding_manager.has_template("customer_a")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue