synodic-client 0.0.1.dev65__tar.gz → 0.0.1.dev66__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/PKG-INFO +1 -1
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/pyproject.toml +1 -1
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/synodic_client/application/screen/schema.py +0 -61
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/synodic_client/application/screen/settings.py +28 -34
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/synodic_client/application/screen/tray.py +15 -2
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/synodic_client/application/screen/update_banner.py +64 -2
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/synodic_client/application/update_controller.py +105 -75
- synodic_client-0.0.1.dev66/synodic_client/application/update_model.py +120 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/tests/unit/qt/test_settings.py +27 -16
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/tests/unit/qt/test_update_banner.py +1 -2
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/tests/unit/qt/test_update_controller.py +80 -62
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/LICENSE.md +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/README.md +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/synodic_client/__init__.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/synodic_client/__main__.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/synodic_client/application/__init__.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/synodic_client/application/bootstrap.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/synodic_client/application/config_store.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/synodic_client/application/data.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/synodic_client/application/icon.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/synodic_client/application/init.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/synodic_client/application/instance.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/synodic_client/application/package_state.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/synodic_client/application/qt.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/synodic_client/application/schema.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/synodic_client/application/screen/__init__.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/synodic_client/application/screen/action_card.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/synodic_client/application/screen/card.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/synodic_client/application/screen/install.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/synodic_client/application/screen/install_workers.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/synodic_client/application/screen/log_panel.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/synodic_client/application/screen/plugin_row.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/synodic_client/application/screen/projects.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/synodic_client/application/screen/screen.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/synodic_client/application/screen/sidebar.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/synodic_client/application/screen/spinner.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/synodic_client/application/screen/tool_update_controller.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/synodic_client/application/theme.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/synodic_client/application/uri.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/synodic_client/application/workers.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/synodic_client/cli.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/synodic_client/client.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/synodic_client/config.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/synodic_client/logging.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/synodic_client/protocol.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/synodic_client/py.typed +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/synodic_client/resolution.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/synodic_client/schema.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/synodic_client/startup.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/synodic_client/subprocess_patch.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/synodic_client/updater.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/tests/__init__.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/tests/conftest.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/tests/unit/__init__.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/tests/unit/qt/__init__.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/tests/unit/qt/conftest.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/tests/unit/qt/test_action_card.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/tests/unit/qt/test_gather_packages.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/tests/unit/qt/test_install_preview.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/tests/unit/qt/test_log_panel.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/tests/unit/qt/test_logging.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/tests/unit/qt/test_preview_model.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/tests/unit/qt/test_sidebar.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/tests/unit/qt/test_tray_window_show.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/tests/unit/qt/test_update_feedback.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/tests/unit/test_cli.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/tests/unit/test_client_updater.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/tests/unit/test_client_version.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/tests/unit/test_config.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/tests/unit/test_examples.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/tests/unit/test_init.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/tests/unit/test_install.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/tests/unit/test_resolution.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/tests/unit/test_updater.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/tests/unit/test_uri.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/tests/unit/test_workers.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/tests/unit/windows/__init__.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/tests/unit/windows/conftest.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/tests/unit/windows/test_protocol.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/tests/unit/windows/test_startup.py +0 -0
|
@@ -11,9 +11,7 @@ from __future__ import annotations
|
|
|
11
11
|
import enum
|
|
12
12
|
from collections.abc import Callable
|
|
13
13
|
from dataclasses import dataclass, field
|
|
14
|
-
from enum import Enum, auto
|
|
15
14
|
from pathlib import Path
|
|
16
|
-
from typing import Protocol, runtime_checkable
|
|
17
15
|
|
|
18
16
|
from porringer.schema import (
|
|
19
17
|
PluginInfo,
|
|
@@ -343,62 +341,3 @@ class _DispatchState:
|
|
|
343
341
|
|
|
344
342
|
action_index: dict[int, int] = field(default_factory=dict)
|
|
345
343
|
got_parsed: bool = False
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
# ---------------------------------------------------------------------------
|
|
349
|
-
# Update view protocol & banner data models
|
|
350
|
-
# ---------------------------------------------------------------------------
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
@runtime_checkable
|
|
354
|
-
class UpdateView(Protocol):
|
|
355
|
-
"""Minimal display contract for the self-update lifecycle.
|
|
356
|
-
|
|
357
|
-
:class:`UpdateBanner` satisfies this protocol implicitly via
|
|
358
|
-
structural typing. The controller broadcasts state transitions
|
|
359
|
-
through a ``list[UpdateView]`` so that every window showing update
|
|
360
|
-
status stays in sync.
|
|
361
|
-
"""
|
|
362
|
-
|
|
363
|
-
def show_downloading(self, version: str) -> None:
|
|
364
|
-
"""Indicate that *version* is being downloaded."""
|
|
365
|
-
...
|
|
366
|
-
|
|
367
|
-
def show_downloading_progress(self, percentage: int) -> None:
|
|
368
|
-
"""Update the download progress indicator."""
|
|
369
|
-
...
|
|
370
|
-
|
|
371
|
-
def show_ready(self, version: str) -> None:
|
|
372
|
-
"""Indicate that *version* is downloaded and ready to install."""
|
|
373
|
-
...
|
|
374
|
-
|
|
375
|
-
def show_error(self, message: str) -> None:
|
|
376
|
-
"""Display an error *message* in the update area."""
|
|
377
|
-
...
|
|
378
|
-
|
|
379
|
-
def hide_banner(self) -> None:
|
|
380
|
-
"""Hide the update banner."""
|
|
381
|
-
...
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
class UpdateBannerState(Enum):
|
|
385
|
-
"""Visual states for the update banner."""
|
|
386
|
-
|
|
387
|
-
HIDDEN = auto()
|
|
388
|
-
DOWNLOADING = auto()
|
|
389
|
-
READY = auto()
|
|
390
|
-
ERROR = auto()
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
@dataclass(frozen=True, slots=True)
|
|
394
|
-
class _BannerConfig:
|
|
395
|
-
"""Bundled visual configuration for a banner state transition."""
|
|
396
|
-
|
|
397
|
-
state: UpdateBannerState
|
|
398
|
-
style: str
|
|
399
|
-
icon: str
|
|
400
|
-
text: str
|
|
401
|
-
text_style: str
|
|
402
|
-
version: str = ''
|
|
403
|
-
action_label: str = ''
|
|
404
|
-
show_progress: bool = False
|
|
@@ -32,7 +32,8 @@ from synodic_client.application.config_store import ConfigStore
|
|
|
32
32
|
from synodic_client.application.icon import app_icon
|
|
33
33
|
from synodic_client.application.screen import _format_relative_time
|
|
34
34
|
from synodic_client.application.screen.card import CardFrame
|
|
35
|
-
from synodic_client.application.theme import SETTINGS_WINDOW_MIN_SIZE
|
|
35
|
+
from synodic_client.application.theme import SETTINGS_WINDOW_MIN_SIZE
|
|
36
|
+
from synodic_client.application.update_model import UpdateModel
|
|
36
37
|
from synodic_client.logging import log_path, set_debug_level
|
|
37
38
|
from synodic_client.schema import GITHUB_REPO_URL
|
|
38
39
|
from synodic_client.startup import is_startup_registered, register_startup, remove_startup
|
|
@@ -237,6 +238,32 @@ class SettingsWindow(QMainWindow):
|
|
|
237
238
|
card.content_layout.addLayout(row)
|
|
238
239
|
return card
|
|
239
240
|
|
|
241
|
+
# ------------------------------------------------------------------
|
|
242
|
+
# Model binding
|
|
243
|
+
# ------------------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
def connect_model(self, model: UpdateModel) -> None:
|
|
246
|
+
"""Connect to an :class:`UpdateModel` for state observation.
|
|
247
|
+
|
|
248
|
+
The model's settings-facing signals drive the update status
|
|
249
|
+
label, check button, restart button, and timestamp label.
|
|
250
|
+
"""
|
|
251
|
+
model.status_text_changed.connect(self._on_status_changed)
|
|
252
|
+
model.check_button_enabled_changed.connect(self._check_updates_btn.setEnabled)
|
|
253
|
+
model.restart_visible_changed.connect(self._restart_btn.setVisible)
|
|
254
|
+
model.last_checked_changed.connect(self._on_last_checked_changed)
|
|
255
|
+
|
|
256
|
+
def _on_status_changed(self, text: str, style: str) -> None:
|
|
257
|
+
"""Apply a status text and style from the model."""
|
|
258
|
+
self._update_status_label.setText(text)
|
|
259
|
+
self._update_status_label.setStyleSheet(style)
|
|
260
|
+
|
|
261
|
+
def _on_last_checked_changed(self, timestamp: str) -> None:
|
|
262
|
+
"""Apply a *last updated* timestamp from the model."""
|
|
263
|
+
relative = _format_relative_time(timestamp)
|
|
264
|
+
self._last_client_update_label.setText(f'Last updated: {relative}')
|
|
265
|
+
self._last_client_update_label.setToolTip(f'Last updated: {timestamp}')
|
|
266
|
+
|
|
240
267
|
# ------------------------------------------------------------------
|
|
241
268
|
# Public API
|
|
242
269
|
# ------------------------------------------------------------------
|
|
@@ -275,37 +302,6 @@ class SettingsWindow(QMainWindow):
|
|
|
275
302
|
else:
|
|
276
303
|
self._last_client_update_label.setText('')
|
|
277
304
|
|
|
278
|
-
def set_update_status(self, text: str, style: str = '') -> None:
|
|
279
|
-
"""Set the inline status text next to the *Check for Updates* button.
|
|
280
|
-
|
|
281
|
-
Args:
|
|
282
|
-
text: The status message.
|
|
283
|
-
style: Optional stylesheet for the label (e.g. color).
|
|
284
|
-
"""
|
|
285
|
-
self._update_status_label.setText(text)
|
|
286
|
-
self._update_status_label.setStyleSheet(style)
|
|
287
|
-
|
|
288
|
-
def set_checking(self) -> None:
|
|
289
|
-
"""Enter the *checking* state — disable button and show status."""
|
|
290
|
-
self._check_updates_btn.setEnabled(False)
|
|
291
|
-
self._restart_btn.hide()
|
|
292
|
-
self._update_status_label.setText('Checking\u2026')
|
|
293
|
-
self._update_status_label.setStyleSheet(UPDATE_STATUS_CHECKING_STYLE)
|
|
294
|
-
|
|
295
|
-
def reset_check_updates_button(self) -> None:
|
|
296
|
-
"""Re-enable the *Check for Updates* button after a check completes."""
|
|
297
|
-
self._check_updates_btn.setEnabled(True)
|
|
298
|
-
|
|
299
|
-
def set_last_checked(self, timestamp: str) -> None:
|
|
300
|
-
"""Update the *last updated* label from an ISO 8601 timestamp."""
|
|
301
|
-
relative = _format_relative_time(timestamp)
|
|
302
|
-
self._last_client_update_label.setText(f'Last updated: {relative}')
|
|
303
|
-
self._last_client_update_label.setToolTip(f'Last updated: {timestamp}')
|
|
304
|
-
|
|
305
|
-
def show_restart_button(self) -> None:
|
|
306
|
-
"""Show the *Restart & Update* button."""
|
|
307
|
-
self._restart_btn.show()
|
|
308
|
-
|
|
309
305
|
def show(self) -> None:
|
|
310
306
|
"""Sync controls from config, size to content, then show the window."""
|
|
311
307
|
self.sync_from_config()
|
|
@@ -360,8 +356,6 @@ class SettingsWindow(QMainWindow):
|
|
|
360
356
|
|
|
361
357
|
def _on_check_updates_clicked(self) -> None:
|
|
362
358
|
"""Handle the *Check for Updates* button click."""
|
|
363
|
-
self._check_updates_btn.setEnabled(False)
|
|
364
|
-
self._update_status_label.setText('Checking\u2026')
|
|
365
359
|
self.check_updates_requested.emit()
|
|
366
360
|
|
|
367
361
|
def _on_channel_changed(self, index: int) -> None:
|
{synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev66}/synodic_client/application/screen/tray.py
RENAMED
|
@@ -16,6 +16,7 @@ from synodic_client.application.screen.screen import MainWindow
|
|
|
16
16
|
from synodic_client.application.screen.settings import SettingsWindow
|
|
17
17
|
from synodic_client.application.screen.tool_update_controller import ToolUpdateOrchestrator
|
|
18
18
|
from synodic_client.application.update_controller import UpdateController
|
|
19
|
+
from synodic_client.application.update_model import UpdateModel
|
|
19
20
|
from synodic_client.client import Client
|
|
20
21
|
|
|
21
22
|
if TYPE_CHECKING:
|
|
@@ -66,17 +67,29 @@ class TrayScreen:
|
|
|
66
67
|
# MainWindow gear button -> open settings
|
|
67
68
|
window.settings_requested.connect(self._show_settings)
|
|
68
69
|
|
|
70
|
+
# Update model — centralised observable state for the update lifecycle
|
|
71
|
+
self._update_model = UpdateModel()
|
|
72
|
+
|
|
69
73
|
# Update controller - owns the self-update lifecycle & timer
|
|
70
74
|
self._banner = window.update_banner
|
|
71
75
|
self._update_controller = UpdateController(
|
|
72
76
|
app,
|
|
73
77
|
client,
|
|
74
|
-
|
|
75
|
-
settings_window=self._settings_window,
|
|
78
|
+
self._update_model,
|
|
76
79
|
store=self._store,
|
|
77
80
|
)
|
|
78
81
|
self._update_controller.set_user_active_predicate(self._is_user_active)
|
|
79
82
|
|
|
83
|
+
# Connect views to the model
|
|
84
|
+
self._banner.connect_model(self._update_model)
|
|
85
|
+
self._settings_window.connect_model(self._update_model)
|
|
86
|
+
|
|
87
|
+
# Wire user-action signals back to the controller
|
|
88
|
+
self._banner.restart_requested.connect(self._update_controller.request_apply)
|
|
89
|
+
self._banner.retry_requested.connect(self._update_controller.request_retry)
|
|
90
|
+
self._settings_window.check_updates_requested.connect(self._update_controller.request_check)
|
|
91
|
+
self._settings_window.restart_requested.connect(self._update_controller.request_apply)
|
|
92
|
+
|
|
80
93
|
# Tool update orchestrator - owns tool/package update lifecycle
|
|
81
94
|
self._tool_orchestrator = ToolUpdateOrchestrator(
|
|
82
95
|
window,
|
|
@@ -15,6 +15,8 @@ The banner slides in/out using a ``QPropertyAnimation`` on
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
17
|
import logging
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from enum import Enum, auto
|
|
18
20
|
|
|
19
21
|
from PySide6.QtCore import (
|
|
20
22
|
QEasingCurve,
|
|
@@ -34,7 +36,6 @@ from PySide6.QtWidgets import (
|
|
|
34
36
|
QWidget,
|
|
35
37
|
)
|
|
36
38
|
|
|
37
|
-
from synodic_client.application.screen.schema import UpdateBannerState, _BannerConfig
|
|
38
39
|
from synodic_client.application.theme import (
|
|
39
40
|
UPDATE_BANNER_ANIMATION_MS,
|
|
40
41
|
UPDATE_BANNER_BTN_STYLE,
|
|
@@ -47,10 +48,34 @@ from synodic_client.application.theme import (
|
|
|
47
48
|
UPDATE_BANNER_STYLE,
|
|
48
49
|
UPDATE_BANNER_VERSION_STYLE,
|
|
49
50
|
)
|
|
51
|
+
from synodic_client.application.update_model import UpdateModel, UpdatePhase
|
|
50
52
|
|
|
51
53
|
logger = logging.getLogger(__name__)
|
|
52
54
|
|
|
53
55
|
|
|
56
|
+
class UpdateBannerState(Enum):
|
|
57
|
+
"""Visual states for the update banner."""
|
|
58
|
+
|
|
59
|
+
HIDDEN = auto()
|
|
60
|
+
DOWNLOADING = auto()
|
|
61
|
+
READY = auto()
|
|
62
|
+
ERROR = auto()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True, slots=True)
|
|
66
|
+
class _BannerConfig:
|
|
67
|
+
"""Bundled visual configuration for a banner state transition."""
|
|
68
|
+
|
|
69
|
+
state: UpdateBannerState
|
|
70
|
+
style: str
|
|
71
|
+
icon: str
|
|
72
|
+
text: str
|
|
73
|
+
text_style: str
|
|
74
|
+
version: str = ''
|
|
75
|
+
action_label: str = ''
|
|
76
|
+
show_progress: bool = False
|
|
77
|
+
|
|
78
|
+
|
|
54
79
|
# Height of the banner content (progress variant is slightly taller).
|
|
55
80
|
_BANNER_HEIGHT = 38
|
|
56
81
|
_BANNER_HEIGHT_WITH_PROGRESS = 44
|
|
@@ -77,6 +102,7 @@ class UpdateBanner(QFrame):
|
|
|
77
102
|
|
|
78
103
|
self._state = UpdateBannerState.HIDDEN
|
|
79
104
|
self._target_version: str = ''
|
|
105
|
+
self._error_dismiss_timer: QTimer | None = None
|
|
80
106
|
|
|
81
107
|
# --- Layout ---
|
|
82
108
|
self._outer = QVBoxLayout(self)
|
|
@@ -129,6 +155,28 @@ class UpdateBanner(QFrame):
|
|
|
129
155
|
self._anim.setEasingCurve(QEasingCurve.Type.OutCubic)
|
|
130
156
|
self._anim.setDuration(UPDATE_BANNER_ANIMATION_MS)
|
|
131
157
|
|
|
158
|
+
# --- Model binding ---
|
|
159
|
+
|
|
160
|
+
def connect_model(self, model: UpdateModel) -> None:
|
|
161
|
+
"""Connect to an :class:`UpdateModel` for state observation.
|
|
162
|
+
|
|
163
|
+
The model's lifecycle signals drive the banner's visual state
|
|
164
|
+
transitions so the controller never needs to call banner
|
|
165
|
+
methods directly.
|
|
166
|
+
"""
|
|
167
|
+
self._model = model
|
|
168
|
+
model.phase_changed.connect(self._on_model_phase)
|
|
169
|
+
model.progress_changed.connect(self.show_downloading_progress)
|
|
170
|
+
|
|
171
|
+
def _on_model_phase(self, phase: UpdatePhase) -> None:
|
|
172
|
+
"""React to a lifecycle phase change from the model."""
|
|
173
|
+
if phase == UpdatePhase.DOWNLOADING:
|
|
174
|
+
self.show_downloading(self._model.version)
|
|
175
|
+
elif phase == UpdatePhase.READY:
|
|
176
|
+
self.show_ready(self._model.version)
|
|
177
|
+
elif phase == UpdatePhase.ERROR:
|
|
178
|
+
self.show_error(self._model.error_message)
|
|
179
|
+
|
|
132
180
|
# --- Public API ---
|
|
133
181
|
|
|
134
182
|
@property
|
|
@@ -191,6 +239,12 @@ class UpdateBanner(QFrame):
|
|
|
191
239
|
Args:
|
|
192
240
|
message: Human-readable error description.
|
|
193
241
|
"""
|
|
242
|
+
# Cancel any pending auto-dismiss from a previous error to avoid
|
|
243
|
+
# stacking timers that would forcibly hide a freshly shown banner.
|
|
244
|
+
if self._error_dismiss_timer is not None:
|
|
245
|
+
self._error_dismiss_timer.stop()
|
|
246
|
+
self._error_dismiss_timer = None
|
|
247
|
+
|
|
194
248
|
self._configure(
|
|
195
249
|
_BannerConfig(
|
|
196
250
|
state=UpdateBannerState.ERROR,
|
|
@@ -201,12 +255,20 @@ class UpdateBanner(QFrame):
|
|
|
201
255
|
action_label='Retry',
|
|
202
256
|
)
|
|
203
257
|
)
|
|
204
|
-
QTimer
|
|
258
|
+
timer = QTimer(self)
|
|
259
|
+
timer.setSingleShot(True)
|
|
260
|
+
timer.setInterval(UPDATE_BANNER_ERROR_DISMISS_MS)
|
|
261
|
+
timer.timeout.connect(self._auto_dismiss_error)
|
|
262
|
+
timer.start()
|
|
263
|
+
self._error_dismiss_timer = timer
|
|
205
264
|
|
|
206
265
|
def hide_banner(self) -> None:
|
|
207
266
|
"""Slide the banner out and reset to hidden."""
|
|
208
267
|
if self._state == UpdateBannerState.HIDDEN:
|
|
209
268
|
return
|
|
269
|
+
if self._error_dismiss_timer is not None:
|
|
270
|
+
self._error_dismiss_timer.stop()
|
|
271
|
+
self._error_dismiss_timer = None
|
|
210
272
|
self._state = UpdateBannerState.HIDDEN
|
|
211
273
|
self._animate_height(0)
|
|
212
274
|
|