synodic-client 0.0.1.dev63__tar.gz → 0.0.1.dev64__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.dev63 → synodic_client-0.0.1.dev64}/PKG-INFO +1 -1
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/pyproject.toml +1 -1
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/synodic_client/application/bootstrap.py +1 -1
- synodic_client-0.0.1.dev64/synodic_client/application/config_store.py +65 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/synodic_client/application/qt.py +5 -3
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/synodic_client/application/schema.py +21 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/synodic_client/application/screen/projects.py +9 -6
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/synodic_client/application/screen/screen.py +21 -21
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/synodic_client/application/screen/settings.py +15 -25
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/synodic_client/application/screen/tool_update_controller.py +91 -85
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/synodic_client/application/screen/tray.py +14 -28
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/synodic_client/application/screen/update_banner.py +25 -17
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/synodic_client/application/update_controller.py +22 -27
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/synodic_client/application/workers.py +15 -14
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/synodic_client/subprocess_patch.py +5 -5
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/tests/unit/qt/test_gather_packages.py +38 -32
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/tests/unit/qt/test_settings.py +39 -70
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/tests/unit/qt/test_tray_window_show.py +25 -7
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/tests/unit/qt/test_update_controller.py +30 -35
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/LICENSE.md +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/README.md +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/synodic_client/__init__.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/synodic_client/__main__.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/synodic_client/application/__init__.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/synodic_client/application/data.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/synodic_client/application/icon.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/synodic_client/application/init.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/synodic_client/application/instance.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/synodic_client/application/screen/__init__.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/synodic_client/application/screen/action_card.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/synodic_client/application/screen/card.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/synodic_client/application/screen/install.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/synodic_client/application/screen/install_workers.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/synodic_client/application/screen/log_panel.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/synodic_client/application/screen/plugin_row.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/synodic_client/application/screen/schema.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/synodic_client/application/screen/sidebar.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/synodic_client/application/screen/spinner.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/synodic_client/application/theme.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/synodic_client/application/uri.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/synodic_client/cli.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/synodic_client/client.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/synodic_client/config.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/synodic_client/logging.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/synodic_client/protocol.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/synodic_client/py.typed +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/synodic_client/resolution.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/synodic_client/schema.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/synodic_client/startup.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/synodic_client/updater.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/tests/__init__.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/tests/conftest.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/tests/unit/__init__.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/tests/unit/qt/__init__.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/tests/unit/qt/conftest.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/tests/unit/qt/test_action_card.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/tests/unit/qt/test_install_preview.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/tests/unit/qt/test_log_panel.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/tests/unit/qt/test_logging.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/tests/unit/qt/test_preview_model.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/tests/unit/qt/test_sidebar.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/tests/unit/qt/test_update_banner.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/tests/unit/qt/test_update_feedback.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/tests/unit/test_cli.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/tests/unit/test_client_updater.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/tests/unit/test_client_version.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/tests/unit/test_config.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/tests/unit/test_examples.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/tests/unit/test_init.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/tests/unit/test_install.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/tests/unit/test_resolution.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/tests/unit/test_updater.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/tests/unit/test_uri.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/tests/unit/test_workers.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/tests/unit/windows/__init__.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/tests/unit/windows/conftest.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/tests/unit/windows/test_protocol.py +0 -0
- {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/tests/unit/windows/test_startup.py +0 -0
{synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/synodic_client/application/bootstrap.py
RENAMED
|
@@ -17,8 +17,8 @@ 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.subprocess_patch import apply as _apply_subprocess_patch
|
|
21
20
|
from synodic_client.protocol import extract_uri_from_args
|
|
21
|
+
from synodic_client.subprocess_patch import apply as _apply_subprocess_patch
|
|
22
22
|
from synodic_client.updater import initialize_velopack
|
|
23
23
|
|
|
24
24
|
# Parse flags early so logging uses the right filename and level.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Centralized configuration store.
|
|
2
|
+
|
|
3
|
+
Provides a single source of truth for :class:`ResolvedConfig` so that
|
|
4
|
+
every consumer (ToolsView, SettingsWindow, UpdateController,
|
|
5
|
+
ToolUpdateOrchestrator) always reads the same snapshot and receives
|
|
6
|
+
change notifications through a Qt signal.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from PySide6.QtCore import QObject, Signal
|
|
12
|
+
|
|
13
|
+
from synodic_client.resolution import ResolvedConfig, resolve_config, update_user_config
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ConfigStore(QObject):
|
|
17
|
+
"""Observable wrapper around :class:`ResolvedConfig`.
|
|
18
|
+
|
|
19
|
+
All config mutations go through :meth:`update` (which persists to
|
|
20
|
+
disk) or :meth:`set` (which replaces without persisting). Both
|
|
21
|
+
emit :attr:`changed` so every connected consumer stays in sync.
|
|
22
|
+
|
|
23
|
+
Typical usage::
|
|
24
|
+
|
|
25
|
+
store = ConfigStore(initial_config)
|
|
26
|
+
store.changed.connect(some_consumer.on_config_changed)
|
|
27
|
+
store.update(auto_apply=False) # persists + emits
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
changed = Signal(object)
|
|
31
|
+
"""Emitted with the new ``ResolvedConfig`` after every mutation."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, config: ResolvedConfig | None = None, parent: QObject | None = None) -> None:
|
|
34
|
+
"""Create a new store, optionally seeded with *config*."""
|
|
35
|
+
super().__init__(parent)
|
|
36
|
+
self._config = config if config is not None else resolve_config()
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def config(self) -> ResolvedConfig:
|
|
40
|
+
"""The current configuration snapshot."""
|
|
41
|
+
return self._config
|
|
42
|
+
|
|
43
|
+
def update(self, **changes: object) -> ResolvedConfig:
|
|
44
|
+
"""Persist *changes* to disk and broadcast the new config.
|
|
45
|
+
|
|
46
|
+
Wraps :func:`~synodic_client.resolution.update_user_config`.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
**changes: Field-name / value pairs forwarded to
|
|
50
|
+
:func:`update_user_config`.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
The fresh :class:`ResolvedConfig`.
|
|
54
|
+
"""
|
|
55
|
+
self._config = update_user_config(**changes)
|
|
56
|
+
self.changed.emit(self._config)
|
|
57
|
+
return self._config
|
|
58
|
+
|
|
59
|
+
def set(self, config: ResolvedConfig) -> None:
|
|
60
|
+
"""Replace the config without persisting and notify listeners.
|
|
61
|
+
|
|
62
|
+
Use for externally resolved configs (e.g. passed at startup).
|
|
63
|
+
"""
|
|
64
|
+
self._config = config
|
|
65
|
+
self.changed.emit(self._config)
|
|
@@ -15,6 +15,7 @@ from porringer.schema import LocalConfiguration
|
|
|
15
15
|
from PySide6.QtCore import QEvent, QObject, Qt, QTimer
|
|
16
16
|
from PySide6.QtWidgets import QApplication, QWidget
|
|
17
17
|
|
|
18
|
+
from synodic_client.application.config_store import ConfigStore
|
|
18
19
|
from synodic_client.application.icon import app_icon
|
|
19
20
|
from synodic_client.application.init import run_startup_preamble
|
|
20
21
|
from synodic_client.application.instance import SingleInstance
|
|
@@ -216,8 +217,9 @@ def application(*, uri: str | None = None, dev_mode: bool = False, debug: bool =
|
|
|
216
217
|
sys.exit(0)
|
|
217
218
|
instance.start_server()
|
|
218
219
|
|
|
219
|
-
|
|
220
|
-
|
|
220
|
+
_store = ConfigStore(config)
|
|
221
|
+
_screen = Screen(porringer, _store)
|
|
222
|
+
_tray = TrayScreen(app, client, _screen.window, store=_store)
|
|
221
223
|
|
|
222
224
|
# Keep install preview windows alive until the app exits
|
|
223
225
|
_install_windows: list[InstallPreviewWindow] = []
|
|
@@ -227,7 +229,7 @@ def application(*, uri: str | None = None, dev_mode: bool = False, debug: bool =
|
|
|
227
229
|
window = InstallPreviewWindow(
|
|
228
230
|
porringer,
|
|
229
231
|
manifest_url,
|
|
230
|
-
config=config,
|
|
232
|
+
config=_store.config,
|
|
231
233
|
)
|
|
232
234
|
_install_windows.append(window)
|
|
233
235
|
window.show()
|
{synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev64}/synodic_client/application/schema.py
RENAMED
|
@@ -51,3 +51,24 @@ class ToolUpdateResult:
|
|
|
51
51
|
failed: int = 0
|
|
52
52
|
updated_packages: set[str] = field(default_factory=set)
|
|
53
53
|
"""Package names that were successfully upgraded."""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True, slots=True)
|
|
57
|
+
class UpdateTarget:
|
|
58
|
+
"""Identifies the scope of a manual tool update.
|
|
59
|
+
|
|
60
|
+
Passed to the shared completion handler so it can clear the correct
|
|
61
|
+
updating state and derive timestamp keys. ``None`` (the default in
|
|
62
|
+
the handler) means the update was periodic / automatic.
|
|
63
|
+
|
|
64
|
+
When *package* is empty the update targeted an entire plugin;
|
|
65
|
+
otherwise it targeted one specific package within the plugin.
|
|
66
|
+
*plugin* always carries the signal key (possibly composite
|
|
67
|
+
``"plugin:tag"``).
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
plugin: str
|
|
71
|
+
"""Signal key for the plugin (may be composite ``"name:tag"``)."""
|
|
72
|
+
|
|
73
|
+
package: str = ''
|
|
74
|
+
"""Package name, or empty when the whole plugin was updated."""
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import asyncio
|
|
6
6
|
import logging
|
|
7
7
|
from pathlib import Path
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
8
9
|
|
|
9
10
|
from porringer.api import API
|
|
10
11
|
from porringer.backend.command.core.discovery import DiscoveredPlugins
|
|
@@ -24,7 +25,9 @@ from synodic_client.application.screen.schema import PreviewPhase
|
|
|
24
25
|
from synodic_client.application.screen.sidebar import ManifestSidebar
|
|
25
26
|
from synodic_client.application.screen.spinner import LoadingIndicator
|
|
26
27
|
from synodic_client.application.theme import COMPACT_MARGINS
|
|
27
|
-
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from synodic_client.application.config_store import ConfigStore
|
|
28
31
|
|
|
29
32
|
logger = logging.getLogger(__name__)
|
|
30
33
|
|
|
@@ -41,7 +44,7 @@ class ProjectsView(QWidget):
|
|
|
41
44
|
def __init__(
|
|
42
45
|
self,
|
|
43
46
|
porringer: API,
|
|
44
|
-
|
|
47
|
+
store: ConfigStore,
|
|
45
48
|
parent: QWidget | None = None,
|
|
46
49
|
*,
|
|
47
50
|
coordinator: DataCoordinator | None = None,
|
|
@@ -50,14 +53,14 @@ class ProjectsView(QWidget):
|
|
|
50
53
|
|
|
51
54
|
Args:
|
|
52
55
|
porringer: The porringer API instance.
|
|
53
|
-
|
|
56
|
+
store: The centralised :class:`ConfigStore`.
|
|
54
57
|
parent: Optional parent widget.
|
|
55
58
|
coordinator: Shared data coordinator for validated directory
|
|
56
59
|
data.
|
|
57
60
|
"""
|
|
58
61
|
super().__init__(parent)
|
|
59
62
|
self._porringer = porringer
|
|
60
|
-
self.
|
|
63
|
+
self._store = store
|
|
61
64
|
self._coordinator = coordinator
|
|
62
65
|
self._refresh_in_progress = False
|
|
63
66
|
self._pending_select: Path | None = None
|
|
@@ -163,7 +166,7 @@ class ProjectsView(QWidget):
|
|
|
163
166
|
widget.load(
|
|
164
167
|
str(path),
|
|
165
168
|
project_directory=path if path.is_dir() else path.parent,
|
|
166
|
-
detect_updates=self.
|
|
169
|
+
detect_updates=self._store.config.detect_updates,
|
|
167
170
|
)
|
|
168
171
|
|
|
169
172
|
except Exception:
|
|
@@ -196,7 +199,7 @@ class ProjectsView(QWidget):
|
|
|
196
199
|
self._porringer,
|
|
197
200
|
self,
|
|
198
201
|
show_close=False,
|
|
199
|
-
config=self.
|
|
202
|
+
config=self._store.config,
|
|
200
203
|
)
|
|
201
204
|
widget._discovered_plugins = discovered
|
|
202
205
|
widget.install_finished.connect(self._on_install_finished)
|
|
@@ -34,6 +34,7 @@ from PySide6.QtWidgets import (
|
|
|
34
34
|
QWidget,
|
|
35
35
|
)
|
|
36
36
|
|
|
37
|
+
from synodic_client.application.config_store import ConfigStore
|
|
37
38
|
from synodic_client.application.data import DataCoordinator
|
|
38
39
|
from synodic_client.application.icon import app_icon
|
|
39
40
|
from synodic_client.application.screen.plugin_row import (
|
|
@@ -65,7 +66,6 @@ from synodic_client.application.theme import (
|
|
|
65
66
|
SEARCH_INPUT_STYLE,
|
|
66
67
|
SETTINGS_GEAR_STYLE,
|
|
67
68
|
)
|
|
68
|
-
from synodic_client.resolution import ResolvedConfig, update_user_config
|
|
69
69
|
|
|
70
70
|
logger = logging.getLogger(__name__)
|
|
71
71
|
|
|
@@ -111,7 +111,7 @@ class ToolsView(QWidget):
|
|
|
111
111
|
def __init__(
|
|
112
112
|
self,
|
|
113
113
|
porringer: API,
|
|
114
|
-
|
|
114
|
+
store: ConfigStore,
|
|
115
115
|
parent: QWidget | None = None,
|
|
116
116
|
*,
|
|
117
117
|
coordinator: DataCoordinator | None = None,
|
|
@@ -120,7 +120,7 @@ class ToolsView(QWidget):
|
|
|
120
120
|
|
|
121
121
|
Args:
|
|
122
122
|
porringer: The porringer API instance.
|
|
123
|
-
|
|
123
|
+
store: The centralised :class:`ConfigStore`.
|
|
124
124
|
parent: Optional parent widget.
|
|
125
125
|
coordinator: Shared data coordinator. When provided, the
|
|
126
126
|
view delegates plugin/directory fetching to the
|
|
@@ -128,7 +128,7 @@ class ToolsView(QWidget):
|
|
|
128
128
|
"""
|
|
129
129
|
super().__init__(parent)
|
|
130
130
|
self._porringer = porringer
|
|
131
|
-
self.
|
|
131
|
+
self._store = store
|
|
132
132
|
self._coordinator = coordinator
|
|
133
133
|
self._section_widgets: list[QWidget] = []
|
|
134
134
|
self._filter_chips: dict[str, FilterChip] = {}
|
|
@@ -379,7 +379,7 @@ class ToolsView(QWidget):
|
|
|
379
379
|
"""Clear existing widgets and rebuild the tool/package tree."""
|
|
380
380
|
self._clear_section_widgets()
|
|
381
381
|
|
|
382
|
-
auto_update_map = self.
|
|
382
|
+
auto_update_map = self._store.config.plugin_auto_update or {}
|
|
383
383
|
kind_buckets = self._bucket_by_kind(
|
|
384
384
|
data.plugins,
|
|
385
385
|
data.packages_map,
|
|
@@ -446,7 +446,7 @@ class ToolsView(QWidget):
|
|
|
446
446
|
|
|
447
447
|
auto_val = auto_update_map.get(plugin.name, True)
|
|
448
448
|
plugin_updates = self._updates_available.get(plugin.name, {})
|
|
449
|
-
tool_timestamps = self.
|
|
449
|
+
tool_timestamps = self._store.config.last_tool_updates or {}
|
|
450
450
|
default_exe = data.default_runtime_executable
|
|
451
451
|
|
|
452
452
|
# Sort: default runtime first, then descending by tag
|
|
@@ -534,7 +534,7 @@ class ToolsView(QWidget):
|
|
|
534
534
|
plugin_manifest = data.manifest_packages.get(plugin.name, set())
|
|
535
535
|
raw_packages = data.packages_map.get(plugin.name, [])
|
|
536
536
|
display_packages = self._build_display_packages(raw_packages, plugin_manifest)
|
|
537
|
-
tool_timestamps = self.
|
|
537
|
+
tool_timestamps = self._store.config.last_tool_updates or {}
|
|
538
538
|
|
|
539
539
|
if display_packages:
|
|
540
540
|
for pkg in display_packages:
|
|
@@ -1069,7 +1069,7 @@ class ToolsView(QWidget):
|
|
|
1069
1069
|
|
|
1070
1070
|
def _on_auto_update_toggled(self, plugin_name: str, enabled: bool) -> None:
|
|
1071
1071
|
"""Persist the plugin-level auto-update toggle change to config."""
|
|
1072
|
-
mapping = dict(self.
|
|
1072
|
+
mapping = dict(self._store.config.plugin_auto_update or {})
|
|
1073
1073
|
|
|
1074
1074
|
if enabled:
|
|
1075
1075
|
mapping.pop(plugin_name, None)
|
|
@@ -1077,7 +1077,7 @@ class ToolsView(QWidget):
|
|
|
1077
1077
|
mapping[plugin_name] = False
|
|
1078
1078
|
|
|
1079
1079
|
new_value = mapping if mapping else None
|
|
1080
|
-
self.
|
|
1080
|
+
self._store.update(plugin_auto_update=new_value)
|
|
1081
1081
|
logger.info('Auto-update for %s set to %s', plugin_name, enabled)
|
|
1082
1082
|
|
|
1083
1083
|
def _on_package_auto_update_toggled(
|
|
@@ -1087,7 +1087,7 @@ class ToolsView(QWidget):
|
|
|
1087
1087
|
enabled: bool,
|
|
1088
1088
|
) -> None:
|
|
1089
1089
|
"""Persist a per-package auto-update override to the nested config dict."""
|
|
1090
|
-
mapping = dict(self.
|
|
1090
|
+
mapping = dict(self._store.config.plugin_auto_update or {})
|
|
1091
1091
|
current = mapping.get(plugin_name)
|
|
1092
1092
|
|
|
1093
1093
|
if isinstance(current, dict):
|
|
@@ -1103,7 +1103,7 @@ class ToolsView(QWidget):
|
|
|
1103
1103
|
mapping.pop(plugin_name, None)
|
|
1104
1104
|
|
|
1105
1105
|
new_value = mapping if mapping else None
|
|
1106
|
-
self.
|
|
1106
|
+
self._store.update(plugin_auto_update=new_value)
|
|
1107
1107
|
logger.info(
|
|
1108
1108
|
'Auto-update for %s/%s set to %s',
|
|
1109
1109
|
plugin_name,
|
|
@@ -1389,17 +1389,17 @@ class MainWindow(QMainWindow):
|
|
|
1389
1389
|
def __init__(
|
|
1390
1390
|
self,
|
|
1391
1391
|
porringer: API | None = None,
|
|
1392
|
-
|
|
1392
|
+
store: ConfigStore | None = None,
|
|
1393
1393
|
) -> None:
|
|
1394
1394
|
"""Initialize the main window.
|
|
1395
1395
|
|
|
1396
1396
|
Args:
|
|
1397
1397
|
porringer: Optional porringer API instance for manifest display.
|
|
1398
|
-
|
|
1398
|
+
store: The centralised :class:`ConfigStore`.
|
|
1399
1399
|
"""
|
|
1400
1400
|
super().__init__()
|
|
1401
1401
|
self._porringer = porringer
|
|
1402
|
-
self.
|
|
1402
|
+
self._store = store
|
|
1403
1403
|
self._coordinator: DataCoordinator | None = DataCoordinator(porringer) if porringer is not None else None
|
|
1404
1404
|
self.setWindowTitle('Synodic Client')
|
|
1405
1405
|
self.setMinimumSize(*MAIN_WINDOW_MIN_SIZE)
|
|
@@ -1445,12 +1445,12 @@ class MainWindow(QMainWindow):
|
|
|
1445
1445
|
|
|
1446
1446
|
def show(self) -> None:
|
|
1447
1447
|
"""Show the window, initializing UI lazily on first show."""
|
|
1448
|
-
if self._tabs is None and self._porringer is not None and self.
|
|
1448
|
+
if self._tabs is None and self._porringer is not None and self._store is not None:
|
|
1449
1449
|
self._tabs = QTabWidget(self)
|
|
1450
1450
|
|
|
1451
1451
|
self._projects_view = ProjectsView(
|
|
1452
1452
|
self._porringer,
|
|
1453
|
-
self.
|
|
1453
|
+
self._store,
|
|
1454
1454
|
self,
|
|
1455
1455
|
coordinator=self._coordinator,
|
|
1456
1456
|
)
|
|
@@ -1458,7 +1458,7 @@ class MainWindow(QMainWindow):
|
|
|
1458
1458
|
|
|
1459
1459
|
self._tools_view = ToolsView(
|
|
1460
1460
|
self._porringer,
|
|
1461
|
-
self.
|
|
1461
|
+
self._store,
|
|
1462
1462
|
self,
|
|
1463
1463
|
coordinator=self._coordinator,
|
|
1464
1464
|
)
|
|
@@ -1507,16 +1507,16 @@ class Screen:
|
|
|
1507
1507
|
def __init__(
|
|
1508
1508
|
self,
|
|
1509
1509
|
porringer: API | None = None,
|
|
1510
|
-
|
|
1510
|
+
store: ConfigStore | None = None,
|
|
1511
1511
|
) -> None:
|
|
1512
1512
|
"""Initialize the screen.
|
|
1513
1513
|
|
|
1514
1514
|
Args:
|
|
1515
1515
|
porringer: Optional porringer API instance.
|
|
1516
|
-
|
|
1516
|
+
store: The centralised :class:`ConfigStore`.
|
|
1517
1517
|
"""
|
|
1518
1518
|
self._porringer = porringer
|
|
1519
|
-
self.
|
|
1519
|
+
self._store = store
|
|
1520
1520
|
|
|
1521
1521
|
@property
|
|
1522
1522
|
def window(self) -> MainWindow:
|
|
@@ -1526,5 +1526,5 @@ class Screen:
|
|
|
1526
1526
|
The MainWindow instance.
|
|
1527
1527
|
"""
|
|
1528
1528
|
if self._window is None:
|
|
1529
|
-
self._window = MainWindow(self._porringer, self.
|
|
1529
|
+
self._window = MainWindow(self._porringer, self._store)
|
|
1530
1530
|
return self._window
|
|
@@ -28,12 +28,12 @@ from PySide6.QtWidgets import (
|
|
|
28
28
|
QWidget,
|
|
29
29
|
)
|
|
30
30
|
|
|
31
|
+
from synodic_client.application.config_store import ConfigStore
|
|
31
32
|
from synodic_client.application.icon import app_icon
|
|
32
33
|
from synodic_client.application.screen import _format_relative_time
|
|
33
34
|
from synodic_client.application.screen.card import CardFrame
|
|
34
35
|
from synodic_client.application.theme import SETTINGS_WINDOW_MIN_SIZE, UPDATE_STATUS_CHECKING_STYLE
|
|
35
36
|
from synodic_client.logging import log_path, set_debug_level
|
|
36
|
-
from synodic_client.resolution import ResolvedConfig, update_user_config
|
|
37
37
|
from synodic_client.schema import GITHUB_REPO_URL
|
|
38
38
|
from synodic_client.startup import is_startup_registered, register_startup, remove_startup
|
|
39
39
|
|
|
@@ -43,14 +43,11 @@ logger = logging.getLogger(__name__)
|
|
|
43
43
|
class SettingsWindow(QMainWindow):
|
|
44
44
|
"""Application settings window with grouped card sections.
|
|
45
45
|
|
|
46
|
-
All controls persist changes immediately via
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
All controls persist changes immediately via the shared
|
|
47
|
+
:class:`ConfigStore`, which broadcasts the new :class:`ResolvedConfig`
|
|
48
|
+
to every connected consumer.
|
|
49
49
|
"""
|
|
50
50
|
|
|
51
|
-
settings_changed = Signal(object)
|
|
52
|
-
"""Emitted with the new ``ResolvedConfig`` whenever a setting is changed and persisted."""
|
|
53
|
-
|
|
54
51
|
check_updates_requested = Signal()
|
|
55
52
|
"""Emitted when the user clicks the *Check for Updates* button."""
|
|
56
53
|
|
|
@@ -74,19 +71,19 @@ class SettingsWindow(QMainWindow):
|
|
|
74
71
|
|
|
75
72
|
def __init__(
|
|
76
73
|
self,
|
|
77
|
-
|
|
74
|
+
store: ConfigStore,
|
|
78
75
|
version: str = '',
|
|
79
76
|
parent: QWidget | None = None,
|
|
80
77
|
) -> None:
|
|
81
78
|
"""Initialise the settings window.
|
|
82
79
|
|
|
83
80
|
Args:
|
|
84
|
-
|
|
81
|
+
store: The centralised configuration store.
|
|
85
82
|
version: The application version string to display.
|
|
86
83
|
parent: Optional parent widget.
|
|
87
84
|
"""
|
|
88
85
|
super().__init__(parent)
|
|
89
|
-
self.
|
|
86
|
+
self._store = store
|
|
90
87
|
self._version = version
|
|
91
88
|
self.setWindowTitle('Synodic Settings')
|
|
92
89
|
self.setMinimumSize(*SETTINGS_WINDOW_MIN_SIZE)
|
|
@@ -254,7 +251,7 @@ class SettingsWindow(QMainWindow):
|
|
|
254
251
|
|
|
255
252
|
Signals are blocked during the update to prevent feedback loops.
|
|
256
253
|
"""
|
|
257
|
-
config = self.
|
|
254
|
+
config = self._store.config
|
|
258
255
|
|
|
259
256
|
with self._block_signals():
|
|
260
257
|
# Channel: index 0 = Stable, 1 = Development
|
|
@@ -305,15 +302,6 @@ class SettingsWindow(QMainWindow):
|
|
|
305
302
|
"""Re-enable the *Check for Updates* button after a check completes."""
|
|
306
303
|
self._check_updates_btn.setEnabled(True)
|
|
307
304
|
|
|
308
|
-
def update_config(self, config: ResolvedConfig) -> None:
|
|
309
|
-
"""Replace the internal config snapshot without emitting signals.
|
|
310
|
-
|
|
311
|
-
Called by controllers that persist timestamps so that the next
|
|
312
|
-
:meth:`sync_from_config` sees fresh data instead of the stale
|
|
313
|
-
snapshot captured at construction time.
|
|
314
|
-
"""
|
|
315
|
-
self._config = config
|
|
316
|
-
|
|
317
305
|
def set_last_checked(self, timestamp: str) -> None:
|
|
318
306
|
"""Update the *last updated* label from an ISO 8601 timestamp."""
|
|
319
307
|
relative = _format_relative_time(timestamp)
|
|
@@ -331,7 +319,11 @@ class SettingsWindow(QMainWindow):
|
|
|
331
319
|
# adjustSize() only reaches the minimum. Compute the ideal
|
|
332
320
|
# height from the content widget directly.
|
|
333
321
|
content_hint = self._scroll_content.sizeHint()
|
|
334
|
-
|
|
322
|
+
layout = self._scroll_content.layout()
|
|
323
|
+
if layout is None:
|
|
324
|
+
super().show()
|
|
325
|
+
return
|
|
326
|
+
margins = layout.contentsMargins()
|
|
335
327
|
ideal_w = max(content_hint.width() + margins.left() + margins.right(), self.minimumWidth())
|
|
336
328
|
ideal_h = max(content_hint.height() + margins.top() + margins.bottom(), self.minimumHeight())
|
|
337
329
|
self.resize(ideal_w, ideal_h)
|
|
@@ -349,8 +341,7 @@ class SettingsWindow(QMainWindow):
|
|
|
349
341
|
Args:
|
|
350
342
|
**changes: Field-name / value pairs to persist.
|
|
351
343
|
"""
|
|
352
|
-
self.
|
|
353
|
-
self.settings_changed.emit(self._config)
|
|
344
|
+
self._store.update(**changes)
|
|
354
345
|
|
|
355
346
|
@contextmanager
|
|
356
347
|
def _block_signals(self) -> Iterator[None]:
|
|
@@ -406,13 +397,12 @@ class SettingsWindow(QMainWindow):
|
|
|
406
397
|
self._persist(auto_apply=checked)
|
|
407
398
|
|
|
408
399
|
def _on_auto_start_changed(self, checked: bool) -> None:
|
|
409
|
-
self.
|
|
400
|
+
self._store.update(auto_start=checked)
|
|
410
401
|
if getattr(sys, 'frozen', False):
|
|
411
402
|
if checked:
|
|
412
403
|
register_startup(sys.executable)
|
|
413
404
|
else:
|
|
414
405
|
remove_startup()
|
|
415
|
-
self.settings_changed.emit(self._config)
|
|
416
406
|
|
|
417
407
|
def _on_debug_logging_changed(self, checked: bool) -> None:
|
|
418
408
|
set_debug_level(enabled=checked)
|