voidremote 1.0.0__py3-none-any.whl
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.
- voidremote/__init__.py +31 -0
- voidremote/adb/__init__.py +28 -0
- voidremote/adb/client.py +385 -0
- voidremote/adb/device_parser.py +286 -0
- voidremote/cli/__init__.py +5 -0
- voidremote/cli/main.py +987 -0
- voidremote/config/__init__.py +37 -0
- voidremote/config/settings.py +203 -0
- voidremote/controllers/__init__.py +5 -0
- voidremote/controllers/app_controller.py +222 -0
- voidremote/core/__init__.py +1 -0
- voidremote/core/automation.py +184 -0
- voidremote/models/__init__.py +31 -0
- voidremote/models/device.py +263 -0
- voidremote/network/__init__.py +1 -0
- voidremote/network/discovery.py +116 -0
- voidremote/services/__init__.py +13 -0
- voidremote/services/device_service.py +340 -0
- voidremote/services/input_service.py +181 -0
- voidremote/services/monitor_service.py +198 -0
- voidremote/ui/__init__.py +1 -0
- voidremote/ui/app.py +70 -0
- voidremote/ui/dialogs/__init__.py +6 -0
- voidremote/ui/dialogs/connect_dialog.py +149 -0
- voidremote/ui/dialogs/pair_dialog.py +189 -0
- voidremote/ui/main_window.py +371 -0
- voidremote/ui/theme.py +529 -0
- voidremote/ui/views/__init__.py +15 -0
- voidremote/ui/views/dashboard_view.py +233 -0
- voidremote/ui/views/files_view.py +305 -0
- voidremote/ui/views/monitor_view.py +236 -0
- voidremote/ui/views/settings_view.py +257 -0
- voidremote/ui/views/shell_view.py +234 -0
- voidremote/ui/widgets/__init__.py +14 -0
- voidremote/ui/widgets/device_card.py +259 -0
- voidremote/ui/widgets/log_view.py +159 -0
- voidremote/ui/widgets/metric_gauge.py +90 -0
- voidremote/utils/__init__.py +28 -0
- voidremote/utils/logging.py +120 -0
- voidremote/utils/security.py +216 -0
- voidremote-1.0.0.dist-info/METADATA +578 -0
- voidremote-1.0.0.dist-info/RECORD +46 -0
- voidremote-1.0.0.dist-info/WHEEL +5 -0
- voidremote-1.0.0.dist-info/entry_points.txt +5 -0
- voidremote-1.0.0.dist-info/licenses/LICENSE +21 -0
- voidremote-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""Real-time device monitoring view with live gauges and graphs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import deque
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from PySide6.QtCore import Qt, QTimer, Slot
|
|
9
|
+
from PySide6.QtWidgets import (
|
|
10
|
+
QComboBox,
|
|
11
|
+
QFrame,
|
|
12
|
+
QGridLayout,
|
|
13
|
+
QHBoxLayout,
|
|
14
|
+
QLabel,
|
|
15
|
+
QProgressBar,
|
|
16
|
+
QVBoxLayout,
|
|
17
|
+
QWidget,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from voidremote.services.monitor_service import DeviceSnapshot
|
|
21
|
+
from voidremote.ui.theme import Colors
|
|
22
|
+
from voidremote.ui.widgets.metric_gauge import MetricGauge
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SparklineWidget(QWidget):
|
|
26
|
+
"""Minimal sparkline chart for a time-series metric."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, color: str = Colors.ACCENT, parent: Optional[QWidget] = None) -> None:
|
|
29
|
+
super().__init__(parent)
|
|
30
|
+
self._color = color
|
|
31
|
+
self._data: deque[float] = deque(maxlen=60)
|
|
32
|
+
self.setMinimumHeight(60)
|
|
33
|
+
|
|
34
|
+
def push(self, value: float) -> None:
|
|
35
|
+
self._data.append(value)
|
|
36
|
+
self.update()
|
|
37
|
+
|
|
38
|
+
def paintEvent(self, event: object) -> None: # type: ignore[override]
|
|
39
|
+
if len(self._data) < 2:
|
|
40
|
+
return
|
|
41
|
+
from PySide6.QtGui import QPainter, QPen, QColor, QPainterPath
|
|
42
|
+
painter = QPainter(self)
|
|
43
|
+
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
|
44
|
+
|
|
45
|
+
w, h = self.width(), self.height()
|
|
46
|
+
data = list(self._data)
|
|
47
|
+
max_v = max(data) or 1.0
|
|
48
|
+
step = w / max(len(data) - 1, 1)
|
|
49
|
+
|
|
50
|
+
path = QPainterPath()
|
|
51
|
+
for i, v in enumerate(data):
|
|
52
|
+
x = i * step
|
|
53
|
+
y = h - (v / max_v) * (h - 4) - 2
|
|
54
|
+
if i == 0:
|
|
55
|
+
path.moveTo(x, y)
|
|
56
|
+
else:
|
|
57
|
+
path.lineTo(x, y)
|
|
58
|
+
|
|
59
|
+
pen = QPen(QColor(self._color))
|
|
60
|
+
pen.setWidth(2)
|
|
61
|
+
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
|
|
62
|
+
pen.setJoinStyle(Qt.PenJoinStyle.RoundJoin)
|
|
63
|
+
painter.setPen(pen)
|
|
64
|
+
painter.drawPath(path)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class MetricCard(QFrame):
|
|
68
|
+
"""Card showing a gauge + sparkline + current value label."""
|
|
69
|
+
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
title: str,
|
|
73
|
+
color: str = Colors.ACCENT,
|
|
74
|
+
parent: Optional[QWidget] = None,
|
|
75
|
+
) -> None:
|
|
76
|
+
super().__init__(parent)
|
|
77
|
+
self.setFrameShape(QFrame.Shape.StyledPanel)
|
|
78
|
+
self.setObjectName("DeviceCard")
|
|
79
|
+
self._color = color
|
|
80
|
+
|
|
81
|
+
layout = QVBoxLayout(self)
|
|
82
|
+
layout.setContentsMargins(16, 14, 16, 14)
|
|
83
|
+
layout.setSpacing(8)
|
|
84
|
+
|
|
85
|
+
title_label = QLabel(title)
|
|
86
|
+
title_label.setStyleSheet(
|
|
87
|
+
f"font-size: 13px; font-weight: 600; color: {Colors.TEXT_SECONDARY}; text-transform: uppercase; letter-spacing: 0.5px;"
|
|
88
|
+
)
|
|
89
|
+
layout.addWidget(title_label)
|
|
90
|
+
|
|
91
|
+
self._gauge = MetricGauge(label="", color=color)
|
|
92
|
+
self._gauge.setFixedHeight(110)
|
|
93
|
+
layout.addWidget(self._gauge, alignment=Qt.AlignmentFlag.AlignCenter)
|
|
94
|
+
|
|
95
|
+
self._spark = SparklineWidget(color=color)
|
|
96
|
+
layout.addWidget(self._spark)
|
|
97
|
+
|
|
98
|
+
self._value_label = QLabel("–")
|
|
99
|
+
self._value_label.setStyleSheet(
|
|
100
|
+
f"font-size: 12px; color: {Colors.TEXT_SECONDARY}; text-align: center;"
|
|
101
|
+
)
|
|
102
|
+
self._value_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
103
|
+
layout.addWidget(self._value_label)
|
|
104
|
+
|
|
105
|
+
def update_value(self, value: float, suffix: str = "%", detail: str = "") -> None:
|
|
106
|
+
self._gauge.set_value(value)
|
|
107
|
+
self._gauge.set_suffix(suffix)
|
|
108
|
+
self._spark.push(value)
|
|
109
|
+
self._value_label.setText(detail or f"{value:.1f}{suffix}")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class MonitorView(QWidget):
|
|
113
|
+
"""
|
|
114
|
+
Live device monitoring dashboard.
|
|
115
|
+
|
|
116
|
+
Shows CPU, RAM, Battery, and Temperature in metric cards
|
|
117
|
+
with sparkline history graphs updated by MonitorService.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
def __init__(self, controller: object, parent: Optional[QWidget] = None) -> None:
|
|
121
|
+
super().__init__(parent)
|
|
122
|
+
self._controller = controller
|
|
123
|
+
self._serial: Optional[str] = None
|
|
124
|
+
self._build_ui()
|
|
125
|
+
|
|
126
|
+
def _build_ui(self) -> None:
|
|
127
|
+
root = QVBoxLayout(self)
|
|
128
|
+
root.setContentsMargins(0, 0, 0, 0)
|
|
129
|
+
root.setSpacing(0)
|
|
130
|
+
|
|
131
|
+
# ── Header ──────────────────────────────────────
|
|
132
|
+
header = QHBoxLayout()
|
|
133
|
+
header.setContentsMargins(24, 16, 24, 12)
|
|
134
|
+
header.setSpacing(12)
|
|
135
|
+
|
|
136
|
+
title = QLabel("Monitor")
|
|
137
|
+
title.setStyleSheet(f"font-size: 22px; font-weight: 700; color: {Colors.TEXT_PRIMARY};")
|
|
138
|
+
header.addWidget(title)
|
|
139
|
+
header.addStretch()
|
|
140
|
+
|
|
141
|
+
header.addWidget(QLabel("Device:"))
|
|
142
|
+
self._device_combo = QComboBox()
|
|
143
|
+
self._device_combo.setMinimumWidth(220)
|
|
144
|
+
self._device_combo.currentTextChanged.connect(self._on_device_changed)
|
|
145
|
+
header.addWidget(self._device_combo)
|
|
146
|
+
|
|
147
|
+
header_w = QWidget()
|
|
148
|
+
header_w.setStyleSheet(
|
|
149
|
+
f"background: {Colors.BG_SURFACE}; border-bottom: 1px solid {Colors.BORDER};"
|
|
150
|
+
)
|
|
151
|
+
header_w.setLayout(header)
|
|
152
|
+
root.addWidget(header_w)
|
|
153
|
+
|
|
154
|
+
# ── Metric cards grid ────────────────────────────
|
|
155
|
+
content = QWidget()
|
|
156
|
+
content.setStyleSheet(f"background: {Colors.BG_BASE};")
|
|
157
|
+
grid = QGridLayout(content)
|
|
158
|
+
grid.setContentsMargins(24, 20, 24, 20)
|
|
159
|
+
grid.setSpacing(16)
|
|
160
|
+
|
|
161
|
+
self._cpu_card = MetricCard("CPU Usage", Colors.ACCENT)
|
|
162
|
+
self._ram_card = MetricCard("RAM Usage", Colors.INFO)
|
|
163
|
+
self._batt_card = MetricCard("Battery", Colors.SUCCESS)
|
|
164
|
+
self._temp_card = MetricCard("Temperature", Colors.WARNING)
|
|
165
|
+
|
|
166
|
+
grid.addWidget(self._cpu_card, 0, 0)
|
|
167
|
+
grid.addWidget(self._ram_card, 0, 1)
|
|
168
|
+
grid.addWidget(self._batt_card, 1, 0)
|
|
169
|
+
grid.addWidget(self._temp_card, 1, 1)
|
|
170
|
+
|
|
171
|
+
root.addWidget(content, stretch=1)
|
|
172
|
+
|
|
173
|
+
# ── Status bar ───────────────────────────────────
|
|
174
|
+
self._status_bar = QLabel("Select a device to begin monitoring")
|
|
175
|
+
self._status_bar.setStyleSheet(
|
|
176
|
+
f"color: {Colors.TEXT_MUTED}; padding: 8px 24px; "
|
|
177
|
+
f"background: {Colors.BG_SURFACE}; border-top: 1px solid {Colors.BORDER};"
|
|
178
|
+
)
|
|
179
|
+
root.addWidget(self._status_bar)
|
|
180
|
+
|
|
181
|
+
def set_devices(self, devices: list) -> None:
|
|
182
|
+
current = self._device_combo.currentData()
|
|
183
|
+
self._device_combo.blockSignals(True)
|
|
184
|
+
self._device_combo.clear()
|
|
185
|
+
for d in devices:
|
|
186
|
+
self._device_combo.addItem(f"{d.info.display_name} ({d.serial})", userData=d.serial)
|
|
187
|
+
idx = self._device_combo.findData(current)
|
|
188
|
+
if idx >= 0:
|
|
189
|
+
self._device_combo.setCurrentIndex(idx)
|
|
190
|
+
self._device_combo.blockSignals(False)
|
|
191
|
+
|
|
192
|
+
def _on_device_changed(self) -> None:
|
|
193
|
+
serial = self._device_combo.currentData()
|
|
194
|
+
if serial == self._serial:
|
|
195
|
+
return
|
|
196
|
+
if self._serial:
|
|
197
|
+
try:
|
|
198
|
+
self._controller.stop_monitoring(self._serial)
|
|
199
|
+
except Exception:
|
|
200
|
+
pass
|
|
201
|
+
self._serial = serial
|
|
202
|
+
if serial:
|
|
203
|
+
try:
|
|
204
|
+
self._controller.start_monitoring(serial, interval=2.0, callback=self._on_snapshot)
|
|
205
|
+
self._status_bar.setText(f"Monitoring {serial}")
|
|
206
|
+
except Exception as exc:
|
|
207
|
+
self._status_bar.setText(f"Error: {exc}")
|
|
208
|
+
|
|
209
|
+
def _on_snapshot(self, snapshot: DeviceSnapshot) -> None:
|
|
210
|
+
# This runs in a background thread; update UI via queued connection
|
|
211
|
+
from PySide6.QtCore import QMetaObject, Q_ARG
|
|
212
|
+
QMetaObject.invokeMethod(self, "_apply_snapshot", Qt.ConnectionType.QueuedConnection,
|
|
213
|
+
Q_ARG(object, snapshot))
|
|
214
|
+
|
|
215
|
+
@Slot(object)
|
|
216
|
+
def _apply_snapshot(self, snapshot: DeviceSnapshot) -> None:
|
|
217
|
+
self._cpu_card.update_value(snapshot.cpu_usage, "%", f"CPU: {snapshot.cpu_usage:.1f}%")
|
|
218
|
+
self._ram_card.update_value(
|
|
219
|
+
snapshot.ram_usage_percent, "%",
|
|
220
|
+
f"{snapshot.ram_used_mb:.0f} / {snapshot.ram_total_mb:.0f} MB"
|
|
221
|
+
)
|
|
222
|
+
self._batt_card.update_value(
|
|
223
|
+
float(snapshot.battery_level), "%",
|
|
224
|
+
f"{snapshot.battery_level}% {'⚡' if snapshot.battery_is_charging else ''}"
|
|
225
|
+
)
|
|
226
|
+
self._temp_card.update_value(
|
|
227
|
+
snapshot.battery_temperature, "°C",
|
|
228
|
+
f"{snapshot.battery_temperature:.1f}°C"
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
def cleanup(self) -> None:
|
|
232
|
+
if self._serial:
|
|
233
|
+
try:
|
|
234
|
+
self._controller.stop_monitoring(self._serial)
|
|
235
|
+
except Exception:
|
|
236
|
+
pass
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""Settings panel view."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from PySide6.QtCore import Qt
|
|
8
|
+
from PySide6.QtWidgets import (
|
|
9
|
+
QCheckBox,
|
|
10
|
+
QComboBox,
|
|
11
|
+
QFormLayout,
|
|
12
|
+
QGroupBox,
|
|
13
|
+
QHBoxLayout,
|
|
14
|
+
QLabel,
|
|
15
|
+
QLineEdit,
|
|
16
|
+
QPushButton,
|
|
17
|
+
QScrollArea,
|
|
18
|
+
QSlider,
|
|
19
|
+
QSpinBox,
|
|
20
|
+
QVBoxLayout,
|
|
21
|
+
QWidget,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
from voidremote.config.settings import AppSettings, get_settings, save_settings
|
|
25
|
+
from voidremote.ui.theme import Colors
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SettingsView(QWidget):
|
|
29
|
+
"""Full settings panel with save/reset functionality."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, parent: Optional[QWidget] = None) -> None:
|
|
32
|
+
super().__init__(parent)
|
|
33
|
+
self._settings = get_settings()
|
|
34
|
+
self._build_ui()
|
|
35
|
+
self._load_values()
|
|
36
|
+
|
|
37
|
+
def _build_ui(self) -> None:
|
|
38
|
+
root = QVBoxLayout(self)
|
|
39
|
+
root.setContentsMargins(0, 0, 0, 0)
|
|
40
|
+
root.setSpacing(0)
|
|
41
|
+
|
|
42
|
+
# ── Header ──────────────────────────────────────
|
|
43
|
+
header = QHBoxLayout()
|
|
44
|
+
header.setContentsMargins(24, 16, 24, 12)
|
|
45
|
+
|
|
46
|
+
title = QLabel("Settings")
|
|
47
|
+
title.setStyleSheet(f"font-size: 22px; font-weight: 700; color: {Colors.TEXT_PRIMARY};")
|
|
48
|
+
header.addWidget(title)
|
|
49
|
+
header.addStretch()
|
|
50
|
+
|
|
51
|
+
btn_reset = QPushButton("Reset Defaults")
|
|
52
|
+
btn_reset.clicked.connect(self._reset_defaults)
|
|
53
|
+
header.addWidget(btn_reset)
|
|
54
|
+
|
|
55
|
+
btn_save = QPushButton("Save")
|
|
56
|
+
btn_save.setObjectName("accent")
|
|
57
|
+
btn_save.setFixedWidth(70)
|
|
58
|
+
btn_save.clicked.connect(self._save)
|
|
59
|
+
header.addWidget(btn_save)
|
|
60
|
+
|
|
61
|
+
header_w = QWidget()
|
|
62
|
+
header_w.setStyleSheet(f"background: {Colors.BG_SURFACE}; border-bottom: 1px solid {Colors.BORDER};")
|
|
63
|
+
header_w.setLayout(header)
|
|
64
|
+
root.addWidget(header_w)
|
|
65
|
+
|
|
66
|
+
# ── Scrollable content ───────────────────────────
|
|
67
|
+
scroll = QScrollArea()
|
|
68
|
+
scroll.setWidgetResizable(True)
|
|
69
|
+
scroll.setFrameShape(QScrollArea.Shape.NoFrame) # type: ignore[attr-defined]
|
|
70
|
+
|
|
71
|
+
content = QWidget()
|
|
72
|
+
content.setStyleSheet(f"background: {Colors.BG_BASE};")
|
|
73
|
+
content_layout = QVBoxLayout(content)
|
|
74
|
+
content_layout.setContentsMargins(32, 24, 32, 24)
|
|
75
|
+
content_layout.setSpacing(20)
|
|
76
|
+
|
|
77
|
+
content_layout.addWidget(self._build_adb_group())
|
|
78
|
+
content_layout.addWidget(self._build_ui_group())
|
|
79
|
+
content_layout.addWidget(self._build_mirror_group())
|
|
80
|
+
content_layout.addWidget(self._build_logging_group())
|
|
81
|
+
content_layout.addStretch()
|
|
82
|
+
|
|
83
|
+
scroll.setWidget(content)
|
|
84
|
+
root.addWidget(scroll, stretch=1)
|
|
85
|
+
|
|
86
|
+
self._status_label = QLabel()
|
|
87
|
+
self._status_label.setStyleSheet(
|
|
88
|
+
f"color: {Colors.SUCCESS}; padding: 6px 24px; "
|
|
89
|
+
f"background: {Colors.BG_SURFACE}; border-top: 1px solid {Colors.BORDER};"
|
|
90
|
+
)
|
|
91
|
+
root.addWidget(self._status_label)
|
|
92
|
+
|
|
93
|
+
def _build_adb_group(self) -> QGroupBox:
|
|
94
|
+
group = QGroupBox("ADB")
|
|
95
|
+
form = QFormLayout(group)
|
|
96
|
+
form.setSpacing(10)
|
|
97
|
+
|
|
98
|
+
self._adb_path = QLineEdit()
|
|
99
|
+
self._adb_path.setPlaceholderText("adb")
|
|
100
|
+
form.addRow("ADB Binary Path:", self._adb_path)
|
|
101
|
+
|
|
102
|
+
self._adb_port = QSpinBox()
|
|
103
|
+
self._adb_port.setRange(1, 65535)
|
|
104
|
+
form.addRow("Default Port:", self._adb_port)
|
|
105
|
+
|
|
106
|
+
self._adb_timeout = QSpinBox()
|
|
107
|
+
self._adb_timeout.setRange(1, 300)
|
|
108
|
+
self._adb_timeout.setSuffix(" s")
|
|
109
|
+
form.addRow("Command Timeout:", self._adb_timeout)
|
|
110
|
+
|
|
111
|
+
self._adb_retry = QSpinBox()
|
|
112
|
+
self._adb_retry.setRange(0, 10)
|
|
113
|
+
form.addRow("Retry Count:", self._adb_retry)
|
|
114
|
+
|
|
115
|
+
self._auto_start = QCheckBox("Auto-start ADB server")
|
|
116
|
+
form.addRow("", self._auto_start)
|
|
117
|
+
|
|
118
|
+
self._kill_on_exit = QCheckBox("Kill ADB server on exit")
|
|
119
|
+
form.addRow("", self._kill_on_exit)
|
|
120
|
+
|
|
121
|
+
return group
|
|
122
|
+
|
|
123
|
+
def _build_ui_group(self) -> QGroupBox:
|
|
124
|
+
group = QGroupBox("Interface")
|
|
125
|
+
form = QFormLayout(group)
|
|
126
|
+
form.setSpacing(10)
|
|
127
|
+
|
|
128
|
+
self._theme_combo = QComboBox()
|
|
129
|
+
self._theme_combo.addItems(["dark", "light", "auto"])
|
|
130
|
+
form.addRow("Theme:", self._theme_combo)
|
|
131
|
+
|
|
132
|
+
self._font_size = QSpinBox()
|
|
133
|
+
self._font_size.setRange(8, 24)
|
|
134
|
+
self._font_size.setSuffix(" px")
|
|
135
|
+
form.addRow("Font Size:", self._font_size)
|
|
136
|
+
|
|
137
|
+
self._animations = QCheckBox("Enable animations")
|
|
138
|
+
form.addRow("", self._animations)
|
|
139
|
+
|
|
140
|
+
self._notifications = QCheckBox("Enable notifications")
|
|
141
|
+
form.addRow("", self._notifications)
|
|
142
|
+
|
|
143
|
+
self._tray_icon = QCheckBox("Show system tray icon")
|
|
144
|
+
form.addRow("", self._tray_icon)
|
|
145
|
+
|
|
146
|
+
return group
|
|
147
|
+
|
|
148
|
+
def _build_mirror_group(self) -> QGroupBox:
|
|
149
|
+
group = QGroupBox("Screen Mirror")
|
|
150
|
+
form = QFormLayout(group)
|
|
151
|
+
form.setSpacing(10)
|
|
152
|
+
|
|
153
|
+
self._mirror_fps = QSpinBox()
|
|
154
|
+
self._mirror_fps.setRange(1, 120)
|
|
155
|
+
self._mirror_fps.setSuffix(" FPS")
|
|
156
|
+
form.addRow("Max FPS:", self._mirror_fps)
|
|
157
|
+
|
|
158
|
+
self._mirror_bitrate = QSpinBox()
|
|
159
|
+
self._mirror_bitrate.setRange(1, 64)
|
|
160
|
+
self._mirror_bitrate.setSuffix(" Mbps")
|
|
161
|
+
form.addRow("Bitrate:", self._mirror_bitrate)
|
|
162
|
+
|
|
163
|
+
self._mirror_width = QSpinBox()
|
|
164
|
+
self._mirror_width.setRange(320, 3840)
|
|
165
|
+
self._mirror_width.setSingleStep(16)
|
|
166
|
+
form.addRow("Max Width:", self._mirror_width)
|
|
167
|
+
|
|
168
|
+
self._stay_awake = QCheckBox("Keep device awake while mirroring")
|
|
169
|
+
form.addRow("", self._stay_awake)
|
|
170
|
+
|
|
171
|
+
self._show_touches = QCheckBox("Show touch points")
|
|
172
|
+
form.addRow("", self._show_touches)
|
|
173
|
+
|
|
174
|
+
return group
|
|
175
|
+
|
|
176
|
+
def _build_logging_group(self) -> QGroupBox:
|
|
177
|
+
group = QGroupBox("Logging")
|
|
178
|
+
form = QFormLayout(group)
|
|
179
|
+
form.setSpacing(10)
|
|
180
|
+
|
|
181
|
+
self._log_level = QComboBox()
|
|
182
|
+
self._log_level.addItems(["DEBUG", "INFO", "WARNING", "ERROR"])
|
|
183
|
+
form.addRow("Log Level:", self._log_level)
|
|
184
|
+
|
|
185
|
+
self._log_file_enabled = QCheckBox("Write log file")
|
|
186
|
+
form.addRow("", self._log_file_enabled)
|
|
187
|
+
|
|
188
|
+
self._log_max_size = QSpinBox()
|
|
189
|
+
self._log_max_size.setRange(1, 100)
|
|
190
|
+
self._log_max_size.setSuffix(" MB")
|
|
191
|
+
form.addRow("Max File Size:", self._log_max_size)
|
|
192
|
+
|
|
193
|
+
self._log_backup = QSpinBox()
|
|
194
|
+
self._log_backup.setRange(0, 20)
|
|
195
|
+
form.addRow("Backup Files:", self._log_backup)
|
|
196
|
+
|
|
197
|
+
return group
|
|
198
|
+
|
|
199
|
+
def _load_values(self) -> None:
|
|
200
|
+
s = self._settings
|
|
201
|
+
self._adb_path.setText(s.adb.path)
|
|
202
|
+
self._adb_port.setValue(s.adb.default_port)
|
|
203
|
+
self._adb_timeout.setValue(int(s.adb.command_timeout))
|
|
204
|
+
self._adb_retry.setValue(s.adb.retry_count)
|
|
205
|
+
self._auto_start.setChecked(s.adb.auto_start_server)
|
|
206
|
+
self._kill_on_exit.setChecked(s.adb.kill_server_on_exit)
|
|
207
|
+
|
|
208
|
+
self._theme_combo.setCurrentText(s.ui.theme)
|
|
209
|
+
self._font_size.setValue(s.ui.font_size)
|
|
210
|
+
self._animations.setChecked(s.ui.animations_enabled)
|
|
211
|
+
self._notifications.setChecked(s.ui.notifications_enabled)
|
|
212
|
+
self._tray_icon.setChecked(s.ui.tray_icon_enabled)
|
|
213
|
+
|
|
214
|
+
self._mirror_fps.setValue(s.mirror.max_fps)
|
|
215
|
+
self._mirror_bitrate.setValue(s.mirror.max_bitrate_mbps)
|
|
216
|
+
self._mirror_width.setValue(s.mirror.max_width)
|
|
217
|
+
self._stay_awake.setChecked(s.mirror.stay_awake)
|
|
218
|
+
self._show_touches.setChecked(s.mirror.show_touches)
|
|
219
|
+
|
|
220
|
+
self._log_level.setCurrentText(s.logging.level)
|
|
221
|
+
self._log_file_enabled.setChecked(s.logging.file_enabled)
|
|
222
|
+
self._log_max_size.setValue(s.logging.max_file_size_mb)
|
|
223
|
+
self._log_backup.setValue(s.logging.backup_count)
|
|
224
|
+
|
|
225
|
+
def _save(self) -> None:
|
|
226
|
+
s = self._settings
|
|
227
|
+
s.adb.path = self._adb_path.text().strip() or "adb"
|
|
228
|
+
s.adb.default_port = self._adb_port.value()
|
|
229
|
+
s.adb.command_timeout = float(self._adb_timeout.value())
|
|
230
|
+
s.adb.retry_count = self._adb_retry.value()
|
|
231
|
+
s.adb.auto_start_server = self._auto_start.isChecked()
|
|
232
|
+
s.adb.kill_server_on_exit = self._kill_on_exit.isChecked()
|
|
233
|
+
|
|
234
|
+
s.ui.theme = self._theme_combo.currentText()
|
|
235
|
+
s.ui.font_size = self._font_size.value()
|
|
236
|
+
s.ui.animations_enabled = self._animations.isChecked()
|
|
237
|
+
s.ui.notifications_enabled = self._notifications.isChecked()
|
|
238
|
+
s.ui.tray_icon_enabled = self._tray_icon.isChecked()
|
|
239
|
+
|
|
240
|
+
s.mirror.max_fps = self._mirror_fps.value()
|
|
241
|
+
s.mirror.max_bitrate_mbps = self._mirror_bitrate.value()
|
|
242
|
+
s.mirror.max_width = self._mirror_width.value()
|
|
243
|
+
s.mirror.stay_awake = self._stay_awake.isChecked()
|
|
244
|
+
s.mirror.show_touches = self._show_touches.isChecked()
|
|
245
|
+
|
|
246
|
+
s.logging.level = self._log_level.currentText()
|
|
247
|
+
s.logging.file_enabled = self._log_file_enabled.isChecked()
|
|
248
|
+
s.logging.max_file_size_mb = self._log_max_size.value()
|
|
249
|
+
s.logging.backup_count = self._log_backup.value()
|
|
250
|
+
|
|
251
|
+
s.save()
|
|
252
|
+
self._status_label.setText("✓ Settings saved")
|
|
253
|
+
|
|
254
|
+
def _reset_defaults(self) -> None:
|
|
255
|
+
self._settings.reset_to_defaults()
|
|
256
|
+
self._load_values()
|
|
257
|
+
self._status_label.setText("Settings reset to defaults")
|