feat: Add branding import/export functionality and enhance settings dialog with new fields
Some checks are pending
Tests & Quality Checks / Test on Python 3.11 (push) Waiting to run
Tests & Quality Checks / Test on Python 3.12 (push) Waiting to run
Tests & Quality Checks / Test on Python 3.11-1 (push) Waiting to run
Tests & Quality Checks / Test on Python 3.12-1 (push) Waiting to run
Tests & Quality Checks / Test on Python 3.10 (push) Waiting to run
Tests & Quality Checks / Test on Python 3.11-2 (push) Waiting to run
Tests & Quality Checks / Test on Python 3.12-2 (push) Waiting to run
Tests & Quality Checks / Build Artifacts (push) Blocked by required conditions
Tests & Quality Checks / Build Artifacts-1 (push) Blocked by required conditions

This commit is contained in:
claudi 2026-04-15 16:26:38 +02:00
parent b826bd9b20
commit 55f2ddf4b1
10 changed files with 296 additions and 10 deletions

View file

@ -99,7 +99,11 @@
"settings.branding.window_title_label": "Fenstertitel (optional):", "settings.branding.window_title_label": "Fenstertitel (optional):",
"settings.branding.logo_path_label": "Logo/Icon-Datei (optional):", "settings.branding.logo_path_label": "Logo/Icon-Datei (optional):",
"settings.branding.save_as_btn": "Branding speichern", "settings.branding.save_as_btn": "Branding speichern",
"settings.branding.export_btn": "Branding exportieren",
"settings.branding.import_btn": "Branding importieren",
"settings.branding.delete_btn": "Branding löschen", "settings.branding.delete_btn": "Branding löschen",
"settings.branding.export_title": "Branding exportieren",
"settings.branding.import_title": "Branding importieren",
"settings.branding.preview_label": "Vorschau:", "settings.branding.preview_label": "Vorschau:",
"settings.branding.no_icon_selected": "Kein Icon ausgewählt", "settings.branding.no_icon_selected": "Kein Icon ausgewählt",
"settings.branding.preview_default_name": "Default", "settings.branding.preview_default_name": "Default",

View file

@ -99,7 +99,11 @@
"settings.branding.window_title_label": "Window title (optional):", "settings.branding.window_title_label": "Window title (optional):",
"settings.branding.logo_path_label": "Logo/Icon file (optional):", "settings.branding.logo_path_label": "Logo/Icon file (optional):",
"settings.branding.save_as_btn": "Save Branding", "settings.branding.save_as_btn": "Save Branding",
"settings.branding.export_btn": "Export Branding",
"settings.branding.import_btn": "Import Branding",
"settings.branding.delete_btn": "Delete Branding", "settings.branding.delete_btn": "Delete Branding",
"settings.branding.export_title": "Export Branding",
"settings.branding.import_title": "Import Branding",
"settings.branding.preview_label": "Preview:", "settings.branding.preview_label": "Preview:",
"settings.branding.no_icon_selected": "No icon selected", "settings.branding.no_icon_selected": "No icon selected",
"settings.branding.preview_default_name": "Default", "settings.branding.preview_default_name": "Default",

View file

@ -99,7 +99,11 @@
"settings.branding.window_title_label": "Titre de la fenêtre (facultatif) :", "settings.branding.window_title_label": "Titre de la fenêtre (facultatif) :",
"settings.branding.logo_path_label": "Chemin du logo (facultatif) :", "settings.branding.logo_path_label": "Chemin du logo (facultatif) :",
"settings.branding.save_as_btn": "Enregistrer le branding", "settings.branding.save_as_btn": "Enregistrer le branding",
"settings.branding.export_btn": "Exporter le branding",
"settings.branding.import_btn": "Importer le branding",
"settings.branding.delete_btn": "Supprimer le branding", "settings.branding.delete_btn": "Supprimer le branding",
"settings.branding.export_title": "Exporter le branding",
"settings.branding.import_title": "Importer le branding",
"settings.branding.preview_label": "Aperçu :", "settings.branding.preview_label": "Aperçu :",
"settings.branding.no_icon_selected": "Aucune icône sélectionnée", "settings.branding.no_icon_selected": "Aucune icône sélectionnée",
"settings.branding.preview_default_name": "Default", "settings.branding.preview_default_name": "Default",

