synodic-client 0.0.1.dev40__tar.gz → 0.0.1.dev42__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.dev40 → synodic_client-0.0.1.dev42}/PKG-INFO +1 -1
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/pyproject.toml +1 -1
- synodic_client-0.0.1.dev42/synodic_client/_version.py +1 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/synodic_client/application/screen/screen.py +59 -61
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/synodic_client/application/screen/settings.py +20 -3
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/synodic_client/application/screen/tray.py +13 -186
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/synodic_client/application/theme.py +15 -0
- synodic_client-0.0.1.dev42/synodic_client/application/update_controller.py +312 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/synodic_client/config.py +4 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/synodic_client/resolution.py +3 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/tests/unit/qt/test_gather_packages.py +25 -44
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/tests/unit/qt/test_settings.py +5 -3
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/tests/unit/qt/test_tray_window_show.py +1 -0
- synodic_client-0.0.1.dev42/tests/unit/qt/test_update_controller.py +298 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/tests/unit/test_config.py +1 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/tests/unit/test_resolution.py +1 -0
- synodic_client-0.0.1.dev40/synodic_client/_version.py +0 -1
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/LICENSE.md +0 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/README.md +0 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/synodic_client/__init__.py +0 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/synodic_client/__main__.py +0 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/synodic_client/application/__init__.py +0 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/synodic_client/application/bootstrap.py +0 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/synodic_client/application/data.py +0 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/synodic_client/application/icon.py +0 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/synodic_client/application/instance.py +0 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/synodic_client/application/qt.py +0 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/synodic_client/application/screen/__init__.py +0 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/synodic_client/application/screen/action_card.py +0 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/synodic_client/application/screen/card.py +0 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/synodic_client/application/screen/install.py +0 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/synodic_client/application/screen/log_panel.py +0 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/synodic_client/application/screen/sidebar.py +0 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/synodic_client/application/screen/spinner.py +0 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/synodic_client/application/screen/update_banner.py +0 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/synodic_client/application/uri.py +0 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/synodic_client/application/workers.py +0 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/synodic_client/cli.py +0 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/synodic_client/client.py +0 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/synodic_client/logging.py +0 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/synodic_client/protocol.py +0 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/synodic_client/py.typed +0 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/synodic_client/startup.py +0 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/synodic_client/updater.py +0 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/tests/__init__.py +0 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/tests/conftest.py +0 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/tests/unit/__init__.py +0 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/tests/unit/qt/__init__.py +0 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/tests/unit/qt/conftest.py +0 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/tests/unit/qt/test_action_card.py +0 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/tests/unit/qt/test_install_preview.py +0 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/tests/unit/qt/test_log_panel.py +0 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/tests/unit/qt/test_logging.py +0 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/tests/unit/qt/test_preview_model.py +0 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/tests/unit/qt/test_sidebar.py +0 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/tests/unit/qt/test_update_banner.py +0 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/tests/unit/qt/test_update_feedback.py +0 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/tests/unit/test_cli.py +0 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/tests/unit/test_client_updater.py +0 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/tests/unit/test_client_version.py +0 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/tests/unit/test_examples.py +0 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/tests/unit/test_install.py +0 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/tests/unit/test_updater.py +0 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/tests/unit/test_uri.py +0 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/tests/unit/test_workers.py +0 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/tests/unit/windows/__init__.py +0 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/tests/unit/windows/conftest.py +0 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/tests/unit/windows/test_protocol.py +0 -0
- {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/tests/unit/windows/test_startup.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '0.0.1.dev42'
|
|
@@ -8,6 +8,7 @@ from pathlib import Path
|
|
|
8
8
|
|
|
9
9
|
from porringer.api import API
|
|
10
10
|
from porringer.backend.builder import Builder
|
|
11
|
+
from porringer.backend.command.core.discovery import DiscoveredPlugins
|
|
11
12
|
from porringer.core.plugin_schema.plugin_manager import PluginManager
|
|
12
13
|
from porringer.core.plugin_schema.project_environment import ProjectEnvironment
|
|
13
14
|
from porringer.schema import (
|
|
@@ -1158,26 +1159,29 @@ class ToolsView(QWidget):
|
|
|
1158
1159
|
return None
|
|
1159
1160
|
return {name for name, chip in self._filter_chips.items() if chip.isChecked()}
|
|
1160
1161
|
|
|
1161
|
-
|
|
1162
|
-
|
|
1162
|
+
@staticmethod
|
|
1163
|
+
def _is_plugin_active(plugin_name: str, active: set[str] | None) -> bool:
|
|
1164
|
+
"""Return whether *plugin_name* passes the chip filter."""
|
|
1165
|
+
return active is None or plugin_name in active
|
|
1163
1166
|
|
|
1164
|
-
|
|
1165
|
-
|
|
1167
|
+
@staticmethod
|
|
1168
|
+
def _finalise_provider(
|
|
1169
|
+
provider: PluginProviderHeader | None,
|
|
1170
|
+
has_visible: bool,
|
|
1171
|
+
) -> bool:
|
|
1172
|
+
"""Set provider visibility and return whether it had visible children."""
|
|
1173
|
+
if provider is not None:
|
|
1174
|
+
provider.setVisible(has_visible)
|
|
1175
|
+
return has_visible
|
|
1166
1176
|
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
search text.
|
|
1170
|
-
* **PluginRow** — visible when its plugin is active **and** its
|
|
1171
|
-
package name or plugin name contains the search text.
|
|
1172
|
-
* **ProjectChildRow** — follows its parent :class:`PluginRow`.
|
|
1173
|
-
* **PluginKindHeader** — visible when at least one child
|
|
1174
|
-
provider in its kind group is visible.
|
|
1177
|
+
def _apply_filter(self, _text: str | None = None) -> None:
|
|
1178
|
+
"""Show/hide section widgets based on search text and active chips.
|
|
1175
1179
|
|
|
1176
|
-
|
|
1180
|
+
Delegates to :meth:`_is_plugin_active` for chip matching and
|
|
1181
|
+
:meth:`_finalise_provider` for provider visibility bookkeeping.
|
|
1177
1182
|
"""
|
|
1178
1183
|
query = self._search_input.text().strip().lower()
|
|
1179
1184
|
active = self._active_chip_plugins()
|
|
1180
|
-
all_active = active is None # None → no chips yet, show all
|
|
1181
1185
|
|
|
1182
1186
|
current_kind_header: PluginKindHeader | None = None
|
|
1183
1187
|
kind_has_visible = False
|
|
@@ -1187,13 +1191,8 @@ class ToolsView(QWidget):
|
|
|
1187
1191
|
|
|
1188
1192
|
for widget in self._section_widgets:
|
|
1189
1193
|
if isinstance(widget, PluginKindHeader):
|
|
1190
|
-
|
|
1194
|
+
kind_has_visible |= self._finalise_provider(current_provider, provider_has_visible_child)
|
|
1191
1195
|
if current_kind_header is not None:
|
|
1192
|
-
# Finalise last provider of previous kind
|
|
1193
|
-
if current_provider is not None:
|
|
1194
|
-
current_provider.setVisible(provider_has_visible_child)
|
|
1195
|
-
if provider_has_visible_child:
|
|
1196
|
-
kind_has_visible = True
|
|
1197
1196
|
current_kind_header.setVisible(kind_has_visible)
|
|
1198
1197
|
|
|
1199
1198
|
current_kind_header = widget
|
|
@@ -1202,31 +1201,20 @@ class ToolsView(QWidget):
|
|
|
1202
1201
|
provider_has_visible_child = False
|
|
1203
1202
|
|
|
1204
1203
|
elif isinstance(widget, PluginProviderHeader):
|
|
1205
|
-
|
|
1206
|
-
if current_provider is not None:
|
|
1207
|
-
current_provider.setVisible(provider_has_visible_child)
|
|
1208
|
-
if provider_has_visible_child:
|
|
1209
|
-
kind_has_visible = True
|
|
1204
|
+
kind_has_visible |= self._finalise_provider(current_provider, provider_has_visible_child)
|
|
1210
1205
|
|
|
1211
1206
|
current_provider = widget
|
|
1212
1207
|
provider_has_visible_child = False
|
|
1213
|
-
|
|
1214
|
-
plugin_active = all_active or (active is not None and plugin_name in active)
|
|
1215
|
-
|
|
1216
|
-
if not plugin_active:
|
|
1217
|
-
# Entire provider hidden
|
|
1208
|
+
if not self._is_plugin_active(widget._plugin_name, active):
|
|
1218
1209
|
widget.setVisible(False)
|
|
1219
|
-
provider_has_visible_child = False
|
|
1220
1210
|
|
|
1221
1211
|
elif isinstance(widget, PluginRow):
|
|
1222
|
-
|
|
1223
|
-
plugin_active = all_active or (active is not None and plugin_name in active)
|
|
1224
|
-
if not plugin_active:
|
|
1212
|
+
if not self._is_plugin_active(widget._plugin_name, active):
|
|
1225
1213
|
widget.setVisible(False)
|
|
1226
1214
|
parent_row_visible = False
|
|
1227
1215
|
continue
|
|
1228
1216
|
|
|
1229
|
-
name_match = not query or query in widget._package_name.lower() or query in
|
|
1217
|
+
name_match = not query or query in widget._package_name.lower() or query in widget._plugin_name.lower()
|
|
1230
1218
|
widget.setVisible(name_match)
|
|
1231
1219
|
parent_row_visible = name_match
|
|
1232
1220
|
if name_match:
|
|
@@ -1236,10 +1224,7 @@ class ToolsView(QWidget):
|
|
|
1236
1224
|
widget.setVisible(parent_row_visible)
|
|
1237
1225
|
|
|
1238
1226
|
# Finalise last provider and kind
|
|
1239
|
-
|
|
1240
|
-
current_provider.setVisible(provider_has_visible_child)
|
|
1241
|
-
if provider_has_visible_child:
|
|
1242
|
-
kind_has_visible = True
|
|
1227
|
+
kind_has_visible |= self._finalise_provider(current_provider, provider_has_visible_child)
|
|
1243
1228
|
if current_kind_header is not None:
|
|
1244
1229
|
current_kind_header.setVisible(kind_has_visible)
|
|
1245
1230
|
|
|
@@ -1834,6 +1819,7 @@ class ProjectsView(QWidget):
|
|
|
1834
1819
|
if self._coordinator is not None:
|
|
1835
1820
|
snapshot = await self._coordinator.refresh()
|
|
1836
1821
|
results = snapshot.validated_directories
|
|
1822
|
+
discovered = snapshot.discovered
|
|
1837
1823
|
else:
|
|
1838
1824
|
loop = asyncio.get_running_loop()
|
|
1839
1825
|
results = await loop.run_in_executor(
|
|
@@ -1843,6 +1829,7 @@ class ProjectsView(QWidget):
|
|
|
1843
1829
|
check_manifest=True,
|
|
1844
1830
|
),
|
|
1845
1831
|
)
|
|
1832
|
+
discovered = None
|
|
1846
1833
|
|
|
1847
1834
|
directories: list[tuple[Path, str, bool]] = []
|
|
1848
1835
|
current_paths: set[Path] = set()
|
|
@@ -1854,32 +1841,12 @@ class ProjectsView(QWidget):
|
|
|
1854
1841
|
current_paths.add(path)
|
|
1855
1842
|
|
|
1856
1843
|
# Remove widgets for directories no longer in cache
|
|
1857
|
-
|
|
1858
|
-
if path not in current_paths:
|
|
1859
|
-
widget = self._widgets.pop(path)
|
|
1860
|
-
self._stack.removeWidget(widget)
|
|
1861
|
-
widget.reset()
|
|
1862
|
-
widget.deleteLater()
|
|
1844
|
+
self._remove_stale_widgets(current_paths)
|
|
1863
1845
|
|
|
1864
1846
|
# Grab pre-discovered plugins so each widget can skip redundant discovery
|
|
1865
|
-
discovered = snapshot.discovered if self._coordinator is not None else None
|
|
1866
1847
|
|
|
1867
1848
|
# Create new widgets for new directories
|
|
1868
|
-
|
|
1869
|
-
if path not in self._widgets and valid:
|
|
1870
|
-
widget = SetupPreviewWidget(
|
|
1871
|
-
self._porringer,
|
|
1872
|
-
self,
|
|
1873
|
-
show_close=False,
|
|
1874
|
-
config=self._config,
|
|
1875
|
-
)
|
|
1876
|
-
widget._discovered_plugins = discovered
|
|
1877
|
-
widget.install_finished.connect(self._on_install_finished)
|
|
1878
|
-
widget.phase_changed.connect(
|
|
1879
|
-
lambda phase, p=path: self._on_widget_phase_changed(p, phase),
|
|
1880
|
-
)
|
|
1881
|
-
self._widgets[path] = widget
|
|
1882
|
-
self._stack.addWidget(widget)
|
|
1849
|
+
self._create_directory_widgets(directories, discovered)
|
|
1883
1850
|
|
|
1884
1851
|
# Rebuild sidebar
|
|
1885
1852
|
self._sidebar.set_directories(directories)
|
|
@@ -1909,6 +1876,37 @@ class ProjectsView(QWidget):
|
|
|
1909
1876
|
|
|
1910
1877
|
# --- Event handlers ---
|
|
1911
1878
|
|
|
1879
|
+
def _remove_stale_widgets(self, current_paths: set[Path]) -> None:
|
|
1880
|
+
"""Remove stacked widgets for directories no longer in the cache."""
|
|
1881
|
+
for path in list(self._widgets):
|
|
1882
|
+
if path not in current_paths:
|
|
1883
|
+
widget = self._widgets.pop(path)
|
|
1884
|
+
self._stack.removeWidget(widget)
|
|
1885
|
+
widget.reset()
|
|
1886
|
+
widget.deleteLater()
|
|
1887
|
+
|
|
1888
|
+
def _create_directory_widgets(
|
|
1889
|
+
self,
|
|
1890
|
+
directories: list[tuple[Path, str, bool]],
|
|
1891
|
+
discovered: DiscoveredPlugins | None,
|
|
1892
|
+
) -> None:
|
|
1893
|
+
"""Create :class:`SetupPreviewWidget` instances for new valid directories."""
|
|
1894
|
+
for path, _name, valid in directories:
|
|
1895
|
+
if path not in self._widgets and valid:
|
|
1896
|
+
widget = SetupPreviewWidget(
|
|
1897
|
+
self._porringer,
|
|
1898
|
+
self,
|
|
1899
|
+
show_close=False,
|
|
1900
|
+
config=self._config,
|
|
1901
|
+
)
|
|
1902
|
+
widget._discovered_plugins = discovered
|
|
1903
|
+
widget.install_finished.connect(self._on_install_finished)
|
|
1904
|
+
widget.phase_changed.connect(
|
|
1905
|
+
lambda phase, p=path: self._on_widget_phase_changed(p, phase),
|
|
1906
|
+
)
|
|
1907
|
+
self._widgets[path] = widget
|
|
1908
|
+
self._stack.addWidget(widget)
|
|
1909
|
+
|
|
1912
1910
|
def _on_selection_changed(self, path: Path) -> None:
|
|
1913
1911
|
"""Handle sidebar selection — switch the stacked widget."""
|
|
1914
1912
|
widget = self._widgets.get(path)
|
|
@@ -29,7 +29,7 @@ from PySide6.QtWidgets import (
|
|
|
29
29
|
|
|
30
30
|
from synodic_client.application.icon import app_icon
|
|
31
31
|
from synodic_client.application.screen.card import CardFrame
|
|
32
|
-
from synodic_client.application.theme import SETTINGS_WINDOW_MIN_SIZE
|
|
32
|
+
from synodic_client.application.theme import SETTINGS_WINDOW_MIN_SIZE, UPDATE_STATUS_CHECKING_STYLE
|
|
33
33
|
from synodic_client.logging import log_path
|
|
34
34
|
from synodic_client.resolution import ResolvedConfig, update_user_config
|
|
35
35
|
from synodic_client.startup import is_startup_registered, register_startup, remove_startup
|
|
@@ -163,6 +163,11 @@ class SettingsWindow(QMainWindow):
|
|
|
163
163
|
self._detect_updates_check.toggled.connect(self._on_detect_updates_changed)
|
|
164
164
|
content.addWidget(self._detect_updates_check)
|
|
165
165
|
|
|
166
|
+
# Automatically apply updates
|
|
167
|
+
self._auto_apply_check = QCheckBox('Automatically apply updates')
|
|
168
|
+
self._auto_apply_check.toggled.connect(self._on_auto_apply_changed)
|
|
169
|
+
content.addWidget(self._auto_apply_check)
|
|
170
|
+
|
|
166
171
|
# Check for Updates
|
|
167
172
|
row = QHBoxLayout()
|
|
168
173
|
self._check_updates_btn = QPushButton('Check for Updates\u2026')
|
|
@@ -218,16 +223,24 @@ class SettingsWindow(QMainWindow):
|
|
|
218
223
|
|
|
219
224
|
# Checkboxes
|
|
220
225
|
self._detect_updates_check.setChecked(config.detect_updates)
|
|
226
|
+
self._auto_apply_check.setChecked(config.auto_apply)
|
|
221
227
|
self._auto_start_check.setChecked(is_startup_registered())
|
|
222
228
|
|
|
223
|
-
def set_update_status(self, text: str) -> None:
|
|
224
|
-
"""Set the inline status text next to the *Check for Updates* button.
|
|
229
|
+
def set_update_status(self, text: str, style: str = '') -> None:
|
|
230
|
+
"""Set the inline status text next to the *Check for Updates* button.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
text: The status message.
|
|
234
|
+
style: Optional stylesheet for the label (e.g. color).
|
|
235
|
+
"""
|
|
225
236
|
self._update_status_label.setText(text)
|
|
237
|
+
self._update_status_label.setStyleSheet(style)
|
|
226
238
|
|
|
227
239
|
def set_checking(self) -> None:
|
|
228
240
|
"""Enter the *checking* state — disable button and show status."""
|
|
229
241
|
self._check_updates_btn.setEnabled(False)
|
|
230
242
|
self._update_status_label.setText('Checking\u2026')
|
|
243
|
+
self._update_status_label.setStyleSheet(UPDATE_STATUS_CHECKING_STYLE)
|
|
231
244
|
|
|
232
245
|
def reset_check_updates_button(self) -> None:
|
|
233
246
|
"""Re-enable the *Check for Updates* button after a check completes."""
|
|
@@ -262,6 +275,7 @@ class SettingsWindow(QMainWindow):
|
|
|
262
275
|
self._auto_update_spin,
|
|
263
276
|
self._tool_update_spin,
|
|
264
277
|
self._detect_updates_check,
|
|
278
|
+
self._auto_apply_check,
|
|
265
279
|
self._auto_start_check,
|
|
266
280
|
self._check_updates_btn,
|
|
267
281
|
)
|
|
@@ -301,6 +315,9 @@ class SettingsWindow(QMainWindow):
|
|
|
301
315
|
def _on_detect_updates_changed(self, checked: bool) -> None:
|
|
302
316
|
self._persist(detect_updates=checked)
|
|
303
317
|
|
|
318
|
+
def _on_auto_apply_changed(self, checked: bool) -> None:
|
|
319
|
+
self._persist(auto_apply=checked)
|
|
320
|
+
|
|
304
321
|
def _on_auto_start_changed(self, checked: bool) -> None:
|
|
305
322
|
self._config = update_user_config(auto_start=checked)
|
|
306
323
|
if checked:
|
{synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/synodic_client/application/screen/tray.py
RENAMED
|
@@ -17,10 +17,9 @@ from PySide6.QtWidgets import (
|
|
|
17
17
|
from synodic_client.application.icon import app_icon
|
|
18
18
|
from synodic_client.application.screen.screen import MainWindow, ToolsView
|
|
19
19
|
from synodic_client.application.screen.settings import SettingsWindow
|
|
20
|
+
from synodic_client.application.update_controller import UpdateController
|
|
20
21
|
from synodic_client.application.workers import (
|
|
21
22
|
ToolUpdateResult,
|
|
22
|
-
check_for_update,
|
|
23
|
-
download_update,
|
|
24
23
|
run_package_remove,
|
|
25
24
|
run_tool_updates,
|
|
26
25
|
)
|
|
@@ -31,7 +30,6 @@ from synodic_client.resolution import (
|
|
|
31
30
|
resolve_config,
|
|
32
31
|
resolve_update_config,
|
|
33
32
|
)
|
|
34
|
-
from synodic_client.updater import UpdateInfo
|
|
35
33
|
|
|
36
34
|
logger = logging.getLogger(__name__)
|
|
37
35
|
|
|
@@ -59,7 +57,6 @@ class TrayScreen:
|
|
|
59
57
|
self._client = client
|
|
60
58
|
self._window = window
|
|
61
59
|
self._config = config
|
|
62
|
-
self._update_task: asyncio.Task[None] | None = None
|
|
63
60
|
self._tool_task: asyncio.Task[None] | None = None
|
|
64
61
|
|
|
65
62
|
self.tray_icon = app_icon()
|
|
@@ -74,14 +71,19 @@ class TrayScreen:
|
|
|
74
71
|
# Settings window (created once, shown/hidden on demand)
|
|
75
72
|
self._settings_window = SettingsWindow(self._resolve_config())
|
|
76
73
|
self._settings_window.settings_changed.connect(self._on_settings_changed)
|
|
77
|
-
self._settings_window.check_updates_requested.connect(self._on_check_updates)
|
|
78
74
|
|
|
79
75
|
# MainWindow gear button → open settings
|
|
80
76
|
window.settings_requested.connect(self._show_settings)
|
|
81
77
|
|
|
82
|
-
#
|
|
83
|
-
self.
|
|
84
|
-
self.
|
|
78
|
+
# Update controller — owns the self-update lifecycle & timer
|
|
79
|
+
self._banner = window.update_banner
|
|
80
|
+
self._update_controller = UpdateController(
|
|
81
|
+
app,
|
|
82
|
+
client,
|
|
83
|
+
self._banner,
|
|
84
|
+
self._settings_window,
|
|
85
|
+
config,
|
|
86
|
+
)
|
|
85
87
|
|
|
86
88
|
# Periodic tool update checking
|
|
87
89
|
self._tool_update_timer: QTimer | None = None
|
|
@@ -90,11 +92,6 @@ class TrayScreen:
|
|
|
90
92
|
# Connect ToolsView signals — deferred because ToolsView is created lazily
|
|
91
93
|
window.tools_view_created.connect(self._connect_tools_view)
|
|
92
94
|
|
|
93
|
-
# Connect update banner signals
|
|
94
|
-
self._banner = window.update_banner
|
|
95
|
-
self._banner.restart_requested.connect(self._apply_update)
|
|
96
|
-
self._banner.retry_requested.connect(lambda: self._do_check_updates(silent=True))
|
|
97
|
-
|
|
98
95
|
def _build_menu(self, app: QApplication, window: MainWindow) -> None:
|
|
99
96
|
"""Build the tray context menu."""
|
|
100
97
|
self.menu = QMenu()
|
|
@@ -105,12 +102,6 @@ class TrayScreen:
|
|
|
105
102
|
|
|
106
103
|
self.menu.addSeparator()
|
|
107
104
|
|
|
108
|
-
self.update_action = QAction('Check for Updates...', self.menu)
|
|
109
|
-
self.update_action.triggered.connect(self._on_check_updates)
|
|
110
|
-
self.menu.addAction(self.update_action)
|
|
111
|
-
|
|
112
|
-
self.menu.addSeparator()
|
|
113
|
-
|
|
114
105
|
self.settings_action = QAction('Settings\u2026', self.menu)
|
|
115
106
|
self.settings_action.triggered.connect(self._show_settings)
|
|
116
107
|
self.menu.addAction(self.settings_action)
|
|
@@ -172,16 +163,6 @@ class TrayScreen:
|
|
|
172
163
|
logger.info('%s enabled (every %d minute(s))', label, interval_minutes)
|
|
173
164
|
return timer
|
|
174
165
|
|
|
175
|
-
def _restart_auto_update_timer(self) -> None:
|
|
176
|
-
"""Start (or restart) the periodic auto-update timer from config."""
|
|
177
|
-
config = resolve_update_config(self._resolve_config())
|
|
178
|
-
self._auto_update_timer = self._restart_timer(
|
|
179
|
-
self._auto_update_timer,
|
|
180
|
-
config.auto_update_interval_minutes,
|
|
181
|
-
self._on_auto_check_updates,
|
|
182
|
-
'Automatic update checking',
|
|
183
|
-
)
|
|
184
|
-
|
|
185
166
|
def _restart_tool_update_timer(self) -> None:
|
|
186
167
|
"""Start (or restart) the periodic tool update timer from config."""
|
|
187
168
|
config = resolve_update_config(self._resolve_config())
|
|
@@ -206,116 +187,10 @@ class TrayScreen:
|
|
|
206
187
|
def _on_settings_changed(self, config: ResolvedConfig) -> None:
|
|
207
188
|
"""React to a change made in the settings window."""
|
|
208
189
|
self._config = config
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
"""Re-derive update settings and restart the updater and timers.
|
|
213
|
-
|
|
214
|
-
The new ``Updater`` starts with the ``importlib.metadata``
|
|
215
|
-
version which may be stale after a Velopack update. The
|
|
216
|
-
authoritative Velopack version is recovered automatically on
|
|
217
|
-
the first ``_get_velopack_manager()`` call (i.e. the next
|
|
218
|
-
update check), so no special handling is required here.
|
|
219
|
-
"""
|
|
220
|
-
update_cfg = resolve_update_config(config)
|
|
221
|
-
self._client.initialize_updater(update_cfg)
|
|
222
|
-
self._restart_auto_update_timer()
|
|
190
|
+
# Delegate updater reinit + immediate check to the controller
|
|
191
|
+
self._update_controller.on_settings_changed(config)
|
|
192
|
+
# Restart tool-update timer with new config
|
|
223
193
|
self._restart_tool_update_timer()
|
|
224
|
-
logger.info('Updater re-initialized (channel: %s, source: %s)', update_cfg.channel.name, update_cfg.repo_url)
|
|
225
|
-
|
|
226
|
-
def _reset_update_action(self) -> None:
|
|
227
|
-
"""Restore the 'Check for Updates' action to its idle state."""
|
|
228
|
-
self.update_action.setEnabled(True)
|
|
229
|
-
self.update_action.setText('Check for Updates...')
|
|
230
|
-
|
|
231
|
-
def _on_check_updates(self) -> None:
|
|
232
|
-
"""Handle manual check for updates action."""
|
|
233
|
-
self._do_check_updates(silent=False)
|
|
234
|
-
|
|
235
|
-
def _on_auto_check_updates(self) -> None:
|
|
236
|
-
"""Handle automatic (periodic) check for updates.
|
|
237
|
-
|
|
238
|
-
Failures and no-update results are logged silently without
|
|
239
|
-
showing the in-app error banner.
|
|
240
|
-
"""
|
|
241
|
-
self._do_check_updates(silent=True)
|
|
242
|
-
|
|
243
|
-
def _do_check_updates(self, *, silent: bool) -> None:
|
|
244
|
-
"""Run an update check.
|
|
245
|
-
|
|
246
|
-
Args:
|
|
247
|
-
silent: When ``True``, suppress the in-app error banner
|
|
248
|
-
for failures and no-update results. The banner is
|
|
249
|
-
always shown when an update *is* available.
|
|
250
|
-
"""
|
|
251
|
-
if self._client.updater is None:
|
|
252
|
-
if not silent:
|
|
253
|
-
self._banner.show_error('Updater is not initialized.')
|
|
254
|
-
return
|
|
255
|
-
|
|
256
|
-
# Disable both the tray action and the settings button while checking
|
|
257
|
-
self.update_action.setEnabled(False)
|
|
258
|
-
self.update_action.setText('Checking for Updates...')
|
|
259
|
-
self._settings_window.set_checking()
|
|
260
|
-
|
|
261
|
-
self._update_task = asyncio.create_task(self._async_check_updates(silent=silent))
|
|
262
|
-
|
|
263
|
-
async def _async_check_updates(self, *, silent: bool) -> None:
|
|
264
|
-
"""Run the update check coroutine and route results."""
|
|
265
|
-
try:
|
|
266
|
-
result = await check_for_update(self._client)
|
|
267
|
-
self._on_update_check_finished(result, silent=silent)
|
|
268
|
-
except Exception as exc:
|
|
269
|
-
logger.exception('Update check failed')
|
|
270
|
-
self._on_update_check_error(str(exc), silent=silent)
|
|
271
|
-
|
|
272
|
-
def _on_update_check_finished(self, result: UpdateInfo | None, *, silent: bool = False) -> None:
|
|
273
|
-
"""Handle update check completion."""
|
|
274
|
-
self._reset_update_action()
|
|
275
|
-
self._settings_window.reset_check_updates_button()
|
|
276
|
-
|
|
277
|
-
if result is None:
|
|
278
|
-
self._settings_window.set_update_status('Check failed')
|
|
279
|
-
if not silent:
|
|
280
|
-
self._banner.show_error('Failed to check for updates.')
|
|
281
|
-
else:
|
|
282
|
-
logger.warning('Automatic update check failed (no result)')
|
|
283
|
-
return
|
|
284
|
-
|
|
285
|
-
if result.error:
|
|
286
|
-
self._settings_window.set_update_status(result.error)
|
|
287
|
-
if not silent:
|
|
288
|
-
self._banner.show_error(result.error)
|
|
289
|
-
else:
|
|
290
|
-
logger.warning('Automatic update check failed: %s', result.error)
|
|
291
|
-
return
|
|
292
|
-
|
|
293
|
-
if not result.available:
|
|
294
|
-
self._settings_window.set_update_status(
|
|
295
|
-
f'Up to date ({result.current_version})',
|
|
296
|
-
)
|
|
297
|
-
if not silent:
|
|
298
|
-
logger.info('No updates available (current: %s)', result.current_version)
|
|
299
|
-
else:
|
|
300
|
-
logger.debug('Automatic update check: no update available')
|
|
301
|
-
return
|
|
302
|
-
|
|
303
|
-
# Update available — show banner and start download automatically
|
|
304
|
-
version = str(result.latest_version)
|
|
305
|
-
self._settings_window.set_update_status(f'Update available: {version}')
|
|
306
|
-
self._banner.show_downloading(version)
|
|
307
|
-
self._start_download(version)
|
|
308
|
-
|
|
309
|
-
def _on_update_check_error(self, error: str, *, silent: bool = False) -> None:
|
|
310
|
-
"""Handle update check error."""
|
|
311
|
-
self._reset_update_action()
|
|
312
|
-
self._settings_window.reset_check_updates_button()
|
|
313
|
-
self._settings_window.set_update_status(f'Error: {error}')
|
|
314
|
-
|
|
315
|
-
if not silent:
|
|
316
|
-
self._banner.show_error(f'Update check error: {error}')
|
|
317
|
-
else:
|
|
318
|
-
logger.warning('Automatic update check error: %s', error)
|
|
319
194
|
|
|
320
195
|
# -- Tool update helpers --
|
|
321
196
|
|
|
@@ -571,51 +446,3 @@ class TrayScreen:
|
|
|
571
446
|
tools_view.refresh()
|
|
572
447
|
|
|
573
448
|
self._window.show()
|
|
574
|
-
|
|
575
|
-
# -- Self-update download & apply --
|
|
576
|
-
|
|
577
|
-
def _start_download(self, version: str) -> None:
|
|
578
|
-
"""Start downloading the update in the background.
|
|
579
|
-
|
|
580
|
-
Args:
|
|
581
|
-
version: The version string being downloaded (for banner display).
|
|
582
|
-
"""
|
|
583
|
-
self._update_task = asyncio.create_task(self._async_download(version))
|
|
584
|
-
|
|
585
|
-
async def _async_download(self, version: str) -> None:
|
|
586
|
-
"""Run the download coroutine and route results."""
|
|
587
|
-
try:
|
|
588
|
-
success = await download_update(
|
|
589
|
-
self._client,
|
|
590
|
-
on_progress=self._banner.show_downloading_progress,
|
|
591
|
-
)
|
|
592
|
-
self._on_download_finished(success, version)
|
|
593
|
-
except Exception as exc:
|
|
594
|
-
logger.exception('Update download failed')
|
|
595
|
-
self._on_download_error(str(exc))
|
|
596
|
-
|
|
597
|
-
def _on_download_finished(self, success: bool, version: str) -> None:
|
|
598
|
-
"""Handle download completion — transition banner to ready state."""
|
|
599
|
-
if not success:
|
|
600
|
-
self._banner.show_error('Download failed. Please try again later.')
|
|
601
|
-
return
|
|
602
|
-
|
|
603
|
-
self._banner.show_ready(version)
|
|
604
|
-
self._settings_window.set_update_status(f'Ready to install: {version}')
|
|
605
|
-
|
|
606
|
-
def _on_download_error(self, error: str) -> None:
|
|
607
|
-
"""Handle download error — show error banner."""
|
|
608
|
-
self._banner.show_error(f'Download error: {error}')
|
|
609
|
-
|
|
610
|
-
def _apply_update(self) -> None:
|
|
611
|
-
"""Apply the downloaded update and restart."""
|
|
612
|
-
if self._client.updater is None:
|
|
613
|
-
return
|
|
614
|
-
|
|
615
|
-
try:
|
|
616
|
-
self._client.apply_update_on_exit(restart=True)
|
|
617
|
-
logger.info('Update scheduled — restarting application')
|
|
618
|
-
self._app.quit()
|
|
619
|
-
except Exception as e:
|
|
620
|
-
logger.error('Failed to apply update: %s', e)
|
|
621
|
-
self._banner.show_error(f'Failed to apply update: {e}')
|
{synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev42}/synodic_client/application/theme.py
RENAMED
|
@@ -436,6 +436,21 @@ SETTINGS_GEAR_STYLE = (
|
|
|
436
436
|
)
|
|
437
437
|
"""Gear button style for the MainWindow tab corner widget."""
|
|
438
438
|
|
|
439
|
+
# ---------------------------------------------------------------------------
|
|
440
|
+
# Settings inline update-status colours
|
|
441
|
+
# ---------------------------------------------------------------------------
|
|
442
|
+
UPDATE_STATUS_UP_TO_DATE_STYLE = 'color: #89d185; font-size: 12px;'
|
|
443
|
+
"""Green text for 'Up to date' / 'Ready' status."""
|
|
444
|
+
|
|
445
|
+
UPDATE_STATUS_AVAILABLE_STYLE = 'color: #cca700; font-size: 12px;'
|
|
446
|
+
"""Orange text for 'Update available' status."""
|
|
447
|
+
|
|
448
|
+
UPDATE_STATUS_ERROR_STYLE = 'color: #f48771; font-size: 12px;'
|
|
449
|
+
"""Red text for error / check-failed status."""
|
|
450
|
+
|
|
451
|
+
UPDATE_STATUS_CHECKING_STYLE = 'color: #808080; font-size: 12px; font-style: italic;'
|
|
452
|
+
"""Grey italic text for 'Checking…' status."""
|
|
453
|
+
|
|
439
454
|
# ---------------------------------------------------------------------------
|
|
440
455
|
# Update banner (in-app self-update notification)
|
|
441
456
|
# ---------------------------------------------------------------------------
|