local-control 0.1.2__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.
- local_control/__init__.py +11 -0
- local_control/app.py +291 -0
- local_control/auth.py +240 -0
- local_control/cli.py +143 -0
- local_control/clipboard.py +342 -0
- local_control/config.py +47 -0
- local_control/control.py +1043 -0
- local_control/startup.py +140 -0
- local_control/static/css/styles.css +393 -0
- local_control/static/index.html +140 -0
- local_control/static/js/app.js +1658 -0
- local_control/utils/__init__.py +9 -0
- local_control/utils/qrcodegen.py +907 -0
- local_control/utils/terminal_qr.py +34 -0
- local_control-0.1.2.dist-info/METADATA +49 -0
- local_control-0.1.2.dist-info/RECORD +20 -0
- local_control-0.1.2.dist-info/WHEEL +5 -0
- local_control-0.1.2.dist-info/entry_points.txt +2 -0
- local_control-0.1.2.dist-info/licenses/LICENSE +21 -0
- local_control-0.1.2.dist-info/top_level.txt +1 -0
local_control/startup.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cross-platform helpers for registering/unregistering the LAN control server at user login.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import plistlib
|
|
9
|
+
import shlex
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import List
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class StartupError(RuntimeError):
|
|
17
|
+
"""Raised when registering or unregistering auto-start fails."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class StartupManager:
|
|
21
|
+
def __init__(self, host: str, port: int, debug: bool = False) -> None:
|
|
22
|
+
self.host = host
|
|
23
|
+
self.port = port
|
|
24
|
+
self.debug = debug
|
|
25
|
+
self.python = Path(sys.executable).resolve()
|
|
26
|
+
self.command = self._build_command()
|
|
27
|
+
|
|
28
|
+
def enable(self) -> None:
|
|
29
|
+
system = sys.platform
|
|
30
|
+
try:
|
|
31
|
+
if system.startswith("win"):
|
|
32
|
+
self._enable_windows()
|
|
33
|
+
elif system == "darwin":
|
|
34
|
+
self._enable_darwin()
|
|
35
|
+
else:
|
|
36
|
+
self._enable_linux()
|
|
37
|
+
except OSError as exc: # pragma: no cover - platform specific
|
|
38
|
+
raise StartupError(str(exc)) from exc
|
|
39
|
+
|
|
40
|
+
def disable(self) -> None:
|
|
41
|
+
system = sys.platform
|
|
42
|
+
try:
|
|
43
|
+
if system.startswith("win"):
|
|
44
|
+
self._disable_windows()
|
|
45
|
+
elif system == "darwin":
|
|
46
|
+
self._disable_darwin()
|
|
47
|
+
else:
|
|
48
|
+
self._disable_linux()
|
|
49
|
+
except OSError as exc: # pragma: no cover - platform specific
|
|
50
|
+
raise StartupError(str(exc)) from exc
|
|
51
|
+
|
|
52
|
+
# ------------------------------------------------------------------ #
|
|
53
|
+
def _build_command(self) -> List[str]:
|
|
54
|
+
cmd = [
|
|
55
|
+
str(self.python),
|
|
56
|
+
"-m",
|
|
57
|
+
"local_control.cli",
|
|
58
|
+
"--host",
|
|
59
|
+
self.host,
|
|
60
|
+
"--port",
|
|
61
|
+
str(self.port),
|
|
62
|
+
]
|
|
63
|
+
if self.debug:
|
|
64
|
+
cmd.append("--debug")
|
|
65
|
+
return cmd
|
|
66
|
+
|
|
67
|
+
# Linux ------------------------------------------------------------- #
|
|
68
|
+
def _autostart_path(self) -> Path:
|
|
69
|
+
return Path.home() / ".config" / "autostart" / "local_control.desktop"
|
|
70
|
+
|
|
71
|
+
def _enable_linux(self) -> None:
|
|
72
|
+
path = self._autostart_path()
|
|
73
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
74
|
+
exec_cmd = " ".join(shlex.quote(part) for part in self.command)
|
|
75
|
+
desktop_entry = "\n".join(
|
|
76
|
+
[
|
|
77
|
+
"[Desktop Entry]",
|
|
78
|
+
"Type=Application",
|
|
79
|
+
"Version=1.0",
|
|
80
|
+
"Name=Local Control",
|
|
81
|
+
"Comment=Start the Local Control server on login",
|
|
82
|
+
f"Exec={exec_cmd}",
|
|
83
|
+
"X-GNOME-Autostart-enabled=true",
|
|
84
|
+
]
|
|
85
|
+
)
|
|
86
|
+
path.write_text(desktop_entry, encoding="utf-8")
|
|
87
|
+
|
|
88
|
+
def _disable_linux(self) -> None:
|
|
89
|
+
path = self._autostart_path()
|
|
90
|
+
if path.exists():
|
|
91
|
+
path.unlink()
|
|
92
|
+
|
|
93
|
+
# macOS ------------------------------------------------------------- #
|
|
94
|
+
def _launch_agent_path(self) -> Path:
|
|
95
|
+
return Path.home() / "Library" / "LaunchAgents" / "com.local_control.server.plist"
|
|
96
|
+
|
|
97
|
+
def _enable_darwin(self) -> None:
|
|
98
|
+
agent_path = self._launch_agent_path()
|
|
99
|
+
agent_path.parent.mkdir(parents=True, exist_ok=True)
|
|
100
|
+
plist = {
|
|
101
|
+
"Label": "com.local_control.server",
|
|
102
|
+
"ProgramArguments": self.command,
|
|
103
|
+
"RunAtLoad": True,
|
|
104
|
+
"KeepAlive": False,
|
|
105
|
+
"WorkingDirectory": str(Path.home()),
|
|
106
|
+
"StandardOutPath": str(agent_path.with_suffix(".log")),
|
|
107
|
+
"StandardErrorPath": str(agent_path.with_suffix(".err.log")),
|
|
108
|
+
}
|
|
109
|
+
agent_path.write_bytes(plistlib.dumps(plist))
|
|
110
|
+
subprocess.run(["launchctl", "load", str(agent_path)], check=False)
|
|
111
|
+
|
|
112
|
+
def _disable_darwin(self) -> None:
|
|
113
|
+
agent_path = self._launch_agent_path()
|
|
114
|
+
subprocess.run(["launchctl", "unload", str(agent_path)], check=False)
|
|
115
|
+
if agent_path.exists():
|
|
116
|
+
agent_path.unlink()
|
|
117
|
+
|
|
118
|
+
# Windows ----------------------------------------------------------- #
|
|
119
|
+
def _startup_script_path(self) -> Path:
|
|
120
|
+
appdata = Path(os.environ.get("APPDATA", Path.home() / "AppData" / "Roaming"))
|
|
121
|
+
startup_dir = appdata / "Microsoft" / "Windows" / "Start Menu" / "Programs" / "Startup"
|
|
122
|
+
startup_dir.mkdir(parents=True, exist_ok=True)
|
|
123
|
+
return startup_dir / "local_control_startup.bat"
|
|
124
|
+
|
|
125
|
+
def _enable_windows(self) -> None:
|
|
126
|
+
path = self._startup_script_path()
|
|
127
|
+
cmd = " ".join(f'"{part}"' if " " in part else part for part in self.command)
|
|
128
|
+
script = "\r\n".join(
|
|
129
|
+
[
|
|
130
|
+
"@echo off",
|
|
131
|
+
"REM Auto-generated by local_control StartupManager",
|
|
132
|
+
f'start "" {cmd}',
|
|
133
|
+
]
|
|
134
|
+
)
|
|
135
|
+
path.write_text(script, encoding="utf-8")
|
|
136
|
+
|
|
137
|
+
def _disable_windows(self) -> None:
|
|
138
|
+
path = self._startup_script_path()
|
|
139
|
+
if path.exists():
|
|
140
|
+
path.unlink()
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
color-scheme: light dark;
|
|
3
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
4
|
+
font-size: 16px;
|
|
5
|
+
line-height: 1.4;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
body {
|
|
9
|
+
margin: 0;
|
|
10
|
+
display: flex;
|
|
11
|
+
min-height: 100vh;
|
|
12
|
+
background: #111;
|
|
13
|
+
color: #f5f5f5;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
main {
|
|
17
|
+
margin: auto;
|
|
18
|
+
width: clamp(320px, 90vw, 1400px);
|
|
19
|
+
padding: 2rem;
|
|
20
|
+
background: rgba(20, 20, 20, 0.85);
|
|
21
|
+
border-radius: 18px;
|
|
22
|
+
box-shadow: 0 24px 48px rgba(0, 0, 0, 0.35);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
h1,
|
|
26
|
+
h2 {
|
|
27
|
+
margin: 0 0 1rem;
|
|
28
|
+
font-weight: 700;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
form,
|
|
32
|
+
.controls,
|
|
33
|
+
.system-actions {
|
|
34
|
+
display: flex;
|
|
35
|
+
flex-direction: column;
|
|
36
|
+
gap: 0.75rem;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
label {
|
|
40
|
+
display: flex;
|
|
41
|
+
flex-direction: column;
|
|
42
|
+
gap: 0.25rem;
|
|
43
|
+
font-size: 0.95rem;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
textarea {
|
|
47
|
+
box-sizing: border-box;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
input[type="text"],
|
|
51
|
+
input[type="password"],
|
|
52
|
+
textarea {
|
|
53
|
+
padding: 0.65rem;
|
|
54
|
+
border-radius: 0.6rem;
|
|
55
|
+
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
56
|
+
background: rgba(255, 255, 255, 0.08);
|
|
57
|
+
color: inherit;
|
|
58
|
+
font: inherit;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
button {
|
|
62
|
+
padding: 0.7rem 1.1rem;
|
|
63
|
+
border-radius: 0.7rem;
|
|
64
|
+
border: none;
|
|
65
|
+
font-size: 1rem;
|
|
66
|
+
font-weight: 600;
|
|
67
|
+
cursor: pointer;
|
|
68
|
+
color: #0f141d;
|
|
69
|
+
background: #4ad66d;
|
|
70
|
+
transition: transform 0.1s, box-shadow 0.1s;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
button:hover,
|
|
74
|
+
button:focus-visible {
|
|
75
|
+
transform: translateY(-1px);
|
|
76
|
+
box-shadow: 0 4px 16px rgba(74, 214, 109, 0.4);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
button.secondary {
|
|
80
|
+
background: rgba(255, 255, 255, 0.2);
|
|
81
|
+
color: #f5f5f5;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
button.warn {
|
|
85
|
+
background: #f6c945;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
button.danger {
|
|
89
|
+
background: #f66a45;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.error {
|
|
93
|
+
color: #f87171;
|
|
94
|
+
min-height: 1.25rem;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.remember {
|
|
98
|
+
flex-direction: row;
|
|
99
|
+
align-items: center;
|
|
100
|
+
gap: 0.5rem;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
#control-view header {
|
|
104
|
+
display: flex;
|
|
105
|
+
justify-content: space-between;
|
|
106
|
+
align-items: center;
|
|
107
|
+
margin-bottom: 1rem;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.header-actions {
|
|
111
|
+
display: flex;
|
|
112
|
+
align-items: center;
|
|
113
|
+
gap: 0.75rem;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.icon-button {
|
|
117
|
+
background: rgba(255, 255, 255, 0.18);
|
|
118
|
+
color: inherit;
|
|
119
|
+
border-radius: 50%;
|
|
120
|
+
width: 2.6rem;
|
|
121
|
+
height: 2.6rem;
|
|
122
|
+
display: inline-flex;
|
|
123
|
+
align-items: center;
|
|
124
|
+
justify-content: center;
|
|
125
|
+
font-size: 1.2rem;
|
|
126
|
+
padding: 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.control-layout {
|
|
130
|
+
display: flex;
|
|
131
|
+
flex-direction: column;
|
|
132
|
+
gap: 1.5rem;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.control-panel {
|
|
136
|
+
display: flex;
|
|
137
|
+
flex-direction: column;
|
|
138
|
+
gap: 1.5rem;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.trackpad-wrapper {
|
|
142
|
+
width: 100%;
|
|
143
|
+
display: flex;
|
|
144
|
+
flex-direction: column;
|
|
145
|
+
gap: 0.9rem;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
#trackpad {
|
|
149
|
+
position: relative;
|
|
150
|
+
border-radius: 16px;
|
|
151
|
+
margin: 0;
|
|
152
|
+
width: 100%;
|
|
153
|
+
aspect-ratio: 4 / 3;
|
|
154
|
+
min-height: 280px;
|
|
155
|
+
max-height: 85vh;
|
|
156
|
+
background: linear-gradient(135deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.02));
|
|
157
|
+
display: flex;
|
|
158
|
+
justify-content: center;
|
|
159
|
+
align-items: center;
|
|
160
|
+
text-align: center;
|
|
161
|
+
color: rgba(255, 255, 255, 0.6);
|
|
162
|
+
user-select: none;
|
|
163
|
+
-webkit-user-select: none;
|
|
164
|
+
-moz-user-select: none;
|
|
165
|
+
-ms-user-select: none;
|
|
166
|
+
touch-action: none;
|
|
167
|
+
overflow: hidden;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
#trackpad .trackpad-hint {
|
|
171
|
+
transition: opacity 0.25s ease;
|
|
172
|
+
pointer-events: none;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
#trackpad.pointer-sync .trackpad-hint {
|
|
176
|
+
opacity: 0;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
#pointer-sync-indicator {
|
|
180
|
+
position: absolute;
|
|
181
|
+
inset: 0;
|
|
182
|
+
display: flex;
|
|
183
|
+
flex-direction: column;
|
|
184
|
+
justify-content: center;
|
|
185
|
+
align-items: center;
|
|
186
|
+
gap: 1.2rem;
|
|
187
|
+
text-align: center;
|
|
188
|
+
color: rgba(245, 245, 245, 0.85);
|
|
189
|
+
padding: 1.5rem;
|
|
190
|
+
opacity: 0;
|
|
191
|
+
transform: scale(0.95);
|
|
192
|
+
transition: opacity 0.25s ease, transform 0.25s ease;
|
|
193
|
+
pointer-events: none;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
#trackpad.pointer-sync #pointer-sync-indicator {
|
|
197
|
+
opacity: 1;
|
|
198
|
+
transform: scale(1);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
#pointer-sync-indicator p {
|
|
202
|
+
margin: 0;
|
|
203
|
+
font-size: 0.95rem;
|
|
204
|
+
max-width: 22rem;
|
|
205
|
+
color: rgba(245, 245, 245, 0.75);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
#help-overlay {
|
|
209
|
+
position: fixed;
|
|
210
|
+
inset: 0;
|
|
211
|
+
background: rgba(0, 0, 0, 0.6);
|
|
212
|
+
display: flex;
|
|
213
|
+
justify-content: center;
|
|
214
|
+
align-items: center;
|
|
215
|
+
padding: 1.5rem;
|
|
216
|
+
z-index: 50;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
#help-overlay[hidden] {
|
|
220
|
+
display: none;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.help-panel {
|
|
224
|
+
position: relative;
|
|
225
|
+
max-width: 520px;
|
|
226
|
+
width: min(90vw, 520px);
|
|
227
|
+
background: rgba(18, 18, 18, 0.92);
|
|
228
|
+
border-radius: 16px;
|
|
229
|
+
padding: 1.75rem;
|
|
230
|
+
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.5);
|
|
231
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.help-panel h3 {
|
|
235
|
+
margin: 0 0 1rem;
|
|
236
|
+
font-size: 1.35rem;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.help-panel ul {
|
|
240
|
+
margin: 0;
|
|
241
|
+
padding-left: 1.2rem;
|
|
242
|
+
display: flex;
|
|
243
|
+
flex-direction: column;
|
|
244
|
+
gap: 0.75rem;
|
|
245
|
+
font-size: 0.98rem;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
#help-close {
|
|
249
|
+
position: absolute;
|
|
250
|
+
top: 0.75rem;
|
|
251
|
+
right: 0.75rem;
|
|
252
|
+
font-size: 1rem;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.controls {
|
|
256
|
+
display: grid;
|
|
257
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
258
|
+
gap: 0.5rem;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.controls button {
|
|
262
|
+
width: 100%;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.control-panel > .controls {
|
|
266
|
+
margin-top: 0;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.realtime,
|
|
270
|
+
.keyboard {
|
|
271
|
+
display: flex;
|
|
272
|
+
flex-direction: column;
|
|
273
|
+
gap: 0.75rem;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.realtime input {
|
|
277
|
+
width: 100%;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.type-input-wrapper {
|
|
281
|
+
display: flex;
|
|
282
|
+
align-items: stretch;
|
|
283
|
+
gap: 0.5rem;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.type-input-wrapper textarea {
|
|
287
|
+
width: 100%;
|
|
288
|
+
font-size: 1.05rem;
|
|
289
|
+
resize: vertical;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.type-input-wrapper button {
|
|
293
|
+
align-self: stretch;
|
|
294
|
+
min-width: 4.5rem;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.clipboard {
|
|
298
|
+
display: flex;
|
|
299
|
+
flex-direction: column;
|
|
300
|
+
gap: 0.75rem;
|
|
301
|
+
padding: 1rem;
|
|
302
|
+
background: rgba(255, 255, 255, 0.05);
|
|
303
|
+
border-radius: 12px;
|
|
304
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.clipboard-header {
|
|
308
|
+
display: flex;
|
|
309
|
+
justify-content: space-between;
|
|
310
|
+
align-items: center;
|
|
311
|
+
gap: 1rem;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.clipboard-actions {
|
|
315
|
+
display: flex;
|
|
316
|
+
flex-wrap: wrap;
|
|
317
|
+
gap: 0.5rem;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.clipboard-preview {
|
|
321
|
+
display: flex;
|
|
322
|
+
flex-direction: column;
|
|
323
|
+
gap: 0.75rem;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
#clipboard-text {
|
|
327
|
+
width: 100%;
|
|
328
|
+
max-width: 100%;
|
|
329
|
+
resize: vertical;
|
|
330
|
+
min-height: 3.5rem;
|
|
331
|
+
background: rgba(0, 0, 0, 0.35);
|
|
332
|
+
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
333
|
+
color: inherit;
|
|
334
|
+
padding: 0.6rem;
|
|
335
|
+
border-radius: 0.6rem;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
#clipboard-image {
|
|
339
|
+
max-width: 100%;
|
|
340
|
+
border-radius: 0.75rem;
|
|
341
|
+
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
342
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.hint {
|
|
346
|
+
font-size: 0.85rem;
|
|
347
|
+
color: rgba(245, 245, 245, 0.65);
|
|
348
|
+
margin: 0;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.hint[data-tone="warn"] {
|
|
352
|
+
color: #f6c945;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.hint[data-tone="error"] {
|
|
356
|
+
color: #f87171;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.system-actions {
|
|
360
|
+
margin-top: 1.5rem;
|
|
361
|
+
gap: 0.5rem;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
@media (min-width: 640px) {
|
|
365
|
+
.controls {
|
|
366
|
+
justify-content: center;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.system-actions {
|
|
370
|
+
flex-direction: row;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
.system-actions button {
|
|
374
|
+
flex: 1;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
@media (min-width: 900px) {
|
|
379
|
+
.control-layout {
|
|
380
|
+
display: grid;
|
|
381
|
+
grid-template-columns: minmax(520px, 2.2fr) minmax(320px, 1fr);
|
|
382
|
+
gap: 2rem;
|
|
383
|
+
align-items: start;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
.trackpad-wrapper {
|
|
387
|
+
margin-right: auto;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
#trackpad {
|
|
391
|
+
max-height: 90vh;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>Local Control</title>
|
|
7
|
+
<link
|
|
8
|
+
rel="icon"
|
|
9
|
+
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 120'%3E%3Ctext y='1em' font-size='96'%3E%F0%9F%96%B1%EF%B8%8F%3C/text%3E%3C/svg%3E"
|
|
10
|
+
/>
|
|
11
|
+
<link rel="stylesheet" href="/static/css/styles.css" />
|
|
12
|
+
</head>
|
|
13
|
+
<body>
|
|
14
|
+
<main>
|
|
15
|
+
<section id="login-view">
|
|
16
|
+
<h1>Local Control</h1>
|
|
17
|
+
<form id="login-form">
|
|
18
|
+
<label>
|
|
19
|
+
Username
|
|
20
|
+
<input type="text" id="username" autocomplete="username" required />
|
|
21
|
+
</label>
|
|
22
|
+
<label>
|
|
23
|
+
Password
|
|
24
|
+
<input type="password" id="password" autocomplete="current-password" required />
|
|
25
|
+
</label>
|
|
26
|
+
<label class="remember">
|
|
27
|
+
<input type="checkbox" id="remember" />
|
|
28
|
+
Remember this device
|
|
29
|
+
</label>
|
|
30
|
+
<button type="submit">Sign In</button>
|
|
31
|
+
<p id="login-error" class="error" aria-live="assertive"></p>
|
|
32
|
+
</form>
|
|
33
|
+
</section>
|
|
34
|
+
|
|
35
|
+
<section id="control-view" hidden>
|
|
36
|
+
<header>
|
|
37
|
+
<div>
|
|
38
|
+
<h2>Remote Pad</h2>
|
|
39
|
+
<p class="status">
|
|
40
|
+
Signed in as <span id="status-user"></span>
|
|
41
|
+
</p>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="header-actions">
|
|
44
|
+
<button id="help-button" class="icon-button" aria-label="Show help">❔</button>
|
|
45
|
+
<button id="logout-button" class="secondary">Log out</button>
|
|
46
|
+
</div>
|
|
47
|
+
</header>
|
|
48
|
+
|
|
49
|
+
<div class="control-layout">
|
|
50
|
+
<div class="trackpad-wrapper">
|
|
51
|
+
<div id="trackpad" role="application" aria-label="Trackpad surface">
|
|
52
|
+
<span class="trackpad-hint">Drag to move, tap for click.</span>
|
|
53
|
+
<div id="pointer-sync-indicator" aria-hidden="true">
|
|
54
|
+
<p>
|
|
55
|
+
Pointer synced. Push against any screen edge or press Esc to escape the lock.
|
|
56
|
+
</p>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
<div class="controls">
|
|
60
|
+
<button data-click="left">Left Click</button>
|
|
61
|
+
<button data-click="right">Right Click</button>
|
|
62
|
+
<button data-click="middle">Middle Click</button>
|
|
63
|
+
<button data-click="double">Double Click</button>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<div class="control-panel">
|
|
68
|
+
<div class="realtime">
|
|
69
|
+
<label for="realtime-input">Realtime input</label>
|
|
70
|
+
<input
|
|
71
|
+
id="realtime-input"
|
|
72
|
+
type="text"
|
|
73
|
+
autocomplete="off"
|
|
74
|
+
autocapitalize="none"
|
|
75
|
+
spellcheck="false"
|
|
76
|
+
placeholder="Focus here to stream keystrokes instantly"
|
|
77
|
+
/>
|
|
78
|
+
<small>Keys send as you type; Backspace/Delete and Enter are forwarded automatically.</small>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<form id="type-form" class="keyboard">
|
|
82
|
+
<label for="type-input">Type or paste text</label>
|
|
83
|
+
<div class="type-input-wrapper">
|
|
84
|
+
<textarea
|
|
85
|
+
id="type-input"
|
|
86
|
+
rows="3"
|
|
87
|
+
autocomplete="off"
|
|
88
|
+
placeholder="Paste text, then press Send or ⌘/Ctrl + Enter"
|
|
89
|
+
></textarea>
|
|
90
|
+
<button type="submit" id="type-send">Send</button>
|
|
91
|
+
</div>
|
|
92
|
+
<small>Special keys (Enter, Backspace, arrows, etc.) are forwarded while this area is focused.</small>
|
|
93
|
+
</form>
|
|
94
|
+
|
|
95
|
+
<div class="clipboard">
|
|
96
|
+
<div class="clipboard-header">
|
|
97
|
+
<h3>Clipboard Bridge</h3>
|
|
98
|
+
<div class="clipboard-actions">
|
|
99
|
+
<button id="clipboard-pull" class="secondary">Pull from host</button>
|
|
100
|
+
<button id="clipboard-push">Push to host</button>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
<div class="clipboard-preview">
|
|
104
|
+
<textarea
|
|
105
|
+
id="clipboard-text"
|
|
106
|
+
rows="3"
|
|
107
|
+
readonly
|
|
108
|
+
placeholder="Clipboard text preview"
|
|
109
|
+
></textarea>
|
|
110
|
+
<img id="clipboard-image" alt="Clipboard image preview" hidden />
|
|
111
|
+
</div>
|
|
112
|
+
<p id="clipboard-status" class="hint"></p>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<div class="system-actions">
|
|
116
|
+
<button id="lock-button" class="warn">Lock Screen</button>
|
|
117
|
+
<button id="shutdown-button" class="danger">Shutdown</button>
|
|
118
|
+
<button id="unlock-button" class="secondary">Wake / Unlock</button>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
</section>
|
|
123
|
+
<div id="help-overlay" hidden>
|
|
124
|
+
<div class="help-panel" role="dialog" aria-modal="true" aria-labelledby="help-title">
|
|
125
|
+
<button id="help-close" class="icon-button" aria-label="Close help">✕</button>
|
|
126
|
+
<h3 id="help-title">Quick Help</h3>
|
|
127
|
+
<ul>
|
|
128
|
+
<li>Single finger swipe moves the remote cursor. Tap once to left click.</li>
|
|
129
|
+
<li>Two fingers drag to scroll. Tap with two fingers for right click.</li>
|
|
130
|
+
<li>Three-finger tap triggers a middle click. Push to screen edges to release pointer lock.</li>
|
|
131
|
+
<li>Realtime input streams keystrokes instantly; the text pad sends blocks of text.</li>
|
|
132
|
+
<li>Use the power buttons for lock, shutdown, or wake actions (wake is best-effort on macOS).</li>
|
|
133
|
+
</ul>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
</main>
|
|
137
|
+
|
|
138
|
+
<script src="/static/js/app.js" defer></script>
|
|
139
|
+
</body>
|
|
140
|
+
</html>
|