View file

@ -99,7 +99,11 @@
"settings.branding.window_title_label": "Titolo finestra (opzionale):", "settings.branding.window_title_label": "Titolo finestra (opzionale):",
"settings.branding.logo_path_label": "Percorso logo (opzionale):", "settings.branding.logo_path_label": "Percorso logo (opzionale):",
"settings.branding.save_as_btn": "Salva branding", "settings.branding.save_as_btn": "Salva branding",
"settings.branding.export_btn": "Esporta branding",
"settings.branding.import_btn": "Importa branding",
"settings.branding.delete_btn": "Elimina branding", "settings.branding.delete_btn": "Elimina branding",
"settings.branding.export_title": "Esporta branding",
"settings.branding.import_title": "Importa branding",
"settings.branding.preview_label": "Anteprima:", "settings.branding.preview_label": "Anteprima:",
"settings.branding.no_icon_selected": "Nessuna icona selezionata", "settings.branding.no_icon_selected": "Nessuna icona selezionata",
"settings.branding.preview_default_name": "Default", "settings.branding.preview_default_name": "Default",

View file

@ -99,7 +99,11 @@
"settings.branding.window_title_label": "Заголовок окна (необязательно):", "settings.branding.window_title_label": "Заголовок окна (необязательно):",
"settings.branding.logo_path_label": "Путь к логотипу (необязательно):", "settings.branding.logo_path_label": "Путь к логотипу (необязательно):",
"settings.branding.save_as_btn": "Сохранить брендинг", "settings.branding.save_as_btn": "Сохранить брендинг",
"settings.branding.export_btn": "Экспортировать брендинг",
"settings.branding.import_btn": "Импортировать брендинг",
"settings.branding.delete_btn": "Удалить брендинг", "settings.branding.delete_btn": "Удалить брендинг",
"settings.branding.export_title": "Экспортировать брендинг",
"settings.branding.import_title": "Импортировать брендинг",
"settings.branding.preview_label": "Предпросмотр:", "settings.branding.preview_label": "Предпросмотр:",
"settings.branding.no_icon_selected": "Значок не выбран", "settings.branding.no_icon_selected": "Значок не выбран",
"settings.branding.preview_default_name": "Default", "settings.branding.preview_default_name": "Default",

View file

@ -99,7 +99,11 @@
"settings.branding.window_title_label": "窗口标题(可选):", "settings.branding.window_title_label": "窗口标题(可选):",
"settings.branding.logo_path_label": "Logo 路径(可选):", "settings.branding.logo_path_label": "Logo 路径(可选):",
"settings.branding.save_as_btn": "保存品牌配置", "settings.branding.save_as_btn": "保存品牌配置",
"settings.branding.export_btn": "导出品牌配置",
"settings.branding.import_btn": "导入品牌配置",
"settings.branding.delete_btn": "删除品牌配置", "settings.branding.delete_btn": "删除品牌配置",
"settings.branding.export_title": "导出品牌配置",
"settings.branding.import_title": "导入品牌配置",
"settings.branding.preview_label": "预览:", "settings.branding.preview_label": "预览:",
"settings.branding.no_icon_selected": "未选择图标", "settings.branding.no_icon_selected": "未选择图标",
"settings.branding.preview_default_name": "Default", "settings.branding.preview_default_name": "Default",

View file

