@linxin666/dsh-remote-web-ui 0.1.1
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.
- package/LICENSE +28 -0
- package/README.md +346 -0
- package/cordis.patch.yml +10 -0
- package/lib/client.js +2817 -0
- package/lib/index.js +1455 -0
- package/lib/invariant.js +21 -0
- package/lib/types/client/PairFailedNotice.d.ts +12 -0
- package/lib/types/client/PairFailedNotice.d.ts.map +1 -0
- package/lib/types/client/PhoneIcon.d.ts +19 -0
- package/lib/types/client/PhoneIcon.d.ts.map +1 -0
- package/lib/types/client/PluginSettingsCard.d.ts +78 -0
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -0
- package/lib/types/client/RemoteEntry.d.ts +10 -0
- package/lib/types/client/RemoteEntry.d.ts.map +1 -0
- package/lib/types/client/RemotePanel.d.ts +50 -0
- package/lib/types/client/RemotePanel.d.ts.map +1 -0
- package/lib/types/client/RemoteSettingsCard.d.ts +75 -0
- package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -0
- package/lib/types/client/deep-link.d.ts +40 -0
- package/lib/types/client/deep-link.d.ts.map +1 -0
- package/lib/types/client/index.d.ts +54 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/client/locales.d.ts +144 -0
- package/lib/types/client/locales.d.ts.map +1 -0
- package/lib/types/client/pair-api.d.ts +100 -0
- package/lib/types/client/pair-api.d.ts.map +1 -0
- package/lib/types/client/settings-form.d.ts +119 -0
- package/lib/types/client/settings-form.d.ts.map +1 -0
- package/lib/types/gate.d.ts +58 -0
- package/lib/types/gate.d.ts.map +1 -0
- package/lib/types/index.d.ts +78 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/invariant.d.ts.map +1 -0
- package/lib/types/lan.d.ts +15 -0
- package/lib/types/lan.d.ts.map +1 -0
- package/lib/types/mobile/api.d.ts +64 -0
- package/lib/types/mobile/api.d.ts.map +1 -0
- package/lib/types/mobile/index.d.ts +10 -0
- package/lib/types/mobile/index.d.ts.map +1 -0
- package/lib/types/mobile/messages.d.ts +103 -0
- package/lib/types/mobile/messages.d.ts.map +1 -0
- package/lib/types/mobile/messages.test.d.ts +2 -0
- package/lib/types/mobile/messages.test.d.ts.map +1 -0
- package/lib/types/mobile/mobile-styles.d.ts +6 -0
- package/lib/types/mobile/mobile-styles.d.ts.map +1 -0
- package/lib/types/mobile/mobile-theme.d.ts +22 -0
- package/lib/types/mobile/mobile-theme.d.ts.map +1 -0
- package/lib/types/mobile/mobile-theme.test.d.ts +2 -0
- package/lib/types/mobile/mobile-theme.test.d.ts.map +1 -0
- package/lib/types/mobile/mux.d.ts +52 -0
- package/lib/types/mobile/mux.d.ts.map +1 -0
- package/lib/types/mobile/rpc.d.ts +38 -0
- package/lib/types/mobile/rpc.d.ts.map +1 -0
- package/lib/types/mobile/theme-toggle.d.ts +12 -0
- package/lib/types/mobile/theme-toggle.d.ts.map +1 -0
- package/lib/types/mobile/views/App.d.ts +59 -0
- package/lib/types/mobile/views/App.d.ts.map +1 -0
- package/lib/types/mobile/views/ChatView.d.ts +39 -0
- package/lib/types/mobile/views/ChatView.d.ts.map +1 -0
- package/lib/types/mobile/views/ChatView.test.d.ts +2 -0
- package/lib/types/mobile/views/ChatView.test.d.ts.map +1 -0
- package/lib/types/mobile/views/SessionListView.d.ts +27 -0
- package/lib/types/mobile/views/SessionListView.d.ts.map +1 -0
- package/lib/types/mobile/views/SessionListView.test.d.ts +2 -0
- package/lib/types/mobile/views/SessionListView.test.d.ts.map +1 -0
- package/lib/types/mobile/views/WorkspaceView.d.ts +19 -0
- package/lib/types/mobile/views/WorkspaceView.d.ts.map +1 -0
- package/lib/types/mobile-api.d.ts +41 -0
- package/lib/types/mobile-api.d.ts.map +1 -0
- package/lib/types/mobile-routes.d.ts +16 -0
- package/lib/types/mobile-routes.d.ts.map +1 -0
- package/lib/types/pairing.d.ts +215 -0
- package/lib/types/pairing.d.ts.map +1 -0
- package/lib/types/routes.d.ts +58 -0
- package/lib/types/routes.d.ts.map +1 -0
- package/lib/types/tunnel.d.ts +99 -0
- package/lib/types/tunnel.d.ts.map +1 -0
- package/package.json +104 -0
- package/src/client/PairFailedNotice.tsx +33 -0
- package/src/client/PhoneIcon.tsx +31 -0
- package/src/client/PluginSettingsCard.tsx +207 -0
- package/src/client/RemoteEntry.tsx +202 -0
- package/src/client/RemotePanel.tsx +186 -0
- package/src/client/RemoteSettingsCard.tsx +220 -0
- package/src/client/deep-link.ts +137 -0
- package/src/client/index.ts +167 -0
- package/src/client/locales.ts +149 -0
- package/src/client/pair-api.ts +176 -0
- package/src/client/remote.module.css +422 -0
- package/src/client/settings-card.module.css +296 -0
- package/src/client/settings-form.ts +294 -0
- package/src/css-modules.d.ts +6 -0
- package/src/gate.ts +121 -0
- package/src/index.ts +299 -0
- package/src/invariant.ts +32 -0
- package/src/lan.ts +21 -0
- package/src/mobile/api.ts +106 -0
- package/src/mobile/index.tsx +27 -0
- package/src/mobile/messages.test.ts +222 -0
- package/src/mobile/messages.ts +555 -0
- package/src/mobile/mobile-styles.ts +853 -0
- package/src/mobile/mobile-theme.test.ts +84 -0
- package/src/mobile/mobile-theme.ts +76 -0
- package/src/mobile/mux.ts +126 -0
- package/src/mobile/rpc.ts +90 -0
- package/src/mobile/theme-toggle.tsx +47 -0
- package/src/mobile/views/App.tsx +152 -0
- package/src/mobile/views/ChatView.test.tsx +243 -0
- package/src/mobile/views/ChatView.tsx +650 -0
- package/src/mobile/views/SessionListView.test.tsx +139 -0
- package/src/mobile/views/SessionListView.tsx +186 -0
- package/src/mobile/views/WorkspaceView.tsx +108 -0
- package/src/mobile-api.ts +271 -0
- package/src/mobile-routes.ts +77 -0
- package/src/pairing.ts +390 -0
- package/src/routes.ts +346 -0
- package/src/tunnel.ts +264 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,2817 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@linxin666/dsh-remote-web-ui",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
//#region \0rolldown/runtime.js
|
|
8
|
+
var __create = Object.create;
|
|
9
|
+
var __defProp$1 = Object.defineProperty;
|
|
10
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
11
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
12
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
13
|
+
var __hasOwnProp$1 = Object.prototype.hasOwnProperty;
|
|
14
|
+
var __copyProps = (to, from, except, desc) => {
|
|
15
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
16
|
+
key = keys[i];
|
|
17
|
+
if (!__hasOwnProp$1.call(to, key) && key !== except) __defProp$1(to, key, {
|
|
18
|
+
get: ((k) => from[k]).bind(null, key),
|
|
19
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
return to;
|
|
23
|
+
};
|
|
24
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp$1(target, "default", {
|
|
25
|
+
value: mod,
|
|
26
|
+
enumerable: true
|
|
27
|
+
}) : target, mod));
|
|
28
|
+
//#endregion
|
|
29
|
+
let react = require("react");
|
|
30
|
+
react = __toESM(react, 1);
|
|
31
|
+
let react_dom_client = require("react-dom/client");
|
|
32
|
+
let react_dom = require("react-dom");
|
|
33
|
+
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
34
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
35
|
+
let _deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-runtime/client");
|
|
36
|
+
//#region ../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs
|
|
37
|
+
function r(e) {
|
|
38
|
+
var t, f, n = "";
|
|
39
|
+
if ("string" == typeof e || "number" == typeof e) n += e;
|
|
40
|
+
else if ("object" == typeof e) if (Array.isArray(e)) {
|
|
41
|
+
var o = e.length;
|
|
42
|
+
for (t = 0; t < o; t++) e[t] && (f = r(e[t])) && (n && (n += " "), n += f);
|
|
43
|
+
} else for (f in e) e[f] && (n && (n += " "), n += f);
|
|
44
|
+
return n;
|
|
45
|
+
}
|
|
46
|
+
function clsx() {
|
|
47
|
+
for (var e, t, f = 0, n = "", o = arguments.length; f < o; f++) (e = arguments[f]) && (t = r(e)) && (n && (n += " "), n += t);
|
|
48
|
+
return n;
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
51
|
+
//#region ../../node_modules/.pnpm/qrcode.react@4.2.0_react@18.3.1/node_modules/qrcode.react/lib/esm/index.js
|
|
52
|
+
var __defProp = Object.defineProperty;
|
|
53
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
54
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
55
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
56
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, {
|
|
57
|
+
enumerable: true,
|
|
58
|
+
configurable: true,
|
|
59
|
+
writable: true,
|
|
60
|
+
value
|
|
61
|
+
}) : obj[key] = value;
|
|
62
|
+
var __spreadValues = (a, b) => {
|
|
63
|
+
for (var prop in b || (b = {})) if (__hasOwnProp.call(b, prop)) __defNormalProp(a, prop, b[prop]);
|
|
64
|
+
if (__getOwnPropSymbols) {
|
|
65
|
+
for (var prop of __getOwnPropSymbols(b)) if (__propIsEnum.call(b, prop)) __defNormalProp(a, prop, b[prop]);
|
|
66
|
+
}
|
|
67
|
+
return a;
|
|
68
|
+
};
|
|
69
|
+
var __objRest = (source, exclude) => {
|
|
70
|
+
var target = {};
|
|
71
|
+
for (var prop in source) if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0) target[prop] = source[prop];
|
|
72
|
+
if (source != null && __getOwnPropSymbols) {
|
|
73
|
+
for (var prop of __getOwnPropSymbols(source)) if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop)) target[prop] = source[prop];
|
|
74
|
+
}
|
|
75
|
+
return target;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* @license QR Code generator library (TypeScript)
|
|
79
|
+
* Copyright (c) Project Nayuki.
|
|
80
|
+
* SPDX-License-Identifier: MIT
|
|
81
|
+
*/
|
|
82
|
+
var qrcodegen;
|
|
83
|
+
((qrcodegen2) => {
|
|
84
|
+
const _QrCode = class _QrCode {
|
|
85
|
+
constructor(version, errorCorrectionLevel, dataCodewords, msk) {
|
|
86
|
+
this.version = version;
|
|
87
|
+
this.errorCorrectionLevel = errorCorrectionLevel;
|
|
88
|
+
this.modules = [];
|
|
89
|
+
this.isFunction = [];
|
|
90
|
+
if (version < _QrCode.MIN_VERSION || version > _QrCode.MAX_VERSION) throw new RangeError("Version value out of range");
|
|
91
|
+
if (msk < -1 || msk > 7) throw new RangeError("Mask value out of range");
|
|
92
|
+
this.size = version * 4 + 17;
|
|
93
|
+
let row = [];
|
|
94
|
+
for (let i = 0; i < this.size; i++) row.push(false);
|
|
95
|
+
for (let i = 0; i < this.size; i++) {
|
|
96
|
+
this.modules.push(row.slice());
|
|
97
|
+
this.isFunction.push(row.slice());
|
|
98
|
+
}
|
|
99
|
+
this.drawFunctionPatterns();
|
|
100
|
+
const allCodewords = this.addEccAndInterleave(dataCodewords);
|
|
101
|
+
this.drawCodewords(allCodewords);
|
|
102
|
+
if (msk == -1) {
|
|
103
|
+
let minPenalty = 1e9;
|
|
104
|
+
for (let i = 0; i < 8; i++) {
|
|
105
|
+
this.applyMask(i);
|
|
106
|
+
this.drawFormatBits(i);
|
|
107
|
+
const penalty = this.getPenaltyScore();
|
|
108
|
+
if (penalty < minPenalty) {
|
|
109
|
+
msk = i;
|
|
110
|
+
minPenalty = penalty;
|
|
111
|
+
}
|
|
112
|
+
this.applyMask(i);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
assert(0 <= msk && msk <= 7);
|
|
116
|
+
this.mask = msk;
|
|
117
|
+
this.applyMask(msk);
|
|
118
|
+
this.drawFormatBits(msk);
|
|
119
|
+
this.isFunction = [];
|
|
120
|
+
}
|
|
121
|
+
static encodeText(text, ecl) {
|
|
122
|
+
const segs = qrcodegen2.QrSegment.makeSegments(text);
|
|
123
|
+
return _QrCode.encodeSegments(segs, ecl);
|
|
124
|
+
}
|
|
125
|
+
static encodeBinary(data, ecl) {
|
|
126
|
+
const seg = qrcodegen2.QrSegment.makeBytes(data);
|
|
127
|
+
return _QrCode.encodeSegments([seg], ecl);
|
|
128
|
+
}
|
|
129
|
+
static encodeSegments(segs, ecl, minVersion = 1, maxVersion = 40, mask = -1, boostEcl = true) {
|
|
130
|
+
if (!(_QrCode.MIN_VERSION <= minVersion && minVersion <= maxVersion && maxVersion <= _QrCode.MAX_VERSION) || mask < -1 || mask > 7) throw new RangeError("Invalid value");
|
|
131
|
+
let version;
|
|
132
|
+
let dataUsedBits;
|
|
133
|
+
for (version = minVersion;; version++) {
|
|
134
|
+
const dataCapacityBits2 = _QrCode.getNumDataCodewords(version, ecl) * 8;
|
|
135
|
+
const usedBits = QrSegment.getTotalBits(segs, version);
|
|
136
|
+
if (usedBits <= dataCapacityBits2) {
|
|
137
|
+
dataUsedBits = usedBits;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
if (version >= maxVersion) throw new RangeError("Data too long");
|
|
141
|
+
}
|
|
142
|
+
for (const newEcl of [
|
|
143
|
+
_QrCode.Ecc.MEDIUM,
|
|
144
|
+
_QrCode.Ecc.QUARTILE,
|
|
145
|
+
_QrCode.Ecc.HIGH
|
|
146
|
+
]) if (boostEcl && dataUsedBits <= _QrCode.getNumDataCodewords(version, newEcl) * 8) ecl = newEcl;
|
|
147
|
+
let bb = [];
|
|
148
|
+
for (const seg of segs) {
|
|
149
|
+
appendBits(seg.mode.modeBits, 4, bb);
|
|
150
|
+
appendBits(seg.numChars, seg.mode.numCharCountBits(version), bb);
|
|
151
|
+
for (const b of seg.getData()) bb.push(b);
|
|
152
|
+
}
|
|
153
|
+
assert(bb.length == dataUsedBits);
|
|
154
|
+
const dataCapacityBits = _QrCode.getNumDataCodewords(version, ecl) * 8;
|
|
155
|
+
assert(bb.length <= dataCapacityBits);
|
|
156
|
+
appendBits(0, Math.min(4, dataCapacityBits - bb.length), bb);
|
|
157
|
+
appendBits(0, (8 - bb.length % 8) % 8, bb);
|
|
158
|
+
assert(bb.length % 8 == 0);
|
|
159
|
+
for (let padByte = 236; bb.length < dataCapacityBits; padByte ^= 253) appendBits(padByte, 8, bb);
|
|
160
|
+
let dataCodewords = [];
|
|
161
|
+
while (dataCodewords.length * 8 < bb.length) dataCodewords.push(0);
|
|
162
|
+
bb.forEach((b, i) => dataCodewords[i >>> 3] |= b << 7 - (i & 7));
|
|
163
|
+
return new _QrCode(version, ecl, dataCodewords, mask);
|
|
164
|
+
}
|
|
165
|
+
getModule(x, y) {
|
|
166
|
+
return 0 <= x && x < this.size && 0 <= y && y < this.size && this.modules[y][x];
|
|
167
|
+
}
|
|
168
|
+
getModules() {
|
|
169
|
+
return this.modules;
|
|
170
|
+
}
|
|
171
|
+
drawFunctionPatterns() {
|
|
172
|
+
for (let i = 0; i < this.size; i++) {
|
|
173
|
+
this.setFunctionModule(6, i, i % 2 == 0);
|
|
174
|
+
this.setFunctionModule(i, 6, i % 2 == 0);
|
|
175
|
+
}
|
|
176
|
+
this.drawFinderPattern(3, 3);
|
|
177
|
+
this.drawFinderPattern(this.size - 4, 3);
|
|
178
|
+
this.drawFinderPattern(3, this.size - 4);
|
|
179
|
+
const alignPatPos = this.getAlignmentPatternPositions();
|
|
180
|
+
const numAlign = alignPatPos.length;
|
|
181
|
+
for (let i = 0; i < numAlign; i++) for (let j = 0; j < numAlign; j++) if (!(i == 0 && j == 0 || i == 0 && j == numAlign - 1 || i == numAlign - 1 && j == 0)) this.drawAlignmentPattern(alignPatPos[i], alignPatPos[j]);
|
|
182
|
+
this.drawFormatBits(0);
|
|
183
|
+
this.drawVersion();
|
|
184
|
+
}
|
|
185
|
+
drawFormatBits(mask) {
|
|
186
|
+
const data = this.errorCorrectionLevel.formatBits << 3 | mask;
|
|
187
|
+
let rem = data;
|
|
188
|
+
for (let i = 0; i < 10; i++) rem = rem << 1 ^ (rem >>> 9) * 1335;
|
|
189
|
+
const bits = (data << 10 | rem) ^ 21522;
|
|
190
|
+
assert(bits >>> 15 == 0);
|
|
191
|
+
for (let i = 0; i <= 5; i++) this.setFunctionModule(8, i, getBit(bits, i));
|
|
192
|
+
this.setFunctionModule(8, 7, getBit(bits, 6));
|
|
193
|
+
this.setFunctionModule(8, 8, getBit(bits, 7));
|
|
194
|
+
this.setFunctionModule(7, 8, getBit(bits, 8));
|
|
195
|
+
for (let i = 9; i < 15; i++) this.setFunctionModule(14 - i, 8, getBit(bits, i));
|
|
196
|
+
for (let i = 0; i < 8; i++) this.setFunctionModule(this.size - 1 - i, 8, getBit(bits, i));
|
|
197
|
+
for (let i = 8; i < 15; i++) this.setFunctionModule(8, this.size - 15 + i, getBit(bits, i));
|
|
198
|
+
this.setFunctionModule(8, this.size - 8, true);
|
|
199
|
+
}
|
|
200
|
+
drawVersion() {
|
|
201
|
+
if (this.version < 7) return;
|
|
202
|
+
let rem = this.version;
|
|
203
|
+
for (let i = 0; i < 12; i++) rem = rem << 1 ^ (rem >>> 11) * 7973;
|
|
204
|
+
const bits = this.version << 12 | rem;
|
|
205
|
+
assert(bits >>> 18 == 0);
|
|
206
|
+
for (let i = 0; i < 18; i++) {
|
|
207
|
+
const color = getBit(bits, i);
|
|
208
|
+
const a = this.size - 11 + i % 3;
|
|
209
|
+
const b = Math.floor(i / 3);
|
|
210
|
+
this.setFunctionModule(a, b, color);
|
|
211
|
+
this.setFunctionModule(b, a, color);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
drawFinderPattern(x, y) {
|
|
215
|
+
for (let dy = -4; dy <= 4; dy++) for (let dx = -4; dx <= 4; dx++) {
|
|
216
|
+
const dist = Math.max(Math.abs(dx), Math.abs(dy));
|
|
217
|
+
const xx = x + dx;
|
|
218
|
+
const yy = y + dy;
|
|
219
|
+
if (0 <= xx && xx < this.size && 0 <= yy && yy < this.size) this.setFunctionModule(xx, yy, dist != 2 && dist != 4);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
drawAlignmentPattern(x, y) {
|
|
223
|
+
for (let dy = -2; dy <= 2; dy++) for (let dx = -2; dx <= 2; dx++) this.setFunctionModule(x + dx, y + dy, Math.max(Math.abs(dx), Math.abs(dy)) != 1);
|
|
224
|
+
}
|
|
225
|
+
setFunctionModule(x, y, isDark) {
|
|
226
|
+
this.modules[y][x] = isDark;
|
|
227
|
+
this.isFunction[y][x] = true;
|
|
228
|
+
}
|
|
229
|
+
addEccAndInterleave(data) {
|
|
230
|
+
const ver = this.version;
|
|
231
|
+
const ecl = this.errorCorrectionLevel;
|
|
232
|
+
if (data.length != _QrCode.getNumDataCodewords(ver, ecl)) throw new RangeError("Invalid argument");
|
|
233
|
+
const numBlocks = _QrCode.NUM_ERROR_CORRECTION_BLOCKS[ecl.ordinal][ver];
|
|
234
|
+
const blockEccLen = _QrCode.ECC_CODEWORDS_PER_BLOCK[ecl.ordinal][ver];
|
|
235
|
+
const rawCodewords = Math.floor(_QrCode.getNumRawDataModules(ver) / 8);
|
|
236
|
+
const numShortBlocks = numBlocks - rawCodewords % numBlocks;
|
|
237
|
+
const shortBlockLen = Math.floor(rawCodewords / numBlocks);
|
|
238
|
+
let blocks = [];
|
|
239
|
+
const rsDiv = _QrCode.reedSolomonComputeDivisor(blockEccLen);
|
|
240
|
+
for (let i = 0, k = 0; i < numBlocks; i++) {
|
|
241
|
+
let dat = data.slice(k, k + shortBlockLen - blockEccLen + (i < numShortBlocks ? 0 : 1));
|
|
242
|
+
k += dat.length;
|
|
243
|
+
const ecc = _QrCode.reedSolomonComputeRemainder(dat, rsDiv);
|
|
244
|
+
if (i < numShortBlocks) dat.push(0);
|
|
245
|
+
blocks.push(dat.concat(ecc));
|
|
246
|
+
}
|
|
247
|
+
let result = [];
|
|
248
|
+
for (let i = 0; i < blocks[0].length; i++) blocks.forEach((block, j) => {
|
|
249
|
+
if (i != shortBlockLen - blockEccLen || j >= numShortBlocks) result.push(block[i]);
|
|
250
|
+
});
|
|
251
|
+
assert(result.length == rawCodewords);
|
|
252
|
+
return result;
|
|
253
|
+
}
|
|
254
|
+
drawCodewords(data) {
|
|
255
|
+
if (data.length != Math.floor(_QrCode.getNumRawDataModules(this.version) / 8)) throw new RangeError("Invalid argument");
|
|
256
|
+
let i = 0;
|
|
257
|
+
for (let right = this.size - 1; right >= 1; right -= 2) {
|
|
258
|
+
if (right == 6) right = 5;
|
|
259
|
+
for (let vert = 0; vert < this.size; vert++) for (let j = 0; j < 2; j++) {
|
|
260
|
+
const x = right - j;
|
|
261
|
+
const y = (right + 1 & 2) == 0 ? this.size - 1 - vert : vert;
|
|
262
|
+
if (!this.isFunction[y][x] && i < data.length * 8) {
|
|
263
|
+
this.modules[y][x] = getBit(data[i >>> 3], 7 - (i & 7));
|
|
264
|
+
i++;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
assert(i == data.length * 8);
|
|
269
|
+
}
|
|
270
|
+
applyMask(mask) {
|
|
271
|
+
if (mask < 0 || mask > 7) throw new RangeError("Mask value out of range");
|
|
272
|
+
for (let y = 0; y < this.size; y++) for (let x = 0; x < this.size; x++) {
|
|
273
|
+
let invert;
|
|
274
|
+
switch (mask) {
|
|
275
|
+
case 0:
|
|
276
|
+
invert = (x + y) % 2 == 0;
|
|
277
|
+
break;
|
|
278
|
+
case 1:
|
|
279
|
+
invert = y % 2 == 0;
|
|
280
|
+
break;
|
|
281
|
+
case 2:
|
|
282
|
+
invert = x % 3 == 0;
|
|
283
|
+
break;
|
|
284
|
+
case 3:
|
|
285
|
+
invert = (x + y) % 3 == 0;
|
|
286
|
+
break;
|
|
287
|
+
case 4:
|
|
288
|
+
invert = (Math.floor(x / 3) + Math.floor(y / 2)) % 2 == 0;
|
|
289
|
+
break;
|
|
290
|
+
case 5:
|
|
291
|
+
invert = x * y % 2 + x * y % 3 == 0;
|
|
292
|
+
break;
|
|
293
|
+
case 6:
|
|
294
|
+
invert = (x * y % 2 + x * y % 3) % 2 == 0;
|
|
295
|
+
break;
|
|
296
|
+
case 7:
|
|
297
|
+
invert = ((x + y) % 2 + x * y % 3) % 2 == 0;
|
|
298
|
+
break;
|
|
299
|
+
default: throw new Error("Unreachable");
|
|
300
|
+
}
|
|
301
|
+
if (!this.isFunction[y][x] && invert) this.modules[y][x] = !this.modules[y][x];
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
getPenaltyScore() {
|
|
305
|
+
let result = 0;
|
|
306
|
+
for (let y = 0; y < this.size; y++) {
|
|
307
|
+
let runColor = false;
|
|
308
|
+
let runX = 0;
|
|
309
|
+
let runHistory = [
|
|
310
|
+
0,
|
|
311
|
+
0,
|
|
312
|
+
0,
|
|
313
|
+
0,
|
|
314
|
+
0,
|
|
315
|
+
0,
|
|
316
|
+
0
|
|
317
|
+
];
|
|
318
|
+
for (let x = 0; x < this.size; x++) if (this.modules[y][x] == runColor) {
|
|
319
|
+
runX++;
|
|
320
|
+
if (runX == 5) result += _QrCode.PENALTY_N1;
|
|
321
|
+
else if (runX > 5) result++;
|
|
322
|
+
} else {
|
|
323
|
+
this.finderPenaltyAddHistory(runX, runHistory);
|
|
324
|
+
if (!runColor) result += this.finderPenaltyCountPatterns(runHistory) * _QrCode.PENALTY_N3;
|
|
325
|
+
runColor = this.modules[y][x];
|
|
326
|
+
runX = 1;
|
|
327
|
+
}
|
|
328
|
+
result += this.finderPenaltyTerminateAndCount(runColor, runX, runHistory) * _QrCode.PENALTY_N3;
|
|
329
|
+
}
|
|
330
|
+
for (let x = 0; x < this.size; x++) {
|
|
331
|
+
let runColor = false;
|
|
332
|
+
let runY = 0;
|
|
333
|
+
let runHistory = [
|
|
334
|
+
0,
|
|
335
|
+
0,
|
|
336
|
+
0,
|
|
337
|
+
0,
|
|
338
|
+
0,
|
|
339
|
+
0,
|
|
340
|
+
0
|
|
341
|
+
];
|
|
342
|
+
for (let y = 0; y < this.size; y++) if (this.modules[y][x] == runColor) {
|
|
343
|
+
runY++;
|
|
344
|
+
if (runY == 5) result += _QrCode.PENALTY_N1;
|
|
345
|
+
else if (runY > 5) result++;
|
|
346
|
+
} else {
|
|
347
|
+
this.finderPenaltyAddHistory(runY, runHistory);
|
|
348
|
+
if (!runColor) result += this.finderPenaltyCountPatterns(runHistory) * _QrCode.PENALTY_N3;
|
|
349
|
+
runColor = this.modules[y][x];
|
|
350
|
+
runY = 1;
|
|
351
|
+
}
|
|
352
|
+
result += this.finderPenaltyTerminateAndCount(runColor, runY, runHistory) * _QrCode.PENALTY_N3;
|
|
353
|
+
}
|
|
354
|
+
for (let y = 0; y < this.size - 1; y++) for (let x = 0; x < this.size - 1; x++) {
|
|
355
|
+
const color = this.modules[y][x];
|
|
356
|
+
if (color == this.modules[y][x + 1] && color == this.modules[y + 1][x] && color == this.modules[y + 1][x + 1]) result += _QrCode.PENALTY_N2;
|
|
357
|
+
}
|
|
358
|
+
let dark = 0;
|
|
359
|
+
for (const row of this.modules) dark = row.reduce((sum, color) => sum + (color ? 1 : 0), dark);
|
|
360
|
+
const total = this.size * this.size;
|
|
361
|
+
const k = Math.ceil(Math.abs(dark * 20 - total * 10) / total) - 1;
|
|
362
|
+
assert(0 <= k && k <= 9);
|
|
363
|
+
result += k * _QrCode.PENALTY_N4;
|
|
364
|
+
assert(0 <= result && result <= 2568888);
|
|
365
|
+
return result;
|
|
366
|
+
}
|
|
367
|
+
getAlignmentPatternPositions() {
|
|
368
|
+
if (this.version == 1) return [];
|
|
369
|
+
else {
|
|
370
|
+
const numAlign = Math.floor(this.version / 7) + 2;
|
|
371
|
+
const step = this.version == 32 ? 26 : Math.ceil((this.version * 4 + 4) / (numAlign * 2 - 2)) * 2;
|
|
372
|
+
let result = [6];
|
|
373
|
+
for (let pos = this.size - 7; result.length < numAlign; pos -= step) result.splice(1, 0, pos);
|
|
374
|
+
return result;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
static getNumRawDataModules(ver) {
|
|
378
|
+
if (ver < _QrCode.MIN_VERSION || ver > _QrCode.MAX_VERSION) throw new RangeError("Version number out of range");
|
|
379
|
+
let result = (16 * ver + 128) * ver + 64;
|
|
380
|
+
if (ver >= 2) {
|
|
381
|
+
const numAlign = Math.floor(ver / 7) + 2;
|
|
382
|
+
result -= (25 * numAlign - 10) * numAlign - 55;
|
|
383
|
+
if (ver >= 7) result -= 36;
|
|
384
|
+
}
|
|
385
|
+
assert(208 <= result && result <= 29648);
|
|
386
|
+
return result;
|
|
387
|
+
}
|
|
388
|
+
static getNumDataCodewords(ver, ecl) {
|
|
389
|
+
return Math.floor(_QrCode.getNumRawDataModules(ver) / 8) - _QrCode.ECC_CODEWORDS_PER_BLOCK[ecl.ordinal][ver] * _QrCode.NUM_ERROR_CORRECTION_BLOCKS[ecl.ordinal][ver];
|
|
390
|
+
}
|
|
391
|
+
static reedSolomonComputeDivisor(degree) {
|
|
392
|
+
if (degree < 1 || degree > 255) throw new RangeError("Degree out of range");
|
|
393
|
+
let result = [];
|
|
394
|
+
for (let i = 0; i < degree - 1; i++) result.push(0);
|
|
395
|
+
result.push(1);
|
|
396
|
+
let root = 1;
|
|
397
|
+
for (let i = 0; i < degree; i++) {
|
|
398
|
+
for (let j = 0; j < result.length; j++) {
|
|
399
|
+
result[j] = _QrCode.reedSolomonMultiply(result[j], root);
|
|
400
|
+
if (j + 1 < result.length) result[j] ^= result[j + 1];
|
|
401
|
+
}
|
|
402
|
+
root = _QrCode.reedSolomonMultiply(root, 2);
|
|
403
|
+
}
|
|
404
|
+
return result;
|
|
405
|
+
}
|
|
406
|
+
static reedSolomonComputeRemainder(data, divisor) {
|
|
407
|
+
let result = divisor.map((_) => 0);
|
|
408
|
+
for (const b of data) {
|
|
409
|
+
const factor = b ^ result.shift();
|
|
410
|
+
result.push(0);
|
|
411
|
+
divisor.forEach((coef, i) => result[i] ^= _QrCode.reedSolomonMultiply(coef, factor));
|
|
412
|
+
}
|
|
413
|
+
return result;
|
|
414
|
+
}
|
|
415
|
+
static reedSolomonMultiply(x, y) {
|
|
416
|
+
if (x >>> 8 != 0 || y >>> 8 != 0) throw new RangeError("Byte out of range");
|
|
417
|
+
let z = 0;
|
|
418
|
+
for (let i = 7; i >= 0; i--) {
|
|
419
|
+
z = z << 1 ^ (z >>> 7) * 285;
|
|
420
|
+
z ^= (y >>> i & 1) * x;
|
|
421
|
+
}
|
|
422
|
+
assert(z >>> 8 == 0);
|
|
423
|
+
return z;
|
|
424
|
+
}
|
|
425
|
+
finderPenaltyCountPatterns(runHistory) {
|
|
426
|
+
const n = runHistory[1];
|
|
427
|
+
assert(n <= this.size * 3);
|
|
428
|
+
const core = n > 0 && runHistory[2] == n && runHistory[3] == n * 3 && runHistory[4] == n && runHistory[5] == n;
|
|
429
|
+
return (core && runHistory[0] >= n * 4 && runHistory[6] >= n ? 1 : 0) + (core && runHistory[6] >= n * 4 && runHistory[0] >= n ? 1 : 0);
|
|
430
|
+
}
|
|
431
|
+
finderPenaltyTerminateAndCount(currentRunColor, currentRunLength, runHistory) {
|
|
432
|
+
if (currentRunColor) {
|
|
433
|
+
this.finderPenaltyAddHistory(currentRunLength, runHistory);
|
|
434
|
+
currentRunLength = 0;
|
|
435
|
+
}
|
|
436
|
+
currentRunLength += this.size;
|
|
437
|
+
this.finderPenaltyAddHistory(currentRunLength, runHistory);
|
|
438
|
+
return this.finderPenaltyCountPatterns(runHistory);
|
|
439
|
+
}
|
|
440
|
+
finderPenaltyAddHistory(currentRunLength, runHistory) {
|
|
441
|
+
if (runHistory[0] == 0) currentRunLength += this.size;
|
|
442
|
+
runHistory.pop();
|
|
443
|
+
runHistory.unshift(currentRunLength);
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
_QrCode.MIN_VERSION = 1;
|
|
447
|
+
_QrCode.MAX_VERSION = 40;
|
|
448
|
+
_QrCode.PENALTY_N1 = 3;
|
|
449
|
+
_QrCode.PENALTY_N2 = 3;
|
|
450
|
+
_QrCode.PENALTY_N3 = 40;
|
|
451
|
+
_QrCode.PENALTY_N4 = 10;
|
|
452
|
+
_QrCode.ECC_CODEWORDS_PER_BLOCK = [
|
|
453
|
+
[
|
|
454
|
+
-1,
|
|
455
|
+
7,
|
|
456
|
+
10,
|
|
457
|
+
15,
|
|
458
|
+
20,
|
|
459
|
+
26,
|
|
460
|
+
18,
|
|
461
|
+
20,
|
|
462
|
+
24,
|
|
463
|
+
30,
|
|
464
|
+
18,
|
|
465
|
+
20,
|
|
466
|
+
24,
|
|
467
|
+
26,
|
|
468
|
+
30,
|
|
469
|
+
22,
|
|
470
|
+
24,
|
|
471
|
+
28,
|
|
472
|
+
30,
|
|
473
|
+
28,
|
|
474
|
+
28,
|
|
475
|
+
28,
|
|
476
|
+
28,
|
|
477
|
+
30,
|
|
478
|
+
30,
|
|
479
|
+
26,
|
|
480
|
+
28,
|
|
481
|
+
30,
|
|
482
|
+
30,
|
|
483
|
+
30,
|
|
484
|
+
30,
|
|
485
|
+
30,
|
|
486
|
+
30,
|
|
487
|
+
30,
|
|
488
|
+
30,
|
|
489
|
+
30,
|
|
490
|
+
30,
|
|
491
|
+
30,
|
|
492
|
+
30,
|
|
493
|
+
30,
|
|
494
|
+
30
|
|
495
|
+
],
|
|
496
|
+
[
|
|
497
|
+
-1,
|
|
498
|
+
10,
|
|
499
|
+
16,
|
|
500
|
+
26,
|
|
501
|
+
18,
|
|
502
|
+
24,
|
|
503
|
+
16,
|
|
504
|
+
18,
|
|
505
|
+
22,
|
|
506
|
+
22,
|
|
507
|
+
26,
|
|
508
|
+
30,
|
|
509
|
+
22,
|
|
510
|
+
22,
|
|
511
|
+
24,
|
|
512
|
+
24,
|
|
513
|
+
28,
|
|
514
|
+
28,
|
|
515
|
+
26,
|
|
516
|
+
26,
|
|
517
|
+
26,
|
|
518
|
+
26,
|
|
519
|
+
28,
|
|
520
|
+
28,
|
|
521
|
+
28,
|
|
522
|
+
28,
|
|
523
|
+
28,
|
|
524
|
+
28,
|
|
525
|
+
28,
|
|
526
|
+
28,
|
|
527
|
+
28,
|
|
528
|
+
28,
|
|
529
|
+
28,
|
|
530
|
+
28,
|
|
531
|
+
28,
|
|
532
|
+
28,
|
|
533
|
+
28,
|
|
534
|
+
28,
|
|
535
|
+
28,
|
|
536
|
+
28,
|
|
537
|
+
28
|
|
538
|
+
],
|
|
539
|
+
[
|
|
540
|
+
-1,
|
|
541
|
+
13,
|
|
542
|
+
22,
|
|
543
|
+
18,
|
|
544
|
+
26,
|
|
545
|
+
18,
|
|
546
|
+
24,
|
|
547
|
+
18,
|
|
548
|
+
22,
|
|
549
|
+
20,
|
|
550
|
+
24,
|
|
551
|
+
28,
|
|
552
|
+
26,
|
|
553
|
+
24,
|
|
554
|
+
20,
|
|
555
|
+
30,
|
|
556
|
+
24,
|
|
557
|
+
28,
|
|
558
|
+
28,
|
|
559
|
+
26,
|
|
560
|
+
30,
|
|
561
|
+
28,
|
|
562
|
+
30,
|
|
563
|
+
30,
|
|
564
|
+
30,
|
|
565
|
+
30,
|
|
566
|
+
28,
|
|
567
|
+
30,
|
|
568
|
+
30,
|
|
569
|
+
30,
|
|
570
|
+
30,
|
|
571
|
+
30,
|
|
572
|
+
30,
|
|
573
|
+
30,
|
|
574
|
+
30,
|
|
575
|
+
30,
|
|
576
|
+
30,
|
|
577
|
+
30,
|
|
578
|
+
30,
|
|
579
|
+
30,
|
|
580
|
+
30
|
|
581
|
+
],
|
|
582
|
+
[
|
|
583
|
+
-1,
|
|
584
|
+
17,
|
|
585
|
+
28,
|
|
586
|
+
22,
|
|
587
|
+
16,
|
|
588
|
+
22,
|
|
589
|
+
28,
|
|
590
|
+
26,
|
|
591
|
+
26,
|
|
592
|
+
24,
|
|
593
|
+
28,
|
|
594
|
+
24,
|
|
595
|
+
28,
|
|
596
|
+
22,
|
|
597
|
+
24,
|
|
598
|
+
24,
|
|
599
|
+
30,
|
|
600
|
+
28,
|
|
601
|
+
28,
|
|
602
|
+
26,
|
|
603
|
+
28,
|
|
604
|
+
30,
|
|
605
|
+
24,
|
|
606
|
+
30,
|
|
607
|
+
30,
|
|
608
|
+
30,
|
|
609
|
+
30,
|
|
610
|
+
30,
|
|
611
|
+
30,
|
|
612
|
+
30,
|
|
613
|
+
30,
|
|
614
|
+
30,
|
|
615
|
+
30,
|
|
616
|
+
30,
|
|
617
|
+
30,
|
|
618
|
+
30,
|
|
619
|
+
30,
|
|
620
|
+
30,
|
|
621
|
+
30,
|
|
622
|
+
30,
|
|
623
|
+
30
|
|
624
|
+
]
|
|
625
|
+
];
|
|
626
|
+
_QrCode.NUM_ERROR_CORRECTION_BLOCKS = [
|
|
627
|
+
[
|
|
628
|
+
-1,
|
|
629
|
+
1,
|
|
630
|
+
1,
|
|
631
|
+
1,
|
|
632
|
+
1,
|
|
633
|
+
1,
|
|
634
|
+
2,
|
|
635
|
+
2,
|
|
636
|
+
2,
|
|
637
|
+
2,
|
|
638
|
+
4,
|
|
639
|
+
4,
|
|
640
|
+
4,
|
|
641
|
+
4,
|
|
642
|
+
4,
|
|
643
|
+
6,
|
|
644
|
+
6,
|
|
645
|
+
6,
|
|
646
|
+
6,
|
|
647
|
+
7,
|
|
648
|
+
8,
|
|
649
|
+
8,
|
|
650
|
+
9,
|
|
651
|
+
9,
|
|
652
|
+
10,
|
|
653
|
+
12,
|
|
654
|
+
12,
|
|
655
|
+
12,
|
|
656
|
+
13,
|
|
657
|
+
14,
|
|
658
|
+
15,
|
|
659
|
+
16,
|
|
660
|
+
17,
|
|
661
|
+
18,
|
|
662
|
+
19,
|
|
663
|
+
19,
|
|
664
|
+
20,
|
|
665
|
+
21,
|
|
666
|
+
22,
|
|
667
|
+
24,
|
|
668
|
+
25
|
|
669
|
+
],
|
|
670
|
+
[
|
|
671
|
+
-1,
|
|
672
|
+
1,
|
|
673
|
+
1,
|
|
674
|
+
1,
|
|
675
|
+
2,
|
|
676
|
+
2,
|
|
677
|
+
4,
|
|
678
|
+
4,
|
|
679
|
+
4,
|
|
680
|
+
5,
|
|
681
|
+
5,
|
|
682
|
+
5,
|
|
683
|
+
8,
|
|
684
|
+
9,
|
|
685
|
+
9,
|
|
686
|
+
10,
|
|
687
|
+
10,
|
|
688
|
+
11,
|
|
689
|
+
13,
|
|
690
|
+
14,
|
|
691
|
+
16,
|
|
692
|
+
17,
|
|
693
|
+
17,
|
|
694
|
+
18,
|
|
695
|
+
20,
|
|
696
|
+
21,
|
|
697
|
+
23,
|
|
698
|
+
25,
|
|
699
|
+
26,
|
|
700
|
+
28,
|
|
701
|
+
29,
|
|
702
|
+
31,
|
|
703
|
+
33,
|
|
704
|
+
35,
|
|
705
|
+
37,
|
|
706
|
+
38,
|
|
707
|
+
40,
|
|
708
|
+
43,
|
|
709
|
+
45,
|
|
710
|
+
47,
|
|
711
|
+
49
|
|
712
|
+
],
|
|
713
|
+
[
|
|
714
|
+
-1,
|
|
715
|
+
1,
|
|
716
|
+
1,
|
|
717
|
+
2,
|
|
718
|
+
2,
|
|
719
|
+
4,
|
|
720
|
+
4,
|
|
721
|
+
6,
|
|
722
|
+
6,
|
|
723
|
+
8,
|
|
724
|
+
8,
|
|
725
|
+
8,
|
|
726
|
+
10,
|
|
727
|
+
12,
|
|
728
|
+
16,
|
|
729
|
+
12,
|
|
730
|
+
17,
|
|
731
|
+
16,
|
|
732
|
+
18,
|
|
733
|
+
21,
|
|
734
|
+
20,
|
|
735
|
+
23,
|
|
736
|
+
23,
|
|
737
|
+
25,
|
|
738
|
+
27,
|
|
739
|
+
29,
|
|
740
|
+
34,
|
|
741
|
+
34,
|
|
742
|
+
35,
|
|
743
|
+
38,
|
|
744
|
+
40,
|
|
745
|
+
43,
|
|
746
|
+
45,
|
|
747
|
+
48,
|
|
748
|
+
51,
|
|
749
|
+
53,
|
|
750
|
+
56,
|
|
751
|
+
59,
|
|
752
|
+
62,
|
|
753
|
+
65,
|
|
754
|
+
68
|
|
755
|
+
],
|
|
756
|
+
[
|
|
757
|
+
-1,
|
|
758
|
+
1,
|
|
759
|
+
1,
|
|
760
|
+
2,
|
|
761
|
+
4,
|
|
762
|
+
4,
|
|
763
|
+
4,
|
|
764
|
+
5,
|
|
765
|
+
6,
|
|
766
|
+
8,
|
|
767
|
+
8,
|
|
768
|
+
11,
|
|
769
|
+
11,
|
|
770
|
+
16,
|
|
771
|
+
16,
|
|
772
|
+
18,
|
|
773
|
+
16,
|
|
774
|
+
19,
|
|
775
|
+
21,
|
|
776
|
+
25,
|
|
777
|
+
25,
|
|
778
|
+
25,
|
|
779
|
+
34,
|
|
780
|
+
30,
|
|
781
|
+
32,
|
|
782
|
+
35,
|
|
783
|
+
37,
|
|
784
|
+
40,
|
|
785
|
+
42,
|
|
786
|
+
45,
|
|
787
|
+
48,
|
|
788
|
+
51,
|
|
789
|
+
54,
|
|
790
|
+
57,
|
|
791
|
+
60,
|
|
792
|
+
63,
|
|
793
|
+
66,
|
|
794
|
+
70,
|
|
795
|
+
74,
|
|
796
|
+
77,
|
|
797
|
+
81
|
|
798
|
+
]
|
|
799
|
+
];
|
|
800
|
+
qrcodegen2.QrCode = _QrCode;
|
|
801
|
+
function appendBits(val, len, bb) {
|
|
802
|
+
if (len < 0 || len > 31 || val >>> len != 0) throw new RangeError("Value out of range");
|
|
803
|
+
for (let i = len - 1; i >= 0; i--) bb.push(val >>> i & 1);
|
|
804
|
+
}
|
|
805
|
+
function getBit(x, i) {
|
|
806
|
+
return (x >>> i & 1) != 0;
|
|
807
|
+
}
|
|
808
|
+
function assert(cond) {
|
|
809
|
+
if (!cond) throw new Error("Assertion error");
|
|
810
|
+
}
|
|
811
|
+
const _QrSegment = class _QrSegment {
|
|
812
|
+
constructor(mode, numChars, bitData) {
|
|
813
|
+
this.mode = mode;
|
|
814
|
+
this.numChars = numChars;
|
|
815
|
+
this.bitData = bitData;
|
|
816
|
+
if (numChars < 0) throw new RangeError("Invalid argument");
|
|
817
|
+
this.bitData = bitData.slice();
|
|
818
|
+
}
|
|
819
|
+
static makeBytes(data) {
|
|
820
|
+
let bb = [];
|
|
821
|
+
for (const b of data) appendBits(b, 8, bb);
|
|
822
|
+
return new _QrSegment(_QrSegment.Mode.BYTE, data.length, bb);
|
|
823
|
+
}
|
|
824
|
+
static makeNumeric(digits) {
|
|
825
|
+
if (!_QrSegment.isNumeric(digits)) throw new RangeError("String contains non-numeric characters");
|
|
826
|
+
let bb = [];
|
|
827
|
+
for (let i = 0; i < digits.length;) {
|
|
828
|
+
const n = Math.min(digits.length - i, 3);
|
|
829
|
+
appendBits(parseInt(digits.substring(i, i + n), 10), n * 3 + 1, bb);
|
|
830
|
+
i += n;
|
|
831
|
+
}
|
|
832
|
+
return new _QrSegment(_QrSegment.Mode.NUMERIC, digits.length, bb);
|
|
833
|
+
}
|
|
834
|
+
static makeAlphanumeric(text) {
|
|
835
|
+
if (!_QrSegment.isAlphanumeric(text)) throw new RangeError("String contains unencodable characters in alphanumeric mode");
|
|
836
|
+
let bb = [];
|
|
837
|
+
let i;
|
|
838
|
+
for (i = 0; i + 2 <= text.length; i += 2) {
|
|
839
|
+
let temp = _QrSegment.ALPHANUMERIC_CHARSET.indexOf(text.charAt(i)) * 45;
|
|
840
|
+
temp += _QrSegment.ALPHANUMERIC_CHARSET.indexOf(text.charAt(i + 1));
|
|
841
|
+
appendBits(temp, 11, bb);
|
|
842
|
+
}
|
|
843
|
+
if (i < text.length) appendBits(_QrSegment.ALPHANUMERIC_CHARSET.indexOf(text.charAt(i)), 6, bb);
|
|
844
|
+
return new _QrSegment(_QrSegment.Mode.ALPHANUMERIC, text.length, bb);
|
|
845
|
+
}
|
|
846
|
+
static makeSegments(text) {
|
|
847
|
+
if (text == "") return [];
|
|
848
|
+
else if (_QrSegment.isNumeric(text)) return [_QrSegment.makeNumeric(text)];
|
|
849
|
+
else if (_QrSegment.isAlphanumeric(text)) return [_QrSegment.makeAlphanumeric(text)];
|
|
850
|
+
else return [_QrSegment.makeBytes(_QrSegment.toUtf8ByteArray(text))];
|
|
851
|
+
}
|
|
852
|
+
static makeEci(assignVal) {
|
|
853
|
+
let bb = [];
|
|
854
|
+
if (assignVal < 0) throw new RangeError("ECI assignment value out of range");
|
|
855
|
+
else if (assignVal < 128) appendBits(assignVal, 8, bb);
|
|
856
|
+
else if (assignVal < 16384) {
|
|
857
|
+
appendBits(2, 2, bb);
|
|
858
|
+
appendBits(assignVal, 14, bb);
|
|
859
|
+
} else if (assignVal < 1e6) {
|
|
860
|
+
appendBits(6, 3, bb);
|
|
861
|
+
appendBits(assignVal, 21, bb);
|
|
862
|
+
} else throw new RangeError("ECI assignment value out of range");
|
|
863
|
+
return new _QrSegment(_QrSegment.Mode.ECI, 0, bb);
|
|
864
|
+
}
|
|
865
|
+
static isNumeric(text) {
|
|
866
|
+
return _QrSegment.NUMERIC_REGEX.test(text);
|
|
867
|
+
}
|
|
868
|
+
static isAlphanumeric(text) {
|
|
869
|
+
return _QrSegment.ALPHANUMERIC_REGEX.test(text);
|
|
870
|
+
}
|
|
871
|
+
getData() {
|
|
872
|
+
return this.bitData.slice();
|
|
873
|
+
}
|
|
874
|
+
static getTotalBits(segs, version) {
|
|
875
|
+
let result = 0;
|
|
876
|
+
for (const seg of segs) {
|
|
877
|
+
const ccbits = seg.mode.numCharCountBits(version);
|
|
878
|
+
if (seg.numChars >= 1 << ccbits) return Infinity;
|
|
879
|
+
result += 4 + ccbits + seg.bitData.length;
|
|
880
|
+
}
|
|
881
|
+
return result;
|
|
882
|
+
}
|
|
883
|
+
static toUtf8ByteArray(str) {
|
|
884
|
+
str = encodeURI(str);
|
|
885
|
+
let result = [];
|
|
886
|
+
for (let i = 0; i < str.length; i++) if (str.charAt(i) != "%") result.push(str.charCodeAt(i));
|
|
887
|
+
else {
|
|
888
|
+
result.push(parseInt(str.substring(i + 1, i + 3), 16));
|
|
889
|
+
i += 2;
|
|
890
|
+
}
|
|
891
|
+
return result;
|
|
892
|
+
}
|
|
893
|
+
};
|
|
894
|
+
_QrSegment.NUMERIC_REGEX = /^[0-9]*$/;
|
|
895
|
+
_QrSegment.ALPHANUMERIC_REGEX = /^[A-Z0-9 $%*+.\/:-]*$/;
|
|
896
|
+
_QrSegment.ALPHANUMERIC_CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:";
|
|
897
|
+
let QrSegment = _QrSegment;
|
|
898
|
+
qrcodegen2.QrSegment = _QrSegment;
|
|
899
|
+
})(qrcodegen || (qrcodegen = {}));
|
|
900
|
+
((qrcodegen2) => {
|
|
901
|
+
((QrCode2) => {
|
|
902
|
+
const _Ecc = class _Ecc {
|
|
903
|
+
constructor(ordinal, formatBits) {
|
|
904
|
+
this.ordinal = ordinal;
|
|
905
|
+
this.formatBits = formatBits;
|
|
906
|
+
}
|
|
907
|
+
};
|
|
908
|
+
_Ecc.LOW = new _Ecc(0, 1);
|
|
909
|
+
_Ecc.MEDIUM = new _Ecc(1, 0);
|
|
910
|
+
_Ecc.QUARTILE = new _Ecc(2, 3);
|
|
911
|
+
_Ecc.HIGH = new _Ecc(3, 2);
|
|
912
|
+
QrCode2.Ecc = _Ecc;
|
|
913
|
+
})(qrcodegen2.QrCode || (qrcodegen2.QrCode = {}));
|
|
914
|
+
})(qrcodegen || (qrcodegen = {}));
|
|
915
|
+
((qrcodegen2) => {
|
|
916
|
+
((QrSegment2) => {
|
|
917
|
+
const _Mode = class _Mode {
|
|
918
|
+
constructor(modeBits, numBitsCharCount) {
|
|
919
|
+
this.modeBits = modeBits;
|
|
920
|
+
this.numBitsCharCount = numBitsCharCount;
|
|
921
|
+
}
|
|
922
|
+
numCharCountBits(ver) {
|
|
923
|
+
return this.numBitsCharCount[Math.floor((ver + 7) / 17)];
|
|
924
|
+
}
|
|
925
|
+
};
|
|
926
|
+
_Mode.NUMERIC = new _Mode(1, [
|
|
927
|
+
10,
|
|
928
|
+
12,
|
|
929
|
+
14
|
|
930
|
+
]);
|
|
931
|
+
_Mode.ALPHANUMERIC = new _Mode(2, [
|
|
932
|
+
9,
|
|
933
|
+
11,
|
|
934
|
+
13
|
|
935
|
+
]);
|
|
936
|
+
_Mode.BYTE = new _Mode(4, [
|
|
937
|
+
8,
|
|
938
|
+
16,
|
|
939
|
+
16
|
|
940
|
+
]);
|
|
941
|
+
_Mode.KANJI = new _Mode(8, [
|
|
942
|
+
8,
|
|
943
|
+
10,
|
|
944
|
+
12
|
|
945
|
+
]);
|
|
946
|
+
_Mode.ECI = new _Mode(7, [
|
|
947
|
+
0,
|
|
948
|
+
0,
|
|
949
|
+
0
|
|
950
|
+
]);
|
|
951
|
+
QrSegment2.Mode = _Mode;
|
|
952
|
+
})(qrcodegen2.QrSegment || (qrcodegen2.QrSegment = {}));
|
|
953
|
+
})(qrcodegen || (qrcodegen = {}));
|
|
954
|
+
var qrcodegen_default = qrcodegen;
|
|
955
|
+
/**
|
|
956
|
+
* @license qrcode.react
|
|
957
|
+
* Copyright (c) Paul O'Shannessy
|
|
958
|
+
* SPDX-License-Identifier: ISC
|
|
959
|
+
*/
|
|
960
|
+
var ERROR_LEVEL_MAP = {
|
|
961
|
+
L: qrcodegen_default.QrCode.Ecc.LOW,
|
|
962
|
+
M: qrcodegen_default.QrCode.Ecc.MEDIUM,
|
|
963
|
+
Q: qrcodegen_default.QrCode.Ecc.QUARTILE,
|
|
964
|
+
H: qrcodegen_default.QrCode.Ecc.HIGH
|
|
965
|
+
};
|
|
966
|
+
var DEFAULT_SIZE = 128;
|
|
967
|
+
var DEFAULT_LEVEL = "L";
|
|
968
|
+
var DEFAULT_BGCOLOR = "#FFFFFF";
|
|
969
|
+
var DEFAULT_FGCOLOR = "#000000";
|
|
970
|
+
var DEFAULT_INCLUDEMARGIN = false;
|
|
971
|
+
var DEFAULT_MINVERSION = 1;
|
|
972
|
+
var SPEC_MARGIN_SIZE = 4;
|
|
973
|
+
var DEFAULT_MARGIN_SIZE = 0;
|
|
974
|
+
var DEFAULT_IMG_SCALE = .1;
|
|
975
|
+
function generatePath(modules, margin = 0) {
|
|
976
|
+
const ops = [];
|
|
977
|
+
modules.forEach(function(row, y) {
|
|
978
|
+
let start = null;
|
|
979
|
+
row.forEach(function(cell, x) {
|
|
980
|
+
if (!cell && start !== null) {
|
|
981
|
+
ops.push(`M${start + margin} ${y + margin}h${x - start}v1H${start + margin}z`);
|
|
982
|
+
start = null;
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
if (x === row.length - 1) {
|
|
986
|
+
if (!cell) return;
|
|
987
|
+
if (start === null) ops.push(`M${x + margin},${y + margin} h1v1H${x + margin}z`);
|
|
988
|
+
else ops.push(`M${start + margin},${y + margin} h${x + 1 - start}v1H${start + margin}z`);
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
if (cell && start === null) start = x;
|
|
992
|
+
});
|
|
993
|
+
});
|
|
994
|
+
return ops.join("");
|
|
995
|
+
}
|
|
996
|
+
function excavateModules(modules, excavation) {
|
|
997
|
+
return modules.slice().map((row, y) => {
|
|
998
|
+
if (y < excavation.y || y >= excavation.y + excavation.h) return row;
|
|
999
|
+
return row.map((cell, x) => {
|
|
1000
|
+
if (x < excavation.x || x >= excavation.x + excavation.w) return cell;
|
|
1001
|
+
return false;
|
|
1002
|
+
});
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
function getImageSettings(cells, size, margin, imageSettings) {
|
|
1006
|
+
if (imageSettings == null) return null;
|
|
1007
|
+
const numCells = cells.length + margin * 2;
|
|
1008
|
+
const defaultSize = Math.floor(size * DEFAULT_IMG_SCALE);
|
|
1009
|
+
const scale = numCells / size;
|
|
1010
|
+
const w = (imageSettings.width || defaultSize) * scale;
|
|
1011
|
+
const h = (imageSettings.height || defaultSize) * scale;
|
|
1012
|
+
const x = imageSettings.x == null ? cells.length / 2 - w / 2 : imageSettings.x * scale;
|
|
1013
|
+
const y = imageSettings.y == null ? cells.length / 2 - h / 2 : imageSettings.y * scale;
|
|
1014
|
+
const opacity = imageSettings.opacity == null ? 1 : imageSettings.opacity;
|
|
1015
|
+
let excavation = null;
|
|
1016
|
+
if (imageSettings.excavate) {
|
|
1017
|
+
let floorX = Math.floor(x);
|
|
1018
|
+
let floorY = Math.floor(y);
|
|
1019
|
+
excavation = {
|
|
1020
|
+
x: floorX,
|
|
1021
|
+
y: floorY,
|
|
1022
|
+
w: Math.ceil(w + x - floorX),
|
|
1023
|
+
h: Math.ceil(h + y - floorY)
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
const crossOrigin = imageSettings.crossOrigin;
|
|
1027
|
+
return {
|
|
1028
|
+
x,
|
|
1029
|
+
y,
|
|
1030
|
+
h,
|
|
1031
|
+
w,
|
|
1032
|
+
excavation,
|
|
1033
|
+
opacity,
|
|
1034
|
+
crossOrigin
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
function getMarginSize(includeMargin, marginSize) {
|
|
1038
|
+
if (marginSize != null) return Math.max(Math.floor(marginSize), 0);
|
|
1039
|
+
return includeMargin ? SPEC_MARGIN_SIZE : DEFAULT_MARGIN_SIZE;
|
|
1040
|
+
}
|
|
1041
|
+
function useQRCode({ value, level, minVersion, includeMargin, marginSize, imageSettings, size, boostLevel }) {
|
|
1042
|
+
let qrcode = react.default.useMemo(() => {
|
|
1043
|
+
const segments = (Array.isArray(value) ? value : [value]).reduce((accum, v) => {
|
|
1044
|
+
accum.push(...qrcodegen_default.QrSegment.makeSegments(v));
|
|
1045
|
+
return accum;
|
|
1046
|
+
}, []);
|
|
1047
|
+
return qrcodegen_default.QrCode.encodeSegments(segments, ERROR_LEVEL_MAP[level], minVersion, void 0, void 0, boostLevel);
|
|
1048
|
+
}, [
|
|
1049
|
+
value,
|
|
1050
|
+
level,
|
|
1051
|
+
minVersion,
|
|
1052
|
+
boostLevel
|
|
1053
|
+
]);
|
|
1054
|
+
const { cells, margin, numCells, calculatedImageSettings } = react.default.useMemo(() => {
|
|
1055
|
+
let cells2 = qrcode.getModules();
|
|
1056
|
+
const margin2 = getMarginSize(includeMargin, marginSize);
|
|
1057
|
+
return {
|
|
1058
|
+
cells: cells2,
|
|
1059
|
+
margin: margin2,
|
|
1060
|
+
numCells: cells2.length + margin2 * 2,
|
|
1061
|
+
calculatedImageSettings: getImageSettings(cells2, size, margin2, imageSettings)
|
|
1062
|
+
};
|
|
1063
|
+
}, [
|
|
1064
|
+
qrcode,
|
|
1065
|
+
size,
|
|
1066
|
+
imageSettings,
|
|
1067
|
+
includeMargin,
|
|
1068
|
+
marginSize
|
|
1069
|
+
]);
|
|
1070
|
+
return {
|
|
1071
|
+
qrcode,
|
|
1072
|
+
margin,
|
|
1073
|
+
cells,
|
|
1074
|
+
numCells,
|
|
1075
|
+
calculatedImageSettings
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
var SUPPORTS_PATH2D = function() {
|
|
1079
|
+
try {
|
|
1080
|
+
new Path2D().addPath(new Path2D());
|
|
1081
|
+
} catch (e) {
|
|
1082
|
+
return false;
|
|
1083
|
+
}
|
|
1084
|
+
return true;
|
|
1085
|
+
}();
|
|
1086
|
+
var QRCodeCanvas = react.default.forwardRef(function QRCodeCanvas2(props, forwardedRef) {
|
|
1087
|
+
const _a = props, { value, size = DEFAULT_SIZE, level = DEFAULT_LEVEL, bgColor = DEFAULT_BGCOLOR, fgColor = DEFAULT_FGCOLOR, includeMargin = DEFAULT_INCLUDEMARGIN, minVersion = DEFAULT_MINVERSION, boostLevel, marginSize, imageSettings } = _a;
|
|
1088
|
+
const _b = __objRest(_a, [
|
|
1089
|
+
"value",
|
|
1090
|
+
"size",
|
|
1091
|
+
"level",
|
|
1092
|
+
"bgColor",
|
|
1093
|
+
"fgColor",
|
|
1094
|
+
"includeMargin",
|
|
1095
|
+
"minVersion",
|
|
1096
|
+
"boostLevel",
|
|
1097
|
+
"marginSize",
|
|
1098
|
+
"imageSettings"
|
|
1099
|
+
]), { style } = _b, otherProps = __objRest(_b, ["style"]);
|
|
1100
|
+
const imgSrc = imageSettings == null ? void 0 : imageSettings.src;
|
|
1101
|
+
const _canvas = react.default.useRef(null);
|
|
1102
|
+
const _image = react.default.useRef(null);
|
|
1103
|
+
const setCanvasRef = react.default.useCallback((node) => {
|
|
1104
|
+
_canvas.current = node;
|
|
1105
|
+
if (typeof forwardedRef === "function") forwardedRef(node);
|
|
1106
|
+
else if (forwardedRef) forwardedRef.current = node;
|
|
1107
|
+
}, [forwardedRef]);
|
|
1108
|
+
const [isImgLoaded, setIsImageLoaded] = react.default.useState(false);
|
|
1109
|
+
const { margin, cells, numCells, calculatedImageSettings } = useQRCode({
|
|
1110
|
+
value,
|
|
1111
|
+
level,
|
|
1112
|
+
minVersion,
|
|
1113
|
+
boostLevel,
|
|
1114
|
+
includeMargin,
|
|
1115
|
+
marginSize,
|
|
1116
|
+
imageSettings,
|
|
1117
|
+
size
|
|
1118
|
+
});
|
|
1119
|
+
react.default.useEffect(() => {
|
|
1120
|
+
if (_canvas.current != null) {
|
|
1121
|
+
const canvas = _canvas.current;
|
|
1122
|
+
const ctx = canvas.getContext("2d");
|
|
1123
|
+
if (!ctx) return;
|
|
1124
|
+
let cellsToDraw = cells;
|
|
1125
|
+
const image = _image.current;
|
|
1126
|
+
const haveImageToRender = calculatedImageSettings != null && image !== null && image.complete && image.naturalHeight !== 0 && image.naturalWidth !== 0;
|
|
1127
|
+
if (haveImageToRender) {
|
|
1128
|
+
if (calculatedImageSettings.excavation != null) cellsToDraw = excavateModules(cells, calculatedImageSettings.excavation);
|
|
1129
|
+
}
|
|
1130
|
+
const pixelRatio = window.devicePixelRatio || 1;
|
|
1131
|
+
canvas.height = canvas.width = size * pixelRatio;
|
|
1132
|
+
const scale = size / numCells * pixelRatio;
|
|
1133
|
+
ctx.scale(scale, scale);
|
|
1134
|
+
ctx.fillStyle = bgColor;
|
|
1135
|
+
ctx.fillRect(0, 0, numCells, numCells);
|
|
1136
|
+
ctx.fillStyle = fgColor;
|
|
1137
|
+
if (SUPPORTS_PATH2D) ctx.fill(new Path2D(generatePath(cellsToDraw, margin)));
|
|
1138
|
+
else cells.forEach(function(row, rdx) {
|
|
1139
|
+
row.forEach(function(cell, cdx) {
|
|
1140
|
+
if (cell) ctx.fillRect(cdx + margin, rdx + margin, 1, 1);
|
|
1141
|
+
});
|
|
1142
|
+
});
|
|
1143
|
+
if (calculatedImageSettings) ctx.globalAlpha = calculatedImageSettings.opacity;
|
|
1144
|
+
if (haveImageToRender) ctx.drawImage(image, calculatedImageSettings.x + margin, calculatedImageSettings.y + margin, calculatedImageSettings.w, calculatedImageSettings.h);
|
|
1145
|
+
}
|
|
1146
|
+
});
|
|
1147
|
+
react.default.useEffect(() => {
|
|
1148
|
+
setIsImageLoaded(false);
|
|
1149
|
+
}, [imgSrc]);
|
|
1150
|
+
const canvasStyle = __spreadValues({
|
|
1151
|
+
height: size,
|
|
1152
|
+
width: size
|
|
1153
|
+
}, style);
|
|
1154
|
+
let img = null;
|
|
1155
|
+
if (imgSrc != null) img = /* @__PURE__ */ react.default.createElement("img", {
|
|
1156
|
+
src: imgSrc,
|
|
1157
|
+
key: imgSrc,
|
|
1158
|
+
style: { display: "none" },
|
|
1159
|
+
onLoad: () => {
|
|
1160
|
+
setIsImageLoaded(true);
|
|
1161
|
+
},
|
|
1162
|
+
ref: _image,
|
|
1163
|
+
crossOrigin: calculatedImageSettings == null ? void 0 : calculatedImageSettings.crossOrigin
|
|
1164
|
+
});
|
|
1165
|
+
return /* @__PURE__ */ react.default.createElement(react.default.Fragment, null, /* @__PURE__ */ react.default.createElement("canvas", __spreadValues({
|
|
1166
|
+
style: canvasStyle,
|
|
1167
|
+
height: size,
|
|
1168
|
+
width: size,
|
|
1169
|
+
ref: setCanvasRef,
|
|
1170
|
+
role: "img"
|
|
1171
|
+
}, otherProps)), img);
|
|
1172
|
+
});
|
|
1173
|
+
QRCodeCanvas.displayName = "QRCodeCanvas";
|
|
1174
|
+
var QRCodeSVG = react.default.forwardRef(function QRCodeSVG2(props, forwardedRef) {
|
|
1175
|
+
const _a = props, { value, size = DEFAULT_SIZE, level = DEFAULT_LEVEL, bgColor = DEFAULT_BGCOLOR, fgColor = DEFAULT_FGCOLOR, includeMargin = DEFAULT_INCLUDEMARGIN, minVersion = DEFAULT_MINVERSION, boostLevel, title, marginSize, imageSettings } = _a, otherProps = __objRest(_a, [
|
|
1176
|
+
"value",
|
|
1177
|
+
"size",
|
|
1178
|
+
"level",
|
|
1179
|
+
"bgColor",
|
|
1180
|
+
"fgColor",
|
|
1181
|
+
"includeMargin",
|
|
1182
|
+
"minVersion",
|
|
1183
|
+
"boostLevel",
|
|
1184
|
+
"title",
|
|
1185
|
+
"marginSize",
|
|
1186
|
+
"imageSettings"
|
|
1187
|
+
]);
|
|
1188
|
+
const { margin, cells, numCells, calculatedImageSettings } = useQRCode({
|
|
1189
|
+
value,
|
|
1190
|
+
level,
|
|
1191
|
+
minVersion,
|
|
1192
|
+
boostLevel,
|
|
1193
|
+
includeMargin,
|
|
1194
|
+
marginSize,
|
|
1195
|
+
imageSettings,
|
|
1196
|
+
size
|
|
1197
|
+
});
|
|
1198
|
+
let cellsToDraw = cells;
|
|
1199
|
+
let image = null;
|
|
1200
|
+
if (imageSettings != null && calculatedImageSettings != null) {
|
|
1201
|
+
if (calculatedImageSettings.excavation != null) cellsToDraw = excavateModules(cells, calculatedImageSettings.excavation);
|
|
1202
|
+
image = /* @__PURE__ */ react.default.createElement("image", {
|
|
1203
|
+
href: imageSettings.src,
|
|
1204
|
+
height: calculatedImageSettings.h,
|
|
1205
|
+
width: calculatedImageSettings.w,
|
|
1206
|
+
x: calculatedImageSettings.x + margin,
|
|
1207
|
+
y: calculatedImageSettings.y + margin,
|
|
1208
|
+
preserveAspectRatio: "none",
|
|
1209
|
+
opacity: calculatedImageSettings.opacity,
|
|
1210
|
+
crossOrigin: calculatedImageSettings.crossOrigin
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
const fgPath = generatePath(cellsToDraw, margin);
|
|
1214
|
+
return /* @__PURE__ */ react.default.createElement("svg", __spreadValues({
|
|
1215
|
+
height: size,
|
|
1216
|
+
width: size,
|
|
1217
|
+
viewBox: `0 0 ${numCells} ${numCells}`,
|
|
1218
|
+
ref: forwardedRef,
|
|
1219
|
+
role: "img"
|
|
1220
|
+
}, otherProps), !!title && /* @__PURE__ */ react.default.createElement("title", null, title), /* @__PURE__ */ react.default.createElement("path", {
|
|
1221
|
+
fill: bgColor,
|
|
1222
|
+
d: `M0,0 h${numCells}v${numCells}H0z`,
|
|
1223
|
+
shapeRendering: "crispEdges"
|
|
1224
|
+
}), /* @__PURE__ */ react.default.createElement("path", {
|
|
1225
|
+
fill: fgColor,
|
|
1226
|
+
d: fgPath,
|
|
1227
|
+
shapeRendering: "crispEdges"
|
|
1228
|
+
}), image);
|
|
1229
|
+
});
|
|
1230
|
+
QRCodeSVG.displayName = "QRCodeSVG";
|
|
1231
|
+
//#endregion
|
|
1232
|
+
//#region src/client/pair-api.ts
|
|
1233
|
+
/**
|
|
1234
|
+
* Mint a fresh pairing token (one active token at a time — this invalidates
|
|
1235
|
+
* any previous link).
|
|
1236
|
+
* @param workspaceId - optional current workspace to deep-link the phone into.
|
|
1237
|
+
* @param address - optional LAN IP literal the QR must be built from (the
|
|
1238
|
+
* default is the first interface); unknown literals refuse with
|
|
1239
|
+
* 'unknown-address'.
|
|
1240
|
+
* @returns the issued link, the lan-required refusal (server never bound
|
|
1241
|
+
* 0.0.0.0), or the forbidden refusal (the loopback-only fence rejected this
|
|
1242
|
+
* origin — the panel is a desktop control endpoint).
|
|
1243
|
+
*/
|
|
1244
|
+
async function issuePair(workspaceId, address) {
|
|
1245
|
+
const response = await fetch("/api/pair/issue", {
|
|
1246
|
+
method: "POST",
|
|
1247
|
+
headers: { "content-type": "application/json" },
|
|
1248
|
+
body: JSON.stringify({
|
|
1249
|
+
...workspaceId !== void 0 ? { workspaceId } : {},
|
|
1250
|
+
...address !== void 0 ? { address } : {}
|
|
1251
|
+
})
|
|
1252
|
+
});
|
|
1253
|
+
if (!response.ok) {
|
|
1254
|
+
if (response.status === 409) return {
|
|
1255
|
+
ok: false,
|
|
1256
|
+
code: "lan-required"
|
|
1257
|
+
};
|
|
1258
|
+
if (response.status === 403) return {
|
|
1259
|
+
ok: false,
|
|
1260
|
+
code: "forbidden"
|
|
1261
|
+
};
|
|
1262
|
+
if (response.status === 400) return {
|
|
1263
|
+
ok: false,
|
|
1264
|
+
code: "unknown-address"
|
|
1265
|
+
};
|
|
1266
|
+
throw new Error(`remote-web-ui: issue failed with ${String(response.status)}`);
|
|
1267
|
+
}
|
|
1268
|
+
return await response.json();
|
|
1269
|
+
}
|
|
1270
|
+
/**
|
|
1271
|
+
* Accept a pairing token (the phone's first open of the QR link). Success
|
|
1272
|
+
* sets the device cookie; the page then reloads to boot with it.
|
|
1273
|
+
* @param token - the token from the URL.
|
|
1274
|
+
* @returns the wire result.
|
|
1275
|
+
*/
|
|
1276
|
+
async function acceptPair(token) {
|
|
1277
|
+
const response = await fetch("/api/pair/accept", {
|
|
1278
|
+
method: "POST",
|
|
1279
|
+
headers: { "content-type": "application/json" },
|
|
1280
|
+
body: JSON.stringify({ token })
|
|
1281
|
+
});
|
|
1282
|
+
if (response.ok) return { ok: true };
|
|
1283
|
+
if (response.status === 404) return {
|
|
1284
|
+
ok: false,
|
|
1285
|
+
code: "invalid"
|
|
1286
|
+
};
|
|
1287
|
+
if (response.status === 409) return {
|
|
1288
|
+
ok: false,
|
|
1289
|
+
code: "used"
|
|
1290
|
+
};
|
|
1291
|
+
return {
|
|
1292
|
+
ok: false,
|
|
1293
|
+
code: "forbidden"
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
/** Revoke mobile access (paired devices + the current token). */
|
|
1297
|
+
async function stopPair() {
|
|
1298
|
+
const response = await fetch("/api/pair/stop", { method: "POST" });
|
|
1299
|
+
if (!response.ok) throw new Error(`remote-web-ui: stop failed with ${String(response.status)}`);
|
|
1300
|
+
}
|
|
1301
|
+
/** Presence heartbeat from a paired phone (unpaired heartbeats 401 harmlessly). */
|
|
1302
|
+
async function sendHeartbeat() {
|
|
1303
|
+
await fetch("/api/pair/heartbeat", { method: "POST" });
|
|
1304
|
+
}
|
|
1305
|
+
/** Whether the current page URL carries a pairing token / workspace target. */
|
|
1306
|
+
function readPairParams(search) {
|
|
1307
|
+
const params = new URLSearchParams(search);
|
|
1308
|
+
const pair = params.get("pair");
|
|
1309
|
+
const workspace = params.get("workspace");
|
|
1310
|
+
return {
|
|
1311
|
+
...pair !== null && pair !== "" ? { pair } : {},
|
|
1312
|
+
...workspace !== null && workspace !== "" ? { workspace } : {}
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
function formatClock(epochMs) {
|
|
1316
|
+
const date = new Date(epochMs);
|
|
1317
|
+
return `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
|
|
1318
|
+
}
|
|
1319
|
+
/**
|
|
1320
|
+
* Copy text to the clipboard with a fallback for insecure contexts
|
|
1321
|
+
* (plain-HTTP LAN origins lack navigator.clipboard).
|
|
1322
|
+
* @param text - the text to copy.
|
|
1323
|
+
* @returns whether the copy succeeded.
|
|
1324
|
+
*/
|
|
1325
|
+
async function copyText(text) {
|
|
1326
|
+
if (typeof navigator !== "undefined" && navigator.clipboard !== void 0) try {
|
|
1327
|
+
await navigator.clipboard.writeText(text);
|
|
1328
|
+
return true;
|
|
1329
|
+
} catch {}
|
|
1330
|
+
try {
|
|
1331
|
+
const area = document.createElement("textarea");
|
|
1332
|
+
area.value = text;
|
|
1333
|
+
area.style.position = "fixed";
|
|
1334
|
+
area.style.opacity = "0";
|
|
1335
|
+
document.body.appendChild(area);
|
|
1336
|
+
area.select();
|
|
1337
|
+
const ok = document.execCommand("copy");
|
|
1338
|
+
area.remove();
|
|
1339
|
+
return ok;
|
|
1340
|
+
} catch {
|
|
1341
|
+
return false;
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
//#endregion
|
|
1345
|
+
//#region \0dsh-css:/Users/zcl/code/dsh-web-ui/packages/dsh-remote-web-ui/src/client/remote.module.css.mjs
|
|
1346
|
+
const css$1 = "._1FzWja_overlay{z-index:1000;justify-content:center;align-items:center;display:flex;position:fixed;inset:0}._1FzWja_mask{background:var(--dsw-alias-bg-mask-1);backdrop-filter:var(--dsw-mask-blur);position:absolute;inset:0}._1FzWja_trigger{width:36px;height:36px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:50%;flex:none;justify-content:center;align-items:center;padding:0;transition:background-color .12s,color .12s,box-shadow .12s;display:inline-flex}._1FzWja_trigger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}._1FzWja_trigger:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active)}._1FzWja_trigger:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2), 0 0 0 4px var(--dsw-alias-brand-primary);outline:none}._1FzWja_trigger:disabled{opacity:.5;cursor:default}._1FzWja_panel{z-index:1;box-sizing:border-box;background:var(--dsw-alias-bg-layer-2);width:560px;max-width:calc(100vw - 48px);max-height:calc(100vh - 48px);box-shadow:var(--dsw-shadow-lv3);color:var(--dsw-alias-label-primary);border-radius:24px;flex-direction:column;gap:14px;padding:24px;font-size:14px;line-height:22px;display:flex;position:relative;overflow:auto}._1FzWja_header{align-items:flex-start;gap:12px;display:flex}._1FzWja_heading{flex:1;min-width:0}._1FzWja_title{margin:0;font-size:18px;font-weight:600;line-height:26px}._1FzWja_subtitle{color:var(--dsw-alias-label-secondary);margin:4px 0 0;font-size:13px}._1FzWja_close{width:28px;height:28px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:50%;flex:none;justify-content:center;align-items:center;padding:0;transition:background-color .12s,color .12s,box-shadow .12s;display:inline-flex}._1FzWja_close:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}._1FzWja_close:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active)}._1FzWja_close:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2), 0 0 0 4px var(--dsw-alias-brand-primary);outline:none}._1FzWja_close:disabled{opacity:.5;cursor:default}._1FzWja_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:16px;flex-direction:column;align-items:center;gap:12px;padding:16px;display:flex}._1FzWja_cardHeader{justify-content:space-between;align-items:center;gap:12px;width:100%;display:flex}._1FzWja_cardTitle{font-weight:500}._1FzWja_badge{white-space:nowrap;border-radius:999px;flex:none;align-items:center;gap:6px;min-width:0;padding:2px 10px;font-size:12px;line-height:18px;display:inline-flex}._1FzWja_badge:before{content:\"\";background:currentColor;border-radius:50%;width:8px;height:8px}._1FzWja_badge-waiting{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}._1FzWja_badge-connected{color:var(--dsw-alias-state-success-primary);background:var(--dsw-alias-interactive-bg-hover)}._1FzWja_badge-disconnected{color:var(--dsw-alias-state-warn-primary);background:var(--dsw-alias-interactive-bg-hover)}._1FzWja_badge-stopped{color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-interactive-bg-hover)}._1FzWja_badgePublic{color:var(--dsw-alias-brand-primary);background:var(--dsw-alias-interactive-bg-hover)}._1FzWja_badges{flex:none;align-items:center;gap:6px;display:inline-flex}._1FzWja_qrWrap{background:var(--dsw-alias-bg-base);border-radius:12px;justify-content:center;align-items:center;padding:12px;display:flex}._1FzWja_qr{display:block}._1FzWja_expired{color:var(--dsw-alias-state-error-primary);margin:0;font-size:13px}._1FzWja_expiry{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px}._1FzWja_hint{color:var(--dsw-alias-label-secondary);margin:0;font-size:13px}._1FzWja_link{text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-caption);font-family:var(--dsw-font-mono,ui-monospace, monospace);margin:0;font-size:12px;overflow:hidden}._1FzWja_stoppedHint{color:var(--dsw-alias-state-error-primary);margin:0;font-size:13px}._1FzWja_tunnelNote{color:var(--dsw-alias-label-secondary);margin:0;font-size:13px}._1FzWja_tunnelFailed{color:var(--dsw-alias-state-error-primary);margin:0;font-size:13px}._1FzWja_actions{gap:8px;display:flex}._1FzWja_action{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-button-elevated-fill);height:34px;color:var(--dsw-alias-label-primary);cursor:pointer;white-space:nowrap;border-radius:10px;justify-content:center;align-items:center;gap:6px;padding:0 14px;font-size:13px;transition:background-color .12s,border-color .12s,box-shadow .12s;display:inline-flex}._1FzWja_action:hover:not(:disabled){background:var(--dsw-alias-button-floating-hover)}._1FzWja_action:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active)}._1FzWja_action:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2), 0 0 0 4px var(--dsw-alias-brand-primary);outline:none}._1FzWja_action:disabled{opacity:.5;cursor:default}._1FzWja_banner{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:16px;padding:16px}._1FzWja_bannerTitle{color:var(--dsw-alias-state-warn-primary);margin:0;font-weight:500}._1FzWja_bannerHint{color:var(--dsw-alias-label-secondary);margin:6px 0 0;font-size:13px}._1FzWja_notice{z-index:2000;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);max-width:min(420px,100vw - 32px);box-shadow:var(--dsw-shadow-lv3);border-radius:12px;padding:12px 16px;position:fixed;top:16px;left:50%;transform:translate(-50%)}._1FzWja_noticeTitle{color:var(--dsw-alias-state-error-primary);margin:0;font-weight:500}._1FzWja_noticeDetail{color:var(--dsw-alias-label-secondary);margin:4px 0 0;font-size:13px}._1FzWja_addresses{border:none;margin:12px 0 0;padding:0}._1FzWja_addresses legend{color:var(--dsw-alias-label-secondary);padding:0;font-size:13px}._1FzWja_address{color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;cursor:pointer;border-radius:6px;align-items:center;gap:8px;margin-top:6px;padding:4px 6px;font-size:13px;transition:background-color .12s;display:flex}._1FzWja_address:hover{background:var(--dsw-alias-interactive-bg-hover)}._1FzWja_address input:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2), 0 0 0 4px var(--dsw-alias-brand-primary);border-radius:50%;outline:none}._1FzWja_addressValue{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-secondary);flex:1;font-size:12px;overflow:hidden}._1FzWja_addressHint{color:var(--dsw-alias-label-tertiary);margin:6px 0 0;font-size:12px}@media (prefers-reduced-motion:reduce){._1FzWja_trigger,._1FzWja_close,._1FzWja_action,._1FzWja_address{transition:none}}";
|
|
1347
|
+
const tagId$1 = "@linxin666/dsh-remote-web-ui/remote.module.css";
|
|
1348
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
|
|
1349
|
+
const tag = document.createElement("style");
|
|
1350
|
+
tag.dataset.plugin = "@linxin666/dsh-remote-web-ui";
|
|
1351
|
+
tag.dataset.pluginCss = tagId$1;
|
|
1352
|
+
tag.textContent = css$1;
|
|
1353
|
+
document.head.appendChild(tag);
|
|
1354
|
+
}
|
|
1355
|
+
var remote_module_css_default = {
|
|
1356
|
+
"action": "_1FzWja_action",
|
|
1357
|
+
"actions": "_1FzWja_actions",
|
|
1358
|
+
"address": "_1FzWja_address",
|
|
1359
|
+
"addressHint": "_1FzWja_addressHint",
|
|
1360
|
+
"addressValue": "_1FzWja_addressValue",
|
|
1361
|
+
"addresses": "_1FzWja_addresses",
|
|
1362
|
+
"badge": "_1FzWja_badge",
|
|
1363
|
+
"badge-connected": "_1FzWja_badge-connected",
|
|
1364
|
+
"badge-disconnected": "_1FzWja_badge-disconnected",
|
|
1365
|
+
"badge-stopped": "_1FzWja_badge-stopped",
|
|
1366
|
+
"badge-waiting": "_1FzWja_badge-waiting",
|
|
1367
|
+
"badgePublic": "_1FzWja_badgePublic",
|
|
1368
|
+
"badges": "_1FzWja_badges",
|
|
1369
|
+
"banner": "_1FzWja_banner",
|
|
1370
|
+
"bannerHint": "_1FzWja_bannerHint",
|
|
1371
|
+
"bannerTitle": "_1FzWja_bannerTitle",
|
|
1372
|
+
"card": "_1FzWja_card",
|
|
1373
|
+
"cardHeader": "_1FzWja_cardHeader",
|
|
1374
|
+
"cardTitle": "_1FzWja_cardTitle",
|
|
1375
|
+
"close": "_1FzWja_close",
|
|
1376
|
+
"expired": "_1FzWja_expired",
|
|
1377
|
+
"expiry": "_1FzWja_expiry",
|
|
1378
|
+
"header": "_1FzWja_header",
|
|
1379
|
+
"heading": "_1FzWja_heading",
|
|
1380
|
+
"hint": "_1FzWja_hint",
|
|
1381
|
+
"link": "_1FzWja_link",
|
|
1382
|
+
"mask": "_1FzWja_mask",
|
|
1383
|
+
"notice": "_1FzWja_notice",
|
|
1384
|
+
"noticeDetail": "_1FzWja_noticeDetail",
|
|
1385
|
+
"noticeTitle": "_1FzWja_noticeTitle",
|
|
1386
|
+
"overlay": "_1FzWja_overlay",
|
|
1387
|
+
"panel": "_1FzWja_panel",
|
|
1388
|
+
"qr": "_1FzWja_qr",
|
|
1389
|
+
"qrWrap": "_1FzWja_qrWrap",
|
|
1390
|
+
"stoppedHint": "_1FzWja_stoppedHint",
|
|
1391
|
+
"subtitle": "_1FzWja_subtitle",
|
|
1392
|
+
"title": "_1FzWja_title",
|
|
1393
|
+
"trigger": "_1FzWja_trigger",
|
|
1394
|
+
"tunnelFailed": "_1FzWja_tunnelFailed",
|
|
1395
|
+
"tunnelNote": "_1FzWja_tunnelNote"
|
|
1396
|
+
};
|
|
1397
|
+
//#endregion
|
|
1398
|
+
//#region src/client/RemotePanel.tsx
|
|
1399
|
+
/**
|
|
1400
|
+
* The mobile remote-control panel body: status card (state text + badge),
|
|
1401
|
+
* the QR code, the open-on-phone hint with the link text, and the three
|
|
1402
|
+
* actions (stop / refresh / copy). Pure presentation — all state and
|
|
1403
|
+
* actions arrive through props from the entry's behavior component.
|
|
1404
|
+
*/
|
|
1405
|
+
/** Badge text + tone per phase (ready states only). */
|
|
1406
|
+
function statusOf(t, state) {
|
|
1407
|
+
switch (state.phase) {
|
|
1408
|
+
case "connected": return {
|
|
1409
|
+
text: t("status.connected", { n: state.onlineCount }),
|
|
1410
|
+
tone: "connected"
|
|
1411
|
+
};
|
|
1412
|
+
case "disconnected": return {
|
|
1413
|
+
text: t("status.disconnected"),
|
|
1414
|
+
tone: "disconnected"
|
|
1415
|
+
};
|
|
1416
|
+
case "stopped": return {
|
|
1417
|
+
text: t("status.stopped"),
|
|
1418
|
+
tone: "stopped"
|
|
1419
|
+
};
|
|
1420
|
+
case "lan-required": return {
|
|
1421
|
+
text: t("status.lanRequired"),
|
|
1422
|
+
tone: "stopped"
|
|
1423
|
+
};
|
|
1424
|
+
case "waiting": return {
|
|
1425
|
+
text: t("status.waiting"),
|
|
1426
|
+
tone: "waiting"
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
/**
|
|
1431
|
+
* Render the pairing panel.
|
|
1432
|
+
* @param props - copy, state, and actions.
|
|
1433
|
+
* @returns the panel element tree.
|
|
1434
|
+
*/
|
|
1435
|
+
function RemotePanel({ t, state, copied, onClose, onStop, onRefresh, onCopy, onPickAddress, onPickPublic }) {
|
|
1436
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1437
|
+
className: remote_module_css_default.panel,
|
|
1438
|
+
role: "dialog",
|
|
1439
|
+
"aria-modal": "true",
|
|
1440
|
+
"aria-label": t("title"),
|
|
1441
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1442
|
+
className: remote_module_css_default.header,
|
|
1443
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1444
|
+
className: remote_module_css_default.heading,
|
|
1445
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
1446
|
+
className: remote_module_css_default.title,
|
|
1447
|
+
children: t("title")
|
|
1448
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1449
|
+
className: remote_module_css_default.subtitle,
|
|
1450
|
+
children: t("subtitle")
|
|
1451
|
+
})]
|
|
1452
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1453
|
+
type: "button",
|
|
1454
|
+
className: remote_module_css_default.close,
|
|
1455
|
+
"aria-label": t("close.label"),
|
|
1456
|
+
onClick: onClose,
|
|
1457
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseOutline16, { size: 14 })
|
|
1458
|
+
})]
|
|
1459
|
+
}), state.kind === "lan-required" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1460
|
+
className: remote_module_css_default.banner,
|
|
1461
|
+
role: "alert",
|
|
1462
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1463
|
+
className: remote_module_css_default.bannerTitle,
|
|
1464
|
+
children: t("status.lanRequired")
|
|
1465
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1466
|
+
className: remote_module_css_default.bannerHint,
|
|
1467
|
+
children: t("status.lanRequiredHint")
|
|
1468
|
+
})]
|
|
1469
|
+
}) : state.kind === "loopback-required" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1470
|
+
className: remote_module_css_default.banner,
|
|
1471
|
+
role: "alert",
|
|
1472
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1473
|
+
className: remote_module_css_default.bannerTitle,
|
|
1474
|
+
children: t("status.loopbackRequired")
|
|
1475
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1476
|
+
className: remote_module_css_default.bannerHint,
|
|
1477
|
+
children: t("status.loopbackRequiredHint")
|
|
1478
|
+
})]
|
|
1479
|
+
}) : state.kind === "unreachable" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1480
|
+
className: remote_module_css_default.banner,
|
|
1481
|
+
role: "alert",
|
|
1482
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1483
|
+
className: remote_module_css_default.bannerTitle,
|
|
1484
|
+
children: t("status.unreachable")
|
|
1485
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1486
|
+
className: remote_module_css_default.bannerHint,
|
|
1487
|
+
children: t("status.unreachableHint")
|
|
1488
|
+
})]
|
|
1489
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
1490
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1491
|
+
className: remote_module_css_default.card,
|
|
1492
|
+
children: [
|
|
1493
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1494
|
+
className: remote_module_css_default.cardHeader,
|
|
1495
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1496
|
+
className: remote_module_css_default.cardTitle,
|
|
1497
|
+
children: t("card.title")
|
|
1498
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1499
|
+
className: remote_module_css_default.badges,
|
|
1500
|
+
children: [state.public && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1501
|
+
className: clsx(remote_module_css_default.badge, remote_module_css_default.badgePublic),
|
|
1502
|
+
children: t("public.badge")
|
|
1503
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1504
|
+
className: clsx(remote_module_css_default.badge, remote_module_css_default[`badge-${statusOf(t, state).tone}`]),
|
|
1505
|
+
children: statusOf(t, state).text
|
|
1506
|
+
})]
|
|
1507
|
+
})]
|
|
1508
|
+
}),
|
|
1509
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1510
|
+
className: remote_module_css_default.qrWrap,
|
|
1511
|
+
"data-testid": "remote-qr",
|
|
1512
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(QRCodeSVG, {
|
|
1513
|
+
value: state.url,
|
|
1514
|
+
size: 184,
|
|
1515
|
+
level: "M",
|
|
1516
|
+
marginSize: 1,
|
|
1517
|
+
className: remote_module_css_default.qr
|
|
1518
|
+
})
|
|
1519
|
+
}),
|
|
1520
|
+
state.expired ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1521
|
+
className: remote_module_css_default.expired,
|
|
1522
|
+
children: t("pair.expired")
|
|
1523
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1524
|
+
className: remote_module_css_default.expiry,
|
|
1525
|
+
children: t("pair.expires", { time: formatClock(state.expiresAt) })
|
|
1526
|
+
})
|
|
1527
|
+
]
|
|
1528
|
+
}),
|
|
1529
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1530
|
+
className: remote_module_css_default.hint,
|
|
1531
|
+
children: state.public ? t("pair.publicHint") : t("pair.hint")
|
|
1532
|
+
}),
|
|
1533
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1534
|
+
className: remote_module_css_default.link,
|
|
1535
|
+
title: state.url,
|
|
1536
|
+
children: state.url
|
|
1537
|
+
}),
|
|
1538
|
+
state.phase === "stopped" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1539
|
+
className: remote_module_css_default.stoppedHint,
|
|
1540
|
+
children: t("stopped.hint")
|
|
1541
|
+
}),
|
|
1542
|
+
state.tunnel !== void 0 && state.tunnel.state !== "running" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1543
|
+
className: state.tunnel.state === "failed" ? remote_module_css_default.tunnelFailed : remote_module_css_default.tunnelNote,
|
|
1544
|
+
role: "status",
|
|
1545
|
+
children: state.tunnel.state === "failed" ? t("tunnel.failed", { error: state.tunnel.error ?? t("tunnel.unknownError") }) : t("tunnel.starting")
|
|
1546
|
+
}),
|
|
1547
|
+
(state.publicBaseUrl !== void 0 || state.lanAddresses.length > 1) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
|
|
1548
|
+
className: remote_module_css_default.addresses,
|
|
1549
|
+
children: [
|
|
1550
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("legend", { children: t("address.label") }),
|
|
1551
|
+
state.publicBaseUrl !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1552
|
+
className: remote_module_css_default.address,
|
|
1553
|
+
children: [
|
|
1554
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1555
|
+
type: "radio",
|
|
1556
|
+
name: "lan-address",
|
|
1557
|
+
"aria-label": t("address.public"),
|
|
1558
|
+
checked: state.public,
|
|
1559
|
+
onChange: onPickPublic
|
|
1560
|
+
}),
|
|
1561
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("address.public") }),
|
|
1562
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
1563
|
+
className: remote_module_css_default.addressValue,
|
|
1564
|
+
children: state.publicBaseUrl
|
|
1565
|
+
})
|
|
1566
|
+
]
|
|
1567
|
+
}, "public"),
|
|
1568
|
+
state.lanAddresses.map((address) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1569
|
+
className: remote_module_css_default.address,
|
|
1570
|
+
children: [
|
|
1571
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1572
|
+
type: "radio",
|
|
1573
|
+
name: "lan-address",
|
|
1574
|
+
"aria-label": address,
|
|
1575
|
+
checked: !state.public && address === state.address,
|
|
1576
|
+
onChange: () => onPickAddress(address)
|
|
1577
|
+
}),
|
|
1578
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("address.lan") }),
|
|
1579
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
1580
|
+
className: remote_module_css_default.addressValue,
|
|
1581
|
+
children: address
|
|
1582
|
+
})
|
|
1583
|
+
]
|
|
1584
|
+
}, address)),
|
|
1585
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1586
|
+
className: remote_module_css_default.addressHint,
|
|
1587
|
+
children: t("address.hint")
|
|
1588
|
+
})
|
|
1589
|
+
]
|
|
1590
|
+
}),
|
|
1591
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1592
|
+
className: remote_module_css_default.actions,
|
|
1593
|
+
children: [
|
|
1594
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1595
|
+
type: "button",
|
|
1596
|
+
className: remote_module_css_default.action,
|
|
1597
|
+
onClick: onStop,
|
|
1598
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconStopFill16, { size: 14 }), t("action.stop")]
|
|
1599
|
+
}),
|
|
1600
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1601
|
+
type: "button",
|
|
1602
|
+
className: remote_module_css_default.action,
|
|
1603
|
+
onClick: onRefresh,
|
|
1604
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRefreshOutline16, { size: 14 }), t("action.refresh")]
|
|
1605
|
+
}),
|
|
1606
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1607
|
+
type: "button",
|
|
1608
|
+
className: remote_module_css_default.action,
|
|
1609
|
+
onClick: onCopy,
|
|
1610
|
+
children: [copied ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCopyOutline16, { size: 14 }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconLinkOutline16, { size: 14 }), copied ? t("action.copied") : t("action.copy")]
|
|
1611
|
+
})
|
|
1612
|
+
]
|
|
1613
|
+
})
|
|
1614
|
+
] })]
|
|
1615
|
+
});
|
|
1616
|
+
}
|
|
1617
|
+
//#endregion
|
|
1618
|
+
//#region src/client/PhoneIcon.tsx
|
|
1619
|
+
/**
|
|
1620
|
+
* Render the phone icon.
|
|
1621
|
+
* @param props - size and optional class.
|
|
1622
|
+
* @returns the svg element.
|
|
1623
|
+
*/
|
|
1624
|
+
function PhoneIcon({ size = 16, className }) {
|
|
1625
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
1626
|
+
width: size,
|
|
1627
|
+
height: size,
|
|
1628
|
+
className,
|
|
1629
|
+
viewBox: "0 0 16 16",
|
|
1630
|
+
fill: "none",
|
|
1631
|
+
"aria-hidden": "true",
|
|
1632
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
1633
|
+
d: "M4.2 1.8h2.4l.9 2.4-1.5 1.1a7.4 7.4 0 0 0 4.7 4.7l1.1-1.5 2.4.9v2.4a.9.9 0 0 1-1 .9A11.4 11.4 0 0 1 3.3 2.8a.9.9 0 0 1 .9-1Z",
|
|
1634
|
+
stroke: "currentColor",
|
|
1635
|
+
strokeWidth: "1.3",
|
|
1636
|
+
strokeLinejoin: "round"
|
|
1637
|
+
})
|
|
1638
|
+
});
|
|
1639
|
+
}
|
|
1640
|
+
//#endregion
|
|
1641
|
+
//#region src/client/RemoteEntry.tsx
|
|
1642
|
+
/**
|
|
1643
|
+
* The sidebar remote-control seat: the phone-icon trigger beside the
|
|
1644
|
+
* settings button plus the pairing panel modal. Owns the panel behavior —
|
|
1645
|
+
* token minting on open, the status SSE subscription, stop/refresh/copy —
|
|
1646
|
+
* and renders the pure {@link RemotePanel} body. Component-local state per
|
|
1647
|
+
* the client stack rules: nothing here survives remounts or crosses
|
|
1648
|
+
* entries.
|
|
1649
|
+
*/
|
|
1650
|
+
/** Apply one status frame onto the current ready state. */
|
|
1651
|
+
function mergeFrame(state, frame) {
|
|
1652
|
+
if (state.kind !== "ready") return state;
|
|
1653
|
+
return {
|
|
1654
|
+
...state,
|
|
1655
|
+
phase: frame.phase,
|
|
1656
|
+
deviceCount: frame.deviceCount,
|
|
1657
|
+
onlineCount: frame.onlineCount,
|
|
1658
|
+
...frame.tunnel !== void 0 ? { tunnel: frame.tunnel } : {}
|
|
1659
|
+
};
|
|
1660
|
+
}
|
|
1661
|
+
/**
|
|
1662
|
+
* Render the remote-control trigger and panel.
|
|
1663
|
+
* @param props - composed slot props (contract in this package).
|
|
1664
|
+
* @returns the entry element tree.
|
|
1665
|
+
*/
|
|
1666
|
+
function RemoteEntry({ wide, useWorkspaces, t }) {
|
|
1667
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
1668
|
+
const [state, setState] = (0, react.useState)({ kind: "lan-required" });
|
|
1669
|
+
const [copied, setCopied] = (0, react.useState)(false);
|
|
1670
|
+
const eventSource = (0, react.useRef)(void 0);
|
|
1671
|
+
const workspaceId = useWorkspaces((s) => s.recentWorkspaceId);
|
|
1672
|
+
const closeEventSource = (0, react.useCallback)(() => {
|
|
1673
|
+
eventSource.current?.close();
|
|
1674
|
+
eventSource.current = void 0;
|
|
1675
|
+
}, []);
|
|
1676
|
+
const mint = (0, react.useCallback)(async (address) => {
|
|
1677
|
+
let result;
|
|
1678
|
+
try {
|
|
1679
|
+
result = await issuePair(workspaceId, address);
|
|
1680
|
+
} catch {
|
|
1681
|
+
return { kind: "unreachable" };
|
|
1682
|
+
}
|
|
1683
|
+
if (!result.ok) {
|
|
1684
|
+
if (result.code === "forbidden") return { kind: "loopback-required" };
|
|
1685
|
+
if (result.code === "unknown-address") return { kind: "unreachable" };
|
|
1686
|
+
return { kind: "lan-required" };
|
|
1687
|
+
}
|
|
1688
|
+
const publicBaseUrl = result.publicBaseUrl;
|
|
1689
|
+
return {
|
|
1690
|
+
kind: "ready",
|
|
1691
|
+
url: result.url,
|
|
1692
|
+
expiresAt: result.expiresAt,
|
|
1693
|
+
expired: Date.now() > result.expiresAt,
|
|
1694
|
+
phase: "waiting",
|
|
1695
|
+
deviceCount: 0,
|
|
1696
|
+
onlineCount: 0,
|
|
1697
|
+
public: publicBaseUrl !== void 0 && result.url.startsWith(publicBaseUrl),
|
|
1698
|
+
...publicBaseUrl !== void 0 ? { publicBaseUrl } : {},
|
|
1699
|
+
address: address ?? result.lanAddresses[0] ?? "",
|
|
1700
|
+
lanAddresses: result.lanAddresses
|
|
1701
|
+
};
|
|
1702
|
+
}, [workspaceId]);
|
|
1703
|
+
const openPanel = (0, react.useCallback)(async () => {
|
|
1704
|
+
setOpen(true);
|
|
1705
|
+
const next = await mint();
|
|
1706
|
+
setState(next);
|
|
1707
|
+
if (next.kind !== "ready") return;
|
|
1708
|
+
const source = new EventSource("/api/pair/events");
|
|
1709
|
+
eventSource.current = source;
|
|
1710
|
+
source.onmessage = (event) => {
|
|
1711
|
+
try {
|
|
1712
|
+
const frame = JSON.parse(event.data);
|
|
1713
|
+
if (frame.type !== "state") return;
|
|
1714
|
+
setState((previous) => mergeFrame(previous, frame));
|
|
1715
|
+
} catch {}
|
|
1716
|
+
};
|
|
1717
|
+
}, [mint]);
|
|
1718
|
+
const closePanel = (0, react.useCallback)(() => {
|
|
1719
|
+
closeEventSource();
|
|
1720
|
+
setOpen(false);
|
|
1721
|
+
}, [closeEventSource]);
|
|
1722
|
+
(0, react.useEffect)(() => {
|
|
1723
|
+
if (state.kind !== "ready") return;
|
|
1724
|
+
if (state.expired) return;
|
|
1725
|
+
const delay = state.expiresAt - Date.now();
|
|
1726
|
+
if (delay <= 0) {
|
|
1727
|
+
setState((previous) => previous.kind === "ready" ? {
|
|
1728
|
+
...previous,
|
|
1729
|
+
expired: true
|
|
1730
|
+
} : previous);
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
const timer = window.setTimeout(() => {
|
|
1734
|
+
setState((previous) => previous.kind === "ready" ? {
|
|
1735
|
+
...previous,
|
|
1736
|
+
expired: true
|
|
1737
|
+
} : previous);
|
|
1738
|
+
}, delay);
|
|
1739
|
+
return () => {
|
|
1740
|
+
window.clearTimeout(timer);
|
|
1741
|
+
};
|
|
1742
|
+
}, [state]);
|
|
1743
|
+
(0, react.useEffect)(() => closeEventSource, [closeEventSource]);
|
|
1744
|
+
const handleStop = (0, react.useCallback)(() => {
|
|
1745
|
+
stopPair().catch(() => {});
|
|
1746
|
+
setState((previous) => previous.kind === "ready" ? {
|
|
1747
|
+
...previous,
|
|
1748
|
+
phase: "stopped"
|
|
1749
|
+
} : previous);
|
|
1750
|
+
}, []);
|
|
1751
|
+
const handleRefresh = (0, react.useCallback)(() => {
|
|
1752
|
+
mint().then(setState);
|
|
1753
|
+
}, [mint]);
|
|
1754
|
+
/** Re-mint against another LAN literal (multi-homed machines). */
|
|
1755
|
+
const handlePickAddress = (0, react.useCallback)((address) => {
|
|
1756
|
+
mint(address).then(setState);
|
|
1757
|
+
}, [mint]);
|
|
1758
|
+
/** Re-mint against the configured public (tunneled) base. */
|
|
1759
|
+
const handlePickPublic = (0, react.useCallback)(() => {
|
|
1760
|
+
mint().then(setState);
|
|
1761
|
+
}, [mint]);
|
|
1762
|
+
const handleCopy = (0, react.useCallback)(() => {
|
|
1763
|
+
if (state.kind !== "ready") return;
|
|
1764
|
+
copyText(state.url).then((ok) => {
|
|
1765
|
+
if (!ok) return;
|
|
1766
|
+
setCopied(true);
|
|
1767
|
+
window.setTimeout(() => {
|
|
1768
|
+
setCopied(false);
|
|
1769
|
+
}, 1500);
|
|
1770
|
+
});
|
|
1771
|
+
}, [state]);
|
|
1772
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipAnchor, {
|
|
1773
|
+
wide,
|
|
1774
|
+
label: t("entry.label"),
|
|
1775
|
+
onClick: openPanel
|
|
1776
|
+
}), open && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1777
|
+
className: remote_module_css_default.overlay,
|
|
1778
|
+
role: "presentation",
|
|
1779
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1780
|
+
className: remote_module_css_default.mask,
|
|
1781
|
+
"aria-hidden": "true",
|
|
1782
|
+
onClick: closePanel
|
|
1783
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RemotePanel, {
|
|
1784
|
+
t,
|
|
1785
|
+
state,
|
|
1786
|
+
copied,
|
|
1787
|
+
onClose: closePanel,
|
|
1788
|
+
onStop: handleStop,
|
|
1789
|
+
onRefresh: handleRefresh,
|
|
1790
|
+
onCopy: handleCopy,
|
|
1791
|
+
onPickAddress: handlePickAddress,
|
|
1792
|
+
onPickPublic: handlePickPublic
|
|
1793
|
+
})]
|
|
1794
|
+
}), document.body)] });
|
|
1795
|
+
}
|
|
1796
|
+
/** The trigger: an icon button matching the settings rail/row geometry. */
|
|
1797
|
+
function TooltipAnchor({ wide, label, onClick }) {
|
|
1798
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1799
|
+
type: "button",
|
|
1800
|
+
className: remote_module_css_default.trigger,
|
|
1801
|
+
"data-wide": wide ? void 0 : "rail",
|
|
1802
|
+
"aria-label": label,
|
|
1803
|
+
title: label,
|
|
1804
|
+
onClick,
|
|
1805
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PhoneIcon, { size: wide ? 16 : 18 })
|
|
1806
|
+
});
|
|
1807
|
+
}
|
|
1808
|
+
//#endregion
|
|
1809
|
+
//#region src/client/PairFailedNotice.tsx
|
|
1810
|
+
/**
|
|
1811
|
+
* One-time failed-pairing notice: a fixed toast rendered on the phone after
|
|
1812
|
+
* a QR accept failed (invalid/used token or a network error). Mounted by
|
|
1813
|
+
* the client apply with a plain React root — no slot machinery for a
|
|
1814
|
+
* transient diagnostic.
|
|
1815
|
+
*/
|
|
1816
|
+
/**
|
|
1817
|
+
* Render the failed-pair toast (auto-dismisses).
|
|
1818
|
+
* @param props - localized copy.
|
|
1819
|
+
* @returns the toast element.
|
|
1820
|
+
*/
|
|
1821
|
+
function PairFailedNotice({ t }) {
|
|
1822
|
+
const [visible, setVisible] = (0, react.useState)(true);
|
|
1823
|
+
(0, react.useEffect)(() => {
|
|
1824
|
+
const timer = window.setTimeout(() => {
|
|
1825
|
+
setVisible(false);
|
|
1826
|
+
}, 8e3);
|
|
1827
|
+
return () => {
|
|
1828
|
+
window.clearTimeout(timer);
|
|
1829
|
+
};
|
|
1830
|
+
}, []);
|
|
1831
|
+
if (!visible) return null;
|
|
1832
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1833
|
+
className: remote_module_css_default.notice,
|
|
1834
|
+
role: "alert",
|
|
1835
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1836
|
+
className: remote_module_css_default.noticeTitle,
|
|
1837
|
+
children: t("pair.failed.title")
|
|
1838
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1839
|
+
className: remote_module_css_default.noticeDetail,
|
|
1840
|
+
children: t("pair.failed.detail")
|
|
1841
|
+
})]
|
|
1842
|
+
});
|
|
1843
|
+
}
|
|
1844
|
+
//#endregion
|
|
1845
|
+
//#region \0dsh-css:/Users/zcl/code/dsh-web-ui/packages/dsh-remote-web-ui/src/client/settings-card.module.css.mjs
|
|
1846
|
+
const css = ".jEUd8W_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:8px;list-style:none;transition:border-color .16s,background .16s;overflow:hidden}.jEUd8W_cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}.jEUd8W_header{cursor:pointer;text-align:left;width:100%;font:inherit;background:0 0;border:0;border-radius:8px;align-items:center;gap:8px;padding:10px 14px;transition:background-color .12s,box-shadow .12s;display:flex}.jEUd8W_header:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.jEUd8W_header:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active)}.jEUd8W_header:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-3), 0 0 0 4px var(--dsw-alias-brand-primary);outline:none}.jEUd8W_header:disabled{opacity:.5;cursor:default}.jEUd8W_headText{flex-direction:column;flex:1;gap:2px;min-width:0;display:flex}.jEUd8W_name{color:var(--dsw-alias-label-primary);font-weight:600}.jEUd8W_description{color:var(--dsw-alias-label-tertiary);font-size:12px}.jEUd8W_pending{color:var(--dsw-alias-state-warn-primary);font-size:12px}.jEUd8W_chevron{color:var(--dsw-alias-label-tertiary);transition:transform .12s}.jEUd8W_chevronOpen{transform:rotate(180deg)}.jEUd8W_body{flex-direction:column;gap:14px;padding:0 14px 14px;display:flex}.jEUd8W_readOnly{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px}.jEUd8W_footer{justify-content:flex-end;align-items:center;gap:8px;display:flex}.jEUd8W_failed{color:var(--dsw-alias-state-error-primary);margin:0 auto 0 0;font-size:12px}.jEUd8W_discard,.jEUd8W_save{font:inherit;cursor:pointer;border-radius:6px;padding:5px 12px;font-size:13px;transition:background-color .12s,border-color .12s,color .12s,box-shadow .12s}.jEUd8W_discard{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}.jEUd8W_discard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}.jEUd8W_discard:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active)}.jEUd8W_discard:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-3), 0 0 0 4px var(--dsw-alias-brand-primary);outline:none}.jEUd8W_save{border:1px solid var(--dsw-alias-button-info-fill);background:var(--dsw-alias-button-info-fill);color:var(--dsw-alias-label-primary-foreground)}.jEUd8W_save:hover:not(:disabled){border-color:var(--dsw-alias-button-info-hover);background:var(--dsw-alias-button-info-hover)}.jEUd8W_save:active:not(:disabled){border-color:var(--dsw-alias-button-info-hover);background:var(--dsw-alias-button-info-hover);filter:brightness(.96)}.jEUd8W_save:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-3), 0 0 0 4px var(--dsw-alias-brand-primary);outline:none}.jEUd8W_discard:disabled,.jEUd8W_save:disabled{opacity:.5;cursor:default}.jEUd8W_field{flex-direction:column;gap:4px;display:flex}.jEUd8W_head{align-items:center;gap:8px;display:flex}.jEUd8W_label{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500}.jEUd8W_badges{align-items:center;gap:6px;display:flex}.jEUd8W_badge{background:var(--dsw-alias-interactive-bg-hover-accent);min-width:0;color:var(--dsw-alias-state-business-primary);white-space:nowrap;border-radius:999px;flex:none;padding:1px 6px;font-size:11px}.jEUd8W_reset{color:var(--dsw-alias-state-business-primary);cursor:pointer;background:0 0;border:0;border-radius:4px;flex:none;padding:1px 2px;font-size:11px;transition:background-color .12s,color .12s,box-shadow .12s}.jEUd8W_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.jEUd8W_reset:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active)}.jEUd8W_reset:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-3), 0 0 0 4px var(--dsw-alias-brand-primary);outline:none}.jEUd8W_reset:disabled{opacity:.5;cursor:default}.jEUd8W_input,.jEUd8W_select{border:1px solid var(--dsw-alias-border-l2);font:inherit;color:var(--dsw-alias-label-primary);background:var(--dsw-specific-input-major);border-radius:6px;padding:6px 8px;font-size:13px;transition:border-color .12s,box-shadow .12s}.jEUd8W_input:focus-visible,.jEUd8W_select:focus-visible{border-color:var(--dsw-alias-brand-primary);box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-3), 0 0 0 4px var(--dsw-alias-brand-primary);outline:none}.jEUd8W_inputInvalid{border:1px solid var(--dsw-alias-state-error-primary);font:inherit;color:var(--dsw-alias-label-primary);border-radius:6px;padding:6px 8px;font-size:13px;transition:border-color .12s,box-shadow .12s}.jEUd8W_inputInvalid:focus-visible{border-color:var(--dsw-alias-state-error-primary);box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-3), 0 0 0 4px var(--dsw-alias-state-error-primary);outline:none}.jEUd8W_input:disabled,.jEUd8W_select:disabled{opacity:.6}.jEUd8W_hint{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px}.jEUd8W_invalid{color:var(--dsw-alias-state-error-primary);margin:0;font-size:12px}@media (prefers-reduced-motion:reduce){.jEUd8W_card,.jEUd8W_header,.jEUd8W_chevron,.jEUd8W_discard,.jEUd8W_save,.jEUd8W_reset,.jEUd8W_input,.jEUd8W_select,.jEUd8W_inputInvalid{transition:none}}";
|
|
1847
|
+
const tagId = "@linxin666/dsh-remote-web-ui/settings-card.module.css";
|
|
1848
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
1849
|
+
const tag = document.createElement("style");
|
|
1850
|
+
tag.dataset.plugin = "@linxin666/dsh-remote-web-ui";
|
|
1851
|
+
tag.dataset.pluginCss = tagId;
|
|
1852
|
+
tag.textContent = css;
|
|
1853
|
+
document.head.appendChild(tag);
|
|
1854
|
+
}
|
|
1855
|
+
var settings_card_module_css_default = {
|
|
1856
|
+
"badge": "jEUd8W_badge",
|
|
1857
|
+
"badges": "jEUd8W_badges",
|
|
1858
|
+
"body": "jEUd8W_body",
|
|
1859
|
+
"card": "jEUd8W_card",
|
|
1860
|
+
"cardOpen": "jEUd8W_cardOpen",
|
|
1861
|
+
"chevron": "jEUd8W_chevron",
|
|
1862
|
+
"chevronOpen": "jEUd8W_chevronOpen",
|
|
1863
|
+
"description": "jEUd8W_description",
|
|
1864
|
+
"discard": "jEUd8W_discard",
|
|
1865
|
+
"failed": "jEUd8W_failed",
|
|
1866
|
+
"field": "jEUd8W_field",
|
|
1867
|
+
"footer": "jEUd8W_footer",
|
|
1868
|
+
"head": "jEUd8W_head",
|
|
1869
|
+
"headText": "jEUd8W_headText",
|
|
1870
|
+
"header": "jEUd8W_header",
|
|
1871
|
+
"hint": "jEUd8W_hint",
|
|
1872
|
+
"input": "jEUd8W_input",
|
|
1873
|
+
"inputInvalid": "jEUd8W_inputInvalid",
|
|
1874
|
+
"invalid": "jEUd8W_invalid",
|
|
1875
|
+
"label": "jEUd8W_label",
|
|
1876
|
+
"name": "jEUd8W_name",
|
|
1877
|
+
"pending": "jEUd8W_pending",
|
|
1878
|
+
"readOnly": "jEUd8W_readOnly",
|
|
1879
|
+
"reset": "jEUd8W_reset",
|
|
1880
|
+
"save": "jEUd8W_save",
|
|
1881
|
+
"select": "jEUd8W_select"
|
|
1882
|
+
};
|
|
1883
|
+
//#endregion
|
|
1884
|
+
//#region src/client/PluginSettingsCard.tsx
|
|
1885
|
+
/**
|
|
1886
|
+
* Shared chrome for the plugin settings card: a disclosure header naming the
|
|
1887
|
+
* plugin and what its settings govern, the controls inside, and the save that
|
|
1888
|
+
* writes them. Renders nothing while the namespace is unavailable — a
|
|
1889
|
+
* deployment that does not compose the owning plugin should show no trace of
|
|
1890
|
+
* it. Mirrors the official ui-plugin-config PluginCard in a self-contained
|
|
1891
|
+
* slice (this package must not depend on a sibling UI package).
|
|
1892
|
+
*/
|
|
1893
|
+
/**
|
|
1894
|
+
* Render one plugin settings card.
|
|
1895
|
+
* @param props - the plugin's copy keys, its form state, and its controls.
|
|
1896
|
+
* @returns the card, or nothing when the namespace is unavailable.
|
|
1897
|
+
*/
|
|
1898
|
+
function PluginSettingsCard(props) {
|
|
1899
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
1900
|
+
const { state } = props;
|
|
1901
|
+
if (!state.available) return null;
|
|
1902
|
+
const title = props.t(props.titleKey);
|
|
1903
|
+
const blocked = !state.dirty || state.invalid || state.saving;
|
|
1904
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
1905
|
+
className: settings_card_module_css_default.card,
|
|
1906
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1907
|
+
type: "button",
|
|
1908
|
+
className: settings_card_module_css_default.header,
|
|
1909
|
+
"aria-expanded": open,
|
|
1910
|
+
"aria-label": `${props.t(open ? "settings.collapse" : "settings.expand")}: ${title}`,
|
|
1911
|
+
onClick: () => {
|
|
1912
|
+
setOpen(!open);
|
|
1913
|
+
},
|
|
1914
|
+
children: [
|
|
1915
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1916
|
+
className: settings_card_module_css_default.headText,
|
|
1917
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1918
|
+
className: settings_card_module_css_default.name,
|
|
1919
|
+
children: title
|
|
1920
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1921
|
+
className: settings_card_module_css_default.description,
|
|
1922
|
+
children: props.t(props.descriptionKey)
|
|
1923
|
+
})]
|
|
1924
|
+
}),
|
|
1925
|
+
state.dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1926
|
+
className: settings_card_module_css_default.pending,
|
|
1927
|
+
children: props.t("settings.unsaved")
|
|
1928
|
+
}) : null,
|
|
1929
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1930
|
+
className: open ? settings_card_module_css_default.chevronOpen : settings_card_module_css_default.chevron,
|
|
1931
|
+
children: "▾"
|
|
1932
|
+
})
|
|
1933
|
+
]
|
|
1934
|
+
}), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1935
|
+
className: settings_card_module_css_default.body,
|
|
1936
|
+
children: [
|
|
1937
|
+
!state.writable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1938
|
+
className: settings_card_module_css_default.readOnly,
|
|
1939
|
+
role: "status",
|
|
1940
|
+
children: props.t("settings.readOnly")
|
|
1941
|
+
}) : null,
|
|
1942
|
+
props.children,
|
|
1943
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1944
|
+
className: settings_card_module_css_default.footer,
|
|
1945
|
+
children: [
|
|
1946
|
+
state.failed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1947
|
+
className: settings_card_module_css_default.failed,
|
|
1948
|
+
role: "status",
|
|
1949
|
+
children: props.t("settings.saveFailed")
|
|
1950
|
+
}) : null,
|
|
1951
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1952
|
+
type: "button",
|
|
1953
|
+
className: settings_card_module_css_default.discard,
|
|
1954
|
+
disabled: !state.dirty || state.saving,
|
|
1955
|
+
onClick: props.onDiscard,
|
|
1956
|
+
children: props.t("settings.discard")
|
|
1957
|
+
}),
|
|
1958
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1959
|
+
type: "button",
|
|
1960
|
+
className: settings_card_module_css_default.save,
|
|
1961
|
+
disabled: blocked,
|
|
1962
|
+
onClick: props.onSave,
|
|
1963
|
+
children: props.t(!state.saving ? "settings.save" : "settings.saving")
|
|
1964
|
+
})
|
|
1965
|
+
]
|
|
1966
|
+
})
|
|
1967
|
+
]
|
|
1968
|
+
}) : null]
|
|
1969
|
+
});
|
|
1970
|
+
}
|
|
1971
|
+
/** A staged value field. `numeric` only hints the keypad: which drafts a field accepts is decided by its spec. */
|
|
1972
|
+
function ValueField(props) {
|
|
1973
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1974
|
+
className: settings_card_module_css_default.field,
|
|
1975
|
+
children: [
|
|
1976
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1977
|
+
className: settings_card_module_css_default.head,
|
|
1978
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
1979
|
+
className: settings_card_module_css_default.label,
|
|
1980
|
+
htmlFor: props.id,
|
|
1981
|
+
children: props.label
|
|
1982
|
+
}), props.overridden ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1983
|
+
className: settings_card_module_css_default.badges,
|
|
1984
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1985
|
+
className: settings_card_module_css_default.badge,
|
|
1986
|
+
children: props.overriddenLabel
|
|
1987
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1988
|
+
type: "button",
|
|
1989
|
+
className: settings_card_module_css_default.reset,
|
|
1990
|
+
disabled: props.disabled,
|
|
1991
|
+
onClick: props.onReset,
|
|
1992
|
+
children: props.resetLabel
|
|
1993
|
+
})]
|
|
1994
|
+
}) : null]
|
|
1995
|
+
}),
|
|
1996
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1997
|
+
id: props.id,
|
|
1998
|
+
className: props.invalid ? settings_card_module_css_default.inputInvalid : settings_card_module_css_default.input,
|
|
1999
|
+
type: "text",
|
|
2000
|
+
...props.numeric === true ? { inputMode: "numeric" } : {},
|
|
2001
|
+
...props.invalid ? { "aria-invalid": true } : {},
|
|
2002
|
+
value: props.text,
|
|
2003
|
+
placeholder: props.placeholder ?? "",
|
|
2004
|
+
disabled: props.disabled,
|
|
2005
|
+
onChange: (event) => {
|
|
2006
|
+
props.onEdit(event.target.value);
|
|
2007
|
+
}
|
|
2008
|
+
}),
|
|
2009
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2010
|
+
className: props.invalid ? settings_card_module_css_default.invalid : settings_card_module_css_default.hint,
|
|
2011
|
+
children: props.invalid ? props.invalidLabel : props.hint
|
|
2012
|
+
})
|
|
2013
|
+
]
|
|
2014
|
+
});
|
|
2015
|
+
}
|
|
2016
|
+
/** A staged boolean field: 继承 / 开 / 关. */
|
|
2017
|
+
function BooleanField(props) {
|
|
2018
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2019
|
+
className: settings_card_module_css_default.field,
|
|
2020
|
+
children: [
|
|
2021
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2022
|
+
className: settings_card_module_css_default.head,
|
|
2023
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
2024
|
+
className: settings_card_module_css_default.label,
|
|
2025
|
+
htmlFor: props.id,
|
|
2026
|
+
children: props.label
|
|
2027
|
+
}), props.overridden ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2028
|
+
className: settings_card_module_css_default.badges,
|
|
2029
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2030
|
+
className: settings_card_module_css_default.badge,
|
|
2031
|
+
children: props.overriddenLabel
|
|
2032
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2033
|
+
type: "button",
|
|
2034
|
+
className: settings_card_module_css_default.reset,
|
|
2035
|
+
disabled: props.disabled,
|
|
2036
|
+
onClick: props.onReset,
|
|
2037
|
+
children: props.resetLabel
|
|
2038
|
+
})]
|
|
2039
|
+
}) : null]
|
|
2040
|
+
}),
|
|
2041
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
2042
|
+
id: props.id,
|
|
2043
|
+
className: settings_card_module_css_default.select,
|
|
2044
|
+
value: props.text,
|
|
2045
|
+
disabled: props.disabled,
|
|
2046
|
+
onChange: (event) => {
|
|
2047
|
+
props.onEdit(event.target.value);
|
|
2048
|
+
},
|
|
2049
|
+
children: [
|
|
2050
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
2051
|
+
value: "",
|
|
2052
|
+
children: props.inheritLabel
|
|
2053
|
+
}),
|
|
2054
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
2055
|
+
value: "true",
|
|
2056
|
+
children: props.onLabel
|
|
2057
|
+
}),
|
|
2058
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
2059
|
+
value: "false",
|
|
2060
|
+
children: props.offLabel
|
|
2061
|
+
})
|
|
2062
|
+
]
|
|
2063
|
+
}),
|
|
2064
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2065
|
+
className: settings_card_module_css_default.hint,
|
|
2066
|
+
children: props.hint
|
|
2067
|
+
})
|
|
2068
|
+
]
|
|
2069
|
+
});
|
|
2070
|
+
}
|
|
2071
|
+
//#endregion
|
|
2072
|
+
//#region src/client/settings-form.ts
|
|
2073
|
+
/** A whole-number field. An empty draft clears the field; any other draft that is not a finite number blocks the save. */
|
|
2074
|
+
function numberField(field) {
|
|
2075
|
+
return {
|
|
2076
|
+
field,
|
|
2077
|
+
format: (value) => typeof value === "number" ? String(value) : "",
|
|
2078
|
+
parse: (text) => {
|
|
2079
|
+
const trimmed = text.trim();
|
|
2080
|
+
if (trimmed === "") return { kind: "clear" };
|
|
2081
|
+
const parsed = Number(trimmed);
|
|
2082
|
+
return Number.isFinite(parsed) ? {
|
|
2083
|
+
kind: "set",
|
|
2084
|
+
value: parsed
|
|
2085
|
+
} : void 0;
|
|
2086
|
+
}
|
|
2087
|
+
};
|
|
2088
|
+
}
|
|
2089
|
+
/** A free-text field. An empty draft clears the field. */
|
|
2090
|
+
function textField(field) {
|
|
2091
|
+
return {
|
|
2092
|
+
field,
|
|
2093
|
+
format: (value) => typeof value === "string" ? value : "",
|
|
2094
|
+
parse: (text) => {
|
|
2095
|
+
const trimmed = text.trim();
|
|
2096
|
+
return trimmed === "" ? { kind: "clear" } : {
|
|
2097
|
+
kind: "set",
|
|
2098
|
+
value: trimmed
|
|
2099
|
+
};
|
|
2100
|
+
}
|
|
2101
|
+
};
|
|
2102
|
+
}
|
|
2103
|
+
/** A boolean field, edited through true/false draft text. */
|
|
2104
|
+
function booleanField(field) {
|
|
2105
|
+
return {
|
|
2106
|
+
field,
|
|
2107
|
+
format: (value) => typeof value === "boolean" ? String(value) : "",
|
|
2108
|
+
parse: (text) => {
|
|
2109
|
+
if (text === "true") return {
|
|
2110
|
+
kind: "set",
|
|
2111
|
+
value: true
|
|
2112
|
+
};
|
|
2113
|
+
if (text === "false") return {
|
|
2114
|
+
kind: "set",
|
|
2115
|
+
value: false
|
|
2116
|
+
};
|
|
2117
|
+
}
|
|
2118
|
+
};
|
|
2119
|
+
}
|
|
2120
|
+
/**
|
|
2121
|
+
* Stages one card's edits over one settings namespace and writes them on save.
|
|
2122
|
+
*
|
|
2123
|
+
* The Host is the only authority on whether a value was accepted — its
|
|
2124
|
+
* validators own the constraints no schema can express — so the outcome is
|
|
2125
|
+
* read back from the section rather than predicted here. A save that did not
|
|
2126
|
+
* land keeps its drafts, so the user can correct them instead of retyping.
|
|
2127
|
+
*/
|
|
2128
|
+
var CardForm = class {
|
|
2129
|
+
scope;
|
|
2130
|
+
specs;
|
|
2131
|
+
staged = /* @__PURE__ */ new Map();
|
|
2132
|
+
listeners = /* @__PURE__ */ new Set();
|
|
2133
|
+
saving = false;
|
|
2134
|
+
failed = false;
|
|
2135
|
+
/** @param scope - the bound settings scope for this card's namespace. */
|
|
2136
|
+
constructor(scope, specs) {
|
|
2137
|
+
this.scope = scope;
|
|
2138
|
+
this.specs = new Map(specs.map((spec) => [spec.field, spec]));
|
|
2139
|
+
scope.subscribe(() => {
|
|
2140
|
+
this.publish();
|
|
2141
|
+
});
|
|
2142
|
+
}
|
|
2143
|
+
/** Publish a projection of this form, rebuilt whenever the scope or a draft changes. */
|
|
2144
|
+
bind(project) {
|
|
2145
|
+
const store = (0, _deepseek_ai_dsh_client_runtime_client.createSnapshotStore)(project());
|
|
2146
|
+
this.listeners.add(() => {
|
|
2147
|
+
store.set(project());
|
|
2148
|
+
});
|
|
2149
|
+
return store;
|
|
2150
|
+
}
|
|
2151
|
+
/** Read the card-level state: what the Host serves, and what a save would do. */
|
|
2152
|
+
shell() {
|
|
2153
|
+
const snapshot = this.scope.getSnapshot();
|
|
2154
|
+
const plan = this.plan();
|
|
2155
|
+
return {
|
|
2156
|
+
available: snapshot.status === "ready",
|
|
2157
|
+
writable: snapshot.writable,
|
|
2158
|
+
dirty: plan.length > 0,
|
|
2159
|
+
invalid: plan.some((item) => item.run === void 0),
|
|
2160
|
+
saving: this.saving,
|
|
2161
|
+
failed: this.failed
|
|
2162
|
+
};
|
|
2163
|
+
}
|
|
2164
|
+
/** Read one field's state from the effective section and its staged draft. */
|
|
2165
|
+
field(field) {
|
|
2166
|
+
const spec = this.specOf(field);
|
|
2167
|
+
const staged = this.staged.get(field);
|
|
2168
|
+
if (staged === void 0) return {
|
|
2169
|
+
text: spec.format(this.sectionValue(field)),
|
|
2170
|
+
overridden: this.stored(field),
|
|
2171
|
+
invalid: false
|
|
2172
|
+
};
|
|
2173
|
+
const write = staged.clear ? { kind: "clear" } : spec.parse(staged.text);
|
|
2174
|
+
return {
|
|
2175
|
+
text: staged.text,
|
|
2176
|
+
overridden: write?.kind === "set",
|
|
2177
|
+
invalid: write === void 0
|
|
2178
|
+
};
|
|
2179
|
+
}
|
|
2180
|
+
/** The actions the card's slot registration injects. */
|
|
2181
|
+
actions() {
|
|
2182
|
+
return {
|
|
2183
|
+
edit: (field, text) => {
|
|
2184
|
+
this.stage(field, {
|
|
2185
|
+
text,
|
|
2186
|
+
clear: false
|
|
2187
|
+
});
|
|
2188
|
+
},
|
|
2189
|
+
resetField: (field) => {
|
|
2190
|
+
this.stage(field, {
|
|
2191
|
+
text: this.specOf(field).format(this.baseValue(field)),
|
|
2192
|
+
clear: true
|
|
2193
|
+
});
|
|
2194
|
+
},
|
|
2195
|
+
save: () => {
|
|
2196
|
+
this.save();
|
|
2197
|
+
},
|
|
2198
|
+
discard: () => {
|
|
2199
|
+
if (this.staged.size === 0 && !this.failed) return;
|
|
2200
|
+
this.staged.clear();
|
|
2201
|
+
this.failed = false;
|
|
2202
|
+
this.publish();
|
|
2203
|
+
}
|
|
2204
|
+
};
|
|
2205
|
+
}
|
|
2206
|
+
/**
|
|
2207
|
+
* Write every staged edit, then re-seed from what the Host accepted.
|
|
2208
|
+
* @returns settlement after every write and the read-back.
|
|
2209
|
+
*/
|
|
2210
|
+
async save() {
|
|
2211
|
+
const plan = this.plan();
|
|
2212
|
+
const writes = plan.flatMap((item) => item.run === void 0 ? [] : [item.run]);
|
|
2213
|
+
if (plan.length === 0 || this.saving || writes.length !== plan.length) return;
|
|
2214
|
+
this.saving = true;
|
|
2215
|
+
this.failed = false;
|
|
2216
|
+
this.publish();
|
|
2217
|
+
let landed = true;
|
|
2218
|
+
for (const write of writes) landed = await write() && landed;
|
|
2219
|
+
if (landed) this.staged.clear();
|
|
2220
|
+
this.saving = false;
|
|
2221
|
+
this.failed = !landed;
|
|
2222
|
+
this.publish();
|
|
2223
|
+
}
|
|
2224
|
+
/**
|
|
2225
|
+
* Every staged edit a save would write. An entry whose draft is not a value
|
|
2226
|
+
* its field accepts carries no write: the form is still dirty, and the save
|
|
2227
|
+
* refuses rather than dropping the edit. A staged edit that matches the
|
|
2228
|
+
* effective section is not a write at all.
|
|
2229
|
+
* @returns the planned writes, in the order the fields were staged.
|
|
2230
|
+
*/
|
|
2231
|
+
plan() {
|
|
2232
|
+
const plan = [];
|
|
2233
|
+
for (const [field, staged] of this.staged) {
|
|
2234
|
+
const spec = this.specOf(field);
|
|
2235
|
+
if (staged.clear) {
|
|
2236
|
+
if (this.stored(field)) plan.push({
|
|
2237
|
+
field,
|
|
2238
|
+
run: () => this.clear(field)
|
|
2239
|
+
});
|
|
2240
|
+
continue;
|
|
2241
|
+
}
|
|
2242
|
+
if (staged.text === spec.format(this.sectionValue(field))) continue;
|
|
2243
|
+
const write = spec.parse(staged.text);
|
|
2244
|
+
if (write === void 0) plan.push({
|
|
2245
|
+
field,
|
|
2246
|
+
run: void 0
|
|
2247
|
+
});
|
|
2248
|
+
else if (write.kind === "clear") plan.push({
|
|
2249
|
+
field,
|
|
2250
|
+
run: () => this.clear(field)
|
|
2251
|
+
});
|
|
2252
|
+
else plan.push({
|
|
2253
|
+
field,
|
|
2254
|
+
run: () => this.store(field, write.value)
|
|
2255
|
+
});
|
|
2256
|
+
}
|
|
2257
|
+
return plan;
|
|
2258
|
+
}
|
|
2259
|
+
async clear(field) {
|
|
2260
|
+
await this.scope.unset(field);
|
|
2261
|
+
return !this.stored(field);
|
|
2262
|
+
}
|
|
2263
|
+
async store(field, value) {
|
|
2264
|
+
await this.scope.set(field, value);
|
|
2265
|
+
return this.userLayer()?.[field] === value;
|
|
2266
|
+
}
|
|
2267
|
+
stage(field, edit) {
|
|
2268
|
+
this.staged.set(field, edit);
|
|
2269
|
+
this.failed = false;
|
|
2270
|
+
this.publish();
|
|
2271
|
+
}
|
|
2272
|
+
specOf(field) {
|
|
2273
|
+
const spec = this.specs.get(field);
|
|
2274
|
+
if (spec === void 0) throw new Error(`settings card has no field ${field}`);
|
|
2275
|
+
return spec;
|
|
2276
|
+
}
|
|
2277
|
+
snapshotOf() {
|
|
2278
|
+
return this.scope.getSnapshot();
|
|
2279
|
+
}
|
|
2280
|
+
sectionValue(field) {
|
|
2281
|
+
return this.snapshotOf().value?.[field];
|
|
2282
|
+
}
|
|
2283
|
+
baseValue(field) {
|
|
2284
|
+
return this.snapshotOf().base?.[field];
|
|
2285
|
+
}
|
|
2286
|
+
userLayer() {
|
|
2287
|
+
return this.snapshotOf().user;
|
|
2288
|
+
}
|
|
2289
|
+
stored(field) {
|
|
2290
|
+
const user = this.userLayer();
|
|
2291
|
+
return user !== void 0 && Object.hasOwn(user, field);
|
|
2292
|
+
}
|
|
2293
|
+
publish() {
|
|
2294
|
+
for (const listener of this.listeners) listener();
|
|
2295
|
+
}
|
|
2296
|
+
};
|
|
2297
|
+
//#endregion
|
|
2298
|
+
//#region src/client/RemoteSettingsCard.tsx
|
|
2299
|
+
/** Bridges the `remote-web-ui` scope onto the card's staged form. */
|
|
2300
|
+
var RemoteSettingsCardController = class {
|
|
2301
|
+
form;
|
|
2302
|
+
store;
|
|
2303
|
+
/** @param scope - the bound settings scope for the `remote-web-ui` namespace. */
|
|
2304
|
+
constructor(scope) {
|
|
2305
|
+
this.form = new CardForm(scope, [
|
|
2306
|
+
booleanField("enabled"),
|
|
2307
|
+
numberField("tokenTtlMs"),
|
|
2308
|
+
numberField("offlineAfterMs"),
|
|
2309
|
+
numberField("maxDevices"),
|
|
2310
|
+
textField("cookieName"),
|
|
2311
|
+
booleanField("requirePairingForLan"),
|
|
2312
|
+
textField("publicBaseUrl"),
|
|
2313
|
+
booleanField("autoTunnel")
|
|
2314
|
+
]);
|
|
2315
|
+
this.store = this.form.bind(() => this.projection());
|
|
2316
|
+
}
|
|
2317
|
+
projection() {
|
|
2318
|
+
return {
|
|
2319
|
+
...this.form.shell(),
|
|
2320
|
+
enabled: this.form.field("enabled"),
|
|
2321
|
+
tokenTtlMs: this.form.field("tokenTtlMs"),
|
|
2322
|
+
offlineAfterMs: this.form.field("offlineAfterMs"),
|
|
2323
|
+
maxDevices: this.form.field("maxDevices"),
|
|
2324
|
+
cookieName: this.form.field("cookieName"),
|
|
2325
|
+
requirePairingForLan: this.form.field("requirePairingForLan"),
|
|
2326
|
+
publicBaseUrl: this.form.field("publicBaseUrl"),
|
|
2327
|
+
autoTunnel: this.form.field("autoTunnel")
|
|
2328
|
+
};
|
|
2329
|
+
}
|
|
2330
|
+
/**
|
|
2331
|
+
* Build the face the card's slot registration injects.
|
|
2332
|
+
* @returns the card's snapshot and its form actions.
|
|
2333
|
+
*/
|
|
2334
|
+
inject() {
|
|
2335
|
+
return {
|
|
2336
|
+
hooks: { remoteSettingsCard: this.store },
|
|
2337
|
+
...this.form.actions()
|
|
2338
|
+
};
|
|
2339
|
+
}
|
|
2340
|
+
};
|
|
2341
|
+
/**
|
|
2342
|
+
* Render the remote-control card.
|
|
2343
|
+
* @param props - locale copy, the card snapshot, and its form actions.
|
|
2344
|
+
* @returns the card.
|
|
2345
|
+
*/
|
|
2346
|
+
function RemoteSettingsCard(props) {
|
|
2347
|
+
const { t } = props;
|
|
2348
|
+
const state = props.useRemoteSettingsCard((snapshot) => snapshot);
|
|
2349
|
+
const disabled = !state.writable;
|
|
2350
|
+
const fieldProps = {
|
|
2351
|
+
overriddenLabel: t("settings.overridden"),
|
|
2352
|
+
resetLabel: t("settings.reset"),
|
|
2353
|
+
invalidLabel: t("settings.invalidNumber"),
|
|
2354
|
+
disabled
|
|
2355
|
+
};
|
|
2356
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(PluginSettingsCard, {
|
|
2357
|
+
t,
|
|
2358
|
+
titleKey: "settings.title",
|
|
2359
|
+
descriptionKey: "settings.description",
|
|
2360
|
+
state,
|
|
2361
|
+
onSave: props.save,
|
|
2362
|
+
onDiscard: props.discard,
|
|
2363
|
+
children: [
|
|
2364
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(BooleanField, {
|
|
2365
|
+
id: "settings-remote-enabled",
|
|
2366
|
+
label: t("settings.enabled"),
|
|
2367
|
+
hint: t("settings.enabledHint"),
|
|
2368
|
+
inheritLabel: t("settings.inherit"),
|
|
2369
|
+
onLabel: t("settings.on"),
|
|
2370
|
+
offLabel: t("settings.off"),
|
|
2371
|
+
...fieldProps,
|
|
2372
|
+
...state.enabled,
|
|
2373
|
+
onEdit: (text) => {
|
|
2374
|
+
props.edit("enabled", text);
|
|
2375
|
+
},
|
|
2376
|
+
onReset: () => {
|
|
2377
|
+
props.resetField("enabled");
|
|
2378
|
+
}
|
|
2379
|
+
}),
|
|
2380
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ValueField, {
|
|
2381
|
+
id: "settings-remote-token-ttl",
|
|
2382
|
+
label: t("settings.tokenTtlMs"),
|
|
2383
|
+
hint: t("settings.tokenTtlMsHint"),
|
|
2384
|
+
numeric: true,
|
|
2385
|
+
...fieldProps,
|
|
2386
|
+
...state.tokenTtlMs,
|
|
2387
|
+
onEdit: (text) => {
|
|
2388
|
+
props.edit("tokenTtlMs", text);
|
|
2389
|
+
},
|
|
2390
|
+
onReset: () => {
|
|
2391
|
+
props.resetField("tokenTtlMs");
|
|
2392
|
+
}
|
|
2393
|
+
}),
|
|
2394
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ValueField, {
|
|
2395
|
+
id: "settings-remote-offline",
|
|
2396
|
+
label: t("settings.offlineAfterMs"),
|
|
2397
|
+
hint: t("settings.offlineAfterMsHint"),
|
|
2398
|
+
numeric: true,
|
|
2399
|
+
...fieldProps,
|
|
2400
|
+
...state.offlineAfterMs,
|
|
2401
|
+
onEdit: (text) => {
|
|
2402
|
+
props.edit("offlineAfterMs", text);
|
|
2403
|
+
},
|
|
2404
|
+
onReset: () => {
|
|
2405
|
+
props.resetField("offlineAfterMs");
|
|
2406
|
+
}
|
|
2407
|
+
}),
|
|
2408
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ValueField, {
|
|
2409
|
+
id: "settings-remote-max-devices",
|
|
2410
|
+
label: t("settings.maxDevices"),
|
|
2411
|
+
hint: t("settings.maxDevicesHint"),
|
|
2412
|
+
numeric: true,
|
|
2413
|
+
...fieldProps,
|
|
2414
|
+
...state.maxDevices,
|
|
2415
|
+
onEdit: (text) => {
|
|
2416
|
+
props.edit("maxDevices", text);
|
|
2417
|
+
},
|
|
2418
|
+
onReset: () => {
|
|
2419
|
+
props.resetField("maxDevices");
|
|
2420
|
+
}
|
|
2421
|
+
}),
|
|
2422
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ValueField, {
|
|
2423
|
+
id: "settings-remote-cookie",
|
|
2424
|
+
label: t("settings.cookieName"),
|
|
2425
|
+
hint: t("settings.cookieNameHint"),
|
|
2426
|
+
...fieldProps,
|
|
2427
|
+
...state.cookieName,
|
|
2428
|
+
onEdit: (text) => {
|
|
2429
|
+
props.edit("cookieName", text);
|
|
2430
|
+
},
|
|
2431
|
+
onReset: () => {
|
|
2432
|
+
props.resetField("cookieName");
|
|
2433
|
+
}
|
|
2434
|
+
}),
|
|
2435
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(BooleanField, {
|
|
2436
|
+
id: "settings-remote-fence",
|
|
2437
|
+
label: t("settings.requirePairingForLan"),
|
|
2438
|
+
hint: t("settings.requirePairingForLanHint"),
|
|
2439
|
+
inheritLabel: t("settings.inherit"),
|
|
2440
|
+
onLabel: t("settings.on"),
|
|
2441
|
+
offLabel: t("settings.off"),
|
|
2442
|
+
...fieldProps,
|
|
2443
|
+
...state.requirePairingForLan,
|
|
2444
|
+
onEdit: (text) => {
|
|
2445
|
+
props.edit("requirePairingForLan", text);
|
|
2446
|
+
},
|
|
2447
|
+
onReset: () => {
|
|
2448
|
+
props.resetField("requirePairingForLan");
|
|
2449
|
+
}
|
|
2450
|
+
}),
|
|
2451
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ValueField, {
|
|
2452
|
+
id: "settings-remote-public-base",
|
|
2453
|
+
label: t("settings.publicBaseUrl"),
|
|
2454
|
+
hint: t("settings.publicBaseUrlHint"),
|
|
2455
|
+
placeholder: "https://example.trycloudflare.com",
|
|
2456
|
+
...fieldProps,
|
|
2457
|
+
...state.publicBaseUrl,
|
|
2458
|
+
onEdit: (text) => {
|
|
2459
|
+
props.edit("publicBaseUrl", text);
|
|
2460
|
+
},
|
|
2461
|
+
onReset: () => {
|
|
2462
|
+
props.resetField("publicBaseUrl");
|
|
2463
|
+
}
|
|
2464
|
+
}),
|
|
2465
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(BooleanField, {
|
|
2466
|
+
id: "settings-remote-auto-tunnel",
|
|
2467
|
+
label: t("settings.autoTunnel"),
|
|
2468
|
+
hint: t("settings.autoTunnelHint"),
|
|
2469
|
+
inheritLabel: t("settings.inherit"),
|
|
2470
|
+
onLabel: t("settings.on"),
|
|
2471
|
+
offLabel: t("settings.off"),
|
|
2472
|
+
...fieldProps,
|
|
2473
|
+
...state.autoTunnel,
|
|
2474
|
+
onEdit: (text) => {
|
|
2475
|
+
props.edit("autoTunnel", text);
|
|
2476
|
+
},
|
|
2477
|
+
onReset: () => {
|
|
2478
|
+
props.resetField("autoTunnel");
|
|
2479
|
+
}
|
|
2480
|
+
})
|
|
2481
|
+
]
|
|
2482
|
+
});
|
|
2483
|
+
}
|
|
2484
|
+
//#endregion
|
|
2485
|
+
//#region src/client/locales.ts
|
|
2486
|
+
/** `remote` namespace dictionaries: the mobile remote-control surface copy. */
|
|
2487
|
+
/** Simplified Chinese dictionary (the key-set source of truth). */
|
|
2488
|
+
const zh = {
|
|
2489
|
+
"entry.label": "移动端远程控制",
|
|
2490
|
+
"title": "移动端远程控制",
|
|
2491
|
+
"subtitle": "扫码或在手机上打开链接,即可远程控制当前工作区",
|
|
2492
|
+
"card.title": "手机扫码连接",
|
|
2493
|
+
"public.badge": "公网",
|
|
2494
|
+
"status.waiting": "等待手机连接",
|
|
2495
|
+
"status.connected": "已连接 {n} 台设备",
|
|
2496
|
+
"status.disconnected": "已配对设备离线",
|
|
2497
|
+
"status.stopped": "已停止移动端访问",
|
|
2498
|
+
"status.lanRequired": "此功能需要以 --host 0.0.0.0 启动 dsh web,或配置公网地址才能使用",
|
|
2499
|
+
"status.lanRequiredHint": "当前服务仅绑定在 127.0.0.1 且未配置公网地址,手机无法访问。请用 dsh web --host 0.0.0.0 重新启动,或在设置中填写内网穿透的公网地址。",
|
|
2500
|
+
"status.loopbackRequired": "配对面板仅限本机使用",
|
|
2501
|
+
"status.loopbackRequiredHint": "请通过 http://127.0.0.1 打开此页面后重试;手机请使用配对链接访问。",
|
|
2502
|
+
"status.unreachable": "无法连接配对服务",
|
|
2503
|
+
"status.unreachableHint": "请刷新页面后重试。",
|
|
2504
|
+
"pair.hint": "无法扫码?可以在手机上打开链接",
|
|
2505
|
+
"pair.publicHint": "公网链接:手机无需与电脑在同一网络",
|
|
2506
|
+
"address.label": "选择二维码指向的网络",
|
|
2507
|
+
"address.public": "公网地址",
|
|
2508
|
+
"address.lan": "局域网",
|
|
2509
|
+
"address.hint": "公网地址适合手机不在同一网络时使用;局域网地址仅在手机与电脑同一网络时可用。",
|
|
2510
|
+
"pair.expires": "二维码有效至 {time}",
|
|
2511
|
+
"pair.expired": "二维码已过期,请刷新",
|
|
2512
|
+
"pair.failed.title": "配对失败",
|
|
2513
|
+
"pair.failed.detail": "链接无效或已使用,请回到电脑端刷新二维码后重新扫码。",
|
|
2514
|
+
"action.stop": "停止",
|
|
2515
|
+
"action.refresh": "刷新二维码",
|
|
2516
|
+
"action.copy": "复制链接",
|
|
2517
|
+
"action.copied": "已复制",
|
|
2518
|
+
"stopped.hint": "已停止移动端访问。点击\"刷新二维码\"重新开启。",
|
|
2519
|
+
"tunnel.starting": "公网隧道启动中,二维码将自动变为公网链接…",
|
|
2520
|
+
"tunnel.failed": "公网隧道启动失败:{error}",
|
|
2521
|
+
"tunnel.unknownError": "未知错误",
|
|
2522
|
+
"close.label": "关闭移动端远程控制面板",
|
|
2523
|
+
"settings.title": "移动端远程控制",
|
|
2524
|
+
"settings.description": "配对安全与设备限额。",
|
|
2525
|
+
"settings.enabled": "启用移动端远程控制",
|
|
2526
|
+
"settings.enabledHint": "关闭后移除侧边栏入口并停用配对路由与局域网栅栏。",
|
|
2527
|
+
"settings.tokenTtlMs": "配对令牌有效期(毫秒)",
|
|
2528
|
+
"settings.tokenTtlMsHint": "新生成的二维码链接在此时间后失效。",
|
|
2529
|
+
"settings.offlineAfterMs": "设备离线判定(毫秒)",
|
|
2530
|
+
"settings.offlineAfterMsHint": "配对设备超过该时长未上报心跳即视为离线。",
|
|
2531
|
+
"settings.maxDevices": "已配对设备上限",
|
|
2532
|
+
"settings.maxDevicesHint": "超过上限时淘汰最旧的设备会话。",
|
|
2533
|
+
"settings.cookieName": "设备 Cookie 名",
|
|
2534
|
+
"settings.cookieNameHint": "携带已配对设备标识的 Cookie 名称。",
|
|
2535
|
+
"settings.requirePairingForLan": "局域网访问要求配对",
|
|
2536
|
+
"settings.requirePairingForLanHint": "开启:所有非本机回环的 /api 请求必须携带有效配对 Cookie;关闭:局域网栅栏放开,配对仅用于令牌与状态管理。",
|
|
2537
|
+
"settings.publicBaseUrl": "公网地址(可选)",
|
|
2538
|
+
"settings.publicBaseUrlHint": "内网穿透的公网地址,如 Cloudflare Tunnel:https://xxx.trycloudflare.com。填写后二维码将生成公网链接,手机不在同一网络也能配对;留空则仅局域网可用。",
|
|
2539
|
+
"settings.autoTunnel": "自动公网隧道",
|
|
2540
|
+
"settings.autoTunnelHint": "开启后插件自动启动 Cloudflare quick tunnel(无需安装任何工具),并自动更新公网地址与信任配置,手机随时可用公网配对;关闭时使用上方手动填写的公网地址。",
|
|
2541
|
+
"settings.inherit": "继承",
|
|
2542
|
+
"settings.on": "开",
|
|
2543
|
+
"settings.off": "关",
|
|
2544
|
+
"settings.overridden": "已覆盖",
|
|
2545
|
+
"settings.reset": "恢复默认",
|
|
2546
|
+
"settings.readOnly": "当前部署的设置只读。",
|
|
2547
|
+
"settings.expand": "展开设置",
|
|
2548
|
+
"settings.collapse": "收起设置",
|
|
2549
|
+
"settings.save": "保存",
|
|
2550
|
+
"settings.saving": "保存中…",
|
|
2551
|
+
"settings.discard": "放弃",
|
|
2552
|
+
"settings.unsaved": "未保存",
|
|
2553
|
+
"settings.saveFailed": "部署未接受这些值,已保留供你修改。",
|
|
2554
|
+
"settings.invalidNumber": "请输入数字,留空则使用默认值。"
|
|
2555
|
+
};
|
|
2556
|
+
/** English dictionary, checked complete against the zh key set. */
|
|
2557
|
+
const en = {
|
|
2558
|
+
"entry.label": "Mobile remote control",
|
|
2559
|
+
"title": "Mobile remote control",
|
|
2560
|
+
"subtitle": "Scan the QR code or open the link on your phone to control this workspace remotely",
|
|
2561
|
+
"card.title": "Scan to connect",
|
|
2562
|
+
"public.badge": "Public",
|
|
2563
|
+
"status.waiting": "Waiting for a phone",
|
|
2564
|
+
"status.connected": "{n} device(s) connected",
|
|
2565
|
+
"status.disconnected": "Paired devices offline",
|
|
2566
|
+
"status.stopped": "Mobile access stopped",
|
|
2567
|
+
"status.lanRequired": "This feature needs dsh web started with --host 0.0.0.0, or a configured public address",
|
|
2568
|
+
"status.lanRequiredHint": "The server is bound to 127.0.0.1 and no public address is configured, so a phone cannot reach it. Restart with dsh web --host 0.0.0.0, or set the tunneled public address in settings.",
|
|
2569
|
+
"status.loopbackRequired": "The pairing panel works on this machine only",
|
|
2570
|
+
"status.loopbackRequiredHint": "Open this page at http://127.0.0.1 to mint a QR code; phones use the paired link.",
|
|
2571
|
+
"status.unreachable": "Cannot reach the pairing service",
|
|
2572
|
+
"status.unreachableHint": "Refresh the page and try again.",
|
|
2573
|
+
"pair.hint": "Cannot scan? Open the link on your phone",
|
|
2574
|
+
"pair.publicHint": "Public link: the phone does not need to be on the same network",
|
|
2575
|
+
"address.label": "Network the QR code points to",
|
|
2576
|
+
"address.public": "Public",
|
|
2577
|
+
"address.lan": "LAN",
|
|
2578
|
+
"address.hint": "Use the public address when the phone is not on the same network; LAN addresses only work on the same network.",
|
|
2579
|
+
"pair.expires": "QR code valid until {time}",
|
|
2580
|
+
"pair.expired": "QR code expired — refresh it",
|
|
2581
|
+
"pair.failed.title": "Pairing failed",
|
|
2582
|
+
"pair.failed.detail": "The link is invalid or was already used. Refresh the QR code on your computer and scan again.",
|
|
2583
|
+
"action.stop": "Stop",
|
|
2584
|
+
"action.refresh": "Refresh QR",
|
|
2585
|
+
"action.copy": "Copy link",
|
|
2586
|
+
"action.copied": "Copied",
|
|
2587
|
+
"stopped.hint": "Mobile access is stopped. Click \"Refresh QR\" to re-enable it.",
|
|
2588
|
+
"tunnel.starting": "The public tunnel is starting; the QR code will switch to a public link shortly…",
|
|
2589
|
+
"tunnel.failed": "The public tunnel failed to start: {error}",
|
|
2590
|
+
"tunnel.unknownError": "unknown error",
|
|
2591
|
+
"close.label": "Close mobile remote control panel",
|
|
2592
|
+
"settings.title": "Mobile remote control",
|
|
2593
|
+
"settings.description": "Pairing security and device limits.",
|
|
2594
|
+
"settings.enabled": "Enable mobile remote control",
|
|
2595
|
+
"settings.enabledHint": "When off, the sidebar entry is removed and pairing routes plus the LAN fence stop.",
|
|
2596
|
+
"settings.tokenTtlMs": "Pairing token lifetime (ms)",
|
|
2597
|
+
"settings.tokenTtlMsHint": "How long a minted QR link stays valid before it dies.",
|
|
2598
|
+
"settings.offlineAfterMs": "Device offline threshold (ms)",
|
|
2599
|
+
"settings.offlineAfterMsHint": "A paired device flips to offline when it has not been seen for this long.",
|
|
2600
|
+
"settings.maxDevices": "Paired device cap",
|
|
2601
|
+
"settings.maxDevicesHint": "Hard cap on paired device sessions; the oldest is evicted when full.",
|
|
2602
|
+
"settings.cookieName": "Device cookie name",
|
|
2603
|
+
"settings.cookieNameHint": "Cookie that carries the paired device id.",
|
|
2604
|
+
"settings.requirePairingForLan": "Require pairing for LAN access",
|
|
2605
|
+
"settings.requirePairingForLanHint": "On: every non-loopback /api request must carry a live paired-device cookie. Off: the LAN fence is open and pairing only manages tokens/status.",
|
|
2606
|
+
"settings.publicBaseUrl": "Public address (optional)",
|
|
2607
|
+
"settings.publicBaseUrlHint": "The public URL of a tunnel in front of this server, e.g. a Cloudflare Tunnel: https://xxx.trycloudflare.com. When set, the QR link is built from it so a phone anywhere can pair; leave blank for LAN-only usage.",
|
|
2608
|
+
"settings.autoTunnel": "Auto public tunnel",
|
|
2609
|
+
"settings.autoTunnelHint": "When on, the plugin runs its own Cloudflare quick tunnel (no tool installation needed) and keeps the public address and trust config in sync automatically, so a phone anywhere can pair at any time; when off, the manually entered public address above applies.",
|
|
2610
|
+
"settings.inherit": "Inherit",
|
|
2611
|
+
"settings.on": "On",
|
|
2612
|
+
"settings.off": "Off",
|
|
2613
|
+
"settings.overridden": "Overridden",
|
|
2614
|
+
"settings.reset": "Reset to default",
|
|
2615
|
+
"settings.readOnly": "This deployment stores settings read-only.",
|
|
2616
|
+
"settings.expand": "Show settings",
|
|
2617
|
+
"settings.collapse": "Hide settings",
|
|
2618
|
+
"settings.save": "Save",
|
|
2619
|
+
"settings.saving": "Saving…",
|
|
2620
|
+
"settings.discard": "Discard",
|
|
2621
|
+
"settings.unsaved": "Unsaved",
|
|
2622
|
+
"settings.saveFailed": "The deployment did not accept these values; they were left for you to correct.",
|
|
2623
|
+
"settings.invalidNumber": "Enter a number, or leave blank to use the default."
|
|
2624
|
+
};
|
|
2625
|
+
//#endregion
|
|
2626
|
+
//#region src/client/deep-link.ts
|
|
2627
|
+
/** sessionStorage key for the failed-pair notice. */
|
|
2628
|
+
const PAIR_FAILED_MARKER = "dsh-remote-pair-failed";
|
|
2629
|
+
/** Poll budget for the runtime services (activation order is unconstrained). */
|
|
2630
|
+
const SERVICE_WAIT_MS = 1e4;
|
|
2631
|
+
/** The browser implementation of {@link PageSurface}. */
|
|
2632
|
+
const browserPage = {
|
|
2633
|
+
get href() {
|
|
2634
|
+
return window.location.href;
|
|
2635
|
+
},
|
|
2636
|
+
replaceState(url) {
|
|
2637
|
+
window.history.replaceState(null, "", url);
|
|
2638
|
+
},
|
|
2639
|
+
navigate(url) {
|
|
2640
|
+
window.location.assign(url);
|
|
2641
|
+
},
|
|
2642
|
+
reload() {
|
|
2643
|
+
window.location.reload();
|
|
2644
|
+
}
|
|
2645
|
+
};
|
|
2646
|
+
/** Whether this browser looks like a phone/tablet (the simplified mobile surface). */
|
|
2647
|
+
function isMobileSurface() {
|
|
2648
|
+
if (typeof navigator === "undefined") return false;
|
|
2649
|
+
return /Android|iPhone|iPad|iPod|Mobile|mobile/i.test(navigator.userAgent);
|
|
2650
|
+
}
|
|
2651
|
+
/**
|
|
2652
|
+
* Run the pair/workspace boot flow for this page load.
|
|
2653
|
+
* @param ctx - client root context (workspaces/sessions read at need time).
|
|
2654
|
+
* @param search - the current location.search.
|
|
2655
|
+
* @param page - the page surface (defaults to the browser).
|
|
2656
|
+
*/
|
|
2657
|
+
function runPairBootFlow(ctx, search, page = browserPage) {
|
|
2658
|
+
const params = readPairParams(search);
|
|
2659
|
+
if (params.pair !== void 0) {
|
|
2660
|
+
runAccept(params.pair, page);
|
|
2661
|
+
return;
|
|
2662
|
+
}
|
|
2663
|
+
if (params.workspace !== void 0) runDeepLink(ctx, params.workspace, page);
|
|
2664
|
+
}
|
|
2665
|
+
/** Accept the token, then reload (the workspace param survives the reload). */
|
|
2666
|
+
async function runAccept(token, page) {
|
|
2667
|
+
let ok = false;
|
|
2668
|
+
try {
|
|
2669
|
+
ok = (await acceptPair(token)).ok;
|
|
2670
|
+
if (!ok) sessionStorage.setItem(PAIR_FAILED_MARKER, "failed");
|
|
2671
|
+
} catch {
|
|
2672
|
+
sessionStorage.setItem(PAIR_FAILED_MARKER, "failed");
|
|
2673
|
+
}
|
|
2674
|
+
const url = new URL(page.href);
|
|
2675
|
+
url.searchParams.delete("pair");
|
|
2676
|
+
page.replaceState(`${url.pathname}${url.search}${url.hash}`);
|
|
2677
|
+
if (ok) if (isMobileSurface()) page.navigate("/m");
|
|
2678
|
+
else page.reload();
|
|
2679
|
+
}
|
|
2680
|
+
/**
|
|
2681
|
+
* Connect the deep-linked workspace and open its session. Waits for the
|
|
2682
|
+
* runtime services AND for the target workspace to appear in the workspace
|
|
2683
|
+
* list (both are asynchronous after boot), then connects and opens; gives
|
|
2684
|
+
* up silently within the budget — the workspace param is stripped either
|
|
2685
|
+
* way, so a late failure cannot loop.
|
|
2686
|
+
* @param ctx - client root context.
|
|
2687
|
+
* @param workspaceId - the target workspace.
|
|
2688
|
+
* @param page - the page surface.
|
|
2689
|
+
*/
|
|
2690
|
+
async function runDeepLink(ctx, workspaceId, page) {
|
|
2691
|
+
const target = workspaceId;
|
|
2692
|
+
const deadline = Date.now() + SERVICE_WAIT_MS;
|
|
2693
|
+
while (Date.now() < deadline) {
|
|
2694
|
+
const workspaces = ctx.get("workspaces");
|
|
2695
|
+
const sessions = ctx.get("sessions");
|
|
2696
|
+
if (workspaces !== void 0 && sessions !== void 0) {
|
|
2697
|
+
if (workspaces.list.getSnapshot().items.some((item) => item.workspaceId === target)) {
|
|
2698
|
+
try {
|
|
2699
|
+
const sessionId = await workspaces.connectWorkspace(target);
|
|
2700
|
+
sessions.open(sessionId);
|
|
2701
|
+
} catch {}
|
|
2702
|
+
break;
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2705
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
2706
|
+
}
|
|
2707
|
+
const url = new URL(page.href);
|
|
2708
|
+
url.searchParams.delete("workspace");
|
|
2709
|
+
page.replaceState(`${url.pathname}${url.search}${url.hash}`);
|
|
2710
|
+
}
|
|
2711
|
+
//#endregion
|
|
2712
|
+
//#region src/client/index.ts
|
|
2713
|
+
/**
|
|
2714
|
+
* Mobile remote control — browser half. Registers the `remote` dictionaries,
|
|
2715
|
+
* the sidebar-foot entry (phone trigger + pairing panel) into the
|
|
2716
|
+
* ui-sidebar-declared `sidebar.remote` seat, and runs the phone-side boot
|
|
2717
|
+
* flow (pair accept + workspace deep-link + presence heartbeats) plus the
|
|
2718
|
+
* one-time failed-pair notice. Export discipline: packages/client/AGENTS.md
|
|
2719
|
+
* — the /client surface carries only what cordis loading needs plus types.
|
|
2720
|
+
*/
|
|
2721
|
+
/** Dictionary namespace owned by this plugin. */
|
|
2722
|
+
const NS = "remote";
|
|
2723
|
+
/** Settings namespace the remote-control card edits (the Host plugin registers it). */
|
|
2724
|
+
const REMOTE_WEB_UI_NS = "remote-web-ui";
|
|
2725
|
+
/** Heartbeat cadence from a paired phone (presence + revocation liveness). */
|
|
2726
|
+
const HEARTBEAT_INTERVAL_MS = 1e4;
|
|
2727
|
+
/** Services required by this plugin. */
|
|
2728
|
+
const inject = [
|
|
2729
|
+
"slots",
|
|
2730
|
+
"locale",
|
|
2731
|
+
"connection",
|
|
2732
|
+
"settingsScope",
|
|
2733
|
+
"remote"
|
|
2734
|
+
];
|
|
2735
|
+
/**
|
|
2736
|
+
* Register the remote-control surface.
|
|
2737
|
+
* @param ctx - client root context.
|
|
2738
|
+
*/
|
|
2739
|
+
function apply(ctx) {
|
|
2740
|
+
ctx.effect(() => ctx.locale.register(NS, {
|
|
2741
|
+
zh,
|
|
2742
|
+
en
|
|
2743
|
+
}), "remote-web-ui: dictionaries");
|
|
2744
|
+
const t = ctx.locale.bind(NS);
|
|
2745
|
+
const settingsScope = ctx.settingsScope.bind({ namespace: REMOTE_WEB_UI_NS });
|
|
2746
|
+
const enabled = () => {
|
|
2747
|
+
const snapshot = settingsScope.getSnapshot();
|
|
2748
|
+
return snapshot.status === "ready" ? snapshot.value?.enabled ?? true : snapshot.status === "unavailable";
|
|
2749
|
+
};
|
|
2750
|
+
ctx.slots.inject("sidebar.remote", () => {
|
|
2751
|
+
let disposeEntry;
|
|
2752
|
+
const syncEntry = () => {
|
|
2753
|
+
if (enabled() && disposeEntry === void 0) disposeEntry = ctx.slots.register({
|
|
2754
|
+
name: "sidebar.remote",
|
|
2755
|
+
locale: NS
|
|
2756
|
+
}, RemoteEntry);
|
|
2757
|
+
else if (!enabled() && disposeEntry !== void 0) {
|
|
2758
|
+
disposeEntry();
|
|
2759
|
+
disposeEntry = void 0;
|
|
2760
|
+
}
|
|
2761
|
+
};
|
|
2762
|
+
const unsubscribe = settingsScope.subscribe(syncEntry);
|
|
2763
|
+
syncEntry();
|
|
2764
|
+
return () => {
|
|
2765
|
+
unsubscribe();
|
|
2766
|
+
disposeEntry?.();
|
|
2767
|
+
};
|
|
2768
|
+
});
|
|
2769
|
+
const remoteSettings = new RemoteSettingsCardController(settingsScope);
|
|
2770
|
+
ctx.slots.inject("web-ui.plugin.item", () => ctx.slots.register({
|
|
2771
|
+
name: "web-ui.plugin.item",
|
|
2772
|
+
id: "remote-web-ui",
|
|
2773
|
+
order: 90,
|
|
2774
|
+
locale: NS,
|
|
2775
|
+
inject: () => remoteSettings.inject()
|
|
2776
|
+
}, RemoteSettingsCard));
|
|
2777
|
+
let disposeRuntime;
|
|
2778
|
+
const syncRuntime = () => {
|
|
2779
|
+
if (enabled() && disposeRuntime === void 0) disposeRuntime = ctx.effect(() => {
|
|
2780
|
+
const loopback = ctx.get("connection")?.isLoopback ?? true;
|
|
2781
|
+
runPairBootFlow(ctx, window.location.search);
|
|
2782
|
+
if (loopback) return () => {};
|
|
2783
|
+
const timer = window.setInterval(() => {
|
|
2784
|
+
sendHeartbeat().catch(() => {});
|
|
2785
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
2786
|
+
return () => {
|
|
2787
|
+
window.clearInterval(timer);
|
|
2788
|
+
};
|
|
2789
|
+
}, "remote-web-ui: pair flow + heartbeats");
|
|
2790
|
+
else if (!enabled() && disposeRuntime !== void 0) {
|
|
2791
|
+
disposeRuntime();
|
|
2792
|
+
disposeRuntime = void 0;
|
|
2793
|
+
}
|
|
2794
|
+
};
|
|
2795
|
+
settingsScope.subscribe(syncRuntime);
|
|
2796
|
+
syncRuntime();
|
|
2797
|
+
ctx.effect(() => {
|
|
2798
|
+
const timer = window.setTimeout(() => {
|
|
2799
|
+
if (sessionStorage.getItem("dsh-remote-pair-failed") === null) return;
|
|
2800
|
+
sessionStorage.removeItem(PAIR_FAILED_MARKER);
|
|
2801
|
+
const mount = document.createElement("div");
|
|
2802
|
+
document.body.appendChild(mount);
|
|
2803
|
+
(0, react_dom_client.createRoot)(mount).render((0, react.createElement)(PairFailedNotice, { t }));
|
|
2804
|
+
}, 1500);
|
|
2805
|
+
return () => {
|
|
2806
|
+
window.clearTimeout(timer);
|
|
2807
|
+
};
|
|
2808
|
+
}, "remote-web-ui: failed-pair notice");
|
|
2809
|
+
}
|
|
2810
|
+
//#endregion
|
|
2811
|
+
exports.apply = apply;
|
|
2812
|
+
exports.inject = inject;
|
|
2813
|
+
return module.exports;
|
|
2814
|
+
}
|
|
2815
|
+
});
|
|
2816
|
+
|
|
2817
|
+
//# sourceMappingURL=client.js.map
|