diff --git a/resources/translations/de.json b/resources/translations/de.json index e70acca..a041f31 100644 --- a/resources/translations/de.json +++ b/resources/translations/de.json @@ -91,16 +91,16 @@ "settings.tab.profiles": "Setups", "settings.tab.general": "Allgemein", "settings.tab.branding": "Branding", - "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.select_label": "Branding:", + "settings.branding.select_tooltip": "Wählen Sie das Branding, das beim Start automatisch geladen werden soll.", + "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": "Name:", "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.logo_path_label": "Logo/Icon-Datei (optional):", + "settings.branding.save_as_btn": "Branding speichern", + "settings.branding.save_as_title": "Branding speichern", + "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.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 9e0b46e..1a471f6 100644 --- a/resources/translations/en.json +++ b/resources/translations/en.json @@ -91,16 +91,16 @@ "settings.tab.profiles": "Setups", "settings.tab.general": "General", "settings.tab.branding": "Branding", - "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.select_label": "Branding:", + "settings.branding.select_tooltip": "Choose the branding that should be loaded automatically on startup.", + "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": "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.logo_path_label": "Logo/Icon file (optional):", + "settings.branding.save_as_btn": "Save Branding", + "settings.branding.save_as_title": "Save Branding", + "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.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 3032138..ae6b260 100644 --- a/resources/translations/fr.json +++ b/resources/translations/fr.json @@ -91,7 +91,7 @@ "settings.tab.profiles": "Configs", "settings.tab.general": "G\u00e9n\u00e9ral", "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.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 :", diff --git a/resources/translations/it.json b/resources/translations/it.json index 8046651..3d1cb5f 100644 --- a/resources/translations/it.json +++ b/resources/translations/it.json @@ -91,7 +91,7 @@ "settings.tab.profiles": "Config", "settings.tab.general": "Generale", "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.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:", diff --git a/resources/translations/ru.json b/resources/translations/ru.json index 7e4d462..7e17cf0 100644 --- a/resources/translations/ru.json +++ b/resources/translations/ru.json @@ -91,7 +91,7 @@ "settings.tab.profiles": "Наборы", "settings.tab.general": "Общие настройки", "settings.tab.branding": "Брендинг", - "settings.branding.select_label": "Шаблон брендинга:", + "settings.branding.select_label": "Брендинг:", "settings.branding.select_tooltip": "Выберите шаблон брендинга, который должен автоматически загружаться при запуске.", "settings.branding.help_text": "Брендинг управляет визуальной идентичностью приложения, например названием и иконками. Он отделен от сохраненных наборов настроек.", "settings.branding.display_name_label": "Отображаемое имя:", diff --git a/resources/translations/zh.json b/resources/translations/zh.json index ea3ce6b..85c305b 100644 --- a/resources/translations/zh.json +++ b/resources/translations/zh.json @@ -91,7 +91,7 @@ "settings.tab.profiles": "设置", "settings.tab.general": "通用", "settings.tab.branding": "品牌", - "settings.branding.select_label": "品牌模板:", + "settings.branding.select_label": "品牌:", "settings.branding.select_tooltip": "选择应用启动时应自动加载的品牌模板。", "settings.branding.help_text": "品牌控制应用的视觉标识,例如名称和图标,并与已保存的设置保持分离。", "settings.branding.display_name_label": "显示名称:", diff --git a/src/webdrop_bridge/core/branding_manager.py b/src/webdrop_bridge/core/branding_manager.py index 8408f33..d682938 100644 --- a/src/webdrop_bridge/core/branding_manager.py +++ b/src/webdrop_bridge/core/branding_manager.py @@ -161,6 +161,8 @@ class BrandingManager: """Save or update a branding template on disk.""" if not template.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) template_path = self.templates_dir / f"{template.template_id}.json" @@ -173,19 +175,31 @@ class BrandingManager: *, template_id: str, display_name: str, - app_name: str, + app_name: str = "", window_title: str = "", logo_path: str = "", ) -> BrandingTemplate: """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( - template_id=template_id.strip(), - display_name=display_name.strip(), - app_name=app_name.strip(), + template_id=safe_id, + display_name=safe_name, + app_name=resolved_app_name, 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: """Delete a user template while protecting built-ins.""" if template_id in BUILTIN_BRANDING_TEMPLATES: @@ -229,20 +243,24 @@ class BrandingManager: requested_id = self.get_active_branding_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 + 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.branding_display_name = template.display_name - if is_non_default_branding or not config.app_name or config.app_name == default_app_name: - config.app_name = template.app_name + if ( + 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 ( - is_non_default_branding + template.template_id != DEFAULT_BRANDING_TEMPLATE_ID or not config.window_title - or config.window_title.startswith(f"{default_app_name} v") - or config.window_title.startswith(f"{config.app_name} v") + or any(config.window_title.startswith(prefix) for prefix in known_title_prefixes) ): config.window_title = ( template.window_title or f"{config.app_name} v{config.app_version}" diff --git a/src/webdrop_bridge/ui/settings_dialog.py b/src/webdrop_bridge/ui/settings_dialog.py index 201349e..cf2d555 100644 --- a/src/webdrop_bridge/ui/settings_dialog.py +++ b/src/webdrop_bridge/ui/settings_dialog.py @@ -196,22 +196,16 @@ class SettingsDialog(QDialog): 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) - + layout.addWidget(QLabel(tr("settings.branding.logo_path_label"))) + logo_layout = QHBoxLayout() 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) + logo_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() 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.""" 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: @@ -259,23 +251,34 @@ class SettingsDialog(QDialog): if 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: - """Save the edited branding as a new reusable template.""" - template_id, ok = QInputDialog.getText( + """Save the edited branding as a new reusable branding entry.""" + branding_name, ok = QInputDialog.getText( self, tr("settings.branding.save_as_title"), 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 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(), + template_id=branding_name, + display_name=branding_name, + app_name=branding_name, logo_path=self.branding_logo_path_input.text(), ) self.branding_manager.save_template(template) diff --git a/tests/unit/test_branding_manager.py b/tests/unit/test_branding_manager.py index 891ce93..2f08abe 100644 --- a/tests/unit/test_branding_manager.py +++ b/tests/unit/test_branding_manager.py @@ -72,3 +72,28 @@ def test_config_from_env_uses_persisted_active_branding(tmp_path, monkeypatch): assert config.active_branding_id == "agravity" assert config.app_name == "Agravity Bridge" 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" diff --git a/tests/unit/test_settings_dialog.py b/tests/unit/test_settings_dialog.py index 80df965..e7a973d 100644 --- a/tests/unit/test_settings_dialog.py +++ b/tests/unit/test_settings_dialog.py @@ -120,7 +120,8 @@ class TestSettingsDialogInitialization: qtbot.addWidget(dialog) 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( self, qtbot, sample_config, monkeypatch, tmp_path @@ -131,9 +132,9 @@ class TestSettingsDialogInitialization: qtbot.addWidget(dialog) 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() assert dialog.branding_manager.has_template("customer_a")