@ -6,6 +6,7 @@ import json
import logging import logging
import os import os
import platform import platform
import shutil
from dataclasses import asdict, dataclass from dataclasses import asdict, dataclass
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@ -15,6 +16,7 @@ from webdrop_bridge.config import DEFAULT_CONFIG_DIR_NAME, Config, Configuration
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
DEFAULT_BRANDING_TEMPLATE_ID = "default" DEFAULT_BRANDING_TEMPLATE_ID = "default"
DEFAULT_LOGO_PATH = "resources/icons/app.png"
SUPPORTED_LOGO_SUFFIXES = {".png", ".jpg", ".jpeg", ".bmp", ".svg", ".ico", ".icns"} SUPPORTED_LOGO_SUFFIXES = {".png", ".jpg", ".jpeg", ".bmp", ".svg", ".ico", ".icns"}
@ -83,6 +85,7 @@ BUILTIN_BRANDING_TEMPLATES: dict[str, BrandingTemplate] = {
display_name="Default", display_name="Default",
app_name="WebDrop Bridge", app_name="WebDrop Bridge",
window_title="", window_title="",
logo_path=DEFAULT_LOGO_PATH,
accent_color="#667eea", accent_color="#667eea",
), ),
"agravity": BrandingTemplate( "agravity": BrandingTemplate(
@ -90,6 +93,7 @@ BUILTIN_BRANDING_TEMPLATES: dict[str, BrandingTemplate] = {
display_name="Agravity", display_name="Agravity",
app_name="Agravity Bridge", app_name="Agravity Bridge",
window_title="", window_title="",
logo_path=DEFAULT_LOGO_PATH,
accent_color="#2d7d6e", accent_color="#2d7d6e",
), ),
} }
@ -103,6 +107,7 @@ class BrandingManager:
resolved_base = Path(env_dir).resolve() if env_dir and base_dir is None else base_dir resolved_base = Path(env_dir).resolve() if env_dir and base_dir is None else base_dir
self.base_dir = resolved_base or self._default_base_dir() self.base_dir = resolved_base or self._default_base_dir()
self.templates_dir = self.base_dir / "templates" self.templates_dir = self.base_dir / "templates"
self.assets_dir = self.base_dir / "assets"
self.active_branding_path = self.base_dir / "active_branding.json" self.active_branding_path = self.base_dir / "active_branding.json"
self.ensure_builtin_templates() self.ensure_builtin_templates()
@ -114,6 +119,7 @@ class BrandingManager:
def ensure_builtin_templates(self) -> None: def ensure_builtin_templates(self) -> None:
"""Ensure built-in templates exist on disk for discovery and later editing.""" """Ensure built-in templates exist on disk for discovery and later editing."""
self.templates_dir.mkdir(parents=True, exist_ok=True) self.templates_dir.mkdir(parents=True, exist_ok=True)
self.assets_dir.mkdir(parents=True, exist_ok=True)
for template in BUILTIN_BRANDING_TEMPLATES.values(): for template in BUILTIN_BRANDING_TEMPLATES.values():
template_path = self.templates_dir / f"{template.template_id}.json" template_path = self.templates_dir / f"{template.template_id}.json"
@ -165,10 +171,29 @@ class BrandingManager:
if template.template_id in BUILTIN_BRANDING_TEMPLATES: if template.template_id in BUILTIN_BRANDING_TEMPLATES:
raise ConfigurationError(f"Cannot overwrite built-in branding: {template.template_id}") raise ConfigurationError(f"Cannot overwrite built-in branding: {template.template_id}")
stored_logo_path = ""
if template.logo_path:
stored_logo_path = self._copy_logo_asset(template.logo_path, template.template_id)
stored_template = BrandingTemplate(
template_id=template.template_id,
display_name=template.display_name,
app_name=template.app_name,
window_title=template.window_title,
logo_path=stored_logo_path or template.logo_path,
app_icon_path_windows=stored_logo_path or template.app_icon_path_windows,
app_icon_path_macos=stored_logo_path or template.app_icon_path_macos,
toolbar_icon_home=template.toolbar_icon_home,
toolbar_icon_reload=template.toolbar_icon_reload,
toolbar_icon_open=template.toolbar_icon_open,
toolbar_icon_openwith=template.toolbar_icon_openwith,
accent_color=template.accent_color,
)
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"{stored_template.template_id}.json"
template_path.write_text(json.dumps(template.to_dict(), indent=2), encoding="utf-8") template_path.write_text(json.dumps(stored_template.to_dict(), indent=2), encoding="utf-8")
logger.info("Branding template saved: %s", template.template_id) logger.info("Branding template saved: %s", stored_template.template_id)
return template_path return template_path
def build_template( def build_template(
@ -191,8 +216,8 @@ class BrandingManager:
if not safe_name: if not safe_name:
raise ConfigurationError("Branding requires a display name") raise ConfigurationError("Branding requires a display name")
if logo: if logo:
logo_file = Path(logo) logo_file = self._resolve_asset_path(logo)
if not logo_file.exists() or not logo_file.is_file(): if logo_file is None or not logo_file.exists() or not logo_file.is_file():
raise ConfigurationError(f"Logo file not found: {logo}") raise ConfigurationError(f"Logo file not found: {logo}")
if logo_file.suffix.lower() not in SUPPORTED_LOGO_SUFFIXES: if logo_file.suffix.lower() not in SUPPORTED_LOGO_SUFFIXES:
raise ConfigurationError( raise ConfigurationError(
@ -214,6 +239,97 @@ class BrandingManager:
"""Convert a human-readable branding name into a stable id.""" """Convert a human-readable branding name into a stable id."""
return "".join(c.lower() if c.isalnum() else "_" for c in value).strip("_") return "".join(c.lower() if c.isalnum() else "_" for c in value).strip("_")
@staticmethod
def _resolve_asset_path(configured_path: str) -> Path | None:
"""Resolve a branding asset path in dev and packaged layouts."""
if not configured_path:
return None
path = Path(configured_path)
candidates = [path] if path.is_absolute() else [Path.cwd() / path]
if not path.is_absolute():
project_root = Path(__file__).resolve().parents[3]
candidates.append(project_root / path)
for candidate in candidates:
if candidate.exists():
return candidate
return None
def _copy_logo_asset(self, configured_path: str, template_id: str) -> str:
"""Copy a user-selected logo into managed branding storage."""
resolved_path = self._resolve_asset_path(configured_path)
if resolved_path is None or not resolved_path.exists() or not resolved_path.is_file():
raise ConfigurationError(f"Logo file not found: {configured_path}")
self.assets_dir.mkdir(parents=True, exist_ok=True)
target_path = self.assets_dir / f"{template_id}{resolved_path.suffix.lower()}"
if resolved_path.resolve() != target_path.resolve():
shutil.copy2(resolved_path, target_path)
return str(target_path)
def export_template(self, template_id: str, export_path: Path) -> Path:
"""Export a branding into a shareable JSON file plus optional logo asset."""
template = self.load_template(template_id)
export_path.parent.mkdir(parents=True, exist_ok=True)
export_data = template.to_dict()
if template.logo_path:
resolved_logo = self._resolve_asset_path(template.logo_path)
if resolved_logo and resolved_logo.exists() and resolved_logo.is_file():
export_logo_path = export_path.parent / resolved_logo.name
if resolved_logo.resolve() != export_logo_path.resolve():
shutil.copy2(resolved_logo, export_logo_path)
export_data["logo_path"] = export_logo_path.name
export_data["app_icon_path_windows"] = export_logo_path.name
export_data["app_icon_path_macos"] = export_logo_path.name
export_path.write_text(json.dumps(export_data, indent=2), encoding="utf-8")
logger.info("Branding template exported: %s -> %s", template_id, export_path)
return export_path
def import_template(self, import_path: Path) -> BrandingTemplate:
"""Import a branding from a previously exported JSON file."""
if not import_path.exists() or not import_path.is_file():
raise ConfigurationError(f"Branding import file not found: {import_path}")
try:
data = json.loads(import_path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
raise ConfigurationError(f"Invalid branding import file: {import_path}") from exc
template = BrandingTemplate.from_dict(data)
imported_template_id = template.template_id
if imported_template_id in BUILTIN_BRANDING_TEMPLATES:
imported_template_id = self._slugify(f"{template.display_name}_imported")
imported_logo_path = ""
if template.logo_path:
candidate_logo = Path(template.logo_path)
if not candidate_logo.is_absolute():
candidate_logo = import_path.parent / candidate_logo
imported_logo_path = self._copy_logo_asset(str(candidate_logo), imported_template_id)
imported_template = BrandingTemplate(
template_id=imported_template_id,
display_name=template.display_name,
app_name=template.app_name,
window_title=template.window_title,
logo_path=imported_logo_path,
app_icon_path_windows=imported_logo_path or template.app_icon_path_windows,
app_icon_path_macos=imported_logo_path or template.app_icon_path_macos,
toolbar_icon_home=template.toolbar_icon_home,
toolbar_icon_reload=template.toolbar_icon_reload,
toolbar_icon_open=template.toolbar_icon_open,
toolbar_icon_openwith=template.toolbar_icon_openwith,
accent_color=template.accent_color,
)
self.save_template(imported_template)
logger.info("Branding template imported: %s", imported_template.template_id)
return imported_template
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

@ -198,6 +198,20 @@ 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()
self.branding_app_name_input.setPlaceholderText(tr("settings.branding.app_name_label"))
self.branding_app_name_input.textChanged.connect(self._update_branding_preview)
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")
)
self.branding_window_title_input.textChanged.connect(self._update_branding_preview)
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"))) layout.addWidget(QLabel(tr("settings.branding.logo_path_label")))
logo_layout = QHBoxLayout() logo_layout = QHBoxLayout()
self.branding_logo_path_input = QLineEdit() self.branding_logo_path_input = QLineEdit()
@ -215,6 +229,10 @@ class SettingsDialog(QDialog):
self.branding_preview_name_label.setStyleSheet("font-weight: bold;") self.branding_preview_name_label.setStyleSheet("font-weight: bold;")
layout.addWidget(self.branding_preview_name_label) layout.addWidget(self.branding_preview_name_label)
self.branding_preview_title_label = QLabel()
self.branding_preview_title_label.setStyleSheet("color: gray;")
layout.addWidget(self.branding_preview_title_label)
self.branding_preview_icon_label = QLabel(tr("settings.branding.no_icon_selected")) self.branding_preview_icon_label = QLabel(tr("settings.branding.no_icon_selected"))
self.branding_preview_icon_label.setFixedSize(72, 72) self.branding_preview_icon_label.setFixedSize(72, 72)
self.branding_preview_icon_label.setStyleSheet( self.branding_preview_icon_label.setStyleSheet(
@ -227,6 +245,14 @@ class SettingsDialog(QDialog):
self.save_branding_as_btn.clicked.connect(self._save_branding_as) self.save_branding_as_btn.clicked.connect(self._save_branding_as)
branding_button_layout.addWidget(self.save_branding_as_btn) branding_button_layout.addWidget(self.save_branding_as_btn)
self.export_branding_btn = QPushButton(tr("settings.branding.export_btn"))
self.export_branding_btn.clicked.connect(self._export_branding)
branding_button_layout.addWidget(self.export_branding_btn)
self.import_branding_btn = QPushButton(tr("settings.branding.import_btn"))
self.import_branding_btn.clicked.connect(self._import_branding)
branding_button_layout.addWidget(self.import_branding_btn)
self.delete_branding_btn = QPushButton(tr("settings.branding.delete_btn")) self.delete_branding_btn = QPushButton(tr("settings.branding.delete_btn"))
self.delete_branding_btn.clicked.connect(self._delete_branding) self.delete_branding_btn.clicked.connect(self._delete_branding)
branding_button_layout.addWidget(self.delete_branding_btn) branding_button_layout.addWidget(self.delete_branding_btn)
@ -262,9 +288,30 @@ class SettingsDialog(QDialog):
"""Load the selected branding into the editable fields.""" """Load the selected branding 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_logo_path_input.setText(template.logo_path) self.branding_app_name_input.setText(template.app_name)
self.branding_window_title_input.setText(
template.window_title or f"{template.app_name} v{self.config.app_version}"
)
self.branding_logo_path_input.setText(template.logo_path or template.get_app_icon_path())
self._update_branding_preview() self._update_branding_preview()
def _resolve_branding_preview_path(self, configured_path: str) -> Optional[Path]:
"""Resolve a branding preview path in both dev and packaged layouts."""
if not configured_path:
return None
path = Path(configured_path)
candidates = [path] if path.is_absolute() else [Path.cwd() / path]
if not path.is_absolute():
project_root = Path(__file__).resolve().parents[3]
candidates.append(project_root / path)
for candidate in candidates:
if candidate.exists() and candidate.is_file():
return candidate
return None
def _update_branding_preview(self) -> None: def _update_branding_preview(self) -> None:
"""Refresh the small branding preview for name and icon.""" """Refresh the small branding preview for name and icon."""
display_name = self.branding_display_name_input.text().strip() or tr( display_name = self.branding_display_name_input.text().strip() or tr(
@ -272,11 +319,17 @@ class SettingsDialog(QDialog):
) )
self.branding_preview_name_label.setText(display_name) self.branding_preview_name_label.setText(display_name)
effective_title = self.branding_window_title_input.text().strip() or (
self.branding_app_name_input.text().strip() or display_name
)
self.branding_preview_title_label.setText(effective_title)
logo_path = self.branding_logo_path_input.text().strip() logo_path = self.branding_logo_path_input.text().strip()
if logo_path and Path(logo_path).exists(): resolved_logo_path = self._resolve_branding_preview_path(logo_path)
pixmap = QPixmap(logo_path) if resolved_logo_path:
pixmap = QPixmap(str(resolved_logo_path))
if pixmap.isNull(): if pixmap.isNull():
icon = QIcon(logo_path) icon = QIcon(str(resolved_logo_path))
pixmap = icon.pixmap(64, 64) pixmap = icon.pixmap(64, 64)
if not pixmap.isNull(): if not pixmap.isNull():
@ -318,10 +371,13 @@ class SettingsDialog(QDialog):
try: try:
display_name = self.branding_display_name_input.text().strip() or branding_name display_name = self.branding_display_name_input.text().strip() or branding_name
app_name = self.branding_app_name_input.text().strip() or display_name
window_title = self.branding_window_title_input.text().strip()
template = self.branding_manager.build_template( template = self.branding_manager.build_template(
template_id=branding_name, template_id=branding_name,
display_name=display_name, display_name=display_name,
app_name=display_name, app_name=app_name,
window_title=window_title,
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)
@ -330,6 +386,44 @@ class SettingsDialog(QDialog):
except ConfigurationError as e: except ConfigurationError as e:
self._show_error(f"Failed to save branding: {e}") self._show_error(f"Failed to save branding: {e}")
def _export_branding(self) -> None:
"""Export the selected branding so it can be shared with other users."""
template_id = self.branding_combo.currentData()
if not template_id:
return
file_path, _ = QFileDialog.getSaveFileName(
self,
tr("settings.branding.export_title"),
str(Path.home() / f"{template_id}.json"),
"JSON Files (*.json);;All Files (*)",
)
if not file_path:
return
try:
self.branding_manager.export_template(template_id, Path(file_path))
except ConfigurationError as e:
self._show_error(f"Failed to export branding: {e}")
def _import_branding(self) -> None:
"""Import a branding package from another user."""
file_path, _ = QFileDialog.getOpenFileName(
self,
tr("settings.branding.import_title"),
str(Path.home()),
"JSON Files (*.json);;All Files (*)",
)
if not file_path:
return
try:
template = self.branding_manager.import_template(Path(file_path))
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 import branding: {e}")
def _delete_branding(self) -> None: def _delete_branding(self) -> None:
"""Delete the currently selected custom branding.""" """Delete the currently selected custom branding."""
template_id = self.branding_combo.currentData() template_id = self.branding_combo.currentData()

View file

@ -1,5 +1,7 @@
"""Tests for runtime branding template management.""" """Tests for runtime branding template management."""
from pathlib import Path
import pytest import pytest
from webdrop_bridge.config import Config, ConfigurationError from webdrop_bridge.config import Config, ConfigurationError
@ -114,6 +116,21 @@ def test_delete_custom_branding_removes_it(tmp_path):
assert not manager.has_template("customer_b") assert not manager.has_template("customer_b")
def test_build_template_preserves_app_and_window_titles(tmp_path):
"""Custom brandings should keep their editable app and window title values."""
manager = BrandingManager(base_dir=tmp_path)
template = manager.build_template(
template_id="Customer C",
display_name="Customer C",
app_name="Customer Bridge",
window_title="Customer Bridge Desktop",
)
assert template.app_name == "Customer Bridge"
assert template.window_title == "Customer Bridge Desktop"
def test_invalid_logo_file_is_rejected(tmp_path): def test_invalid_logo_file_is_rejected(tmp_path):
"""Non-existent logo files should not be accepted for saved brandings.""" """Non-existent logo files should not be accepted for saved brandings."""
manager = BrandingManager(base_dir=tmp_path) manager = BrandingManager(base_dir=tmp_path)
@ -124,3 +141,32 @@ def test_invalid_logo_file_is_rejected(tmp_path):
display_name="Customer C", display_name="Customer C",
logo_path=str(tmp_path / "missing-logo.png"), logo_path=str(tmp_path / "missing-logo.png"),
) )
def test_exported_branding_can_be_imported_for_another_user(tmp_path):
"""Exported brandings should be shareable and importable by another user."""
source_logo = tmp_path / "shared-logo.png"
source_logo.write_bytes(b"fake-png-data")
source_manager = BrandingManager(base_dir=tmp_path / "source")
template = source_manager.build_template(
template_id="Customer D",
display_name="Customer D",
app_name="Customer Bridge",
window_title="Customer Window",
logo_path=str(source_logo),
)
source_manager.save_template(template)
export_path = tmp_path / "export" / "customer_d.json"
source_manager.export_template("customer_d", export_path)
target_manager = BrandingManager(base_dir=tmp_path / "target")
imported = target_manager.import_template(export_path)
assert imported.template_id == "customer_d"
assert imported.display_name == "Customer D"
assert imported.app_name == "Customer Bridge"
assert imported.window_title == "Customer Window"
assert Path(imported.logo_path).exists()
assert target_manager.has_template("customer_d")

View file

@ -120,9 +120,15 @@ 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 "WebDrop Bridge" in dialog.branding_window_title_input.text()
assert dialog.branding_logo_path_input is not None assert dialog.branding_logo_path_input is not None
assert dialog.browse_branding_logo_btn is not None assert dialog.browse_branding_logo_btn is not None
assert dialog.branding_preview_name_label.text() == "Default" assert dialog.branding_preview_name_label.text() == "Default"
assert dialog.branding_preview_icon_label.pixmap() is not None
assert not dialog.branding_preview_icon_label.pixmap().isNull()
assert dialog.export_branding_btn is not None
assert dialog.import_branding_btn is not None
assert dialog.delete_branding_btn is not None assert dialog.delete_branding_btn is not None
def test_save_branding_as_creates_custom_template( def test_save_branding_as_creates_custom_template(