feat: Add branding change prompts and corresponding translations for restart notifications

This commit is contained in:
claudi 2026-04-15 11:49:09 +02:00
parent ca7105a6bc
commit 2ecd299f31
5 changed files with 79 additions and 15 deletions

View file

@ -58,6 +58,10 @@
"dialog.language_changed.msg": "Die Spracheinstellung wurde aktualisiert. Starten Sie jetzt neu, um die ausgew\u00e4hlte Sprache \u00fcberall anzuwenden.", "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_now": "Jetzt neu starten",
"dialog.language_changed.restart_later": "Sp\u00e4ter 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.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.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", "dialog.update_timeout.title": "Zeitüberschreitung bei der Update-Pr\u00fcfung",

View file

@ -58,6 +58,10 @@
"dialog.language_changed.msg": "The language setting was updated. Restart now to apply the selected language everywhere.", "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_now": "Restart Now",
"dialog.language_changed.restart_later": "Restart Later", "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.title": "Restart Failed",
"dialog.restart_failed.msg": "Could not automatically restart the application:\n\n{error}\n\nPlease restart manually.", "dialog.restart_failed.msg": "Could not automatically restart the application:\n\n{error}\n\nPlease restart manually.",
"dialog.update_timeout.title": "Update Check Timeout", "dialog.update_timeout.title": "Update Check Timeout",

View file

@ -1958,6 +1958,7 @@ class MainWindow(QMainWindow):
# Store current URL before opening dialog # Store current URL before opening dialog
old_webapp_url = self.config.webapp_url old_webapp_url = self.config.webapp_url
old_language = self.config.language old_language = self.config.language
old_branding_id = self.config.active_branding_id
# Show dialog # Show dialog
dialog = SettingsDialog(self.config, self) dialog = SettingsDialog(self.config, self)
@ -1966,6 +1967,9 @@ class MainWindow(QMainWindow):
# Check if webapp URL changed # Check if webapp URL changed
new_webapp_url = self.config.webapp_url new_webapp_url = self.config.webapp_url
language_changed = old_language != self.config.language 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: if old_webapp_url != new_webapp_url:
logger.info(f"Web application URL changed: {old_webapp_url}{new_webapp_url}") logger.info(f"Web application URL changed: {old_webapp_url}{new_webapp_url}")
@ -1975,6 +1979,7 @@ class MainWindow(QMainWindow):
if domain_changed: if domain_changed:
logger.warning("Domain has changed - recommending restart") logger.warning("Domain has changed - recommending restart")
self._handle_domain_change_restart() self._handle_domain_change_restart()
restart_prompt_shown = True
else: else:
logger.info("Path changed but domain is same - reloading...") logger.info("Path changed but domain is same - reloading...")
# Clear cache and navigate to home asynchronously # Clear cache and navigate to home asynchronously
@ -1982,7 +1987,16 @@ class MainWindow(QMainWindow):
self.web_view.clear_cache_and_cookies() self.web_view.clear_cache_and_cookies()
QTimer.singleShot(100, self._navigate_home) QTimer.singleShot(100, self._navigate_home)
if language_changed: 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:
logger.info(f"Language changed: {old_language}{self.config.language}") logger.info(f"Language changed: {old_language}{self.config.language}")
self._handle_language_change_restart() self._handle_language_change_restart()
@ -2046,21 +2060,42 @@ class MainWindow(QMainWindow):
self.web_view.clear_cache_and_cookies() self.web_view.clear_cache_and_cookies()
self._navigate_home() 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: def _handle_language_change_restart(self) -> None:
"""Handle language change by prompting for an optional restart.""" """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 from PySide6.QtWidgets import QMessageBox
msg = QMessageBox(self) msg = QMessageBox(self)
msg.setWindowTitle(tr("dialog.language_changed.title")) msg.setWindowTitle(tr(title_key))
msg.setIcon(QMessageBox.Icon.Information) msg.setIcon(QMessageBox.Icon.Information)
msg.setText(tr("dialog.language_changed.msg")) msg.setText(tr(message_key))
restart_now_btn = msg.addButton( restart_now_btn = msg.addButton(tr(restart_now_key), QMessageBox.ButtonRole.AcceptRole)
tr("dialog.language_changed.restart_now"), QMessageBox.ButtonRole.AcceptRole msg.addButton(tr(restart_later_key), QMessageBox.ButtonRole.RejectRole)
)
msg.addButton(
tr("dialog.language_changed.restart_later"), QMessageBox.ButtonRole.RejectRole
)
msg.exec() msg.exec()

View file

@ -9,11 +9,14 @@ from webdrop_bridge.config import Config, ConfigurationError
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def clear_env(): def clear_env(tmp_path):
"""Clear environment variables before each test to avoid persistence.""" """Clear environment variables before each test to avoid persistence."""
# Save current env # Save current env
saved_env = os.environ.copy() 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 # Clear relevant variables
for key in list(os.environ.keys()): for key in list(os.environ.keys()):
if key.startswith( if key.startswith(

View file

@ -82,6 +82,25 @@ class TestMainWindowInitialization:
assert window.drag_interceptor is not None 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: class TestMainWindowDragIntegration:
"""Test drag-and-drop integration.""" """Test drag-and-drop integration."""
@ -207,15 +226,15 @@ class TestMainWindowOpenWith:
test_file.write_text("test") test_file.write_text("test")
call_count = [0] # Use list to make it mutable in nested function call_count = [0] # Use list to make it mutable in nested function
class _AppChooseResult: class _AppChooseResult:
returncode = 0 returncode = 0
stdout = "TextEdit" # Simulated chosen app name stdout = "TextEdit" # Simulated chosen app name
class _OpenResult: class _OpenResult:
returncode = 0 returncode = 0
stdout = "" stdout = ""
def mock_run(*args, **kwargs): def mock_run(*args, **kwargs):
"""Mock subprocess.run with two different behaviors per call.""" """Mock subprocess.run with two different behaviors per call."""
call_count[0] += 1 call_count[0] += 1
@ -227,8 +246,7 @@ class TestMainWindowOpenWith:
return _OpenResult() return _OpenResult()
else: else:
raise AssertionError(f"Unexpected call #{call_count[0]} to subprocess.run") 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.sys.platform", "darwin"):
with patch("webdrop_bridge.ui.main_window.subprocess.run", side_effect=mock_run): with patch("webdrop_bridge.ui.main_window.subprocess.run", side_effect=mock_run):
assert window._open_with_app_chooser(str(test_file)) is True assert window._open_with_app_chooser(str(test_file)) is True