diff --git a/resources/translations/de.json b/resources/translations/de.json index 5be5e9e..7be1ed0 100644 --- a/resources/translations/de.json +++ b/resources/translations/de.json @@ -58,10 +58,6 @@ "dialog.language_changed.msg": "Die Spracheinstellung wurde aktualisiert. Starten Sie jetzt neu, um die ausgew\u00e4hlte Sprache \u00fcberall anzuwenden.", "dialog.language_changed.restart_now": "Jetzt neu starten", "dialog.language_changed.restart_later": "Sp\u00e4ter neu starten", - "dialog.branding_changed.title": "Branding ge\u00e4ndert", - "dialog.branding_changed.msg": "Das aktive Branding wurde geändert. Starten Sie jetzt neu, damit die aktualisierte visuelle Identität überall angewendet wird.", - "dialog.branding_changed.restart_now": "Jetzt neu starten", - "dialog.branding_changed.restart_later": "Sp\u00e4ter neu starten", "dialog.restart_failed.title": "Neustart fehlgeschlagen", "dialog.restart_failed.msg": "Die Anwendung konnte nicht automatisch neu gestartet werden:\n\n{error}\n\nBitte starten Sie manuell neu.", "dialog.update_timeout.title": "Zeitüberschreitung bei der Update-Pr\u00fcfung", @@ -90,26 +86,6 @@ "settings.tab.window": "Fenster", "settings.tab.profiles": "Setups", "settings.tab.general": "Allgemein", - "settings.tab.branding": "Branding", - "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": "Logo/Icon-Datei (optional):", - "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.export_title": "Branding exportieren", - "settings.branding.import_title": "Branding importieren", - "settings.branding.preview_label": "Vorschau:", - "settings.branding.no_icon_selected": "Kein Icon ausgewählt", - "settings.branding.preview_default_name": "Default", - "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", "settings.web_url.open_btn": "\u00d6ffnen", diff --git a/resources/translations/en.json b/resources/translations/en.json index 799c546..57798d5 100644 --- a/resources/translations/en.json +++ b/resources/translations/en.json @@ -58,10 +58,6 @@ "dialog.language_changed.msg": "The language setting was updated. Restart now to apply the selected language everywhere.", "dialog.language_changed.restart_now": "Restart Now", "dialog.language_changed.restart_later": "Restart Later", - "dialog.branding_changed.title": "Branding Changed", - "dialog.branding_changed.msg": "The active branding was changed. Restart now so the updated visual identity is applied everywhere.", - "dialog.branding_changed.restart_now": "Restart Now", - "dialog.branding_changed.restart_later": "Restart Later", "dialog.restart_failed.title": "Restart Failed", "dialog.restart_failed.msg": "Could not automatically restart the application:\n\n{error}\n\nPlease restart manually.", "dialog.update_timeout.title": "Update Check Timeout", @@ -90,26 +86,6 @@ "settings.tab.window": "Window", "settings.tab.profiles": "Setups", "settings.tab.general": "General", - "settings.tab.branding": "Branding", - "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/Icon file (optional):", - "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.export_title": "Export Branding", - "settings.branding.import_title": "Import Branding", - "settings.branding.preview_label": "Preview:", - "settings.branding.no_icon_selected": "No icon selected", - "settings.branding.preview_default_name": "Default", - "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", "settings.web_url.open_btn": "Open", diff --git a/resources/translations/fr.json b/resources/translations/fr.json index 9369246..d024e21 100644 --- a/resources/translations/fr.json +++ b/resources/translations/fr.json @@ -58,10 +58,6 @@ "dialog.language_changed.msg": "Le param\u00e8tre de langue a \u00e9t\u00e9 mis \u00e0 jour. Red\u00e9marrez maintenant pour appliquer la langue s\u00e9lectionn\u00e9e partout.", "dialog.language_changed.restart_now": "Red\u00e9marrer maintenant", "dialog.language_changed.restart_later": "Red\u00e9marrer plus tard", - "dialog.branding_changed.title": "Branding modifié", - "dialog.branding_changed.msg": "Le branding actif a été modifié. Redémarrez maintenant pour appliquer partout l’identité visuelle mise à jour.", - "dialog.branding_changed.restart_now": "Redémarrer maintenant", - "dialog.branding_changed.restart_later": "Redémarrer plus tard", "dialog.restart_failed.title": "\u00c9chec du red\u00e9marrage", "dialog.restart_failed.msg": "Impossible de red\u00e9marrer automatiquement l'application\u00a0:\n\n{error}\n\nVeuillez red\u00e9marrer manuellement.", "dialog.update_timeout.title": "D\u00e9lai de v\u00e9rification des mises \u00e0 jour d\u00e9pass\u00e9", @@ -90,26 +86,6 @@ "settings.tab.window": "Fen\u00eatre", "settings.tab.profiles": "Configs", "settings.tab.general": "G\u00e9n\u00e9ral", - "settings.tab.branding": "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 :", - "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 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.export_title": "Exporter le branding", - "settings.branding.import_title": "Importer le branding", - "settings.branding.preview_label": "Aperçu :", - "settings.branding.no_icon_selected": "Aucune icône sélectionnée", - "settings.branding.preview_default_name": "Default", - "settings.branding.save_as_title": "Enregistrer le branding", - "settings.branding.save_as_prompt": "Entrez un nom pour le branding :", - "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", "settings.web_url.open_btn": "Ouvrir", diff --git a/resources/translations/it.json b/resources/translations/it.json index 0ad9c7d..25e026b 100644 --- a/resources/translations/it.json +++ b/resources/translations/it.json @@ -58,10 +58,6 @@ "dialog.language_changed.msg": "La lingua è stata aggiornata. Riavvia ora per applicarla ovunque.", "dialog.language_changed.restart_now": "Riavvia ora", "dialog.language_changed.restart_later": "Riavvia più tardi", - "dialog.branding_changed.title": "Branding cambiato", - "dialog.branding_changed.msg": "Il branding attivo è stato modificato. Riavvia ora per applicare ovunque l’identità visiva aggiornata.", - "dialog.branding_changed.restart_now": "Riavvia ora", - "dialog.branding_changed.restart_later": "Riavvia più tardi", "dialog.restart_failed.title": "Riavvio non riuscito", "dialog.restart_failed.msg": "Impossibile riavviare automaticamente l'applicazione:\n\n{error}\n\nRiavvia manualmente.", "dialog.update_timeout.title": "Timeout controllo aggiornamenti", @@ -90,26 +86,6 @@ "settings.tab.window": "Finestra", "settings.tab.profiles": "Config", "settings.tab.general": "Generale", - "settings.tab.branding": "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:", - "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 branding", - "settings.branding.export_btn": "Esporta branding", - "settings.branding.import_btn": "Importa 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.no_icon_selected": "Nessuna icona selezionata", - "settings.branding.preview_default_name": "Default", - "settings.branding.save_as_title": "Salva branding", - "settings.branding.save_as_prompt": "Inserisci un nome per il branding:", - "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", "settings.web_url.open_btn": "Apri", diff --git a/resources/translations/ru.json b/resources/translations/ru.json index da0b804..a7abdec 100644 --- a/resources/translations/ru.json +++ b/resources/translations/ru.json @@ -58,10 +58,6 @@ "dialog.language_changed.msg": "Настройка языка обновлена. Перезапустите сейчас, чтобы применить язык везде.", "dialog.language_changed.restart_now": "Перезапустить сейчас", "dialog.language_changed.restart_later": "Перезапустить позже", - "dialog.branding_changed.title": "Брендинг изменен", - "dialog.branding_changed.msg": "Активный брендинг был изменен. Перезапустите приложение сейчас, чтобы обновленная визуальная идентичность применялась везде.", - "dialog.branding_changed.restart_now": "Перезапустить сейчас", - "dialog.branding_changed.restart_later": "Перезапустить позже", "dialog.restart_failed.title": "Сбой перезапуска", "dialog.restart_failed.msg": "Не удалось автоматически перезапустить приложение:\n\n{error}\n\nПерезапустите вручную.", "dialog.update_timeout.title": "Таймаут проверки обновлений", @@ -90,26 +86,6 @@ "settings.tab.window": "Окно", "settings.tab.profiles": "Наборы", "settings.tab.general": "Общие настройки", - "settings.tab.branding": "Брендинг", - "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.export_btn": "Экспортировать брендинг", - "settings.branding.import_btn": "Импортировать брендинг", - "settings.branding.delete_btn": "Удалить брендинг", - "settings.branding.export_title": "Экспортировать брендинг", - "settings.branding.import_title": "Импортировать брендинг", - "settings.branding.preview_label": "Предпросмотр:", - "settings.branding.no_icon_selected": "Значок не выбран", - "settings.branding.preview_default_name": "Default", - "settings.branding.save_as_title": "Сохранить брендинг", - "settings.branding.save_as_prompt": "Введите имя для брендинга:", - "settings.branding.restart_note": "Изменения брендинга сохраняются постоянно и будут полностью применены после перезапуска приложения.", "settings.web_url.label": "URL веб-приложения:", "settings.web_url.placeholder": "например, http://localhost:8080 или file:///./webapp/index.html", "settings.web_url.open_btn": "Открыть", diff --git a/resources/translations/zh.json b/resources/translations/zh.json index f3e61fe..ea2686b 100644 --- a/resources/translations/zh.json +++ b/resources/translations/zh.json @@ -58,10 +58,6 @@ "dialog.language_changed.msg": "语言设置已更新。立即重启可在所有界面生效。", "dialog.language_changed.restart_now": "立即重启", "dialog.language_changed.restart_later": "稍后重启", - "dialog.branding_changed.title": "品牌已更改", - "dialog.branding_changed.msg": "当前品牌配置已更改。请立即重启,以便在所有界面应用更新后的视觉标识。", - "dialog.branding_changed.restart_now": "立即重启", - "dialog.branding_changed.restart_later": "稍后重启", "dialog.restart_failed.title": "重启失败", "dialog.restart_failed.msg": "无法自动重启应用:\n\n{error}\n\n请手动重启。", "dialog.update_timeout.title": "更新检查超时", @@ -90,26 +86,6 @@ "settings.tab.window": "窗口", "settings.tab.profiles": "设置", "settings.tab.general": "通用", - "settings.tab.branding": "品牌", - "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.export_btn": "导出品牌配置", - "settings.branding.import_btn": "导入品牌配置", - "settings.branding.delete_btn": "删除品牌配置", - "settings.branding.export_title": "导出品牌配置", - "settings.branding.import_title": "导入品牌配置", - "settings.branding.preview_label": "预览:", - "settings.branding.no_icon_selected": "未选择图标", - "settings.branding.preview_default_name": "Default", - "settings.branding.save_as_title": "保存品牌配置", - "settings.branding.save_as_prompt": "输入品牌名称:", - "settings.branding.restart_note": "品牌更改会被持久保存,并将在应用重启后完整生效。", "settings.web_url.label": "Web 应用 URL:", "settings.web_url.placeholder": "例如: http://localhost:8080 或 file:///./webapp/index.html", "settings.web_url.open_btn": "打开", diff --git a/src/webdrop_bridge/config.py b/src/webdrop_bridge/config.py index d8a5740..12bdeb7 100644 --- a/src/webdrop_bridge/config.py +++ b/src/webdrop_bridge/config.py @@ -18,12 +18,6 @@ DEFAULT_UPDATE_BASE_URL = "https://git.him-tools.de" DEFAULT_UPDATE_REPO = "HIM-public/webdrop-bridge" DEFAULT_UPDATE_CHANNEL = "stable" DEFAULT_UPDATE_MANIFEST_NAME = "release-manifest.json" -DEFAULT_ACTIVE_BRANDING_ID = "default" -DEFAULT_APP_ICON_PATH = "resources/icons/app.ico" -DEFAULT_TOOLBAR_ICON_HOME = "resources/icons/home.ico" -DEFAULT_TOOLBAR_ICON_RELOAD = "resources/icons/reload.ico" -DEFAULT_TOOLBAR_ICON_OPEN = "resources/icons/open.ico" -DEFAULT_TOOLBAR_ICON_OPENWITH = "resources/icons/openwith.ico" class ConfigurationError(Exception): @@ -102,14 +96,6 @@ class Config: enable_logging: bool = True enable_checkout: bool = False language: str = "auto" - active_branding_id: str = DEFAULT_ACTIVE_BRANDING_ID - branding_display_name: str = "Default" - logo_path: str = "" - app_icon_path: str = DEFAULT_APP_ICON_PATH - toolbar_icon_home: str = DEFAULT_TOOLBAR_ICON_HOME - toolbar_icon_reload: str = DEFAULT_TOOLBAR_ICON_RELOAD - toolbar_icon_open: str = DEFAULT_TOOLBAR_ICON_OPEN - toolbar_icon_openwith: str = DEFAULT_TOOLBAR_ICON_OPENWITH brand_id: str = DEFAULT_BRAND_ID config_dir_name: str = DEFAULT_CONFIG_DIR_NAME update_base_url: str = DEFAULT_UPDATE_BASE_URL @@ -193,7 +179,7 @@ class Config: # No window title specified, use default window_title = f"{app_name} v{__version__}" - config = cls( + return cls( app_name=app_name, app_version=__version__, log_level=data.get("log_level", "INFO").upper(), @@ -211,13 +197,6 @@ class Config: enable_logging=data.get("enable_logging", True), enable_checkout=data.get("enable_checkout", False), language=data.get("language", "auto"), - active_branding_id=data.get("active_branding_id", DEFAULT_ACTIVE_BRANDING_ID), - logo_path=data.get("logo_path", ""), - app_icon_path=data.get("app_icon_path", DEFAULT_APP_ICON_PATH), - toolbar_icon_home=data.get("toolbar_icon_home", DEFAULT_TOOLBAR_ICON_HOME), - toolbar_icon_reload=data.get("toolbar_icon_reload", DEFAULT_TOOLBAR_ICON_RELOAD), - toolbar_icon_open=data.get("toolbar_icon_open", DEFAULT_TOOLBAR_ICON_OPEN), - toolbar_icon_openwith=data.get("toolbar_icon_openwith", DEFAULT_TOOLBAR_ICON_OPENWITH), brand_id=brand_id, config_dir_name=config_dir_name, update_base_url=data.get("update_base_url", DEFAULT_UPDATE_BASE_URL), @@ -225,7 +204,6 @@ class Config: update_channel=data.get("update_channel", DEFAULT_UPDATE_CHANNEL), update_manifest_name=data.get("update_manifest_name", DEFAULT_UPDATE_MANIFEST_NAME), ) - return cls._apply_runtime_branding(config) @classmethod def from_env(cls, env_file: str | None = None) -> "Config": @@ -268,12 +246,6 @@ class Config: enable_logging = os.getenv("ENABLE_LOGGING", "true").lower() == "true" enable_checkout = os.getenv("ENABLE_CHECKOUT", "false").lower() == "true" language = os.getenv("LANGUAGE", "auto") - active_branding_id = os.getenv("BRAND_TEMPLATE", DEFAULT_ACTIVE_BRANDING_ID) - app_icon_path = os.getenv("APP_ICON_PATH", DEFAULT_APP_ICON_PATH) - toolbar_icon_home = os.getenv("TOOLBAR_ICON_HOME", DEFAULT_TOOLBAR_ICON_HOME) - toolbar_icon_reload = os.getenv("TOOLBAR_ICON_RELOAD", DEFAULT_TOOLBAR_ICON_RELOAD) - toolbar_icon_open = os.getenv("TOOLBAR_ICON_OPEN", DEFAULT_TOOLBAR_ICON_OPEN) - toolbar_icon_openwith = os.getenv("TOOLBAR_ICON_OPENWITH", DEFAULT_TOOLBAR_ICON_OPENWITH) update_base_url = os.getenv("UPDATE_BASE_URL", DEFAULT_UPDATE_BASE_URL) update_repo = os.getenv("UPDATE_REPO", DEFAULT_UPDATE_REPO) update_channel = os.getenv("UPDATE_CHANNEL", DEFAULT_UPDATE_CHANNEL) @@ -356,7 +328,7 @@ class Config: f"Invalid URL_MAPPINGS: {url_mappings_str}. Error: {e}" ) from e - config = cls( + return cls( app_name=app_name, app_version=app_version, log_level=log_level, @@ -371,12 +343,6 @@ class Config: enable_logging=enable_logging, enable_checkout=enable_checkout, language=language, - active_branding_id=active_branding_id, - app_icon_path=app_icon_path, - toolbar_icon_home=toolbar_icon_home, - toolbar_icon_reload=toolbar_icon_reload, - toolbar_icon_open=toolbar_icon_open, - toolbar_icon_openwith=toolbar_icon_openwith, brand_id=brand_id, config_dir_name=config_dir_name, update_base_url=update_base_url, @@ -384,7 +350,6 @@ class Config: update_channel=update_channel, update_manifest_name=update_manifest_name, ) - return cls._apply_runtime_branding(config) def to_file(self, config_path: Path) -> None: """Save configuration to JSON file. @@ -413,7 +378,6 @@ class Config: "enable_logging": self.enable_logging, "enable_checkout": self.enable_checkout, "language": self.language, - "active_branding_id": self.active_branding_id, "brand_id": self.brand_id, "config_dir_name": self.config_dir_name, "update_base_url": self.update_base_url, @@ -426,17 +390,6 @@ class Config: with open(config_path, "w", encoding="utf-8") as f: json.dump(data, f, indent=2) - @staticmethod - def _apply_runtime_branding(config: "Config") -> "Config": - """Apply the persisted runtime branding template to cosmetic fields.""" - try: - from webdrop_bridge.core.branding_manager import BrandingManager - - BrandingManager().apply_to_config(config) - except Exception as e: - logger.warning(f"Failed to apply runtime branding: {e}") - return config - @staticmethod def load_bootstrap_env(env_file: str | None = None) -> Path | None: """Load a bootstrap .env before configuration path lookup. diff --git a/src/webdrop_bridge/core/branding_manager.py b/src/webdrop_bridge/core/branding_manager.py deleted file mode 100644 index ae1726f..0000000 --- a/src/webdrop_bridge/core/branding_manager.py +++ /dev/null @@ -1,406 +0,0 @@ -"""Runtime branding template management for the shared application.""" - -from __future__ import annotations - -import json -import logging -import os -import platform -import shutil -from dataclasses import asdict, dataclass -from pathlib import Path -from typing import Any - -from webdrop_bridge.config import DEFAULT_CONFIG_DIR_NAME, Config, ConfigurationError - -logger = logging.getLogger(__name__) - -DEFAULT_BRANDING_TEMPLATE_ID = "default" -DEFAULT_LOGO_PATH = "resources/icons/app.png" -SUPPORTED_LOGO_SUFFIXES = {".png", ".jpg", ".jpeg", ".bmp", ".svg", ".ico", ".icns"} - - -@dataclass(frozen=True) -class BrandingTemplate: - """Serializable runtime branding template.""" - - template_id: str - display_name: str - app_name: str - window_title: str = "" - logo_path: str = "" - app_icon_path_windows: str = "resources/icons/app.ico" - app_icon_path_macos: str = "resources/icons/app.icns" - toolbar_icon_home: str = "resources/icons/home.ico" - toolbar_icon_reload: str = "resources/icons/reload.ico" - toolbar_icon_open: str = "resources/icons/open.ico" - toolbar_icon_openwith: str = "resources/icons/openwith.ico" - accent_color: str = "#667eea" - - @classmethod - def from_dict(cls, data: dict[str, Any]) -> "BrandingTemplate": - """Create a template from a JSON-compatible dictionary.""" - template_id = str(data.get("template_id") or data.get("id") or "").strip() - display_name = str(data.get("display_name") or template_id or "").strip() - app_name = str(data.get("app_name") or display_name or "").strip() - - if not template_id: - raise ConfigurationError("Branding template requires a template_id") - if not display_name: - raise ConfigurationError("Branding template requires a display_name") - if not app_name: - raise ConfigurationError("Branding template requires an app_name") - - return cls( - template_id=template_id, - display_name=display_name, - app_name=app_name, - window_title=str(data.get("window_title", "")), - logo_path=str(data.get("logo_path", "")), - app_icon_path_windows=str(data.get("app_icon_path_windows", "resources/icons/app.ico")), - app_icon_path_macos=str(data.get("app_icon_path_macos", "resources/icons/app.icns")), - toolbar_icon_home=str(data.get("toolbar_icon_home", "resources/icons/home.ico")), - toolbar_icon_reload=str(data.get("toolbar_icon_reload", "resources/icons/reload.ico")), - toolbar_icon_open=str(data.get("toolbar_icon_open", "resources/icons/open.ico")), - toolbar_icon_openwith=str( - data.get("toolbar_icon_openwith", "resources/icons/openwith.ico") - ), - accent_color=str(data.get("accent_color", "#667eea")), - ) - - def to_dict(self) -> dict[str, Any]: - """Convert the template to a JSON-compatible dictionary.""" - return asdict(self) - - def get_app_icon_path(self) -> str: - """Return the best app icon path for the current platform.""" - if platform.system() == "Darwin": - return self.app_icon_path_macos - return self.app_icon_path_windows - - -BUILTIN_BRANDING_TEMPLATES: dict[str, BrandingTemplate] = { - "default": BrandingTemplate( - template_id="default", - display_name="Default", - app_name="WebDrop Bridge", - window_title="", - logo_path=DEFAULT_LOGO_PATH, - accent_color="#667eea", - ), - "agravity": BrandingTemplate( - template_id="agravity", - display_name="Agravity", - app_name="Agravity Bridge", - window_title="", - logo_path=DEFAULT_LOGO_PATH, - accent_color="#2d7d6e", - ), -} - - -class BrandingManager: - """Manage runtime branding templates independently from saved setups.""" - - def __init__(self, base_dir: Path | None = None) -> None: - env_dir = os.getenv("WEBDROP_BRANDING_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.templates_dir = self.base_dir / "templates" - self.assets_dir = self.base_dir / "assets" - self.active_branding_path = self.base_dir / "active_branding.json" - self.ensure_builtin_templates() - - @staticmethod - def _default_base_dir() -> Path: - """Return the shared branding storage directory.""" - return Config.get_default_config_path(DEFAULT_CONFIG_DIR_NAME).parent / "branding" - - def ensure_builtin_templates(self) -> None: - """Ensure built-in templates exist on disk for discovery and later editing.""" - 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(): - template_path = self.templates_dir / f"{template.template_id}.json" - if not template_path.exists(): - template_path.write_text(json.dumps(template.to_dict(), indent=2), encoding="utf-8") - - def list_templates(self) -> list[BrandingTemplate]: - """List all available templates with built-ins guaranteed.""" - self.ensure_builtin_templates() - - templates: dict[str, BrandingTemplate] = {} - for template_path in self.templates_dir.glob("*.json"): - try: - data = json.loads(template_path.read_text(encoding="utf-8")) - template = BrandingTemplate.from_dict(data) - templates[template.template_id] = template - except (OSError, json.JSONDecodeError, ConfigurationError) as exc: - logger.warning("Skipping invalid branding template %s: %s", template_path, exc) - - for template_id, template in BUILTIN_BRANDING_TEMPLATES.items(): - templates.setdefault(template_id, template) - - ordered_templates = sorted( - templates.values(), - key=lambda template: ( - template.template_id != DEFAULT_BRANDING_TEMPLATE_ID, - template.display_name.lower(), - ), - ) - return ordered_templates - - def has_template(self, template_id: str) -> bool: - """Return whether a template with the given id exists.""" - return any(template.template_id == template_id for template in self.list_templates()) - - def load_template(self, template_id: str) -> BrandingTemplate: - """Load a template by id, falling back to the default template if missing.""" - for template in self.list_templates(): - if template.template_id == template_id: - return template - - logger.warning("Branding template '%s' not found. Falling back to default.", template_id) - return BUILTIN_BRANDING_TEMPLATES[DEFAULT_BRANDING_TEMPLATE_ID] - - def save_template(self, template: BrandingTemplate) -> Path: - """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}") - - 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) - template_path = self.templates_dir / f"{stored_template.template_id}.json" - template_path.write_text(json.dumps(stored_template.to_dict(), indent=2), encoding="utf-8") - logger.info("Branding template saved: %s", stored_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.""" - 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() - - if not safe_id: - raise ConfigurationError("Branding requires a name") - if not safe_name: - raise ConfigurationError("Branding requires a display name") - if logo: - logo_file = self._resolve_asset_path(logo) - if logo_file is None or not logo_file.exists() or not logo_file.is_file(): - raise ConfigurationError(f"Logo file not found: {logo}") - if logo_file.suffix.lower() not in SUPPORTED_LOGO_SUFFIXES: - raise ConfigurationError( - "Unsupported logo format. Use PNG, JPG, BMP, SVG, ICO, or ICNS." - ) - - return BrandingTemplate( - template_id=safe_id, - display_name=safe_name, - app_name=resolved_app_name, - window_title=window_title.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("_") - - @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: - """Delete a user template while protecting built-ins.""" - if template_id in BUILTIN_BRANDING_TEMPLATES: - raise ConfigurationError(f"Cannot delete built-in branding template: {template_id}") - - template_path = self.templates_dir / f"{template_id}.json" - if not template_path.exists(): - raise ConfigurationError(f"Branding template not found: {template_id}") - - template_path.unlink() - logger.info("Branding template deleted: %s", template_id) - - def get_active_branding_id(self) -> str: - """Return the persisted active branding selection.""" - if not self.active_branding_path.exists(): - return DEFAULT_BRANDING_TEMPLATE_ID - - try: - data = json.loads(self.active_branding_path.read_text(encoding="utf-8")) - template_id = str(data.get("active_branding_id", DEFAULT_BRANDING_TEMPLATE_ID)) - return template_id if self.has_template(template_id) else DEFAULT_BRANDING_TEMPLATE_ID - except (OSError, json.JSONDecodeError): - return DEFAULT_BRANDING_TEMPLATE_ID - - def set_active_branding_id(self, template_id: str) -> None: - """Persist the active branding selection.""" - if not self.has_template(template_id): - raise ConfigurationError(f"Branding template not found: {template_id}") - - self.base_dir.mkdir(parents=True, exist_ok=True) - self.active_branding_path.write_text( - json.dumps({"active_branding_id": template_id}, indent=2), - encoding="utf-8", - ) - logger.info("Active branding set to %s", template_id) - - def apply_to_config(self, config: Config) -> BrandingTemplate: - """Apply the active branding template to cosmetic config fields only.""" - requested_id = (getattr(config, "active_branding_id", "") or "").strip() - if not requested_id or requested_id == DEFAULT_BRANDING_TEMPLATE_ID: - requested_id = self.get_active_branding_id() - - template = self.load_template(requested_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 ( - 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 ( - template.template_id != DEFAULT_BRANDING_TEMPLATE_ID - or not config.window_title - 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}" - ) - - config.logo_path = template.logo_path - config.app_icon_path = template.get_app_icon_path() - config.toolbar_icon_home = template.toolbar_icon_home - config.toolbar_icon_reload = template.toolbar_icon_reload - config.toolbar_icon_open = template.toolbar_icon_open - config.toolbar_icon_openwith = template.toolbar_icon_openwith - - return template diff --git a/src/webdrop_bridge/ui/main_window.py b/src/webdrop_bridge/ui/main_window.py index a32a09e..f75d872 100644 --- a/src/webdrop_bridge/ui/main_window.py +++ b/src/webdrop_bridge/ui/main_window.py @@ -427,9 +427,7 @@ class MainWindow(QMainWindow): self._background_threads = [] # Keep references to background threads self._background_workers = {} # Keep references to background workers self._bridge_script_source = "" # Cache combined bridge source for recovery injection - self._bridge_script_re_registered = ( - False # Flag to prevent duplicate re-registration on same load - ) + self._bridge_script_re_registered = False # Flag to prevent duplicate re-registration on same load self._is_page_loading = False # Track if a page load is currently in progress self._pending_reload = False # Coalesce multiple rapid reload requests into one self._load_sequence = 0 # Monotonic counter to ignore stale async recovery callbacks @@ -446,13 +444,22 @@ class MainWindow(QMainWindow): config.window_height, ) - # Set window icon from the active runtime branding - icon_path = self._resolve_toolbar_icon_path(config.app_icon_path) - if icon_path is not None: + # Set window icon + # Support both development mode and PyInstaller bundle + if hasattr(sys, "_MEIPASS"): + # Running as PyInstaller bundle + icon_path = Path(sys._MEIPASS) / "resources" / "icons" / "app.ico" # type: ignore + else: + # Running in development mode + icon_path = ( + Path(__file__).parent.parent.parent.parent / "resources" / "icons" / "app.ico" + ) + + if icon_path.exists(): self.setWindowIcon(QIcon(str(icon_path))) logger.debug(f"Window icon set from {icon_path}") else: - logger.warning(f"Window icon not found for configured path: {config.app_icon_path}") + logger.warning(f"Window icon not found at {icon_path}") # Create web engine view with URL for profile isolation self.web_view = RestrictedWebEngineView( @@ -1182,9 +1189,7 @@ class MainWindow(QMainWindow): # This more reliably opens files with chosen applications. # Use a simple, more direct approach # Get the chosen app via AppleScript, then use open command - get_app_script = ( - '''choose application with title "Select an application to open the file"''' - ) + get_app_script = '''choose application with title "Select an application to open the file"''' try: # Get the chosen application app_result = subprocess.run( @@ -1194,21 +1199,19 @@ class MainWindow(QMainWindow): text=True, timeout=30, ) - + if app_result.returncode != 0: - logger.warning( - f"User cancelled app chooser or error occurred: {app_result.stderr}" - ) + logger.warning(f"User cancelled app chooser or error occurred: {app_result.stderr}") return False - + # Get the application name (strip whitespace) chosen_app = app_result.stdout.strip() if not chosen_app: logger.warning("No application was selected") return False - + logger.info(f"User selected app: {chosen_app}") - + # Now open the file with the chosen app using the 'open' command open_result = subprocess.run( ["open", "-a", chosen_app, normalized_path], @@ -1217,16 +1220,14 @@ class MainWindow(QMainWindow): text=True, timeout=10, ) - + if open_result.returncode == 0: logger.info(f"Opened '{normalized_path}' with '{chosen_app}'") return True else: - logger.warning( - f"Failed to open file with '{chosen_app}': {open_result.stderr}" - ) + logger.warning(f"Failed to open file with '{chosen_app}': {open_result.stderr}") return False - + except subprocess.TimeoutExpired: logger.warning("App chooser timed out") return False @@ -1392,7 +1393,7 @@ class MainWindow(QMainWindow): Re-registers the bridge script to ensure it will be injected on reload, page navigation, or any load event. - + Uses a flag to prevent duplicate re-registrations if loadStarted fires multiple times. """ self._is_page_loading = True @@ -1411,7 +1412,7 @@ class MainWindow(QMainWindow): Checks if the bridge script was successfully injected, with automatic recovery for page reloads and redirects. - + Resets the re-registration flag for the next load cycle. Args: @@ -1432,11 +1433,9 @@ class MainWindow(QMainWindow): logger.warning("Page failed to load") return - def _verify_bridge_loaded( - stage: str, attempt: int = 1, sequence: int = finished_sequence - ) -> None: + def _verify_bridge_loaded(stage: str, attempt: int = 1, sequence: int = finished_sequence) -> None: """Check if bridge marker exists and optionally recover script injection. - + Implements multi-attempt recovery strategy: - initial: First check after page load (50ms delay) - recovery_N: Recovery attempts with progressive delays @@ -1486,7 +1485,9 @@ class MainWindow(QMainWindow): delay = int(100 * (1.5 ** (attempt - 1))) QTimer.singleShot( delay, - lambda: _verify_bridge_loaded("recovery", attempt + 1, sequence), + lambda: _verify_bridge_loaded( + "recovery", attempt + 1, sequence + ), ) self.web_view.page().runJavaScript(self._bridge_script_source, after_retry) @@ -1506,15 +1507,11 @@ class MainWindow(QMainWindow): ) self._re_register_bridge_script() - self.web_view.page().runJavaScript( - self._bridge_script_source, after_re_register - ) + self.web_view.page().runJavaScript(self._bridge_script_source, after_re_register) return # All recovery attempts exhausted - logger.error( - "❌ WebDrop Bridge script failed to inject after all recovery attempts!" - ) + logger.error("❌ WebDrop Bridge script failed to inject after all recovery attempts!") logger.error(" Drag-and-drop functionality is DISABLED") logger.debug(f" Stage: {stage}, Attempt: {attempt}") @@ -1546,21 +1543,21 @@ class MainWindow(QMainWindow): def _ensure_bridge_script_exists(self, verbose: bool = False) -> None: """Ensure bridge script exists in QWebEngineScript collection (idempotent). - + Checks if the script already exists. If not, adds it. Never removes/re-adds to avoid race conditions with Qt's injection mechanism. - + This is safer than removing+re-adding because: - Avoids concurrent access conflicts with Qt's internal injection - Prevents missing injections during rapid reloads - Guarantees script is available without timing gaps - + Args: verbose: If True, use debug logging; otherwise use minimal logging """ try: scripts = self.web_view.page().scripts() - + # Check if script already exists already_exists = False for script in scripts.toList(): # type: ignore @@ -1569,7 +1566,7 @@ class MainWindow(QMainWindow): if verbose: logger.debug("Bridge script already exists in page().scripts()") break - + # If script doesn't exist, add it if not already_exists and self._bridge_script_source: new_script = QWebEngineScript() @@ -1585,18 +1582,16 @@ class MainWindow(QMainWindow): new_script.setSourceCode(self._bridge_script_source) scripts.insert(new_script) - logger.debug( - f"✓ Added bridge script to collection ({len(self._bridge_script_source)} chars)" - ) + logger.debug(f"✓ Added bridge script to collection ({len(self._bridge_script_source)} chars)") except Exception as e: logger.error(f"Failed to ensure bridge script exists: {e}") def _re_register_bridge_script(self, verbose: bool = False) -> None: """Force re-registration of bridge script in QWebEngineScript collection. - + Removes old script and re-adds it to ensure it's injected on next page load. This is a fallback for recovery mechanics when normal injection fails. - + Args: verbose: If True, use debug logging; otherwise use minimal logging """ @@ -1627,9 +1622,7 @@ class MainWindow(QMainWindow): scripts.insert(new_script) if verbose or removed: - logger.debug( - f"✓ Re-registered webdrop-bridge script ({len(self._bridge_script_source)} chars)" - ) + logger.debug(f"✓ Re-registered webdrop-bridge script ({len(self._bridge_script_source)} chars)") except Exception as e: logger.error(f"Failed to re-register bridge script: {e}") @@ -1656,7 +1649,9 @@ class MainWindow(QMainWindow): toolbar.addSeparator() # Home button - home_icon_path = self._resolve_toolbar_icon_path(self.config.toolbar_icon_home) + home_icon_path = self._resolve_toolbar_icon_path( + os.getenv("TOOLBAR_ICON_HOME", "resources/icons/home.ico") + ) home_icon = ( QIcon(str(home_icon_path)) if home_icon_path is not None @@ -1668,7 +1663,9 @@ class MainWindow(QMainWindow): # Refresh button refresh_action = toolbar.addAction("") - reload_icon_path = self._resolve_toolbar_icon_path(self.config.toolbar_icon_reload) + reload_icon_path = self._resolve_toolbar_icon_path( + os.getenv("TOOLBAR_ICON_RELOAD", "resources/icons/reload.ico") + ) if reload_icon_path is not None: refresh_action.setIcon(QIcon(str(reload_icon_path))) else: @@ -1680,7 +1677,9 @@ class MainWindow(QMainWindow): # Open-with-default-app drop zone (right of Reload) self._open_drop_zone = OpenDropZone() - open_icon_path = self._resolve_toolbar_icon_path(self.config.toolbar_icon_open) + open_icon_path = self._resolve_toolbar_icon_path( + os.getenv("TOOLBAR_ICON_OPEN", "resources/icons/open.ico") + ) if open_icon_path is not None: self._open_drop_zone.set_icon(QIcon(str(open_icon_path))) self._open_drop_zone.file_opened.connect(self._on_file_opened_via_drop) @@ -1691,7 +1690,9 @@ class MainWindow(QMainWindow): # Open-with chooser drop zone (right of Open-with-default-app) self._open_with_drop_zone = OpenWithDropZone() - open_with_icon_path = self._resolve_toolbar_icon_path(self.config.toolbar_icon_openwith) + open_with_icon_path = self._resolve_toolbar_icon_path( + os.getenv("TOOLBAR_ICON_OPENWITH", "resources/icons/openwith.ico") + ) if open_with_icon_path is not None: self._open_with_drop_zone.set_icon(QIcon(str(open_with_icon_path))) self._open_with_drop_zone.file_open_with_requested.connect( @@ -1958,7 +1959,6 @@ class MainWindow(QMainWindow): # Store current URL before opening dialog old_webapp_url = self.config.webapp_url old_language = self.config.language - old_branding_id = self.config.active_branding_id # Show dialog dialog = SettingsDialog(self.config, self) @@ -1967,9 +1967,6 @@ class MainWindow(QMainWindow): # Check if webapp URL changed new_webapp_url = self.config.webapp_url language_changed = old_language != self.config.language - branding_changed = old_branding_id != self.config.active_branding_id - restart_prompt_shown = False - if old_webapp_url != new_webapp_url: logger.info(f"Web application URL changed: {old_webapp_url} → {new_webapp_url}") @@ -1979,7 +1976,6 @@ class MainWindow(QMainWindow): if domain_changed: logger.warning("Domain has changed - recommending restart") self._handle_domain_change_restart() - restart_prompt_shown = True else: logger.info("Path changed but domain is same - reloading...") # Clear cache and navigate to home asynchronously @@ -1987,16 +1983,7 @@ class MainWindow(QMainWindow): self.web_view.clear_cache_and_cookies() QTimer.singleShot(100, self._navigate_home) - if not restart_prompt_shown and branding_changed: - logger.info( - "Branding changed: %s → %s", - old_branding_id, - self.config.active_branding_id, - ) - self._handle_branding_change_restart() - restart_prompt_shown = True - - if not restart_prompt_shown and language_changed: + if language_changed: logger.info(f"Language changed: {old_language} → {self.config.language}") self._handle_language_change_restart() @@ -2060,42 +2047,21 @@ class MainWindow(QMainWindow): self.web_view.clear_cache_and_cookies() self._navigate_home() - def _handle_branding_change_restart(self) -> None: - """Handle branding change by prompting for an optional restart.""" - self._show_restart_prompt( - title_key="dialog.branding_changed.title", - message_key="dialog.branding_changed.msg", - restart_now_key="dialog.branding_changed.restart_now", - restart_later_key="dialog.branding_changed.restart_later", - ) - def _handle_language_change_restart(self) -> None: """Handle language change by prompting for an optional restart.""" - self._show_restart_prompt( - title_key="dialog.language_changed.title", - message_key="dialog.language_changed.msg", - restart_now_key="dialog.language_changed.restart_now", - restart_later_key="dialog.language_changed.restart_later", - ) - - def _show_restart_prompt( - self, - *, - title_key: str, - message_key: str, - restart_now_key: str, - restart_later_key: str, - ) -> None: - """Show a restart prompt for settings that require a full restart.""" from PySide6.QtWidgets import QMessageBox msg = QMessageBox(self) - msg.setWindowTitle(tr(title_key)) + msg.setWindowTitle(tr("dialog.language_changed.title")) msg.setIcon(QMessageBox.Icon.Information) - msg.setText(tr(message_key)) + msg.setText(tr("dialog.language_changed.msg")) - restart_now_btn = msg.addButton(tr(restart_now_key), QMessageBox.ButtonRole.AcceptRole) - msg.addButton(tr(restart_later_key), QMessageBox.ButtonRole.RejectRole) + restart_now_btn = msg.addButton( + tr("dialog.language_changed.restart_now"), QMessageBox.ButtonRole.AcceptRole + ) + msg.addButton( + tr("dialog.language_changed.restart_later"), QMessageBox.ButtonRole.RejectRole + ) msg.exec() diff --git a/src/webdrop_bridge/ui/settings_dialog.py b/src/webdrop_bridge/ui/settings_dialog.py index 9830cff..1da3eaa 100644 --- a/src/webdrop_bridge/ui/settings_dialog.py +++ b/src/webdrop_bridge/ui/settings_dialog.py @@ -4,14 +4,12 @@ import logging from pathlib import Path from typing import Any, Dict, Optional -from PySide6.QtGui import QIcon, QPixmap from PySide6.QtWidgets import ( QComboBox, QDialog, QDialogButtonBox, QFileDialog, QHBoxLayout, - QInputDialog, QLabel, QLineEdit, QListWidget, @@ -25,7 +23,6 @@ from PySide6.QtWidgets import ( ) from webdrop_bridge.config import Config, ConfigurationError -from webdrop_bridge.core.branding_manager import BrandingManager from webdrop_bridge.core.config_manager import ConfigExporter, ConfigProfile, ConfigValidator from webdrop_bridge.utils.i18n import get_available_languages, tr from webdrop_bridge.utils.logging import reconfigure_logging @@ -45,7 +42,6 @@ class SettingsDialog(QDialog): """ super().__init__(parent) self.config = config - self.branding_manager = BrandingManager() self.profile_manager = ConfigProfile(config.config_dir_name) self.setWindowTitle(tr("settings.title")) self.setGeometry(100, 100, 600, 500) @@ -58,7 +54,6 @@ class SettingsDialog(QDialog): self.tabs = QTabWidget() self.tabs.addTab(self._create_general_tab(), tr("settings.tab.general")) - self.tabs.addTab(self._create_branding_tab(), tr("settings.tab.branding")) self.tabs.addTab(self._create_web_source_tab(), tr("settings.tab.web_source")) self.tabs.addTab(self._create_paths_tab(), tr("settings.tab.paths")) self.tabs.addTab(self._create_urls_tab(), tr("settings.tab.urls")) @@ -88,14 +83,6 @@ class SettingsDialog(QDialog): for m in config_data["url_mappings"] ] - selected_branding_id = config_data.get( - "active_branding_id", self.config.active_branding_id - ) - old_branding_id = self.config.active_branding_id - self.branding_manager.set_active_branding_id(selected_branding_id) - self.config.active_branding_id = selected_branding_id - self.branding_manager.apply_to_config(self.config) - old_log_level = self.config.log_level self.config.language = config_data["language"] self.config.log_level = config_data["log_level"] @@ -115,12 +102,6 @@ class SettingsDialog(QDialog): logger.info(f"Configuration saved to {config_path}") logger.info(f" Log level: {self.config.log_level} (was: {old_log_level})") logger.info(f" Window size: {self.config.window_width}x{self.config.window_height}") - if old_branding_id != self.config.active_branding_id: - logger.info( - " Active branding changed: %s -> %s", - old_branding_id, - self.config.active_branding_id, - ) if old_log_level != self.config.log_level: reconfigure_logging( @@ -170,273 +151,6 @@ class SettingsDialog(QDialog): widget.setLayout(layout) return widget - def _create_branding_tab(self) -> QWidget: - """Create runtime branding tab.""" - widget = QWidget() - layout = QVBoxLayout() - - label = QLabel(tr("settings.branding.select_label")) - label.setToolTip(tr("settings.branding.select_tooltip")) - layout.addWidget(label) - - help_label = QLabel(tr("settings.branding.help_text")) - help_label.setWordWrap(True) - help_label.setStyleSheet("color: gray; font-size: 11px;") - layout.addWidget(help_label) - - self.branding_combo = QComboBox() - self.branding_combo.setToolTip(tr("settings.branding.select_tooltip")) - 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") - ) - self.branding_display_name_input.textChanged.connect(self._update_branding_preview) - 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")) - 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"))) - logo_layout = QHBoxLayout() - self.branding_logo_path_input = QLineEdit() - self.branding_logo_path_input.setPlaceholderText(tr("settings.branding.logo_path_label")) - self.branding_logo_path_input.textChanged.connect(self._update_branding_preview) - 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) - - layout.addWidget(QLabel(tr("settings.branding.preview_label"))) - self.branding_preview_name_label = QLabel() - self.branding_preview_name_label.setStyleSheet("font-weight: bold;") - 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.setFixedSize(72, 72) - self.branding_preview_icon_label.setStyleSheet( - "border: 1px solid #ccc; padding: 4px; background: #fafafa;" - ) - layout.addWidget(self.branding_preview_icon_label) - - 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) - - 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.clicked.connect(self._delete_branding) - branding_button_layout.addWidget(self.delete_branding_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;") - layout.addWidget(note) - - layout.addStretch() - 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 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 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() - - 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: - """Refresh the small branding preview for name and icon.""" - display_name = self.branding_display_name_input.text().strip() or tr( - "settings.branding.preview_default_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() - resolved_logo_path = self._resolve_branding_preview_path(logo_path) - if resolved_logo_path: - pixmap = QPixmap(str(resolved_logo_path)) - if pixmap.isNull(): - icon = QIcon(str(resolved_logo_path)) - pixmap = icon.pixmap(64, 64) - - if not pixmap.isNull(): - self.branding_preview_icon_label.setPixmap(pixmap.scaled(64, 64)) - self.branding_preview_icon_label.setText("") - return - - self.branding_preview_icon_label.setPixmap(QPixmap()) - self.branding_preview_icon_label.setText(tr("settings.branding.no_icon_selected")) - - 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 _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 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 branding_name: - return - - try: - 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_id=branding_name, - display_name=display_name, - app_name=app_name, - window_title=window_title, - 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 _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: - """Delete the currently selected custom branding.""" - template_id = self.branding_combo.currentData() - if not template_id: - return - - try: - self.branding_manager.delete_template(template_id) - self._refresh_branding_combo("default") - self._load_branding_into_editor("default") - except ConfigurationError as e: - self._show_error(f"Failed to delete branding: {e}") - def _create_web_source_tab(self) -> QWidget: """Create web source configuration tab.""" widget = QWidget() @@ -909,7 +623,6 @@ class SettingsDialog(QDialog): "app_name": self.config.app_name, "app_version": self.config.app_version, "language": self.language_combo.currentData(), - "active_branding_id": self.branding_combo.currentData(), "log_level": self.log_level_combo.currentText(), "log_file": self.log_file_input.text() or None, "allowed_roots": [ diff --git a/tests/unit/test_branding_manager.py b/tests/unit/test_branding_manager.py deleted file mode 100644 index 60997d4..0000000 --- a/tests/unit/test_branding_manager.py +++ /dev/null @@ -1,172 +0,0 @@ -"""Tests for runtime branding template management.""" - -from pathlib import Path - -import pytest - -from webdrop_bridge.config import Config, ConfigurationError -from webdrop_bridge.core.branding_manager import BrandingManager - - -def test_builtin_brandings_are_available(tmp_path): - """Built-in default and Agravity templates should always be available.""" - manager = BrandingManager(base_dir=tmp_path) - - brandings = manager.list_templates() - template_ids = [template.template_id for template in brandings] - - assert "default" in template_ids - assert "agravity" in template_ids - - -def test_active_branding_persists_across_manager_instances(tmp_path): - """Selected active branding should persist on disk.""" - manager = BrandingManager(base_dir=tmp_path) - manager.set_active_branding_id("agravity") - - reloaded_manager = BrandingManager(base_dir=tmp_path) - - assert reloaded_manager.get_active_branding_id() == "agravity" - - -def test_apply_branding_updates_cosmetic_fields_only(tmp_path): - """Applying a branding template should not overwrite setup-specific values.""" - allowed_root = tmp_path / "allowed" - allowed_root.mkdir() - - config = Config( - app_name="WebDrop Bridge", - app_version="1.0.0", - log_level="INFO", - log_file=None, - allowed_roots=[allowed_root], - allowed_urls=["example.com"], - webapp_url="http://localhost:8080", - window_width=1024, - window_height=768, - enable_logging=True, - active_branding_id="agravity", - ) - - manager = BrandingManager(base_dir=tmp_path) - manager.apply_to_config(config) - - assert config.active_branding_id == "agravity" - assert config.app_name == "Agravity Bridge" - assert config.webapp_url == "http://localhost:8080" - assert config.allowed_roots == [allowed_root] - assert config.toolbar_icon_home.endswith("home.ico") - - -def test_config_from_env_uses_persisted_active_branding(tmp_path, monkeypatch): - """Config loading should apply the persisted active branding automatically.""" - branding_dir = tmp_path / "branding-state" - manager = BrandingManager(base_dir=branding_dir) - manager.set_active_branding_id("agravity") - - monkeypatch.setenv("WEBDROP_BRANDING_DIR", str(branding_dir)) - - root = tmp_path / "root" - root.mkdir() - env_file = tmp_path / ".env" - env_file.write_text(f"ALLOWED_ROOTS={root}\n", encoding="utf-8") - - config = Config.from_env(str(env_file)) - - 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" - - -def test_delete_custom_branding_removes_it(tmp_path): - """Custom brandings should be removable while built-ins stay protected.""" - manager = BrandingManager(base_dir=tmp_path) - template = manager.build_template(template_id="Customer B", display_name="Customer B") - manager.save_template(template) - - assert manager.has_template("customer_b") - - manager.delete_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): - """Non-existent logo files should not be accepted for saved brandings.""" - manager = BrandingManager(base_dir=tmp_path) - - with pytest.raises(ConfigurationError): - manager.build_template( - template_id="customer_c", - display_name="Customer C", - 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") diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index 065caee..09e4d6d 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -9,14 +9,11 @@ from webdrop_bridge.config import Config, ConfigurationError @pytest.fixture(autouse=True) -def clear_env(tmp_path): +def clear_env(): """Clear environment variables before each test to avoid persistence.""" # Save current env saved_env = os.environ.copy() - # Isolate runtime branding state from the developer machine - os.environ["WEBDROP_BRANDING_DIR"] = str(tmp_path / "branding") - # Clear relevant variables for key in list(os.environ.keys()): if key.startswith( diff --git a/tests/unit/test_main_window.py b/tests/unit/test_main_window.py index 1fa7d66..be89ab6 100644 --- a/tests/unit/test_main_window.py +++ b/tests/unit/test_main_window.py @@ -82,25 +82,6 @@ class TestMainWindowInitialization: assert window.drag_interceptor is not None -class TestSettingsRestartBehavior: - """Test restart prompts for settings changes that require a restart.""" - - def test_branding_change_prompts_restart(self, qtbot, sample_config): - """Changing the active branding should trigger the restart flow.""" - window = MainWindow(sample_config) - qtbot.addWidget(window) - - with patch.object(window, "_handle_branding_change_restart") as mock_restart: - with patch("webdrop_bridge.ui.settings_dialog.SettingsDialog") as mock_dialog_cls: - mock_dialog = mock_dialog_cls.return_value - mock_dialog.exec.side_effect = lambda: setattr( - window.config, "active_branding_id", "agravity" - ) - window._show_settings_dialog() - - mock_restart.assert_called_once() - - class TestMainWindowDragIntegration: """Test drag-and-drop integration.""" @@ -226,15 +207,15 @@ class TestMainWindowOpenWith: test_file.write_text("test") call_count = [0] # Use list to make it mutable in nested function - + class _AppChooseResult: returncode = 0 stdout = "TextEdit" # Simulated chosen app name - + class _OpenResult: returncode = 0 stdout = "" - + def mock_run(*args, **kwargs): """Mock subprocess.run with two different behaviors per call.""" call_count[0] += 1 @@ -246,7 +227,8 @@ class TestMainWindowOpenWith: return _OpenResult() else: raise AssertionError(f"Unexpected call #{call_count[0]} to subprocess.run") - + + with patch("webdrop_bridge.ui.main_window.sys.platform", "darwin"): with patch("webdrop_bridge.ui.main_window.subprocess.run", side_effect=mock_run): assert window._open_with_app_chooser(str(test_file)) is True diff --git a/tests/unit/test_settings_dialog.py b/tests/unit/test_settings_dialog.py index b05239a..1ff1773 100644 --- a/tests/unit/test_settings_dialog.py +++ b/tests/unit/test_settings_dialog.py @@ -1,11 +1,11 @@ """Tests for settings dialog.""" from pathlib import Path -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pytest -from webdrop_bridge.config import Config +from webdrop_bridge.config import Config, ConfigurationError from webdrop_bridge.ui.settings_dialog import SettingsDialog @@ -44,7 +44,7 @@ class TestSettingsDialogInitialization: qtbot.addWidget(dialog) assert dialog.tabs is not None - assert dialog.tabs.count() == 8 # General + Branding + previous 6 tabs + assert dialog.tabs.count() == 7 # General + previous 6 tabs def test_dialog_has_general_tab(self, qtbot, sample_config): """Test General tab exists.""" @@ -53,54 +53,47 @@ class TestSettingsDialogInitialization: assert dialog.tabs.tabText(0) == "General" - def test_dialog_has_branding_tab(self, qtbot, sample_config): - """Test Branding tab exists.""" - dialog = SettingsDialog(sample_config) - qtbot.addWidget(dialog) - - assert dialog.tabs.tabText(1) == "Branding" - def test_dialog_has_web_source_tab(self, qtbot, sample_config): """Test Web Source tab exists.""" dialog = SettingsDialog(sample_config) qtbot.addWidget(dialog) - assert dialog.tabs.tabText(2) == "Web Source" + assert dialog.tabs.tabText(1) == "Web Source" def test_dialog_has_paths_tab(self, qtbot, sample_config): """Test Paths tab exists.""" dialog = SettingsDialog(sample_config) qtbot.addWidget(dialog) - assert dialog.tabs.tabText(3) == "Paths" + assert dialog.tabs.tabText(2) == "Paths" def test_dialog_has_urls_tab(self, qtbot, sample_config): """Test URLs tab exists.""" dialog = SettingsDialog(sample_config) qtbot.addWidget(dialog) - assert dialog.tabs.tabText(4) == "URLs" + assert dialog.tabs.tabText(3) == "URLs" def test_dialog_has_logging_tab(self, qtbot, sample_config): """Test Logging tab exists.""" dialog = SettingsDialog(sample_config) qtbot.addWidget(dialog) - assert dialog.tabs.tabText(5) == "Logging" + assert dialog.tabs.tabText(4) == "Logging" def test_dialog_has_window_tab(self, qtbot, sample_config): """Test Window tab exists.""" dialog = SettingsDialog(sample_config) qtbot.addWidget(dialog) - assert dialog.tabs.tabText(6) == "Window" + assert dialog.tabs.tabText(5) == "Window" def test_dialog_has_profiles_tab(self, qtbot, sample_config): """Test Setups tab exists with clearer wording.""" dialog = SettingsDialog(sample_config) qtbot.addWidget(dialog) - assert dialog.tabs.tabText(7) == "Setups" + assert dialog.tabs.tabText(6) == "Setups" def test_profiles_actions_have_explanatory_tooltips(self, qtbot, sample_config): """Test profile/config actions expose helpful explanations.""" @@ -111,46 +104,6 @@ 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" - assert "WebDrop Bridge" in dialog.branding_window_title_input.text() - assert dialog.branding_logo_path_input is not None - assert dialog.browse_branding_logo_btn is not None - 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 - - 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) - - logo_path = tmp_path / "customer-logo.png" - logo_path.write_bytes(b"fake-png-data") - - dialog.branding_display_name_input.setText("Customer A") - dialog.branding_logo_path_input.setText(str(logo_path)) - - 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."""