synodic-client 0.0.1.dev64__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.dev64 → synodic_client-0.0.1.dev66}/PKG-INFO +2 -2
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/pyproject.toml +2 -2
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/synodic_client/application/config_store.py +1 -1
- synodic_client-0.0.1.dev66/synodic_client/application/package_state.py +125 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/synodic_client/application/screen/install.py +14 -7
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/synodic_client/application/screen/install_workers.py +0 -1
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/synodic_client/application/screen/projects.py +5 -1
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/synodic_client/application/screen/schema.py +0 -62
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/synodic_client/application/screen/screen.py +60 -31
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/synodic_client/application/screen/settings.py +28 -44
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/synodic_client/application/screen/tool_update_controller.py +10 -4
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/synodic_client/application/screen/tray.py +15 -2
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/synodic_client/application/screen/update_banner.py +64 -2
- {synodic_client-0.0.1.dev64 → 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.dev64 → synodic_client-0.0.1.dev66}/synodic_client/resolution.py +0 -1
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/synodic_client/schema.py +0 -5
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/tests/unit/qt/test_gather_packages.py +1 -2
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/tests/unit/qt/test_install_preview.py +6 -9
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/tests/unit/qt/test_settings.py +27 -43
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/tests/unit/qt/test_tray_window_show.py +0 -1
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/tests/unit/qt/test_update_banner.py +1 -2
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/tests/unit/qt/test_update_controller.py +80 -63
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/tests/unit/test_config.py +0 -2
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/tests/unit/test_resolution.py +0 -1
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/LICENSE.md +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/README.md +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/synodic_client/__init__.py +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/synodic_client/__main__.py +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/synodic_client/application/__init__.py +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/synodic_client/application/bootstrap.py +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/synodic_client/application/data.py +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/synodic_client/application/icon.py +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/synodic_client/application/init.py +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/synodic_client/application/instance.py +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/synodic_client/application/qt.py +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/synodic_client/application/schema.py +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/synodic_client/application/screen/__init__.py +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/synodic_client/application/screen/action_card.py +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/synodic_client/application/screen/card.py +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/synodic_client/application/screen/log_panel.py +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/synodic_client/application/screen/plugin_row.py +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/synodic_client/application/screen/sidebar.py +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/synodic_client/application/screen/spinner.py +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/synodic_client/application/theme.py +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/synodic_client/application/uri.py +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/synodic_client/application/workers.py +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/synodic_client/cli.py +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/synodic_client/client.py +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/synodic_client/config.py +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/synodic_client/logging.py +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/synodic_client/protocol.py +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/synodic_client/py.typed +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/synodic_client/startup.py +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/synodic_client/subprocess_patch.py +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/synodic_client/updater.py +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/tests/__init__.py +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/tests/conftest.py +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/tests/unit/__init__.py +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/tests/unit/qt/__init__.py +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/tests/unit/qt/conftest.py +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/tests/unit/qt/test_action_card.py +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/tests/unit/qt/test_log_panel.py +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/tests/unit/qt/test_logging.py +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/tests/unit/qt/test_preview_model.py +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/tests/unit/qt/test_sidebar.py +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/tests/unit/qt/test_update_feedback.py +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/tests/unit/test_cli.py +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/tests/unit/test_client_updater.py +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/tests/unit/test_client_version.py +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/tests/unit/test_examples.py +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/tests/unit/test_init.py +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/tests/unit/test_install.py +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/tests/unit/test_updater.py +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/tests/unit/test_uri.py +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/tests/unit/test_workers.py +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/tests/unit/windows/__init__.py +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/tests/unit/windows/conftest.py +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/tests/unit/windows/test_protocol.py +0 -0
- {synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/tests/unit/windows/test_startup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: synodic_client
|
|
3
|
-
Version: 0.0.1.
|
|
3
|
+
Version: 0.0.1.dev66
|
|
4
4
|
Author-Email: Synodic Software <contact@synodic.software>
|
|
5
5
|
License: LGPL-3.0-or-later
|
|
6
6
|
Project-URL: homepage, https://github.com/synodic/synodic-client
|
|
@@ -8,7 +8,7 @@ Project-URL: repository, https://github.com/synodic/synodic-client
|
|
|
8
8
|
Requires-Python: <3.15,>=3.14
|
|
9
9
|
Requires-Dist: pyside6>=6.10.2
|
|
10
10
|
Requires-Dist: packaging>=26.0
|
|
11
|
-
Requires-Dist: porringer>=0.2.1.
|
|
11
|
+
Requires-Dist: porringer>=0.2.1.dev78
|
|
12
12
|
Requires-Dist: qasync>=0.28.0
|
|
13
13
|
Requires-Dist: velopack>=0.0.1444.dev49733
|
|
14
14
|
Requires-Dist: typer>=0.24.1
|
|
@@ -10,12 +10,12 @@ requires-python = ">=3.14, <3.15"
|
|
|
10
10
|
dependencies = [
|
|
11
11
|
"pyside6>=6.10.2",
|
|
12
12
|
"packaging>=26.0",
|
|
13
|
-
"porringer>=0.2.1.
|
|
13
|
+
"porringer>=0.2.1.dev78",
|
|
14
14
|
"qasync>=0.28.0",
|
|
15
15
|
"velopack>=0.0.1444.dev49733",
|
|
16
16
|
"typer>=0.24.1",
|
|
17
17
|
]
|
|
18
|
-
version = "0.0.1.
|
|
18
|
+
version = "0.0.1.dev66"
|
|
19
19
|
|
|
20
20
|
[project.license]
|
|
21
21
|
text = "LGPL-3.0-or-later"
|
{synodic_client-0.0.1.dev64 → synodic_client-0.0.1.dev66}/synodic_client/application/config_store.py
RENAMED
|
@@ -24,7 +24,7 @@ class ConfigStore(QObject):
|
|
|
24
24
|
|
|
25
25
|
store = ConfigStore(initial_config)
|
|
26
26
|
store.changed.connect(some_consumer.on_config_changed)
|
|
27
|
-
store.update(auto_apply=False)
|
|
27
|
+
store.update(auto_apply=False) # persists + emits
|
|
28
28
|
"""
|
|
29
29
|
|
|
30
30
|
changed = Signal(object)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Shared package update state registry.
|
|
2
|
+
|
|
3
|
+
Provides :class:`PackageStateStore`, a centralised record of
|
|
4
|
+
per-package update status used by both ToolsView and ProjectsView
|
|
5
|
+
so that version/update information discovered in one view is
|
|
6
|
+
immediately available to the other.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
|
|
13
|
+
from PySide6.QtCore import QObject, Signal
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(slots=True)
|
|
17
|
+
class PackageState:
|
|
18
|
+
"""Canonical update state for a single package.
|
|
19
|
+
|
|
20
|
+
Keyed by ``(signal_key, name)`` inside :class:`PackageStateStore`.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
name: str
|
|
24
|
+
"""Package name (e.g. ``"ruff"``)."""
|
|
25
|
+
|
|
26
|
+
installed_version: str = ''
|
|
27
|
+
"""Currently installed version, or empty if unknown."""
|
|
28
|
+
|
|
29
|
+
available_version: str = ''
|
|
30
|
+
"""Latest available version, or empty if unknown."""
|
|
31
|
+
|
|
32
|
+
has_update: bool = False
|
|
33
|
+
"""Whether the package has a newer version available."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class PackageStateStore(QObject):
|
|
37
|
+
"""Shared registry of package update states across views.
|
|
38
|
+
|
|
39
|
+
Both ToolsView and ProjectsView write discovered update information
|
|
40
|
+
into the store; each view can then read the canonical state
|
|
41
|
+
regardless of which view made the discovery.
|
|
42
|
+
|
|
43
|
+
:attr:`state_changed` is emitted whenever new data arrives so
|
|
44
|
+
listeners can refresh their badges/labels without polling.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
state_changed = Signal()
|
|
48
|
+
"""Emitted whenever any package state is created or modified."""
|
|
49
|
+
|
|
50
|
+
def __init__(self, parent: QObject | None = None) -> None:
|
|
51
|
+
"""Initialise with an empty registry."""
|
|
52
|
+
super().__init__(parent)
|
|
53
|
+
self._data: dict[str, dict[str, PackageState]] = {}
|
|
54
|
+
|
|
55
|
+
# -- Bulk write (ToolsView check_updates) ----------------------------
|
|
56
|
+
|
|
57
|
+
def set_check_results(self, available: dict[str, dict[str, str]]) -> None:
|
|
58
|
+
"""Populate from ``check_updates`` results.
|
|
59
|
+
|
|
60
|
+
*available* maps ``{signal_key: {package_name: latest_version}}``
|
|
61
|
+
— the same shape previously stored in
|
|
62
|
+
``ToolsView._updates_available``.
|
|
63
|
+
"""
|
|
64
|
+
self._data.clear()
|
|
65
|
+
for key, packages in available.items():
|
|
66
|
+
for pkg_name, latest in packages.items():
|
|
67
|
+
self._data.setdefault(key, {})[pkg_name] = PackageState(
|
|
68
|
+
name=pkg_name,
|
|
69
|
+
available_version=latest,
|
|
70
|
+
has_update=True,
|
|
71
|
+
)
|
|
72
|
+
self.state_changed.emit()
|
|
73
|
+
|
|
74
|
+
# -- Single-action write (ProjectsView dry-run) ----------------------
|
|
75
|
+
|
|
76
|
+
def record_action_result(
|
|
77
|
+
self,
|
|
78
|
+
signal_key: str,
|
|
79
|
+
pkg_name: str,
|
|
80
|
+
*,
|
|
81
|
+
installed_version: str = '',
|
|
82
|
+
available_version: str = '',
|
|
83
|
+
has_update: bool = False,
|
|
84
|
+
) -> None:
|
|
85
|
+
"""Record a single dry-run result.
|
|
86
|
+
|
|
87
|
+
Merges with any existing state so that data discovered by
|
|
88
|
+
different views accumulates rather than overwrites.
|
|
89
|
+
"""
|
|
90
|
+
existing = self._data.get(signal_key, {}).get(pkg_name)
|
|
91
|
+
state = PackageState(
|
|
92
|
+
name=pkg_name,
|
|
93
|
+
installed_version=installed_version or (existing.installed_version if existing else ''),
|
|
94
|
+
available_version=available_version or (existing.available_version if existing else ''),
|
|
95
|
+
has_update=has_update or (existing.has_update if existing else False),
|
|
96
|
+
)
|
|
97
|
+
self._data.setdefault(signal_key, {})[pkg_name] = state
|
|
98
|
+
self.state_changed.emit()
|
|
99
|
+
|
|
100
|
+
# -- Read API --------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
def get_updates(self, signal_key: str) -> dict[str, str]:
|
|
103
|
+
"""Return ``{package_name: latest_version}`` for packages with updates.
|
|
104
|
+
|
|
105
|
+
Drop-in replacement for ``_updates_available.get(key, {})``.
|
|
106
|
+
"""
|
|
107
|
+
bucket = self._data.get(signal_key, {})
|
|
108
|
+
return {name: s.available_version for name, s in bucket.items() if s.has_update}
|
|
109
|
+
|
|
110
|
+
def has_updates_for(self, signal_key: str) -> bool:
|
|
111
|
+
"""Return whether any package under *signal_key* has an update."""
|
|
112
|
+
return any(s.has_update for s in self._data.get(signal_key, {}).values())
|
|
113
|
+
|
|
114
|
+
def get(self, signal_key: str, pkg_name: str) -> PackageState | None:
|
|
115
|
+
"""Return the state for a specific package, or ``None``."""
|
|
116
|
+
return self._data.get(signal_key, {}).get(pkg_name)
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def has_data(self) -> bool:
|
|
120
|
+
"""Return whether any update data has been recorded."""
|
|
121
|
+
return bool(self._data)
|
|
122
|
+
|
|
123
|
+
def clear(self) -> None:
|
|
124
|
+
"""Remove all recorded state."""
|
|
125
|
+
self._data.clear()
|
|
@@ -43,6 +43,7 @@ from PySide6.QtWidgets import (
|
|
|
43
43
|
QWidget,
|
|
44
44
|
)
|
|
45
45
|
|
|
46
|
+
from synodic_client.application.package_state import PackageStateStore
|
|
46
47
|
from synodic_client.application.screen import skip_reason_label
|
|
47
48
|
from synodic_client.application.screen.action_card import ActionCardList, action_key
|
|
48
49
|
from synodic_client.application.screen.card import CardFrame
|
|
@@ -113,6 +114,7 @@ class SetupPreviewWidget(QWidget):
|
|
|
113
114
|
*,
|
|
114
115
|
show_close: bool = True,
|
|
115
116
|
config: ResolvedConfig | None = None,
|
|
117
|
+
package_store: PackageStateStore | None = None,
|
|
116
118
|
) -> None:
|
|
117
119
|
"""Initialize the preview widget.
|
|
118
120
|
|
|
@@ -122,11 +124,13 @@ class SetupPreviewWidget(QWidget):
|
|
|
122
124
|
show_close: Whether to show the Close button. Set ``False``
|
|
123
125
|
when embedding inside a persistent view (e.g. a tab).
|
|
124
126
|
config: Global configuration for per-manifest pre-release state.
|
|
127
|
+
package_store: Shared package update state registry.
|
|
125
128
|
"""
|
|
126
129
|
super().__init__(parent)
|
|
127
130
|
self._porringer = porringer
|
|
128
131
|
self._show_close = show_close
|
|
129
132
|
self._config = config
|
|
133
|
+
self._package_store = package_store
|
|
130
134
|
self._discovered_plugins: DiscoveredPlugins | None = None
|
|
131
135
|
|
|
132
136
|
self._model = PreviewModel()
|
|
@@ -293,7 +297,6 @@ class SetupPreviewWidget(QWidget):
|
|
|
293
297
|
path_or_url: str,
|
|
294
298
|
*,
|
|
295
299
|
project_directory: Path | None = None,
|
|
296
|
-
detect_updates: bool = True,
|
|
297
300
|
) -> None:
|
|
298
301
|
"""Load a manifest preview, or skip if the same manifest is already showing results.
|
|
299
302
|
|
|
@@ -305,7 +308,6 @@ class SetupPreviewWidget(QWidget):
|
|
|
305
308
|
Args:
|
|
306
309
|
path_or_url: Manifest path or URL.
|
|
307
310
|
project_directory: Working directory for project sync actions.
|
|
308
|
-
detect_updates: Query package indices for newer versions.
|
|
309
311
|
"""
|
|
310
312
|
key = normalize_manifest_key(path_or_url)
|
|
311
313
|
|
|
@@ -345,7 +347,6 @@ class SetupPreviewWidget(QWidget):
|
|
|
345
347
|
self._run_preview_task(
|
|
346
348
|
path_or_url,
|
|
347
349
|
project_directory=self._model.project_directory,
|
|
348
|
-
detect_updates=detect_updates,
|
|
349
350
|
prerelease_packages=overrides,
|
|
350
351
|
),
|
|
351
352
|
)
|
|
@@ -474,7 +475,6 @@ class SetupPreviewWidget(QWidget):
|
|
|
474
475
|
path_or_url: str,
|
|
475
476
|
*,
|
|
476
477
|
project_directory: Path | None = None,
|
|
477
|
-
detect_updates: bool = True,
|
|
478
478
|
prerelease_packages: set[str] | None = None,
|
|
479
479
|
) -> None:
|
|
480
480
|
"""Run the preview coroutine and route completion/errors."""
|
|
@@ -484,7 +484,6 @@ class SetupPreviewWidget(QWidget):
|
|
|
484
484
|
path_or_url,
|
|
485
485
|
config=PreviewConfig(
|
|
486
486
|
project_directory=project_directory,
|
|
487
|
-
detect_updates=detect_updates,
|
|
488
487
|
prerelease_packages=prerelease_packages,
|
|
489
488
|
),
|
|
490
489
|
callbacks=PreviewCallbacks(
|
|
@@ -640,6 +639,16 @@ class SetupPreviewWidget(QWidget):
|
|
|
640
639
|
if card is not None:
|
|
641
640
|
card.set_check_result(result)
|
|
642
641
|
|
|
642
|
+
# Record in shared store so ToolsView can reflect the update
|
|
643
|
+
if self._package_store is not None and action.installer and action.package:
|
|
644
|
+
self._package_store.record_action_result(
|
|
645
|
+
action.installer,
|
|
646
|
+
str(action.package.name),
|
|
647
|
+
installed_version=result.installed_version or '',
|
|
648
|
+
available_version=result.available_version or '',
|
|
649
|
+
has_update=result.skip_reason == SkipReason.UPDATE_AVAILABLE,
|
|
650
|
+
)
|
|
651
|
+
|
|
643
652
|
# Update phase text
|
|
644
653
|
m.checked_count += 1
|
|
645
654
|
total = len(m.action_states)
|
|
@@ -987,11 +996,9 @@ class InstallPreviewWindow(QMainWindow):
|
|
|
987
996
|
logger.info('Starting install preview for: %s', self._manifest_url)
|
|
988
997
|
self._url_label.setText(f'<b>Manifest:</b> {self._manifest_url}')
|
|
989
998
|
|
|
990
|
-
detect = self._config.detect_updates if self._config else True
|
|
991
999
|
self._preview_widget.load(
|
|
992
1000
|
self._manifest_url,
|
|
993
1001
|
project_directory=self._project_directory,
|
|
994
|
-
detect_updates=detect,
|
|
995
1002
|
)
|
|
996
1003
|
|
|
997
1004
|
# --- Callbacks ---
|
|
@@ -20,6 +20,7 @@ from PySide6.QtWidgets import (
|
|
|
20
20
|
)
|
|
21
21
|
|
|
22
22
|
from synodic_client.application.data import DataCoordinator
|
|
23
|
+
from synodic_client.application.package_state import PackageStateStore
|
|
23
24
|
from synodic_client.application.screen.install import SetupPreviewWidget
|
|
24
25
|
from synodic_client.application.screen.schema import PreviewPhase
|
|
25
26
|
from synodic_client.application.screen.sidebar import ManifestSidebar
|
|
@@ -48,6 +49,7 @@ class ProjectsView(QWidget):
|
|
|
48
49
|
parent: QWidget | None = None,
|
|
49
50
|
*,
|
|
50
51
|
coordinator: DataCoordinator | None = None,
|
|
52
|
+
package_store: PackageStateStore | None = None,
|
|
51
53
|
) -> None:
|
|
52
54
|
"""Initialize the projects view.
|
|
53
55
|
|
|
@@ -57,11 +59,13 @@ class ProjectsView(QWidget):
|
|
|
57
59
|
parent: Optional parent widget.
|
|
58
60
|
coordinator: Shared data coordinator for validated directory
|
|
59
61
|
data.
|
|
62
|
+
package_store: Shared package update state registry.
|
|
60
63
|
"""
|
|
61
64
|
super().__init__(parent)
|
|
62
65
|
self._porringer = porringer
|
|
63
66
|
self._store = store
|
|
64
67
|
self._coordinator = coordinator
|
|
68
|
+
self._package_store = package_store
|
|
65
69
|
self._refresh_in_progress = False
|
|
66
70
|
self._pending_select: Path | None = None
|
|
67
71
|
self._widgets: dict[Path, SetupPreviewWidget] = {}
|
|
@@ -166,7 +170,6 @@ class ProjectsView(QWidget):
|
|
|
166
170
|
widget.load(
|
|
167
171
|
str(path),
|
|
168
172
|
project_directory=path if path.is_dir() else path.parent,
|
|
169
|
-
detect_updates=self._store.config.detect_updates,
|
|
170
173
|
)
|
|
171
174
|
|
|
172
175
|
except Exception:
|
|
@@ -200,6 +203,7 @@ class ProjectsView(QWidget):
|
|
|
200
203
|
self,
|
|
201
204
|
show_close=False,
|
|
202
205
|
config=self._store.config,
|
|
206
|
+
package_store=self._package_store,
|
|
203
207
|
)
|
|
204
208
|
widget._discovered_plugins = discovered
|
|
205
209
|
widget.install_finished.connect(self._on_install_finished)
|
|
@@ -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,
|
|
@@ -334,7 +332,6 @@ class PreviewConfig:
|
|
|
334
332
|
"""Optional execution parameters for :func:`run_preview`."""
|
|
335
333
|
|
|
336
334
|
project_directory: Path | None = None
|
|
337
|
-
detect_updates: bool = True
|
|
338
335
|
prerelease_packages: set[str] | None = None
|
|
339
336
|
|
|
340
337
|
|
|
@@ -344,62 +341,3 @@ class _DispatchState:
|
|
|
344
341
|
|
|
345
342
|
action_index: dict[int, int] = field(default_factory=dict)
|
|
346
343
|
got_parsed: bool = False
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
# ---------------------------------------------------------------------------
|
|
350
|
-
# Update view protocol & banner data models
|
|
351
|
-
# ---------------------------------------------------------------------------
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
@runtime_checkable
|
|
355
|
-
class UpdateView(Protocol):
|
|
356
|
-
"""Minimal display contract for the self-update lifecycle.
|
|
357
|
-
|
|
358
|
-
:class:`UpdateBanner` satisfies this protocol implicitly via
|
|
359
|
-
structural typing. The controller broadcasts state transitions
|
|
360
|
-
through a ``list[UpdateView]`` so that every window showing update
|
|
361
|
-
status stays in sync.
|
|
362
|
-
"""
|
|
363
|
-
|
|
364
|
-
def show_downloading(self, version: str) -> None:
|
|
365
|
-
"""Indicate that *version* is being downloaded."""
|
|
366
|
-
...
|
|
367
|
-
|
|
368
|
-
def show_downloading_progress(self, percentage: int) -> None:
|
|
369
|
-
"""Update the download progress indicator."""
|
|
370
|
-
...
|
|
371
|
-
|
|
372
|
-
def show_ready(self, version: str) -> None:
|
|
373
|
-
"""Indicate that *version* is downloaded and ready to install."""
|
|
374
|
-
...
|
|
375
|
-
|
|
376
|
-
def show_error(self, message: str) -> None:
|
|
377
|
-
"""Display an error *message* in the update area."""
|
|
378
|
-
...
|
|
379
|
-
|
|
380
|
-
def hide_banner(self) -> None:
|
|
381
|
-
"""Hide the update banner."""
|
|
382
|
-
...
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
class UpdateBannerState(Enum):
|
|
386
|
-
"""Visual states for the update banner."""
|
|
387
|
-
|
|
388
|
-
HIDDEN = auto()
|
|
389
|
-
DOWNLOADING = auto()
|
|
390
|
-
READY = auto()
|
|
391
|
-
ERROR = auto()
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
@dataclass(frozen=True, slots=True)
|
|
395
|
-
class _BannerConfig:
|
|
396
|
-
"""Bundled visual configuration for a banner state transition."""
|
|
397
|
-
|
|
398
|
-
state: UpdateBannerState
|
|
399
|
-
style: str
|
|
400
|
-
icon: str
|
|
401
|
-
text: str
|
|
402
|
-
text_style: str
|
|
403
|
-
version: str = ''
|
|
404
|
-
action_label: str = ''
|
|
405
|
-
show_progress: bool = False
|
|
@@ -37,6 +37,7 @@ from PySide6.QtWidgets import (
|
|
|
37
37
|
from synodic_client.application.config_store import ConfigStore
|
|
38
38
|
from synodic_client.application.data import DataCoordinator
|
|
39
39
|
from synodic_client.application.icon import app_icon
|
|
40
|
+
from synodic_client.application.package_state import PackageStateStore
|
|
40
41
|
from synodic_client.application.screen.plugin_row import (
|
|
41
42
|
FilterChip,
|
|
42
43
|
PluginKindHeader,
|
|
@@ -115,6 +116,7 @@ class ToolsView(QWidget):
|
|
|
115
116
|
parent: QWidget | None = None,
|
|
116
117
|
*,
|
|
117
118
|
coordinator: DataCoordinator | None = None,
|
|
119
|
+
package_store: PackageStateStore | None = None,
|
|
118
120
|
) -> None:
|
|
119
121
|
"""Initialize the tools view.
|
|
120
122
|
|
|
@@ -125,22 +127,25 @@ class ToolsView(QWidget):
|
|
|
125
127
|
coordinator: Shared data coordinator. When provided, the
|
|
126
128
|
view delegates plugin/directory fetching to the
|
|
127
129
|
coordinator instead of calling porringer directly.
|
|
130
|
+
package_store: Shared package update state registry.
|
|
128
131
|
"""
|
|
129
132
|
super().__init__(parent)
|
|
130
133
|
self._porringer = porringer
|
|
131
134
|
self._store = store
|
|
132
135
|
self._coordinator = coordinator
|
|
136
|
+
self._package_store = package_store
|
|
133
137
|
self._section_widgets: list[QWidget] = []
|
|
134
138
|
self._filter_chips: dict[str, FilterChip] = {}
|
|
135
139
|
self._deselected_plugins: set[str] = set()
|
|
136
140
|
self._refresh_in_progress = False
|
|
137
141
|
self._check_in_progress = False
|
|
138
|
-
self._updates_checked = False
|
|
139
|
-
self._updates_available: dict[str, dict[str, str]] = {}
|
|
140
142
|
self._directories: list[ManifestDirectory] = []
|
|
141
143
|
self._timestamp_timer: QTimer | None = None
|
|
142
144
|
self._init_ui()
|
|
143
145
|
|
|
146
|
+
if self._package_store is not None:
|
|
147
|
+
self._package_store.state_changed.connect(self._on_package_state_changed)
|
|
148
|
+
|
|
144
149
|
def _init_ui(self) -> None:
|
|
145
150
|
"""Initialize the UI components."""
|
|
146
151
|
outer = QVBoxLayout(self)
|
|
@@ -234,6 +239,11 @@ class ToolsView(QWidget):
|
|
|
234
239
|
|
|
235
240
|
# --- Public API ---
|
|
236
241
|
|
|
242
|
+
def invalidate_update_data(self) -> None:
|
|
243
|
+
"""Clear cached update state so the next refresh re-checks."""
|
|
244
|
+
if self._package_store is not None:
|
|
245
|
+
self._package_store.clear()
|
|
246
|
+
|
|
237
247
|
def refresh(self) -> None:
|
|
238
248
|
"""Schedule an asynchronous rebuild of the tool list."""
|
|
239
249
|
if self._refresh_in_progress:
|
|
@@ -257,7 +267,7 @@ class ToolsView(QWidget):
|
|
|
257
267
|
|
|
258
268
|
try:
|
|
259
269
|
data = await self._gather_refresh_data()
|
|
260
|
-
need_deferred_check = not self.
|
|
270
|
+
need_deferred_check = not self._has_update_data
|
|
261
271
|
self._build_widget_tree(data)
|
|
262
272
|
except Exception:
|
|
263
273
|
logger.exception('Failed to refresh tools')
|
|
@@ -272,7 +282,7 @@ class ToolsView(QWidget):
|
|
|
272
282
|
# Fire-and-forget: detect updates in the background, then patch
|
|
273
283
|
# the just-rendered widget tree with update badges.
|
|
274
284
|
if need_deferred_check:
|
|
275
|
-
asyncio.create_task(self._deferred_update_check(
|
|
285
|
+
asyncio.create_task(self._deferred_update_check())
|
|
276
286
|
|
|
277
287
|
# ------------------------------------------------------------------
|
|
278
288
|
# _async_refresh helper methods
|
|
@@ -445,7 +455,6 @@ class ToolsView(QWidget):
|
|
|
445
455
|
return
|
|
446
456
|
|
|
447
457
|
auto_val = auto_update_map.get(plugin.name, True)
|
|
448
|
-
plugin_updates = self._updates_available.get(plugin.name, {})
|
|
449
458
|
tool_timestamps = self._store.config.last_tool_updates or {}
|
|
450
459
|
default_exe = data.default_runtime_executable
|
|
451
460
|
|
|
@@ -462,11 +471,14 @@ class ToolsView(QWidget):
|
|
|
462
471
|
if is_default:
|
|
463
472
|
tag_text += ' (default)'
|
|
464
473
|
|
|
474
|
+
# Runtime updates use composite keys "plugin:tag"
|
|
475
|
+
rt_updates = self._get_plugin_updates(f'{plugin.name}:{rt.tag}')
|
|
476
|
+
|
|
465
477
|
provider = PluginProviderHeader(
|
|
466
478
|
plugin,
|
|
467
479
|
auto_val is not False,
|
|
468
480
|
show_controls=True,
|
|
469
|
-
has_updates=bool(
|
|
481
|
+
has_updates=bool(rt_updates),
|
|
470
482
|
parent=self._container,
|
|
471
483
|
)
|
|
472
484
|
provider.set_runtime(rt.tag, label=tag_text)
|
|
@@ -496,7 +508,7 @@ class ToolsView(QWidget):
|
|
|
496
508
|
plugin_name=plugin.name,
|
|
497
509
|
auto_update=pkg_auto,
|
|
498
510
|
show_toggle=True,
|
|
499
|
-
has_update=pkg.name in
|
|
511
|
+
has_update=pkg.name in rt_updates,
|
|
500
512
|
is_global=True,
|
|
501
513
|
host_tool=pkg.host_tool,
|
|
502
514
|
runtime_tag=rt.tag,
|
|
@@ -518,7 +530,7 @@ class ToolsView(QWidget):
|
|
|
518
530
|
``ProjectChildRow`` widgets are no longer used.
|
|
519
531
|
"""
|
|
520
532
|
auto_val = auto_update_map.get(plugin.name, True)
|
|
521
|
-
plugin_updates = self.
|
|
533
|
+
plugin_updates = self._get_plugin_updates(plugin.name)
|
|
522
534
|
|
|
523
535
|
provider = PluginProviderHeader(
|
|
524
536
|
plugin,
|
|
@@ -1121,11 +1133,15 @@ class ToolsView(QWidget):
|
|
|
1121
1133
|
asyncio.create_task(self._run_inline_update_check())
|
|
1122
1134
|
|
|
1123
1135
|
async def _run_inline_update_check(self) -> None:
|
|
1124
|
-
"""Check for updates with inline spinners (no overlay / rebuild).
|
|
1136
|
+
"""Check for updates with inline spinners (no overlay / rebuild).
|
|
1137
|
+
|
|
1138
|
+
Used by both the manual *Check for Updates* button and the
|
|
1139
|
+
automatic deferred check after initial refresh.
|
|
1140
|
+
"""
|
|
1125
1141
|
self._check_in_progress = True
|
|
1126
1142
|
try:
|
|
1127
|
-
|
|
1128
|
-
self.
|
|
1143
|
+
available = await self._check_for_updates(self._directories)
|
|
1144
|
+
self._store_check_results(available)
|
|
1129
1145
|
self._apply_update_badges()
|
|
1130
1146
|
except Exception:
|
|
1131
1147
|
logger.debug('Inline update check failed', exc_info=True)
|
|
@@ -1225,7 +1241,6 @@ class ToolsView(QWidget):
|
|
|
1225
1241
|
params = SetupParameters(
|
|
1226
1242
|
paths=[str(manifest_path)],
|
|
1227
1243
|
dry_run=True,
|
|
1228
|
-
detect_updates=True,
|
|
1229
1244
|
project_directory=path,
|
|
1230
1245
|
)
|
|
1231
1246
|
async for event in self._porringer.sync.execute_stream(params):
|
|
@@ -1247,31 +1262,42 @@ class ToolsView(QWidget):
|
|
|
1247
1262
|
)
|
|
1248
1263
|
return available
|
|
1249
1264
|
|
|
1250
|
-
async def _deferred_update_check(
|
|
1251
|
-
self,
|
|
1252
|
-
directories: list[ManifestDirectory],
|
|
1253
|
-
) -> None:
|
|
1265
|
+
async def _deferred_update_check(self) -> None:
|
|
1254
1266
|
"""Run update detection in the background, then patch the widget tree.
|
|
1255
1267
|
|
|
1256
1268
|
Called after the initial render so the user sees the tool list
|
|
1257
1269
|
immediately while update badges are populated asynchronously.
|
|
1258
|
-
|
|
1270
|
+
Delegates to :meth:`_run_inline_update_check`.
|
|
1259
1271
|
"""
|
|
1260
|
-
self._check_in_progress = True
|
|
1261
1272
|
self._check_btn.setEnabled(False)
|
|
1262
1273
|
self._check_btn.setText('Checking\u2026')
|
|
1263
1274
|
self._set_all_checking(True)
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1275
|
+
await self._run_inline_update_check()
|
|
1276
|
+
|
|
1277
|
+
def _get_plugin_updates(self, signal_key: str) -> dict[str, str]:
|
|
1278
|
+
"""Return ``{package_name: latest_version}`` for *signal_key*.
|
|
1279
|
+
|
|
1280
|
+
Reads from the shared :class:`PackageStateStore` when available,
|
|
1281
|
+
otherwise returns an empty dict.
|
|
1282
|
+
"""
|
|
1283
|
+
if self._package_store is not None:
|
|
1284
|
+
return self._package_store.get_updates(signal_key)
|
|
1285
|
+
return {}
|
|
1286
|
+
|
|
1287
|
+
def _store_check_results(self, available: dict[str, dict[str, str]]) -> None:
|
|
1288
|
+
"""Push check results into the store."""
|
|
1289
|
+
if self._package_store is not None:
|
|
1290
|
+
self._package_store.set_check_results(available)
|
|
1291
|
+
|
|
1292
|
+
@property
|
|
1293
|
+
def _has_update_data(self) -> bool:
|
|
1294
|
+
"""Return whether update data has been fetched at least once."""
|
|
1295
|
+
return self._package_store is not None and self._package_store.has_data
|
|
1296
|
+
|
|
1297
|
+
def _on_package_state_changed(self) -> None:
|
|
1298
|
+
"""Re-apply badges when another view writes to the shared store."""
|
|
1299
|
+
if not self._check_in_progress and not self._refresh_in_progress:
|
|
1267
1300
|
self._apply_update_badges()
|
|
1268
|
-
except Exception:
|
|
1269
|
-
logger.debug('Deferred update check failed', exc_info=True)
|
|
1270
|
-
finally:
|
|
1271
|
-
self._set_all_checking(False)
|
|
1272
|
-
self._check_btn.setEnabled(True)
|
|
1273
|
-
self._check_btn.setText('Check for Updates')
|
|
1274
|
-
self._check_in_progress = False
|
|
1275
1301
|
|
|
1276
1302
|
def _apply_update_badges(self) -> None:
|
|
1277
1303
|
"""Walk existing widgets and show/hide Update buttons + set inline status."""
|
|
@@ -1279,12 +1305,12 @@ class ToolsView(QWidget):
|
|
|
1279
1305
|
for widget in self._section_widgets:
|
|
1280
1306
|
if isinstance(widget, PluginProviderHeader):
|
|
1281
1307
|
current_plugin = widget._signal_key
|
|
1282
|
-
plugin_updates = self.
|
|
1308
|
+
plugin_updates = self._get_plugin_updates(current_plugin)
|
|
1283
1309
|
has = bool(plugin_updates)
|
|
1284
1310
|
if widget._update_btn is not None:
|
|
1285
1311
|
widget._update_btn.setVisible(has)
|
|
1286
1312
|
elif isinstance(widget, PluginRow) and widget._plugin_name:
|
|
1287
|
-
plugin_updates = self.
|
|
1313
|
+
plugin_updates = self._get_plugin_updates(widget._signal_key)
|
|
1288
1314
|
latest_version = plugin_updates.get(widget._package_name)
|
|
1289
1315
|
has_update = latest_version is not None
|
|
1290
1316
|
|
|
@@ -1295,7 +1321,7 @@ class ToolsView(QWidget):
|
|
|
1295
1321
|
if has_update:
|
|
1296
1322
|
version_text = f'v{latest_version} available' if latest_version else 'Update available'
|
|
1297
1323
|
widget.set_update_status(version_text, PLUGIN_ROW_STATUS_AVAILABLE_STYLE)
|
|
1298
|
-
elif self.
|
|
1324
|
+
elif self._has_update_data:
|
|
1299
1325
|
widget.set_update_status('Up to date', PLUGIN_ROW_STATUS_UP_TO_DATE_STYLE)
|
|
1300
1326
|
|
|
1301
1327
|
def _set_all_checking(self, checking: bool) -> None:
|
|
@@ -1401,6 +1427,7 @@ class MainWindow(QMainWindow):
|
|
|
1401
1427
|
self._porringer = porringer
|
|
1402
1428
|
self._store = store
|
|
1403
1429
|
self._coordinator: DataCoordinator | None = DataCoordinator(porringer) if porringer is not None else None
|
|
1430
|
+
self._package_store: PackageStateStore | None = PackageStateStore(self) if porringer is not None else None
|
|
1404
1431
|
self.setWindowTitle('Synodic Client')
|
|
1405
1432
|
self.setMinimumSize(*MAIN_WINDOW_MIN_SIZE)
|
|
1406
1433
|
self.setWindowIcon(app_icon())
|
|
@@ -1453,6 +1480,7 @@ class MainWindow(QMainWindow):
|
|
|
1453
1480
|
self._store,
|
|
1454
1481
|
self,
|
|
1455
1482
|
coordinator=self._coordinator,
|
|
1483
|
+
package_store=self._package_store,
|
|
1456
1484
|
)
|
|
1457
1485
|
self._tabs.addTab(self._projects_view, 'Projects')
|
|
1458
1486
|
|
|
@@ -1461,6 +1489,7 @@ class MainWindow(QMainWindow):
|
|
|
1461
1489
|
self._store,
|
|
1462
1490
|
self,
|
|
1463
1491
|
coordinator=self._coordinator,
|
|
1492
|
+
package_store=self._package_store,
|
|
1464
1493
|
)
|
|
1465
1494
|
self._tabs.addTab(self._tools_view, 'Tools')
|
|
1466
1495
|
self.tools_view_created.emit(self._tools_view)
|