synodic-client 0.0.1.dev41__tar.gz → 0.0.1.dev44__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.dev41 → synodic_client-0.0.1.dev44}/PKG-INFO +1 -1
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/pyproject.toml +1 -1
- synodic_client-0.0.1.dev44/synodic_client/_version.py +1 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/synodic_client/application/bootstrap.py +3 -13
- synodic_client-0.0.1.dev44/synodic_client/application/init.py +61 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/synodic_client/application/qt.py +4 -16
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/synodic_client/application/screen/settings.py +26 -3
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/synodic_client/application/screen/tray.py +13 -186
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/synodic_client/application/theme.py +15 -0
- synodic_client-0.0.1.dev44/synodic_client/application/update_controller.py +312 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/synodic_client/config.py +4 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/synodic_client/resolution.py +3 -0
- synodic_client-0.0.1.dev44/synodic_client/startup.py +156 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/synodic_client/updater.py +11 -3
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/tests/unit/qt/test_gather_packages.py +1 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/tests/unit/qt/test_settings.py +5 -3
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/tests/unit/qt/test_tray_window_show.py +1 -0
- synodic_client-0.0.1.dev44/tests/unit/qt/test_update_controller.py +298 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/tests/unit/test_config.py +1 -0
- synodic_client-0.0.1.dev44/tests/unit/test_init.py +103 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/tests/unit/test_resolution.py +1 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/tests/unit/test_updater.py +16 -0
- synodic_client-0.0.1.dev44/tests/unit/windows/test_startup.py +240 -0
- synodic_client-0.0.1.dev41/synodic_client/_version.py +0 -1
- synodic_client-0.0.1.dev41/synodic_client/startup.py +0 -91
- synodic_client-0.0.1.dev41/tests/unit/windows/test_startup.py +0 -121
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/LICENSE.md +0 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/README.md +0 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/synodic_client/__init__.py +0 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/synodic_client/__main__.py +0 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/synodic_client/application/__init__.py +0 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/synodic_client/application/data.py +0 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/synodic_client/application/icon.py +0 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/synodic_client/application/instance.py +0 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/synodic_client/application/screen/__init__.py +0 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/synodic_client/application/screen/action_card.py +0 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/synodic_client/application/screen/card.py +0 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/synodic_client/application/screen/install.py +0 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/synodic_client/application/screen/log_panel.py +0 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/synodic_client/application/screen/screen.py +0 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/synodic_client/application/screen/sidebar.py +0 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/synodic_client/application/screen/spinner.py +0 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/synodic_client/application/screen/update_banner.py +0 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/synodic_client/application/uri.py +0 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/synodic_client/application/workers.py +0 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/synodic_client/cli.py +0 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/synodic_client/client.py +0 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/synodic_client/logging.py +0 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/synodic_client/protocol.py +0 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/synodic_client/py.typed +0 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/tests/__init__.py +0 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/tests/conftest.py +0 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/tests/unit/__init__.py +0 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/tests/unit/qt/__init__.py +0 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/tests/unit/qt/conftest.py +0 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/tests/unit/qt/test_action_card.py +0 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/tests/unit/qt/test_install_preview.py +0 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/tests/unit/qt/test_log_panel.py +0 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/tests/unit/qt/test_logging.py +0 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/tests/unit/qt/test_preview_model.py +0 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/tests/unit/qt/test_sidebar.py +0 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/tests/unit/qt/test_update_banner.py +0 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/tests/unit/qt/test_update_feedback.py +0 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/tests/unit/test_cli.py +0 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/tests/unit/test_client_updater.py +0 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/tests/unit/test_client_version.py +0 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/tests/unit/test_examples.py +0 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/tests/unit/test_install.py +0 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/tests/unit/test_uri.py +0 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/tests/unit/test_workers.py +0 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/tests/unit/windows/__init__.py +0 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/tests/unit/windows/conftest.py +0 -0
- {synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/tests/unit/windows/test_protocol.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '0.0.1.dev44'
|
{synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/synodic_client/application/bootstrap.py
RENAMED
|
@@ -9,7 +9,7 @@ Import order matters:
|
|
|
9
9
|
1. stdlib + config (pure-Python, fast)
|
|
10
10
|
2. configure_logging() — now Qt-free
|
|
11
11
|
3. initialize_velopack() — hooks run with logging active
|
|
12
|
-
4.
|
|
12
|
+
4. run_startup_preamble() — protocol, config seed, auto-startup
|
|
13
13
|
5. import qt.application — PySide6 / porringer loaded here
|
|
14
14
|
"""
|
|
15
15
|
|
|
@@ -17,9 +17,6 @@ import sys
|
|
|
17
17
|
|
|
18
18
|
from synodic_client.config import set_dev_mode
|
|
19
19
|
from synodic_client.logging import configure_logging
|
|
20
|
-
from synodic_client.protocol import register_protocol
|
|
21
|
-
from synodic_client.resolution import resolve_config, seed_user_config_from_build
|
|
22
|
-
from synodic_client.startup import register_startup, remove_startup
|
|
23
20
|
from synodic_client.updater import initialize_velopack
|
|
24
21
|
|
|
25
22
|
_PROTOCOL_SCHEME = 'synodic'
|
|
@@ -32,16 +29,9 @@ configure_logging()
|
|
|
32
29
|
initialize_velopack()
|
|
33
30
|
|
|
34
31
|
if not _dev_mode:
|
|
35
|
-
|
|
36
|
-
seed_user_config_from_build()
|
|
32
|
+
from synodic_client.application.init import run_startup_preamble
|
|
37
33
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
_config = resolve_config()
|
|
41
|
-
if _config.auto_start:
|
|
42
|
-
register_startup(sys.executable)
|
|
43
|
-
else:
|
|
44
|
-
remove_startup()
|
|
34
|
+
run_startup_preamble(sys.executable)
|
|
45
35
|
|
|
46
36
|
# Heavy imports happen here — PySide6, porringer, etc.
|
|
47
37
|
from synodic_client.application.qt import application
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Shared startup preamble for frozen and CLI entry points.
|
|
2
|
+
|
|
3
|
+
Encapsulates the one-time initialisation that both
|
|
4
|
+
:mod:`synodic_client.application.bootstrap` (PyInstaller) and
|
|
5
|
+
:mod:`synodic_client.application.qt` (CLI / dev-script) need to
|
|
6
|
+
perform before the GUI event loop starts:
|
|
7
|
+
|
|
8
|
+
1. Seed user config from the build config (one-time propagation).
|
|
9
|
+
2. Register the ``synodic://`` URI protocol handler.
|
|
10
|
+
3. Synchronise the Windows auto-startup registry entry with the
|
|
11
|
+
persisted ``auto_start`` preference.
|
|
12
|
+
|
|
13
|
+
Heavy dependencies (PySide6, porringer) are **not** imported here so
|
|
14
|
+
that the bootstrap path can call this before loading them.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
import sys
|
|
19
|
+
|
|
20
|
+
from synodic_client.protocol import register_protocol
|
|
21
|
+
from synodic_client.resolution import resolve_config, seed_user_config_from_build
|
|
22
|
+
from synodic_client.startup import register_startup, remove_startup
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
_preamble_done = False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def run_startup_preamble(exe_path: str | None = None) -> None:
|
|
30
|
+
"""Run the shared startup preamble for non-dev-mode launches.
|
|
31
|
+
|
|
32
|
+
Both the frozen entry point
|
|
33
|
+
(:mod:`~synodic_client.application.bootstrap`) and the CLI entry
|
|
34
|
+
point (:func:`~synodic_client.application.qt.application`) call
|
|
35
|
+
this unconditionally. An internal guard ensures the work only
|
|
36
|
+
executes once per process.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
exe_path: Absolute path to the application executable. Defaults
|
|
40
|
+
to ``sys.executable`` when not supplied.
|
|
41
|
+
"""
|
|
42
|
+
global _preamble_done # noqa: PLW0603
|
|
43
|
+
if _preamble_done:
|
|
44
|
+
return
|
|
45
|
+
_preamble_done = True
|
|
46
|
+
|
|
47
|
+
if exe_path is None:
|
|
48
|
+
exe_path = sys.executable
|
|
49
|
+
|
|
50
|
+
# Seed user config from the build config (one-time propagation).
|
|
51
|
+
seed_user_config_from_build()
|
|
52
|
+
|
|
53
|
+
register_protocol(exe_path)
|
|
54
|
+
|
|
55
|
+
config = resolve_config()
|
|
56
|
+
if config.auto_start:
|
|
57
|
+
register_startup(exe_path)
|
|
58
|
+
else:
|
|
59
|
+
remove_startup()
|
|
60
|
+
|
|
61
|
+
logger.info('Startup preamble complete (auto_start=%s)', config.auto_start)
|
|
@@ -15,6 +15,7 @@ from PySide6.QtCore import Qt, QTimer
|
|
|
15
15
|
from PySide6.QtWidgets import QApplication
|
|
16
16
|
|
|
17
17
|
from synodic_client.application.icon import app_icon
|
|
18
|
+
from synodic_client.application.init import run_startup_preamble
|
|
18
19
|
from synodic_client.application.instance import SingleInstance
|
|
19
20
|
from synodic_client.application.screen.install import InstallPreviewWindow
|
|
20
21
|
from synodic_client.application.screen.screen import Screen
|
|
@@ -23,15 +24,12 @@ from synodic_client.application.uri import parse_uri
|
|
|
23
24
|
from synodic_client.client import Client
|
|
24
25
|
from synodic_client.config import set_dev_mode
|
|
25
26
|
from synodic_client.logging import configure_logging
|
|
26
|
-
from synodic_client.protocol import register_protocol
|
|
27
27
|
from synodic_client.resolution import (
|
|
28
28
|
ResolvedConfig,
|
|
29
29
|
resolve_config,
|
|
30
30
|
resolve_update_config,
|
|
31
31
|
resolve_version,
|
|
32
|
-
seed_user_config_from_build,
|
|
33
32
|
)
|
|
34
|
-
from synodic_client.startup import register_startup, remove_startup
|
|
35
33
|
from synodic_client.updater import initialize_velopack
|
|
36
34
|
|
|
37
35
|
|
|
@@ -139,20 +137,10 @@ def application(*, uri: str | None = None, dev_mode: bool = False) -> None:
|
|
|
139
137
|
_install_exception_hook(logger)
|
|
140
138
|
|
|
141
139
|
if not dev_mode:
|
|
142
|
-
#
|
|
143
|
-
#
|
|
144
|
-
# PyInstaller runtime hook (rthook_no_console.py).
|
|
140
|
+
# All three functions are idempotent — safe to call even when
|
|
141
|
+
# bootstrap.py has already executed them before heavy imports.
|
|
145
142
|
initialize_velopack()
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
# Seed user config from build config (one-time propagation).
|
|
149
|
-
seed_user_config_from_build()
|
|
150
|
-
|
|
151
|
-
startup_config = resolve_config()
|
|
152
|
-
if startup_config.auto_start:
|
|
153
|
-
register_startup(sys.executable)
|
|
154
|
-
else:
|
|
155
|
-
remove_startup()
|
|
143
|
+
run_startup_preamble(sys.executable)
|
|
156
144
|
|
|
157
145
|
if uri:
|
|
158
146
|
logger.info('Received URI: %s', uri)
|
|
@@ -27,9 +27,10 @@ from PySide6.QtWidgets import (
|
|
|
27
27
|
QWidget,
|
|
28
28
|
)
|
|
29
29
|
|
|
30
|
+
from synodic_client._version import __version__
|
|
30
31
|
from synodic_client.application.icon import app_icon
|
|
31
32
|
from synodic_client.application.screen.card import CardFrame
|
|
32
|
-
from synodic_client.application.theme import SETTINGS_WINDOW_MIN_SIZE
|
|
33
|
+
from synodic_client.application.theme import SETTINGS_WINDOW_MIN_SIZE, UPDATE_STATUS_CHECKING_STYLE
|
|
33
34
|
from synodic_client.logging import log_path
|
|
34
35
|
from synodic_client.resolution import ResolvedConfig, update_user_config
|
|
35
36
|
from synodic_client.startup import is_startup_registered, register_startup, remove_startup
|
|
@@ -92,6 +93,11 @@ class SettingsWindow(QMainWindow):
|
|
|
92
93
|
layout.addWidget(self._build_advanced_section())
|
|
93
94
|
layout.addStretch()
|
|
94
95
|
|
|
96
|
+
version_label = QLabel(f'Version {__version__}')
|
|
97
|
+
version_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
98
|
+
version_label.setStyleSheet('color: rgba(255, 255, 255, 0.4); font-size: 11px;')
|
|
99
|
+
layout.addWidget(version_label)
|
|
100
|
+
|
|
95
101
|
scroll.setWidget(container)
|
|
96
102
|
self.setCentralWidget(scroll)
|
|
97
103
|
|
|
@@ -163,6 +169,11 @@ class SettingsWindow(QMainWindow):
|
|
|
163
169
|
self._detect_updates_check.toggled.connect(self._on_detect_updates_changed)
|
|
164
170
|
content.addWidget(self._detect_updates_check)
|
|
165
171
|
|
|
172
|
+
# Automatically apply updates
|
|
173
|
+
self._auto_apply_check = QCheckBox('Automatically apply updates')
|
|
174
|
+
self._auto_apply_check.toggled.connect(self._on_auto_apply_changed)
|
|
175
|
+
content.addWidget(self._auto_apply_check)
|
|
176
|
+
|
|
166
177
|
# Check for Updates
|
|
167
178
|
row = QHBoxLayout()
|
|
168
179
|
self._check_updates_btn = QPushButton('Check for Updates\u2026')
|
|
@@ -218,16 +229,24 @@ class SettingsWindow(QMainWindow):
|
|
|
218
229
|
|
|
219
230
|
# Checkboxes
|
|
220
231
|
self._detect_updates_check.setChecked(config.detect_updates)
|
|
232
|
+
self._auto_apply_check.setChecked(config.auto_apply)
|
|
221
233
|
self._auto_start_check.setChecked(is_startup_registered())
|
|
222
234
|
|
|
223
|
-
def set_update_status(self, text: str) -> None:
|
|
224
|
-
"""Set the inline status text next to the *Check for Updates* button.
|
|
235
|
+
def set_update_status(self, text: str, style: str = '') -> None:
|
|
236
|
+
"""Set the inline status text next to the *Check for Updates* button.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
text: The status message.
|
|
240
|
+
style: Optional stylesheet for the label (e.g. color).
|
|
241
|
+
"""
|
|
225
242
|
self._update_status_label.setText(text)
|
|
243
|
+
self._update_status_label.setStyleSheet(style)
|
|
226
244
|
|
|
227
245
|
def set_checking(self) -> None:
|
|
228
246
|
"""Enter the *checking* state — disable button and show status."""
|
|
229
247
|
self._check_updates_btn.setEnabled(False)
|
|
230
248
|
self._update_status_label.setText('Checking\u2026')
|
|
249
|
+
self._update_status_label.setStyleSheet(UPDATE_STATUS_CHECKING_STYLE)
|
|
231
250
|
|
|
232
251
|
def reset_check_updates_button(self) -> None:
|
|
233
252
|
"""Re-enable the *Check for Updates* button after a check completes."""
|
|
@@ -262,6 +281,7 @@ class SettingsWindow(QMainWindow):
|
|
|
262
281
|
self._auto_update_spin,
|
|
263
282
|
self._tool_update_spin,
|
|
264
283
|
self._detect_updates_check,
|
|
284
|
+
self._auto_apply_check,
|
|
265
285
|
self._auto_start_check,
|
|
266
286
|
self._check_updates_btn,
|
|
267
287
|
)
|
|
@@ -301,6 +321,9 @@ class SettingsWindow(QMainWindow):
|
|
|
301
321
|
def _on_detect_updates_changed(self, checked: bool) -> None:
|
|
302
322
|
self._persist(detect_updates=checked)
|
|
303
323
|
|
|
324
|
+
def _on_auto_apply_changed(self, checked: bool) -> None:
|
|
325
|
+
self._persist(auto_apply=checked)
|
|
326
|
+
|
|
304
327
|
def _on_auto_start_changed(self, checked: bool) -> None:
|
|
305
328
|
self._config = update_user_config(auto_start=checked)
|
|
306
329
|
if checked:
|
{synodic_client-0.0.1.dev41 → synodic_client-0.0.1.dev44}/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.dev41 → synodic_client-0.0.1.dev44}/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
|
# ---------------------------------------------------------------------------
|