synodic-client 0.0.1.dev30__tar.gz → 0.0.1.dev31__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.dev30 → synodic_client-0.0.1.dev31}/PKG-INFO +2 -2
- {synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/pyproject.toml +2 -2
- synodic_client-0.0.1.dev31/synodic_client/_version.py +1 -0
- {synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/synodic_client/application/qt.py +1 -2
- {synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/synodic_client/application/screen/screen.py +11 -0
- synodic_client-0.0.1.dev31/synodic_client/application/screen/settings.py +289 -0
- {synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/synodic_client/application/screen/tray.py +33 -129
- {synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/synodic_client/application/theme.py +12 -1
- {synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/synodic_client/resolution.py +5 -1
- {synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/synodic_client/updater.py +40 -2
- {synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/tests/unit/qt/test_logging.py +8 -8
- synodic_client-0.0.1.dev31/tests/unit/qt/test_settings.py +308 -0
- {synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/tests/unit/test_resolution.py +14 -7
- {synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/tests/unit/test_updater.py +36 -0
- synodic_client-0.0.1.dev30/synodic_client/_version.py +0 -1
- {synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/LICENSE.md +0 -0
- {synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/README.md +0 -0
- {synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/synodic_client/__init__.py +0 -0
- {synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/synodic_client/__main__.py +0 -0
- {synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/synodic_client/application/__init__.py +0 -0
- {synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/synodic_client/application/bootstrap.py +0 -0
- {synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/synodic_client/application/icon.py +0 -0
- {synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/synodic_client/application/instance.py +0 -0
- {synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/synodic_client/application/screen/__init__.py +0 -0
- {synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/synodic_client/application/screen/action_card.py +0 -0
- {synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/synodic_client/application/screen/card.py +0 -0
- {synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/synodic_client/application/screen/install.py +0 -0
- {synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/synodic_client/application/screen/log_panel.py +0 -0
- {synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/synodic_client/application/screen/spinner.py +0 -0
- {synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/synodic_client/application/uri.py +0 -0
- {synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/synodic_client/cli.py +0 -0
- {synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/synodic_client/client.py +0 -0
- {synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/synodic_client/config.py +0 -0
- {synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/synodic_client/logging.py +0 -0
- {synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/synodic_client/protocol.py +0 -0
- {synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/synodic_client/py.typed +0 -0
- {synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/synodic_client/startup.py +0 -0
- {synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/tests/__init__.py +0 -0
- {synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/tests/conftest.py +0 -0
- {synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/tests/unit/__init__.py +0 -0
- {synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/tests/unit/qt/__init__.py +0 -0
- {synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/tests/unit/qt/conftest.py +0 -0
- {synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/tests/unit/qt/test_action_card.py +0 -0
- {synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/tests/unit/qt/test_install_preview.py +0 -0
- {synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/tests/unit/qt/test_log_panel.py +0 -0
- {synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/tests/unit/test_cli.py +0 -0
- {synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/tests/unit/test_client_updater.py +0 -0
- {synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/tests/unit/test_client_version.py +0 -0
- {synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/tests/unit/test_config.py +0 -0
- {synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/tests/unit/test_examples.py +0 -0
- {synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/tests/unit/test_install.py +0 -0
- {synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/tests/unit/test_uri.py +0 -0
- {synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/tests/unit/windows/__init__.py +0 -0
- {synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/tests/unit/windows/conftest.py +0 -0
- {synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/tests/unit/windows/test_protocol.py +0 -0
- {synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/tests/unit/windows/test_startup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: synodic_client
|
|
3
|
-
Version: 0.0.1.
|
|
3
|
+
Version: 0.0.1.dev31
|
|
4
4
|
Author-Email: Synodic Software <contact@synodic.software>
|
|
5
5
|
License: LGPL-3.0-or-later
|
|
6
6
|
Project-URL: homepage, https://github.com/synodic/synodic-client
|
|
@@ -8,7 +8,7 @@ Project-URL: repository, https://github.com/synodic/synodic-client
|
|
|
8
8
|
Requires-Python: <3.15,>=3.14
|
|
9
9
|
Requires-Dist: pyside6>=6.10.2
|
|
10
10
|
Requires-Dist: packaging>=26.0
|
|
11
|
-
Requires-Dist: porringer>=0.2.1.
|
|
11
|
+
Requires-Dist: porringer>=0.2.1.dev50
|
|
12
12
|
Requires-Dist: qasync>=0.28.0
|
|
13
13
|
Requires-Dist: velopack>=0.0.1442.dev64255
|
|
14
14
|
Requires-Dist: typer>=0.24.1
|
|
@@ -10,12 +10,12 @@ requires-python = ">=3.14, <3.15"
|
|
|
10
10
|
dependencies = [
|
|
11
11
|
"pyside6>=6.10.2",
|
|
12
12
|
"packaging>=26.0",
|
|
13
|
-
"porringer>=0.2.1.
|
|
13
|
+
"porringer>=0.2.1.dev50",
|
|
14
14
|
"qasync>=0.28.0",
|
|
15
15
|
"velopack>=0.0.1442.dev64255",
|
|
16
16
|
"typer>=0.24.1",
|
|
17
17
|
]
|
|
18
|
-
version = "0.0.1.
|
|
18
|
+
version = "0.0.1.dev31"
|
|
19
19
|
|
|
20
20
|
[project.license]
|
|
21
21
|
text = "LGPL-3.0-or-later"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '0.0.1.dev31'
|
|
@@ -47,8 +47,7 @@ def _init_services(logger: logging.Logger) -> tuple[Client, API, GlobalConfigura
|
|
|
47
47
|
cached_dirs = porringer.cache.list_directories()
|
|
48
48
|
|
|
49
49
|
logger.info(
|
|
50
|
-
'Synodic Client v%s started (channel: %s, source: %s, '
|
|
51
|
-
'config_fields_set: %s, cached_projects: %d)',
|
|
50
|
+
'Synodic Client v%s started (channel: %s, source: %s, config_fields_set: %s, cached_projects: %d)',
|
|
52
51
|
client.version,
|
|
53
52
|
update_config.channel.name,
|
|
54
53
|
update_config.repo_url,
|
|
@@ -51,6 +51,7 @@ from synodic_client.application.theme import (
|
|
|
51
51
|
PLUGIN_SECTION_SPACING,
|
|
52
52
|
PLUGIN_TOGGLE_STYLE,
|
|
53
53
|
PLUGIN_UPDATE_STYLE,
|
|
54
|
+
SETTINGS_GEAR_STYLE,
|
|
54
55
|
)
|
|
55
56
|
from synodic_client.config import GlobalConfiguration, save_config
|
|
56
57
|
|
|
@@ -794,6 +795,9 @@ class ProjectsView(QWidget):
|
|
|
794
795
|
class MainWindow(QMainWindow):
|
|
795
796
|
"""Main window for the application."""
|
|
796
797
|
|
|
798
|
+
settings_requested = Signal()
|
|
799
|
+
"""Emitted when the user clicks the settings gear button."""
|
|
800
|
+
|
|
797
801
|
_tabs: QTabWidget | None = None
|
|
798
802
|
_plugins_view: PluginsView | None = None
|
|
799
803
|
_projects_view: ProjectsView | None = None
|
|
@@ -837,6 +841,13 @@ class MainWindow(QMainWindow):
|
|
|
837
841
|
self._plugins_view = PluginsView(self._porringer, self._config, self)
|
|
838
842
|
self._tabs.addTab(self._plugins_view, 'Plugins')
|
|
839
843
|
|
|
844
|
+
gear_btn = QPushButton('\u2699')
|
|
845
|
+
gear_btn.setStyleSheet(SETTINGS_GEAR_STYLE)
|
|
846
|
+
gear_btn.setToolTip('Settings')
|
|
847
|
+
gear_btn.setFlat(True)
|
|
848
|
+
gear_btn.clicked.connect(self.settings_requested.emit)
|
|
849
|
+
self._tabs.setCornerWidget(gear_btn)
|
|
850
|
+
|
|
840
851
|
self.setCentralWidget(self._tabs)
|
|
841
852
|
|
|
842
853
|
# Paint the window immediately, then refresh data asynchronously
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""Settings window for the Synodic Client application.
|
|
2
|
+
|
|
3
|
+
Provides a single-page window with grouped sections for all application
|
|
4
|
+
settings. Quick-access items (Channel, Check for Updates) remain in the
|
|
5
|
+
tray menu; the full set is available here.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import sys
|
|
10
|
+
from collections.abc import Iterator
|
|
11
|
+
from contextlib import contextmanager
|
|
12
|
+
|
|
13
|
+
from PySide6.QtCore import QUrl, Signal
|
|
14
|
+
from PySide6.QtGui import QDesktopServices
|
|
15
|
+
from PySide6.QtWidgets import (
|
|
16
|
+
QCheckBox,
|
|
17
|
+
QComboBox,
|
|
18
|
+
QFileDialog,
|
|
19
|
+
QHBoxLayout,
|
|
20
|
+
QLabel,
|
|
21
|
+
QLineEdit,
|
|
22
|
+
QMainWindow,
|
|
23
|
+
QPushButton,
|
|
24
|
+
QScrollArea,
|
|
25
|
+
QSpinBox,
|
|
26
|
+
QVBoxLayout,
|
|
27
|
+
QWidget,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
from synodic_client.application.icon import app_icon
|
|
31
|
+
from synodic_client.application.screen.card import CardFrame
|
|
32
|
+
from synodic_client.application.theme import SETTINGS_WINDOW_MIN_SIZE
|
|
33
|
+
from synodic_client.config import GlobalConfiguration, save_config
|
|
34
|
+
from synodic_client.logging import log_path
|
|
35
|
+
from synodic_client.startup import is_startup_registered, register_startup, remove_startup
|
|
36
|
+
from synodic_client.updater import (
|
|
37
|
+
DEFAULT_AUTO_UPDATE_INTERVAL_MINUTES,
|
|
38
|
+
DEFAULT_TOOL_UPDATE_INTERVAL_MINUTES,
|
|
39
|
+
GITHUB_REPO_URL,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
logger = logging.getLogger(__name__)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class SettingsWindow(QMainWindow):
|
|
46
|
+
"""Application settings window with grouped card sections.
|
|
47
|
+
|
|
48
|
+
All controls persist changes immediately via :func:`save_config` and
|
|
49
|
+
emit :attr:`settings_changed` so that the tray and updater can react.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
settings_changed = Signal()
|
|
53
|
+
"""Emitted whenever a setting is changed and persisted."""
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
config: GlobalConfiguration,
|
|
58
|
+
parent: QWidget | None = None,
|
|
59
|
+
) -> None:
|
|
60
|
+
"""Initialise the settings window.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
config: The shared global configuration object.
|
|
64
|
+
parent: Optional parent widget.
|
|
65
|
+
"""
|
|
66
|
+
super().__init__(parent)
|
|
67
|
+
self._config = config
|
|
68
|
+
self.setWindowTitle('Synodic Settings')
|
|
69
|
+
self.setMinimumSize(*SETTINGS_WINDOW_MIN_SIZE)
|
|
70
|
+
self.setWindowIcon(app_icon())
|
|
71
|
+
self._init_ui()
|
|
72
|
+
|
|
73
|
+
# ------------------------------------------------------------------
|
|
74
|
+
# UI construction
|
|
75
|
+
# ------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
def _init_ui(self) -> None:
|
|
78
|
+
"""Build the scrollable settings layout."""
|
|
79
|
+
scroll = QScrollArea()
|
|
80
|
+
scroll.setWidgetResizable(True)
|
|
81
|
+
scroll.setFrameShape(QScrollArea.Shape.NoFrame)
|
|
82
|
+
|
|
83
|
+
container = QWidget()
|
|
84
|
+
layout = QVBoxLayout(container)
|
|
85
|
+
layout.setContentsMargins(16, 16, 16, 16)
|
|
86
|
+
layout.setSpacing(12)
|
|
87
|
+
|
|
88
|
+
layout.addWidget(self._build_updates_section())
|
|
89
|
+
layout.addWidget(self._build_startup_section())
|
|
90
|
+
layout.addWidget(self._build_advanced_section())
|
|
91
|
+
layout.addStretch()
|
|
92
|
+
|
|
93
|
+
scroll.setWidget(container)
|
|
94
|
+
self.setCentralWidget(scroll)
|
|
95
|
+
|
|
96
|
+
def _build_updates_section(self) -> CardFrame:
|
|
97
|
+
"""Construct the *Updates* settings card."""
|
|
98
|
+
card = CardFrame('Updates')
|
|
99
|
+
content = card.content_layout
|
|
100
|
+
|
|
101
|
+
# Channel
|
|
102
|
+
row = QHBoxLayout()
|
|
103
|
+
label = QLabel('Channel')
|
|
104
|
+
label.setMinimumWidth(160)
|
|
105
|
+
row.addWidget(label)
|
|
106
|
+
self._channel_combo = QComboBox()
|
|
107
|
+
self._channel_combo.addItems(['Stable', 'Development'])
|
|
108
|
+
self._channel_combo.currentIndexChanged.connect(self._on_channel_changed)
|
|
109
|
+
row.addWidget(self._channel_combo)
|
|
110
|
+
row.addStretch()
|
|
111
|
+
content.addLayout(row)
|
|
112
|
+
|
|
113
|
+
# Update Source
|
|
114
|
+
row = QHBoxLayout()
|
|
115
|
+
label = QLabel('Update source')
|
|
116
|
+
label.setMinimumWidth(160)
|
|
117
|
+
row.addWidget(label)
|
|
118
|
+
self._source_edit = QLineEdit()
|
|
119
|
+
self._source_edit.setPlaceholderText(GITHUB_REPO_URL)
|
|
120
|
+
self._source_edit.editingFinished.connect(self._on_source_changed)
|
|
121
|
+
row.addWidget(self._source_edit, 1)
|
|
122
|
+
browse_btn = QPushButton('Browse\u2026')
|
|
123
|
+
browse_btn.clicked.connect(self._on_browse_source)
|
|
124
|
+
row.addWidget(browse_btn)
|
|
125
|
+
content.addLayout(row)
|
|
126
|
+
|
|
127
|
+
# Auto-update interval
|
|
128
|
+
row = QHBoxLayout()
|
|
129
|
+
label = QLabel('App update interval (min)')
|
|
130
|
+
label.setMinimumWidth(160)
|
|
131
|
+
row.addWidget(label)
|
|
132
|
+
self._auto_update_spin = QSpinBox()
|
|
133
|
+
self._auto_update_spin.setRange(0, 1440)
|
|
134
|
+
self._auto_update_spin.setSpecialValueText('Disabled')
|
|
135
|
+
self._auto_update_spin.valueChanged.connect(self._on_auto_update_interval_changed)
|
|
136
|
+
row.addWidget(self._auto_update_spin)
|
|
137
|
+
row.addStretch()
|
|
138
|
+
content.addLayout(row)
|
|
139
|
+
|
|
140
|
+
# Tool-update interval
|
|
141
|
+
row = QHBoxLayout()
|
|
142
|
+
label = QLabel('Tool update interval (min)')
|
|
143
|
+
label.setMinimumWidth(160)
|
|
144
|
+
row.addWidget(label)
|
|
145
|
+
self._tool_update_spin = QSpinBox()
|
|
146
|
+
self._tool_update_spin.setRange(0, 1440)
|
|
147
|
+
self._tool_update_spin.setSpecialValueText('Disabled')
|
|
148
|
+
self._tool_update_spin.valueChanged.connect(self._on_tool_update_interval_changed)
|
|
149
|
+
row.addWidget(self._tool_update_spin)
|
|
150
|
+
row.addStretch()
|
|
151
|
+
content.addLayout(row)
|
|
152
|
+
|
|
153
|
+
# Detect updates during previews
|
|
154
|
+
self._detect_updates_check = QCheckBox('Detect updates during previews')
|
|
155
|
+
self._detect_updates_check.toggled.connect(self._on_detect_updates_changed)
|
|
156
|
+
content.addWidget(self._detect_updates_check)
|
|
157
|
+
|
|
158
|
+
return card
|
|
159
|
+
|
|
160
|
+
def _build_startup_section(self) -> CardFrame:
|
|
161
|
+
"""Construct the *Startup* settings card."""
|
|
162
|
+
card = CardFrame('Startup')
|
|
163
|
+
self._auto_start_check = QCheckBox('Start with Windows')
|
|
164
|
+
self._auto_start_check.toggled.connect(self._on_auto_start_changed)
|
|
165
|
+
card.content_layout.addWidget(self._auto_start_check)
|
|
166
|
+
return card
|
|
167
|
+
|
|
168
|
+
def _build_advanced_section(self) -> CardFrame:
|
|
169
|
+
"""Construct the *Advanced* settings card."""
|
|
170
|
+
card = CardFrame('Advanced')
|
|
171
|
+
row = QHBoxLayout()
|
|
172
|
+
open_log_btn = QPushButton('Open Log\u2026')
|
|
173
|
+
open_log_btn.clicked.connect(self._open_log)
|
|
174
|
+
row.addWidget(open_log_btn)
|
|
175
|
+
row.addStretch()
|
|
176
|
+
card.content_layout.addLayout(row)
|
|
177
|
+
return card
|
|
178
|
+
|
|
179
|
+
# ------------------------------------------------------------------
|
|
180
|
+
# Public API
|
|
181
|
+
# ------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
def sync_from_config(self) -> None:
|
|
184
|
+
"""Synchronize all controls from the current configuration.
|
|
185
|
+
|
|
186
|
+
Signals are blocked during the update to prevent feedback loops.
|
|
187
|
+
"""
|
|
188
|
+
config = self._config
|
|
189
|
+
|
|
190
|
+
with self._block_signals():
|
|
191
|
+
# Channel: index 0 = Stable, 1 = Development
|
|
192
|
+
is_dev = config.update_channel == 'dev'
|
|
193
|
+
self._channel_combo.setCurrentIndex(1 if is_dev else 0)
|
|
194
|
+
|
|
195
|
+
# Update source
|
|
196
|
+
self._source_edit.setText(config.update_source or '')
|
|
197
|
+
|
|
198
|
+
# Intervals
|
|
199
|
+
auto_interval = config.auto_update_interval_minutes
|
|
200
|
+
self._auto_update_spin.setValue(
|
|
201
|
+
auto_interval if auto_interval is not None else DEFAULT_AUTO_UPDATE_INTERVAL_MINUTES,
|
|
202
|
+
)
|
|
203
|
+
tool_interval = config.tool_update_interval_minutes
|
|
204
|
+
self._tool_update_spin.setValue(
|
|
205
|
+
tool_interval if tool_interval is not None else DEFAULT_TOOL_UPDATE_INTERVAL_MINUTES,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# Checkboxes
|
|
209
|
+
self._detect_updates_check.setChecked(config.detect_updates)
|
|
210
|
+
self._auto_start_check.setChecked(is_startup_registered())
|
|
211
|
+
|
|
212
|
+
def show(self) -> None:
|
|
213
|
+
"""Sync controls from config, then show the window."""
|
|
214
|
+
self.sync_from_config()
|
|
215
|
+
super().show()
|
|
216
|
+
self.raise_()
|
|
217
|
+
self.activateWindow()
|
|
218
|
+
|
|
219
|
+
# ------------------------------------------------------------------
|
|
220
|
+
# Callbacks
|
|
221
|
+
# ------------------------------------------------------------------
|
|
222
|
+
|
|
223
|
+
def _persist(self) -> None:
|
|
224
|
+
"""Save config and notify listeners."""
|
|
225
|
+
save_config(self._config)
|
|
226
|
+
self.settings_changed.emit()
|
|
227
|
+
|
|
228
|
+
@contextmanager
|
|
229
|
+
def _block_signals(self) -> Iterator[None]:
|
|
230
|
+
"""Temporarily block signals on all settings controls."""
|
|
231
|
+
widgets = (
|
|
232
|
+
self._channel_combo,
|
|
233
|
+
self._source_edit,
|
|
234
|
+
self._auto_update_spin,
|
|
235
|
+
self._tool_update_spin,
|
|
236
|
+
self._detect_updates_check,
|
|
237
|
+
self._auto_start_check,
|
|
238
|
+
)
|
|
239
|
+
for w in widgets:
|
|
240
|
+
w.blockSignals(True)
|
|
241
|
+
try:
|
|
242
|
+
yield
|
|
243
|
+
finally:
|
|
244
|
+
for w in widgets:
|
|
245
|
+
w.blockSignals(False)
|
|
246
|
+
|
|
247
|
+
def _on_channel_changed(self, index: int) -> None:
|
|
248
|
+
self._config.update_channel = 'dev' if index == 1 else 'stable'
|
|
249
|
+
self._persist()
|
|
250
|
+
|
|
251
|
+
def _on_source_changed(self) -> None:
|
|
252
|
+
text = self._source_edit.text().strip()
|
|
253
|
+
self._config.update_source = text or None
|
|
254
|
+
self._persist()
|
|
255
|
+
|
|
256
|
+
def _on_browse_source(self) -> None:
|
|
257
|
+
path = QFileDialog.getExistingDirectory(self, 'Select Releases Directory')
|
|
258
|
+
if path:
|
|
259
|
+
self._source_edit.setText(path)
|
|
260
|
+
self._on_source_changed()
|
|
261
|
+
|
|
262
|
+
def _on_auto_update_interval_changed(self, value: int) -> None:
|
|
263
|
+
self._config.auto_update_interval_minutes = value
|
|
264
|
+
self._persist()
|
|
265
|
+
|
|
266
|
+
def _on_tool_update_interval_changed(self, value: int) -> None:
|
|
267
|
+
self._config.tool_update_interval_minutes = value
|
|
268
|
+
self._persist()
|
|
269
|
+
|
|
270
|
+
def _on_detect_updates_changed(self, checked: bool) -> None:
|
|
271
|
+
self._config.detect_updates = checked
|
|
272
|
+
self._persist()
|
|
273
|
+
|
|
274
|
+
def _on_auto_start_changed(self, checked: bool) -> None:
|
|
275
|
+
self._config.auto_start = checked
|
|
276
|
+
save_config(self._config)
|
|
277
|
+
if checked:
|
|
278
|
+
register_startup(sys.executable)
|
|
279
|
+
else:
|
|
280
|
+
remove_startup()
|
|
281
|
+
self.settings_changed.emit()
|
|
282
|
+
|
|
283
|
+
@staticmethod
|
|
284
|
+
def _open_log() -> None:
|
|
285
|
+
"""Open the log file in the system's default editor."""
|
|
286
|
+
path = log_path()
|
|
287
|
+
if not path.exists():
|
|
288
|
+
path.touch()
|
|
289
|
+
QDesktopServices.openUrl(QUrl.fromLocalFile(str(path)))
|
{synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/synodic_client/application/screen/tray.py
RENAMED
|
@@ -2,43 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import logging
|
|
5
|
-
import sys
|
|
6
5
|
from pathlib import Path
|
|
7
6
|
|
|
8
7
|
from porringer.api import API
|
|
9
8
|
from porringer.schema import SetupParameters, SyncStrategy
|
|
10
|
-
from PySide6.QtCore import QThread, QTimer,
|
|
11
|
-
from PySide6.QtGui import QAction
|
|
9
|
+
from PySide6.QtCore import QThread, QTimer, Signal
|
|
10
|
+
from PySide6.QtGui import QAction
|
|
12
11
|
from PySide6.QtWidgets import (
|
|
13
12
|
QApplication,
|
|
14
|
-
QDialog,
|
|
15
|
-
QFileDialog,
|
|
16
|
-
QHBoxLayout,
|
|
17
|
-
QLabel,
|
|
18
|
-
QLineEdit,
|
|
19
13
|
QMenu,
|
|
20
14
|
QMessageBox,
|
|
21
15
|
QProgressDialog,
|
|
22
|
-
QPushButton,
|
|
23
16
|
QSystemTrayIcon,
|
|
24
|
-
QVBoxLayout,
|
|
25
|
-
QWidget,
|
|
26
17
|
)
|
|
27
18
|
|
|
28
19
|
from synodic_client.application.icon import app_icon
|
|
29
20
|
from synodic_client.application.screen.screen import MainWindow
|
|
30
|
-
from synodic_client.application.
|
|
21
|
+
from synodic_client.application.screen.settings import SettingsWindow
|
|
31
22
|
from synodic_client.client import Client
|
|
32
|
-
from synodic_client.config import GlobalConfiguration
|
|
33
|
-
from synodic_client.logging import log_path
|
|
23
|
+
from synodic_client.config import GlobalConfiguration
|
|
34
24
|
from synodic_client.resolution import (
|
|
35
25
|
resolve_config,
|
|
36
26
|
resolve_enabled_plugins,
|
|
37
27
|
resolve_update_config,
|
|
38
28
|
update_and_resolve,
|
|
39
29
|
)
|
|
40
|
-
from synodic_client.
|
|
41
|
-
from synodic_client.updater import GITHUB_REPO_URL, UpdateChannel, UpdateInfo
|
|
30
|
+
from synodic_client.updater import UpdateChannel, UpdateInfo
|
|
42
31
|
|
|
43
32
|
logger = logging.getLogger(__name__)
|
|
44
33
|
|
|
@@ -138,60 +127,6 @@ class ToolUpdateWorker(QThread):
|
|
|
138
127
|
pass # consume events to completion
|
|
139
128
|
|
|
140
129
|
|
|
141
|
-
class UpdateSourceDialog(QDialog):
|
|
142
|
-
"""Dialog for editing the Velopack update source URL or local path."""
|
|
143
|
-
|
|
144
|
-
def __init__(self, current_source: str | None, parent: QWidget | None = None) -> None:
|
|
145
|
-
"""Initialise the dialog.
|
|
146
|
-
|
|
147
|
-
Args:
|
|
148
|
-
current_source: The current update source value (may be ``None``).
|
|
149
|
-
parent: Optional parent widget.
|
|
150
|
-
"""
|
|
151
|
-
super().__init__(parent)
|
|
152
|
-
self.setWindowTitle('Update Source')
|
|
153
|
-
self.setMinimumWidth(UPDATE_SOURCE_DIALOG_MIN_WIDTH)
|
|
154
|
-
|
|
155
|
-
layout = QVBoxLayout(self)
|
|
156
|
-
|
|
157
|
-
label = QLabel(
|
|
158
|
-
'Enter a URL or local path for Velopack releases.\nLeave blank to use the default GitHub source.',
|
|
159
|
-
)
|
|
160
|
-
layout.addWidget(label)
|
|
161
|
-
|
|
162
|
-
self._source_edit = QLineEdit(current_source or '')
|
|
163
|
-
self._source_edit.setPlaceholderText(GITHUB_REPO_URL)
|
|
164
|
-
|
|
165
|
-
browse_button = QPushButton('Browse...')
|
|
166
|
-
browse_button.clicked.connect(self._browse)
|
|
167
|
-
|
|
168
|
-
row = QHBoxLayout()
|
|
169
|
-
row.addWidget(self._source_edit)
|
|
170
|
-
row.addWidget(browse_button)
|
|
171
|
-
layout.addLayout(row)
|
|
172
|
-
|
|
173
|
-
button_row = QHBoxLayout()
|
|
174
|
-
ok_button = QPushButton('OK')
|
|
175
|
-
cancel_button = QPushButton('Cancel')
|
|
176
|
-
button_row.addStretch()
|
|
177
|
-
button_row.addWidget(ok_button)
|
|
178
|
-
button_row.addWidget(cancel_button)
|
|
179
|
-
layout.addLayout(button_row)
|
|
180
|
-
|
|
181
|
-
ok_button.clicked.connect(self.accept)
|
|
182
|
-
cancel_button.clicked.connect(self.reject)
|
|
183
|
-
|
|
184
|
-
def _browse(self) -> None:
|
|
185
|
-
path = QFileDialog.getExistingDirectory(self, 'Select Releases Directory')
|
|
186
|
-
if path:
|
|
187
|
-
self._source_edit.setText(path)
|
|
188
|
-
|
|
189
|
-
@property
|
|
190
|
-
def source(self) -> str | None:
|
|
191
|
-
"""Return the trimmed source text, or ``None`` if blank."""
|
|
192
|
-
return self._source_edit.text().strip() or None
|
|
193
|
-
|
|
194
|
-
|
|
195
130
|
class TrayScreen:
|
|
196
131
|
"""Tray screen for the application."""
|
|
197
132
|
|
|
@@ -231,6 +166,13 @@ class TrayScreen:
|
|
|
231
166
|
|
|
232
167
|
self._build_menu(app, window)
|
|
233
168
|
|
|
169
|
+
# Settings window (created once, shown/hidden on demand)
|
|
170
|
+
self._settings_window = SettingsWindow(self._resolve_config())
|
|
171
|
+
self._settings_window.settings_changed.connect(self._on_settings_changed)
|
|
172
|
+
|
|
173
|
+
# MainWindow gear button → open settings
|
|
174
|
+
window.settings_requested.connect(self._show_settings)
|
|
175
|
+
|
|
234
176
|
# Periodic auto-update checking
|
|
235
177
|
self._auto_update_timer: QTimer | None = None
|
|
236
178
|
self._start_auto_update_timer()
|
|
@@ -253,24 +195,15 @@ class TrayScreen:
|
|
|
253
195
|
self.menu.addAction(self.open_action)
|
|
254
196
|
self.open_action.triggered.connect(window.show)
|
|
255
197
|
|
|
256
|
-
|
|
257
|
-
self.settings_menu = QMenu('Settings', self.menu)
|
|
258
|
-
self.menu.addMenu(self.settings_menu)
|
|
198
|
+
self.menu.addSeparator()
|
|
259
199
|
|
|
260
|
-
self.update_action = QAction('Check for Updates...', self.
|
|
200
|
+
self.update_action = QAction('Check for Updates...', self.menu)
|
|
261
201
|
self.update_action.triggered.connect(self._on_check_updates)
|
|
262
|
-
self.
|
|
263
|
-
|
|
264
|
-
self.settings_menu.addSeparator()
|
|
265
|
-
|
|
266
|
-
# Update Source action
|
|
267
|
-
self.update_source_action = QAction('Update Source...', self.settings_menu)
|
|
268
|
-
self.update_source_action.triggered.connect(self._on_update_source)
|
|
269
|
-
self.settings_menu.addAction(self.update_source_action)
|
|
202
|
+
self.menu.addAction(self.update_action)
|
|
270
203
|
|
|
271
204
|
# Update Channel submenu
|
|
272
|
-
self.channel_menu = QMenu('Update Channel', self.
|
|
273
|
-
self.
|
|
205
|
+
self.channel_menu = QMenu('Update Channel', self.menu)
|
|
206
|
+
self.menu.addMenu(self.channel_menu)
|
|
274
207
|
|
|
275
208
|
self._channel_stable_action = QAction('Stable', self.channel_menu)
|
|
276
209
|
self._channel_stable_action.setCheckable(True)
|
|
@@ -285,20 +218,11 @@ class TrayScreen:
|
|
|
285
218
|
# Set initial channel check state from config
|
|
286
219
|
self._sync_channel_checks()
|
|
287
220
|
|
|
288
|
-
self.
|
|
289
|
-
|
|
290
|
-
# Start with Windows toggle
|
|
291
|
-
self._auto_start_action = QAction('Start with Windows', self.settings_menu)
|
|
292
|
-
self._auto_start_action.setCheckable(True)
|
|
293
|
-
self._auto_start_action.setChecked(is_startup_registered())
|
|
294
|
-
self._auto_start_action.triggered.connect(self._on_auto_start_toggled)
|
|
295
|
-
self.settings_menu.addAction(self._auto_start_action)
|
|
296
|
-
|
|
297
|
-
self.settings_menu.addSeparator()
|
|
221
|
+
self.menu.addSeparator()
|
|
298
222
|
|
|
299
|
-
self.
|
|
300
|
-
self.
|
|
301
|
-
self.
|
|
223
|
+
self.settings_action = QAction('Settings\u2026', self.menu)
|
|
224
|
+
self.settings_action.triggered.connect(self._show_settings)
|
|
225
|
+
self.menu.addAction(self.settings_action)
|
|
302
226
|
|
|
303
227
|
self.menu.addSeparator()
|
|
304
228
|
|
|
@@ -310,14 +234,6 @@ class TrayScreen:
|
|
|
310
234
|
|
|
311
235
|
# -- Config helpers --
|
|
312
236
|
|
|
313
|
-
@staticmethod
|
|
314
|
-
def _open_log() -> None:
|
|
315
|
-
"""Open the log file in the system's default editor."""
|
|
316
|
-
path = log_path()
|
|
317
|
-
if not path.exists():
|
|
318
|
-
path.touch()
|
|
319
|
-
QDesktopServices.openUrl(QUrl.fromLocalFile(str(path)))
|
|
320
|
-
|
|
321
237
|
def _resolve_config(self) -> GlobalConfiguration:
|
|
322
238
|
"""Return the injected config or resolve from disk."""
|
|
323
239
|
if self._config is not None:
|
|
@@ -376,38 +292,26 @@ class TrayScreen:
|
|
|
376
292
|
self._window.raise_()
|
|
377
293
|
self._window.activateWindow()
|
|
378
294
|
|
|
379
|
-
def
|
|
380
|
-
"""
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
parent = self._window if self._window.isVisible() else None
|
|
384
|
-
dialog = UpdateSourceDialog(config.update_source, parent)
|
|
295
|
+
def _show_settings(self) -> None:
|
|
296
|
+
"""Show the settings window."""
|
|
297
|
+
self._settings_window.show()
|
|
385
298
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
299
|
+
def _on_settings_changed(self) -> None:
|
|
300
|
+
"""React to a change made in the settings window."""
|
|
301
|
+
config = self._resolve_config()
|
|
302
|
+
self._reinitialize_updater(config)
|
|
303
|
+
self._sync_channel_checks()
|
|
390
304
|
|
|
391
305
|
def _on_channel_changed(self, channel: UpdateChannel) -> None:
|
|
392
|
-
"""Handle channel selection change."""
|
|
306
|
+
"""Handle channel selection change from the tray submenu."""
|
|
393
307
|
config = self._resolve_config()
|
|
394
308
|
config.update_channel = 'dev' if channel == UpdateChannel.DEVELOPMENT else 'stable'
|
|
395
309
|
logger.info('Update channel changed to: %s', config.update_channel)
|
|
396
310
|
self._sync_channel_checks()
|
|
397
311
|
self._reinitialize_updater(config)
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
config = self._resolve_config()
|
|
402
|
-
config.auto_start = checked
|
|
403
|
-
save_config(config)
|
|
404
|
-
|
|
405
|
-
if checked:
|
|
406
|
-
register_startup(sys.executable)
|
|
407
|
-
else:
|
|
408
|
-
remove_startup()
|
|
409
|
-
|
|
410
|
-
logger.info('Auto-startup %s', 'enabled' if checked else 'disabled')
|
|
312
|
+
# Keep the settings window in sync if it is visible
|
|
313
|
+
if self._settings_window.isVisible():
|
|
314
|
+
self._settings_window.sync_from_config()
|
|
411
315
|
|
|
412
316
|
def _reinitialize_updater(self, config: GlobalConfiguration) -> None:
|
|
413
317
|
"""Re-derive update settings and restart the updater and timers."""
|
{synodic_client-0.0.1.dev30 → synodic_client-0.0.1.dev31}/synodic_client/application/theme.py
RENAMED
|
@@ -9,7 +9,6 @@ code focused on layout and behaviour rather than pixel tweaking.
|
|
|
9
9
|
# ---------------------------------------------------------------------------
|
|
10
10
|
INSTALL_PREVIEW_MIN_SIZE = (650, 400)
|
|
11
11
|
MAIN_WINDOW_MIN_SIZE = (600, 400)
|
|
12
|
-
UPDATE_SOURCE_DIALOG_MIN_WIDTH = 450
|
|
13
12
|
|
|
14
13
|
# ---------------------------------------------------------------------------
|
|
15
14
|
# Layout margins (left, top, right, bottom)
|
|
@@ -275,3 +274,15 @@ METADATA_SKELETON_STYLE = (
|
|
|
275
274
|
'}'
|
|
276
275
|
)
|
|
277
276
|
"""Muted card frame used as the metadata placeholder during loading."""
|
|
277
|
+
|
|
278
|
+
# ---------------------------------------------------------------------------
|
|
279
|
+
# Settings window
|
|
280
|
+
# ---------------------------------------------------------------------------
|
|
281
|
+
SETTINGS_WINDOW_MIN_SIZE = (500, 450)
|
|
282
|
+
"""Minimum size (width, height) for the Settings window."""
|
|
283
|
+
|
|
284
|
+
SETTINGS_GEAR_STYLE = (
|
|
285
|
+
'QPushButton { border: none; font-size: 16px; padding: 2px 6px; }'
|
|
286
|
+
'QPushButton:hover { background: palette(midlight); border-radius: 3px; }'
|
|
287
|
+
)
|
|
288
|
+
"""Gear button style for the MainWindow tab corner widget."""
|
|
@@ -21,6 +21,7 @@ from synodic_client.updater import (
|
|
|
21
21
|
GITHUB_REPO_URL,
|
|
22
22
|
UpdateChannel,
|
|
23
23
|
UpdateConfig,
|
|
24
|
+
github_release_asset_url,
|
|
24
25
|
)
|
|
25
26
|
|
|
26
27
|
logger = logging.getLogger(__name__)
|
|
@@ -84,7 +85,10 @@ def resolve_update_config(config: GlobalConfiguration) -> UpdateConfig:
|
|
|
84
85
|
else:
|
|
85
86
|
channel = UpdateChannel.DEVELOPMENT if is_dev else UpdateChannel.STABLE
|
|
86
87
|
|
|
87
|
-
repo_url =
|
|
88
|
+
repo_url = github_release_asset_url(
|
|
89
|
+
config.update_source or GITHUB_REPO_URL,
|
|
90
|
+
channel,
|
|
91
|
+
)
|
|
88
92
|
|
|
89
93
|
interval = config.auto_update_interval_minutes
|
|
90
94
|
if interval is None:
|