dsh-remote-bridge 2.9.0 → 2.9.2
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/README.en.md +20 -32
- package/README.md +45 -29
- package/assets/promo-16x9.png +0 -0
- package/assets/promo-social.png +0 -0
- package/assets/screenshots/panel-app-download.png +0 -0
- package/assets/screenshots/panel-devices.png +0 -0
- package/assets/screenshots/panel-plugins.png +0 -0
- package/assets/screenshots/panel-remote.png +0 -0
- package/assets/screenshots/panel-roots.png +0 -0
- package/assets/screenshots/panel-tunnel.png +0 -0
- package/docs/api/asyncapi.yaml +1 -1
- package/docs/api/openapi.yaml +2 -2
- package/lib/client.cjs +1972 -1595
- package/lib/index.js +1 -1
- package/package.json +1 -1
- package/screenshots.json +2 -0
package/lib/client.cjs
CHANGED
|
@@ -8277,7 +8277,7 @@ window.__ModuleLoader__.load({
|
|
|
8277
8277
|
key: "1quai1"
|
|
8278
8278
|
}]
|
|
8279
8279
|
]);
|
|
8280
|
-
createLucideIcon("Cloud", [["path", {
|
|
8280
|
+
const Cloud = createLucideIcon("Cloud", [["path", {
|
|
8281
8281
|
d: "M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z",
|
|
8282
8282
|
key: "p7xjir"
|
|
8283
8283
|
}]]);
|
|
@@ -23958,7 +23958,7 @@ window.__ModuleLoader__.load({
|
|
|
23958
23958
|
d: "M11 16.586V19a1 1 0 0 1-1 1H2L18.37 3.63a1 1 0 1 1 3 3l-9.663 9.663a1 1 0 0 1-1.414 0L8 14",
|
|
23959
23959
|
key: "1sllp5"
|
|
23960
23960
|
}]]);
|
|
23961
|
-
createLucideIcon("SlidersHorizontal", [
|
|
23961
|
+
const SlidersHorizontal = createLucideIcon("SlidersHorizontal", [
|
|
23962
23962
|
["line", {
|
|
23963
23963
|
x1: "21",
|
|
23964
23964
|
x2: "14",
|
|
@@ -29825,6 +29825,7 @@ window.__ModuleLoader__.load({
|
|
|
29825
29825
|
}]
|
|
29826
29826
|
]);
|
|
29827
29827
|
exports.Check = Check;
|
|
29828
|
+
exports.Cloud = Cloud;
|
|
29828
29829
|
exports.Copy = Copy;
|
|
29829
29830
|
exports.Download = Download;
|
|
29830
29831
|
exports.File = File;
|
|
@@ -29833,6 +29834,7 @@ window.__ModuleLoader__.load({
|
|
|
29833
29834
|
exports.Languages = Languages;
|
|
29834
29835
|
exports.Package = Package;
|
|
29835
29836
|
exports.RefreshCw = RefreshCw;
|
|
29837
|
+
exports.SlidersHorizontal = SlidersHorizontal;
|
|
29836
29838
|
exports.Smartphone = Smartphone;
|
|
29837
29839
|
exports.Trash2 = Trash2;
|
|
29838
29840
|
exports.Upload = Upload;
|
|
@@ -29894,17 +29896,21 @@ window.__ModuleLoader__.load({
|
|
|
29894
29896
|
close: "关闭",
|
|
29895
29897
|
rootsTab: "根目录",
|
|
29896
29898
|
remoteTab: "远程访问",
|
|
29899
|
+
tunnelTab: "外网隧道",
|
|
29897
29900
|
devicesTab: "设备",
|
|
29898
29901
|
trashTab: "回收站",
|
|
29899
29902
|
auditTab: "审计",
|
|
29900
29903
|
imageTab: "识图",
|
|
29901
29904
|
rootsTabDesc: "授权手机访问这台机器上的哪些目录。新增、命名、移除都在这里。",
|
|
29902
29905
|
remoteTabDesc: "绑定 IP 与端口、开关远程访问、生成配对码与配置文本。",
|
|
29906
|
+
tunnelTabDesc: "人在外网时用:一键创建临时公网地址,把本机端口经 Cloudflare 边缘暴露出去。",
|
|
29903
29907
|
devicesTabDesc: "查看已配对的手机,修改它们的权限范围与可访问目录,或吊销设备。",
|
|
29904
29908
|
imageTabDesc: "设置图片交给识图工具的落盘目录名。",
|
|
29905
29909
|
trashTabDesc: "经手机端删除的文件留在这里,可恢复或彻底清理。",
|
|
29906
29910
|
auditTabDesc: "最近的操作记录:谁、在什么时间、动了哪个路径。",
|
|
29907
29911
|
appDownloadTabDesc: "手机客户端的下载二维码与地址。",
|
|
29912
|
+
advancedGroupTitle: "高级设置",
|
|
29913
|
+
advancedGroupDesc: "平时用不到的几项,收在这里,功能与主列表一致。",
|
|
29908
29914
|
imageTitle: "识图路径",
|
|
29909
29915
|
imageHint: "纯文本模型收到图片时,图片会落在这个目录里,消息里只留路径给识图工具读。这里改的就是那个目录。",
|
|
29910
29916
|
imageSubdir: "识图路径(目录名)",
|
|
@@ -29960,6 +29966,7 @@ window.__ModuleLoader__.load({
|
|
|
29960
29966
|
tunnelStop: "停止云隧道",
|
|
29961
29967
|
tunnelEndpointLabel: "云隧道",
|
|
29962
29968
|
tunnelGenConfigReady: "生成隧道配置文件",
|
|
29969
|
+
tunnelGenConfigByUrl: "用隧道地址生成配置",
|
|
29963
29970
|
tunnelGenConfigOff: "隧道未开启",
|
|
29964
29971
|
tunnelSecurityWarning: "开启后将本机远程端口经 Cloudflare 边缘暴露到公网,任何人拿到下面的临时地址即可尝试连接。请仅在需要外网连回来时开启,用完及时停止隧道。",
|
|
29965
29972
|
tunnelStartConfirm: "开启云隧道会把你设置页里的远程端口经 Cloudflare 暴露到公网,强密码(token)才能挡住乱连。确认开启吗?",
|
|
@@ -30153,17 +30160,21 @@ window.__ModuleLoader__.load({
|
|
|
30153
30160
|
close: "Close",
|
|
30154
30161
|
rootsTab: "Roots",
|
|
30155
30162
|
remoteTab: "Remote",
|
|
30163
|
+
tunnelTab: "Public tunnel",
|
|
30156
30164
|
devicesTab: "Devices",
|
|
30157
30165
|
trashTab: "Trash",
|
|
30158
30166
|
auditTab: "Audit",
|
|
30159
30167
|
imageTab: "Image reading",
|
|
30160
30168
|
rootsTabDesc: "Which directories on this machine phones may reach. Add, rename and remove them here.",
|
|
30161
30169
|
remoteTabDesc: "Bind address and port, switch remote access on or off, mint pairing codes and config texts.",
|
|
30170
|
+
tunnelTabDesc: "For when you are out on another network: create a temporary public address that exposes the local port through the Cloudflare edge.",
|
|
30162
30171
|
devicesTabDesc: "Paired phones, their permission scopes and reachable roots — or revoke them outright.",
|
|
30163
30172
|
imageTabDesc: "Choose the directory name where images land for the image-reading tool.",
|
|
30164
30173
|
trashTabDesc: "Files deleted from a phone land here. Restore them, or purge them for good.",
|
|
30165
30174
|
auditTabDesc: "Recent activity: who, when, and which path was touched.",
|
|
30166
30175
|
appDownloadTabDesc: "QR codes and addresses for the phone app download.",
|
|
30176
|
+
advancedGroupTitle: "Advanced settings",
|
|
30177
|
+
advancedGroupDesc: "The few sections nobody opens daily, kept here — every one of them works exactly as it would in the main list.",
|
|
30167
30178
|
imageTitle: "Image-reading path",
|
|
30168
30179
|
imageHint: "When a text-only model receives a picture, the file lands in this directory and the message keeps only its path for the image-reading tool. This setting is that directory.",
|
|
30169
30180
|
imageSubdir: "Image path (directory name)",
|
|
@@ -30219,6 +30230,7 @@ window.__ModuleLoader__.load({
|
|
|
30219
30230
|
tunnelStop: "Stop tunnel",
|
|
30220
30231
|
tunnelEndpointLabel: "Cloudflare tunnel",
|
|
30221
30232
|
tunnelGenConfigReady: "Generate config file from tunnel",
|
|
30233
|
+
tunnelGenConfigByUrl: "Generate config with tunnel address",
|
|
30222
30234
|
tunnelGenConfigOff: "Tunnel not started",
|
|
30223
30235
|
tunnelSecurityWarning: "Starting exposes your remote port to the public internet through Cloudflare edge; anyone with the temporary address below can try to connect. Only start it when you need to reach the machine from outside, and stop the tunnel when done.",
|
|
30224
30236
|
tunnelStartConfirm: "Starting the tunnel exposes your remote port through Cloudflare to the public internet; only the device token (a strong password) keeps strangers out. Start anyway?",
|
|
@@ -30911,68 +30923,15 @@ window.__ModuleLoader__.load({
|
|
|
30911
30923
|
}
|
|
30912
30924
|
};
|
|
30913
30925
|
//#endregion
|
|
30914
|
-
//#region src/shared
|
|
30915
|
-
const DEVICE_SCOPES = [
|
|
30916
|
-
"chat.read",
|
|
30917
|
-
"chat.write",
|
|
30918
|
-
"files.read",
|
|
30919
|
-
"files.write",
|
|
30920
|
-
"files.delete",
|
|
30921
|
-
"settings.read",
|
|
30922
|
-
"settings.write"
|
|
30923
|
-
];
|
|
30924
|
-
/**
|
|
30925
|
-
* 授予新设备时的默认权限(**五个非破坏性权限**)。
|
|
30926
|
-
*
|
|
30927
|
-
* 刻意不含 `files.delete` 与 `settings.write`:这两个是破坏性权限,应当是用户显式
|
|
30928
|
-
* 要求才授予。`settings.write` 尤其危险 —— 拿到它就能改供应商 `baseURL`,把模型
|
|
30929
|
-
* 请求连同真实凭据引到任意地址(SEC-H11),所以它绝不能是默认值。
|
|
30930
|
-
*
|
|
30931
|
-
* 三条路径必须共用这一份,否则「同一个产品里三处默认权限不一样」这种缺陷会反复
|
|
30932
|
-
* 复发:服务端生成配置文本时的缺省、Web UI 配对码面板的初始勾选、隧道一键路径。
|
|
30933
|
-
*/
|
|
30934
|
-
const DEFAULT_DEVICE_SCOPES = [
|
|
30935
|
-
"chat.read",
|
|
30936
|
-
"chat.write",
|
|
30937
|
-
"files.read",
|
|
30938
|
-
"files.write",
|
|
30939
|
-
"settings.read"
|
|
30940
|
-
];
|
|
30941
|
-
new Set(DEVICE_SCOPES);
|
|
30942
|
-
//#endregion
|
|
30943
|
-
//#region src/client/app-qr.ts
|
|
30944
|
-
const APP_QR_CODES = [{
|
|
30945
|
-
"key": "github",
|
|
30946
|
-
"label": "GitHub",
|
|
30947
|
-
"url": "https://github.com/westanke/dsh-companion/releases",
|
|
30948
|
-
"dataUrl": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNjQiIGhlaWdodD0iMjY0IiB2aWV3Qm94PSIwIDAgNDEgNDEiIHNoYXBlLXJlbmRlcmluZz0iY3Jpc3BFZGdlcyI+PHBhdGggZmlsbD0iI2ZmZmZmZiIgZD0iTTAgMGg0MXY0MUgweiIvPjxwYXRoIHN0cm9rZT0iIzAwMDAwMCIgZD0iTTQgNC41aDdtMSAwaDFtMiAwaDFtMyAwaDNtMyAwaDFtMSAwaDFtMiAwaDdNNCA1LjVoMW01IDBoMW0zIDBoM200IDBoMW0xIDBoMW0yIDBoMW0zIDBoMW01IDBoMU00IDYuNWgxbTEgMGgzbTEgMGgxbTMgMGgybTEgMGgxbTEgMGgxbTQgMGg1bTEgMGgxbTEgMGgzbTEgMGgxTTQgNy41aDFtMSAwaDNtMSAwaDFtMSAwaDJtMiAwaDFtMSAwaDJtMSAwaDJtMSAwaDFtMiAwaDFtMiAwaDFtMSAwaDNtMSAwaDFNNCA4LjVoMW0xIDBoM20xIDBoMW0xIDBoNG0xIDBoMm0zIDBoM20yIDBoMW0yIDBoMW0xIDBoM20xIDBoMU00IDkuNWgxbTUgMGgxbTEgMGgxbTEgMGgxbTMgMGgxbTEgMGg0bTEgMGgxbTQgMGgxbTUgMGgxTTQgMTAuNWg3bTEgMGgxbTEgMGgxbTEgMGgxbTEgMGgxbTEgMGgxbTEgMGgxbTEgMGgxbTEgMGgxbTEgMGgxbTEgMGg3TTEyIDExLjVoMW01IDBoNW0yIDBoNE00IDEyLjVoMW0zIDBoMW0xIDBoNG03IDBoMW0xIDBoM20zIDBoNW0yIDBoMU00IDEzLjVoMW02IDBoMW0zIDBoMW0xIDBoNG02IDBoMW0xIDBoMW0zIDBoM000IDE0LjVoMm00IDBoNm0zIDBoMW0xIDBoMW0yIDBoMm0xIDBoMW0yIDBoMW0yIDBoMW0xIDBoMU00IDE1LjVoMW0yIDBoMm0zIDBoMW0xIDBoM20xIDBoMW0yIDBoNW0xIDBoMW0xIDBoMW01IDBoMk01IDE2LjVoMW0xIDBoMW0yIDBoMW0xIDBoMW0yIDBoMW0xIDBoM20yIDBoN20xIDBoMW0xIDBoMm0xIDBoMk00IDE3LjVoMW02IDBoMW0xIDBoNW0yIDBoMW0yIDBoMW0xIDBoMW0xIDBoMW0xIDBoMW0xIDBoMW0xIDBoMU02IDE4LjVoMm0xIDBoM20xIDBoMm01IDBoNG0zIDBoMW0xIDBoM20yIDBoMk00IDE5LjVoMW0yIDBoMm00IDBoMm0zIDBoMW0yIDBoMW0xIDBoNG0yIDBoMk02IDIwLjVoMW0yIDBoMm0yIDBoMm0yIDBoM20xIDBoMW0zIDBoMm0xIDBoMW0xIDBoMW0xIDBoMW0yIDBoMU00IDIxLjVoMW0xIDBoMW0yIDBoMW0yIDBoMW0xIDBoMm0yIDBoMm0xIDBoMm00IDBoMW0xIDBoMW0zIDBoM000IDIyLjVoNW0xIDBoMW0xIDBoMm0xIDBoM20xIDBoMW0yIDBoMW0xIDBoNG0xIDBoM20xIDBoMW0xIDBoMU01IDIzLjVoMm0xIDBoMm0yIDBoMW0yIDBoMm0yIDBoMW0zIDBoM20xIDBoNW0zIDBoMU01IDI0LjVoMW0xIDBoMm0xIDBoMW0zIDBoMm0xIDBoMm0xIDBoMW0zIDBoM20zIDBoMW0xIDBoMk00IDI1LjVoM20xIDBoMW0yIDBoMm05IDBoM20xIDBoMm0xIDBoMW0xIDBoMW0xIDBoMW0xIDBoMU02IDI2LjVoMW0xIDBoMW0xIDBoMW0xIDBoMW00IDBoM20xIDBoMW0yIDBoMW0xIDBoNG0yIDBoMW0yIDBoMU04IDI3LjVoMm0xIDBoMW0xIDBoMm0yIDBoMW0xIDBoNW0xIDBoMW0yIDBoNW0zIDBoMU00IDI4LjVoNW0xIDBoMW0yIDBoNG0xIDBoMW0yIDBoMW0xIDBoMTFtMSAwaDJNMTIgMjkuNWgybTIgMGgybTEgMGgybTEgMGgxbTEgMGgxbTIgMGgybTMgMGgxbTEgMGgyTTQgMzAuNWg3bTEgMGgxbTEgMGgxbTQgMGgxbTEgMGgxbTIgMGgxbTEgMGgxbTEgMGgxbTEgMGgxbTEgMGgyTTQgMzEuNWgxbTUgMGgxbTMgMGgxbTUgMGgzbTEgMGgybTIgMGgxbTMgMGgxTTQgMzIuNWgxbTEgMGgzbTEgMGgxbTEgMGgxbTQgMGgzbTIgMGgxbTIgMGgybTEgMGg2bTEgMGgxTTQgMzMuNWgxbTEgMGgzbTEgMGgxbTIgMGgxbTEgMGgybTMgMGgybTMgMGgxbTEgMGgybTEgMGgzbTEgMGgxTTQgMzQuNWgxbTEgMGgzbTEgMGgxbTIgMGgzbTQgMGgxbTEgMGgxbTEgMGgxbTIgMGgybTEgMGg0TTQgMzUuNWgxbTUgMGgxbTQgMGgxbTUgMGgxbTIgMGgybTEgMGg0TTQgMzYuNWg3bTEgMGgxbTEgMGgzbTEgMGgybTEgMGgxbTEgMGg0bTEgMGgybTYgMGgxIi8+PC9zdmc+Cg=="
|
|
30949
|
-
}, {
|
|
30950
|
-
"key": "gitee",
|
|
30951
|
-
"label": "Gitee",
|
|
30952
|
-
"url": "https://gitee.com/westanke/dsh-companion/releases",
|
|
30953
|
-
"dataUrl": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNjQiIGhlaWdodD0iMjY0IiB2aWV3Qm94PSIwIDAgNDEgNDEiIHNoYXBlLXJlbmRlcmluZz0iY3Jpc3BFZGdlcyI+PHBhdGggZmlsbD0iI2ZmZmZmZiIgZD0iTTAgMGg0MXY0MUgweiIvPjxwYXRoIHN0cm9rZT0iIzAwMDAwMCIgZD0iTTQgNC41aDdtMSAwaDFtMSAwaDJtMSAwaDRtNiAwaDJtMSAwaDdNNCA1LjVoMW01IDBoMW0zIDBoMm0xIDBoMm0xIDBoMW0xIDBoMm0yIDBoMW0zIDBoMW01IDBoMU00IDYuNWgxbTEgMGgzbTEgMGgxbTIgMGgybTEgMGgxbTEgMGgxbTEgMGg0bTEgMGgxbTEgMGgybTEgMGgxbTEgMGgzbTEgMGgxTTQgNy41aDFtMSAwaDNtMSAwaDFtMSAwaDVtMiAwaDNtNSAwaDFtMiAwaDFtMSAwaDNtMSAwaDFNNCA4LjVoMW0xIDBoM20xIDBoMW0xIDBoMW0xIDBoM20zIDBoMW0yIDBoMW0xIDBoMW0xIDBoMW0yIDBoMW0xIDBoM20xIDBoMU00IDkuNWgxbTUgMGgxbTEgMGgybTMgMGgzbTQgMGgzbTEgMGgxbTEgMGgxbTUgMGgxTTQgMTAuNWg3bTEgMGgxbTEgMGgxbTEgMGgxbTEgMGgxbTEgMGgxbTEgMGgxbTEgMGgxbTEgMGgxbTEgMGgxbTEgMGg3TTEyIDExLjVoNW00IDBoMW0xIDBoNk00IDEyLjVoMW0zIDBoMW0xIDBoNG0xIDBoMm0xIDBoNG0zIDBoOW0yIDBoMU00IDEzLjVoMW0xIDBoM201IDBoMW0xIDBoMW0xIDBoNW00IDBoMW0xIDBoMW0zIDBoM000IDE0LjVoMW0xIDBoMW0xIDBoMW0xIDBoNG0zIDBoNG0xIDBoMW0xIDBoN20yIDBoMW0xIDBoMU01IDE1LjVoMW0zIDBoMW0xIDBoMm0yIDBoMW0xIDBoMW0xIDBoMW00IDBoMm0xIDBoNG00IDBoMk00IDE2LjVoMm0yIDBoMW0xIDBoMW0yIDBoMm0yIDBoMW0yIDBoMW0yIDBoMW0xIDBoMm0xIDBoMW0xIDBoMW0xIDBoMk04IDE3LjVoMm0xIDBoMW0yIDBoM20xIDBoMW0zIDBoM20xIDBoMm0xIDBoMW0xIDBoMW0xIDBoMU00IDE4LjVoMW0xIDBoMW0xIDBoM20xIDBoMW0xIDBoMW0xIDBoMW0xIDBoMm0xIDBoMW0yIDBoMW0xIDBoNW0xIDBoMW0yIDBoMU00IDE5LjVoMW00IDBoMW0yIDBoMW0yIDBoM20xIDBoNG0yIDBoMW0xIDBoNU00IDIwLjVoNW0xIDBoNW0yIDBoMW0zIDBoMW0xIDBoNG0xIDBoMW0xIDBoMW0xIDBoMm0xIDBoMU01IDIxLjVoMW01IDBoMm01IDBoM20xIDBoMW00IDBoMW0xIDBoMW0zIDBoM001IDIyLjVoMm0yIDBoM20yIDBoMW0xIDBoMW0xIDBoMm0xIDBoMW0yIDBoMW0xIDBoMm0xIDBoM20xIDBoMW0xIDBoMU00IDIzLjVoMW0yIDBoMW0zIDBoNW01IDBoNW0xIDBoMm0xIDBoMm0zIDBoMk00IDI0LjVoNG0xIDBoMm0xIDBoMm0xIDBoMW0yIDBoMm0yIDBoNW0xIDBoM20xIDBoMm0xIDBoMk00IDI1LjVoNG00IDBoM20xIDBoMW0zIDBoMm0xIDBoMW0xIDBoMW0xIDBoMW0xIDBoMW0xIDBoMW0yIDBoMk04IDI2LjVoNW0yIDBoNG0xIDBoMW0xIDBoMm0zIDBoMW0xIDBoMW0xIDBoMW0xIDBoMW0xIDBoMU02IDI3LjVoMW0yIDBoMW00IDBoM200IDBoMW0xIDBoM20zIDBoMm00IDBoMU00IDI4LjVoM20zIDBoM200IDBoMW0xIDBoMW0xIDBoMW0zIDBoMm0xIDBoNW0yIDBoMk0xMiAyOS41aDFtMiAwaDFtMyAwaDFtMSAwaDJtNCAwaDJtMyAwaDFtMSAwaDJNNCAzMC41aDdtMSAwaDFtMyAwaDRtMiAwaDFtMSAwaDJtMiAwaDFtMSAwaDFtMSAwaDJtMSAwaDFNNCAzMS41aDFtNSAwaDFtMiAwaDFtMiAwaDFtMiAwaDFtMyAwaDNtMiAwaDFtMyAwaDFtMiAwaDFNNCAzMi41aDFtMSAwaDNtMSAwaDFtMSAwaDFtMSAwaDFtMSAwaDJtMiAwaDFtMyAwaDNtMSAwaDZNNCAzMy41aDFtMSAwaDNtMSAwaDFtMiAwaDFtMiAwaDFtNCAwaDhtMSAwaDNtMSAwaDFNNCAzNC41aDFtMSAwaDNtMSAwaDFtMiAwaDFtMSAwaDJtMSAwaDJtNCAwaDJtMSAwaDJtMSAwaDNNNCAzNS41aDFtNSAwaDFtNCAwaDNtMSAwaDVtMSAwaDFtMiAwaDNNNCAzNi41aDdtMSAwaDFtMiAwaDJtMSAwaDFtMiAwaDFtMSAwaDNtMSAwaDJtNyAwaDEiLz48L3N2Zz4K"
|
|
30954
|
-
}];
|
|
30955
|
-
//#endregion
|
|
30956
|
-
//#region src/client/workspace.tsx
|
|
30957
|
-
/** What the settings-page launcher opens: authorized roots, the first section. */
|
|
30958
|
-
const DEFAULT_PANEL_SECTION = "roots";
|
|
30926
|
+
//#region src/client/cards/shared.tsx
|
|
30959
30927
|
/**
|
|
30960
|
-
*
|
|
30928
|
+
* 跨卡复用的小部件与纯函数。
|
|
30961
30929
|
*
|
|
30962
|
-
*
|
|
30963
|
-
* ask for `app-downloadTab` — a key nobody should have to remember the spelling of. Map it
|
|
30964
|
-
* explicitly instead of encoding a naming rule into a template.
|
|
30930
|
+
* 判据同 `context.ts`:**至少两张卡在用**才放这里。只服务一张卡的留在那张卡的文件里。
|
|
30965
30931
|
*/
|
|
30966
|
-
function tabLabelKey(tab) {
|
|
30967
|
-
return tab === "app-download" ? "appDownloadTab" : `${tab}Tab`;
|
|
30968
|
-
}
|
|
30969
|
-
/** 配对码剩余分钟:向上取整 —— 显示「约 9 分钟」比「约 8 分钟」在最后一刻更诚实。 */
|
|
30970
|
-
function pairingMinutesLeft(expiresAt, now) {
|
|
30971
|
-
return Math.max(0, Math.ceil((expiresAt - now) / 6e4));
|
|
30972
|
-
}
|
|
30973
30932
|
/**
|
|
30974
|
-
* 通用复制小按钮:成功后短暂换成对勾
|
|
30975
|
-
* 非安全上下文(HTTP/LAN)拿不到 Clipboard API 时退化为选中文本提示,见 copyText 的调用方。
|
|
30933
|
+
* 通用复制小按钮:成功后短暂换成对勾 +「已复制」,2s 自动复位;
|
|
30934
|
+
* 非安全上下文(HTTP/LAN)拿不到 Clipboard API 时退化为选中文本提示,见 `copyText` 的调用方。
|
|
30976
30935
|
*/
|
|
30977
30936
|
function CopyButton(props) {
|
|
30978
30937
|
const [state, setState] = (0, react.useState)("idle");
|
|
@@ -30998,1013 +30957,1117 @@ window.__ModuleLoader__.load({
|
|
|
30998
30957
|
})]
|
|
30999
30958
|
});
|
|
31000
30959
|
}
|
|
31001
|
-
/**
|
|
31002
|
-
|
|
31003
|
-
|
|
31004
|
-
* 键名是 `${tab}TabDesc`(**带 Tab**),不是 `${tab}Desc`。这个中间段不是冗余:它让
|
|
31005
|
-
* 描述键与标题键 `rootsTab` / `rootsTabDesc` 成对,读的人一眼能看出这两个属于同一张卡。
|
|
31006
|
-
*
|
|
31007
|
-
* 踩过的坑:早先这里写成 `${tab}Desc`,`rootsDesc` 之类根本不在字典里 —— `translate` 直接
|
|
31008
|
-
* `dictionaries[locale][key].replace(...)` 抛 TypeError,整个设置面板白屏。`as MessageKey`
|
|
31009
|
-
* 把类型检查骗过去了,所以单测全绿也照样白屏。**键名靠模板拼出来时,类型系统是最后一道
|
|
31010
|
-
* 防线,这里偏偏被断言关掉了。** 现在改成显式查表,见下。
|
|
31011
|
-
*/
|
|
31012
|
-
const TAB_DESC_KEYS = {
|
|
31013
|
-
roots: "rootsTabDesc",
|
|
31014
|
-
remote: "remoteTabDesc",
|
|
31015
|
-
devices: "devicesTabDesc",
|
|
31016
|
-
image: "imageTabDesc",
|
|
31017
|
-
trash: "trashTabDesc",
|
|
31018
|
-
plugins: "pluginsTabDesc",
|
|
31019
|
-
"app-download": "appDownloadTabDesc"
|
|
31020
|
-
};
|
|
31021
|
-
function tabDescKey(tab) {
|
|
31022
|
-
return TAB_DESC_KEYS[tab];
|
|
30960
|
+
/** 配对码剩余分钟:向上取整 —— 显示「约 9 分钟」比「约 8 分钟」在最后一刻更诚实。 */
|
|
30961
|
+
function pairingMinutesLeft(expiresAt, now) {
|
|
30962
|
+
return Math.max(0, Math.ceil((expiresAt - now) / 6e4));
|
|
31023
30963
|
}
|
|
31024
30964
|
/**
|
|
31025
|
-
*
|
|
31026
|
-
*
|
|
31027
|
-
* Settings only — the five admin sections. The file tree used to live here as a sixth row; it does
|
|
31028
|
-
* not any more, so this is the old `AdminOverlay` body with a rail, not a workspace host.
|
|
31029
|
-
*/
|
|
31030
|
-
/**
|
|
31031
|
-
* 导航栏 + 内容区,**不含任何对话框外壳**。
|
|
31032
|
-
*
|
|
31033
|
-
* 抽出来是为了让 DSH 设置页能把它**内联**渲染。
|
|
30965
|
+
* `host:port` 的统一显示。
|
|
31034
30966
|
*
|
|
31035
|
-
*
|
|
31036
|
-
*
|
|
31037
|
-
* 这就是把弹窗嵌进设置页的代价:入口看起来是空的,因为内容在下一层。
|
|
31038
|
-
* 设置页本身就是一层容器,没有任何理由再套一层。
|
|
30967
|
+
* IPv6 要加方括号(`[::1]:3090`),否则地址里的冒号会让人把它读成「主机 `::1` + 端口」以外的
|
|
30968
|
+
* 东西,复制出去也非法。端口非有限值时不显示数字而不是显示 `NaN`。
|
|
31039
30969
|
*/
|
|
30970
|
+
function formatEndpoint(host, port) {
|
|
30971
|
+
return `${host.includes(":") ? `[${host}]` : host}:${Number.isFinite(port) ? port : ""}`;
|
|
30972
|
+
}
|
|
30973
|
+
/** 字节数转人话。回收站里常有 KB/MB 级文件,只显示原始字节数没人读得动。 */
|
|
30974
|
+
function formatBytes(value) {
|
|
30975
|
+
if (value < 1024) return `${value} B`;
|
|
30976
|
+
if (value < 1048576) return `${(value / 1024).toFixed(1)} KB`;
|
|
30977
|
+
return `${(value / 1024 / 1024).toFixed(1)} MB`;
|
|
30978
|
+
}
|
|
31040
30979
|
/**
|
|
31041
|
-
*
|
|
31042
|
-
*
|
|
31043
|
-
* 上一版留了个分区导航栏、一次只显示一张卡。对照 Vision Router 的实际长相才发现差得远:
|
|
31044
|
-
* 它是全部卡片一起列出来、想开哪张开哪张,标题栏自己就是入口。那句提示词写得很直白 ——
|
|
31045
|
-
* 「多张卡可以同时保持展开」。所以导航栏这一层是多余的,用户还得先点导航、再点卡片头,
|
|
31046
|
-
* 两次点击才走一步。
|
|
31047
|
-
*
|
|
31048
|
-
* 抽出来是为了让 DSH 设置页能把它**内联**渲染。
|
|
30980
|
+
* 监听器操作失败时的人话。
|
|
31049
30981
|
*
|
|
31050
|
-
*
|
|
31051
|
-
*
|
|
31052
|
-
* 这就是把弹窗嵌进设置页的代价:入口看起来是空的,因为内容在下一层。
|
|
31053
|
-
* 设置页本身就是一层容器,没有任何理由再套一层。
|
|
31054
|
-
*
|
|
31055
|
-
* `section` / `onSection` 保留是为了兼容旧调用方,但现在**不再决定显示哪一张卡**,
|
|
31056
|
-
* 只用来决定默认展开哪一张。
|
|
30982
|
+
* `code` 与 `message` 两个来源都要看:服务端给了已知码就翻成人话,否则退回原始 message;
|
|
30983
|
+
* 两个都没有才用 fallback。只认其中一个会在另一种情况下显示空白。
|
|
31057
30984
|
*/
|
|
31058
|
-
function
|
|
31059
|
-
|
|
31060
|
-
|
|
31061
|
-
|
|
31062
|
-
|
|
30985
|
+
function listenerOperationError(code, fallback, t) {
|
|
30986
|
+
if (code === "LISTENER_ADDRESS_IN_USE") return t("listenerAddressInUse");
|
|
30987
|
+
if (code === "LISTENER_ADDRESS_UNAVAILABLE") return t("listenerAddressUnavailable");
|
|
30988
|
+
if (code === "LISTENER_PERMISSION_DENIED") return t("listenerPermissionDenied");
|
|
30989
|
+
return fallback ?? t("listenerFailed");
|
|
30990
|
+
}
|
|
30991
|
+
/** 等一会儿。轮询与「等隧道出地址」都用它。 */
|
|
30992
|
+
async function delay(ms) {
|
|
30993
|
+
await new Promise((resolve) => {
|
|
30994
|
+
setTimeout(resolve, ms);
|
|
31063
30995
|
});
|
|
31064
30996
|
}
|
|
31065
30997
|
/**
|
|
31066
|
-
*
|
|
30998
|
+
* 错误码 → 人话。
|
|
31067
30999
|
*
|
|
31068
|
-
*
|
|
31069
|
-
*
|
|
31070
|
-
*
|
|
31071
|
-
*
|
|
31072
|
-
* 七张卡同时挂在页面上,各张独立开合 —— 导航栏已经删掉了,卡片头自己就是入口。
|
|
31000
|
+
* 这张表是「用户看到什么」的唯一来源:`messageOf` 与各卡片都靠它把裸码翻成人话。
|
|
31001
|
+
* 压成一句「操作失败」会让用户反复点同一个按钮 —— 每一个码都指向不同的下一步,
|
|
31002
|
+
* 例如 `TUNNEL_MODE_ADMIN_DISABLED` 要人去停隧道、`ADMIN_HOST_NOT_LOOPBACK` 要人换地址打开。
|
|
31073
31003
|
*/
|
|
31074
|
-
function
|
|
31075
|
-
|
|
31076
|
-
|
|
31077
|
-
|
|
31078
|
-
|
|
31079
|
-
|
|
31080
|
-
|
|
31081
|
-
|
|
31082
|
-
|
|
31083
|
-
|
|
31084
|
-
|
|
31085
|
-
|
|
31086
|
-
|
|
31087
|
-
|
|
31088
|
-
|
|
31089
|
-
|
|
31090
|
-
|
|
31091
|
-
|
|
31092
|
-
|
|
31093
|
-
|
|
31094
|
-
|
|
31095
|
-
|
|
31096
|
-
|
|
31097
|
-
|
|
31098
|
-
|
|
31099
|
-
|
|
31100
|
-
|
|
31101
|
-
|
|
31102
|
-
|
|
31103
|
-
|
|
31104
|
-
|
|
31105
|
-
|
|
31106
|
-
|
|
31107
|
-
|
|
31108
|
-
|
|
31109
|
-
|
|
31004
|
+
function apiErrorKey(error) {
|
|
31005
|
+
if (error.status === 401) return "errorUnauthorized";
|
|
31006
|
+
if (error.status === 429) return "errorRateLimited";
|
|
31007
|
+
if (error.code === "ROOT_NOT_GRANTED" || error.code === "ROOT_FORBIDDEN") return "errorRootDenied";
|
|
31008
|
+
if (error.code === "ROOT_BUILTIN") return "errorRootBuiltin";
|
|
31009
|
+
if (error.code === "ETAG_MISMATCH") return "errorEtag";
|
|
31010
|
+
if (error.code === "LISTENER_HOST_INVALID") return "listenerHostInvalid";
|
|
31011
|
+
if (error.code === "LISTENER_PORT_INVALID") return "listenerPortInvalid";
|
|
31012
|
+
if (error.code === "REMOTE_ENABLED") return "disableToEditListener";
|
|
31013
|
+
if (error.code === "NO_ENDPOINT") return "configTextNoEndpoints";
|
|
31014
|
+
if (error.code === "TUNNEL_MODE_ADMIN_DISABLED") return "errorTunnelModeAdminDisabled";
|
|
31015
|
+
if (error.code === "ADMIN_PROXY_FORWARDED") return "errorAdminProxyForwarded";
|
|
31016
|
+
if (error.code === "ADMIN_HOST_NOT_LOOPBACK") return "errorAdminHostNotLoopback";
|
|
31017
|
+
if (error.code === "ADMIN_LOOPBACK_REQUIRED") return "errorAdminLoopbackRequired";
|
|
31018
|
+
if (error.code === "PLUGIN_OFFICIAL_IMMUTABLE") return "pluginsErrorOfficial";
|
|
31019
|
+
if (error.code === "PLUGIN_NAME_INVALID") return "pluginsErrorNameInvalid";
|
|
31020
|
+
if (error.code === "PROFILE_UNKNOWN") return "pluginsErrorProfileUnknown";
|
|
31021
|
+
if (error.code === "MANIFEST_UNREADABLE") return "pluginsErrorManifestUnreadable";
|
|
31022
|
+
if (error.code === "PLUGIN_NOT_INSTALLED") return "pluginsErrorNotInstalled";
|
|
31023
|
+
if (error.code.includes("PATH") || error.code.includes("SYMLINK") || error.code.includes("REPARSE")) return "errorPathInvalid";
|
|
31024
|
+
if (error.status === 403) return "errorForbidden";
|
|
31025
|
+
if (error.status === 404) return "errorNotFound";
|
|
31026
|
+
if (error.status === 409) return "errorConflict";
|
|
31027
|
+
}
|
|
31028
|
+
function messageOf(cause, t) {
|
|
31029
|
+
if (cause instanceof WorkspaceApiError) {
|
|
31030
|
+
const translated = apiErrorKey(cause);
|
|
31031
|
+
if (translated !== void 0) return t(translated);
|
|
31032
|
+
}
|
|
31033
|
+
return cause instanceof Error ? cause.message : String(cause);
|
|
31034
|
+
}
|
|
31035
|
+
function pluginStateKey(state) {
|
|
31036
|
+
if (state === "loaded") return "pluginsStateLoaded";
|
|
31037
|
+
if (state === "runtime-provided") return "pluginsStateRuntimeProvided";
|
|
31038
|
+
if (state === "installed-not-loaded") return "pluginsStateInstalledNotLoaded";
|
|
31039
|
+
return "pluginsStateDeclaredMissing";
|
|
31110
31040
|
}
|
|
31111
31041
|
/**
|
|
31112
|
-
*
|
|
31113
|
-
* the standalone `/dsh-workspace` page render exactly the same body.
|
|
31114
|
-
*/
|
|
31115
|
-
/**
|
|
31116
|
-
* 设置面板:七张卡片平铺,各自独立开合。
|
|
31042
|
+
* Shows a freshly minted `DSH1:` config text.
|
|
31117
31043
|
*
|
|
31118
|
-
*
|
|
31119
|
-
*
|
|
31044
|
+
* The text is a credential carrier, so this panel keeps the "it is a key" reminder visible and lists
|
|
31045
|
+
* the addresses the host actually detected, letting the user confirm them before sending the text away.
|
|
31046
|
+
* Copying degrades to "the text is selected, copy it yourself" when the Clipboard API is unavailable —
|
|
31047
|
+
* the normal case when this WebUI is opened over plain HTTP on a LAN address.
|
|
31120
31048
|
*/
|
|
31121
|
-
function
|
|
31122
|
-
const {
|
|
31123
|
-
const [
|
|
31124
|
-
const
|
|
31125
|
-
const
|
|
31126
|
-
const [label, setLabel] = (0, react.useState)("");
|
|
31127
|
-
const [listenerHost, setListenerHost] = (0, react.useState)("0.0.0.0");
|
|
31128
|
-
const [listenerPort, setListenerPort] = (0, react.useState)("3090");
|
|
31129
|
-
const [pairing, setPairing] = (0, react.useState)();
|
|
31130
|
-
const [configText, setConfigText] = (0, react.useState)();
|
|
31131
|
-
const [configEndpoint, setConfigEndpoint] = (0, react.useState)("");
|
|
31132
|
-
const [configTextBusy, setConfigTextBusy] = (0, react.useState)(false);
|
|
31133
|
-
const [image, setImage] = (0, react.useState)();
|
|
31134
|
-
const [imageDraft, setImageDraft] = (0, react.useState)("");
|
|
31135
|
-
const [imageBusy, setImageBusy] = (0, react.useState)(false);
|
|
31136
|
-
const [openCards, setOpenCards] = (0, react.useState)(() => /* @__PURE__ */ new Set([props.initialOpen]));
|
|
31137
|
-
const [imageSaved, setImageSaved] = (0, react.useState)(false);
|
|
31138
|
-
const [error, setError] = (0, react.useState)();
|
|
31139
|
-
const [notice, setNotice] = (0, react.useState)();
|
|
31140
|
-
const [tunnel, setTunnel] = (0, react.useState)();
|
|
31141
|
-
const [tunnelBusy, setTunnelBusy] = (0, react.useState)(false);
|
|
31142
|
-
const [activity, setActivity] = (0, react.useState)();
|
|
31143
|
-
const [pruneDays, setPruneDays] = (0, react.useState)(30);
|
|
31144
|
-
const [pruneBusy, setPruneBusy] = (0, react.useState)(false);
|
|
31145
|
-
const [now, setNow] = (0, react.useState)(() => Date.now());
|
|
31146
|
-
const notify = (0, react.useCallback)((key, params) => {
|
|
31147
|
-
setNotice(params === void 0 ? {
|
|
31148
|
-
id: Date.now() + Math.random(),
|
|
31149
|
-
key
|
|
31150
|
-
} : {
|
|
31151
|
-
id: Date.now() + Math.random(),
|
|
31152
|
-
key,
|
|
31153
|
-
params
|
|
31154
|
-
});
|
|
31155
|
-
}, []);
|
|
31156
|
-
(0, react.useEffect)(() => {
|
|
31157
|
-
if (notice === void 0) return;
|
|
31158
|
-
const timer = window.setTimeout(() => setNotice(void 0), 3500);
|
|
31159
|
-
return () => window.clearTimeout(timer);
|
|
31160
|
-
}, [notice]);
|
|
31161
|
-
(0, react.useEffect)(() => {
|
|
31162
|
-
const timer = window.setInterval(() => setNow(Date.now()), 3e4);
|
|
31163
|
-
return () => window.clearInterval(timer);
|
|
31164
|
-
}, []);
|
|
31165
|
-
const copyText = (0, react.useCallback)(async (text) => {
|
|
31049
|
+
function ConfigTextPanel(props) {
|
|
31050
|
+
const { t } = useWorkspaceI18n();
|
|
31051
|
+
const [copyState, setCopyState] = (0, react.useState)();
|
|
31052
|
+
const textRef = (0, react.useRef)(null);
|
|
31053
|
+
const copy = (0, react.useCallback)(async () => {
|
|
31166
31054
|
const clipboard = globalThis.navigator?.clipboard;
|
|
31167
31055
|
if (clipboard !== void 0) try {
|
|
31168
|
-
await clipboard.writeText(text);
|
|
31169
|
-
|
|
31056
|
+
await clipboard.writeText(props.result.text);
|
|
31057
|
+
setCopyState("done");
|
|
31058
|
+
return;
|
|
31170
31059
|
} catch {}
|
|
31171
|
-
|
|
31172
|
-
|
|
31173
|
-
node.
|
|
31174
|
-
node.style.position = "fixed";
|
|
31175
|
-
node.style.top = "-9999px";
|
|
31176
|
-
node.setAttribute("readonly", "");
|
|
31177
|
-
document.body.append(node);
|
|
31060
|
+
const node = textRef.current;
|
|
31061
|
+
if (node !== null) {
|
|
31062
|
+
node.focus();
|
|
31178
31063
|
node.select();
|
|
31179
|
-
const ok = document.execCommand("copy");
|
|
31180
|
-
node.remove();
|
|
31181
|
-
if (ok) return "done";
|
|
31182
|
-
} catch {}
|
|
31183
|
-
return "manual";
|
|
31184
|
-
}, []);
|
|
31185
|
-
const isOpen = (0, react.useCallback)((card) => openCards.has(card), [openCards]);
|
|
31186
|
-
const toggleCard = (0, react.useCallback)((card) => {
|
|
31187
|
-
setOpenCards((previous) => {
|
|
31188
|
-
const next = new Set(previous);
|
|
31189
|
-
if (!next.delete(card)) next.add(card);
|
|
31190
|
-
return next;
|
|
31191
|
-
});
|
|
31192
|
-
}, []);
|
|
31193
|
-
const refresh = (0, react.useCallback)(async () => {
|
|
31194
|
-
try {
|
|
31195
|
-
setTunnel(await props.api.tunnelStatus());
|
|
31196
|
-
} catch {}
|
|
31197
|
-
try {
|
|
31198
|
-
const next = await props.api.status();
|
|
31199
|
-
setStatus(next);
|
|
31200
|
-
if (openCards.has("trash")) setTrash((await props.api.trashItems()).items);
|
|
31201
|
-
if (openCards.has("image")) {
|
|
31202
|
-
const settings = await props.api.imageSettings();
|
|
31203
|
-
setImage(settings);
|
|
31204
|
-
setImageDraft(settings.subdir);
|
|
31205
|
-
setImageSaved(false);
|
|
31206
|
-
}
|
|
31207
|
-
} catch (cause) {
|
|
31208
|
-
if (!(cause instanceof WorkspaceApiError && cause.status === 403 && (cause.code === "TUNNEL_MODE_ADMIN_DISABLED" || cause.code === "ADMIN_PROXY_FORWARDED"))) setError(cause);
|
|
31209
|
-
}
|
|
31210
|
-
}, [props.api, openCards]);
|
|
31211
|
-
const tunnelErrorText = (0, react.useCallback)((s) => {
|
|
31212
|
-
if (s.errorKey !== void 0) return t(s.errorKey, s.errorParams ?? {});
|
|
31213
|
-
return s.error ?? t("listenerFailed");
|
|
31214
|
-
}, [t]);
|
|
31215
|
-
const pollActivityOnce = (0, react.useCallback)(async (active) => {
|
|
31216
|
-
while (active()) {
|
|
31217
|
-
try {
|
|
31218
|
-
const a = await props.api.tunnelActivity();
|
|
31219
|
-
if (!active()) return;
|
|
31220
|
-
setActivity(a.stage === "idle" ? void 0 : a);
|
|
31221
|
-
} catch {}
|
|
31222
|
-
await delay(400);
|
|
31223
|
-
}
|
|
31224
|
-
}, [props.api]);
|
|
31225
|
-
const waitForTunnelUrl = (0, react.useCallback)(async (active) => {
|
|
31226
|
-
for (let attempt = 0; attempt < 40 && active(); attempt += 1) {
|
|
31227
|
-
await delay(500);
|
|
31228
|
-
const s = await props.api.tunnelStatus();
|
|
31229
|
-
if (!active()) return null;
|
|
31230
|
-
setTunnel(s);
|
|
31231
|
-
if (s.errorKey !== void 0 || s.error !== void 0) throw new Error(tunnelErrorText(s));
|
|
31232
|
-
if (s.running && s.url !== null) return s.url;
|
|
31233
|
-
}
|
|
31234
|
-
return null;
|
|
31235
|
-
}, [props.api, tunnelErrorText]);
|
|
31236
|
-
const generateConfigForUrl = (0, react.useCallback)(async (baseUrl, noticeKey = "configCreated") => {
|
|
31237
|
-
setConfigTextBusy(true);
|
|
31238
|
-
setError(void 0);
|
|
31239
|
-
try {
|
|
31240
|
-
const currentRoots = status?.roots ?? [];
|
|
31241
|
-
const value = await props.api.createConfigText({
|
|
31242
|
-
...currentRoots.length === 0 ? {} : { rootIds: currentRoots.map((root) => root.id) },
|
|
31243
|
-
endpoints: [{
|
|
31244
|
-
label: t("tunnelEndpointLabel"),
|
|
31245
|
-
baseUrl
|
|
31246
|
-
}]
|
|
31247
|
-
});
|
|
31248
|
-
setConfigText(value);
|
|
31249
|
-
notify(noticeKey);
|
|
31250
|
-
return true;
|
|
31251
|
-
} catch (cause) {
|
|
31252
|
-
setError(cause);
|
|
31253
|
-
return false;
|
|
31254
|
-
} finally {
|
|
31255
|
-
setConfigTextBusy(false);
|
|
31256
|
-
}
|
|
31257
|
-
}, [
|
|
31258
|
-
props.api,
|
|
31259
|
-
status,
|
|
31260
|
-
t,
|
|
31261
|
-
notify
|
|
31262
|
-
]);
|
|
31263
|
-
const createTunnel = (0, react.useCallback)(async (opts = {}) => {
|
|
31264
|
-
if (!window.confirm(t("tunnelStartConfirm"))) return;
|
|
31265
|
-
const wantConfig = opts.thenGenConfig === true;
|
|
31266
|
-
let alive = true;
|
|
31267
|
-
setTunnelBusy(true);
|
|
31268
|
-
setError(void 0);
|
|
31269
|
-
setConfigText(void 0);
|
|
31270
|
-
setActivity({
|
|
31271
|
-
stage: "locating",
|
|
31272
|
-
labelKey: "tunnelStageLocating",
|
|
31273
|
-
percent: null
|
|
31274
|
-
});
|
|
31275
|
-
pollActivityOnce(() => alive);
|
|
31276
|
-
try {
|
|
31277
|
-
const currentRoots = status?.roots ?? [];
|
|
31278
|
-
const created = await props.api.tunnelCreate(wantConfig, { ...currentRoots.length === 0 ? {} : { rootIds: currentRoots.map((root) => root.id) } });
|
|
31279
|
-
setTunnel(created);
|
|
31280
|
-
setActivity(void 0);
|
|
31281
|
-
if (created.errorKey !== void 0 || created.error !== void 0) throw new Error(tunnelErrorText(created));
|
|
31282
|
-
if (wantConfig && created.configText !== void 0) {
|
|
31283
|
-
setConfigText(created.configText);
|
|
31284
|
-
await refresh();
|
|
31285
|
-
notify("tunnelConfigAuto");
|
|
31286
|
-
return;
|
|
31287
|
-
}
|
|
31288
|
-
const url = created.url ?? await waitForTunnelUrl(() => alive);
|
|
31289
|
-
await refresh();
|
|
31290
|
-
if (url === null) {
|
|
31291
|
-
notify("tunnelUrlPending");
|
|
31292
|
-
return;
|
|
31293
|
-
}
|
|
31294
|
-
if (wantConfig) await generateConfigForUrl(url, "tunnelConfigAuto");
|
|
31295
|
-
else notify("tunnelStarted");
|
|
31296
|
-
} catch (cause) {
|
|
31297
|
-
setError(cause);
|
|
31298
|
-
} finally {
|
|
31299
|
-
alive = false;
|
|
31300
|
-
setActivity(void 0);
|
|
31301
|
-
setTunnelBusy(false);
|
|
31302
|
-
}
|
|
31303
|
-
}, [
|
|
31304
|
-
props.api,
|
|
31305
|
-
refresh,
|
|
31306
|
-
status,
|
|
31307
|
-
t,
|
|
31308
|
-
notify,
|
|
31309
|
-
pollActivityOnce,
|
|
31310
|
-
waitForTunnelUrl,
|
|
31311
|
-
tunnelErrorText,
|
|
31312
|
-
generateConfigForUrl
|
|
31313
|
-
]);
|
|
31314
|
-
const stopTunnel = (0, react.useCallback)(async () => {
|
|
31315
|
-
if (!window.confirm(t("tunnelStopConfirm"))) return;
|
|
31316
|
-
setTunnelBusy(true);
|
|
31317
|
-
setError(void 0);
|
|
31318
|
-
try {
|
|
31319
|
-
const next = await props.api.tunnelStop();
|
|
31320
|
-
setTunnel(next);
|
|
31321
|
-
await refresh();
|
|
31322
|
-
notify("tunnelStopped");
|
|
31323
|
-
} catch (cause) {
|
|
31324
|
-
setError(cause);
|
|
31325
|
-
} finally {
|
|
31326
|
-
setTunnelBusy(false);
|
|
31327
|
-
}
|
|
31328
|
-
}, [
|
|
31329
|
-
props.api,
|
|
31330
|
-
refresh,
|
|
31331
|
-
t,
|
|
31332
|
-
notify
|
|
31333
|
-
]);
|
|
31334
|
-
const tunnelConfigClick = (0, react.useCallback)(async () => {
|
|
31335
|
-
if (tunnel?.running === true) {
|
|
31336
|
-
if (!window.confirm(t("tunnelStopConfirm"))) return;
|
|
31337
|
-
setTunnelBusy(true);
|
|
31338
|
-
setError(void 0);
|
|
31339
|
-
try {
|
|
31340
|
-
const stopped = await props.api.tunnelStop();
|
|
31341
|
-
setTunnel(stopped);
|
|
31342
|
-
await refresh();
|
|
31343
|
-
} catch (cause) {
|
|
31344
|
-
setError(cause);
|
|
31345
|
-
} finally {
|
|
31346
|
-
setTunnelBusy(false);
|
|
31347
|
-
}
|
|
31348
|
-
await createTunnel({ thenGenConfig: true });
|
|
31349
|
-
} else await createTunnel({ thenGenConfig: true });
|
|
31350
|
-
}, [
|
|
31351
|
-
props.api,
|
|
31352
|
-
tunnel,
|
|
31353
|
-
t,
|
|
31354
|
-
createTunnel,
|
|
31355
|
-
refresh,
|
|
31356
|
-
setError
|
|
31357
|
-
]);
|
|
31358
|
-
(0, react.useEffect)(() => {
|
|
31359
|
-
refresh();
|
|
31360
|
-
}, [refresh]);
|
|
31361
|
-
(0, react.useEffect)(() => {
|
|
31362
|
-
if (status === void 0) return;
|
|
31363
|
-
setListenerHost(status.configuredHost);
|
|
31364
|
-
setListenerPort(String(status.configuredPort));
|
|
31365
|
-
}, [status?.configuredHost, status?.configuredPort]);
|
|
31366
|
-
const roots = status?.roots ?? [];
|
|
31367
|
-
const managementLocked = tunnel?.running === true;
|
|
31368
|
-
const [grantedScopes, setGrantedScopes] = (0, react.useState)([...DEFAULT_DEVICE_SCOPES]);
|
|
31369
|
-
const toggleScope = (0, react.useCallback)((scope) => {
|
|
31370
|
-
setGrantedScopes((current) => current.includes(scope) ? current.filter((item) => item !== scope) : [...current, scope]);
|
|
31371
|
-
}, []);
|
|
31372
|
-
const grantNotice = grantedScopes.length === 0 ? t("scopeGrantNoticeEmpty") : (status?.devices?.length ?? 0) === 0 ? t("scopeGrantNotice") : t("scopeGrantNoticeExisting");
|
|
31373
|
-
const parsedListenerPort = Number(listenerPort);
|
|
31374
|
-
const listenerPortValid = Number.isInteger(parsedListenerPort) && parsedListenerPort >= 1 && parsedListenerPort <= 65535;
|
|
31375
|
-
const listenerDirty = status !== void 0 && (listenerHost.trim() !== status.configuredHost || parsedListenerPort !== status.configuredPort);
|
|
31376
|
-
const poll = async (id) => {
|
|
31377
|
-
for (let attempt = 0; attempt < 30; attempt += 1) {
|
|
31378
|
-
await delay(300);
|
|
31379
|
-
const next = await props.api.operation(id);
|
|
31380
|
-
setStatus(next);
|
|
31381
|
-
const operation = next.operation;
|
|
31382
|
-
if (operation?.state === "succeeded") {
|
|
31383
|
-
if (operation.pairingCode !== void 0) setPairing({
|
|
31384
|
-
code: operation.pairingCode,
|
|
31385
|
-
...operation.pairingExpiresAt === void 0 ? {} : { expiresAt: operation.pairingExpiresAt }
|
|
31386
|
-
});
|
|
31387
|
-
return;
|
|
31388
|
-
}
|
|
31389
|
-
if (operation?.state === "failed") throw new Error(listenerOperationError(operation.code, operation.message, t));
|
|
31390
31064
|
}
|
|
31391
|
-
|
|
31392
|
-
};
|
|
31393
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("
|
|
31394
|
-
|
|
31065
|
+
setCopyState("manual");
|
|
31066
|
+
}, [props.result.text]);
|
|
31067
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
31068
|
+
style: { marginTop: 14 },
|
|
31395
31069
|
children: [
|
|
31396
31070
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31397
|
-
className: "daw-admin-topbar",
|
|
31398
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LanguageToggle, {})
|
|
31399
|
-
}),
|
|
31400
|
-
error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31401
31071
|
className: "daw-warning",
|
|
31402
|
-
|
|
31403
|
-
children: messageOf(error, t)
|
|
31072
|
+
children: t("configTextSecurity")
|
|
31404
31073
|
}),
|
|
31405
|
-
|
|
31406
|
-
className: "daw-
|
|
31407
|
-
|
|
31408
|
-
children: t(notice.key, notice.params)
|
|
31074
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31075
|
+
className: "daw-list-meta",
|
|
31076
|
+
children: t("configTextHint")
|
|
31409
31077
|
}),
|
|
31410
|
-
|
|
31411
|
-
|
|
31412
|
-
|
|
31413
|
-
|
|
31078
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
31079
|
+
ref: textRef,
|
|
31080
|
+
className: "daw-input",
|
|
31081
|
+
style: {
|
|
31082
|
+
width: "100%",
|
|
31083
|
+
height: "auto",
|
|
31084
|
+
minHeight: 76,
|
|
31085
|
+
marginTop: 8,
|
|
31086
|
+
padding: "8px 9px",
|
|
31087
|
+
fontFamily: "ui-monospace,SFMono-Regular,Consolas,monospace",
|
|
31088
|
+
fontSize: 12,
|
|
31089
|
+
lineHeight: 1.5,
|
|
31090
|
+
resize: "vertical"
|
|
31091
|
+
},
|
|
31092
|
+
readOnly: true,
|
|
31093
|
+
rows: 4,
|
|
31094
|
+
spellCheck: false,
|
|
31095
|
+
value: props.result.text,
|
|
31096
|
+
onFocus: (event) => event.currentTarget.select()
|
|
31414
31097
|
}),
|
|
31415
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(
|
|
31416
|
-
|
|
31417
|
-
|
|
31418
|
-
|
|
31419
|
-
|
|
31420
|
-
|
|
31098
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
31099
|
+
className: "daw-form-row",
|
|
31100
|
+
style: {
|
|
31101
|
+
marginTop: 8,
|
|
31102
|
+
marginBottom: 10,
|
|
31103
|
+
gridTemplateColumns: "auto auto minmax(0,1fr)",
|
|
31104
|
+
alignItems: "center"
|
|
31105
|
+
},
|
|
31421
31106
|
children: [
|
|
31422
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
31423
|
-
className: "daw-
|
|
31424
|
-
|
|
31425
|
-
|
|
31426
|
-
|
|
31427
|
-
|
|
31428
|
-
value: path,
|
|
31429
|
-
onChange: (event) => setPath(event.target.value)
|
|
31430
|
-
}),
|
|
31431
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
31432
|
-
className: "daw-input",
|
|
31433
|
-
placeholder: t("displayLabel"),
|
|
31434
|
-
value: label,
|
|
31435
|
-
onChange: (event) => setLabel(event.target.value)
|
|
31436
|
-
}),
|
|
31437
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
31438
|
-
className: "daw-command primary",
|
|
31439
|
-
disabled: managementLocked,
|
|
31440
|
-
title: managementLocked ? t("managementLockedWhileTunnel") : void 0,
|
|
31441
|
-
onClick: () => {
|
|
31442
|
-
setError(void 0);
|
|
31443
|
-
props.api.addRoot(path, label || void 0).then(() => {
|
|
31444
|
-
setPath("");
|
|
31445
|
-
setLabel("");
|
|
31446
|
-
notify("rootAdded");
|
|
31447
|
-
return refresh();
|
|
31448
|
-
}).catch(setError);
|
|
31449
|
-
},
|
|
31450
|
-
children: t("addRoot")
|
|
31451
|
-
})
|
|
31452
|
-
]
|
|
31107
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
31108
|
+
className: "daw-command primary",
|
|
31109
|
+
onClick: () => {
|
|
31110
|
+
copy();
|
|
31111
|
+
},
|
|
31112
|
+
children: t("copy")
|
|
31453
31113
|
}),
|
|
31454
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
31455
|
-
className: "daw-
|
|
31456
|
-
children:
|
|
31457
|
-
className: "daw-list-row",
|
|
31458
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
31459
|
-
className: "daw-list-title",
|
|
31460
|
-
children: [root.label, root.builtin === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
31461
|
-
className: "daw-pill on",
|
|
31462
|
-
children: t("builtinRootBadge")
|
|
31463
|
-
})]
|
|
31464
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31465
|
-
className: "daw-list-meta",
|
|
31466
|
-
children: root.realPath
|
|
31467
|
-
})] }), root.builtin === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
31468
|
-
className: "daw-list-meta daw-builtin-hint",
|
|
31469
|
-
children: t("builtinRootHint")
|
|
31470
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
31471
|
-
className: "daw-icon danger",
|
|
31472
|
-
title: t("removeRoot"),
|
|
31473
|
-
"aria-label": t("removeRoot"),
|
|
31474
|
-
disabled: managementLocked,
|
|
31475
|
-
onClick: () => {
|
|
31476
|
-
if (!window.confirm(t("removeRootConfirm", { label: root.label }))) return;
|
|
31477
|
-
setError(void 0);
|
|
31478
|
-
props.api.removeRoot(root.id).then(() => {
|
|
31479
|
-
notify("rootRemoved");
|
|
31480
|
-
return refresh();
|
|
31481
|
-
}).catch(setError);
|
|
31482
|
-
},
|
|
31483
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Trash2, { size: 15 })
|
|
31484
|
-
})]
|
|
31485
|
-
}, root.id))
|
|
31114
|
+
copyState === "done" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
31115
|
+
className: "daw-pill on",
|
|
31116
|
+
children: t("copied")
|
|
31486
31117
|
}),
|
|
31487
|
-
|
|
31488
|
-
className: "daw-list-meta
|
|
31489
|
-
children: t("
|
|
31118
|
+
copyState === "manual" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
31119
|
+
className: "daw-list-meta",
|
|
31120
|
+
children: t("copyManual")
|
|
31490
31121
|
})
|
|
31491
31122
|
]
|
|
31492
|
-
}
|
|
31493
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(
|
|
31494
|
-
|
|
31495
|
-
|
|
31496
|
-
descKey: tabDescKey("remote"),
|
|
31497
|
-
open: isOpen("remote"),
|
|
31498
|
-
onToggle: () => toggleCard("remote"),
|
|
31123
|
+
}),
|
|
31124
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
31125
|
+
className: "daw-list-meta",
|
|
31126
|
+
style: { marginBottom: 2 },
|
|
31499
31127
|
children: [
|
|
31500
|
-
|
|
31501
|
-
|
|
31502
|
-
|
|
31503
|
-
|
|
31504
|
-
|
|
31505
|
-
|
|
31506
|
-
|
|
31507
|
-
|
|
31508
|
-
|
|
31509
|
-
|
|
31128
|
+
t("configTextDevice"),
|
|
31129
|
+
": ",
|
|
31130
|
+
props.result.deviceName,
|
|
31131
|
+
" · ",
|
|
31132
|
+
t("configTextDeviceId"),
|
|
31133
|
+
": ",
|
|
31134
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
31135
|
+
style: { fontFamily: "ui-monospace,SFMono-Regular,Consolas,monospace" },
|
|
31136
|
+
children: props.result.deviceId
|
|
31137
|
+
})
|
|
31138
|
+
]
|
|
31139
|
+
}),
|
|
31140
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31141
|
+
className: "daw-list-meta",
|
|
31142
|
+
style: { marginBottom: 10 },
|
|
31143
|
+
children: t("configTextScopes")
|
|
31144
|
+
}),
|
|
31145
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t("configTextDetected") }),
|
|
31146
|
+
props.result.endpoints.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31147
|
+
className: "daw-warning",
|
|
31148
|
+
style: { marginTop: 8 },
|
|
31149
|
+
children: t("configTextNoEndpoints")
|
|
31150
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31151
|
+
className: "daw-list",
|
|
31152
|
+
children: props.result.endpoints.map((endpoint) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31153
|
+
className: "daw-list-row",
|
|
31154
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31155
|
+
className: "daw-list-title",
|
|
31156
|
+
children: endpoint.label
|
|
31157
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
31158
|
+
className: "daw-list-meta",
|
|
31159
|
+
children: [endpoint.baseUrl, endpoint.kind === void 0 ? "" : ` · ${endpoint.kind}`]
|
|
31160
|
+
})] })
|
|
31161
|
+
}, `${endpoint.label}:${endpoint.baseUrl}`))
|
|
31162
|
+
})
|
|
31163
|
+
]
|
|
31164
|
+
});
|
|
31165
|
+
}
|
|
31166
|
+
//#endregion
|
|
31167
|
+
//#region src/client/plugins.tsx
|
|
31168
|
+
/**
|
|
31169
|
+
* 插件清单的右侧栏形态:一个真正的 `SidebarRightTabDefinition`。
|
|
31170
|
+
*
|
|
31171
|
+
* ## 为什么不是「再注册一个 slot」
|
|
31172
|
+
*
|
|
31173
|
+
* 报告里第 2 项讲的是 DSH 0.2.1-rc.3 的三处 pin 机制(`view-pins` / `panel-pins` /
|
|
31174
|
+
* `guide-pins`)。pin 只作用于**注册了 panel / view / guide 的插件**——桥原来那两个座位
|
|
31175
|
+
* (`conversation.chat.turnTail` 与 `settings.section`)都不在这三者的管辖内,所以哪怕把
|
|
31176
|
+
* 三个新 slot 全注册一遍,桥自己的东西也一个都 pin 不到,只能消费别人的条目。
|
|
31177
|
+
*
|
|
31178
|
+
* 要真拿到 pin,唯一的路是**成为 tab 类型提供方**:往 `sidebarRightTabs` 注册一个
|
|
31179
|
+
* definition,并在 `sidebar.right.pane.tab` / `.title` 两个 keyed seat 上按 `definition.id`
|
|
31180
|
+
* 挂上 body 与标题。这样 guide 页会列出这张卡片,用户能在 rc.3 的 More 菜单里把它
|
|
31181
|
+
* 固定 / 取消固定(`guide-pins.d.ts` 的 `FIXED_GUIDE_KIND = "files"` 只豁免文件卡片,
|
|
31182
|
+
* 桥的 kind 不在豁免名单里)。
|
|
31183
|
+
*
|
|
31184
|
+
* ## 为什么单独一个文件
|
|
31185
|
+
*
|
|
31186
|
+
* `workspace.tsx` 已经 1200 多行,而 `docs/project/TECH-DEBT.md` 的行数预算写的是
|
|
31187
|
+
* 600 行/文件、超预算要先登记。把新 UI 面拆出来既不加剧那笔债,也让「设置页里的折叠卡」
|
|
31188
|
+
* 与「右侧栏的常驻 tab」这两种形态各自独立演进。
|
|
31189
|
+
*
|
|
31190
|
+
* ## rc.2 上的降级
|
|
31191
|
+
*
|
|
31192
|
+
* `sidebarRightTabs` 在 rc.2 与 rc.3 都提供,definition 契约两版一致,所以 tab 本身能用;
|
|
31193
|
+
* 但 rc.2 的 `GuideBody` 没有 `entryPins` / `entryActions` / More 菜单,卡片会正常列出、
|
|
31194
|
+
* 点开也正常,只是**不能 pin**。这是可接受的降级:能力随宿主版本生长,桥不做版本判断。
|
|
31195
|
+
*/
|
|
31196
|
+
/** definition 的 id:tab 系统的实现身份,同时是 body/title 两个 keyed seat 的 key。 */
|
|
31197
|
+
const PLUGINS_TAB_ID = "dsh-remote-bridge-plugins";
|
|
31198
|
+
/**
|
|
31199
|
+
* tab 类型名。
|
|
31200
|
+
*
|
|
31201
|
+
* 必须与 DSH 内置类型不冲突 —— 内置的有 `guide` / `files` / `terminal` / `browser` /
|
|
31202
|
+
* `plan` / `schedule` / `deliverables` 等。冲突时 `sidebarRightTabs.register` 会抛
|
|
31203
|
+
* `sidebarRight: tab kind "…" is already registered`,整个插件起不来,所以带上插件前缀。
|
|
31204
|
+
*/
|
|
31205
|
+
const PLUGINS_TAB_KIND = "remote-bridge-plugins";
|
|
31206
|
+
/**
|
|
31207
|
+
* 构造 tab 类型定义。
|
|
31208
|
+
*
|
|
31209
|
+
* 文案用 thunk(`() => string`)而不是现取的字符串:契约里写明 thunk 每次使用都会重读,
|
|
31210
|
+
* 所以切换语言不需要重新注册。`title` 是 tab chip 的初始文字,开 tab 的那一刻被捕获进
|
|
31211
|
+
* layout record。
|
|
31212
|
+
*
|
|
31213
|
+
* `patterns` 故意省略 —— 省略即「page type」,只能按 kind 打开(guide 卡片点一下就是这条路),
|
|
31214
|
+
* 不会被文件地址意外匹配到。`priority` 用默认的 `extension`。
|
|
31215
|
+
*/
|
|
31216
|
+
function pluginsTabDefinition() {
|
|
31217
|
+
const label = () => translate(getWorkspaceLocale(), "pluginsTab");
|
|
31218
|
+
const description = () => translate(getWorkspaceLocale(), "pluginsTabDesc");
|
|
31219
|
+
return {
|
|
31220
|
+
id: PLUGINS_TAB_ID,
|
|
31221
|
+
kind: PLUGINS_TAB_KIND,
|
|
31222
|
+
title: label,
|
|
31223
|
+
guide: [{
|
|
31224
|
+
id: "plugins",
|
|
31225
|
+
order: 40,
|
|
31226
|
+
title: label,
|
|
31227
|
+
description,
|
|
31228
|
+
icon: import_lucide_react.Package
|
|
31229
|
+
}]
|
|
31230
|
+
};
|
|
31231
|
+
}
|
|
31232
|
+
/**
|
|
31233
|
+
* tab chip 里的文字。
|
|
31234
|
+
*
|
|
31235
|
+
* 不读 `useTabInfo` 是刻意的:标题在开 tab 时就被捕获了,跟着当前语言走没有意义;
|
|
31236
|
+
* 而且 rc.2 / rc.3 的 hook 注入形状不同,少依赖一处就少一处版本耦合。
|
|
31237
|
+
*/
|
|
31238
|
+
function PluginsTabTitle() {
|
|
31239
|
+
const { t } = useWorkspaceI18n();
|
|
31240
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: t("pluginsTab") });
|
|
31241
|
+
}
|
|
31242
|
+
/**
|
|
31243
|
+
* 右侧栏里的插件清单。
|
|
31244
|
+
*
|
|
31245
|
+
* 与设置页那张折叠卡读同一对端点(`GET/PATCH /settings/plugins`),但形态不同:
|
|
31246
|
+
* 这里是**常驻面板**,没有卡片外壳,清单直接铺开;刷新按钮常驻而不是「展开才拉」——
|
|
31247
|
+
* 用户既然把这张卡 pin 到了 guide 页,就是打算经常看。
|
|
31248
|
+
*
|
|
31249
|
+
* 三条与设置页一致的产品约束(详见 `workspace.tsx` 的 `PluginsSection` 注释):
|
|
31250
|
+
* 官方包不可停用、`runtime-provided` 不是故障、改完要重启才生效。
|
|
31251
|
+
*/
|
|
31252
|
+
function makePluginsTabBody(api) {
|
|
31253
|
+
return function PluginsTabBody() {
|
|
31254
|
+
const { t } = useWorkspaceI18n();
|
|
31255
|
+
const [inventory, setInventory] = (0, react.useState)(void 0);
|
|
31256
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
31257
|
+
const [checking, setChecking] = (0, react.useState)(false);
|
|
31258
|
+
const [toggling, setToggling] = (0, react.useState)(void 0);
|
|
31259
|
+
const [error, setError] = (0, react.useState)(void 0);
|
|
31260
|
+
const [restartRequired, setRestartRequired] = (0, react.useState)(false);
|
|
31261
|
+
const load = (0, react.useCallback)(async (checkUpdates) => {
|
|
31262
|
+
if (checkUpdates) setChecking(true);
|
|
31263
|
+
else setBusy(true);
|
|
31264
|
+
setError(void 0);
|
|
31265
|
+
try {
|
|
31266
|
+
setInventory(await api.pluginInventory(checkUpdates));
|
|
31267
|
+
} catch (cause) {
|
|
31268
|
+
setError(messageOf(cause, t));
|
|
31269
|
+
} finally {
|
|
31270
|
+
if (checkUpdates) setChecking(false);
|
|
31271
|
+
else setBusy(false);
|
|
31272
|
+
}
|
|
31273
|
+
}, [t]);
|
|
31274
|
+
(0, react.useEffect)(() => {
|
|
31275
|
+
load(false);
|
|
31276
|
+
}, [load]);
|
|
31277
|
+
const toggle = (0, react.useCallback)(async (entry) => {
|
|
31278
|
+
setToggling(entry.name);
|
|
31279
|
+
setError(void 0);
|
|
31280
|
+
try {
|
|
31281
|
+
await api.togglePlugin(entry.name, !entry.loaded);
|
|
31282
|
+
setInventory(await api.pluginInventory(false));
|
|
31283
|
+
setRestartRequired(true);
|
|
31284
|
+
} catch (cause) {
|
|
31285
|
+
setError(messageOf(cause, t));
|
|
31286
|
+
} finally {
|
|
31287
|
+
setToggling(void 0);
|
|
31288
|
+
}
|
|
31289
|
+
}, [t]);
|
|
31290
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
31291
|
+
className: "daw-plugins-tab",
|
|
31292
|
+
children: [
|
|
31293
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
31294
|
+
className: "daw-list-meta",
|
|
31295
|
+
children: t("pluginsHint")
|
|
31296
|
+
}),
|
|
31297
|
+
error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31298
|
+
className: "daw-warning",
|
|
31299
|
+
role: "alert",
|
|
31300
|
+
children: error
|
|
31301
|
+
}),
|
|
31302
|
+
restartRequired && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31303
|
+
className: "daw-success",
|
|
31304
|
+
role: "status",
|
|
31305
|
+
children: t("pluginsRestartRequired")
|
|
31306
|
+
}),
|
|
31307
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
31308
|
+
className: "daw-form-row",
|
|
31309
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
31310
|
+
type: "button",
|
|
31311
|
+
className: "daw-command",
|
|
31312
|
+
disabled: busy,
|
|
31313
|
+
onClick: () => {
|
|
31314
|
+
load(false);
|
|
31315
|
+
},
|
|
31510
31316
|
children: [
|
|
31511
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
31512
|
-
className: "daw-field",
|
|
31513
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("bindIp") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
31514
|
-
className: "daw-input",
|
|
31515
|
-
value: listenerHost,
|
|
31516
|
-
disabled: status?.remoteEnabled || managementLocked,
|
|
31517
|
-
spellCheck: false,
|
|
31518
|
-
placeholder: "0.0.0.0",
|
|
31519
|
-
onChange: (event) => setListenerHost(event.target.value)
|
|
31520
|
-
})]
|
|
31521
|
-
}),
|
|
31522
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
31523
|
-
className: "daw-field",
|
|
31524
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("bindPort") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
31525
|
-
className: "daw-input",
|
|
31526
|
-
type: "number",
|
|
31527
|
-
min: 1,
|
|
31528
|
-
max: 65535,
|
|
31529
|
-
inputMode: "numeric",
|
|
31530
|
-
value: listenerPort,
|
|
31531
|
-
disabled: status?.remoteEnabled || managementLocked,
|
|
31532
|
-
onChange: (event) => setListenerPort(event.target.value)
|
|
31533
|
-
})]
|
|
31534
|
-
}),
|
|
31535
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
31536
|
-
className: "daw-command",
|
|
31537
|
-
disabled: managementLocked || status?.remoteEnabled || listenerHost.trim() === "" || !listenerPortValid || !listenerDirty,
|
|
31538
|
-
onClick: () => {
|
|
31539
|
-
setError(void 0);
|
|
31540
|
-
props.api.configureListener(listenerHost.trim(), parsedListenerPort).then((result) => poll(result.id)).then(() => {
|
|
31541
|
-
notify("listenerSaved");
|
|
31542
|
-
return refresh();
|
|
31543
|
-
}).catch(setError);
|
|
31544
|
-
},
|
|
31545
|
-
children: t("saveListener")
|
|
31546
|
-
})
|
|
31547
|
-
]
|
|
31548
|
-
}),
|
|
31549
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
31550
|
-
className: "daw-listener-status",
|
|
31551
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
31552
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("strong", { children: [t("configuredListener"), ":"] }),
|
|
31553
|
-
" ",
|
|
31554
|
-
formatEndpoint(status?.configuredHost ?? listenerHost, status?.configuredPort ?? parsedListenerPort)
|
|
31555
|
-
] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
31556
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("strong", { children: [t("actualListener"), ":"] }),
|
|
31317
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.RefreshCw, { size: 14 }),
|
|
31557
31318
|
" ",
|
|
31558
|
-
|
|
31559
|
-
] })]
|
|
31560
|
-
}),
|
|
31561
|
-
status?.remoteEnabled && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31562
|
-
className: "daw-list-meta daw-listener-hint",
|
|
31563
|
-
children: t("disableToEditListener")
|
|
31564
|
-
}),
|
|
31565
|
-
!status?.remoteEnabled && listenerDirty && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31566
|
-
className: "daw-list-meta daw-listener-hint",
|
|
31567
|
-
children: t("saveBeforeEnable")
|
|
31568
|
-
}),
|
|
31569
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
31570
|
-
className: `daw-pill${status?.remoteEnabled ? " on" : ""}`,
|
|
31571
|
-
children: status?.remoteEnabled ? t("remoteEnabled") : t("loopbackOnly")
|
|
31572
|
-
}) }),
|
|
31573
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
31574
|
-
className: "daw-tunnel",
|
|
31575
|
-
children: [
|
|
31576
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
31577
|
-
className: "daw-tunnel-head",
|
|
31578
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t("tunnelTitle") }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
31579
|
-
className: `daw-pill${tunnel?.running ? " on" : ""}`,
|
|
31580
|
-
children: tunnel?.running ? t("tunnelRunning") : t("tunnelStopped")
|
|
31581
|
-
})]
|
|
31582
|
-
}),
|
|
31583
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
31584
|
-
className: "daw-list-meta",
|
|
31585
|
-
children: t("tunnelHint")
|
|
31586
|
-
}),
|
|
31587
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31588
|
-
className: "daw-warning",
|
|
31589
|
-
children: t("tunnelSecurityWarning")
|
|
31590
|
-
}),
|
|
31591
|
-
tunnel?.errorKey !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31592
|
-
className: "daw-warning",
|
|
31593
|
-
role: "alert",
|
|
31594
|
-
children: t(tunnel.errorKey, tunnel.errorParams ?? {})
|
|
31595
|
-
}),
|
|
31596
|
-
tunnel?.errorKey === void 0 && tunnel?.error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31597
|
-
className: "daw-warning",
|
|
31598
|
-
role: "alert",
|
|
31599
|
-
children: tunnel.error
|
|
31600
|
-
}),
|
|
31601
|
-
tunnel?.url !== void 0 && tunnel.url !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
31602
|
-
className: "daw-url-row",
|
|
31603
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31604
|
-
className: "daw-tunnel-url",
|
|
31605
|
-
children: tunnel.url
|
|
31606
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopyButton, {
|
|
31607
|
-
text: tunnel.url,
|
|
31608
|
-
title: t("copyUrl"),
|
|
31609
|
-
copiedText: t("copied"),
|
|
31610
|
-
manualText: t("copyManual"),
|
|
31611
|
-
copyText
|
|
31612
|
-
})]
|
|
31613
|
-
}),
|
|
31614
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
31615
|
-
className: "daw-command primary",
|
|
31616
|
-
style: { marginTop: 10 },
|
|
31617
|
-
disabled: tunnelBusy || tunnel?.running !== true && roots.length === 0,
|
|
31618
|
-
onClick: () => {
|
|
31619
|
-
if (tunnel?.running === true) stopTunnel();
|
|
31620
|
-
else createTunnel();
|
|
31621
|
-
},
|
|
31622
|
-
children: tunnel?.running === true ? t("tunnelStop") : t("tunnelCreate")
|
|
31623
|
-
}),
|
|
31624
|
-
activity !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
31625
|
-
className: "daw-tunnel-progress",
|
|
31626
|
-
style: { marginTop: 10 },
|
|
31627
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31628
|
-
className: "daw-progress-track",
|
|
31629
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31630
|
-
className: `daw-progress-fill${activity.percent === null ? " indeterminate" : ""}`,
|
|
31631
|
-
style: activity.percent === null ? void 0 : { width: `${activity.percent}%` }
|
|
31632
|
-
})
|
|
31633
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31634
|
-
className: "daw-list-meta",
|
|
31635
|
-
style: { marginTop: 4 },
|
|
31636
|
-
children: activity.labelKey === "" ? "" : t(activity.labelKey, activity.labelParams ?? {})
|
|
31637
|
-
})]
|
|
31638
|
-
})
|
|
31319
|
+
busy ? t("loading") : t("refresh")
|
|
31639
31320
|
]
|
|
31640
|
-
}),
|
|
31641
|
-
|
|
31642
|
-
|
|
31643
|
-
|
|
31644
|
-
|
|
31645
|
-
|
|
31646
|
-
padding: 0
|
|
31321
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
31322
|
+
type: "button",
|
|
31323
|
+
className: "daw-command",
|
|
31324
|
+
disabled: checking,
|
|
31325
|
+
onClick: () => {
|
|
31326
|
+
load(true);
|
|
31647
31327
|
},
|
|
31648
|
-
children:
|
|
31649
|
-
|
|
31650
|
-
|
|
31651
|
-
|
|
31328
|
+
children: checking ? t("pluginsChecking") : t("pluginsCheckUpdates")
|
|
31329
|
+
})]
|
|
31330
|
+
}),
|
|
31331
|
+
inventory === void 0 ? busy ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31332
|
+
className: "daw-list-meta",
|
|
31333
|
+
children: t("loading")
|
|
31334
|
+
}) : null : !inventory.available ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31335
|
+
className: "daw-list-meta",
|
|
31336
|
+
children: t("pluginsUnavailable", { reason: inventory.reason ?? "" })
|
|
31337
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
31338
|
+
className: "daw-list-meta",
|
|
31339
|
+
children: [
|
|
31340
|
+
inventory.profile !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [inventory.profile, " · "] }),
|
|
31341
|
+
inventory.loadedCount,
|
|
31342
|
+
" / ",
|
|
31343
|
+
inventory.items.length,
|
|
31344
|
+
inventory.problemCount > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [" · ", t("pluginsProblems", { count: inventory.problemCount })] })
|
|
31345
|
+
]
|
|
31346
|
+
}), inventory.items.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31347
|
+
className: "daw-list-meta daw-empty",
|
|
31348
|
+
children: t("pluginsEmpty")
|
|
31349
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31350
|
+
className: "daw-list",
|
|
31351
|
+
children: inventory.items.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
31352
|
+
className: "daw-list-row",
|
|
31353
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
|
|
31354
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
31355
|
+
className: "daw-list-title",
|
|
31356
|
+
children: [entry.name, entry.installed !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
31357
|
+
className: "daw-list-meta",
|
|
31358
|
+
children: [" · ", entry.installed]
|
|
31359
|
+
})]
|
|
31360
|
+
}),
|
|
31361
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31362
|
+
className: "daw-list-meta",
|
|
31363
|
+
children: entry.description ?? t("pluginsNoDescription")
|
|
31364
|
+
}),
|
|
31365
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
31366
|
+
className: "daw-list-meta",
|
|
31652
31367
|
children: [
|
|
31653
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
31654
|
-
|
|
31655
|
-
|
|
31656
|
-
onChange: () => toggleScope(scope)
|
|
31368
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
31369
|
+
className: `daw-pill${entry.loaded ? " on" : ""}`,
|
|
31370
|
+
children: t(pluginStateKey(entry.state))
|
|
31657
31371
|
}),
|
|
31658
|
-
" ",
|
|
31659
|
-
|
|
31372
|
+
entry.updateAvailable === true && entry.latestVersion !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [" ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
31373
|
+
className: "daw-pill",
|
|
31374
|
+
children: t("pluginsUpdateAvailable", { version: entry.latestVersion })
|
|
31375
|
+
})] }),
|
|
31376
|
+
entry.updateAvailable === false && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [" · ", t("pluginsLatest")] })
|
|
31660
31377
|
]
|
|
31661
|
-
}
|
|
31662
|
-
})
|
|
31663
|
-
|
|
31664
|
-
|
|
31665
|
-
|
|
31666
|
-
children: grantNotice
|
|
31667
|
-
}),
|
|
31668
|
-
status?.remoteEnabled ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
31669
|
-
className: "daw-command danger",
|
|
31670
|
-
disabled: managementLocked,
|
|
31671
|
-
title: managementLocked ? t("managementLockedWhileTunnel") : void 0,
|
|
31672
|
-
onClick: () => {
|
|
31673
|
-
if (!window.confirm(t("disableRemoteConfirm"))) return;
|
|
31674
|
-
setError(void 0);
|
|
31675
|
-
props.api.disableRemote().then((result) => poll(result.id)).then(() => {
|
|
31676
|
-
notify("remoteDisabled");
|
|
31677
|
-
return refresh();
|
|
31678
|
-
}).catch(setError);
|
|
31679
|
-
},
|
|
31680
|
-
children: t("disableRemote")
|
|
31681
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
31682
|
-
className: "daw-command primary",
|
|
31683
|
-
disabled: managementLocked || roots.length === 0 || listenerDirty || !listenerPortValid || listenerHost.trim() === "" || grantedScopes.length === 0,
|
|
31684
|
-
onClick: () => {
|
|
31685
|
-
setError(void 0);
|
|
31686
|
-
props.api.enableRemote(roots.map((root) => root.id), grantedScopes).then((result) => poll(result.id)).catch(setError);
|
|
31687
|
-
},
|
|
31688
|
-
children: t("enablePairing")
|
|
31689
|
-
}),
|
|
31690
|
-
(status?.remoteEnabled === true || managementLocked) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
31691
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
31692
|
-
className: "daw-command daw-inline-command",
|
|
31693
|
-
disabled: managementLocked || grantedScopes.length === 0,
|
|
31694
|
-
title: managementLocked ? t("managementLockedWhileTunnel") : void 0,
|
|
31695
|
-
onClick: () => {
|
|
31696
|
-
setError(void 0);
|
|
31697
|
-
props.api.createPairing(roots.map((root) => root.id), grantedScopes).then((value) => {
|
|
31698
|
-
setPairing(value);
|
|
31699
|
-
notify("pairingCreated");
|
|
31700
|
-
}).catch(setError);
|
|
31701
|
-
},
|
|
31702
|
-
children: t("newPairingCode")
|
|
31703
|
-
}),
|
|
31704
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
31705
|
-
className: "daw-command daw-inline-command",
|
|
31706
|
-
disabled: managementLocked || configTextBusy,
|
|
31707
|
-
title: managementLocked ? t("managementLockedWhileTunnel") : void 0,
|
|
31708
|
-
onClick: () => {
|
|
31709
|
-
const manual = configEndpoint.trim();
|
|
31710
|
-
setConfigTextBusy(true);
|
|
31711
|
-
setError(void 0);
|
|
31712
|
-
props.api.createConfigText({
|
|
31713
|
-
...roots.length === 0 ? {} : { rootIds: roots.map((root) => root.id) },
|
|
31714
|
-
...manual === "" ? {} : { endpoints: [{
|
|
31715
|
-
label: t("configTextManualLabel"),
|
|
31716
|
-
baseUrl: manual
|
|
31717
|
-
}] }
|
|
31718
|
-
}).then((value) => {
|
|
31719
|
-
setConfigText(value);
|
|
31720
|
-
notify("configCreated");
|
|
31721
|
-
}).catch(setError).finally(() => setConfigTextBusy(false));
|
|
31722
|
-
},
|
|
31723
|
-
children: t("newConfigText")
|
|
31724
|
-
}),
|
|
31725
|
-
tunnel !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
31726
|
-
className: "daw-command daw-inline-command",
|
|
31727
|
-
disabled: configTextBusy || tunnelBusy,
|
|
31378
|
+
})
|
|
31379
|
+
] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: entry.toggleable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
31380
|
+
type: "button",
|
|
31381
|
+
className: "daw-command",
|
|
31382
|
+
disabled: toggling === entry.name,
|
|
31728
31383
|
onClick: () => {
|
|
31729
|
-
|
|
31384
|
+
toggle(entry);
|
|
31730
31385
|
},
|
|
31731
|
-
children:
|
|
31732
|
-
})
|
|
31733
|
-
|
|
31734
|
-
|
|
31735
|
-
|
|
31736
|
-
|
|
31737
|
-
|
|
31738
|
-
|
|
31739
|
-
|
|
31740
|
-
|
|
31741
|
-
|
|
31742
|
-
|
|
31743
|
-
|
|
31744
|
-
|
|
31745
|
-
|
|
31746
|
-
|
|
31747
|
-
|
|
31748
|
-
|
|
31749
|
-
|
|
31750
|
-
|
|
31751
|
-
|
|
31752
|
-
|
|
31753
|
-
|
|
31754
|
-
|
|
31755
|
-
|
|
31756
|
-
|
|
31757
|
-
|
|
31758
|
-
|
|
31759
|
-
|
|
31760
|
-
|
|
31761
|
-
|
|
31762
|
-
|
|
31763
|
-
|
|
31764
|
-
|
|
31765
|
-
|
|
31766
|
-
|
|
31767
|
-
|
|
31768
|
-
|
|
31769
|
-
|
|
31770
|
-
|
|
31771
|
-
|
|
31772
|
-
|
|
31773
|
-
|
|
31774
|
-
|
|
31775
|
-
|
|
31386
|
+
children: entry.loaded ? t("pluginsDisable") : t("pluginsEnable")
|
|
31387
|
+
}) })]
|
|
31388
|
+
}, entry.name))
|
|
31389
|
+
})] })
|
|
31390
|
+
]
|
|
31391
|
+
});
|
|
31392
|
+
};
|
|
31393
|
+
}
|
|
31394
|
+
//#endregion
|
|
31395
|
+
//#region src/client/styles.ts
|
|
31396
|
+
const STYLE_ID = "dsh-workspace-style";
|
|
31397
|
+
/**
|
|
31398
|
+
* 插件的全部样式。
|
|
31399
|
+
*
|
|
31400
|
+
* 导出出来是为了让测试能守住一条容易失守的不变量:**CSS 变量块必须同时覆盖
|
|
31401
|
+
* `.daw-root` 与 `.daw-settings-section`**。
|
|
31402
|
+
*
|
|
31403
|
+
* 这条防线来自一个真实发生过的隐形 bug:设置页内联渲染时,那一节没有 `.daw-root` 祖先,
|
|
31404
|
+
* 于是 `var(--daw-accent)` 解析失败 → `.daw-command.primary` 的 `background` 失效变透明、
|
|
31405
|
+
* 而 `color` 仍是 `#fff` → **白字落在白底上,按钮彻底看不见**。
|
|
31406
|
+
* 类型检查、单测、构建全都发现不了,只有肉眼看渲染或量计算样式才抓得到。
|
|
31407
|
+
*/
|
|
31408
|
+
const WORKSPACE_CSS = `
|
|
31409
|
+
.daw-root,.daw-settings-section,.daw-plugins-tab{--daw-bg:var(--dsw-alias-bg-base,var(--dsw-alias-bg-layer-2,#f7f8fa));--daw-panel:var(--dsw-alias-bg-layer-3,var(--dsw-alias-bg-layer-2,#fff));--daw-ink:var(--dsw-alias-label-primary,#18202a);--daw-muted:var(--dsw-alias-label-tertiary,#687381);--daw-line:var(--dsw-alias-border-l2,#dfe4e8);--daw-accent:var(--dsw-alias-brand-primary,#087f8c);--daw-accent-soft:var(--dsw-alias-bg-module-platform,#e5f5f6);--daw-good:var(--dsw-alias-state-success-primary,#2f7d4a);--daw-danger:var(--dsw-alias-state-error-primary,#b33a3a);color:var(--daw-ink);background:transparent;font:13px/1.45 Inter,ui-sans-serif,system-ui,sans-serif;letter-spacing:0;min-width:0}
|
|
31410
|
+
.daw-root *,.daw-settings-section *,.daw-plugins-tab *{box-sizing:border-box;letter-spacing:0}
|
|
31411
|
+
/* 输入控件基线(UI-04):此前只有 :focus-visible 一条,非聚焦态全靠浏览器默认(2px inset/Arial/灰底),
|
|
31412
|
+
深色令牌下实测白底黑字与面板割裂。这里给齐边框/底/字/内边距,聚焦态只换描边。 */
|
|
31413
|
+
.daw-input{width:100%;min-width:0;height:30px;padding:4px 9px;border:1px solid var(--dsw-alias-border-l2,var(--daw-line));border-radius:6px;background:var(--dsw-alias-bg-layer-3,var(--daw-panel));color:var(--dsw-alias-label-primary,var(--daw-ink));font:inherit}
|
|
31414
|
+
.daw-input::placeholder{color:var(--dsw-alias-label-tertiary,var(--daw-muted))}
|
|
31415
|
+
.daw-input:focus-visible{border-color:var(--dsw-alias-brand-primary,var(--daw-accent));outline:none}
|
|
31416
|
+
.daw-input:disabled{opacity:.55;cursor:not-allowed}.daw-icon{width:30px;padding:0}
|
|
31417
|
+
.daw-icon:hover{background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft))}
|
|
31418
|
+
.daw-icon.danger{color:var(--daw-danger);border-color:transparent;background:transparent}
|
|
31419
|
+
.daw-icon:disabled{opacity:.45;cursor:not-allowed}
|
|
31420
|
+
/* 普通命令按钮基线(UI-04):旧版写死 background:#fff/border:#cbd2d8,深色令牌下成了白块黑字。
|
|
31421
|
+
全部改走 DSH 令牌;.primary 沿用面板底色做次按钮、真正的主操作由调用方加 .primary 配合。 */
|
|
31422
|
+
.daw-command{height:30px;padding:0 12px;border-radius:6px;border:1px solid var(--dsw-alias-border-l2,var(--daw-line));background:var(--dsw-alias-bg-layer-3,var(--daw-panel));color:var(--dsw-alias-label-primary,var(--daw-ink));font:inherit;cursor:pointer}
|
|
31423
|
+
.daw-command:hover:not(:disabled){border-color:var(--dsw-alias-label-dimmed,var(--daw-line))}
|
|
31424
|
+
.daw-command:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,var(--daw-accent));outline-offset:1px}
|
|
31425
|
+
/* 禁用态(UI-05):此前无任何 :disabled 样式,只剩浏览器默认的 30% 文字且仍是箭头手型。 */
|
|
31426
|
+
.daw-command:disabled{opacity:.5;cursor:not-allowed}
|
|
31427
|
+
.daw-command.primary{color:var(--dsw-alias-label-primary,var(--daw-ink));background:var(--dsw-alias-bg-module-platform,var(--daw-panel));border-color:var(--dsw-alias-border-l2,var(--daw-line));font-weight:500}
|
|
31428
|
+
.daw-command.primary:hover:not(:disabled){background:var(--dsw-alias-bg-layer-2,var(--daw-panel));border-color:var(--dsw-alias-label-dimmed,var(--daw-line))}
|
|
31429
|
+
.daw-command.danger{color:var(--daw-danger)}
|
|
31430
|
+
.daw-command.danger:hover:not(:disabled){border-color:var(--daw-danger)}
|
|
31431
|
+
/* 图标+文字的小复制按钮:图标和文字对齐,窄屏只露图标也不改变按钮高度。 */
|
|
31432
|
+
.daw-command.icon-as-command{display:inline-flex;align-items:center;gap:6px;padding:0 10px;white-space:nowrap}
|
|
31433
|
+
.daw-copy-label{font-size:12px}
|
|
31434
|
+
.daw-empty{padding:14px 2px;text-align:center}
|
|
31435
|
+
.daw-admin{overflow:auto;padding:18px;display:flex;flex-direction:column;gap:10px}.daw-admin h3{font-size:14px;margin:0 0 12px}
|
|
31436
|
+
.daw-card{border:1px solid var(--dsw-alias-border-l2,var(--daw-line));background:var(--dsw-alias-bg-layer-3,var(--daw-panel));border-radius:12px;list-style:none;transition:border-color .16s,background .16s}
|
|
31437
|
+
.daw-card:hover{border-color:var(--dsw-alias-label-dimmed,var(--daw-line))}
|
|
31438
|
+
.daw-card-open{background:var(--dsw-alias-bg-layer-2,var(--daw-panel));border-color:var(--dsw-alias-label-dimmed,var(--daw-line))}
|
|
31439
|
+
.daw-card-header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}
|
|
31440
|
+
.daw-card-header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,var(--daw-accent));outline-offset:-2px}
|
|
31441
|
+
.daw-card-icon{color:var(--dsw-alias-label-tertiary,var(--daw-muted));flex:none;display:flex;align-items:center}
|
|
31442
|
+
.daw-card-headtext{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}
|
|
31443
|
+
.daw-card-name{color:var(--dsw-alias-label-primary,var(--daw-ink));font-size:15px;font-weight:600;line-height:1.4}
|
|
31444
|
+
.daw-card-desc{color:var(--dsw-alias-label-tertiary,var(--daw-muted));font-size:13px;line-height:1.5}
|
|
31445
|
+
.daw-card-chevron{color:var(--dsw-alias-label-tertiary,var(--daw-muted));flex:none;transition:transform .16s;font-size:12px;line-height:1}
|
|
31446
|
+
.daw-card-chevron-open{transform:rotate(180deg)}
|
|
31447
|
+
.daw-card-body{border-top:1px solid var(--dsw-alias-border-l2,var(--daw-line));margin:0 16px;padding-bottom:8px}.daw-settings-section{display:grid;grid-template-rows:auto minmax(0,1fr);gap:10px;min-width:0;min-height:0}.daw-plugins-tab{overflow:auto;padding:14px 16px;display:flex;flex-direction:column;gap:10px;min-width:0;min-height:0}.daw-settings-section>.daw-list-meta{margin:0}.daw-settings-section .daw-admin{padding:14px 2px}.daw-qr-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px;margin-top:12px}.daw-qr-card{margin:0;display:grid;justify-items:center;gap:8px;padding:16px;border:1px solid var(--daw-line);border-radius:6px;background:var(--daw-panel)}.daw-qr-image{width:180px;height:180px;background:var(--dsw-alias-bg-layer-3,#fff);border-radius:4px}.daw-qr-site{font-weight:600}.daw-qr-url{font:12px ui-monospace,SFMono-Regular,Consolas,monospace;color:var(--daw-accent);text-decoration:none;overflow-wrap:anywhere;text-align:center}.daw-qr-url:hover{text-decoration:underline}.daw-form-row{display:grid;grid-template-columns:minmax(0,1fr) minmax(120px,.45fr) auto;gap:8px;margin-bottom:14px}.daw-listener-form{display:grid;grid-template-columns:minmax(180px,1fr) 130px auto;gap:8px;align-items:end}.daw-field{display:grid;gap:5px;color:var(--dsw-alias-label-secondary,var(--daw-muted));font-size:12px}.daw-listener-status{display:flex;flex-wrap:wrap;gap:5px 18px;padding:9px 0;color:var(--daw-muted);font:12px ui-monospace,SFMono-Regular,Consolas,monospace}.daw-listener-hint{margin:0 0 8px}.daw-list{border-top:1px solid var(--daw-line)}.daw-list-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:12px;align-items:center;min-height:48px;border-bottom:1px solid var(--daw-line);padding:7px 2px}.daw-list-title{font-weight:600;overflow-wrap:anywhere}
|
|
31448
|
+
.daw-list-title .daw-pill{margin-left:8px;vertical-align:middle}
|
|
31449
|
+
.daw-builtin-hint{text-align:right;align-self:center}
|
|
31450
|
+
.daw-builtin-notice{margin-top:10px;padding:8px 10px;border-radius:8px;background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft))}.daw-list-meta{font-size:12px;color:var(--daw-muted);overflow-wrap:anywhere}.daw-scope-grid{display:flex;flex-wrap:wrap;gap:6px 12px;padding:8px 0}.daw-check{display:inline-flex;align-items:center;gap:5px;color:var(--dsw-alias-label-secondary,var(--daw-muted))}.daw-code{font:600 22px ui-monospace,SFMono-Regular,Consolas,monospace;letter-spacing:2px!important;padding:14px;border:1px solid var(--dsw-alias-border-l2,var(--daw-line));background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));border-radius:8px;text-align:center;margin:12px 0}.daw-warning{flex:none;border:1px solid var(--dsw-alias-border-l2,var(--daw-line));border-radius:8px;background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));padding:9px 11px;color:var(--daw-muted);margin-bottom:0}.daw-pill{display:inline-flex;align-items:center;gap:5px;border-radius:999px;padding:1px 8px;background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));color:var(--dsw-alias-label-secondary,var(--daw-muted));font-size:11px;font-weight:500;line-height:17px}.daw-pill.on{background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));color:var(--dsw-alias-state-success-primary,var(--daw-good))}.daw-tunnel{margin-top:14px;border-top:1px dashed var(--dsw-alias-border-l2,var(--daw-line));padding-top:12px}.daw-tunnel-head{display:flex;align-items:center;gap:8px;justify-content:space-between}.daw-tunnel-url{font:500 13px ui-monospace,SFMono-Regular,Consolas,monospace;overflow-wrap:anywhere;padding:9px 11px;border:1px solid var(--dsw-alias-border-l2,var(--daw-line));background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));border-radius:8px}.daw-tunnel-progress{max-width:360px}.daw-progress-track{height:6px;border-radius:999px;background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));overflow:hidden}.daw-progress-fill{height:100%;border-radius:999px;background:var(--dsw-alias-state-info-primary,var(--daw-accent,var(--daw-line)));transition:width .25s ease}.daw-inline-command{margin-left:8px}
|
|
31451
|
+
/* 成功反馈条(UI-07):与 .daw-warning 同形,但走成功色,操作成功不再静默。 */
|
|
31452
|
+
.daw-success{flex:none;border:1px solid var(--dsw-alias-state-success-primary,var(--daw-good));border-radius:8px;background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));padding:9px 11px;color:var(--dsw-alias-state-success-primary,var(--daw-good));margin-bottom:0}
|
|
31453
|
+
.daw-admin-topbar{display:flex;justify-content:flex-end;margin-bottom:8px}
|
|
31454
|
+
.daw-url-row{display:flex;align-items:center;gap:8px;margin-top:8px}.daw-url-row .daw-tunnel-url{flex:1 1 auto;margin:0;min-width:0}
|
|
31455
|
+
.daw-code-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:12px 0}.daw-code-row .daw-code{margin:0;flex:1 1 220px;min-width:0}.daw-copy-inline{flex:none}
|
|
31456
|
+
/* 隧道/监听进行中的流动条(UI-08):percent=null 时铺一段高光在轨道里循环滑动,
|
|
31457
|
+
不再把「未知进度」伪装成恒定 30%。 */
|
|
31458
|
+
.daw-progress-fill.indeterminate{width:40%;background:linear-gradient(90deg,transparent,var(--dsw-alias-state-info-primary,var(--daw-accent,var(--daw-line))),transparent);animation:daw-indeterminate 1.1s ease-in-out infinite}
|
|
31459
|
+
@keyframes daw-indeterminate{0%{transform:translateX(-120%)}100%{transform:translateX(280%)}}
|
|
31460
|
+
/* 「高级设置」组:外层是个普通卡片,组内的卡片缩进一格并挂一道左侧色条,
|
|
31461
|
+
让人一眼看出「这些是嵌进去的,不是与主列表同级」。功能上与主列表完全一致。 */
|
|
31462
|
+
.daw-advanced-group .daw-card-body{display:flex;flex-direction:column;gap:10px;padding-bottom:14px}
|
|
31463
|
+
.daw-advanced-body .daw-card{border-left:3px solid var(--dsw-alias-state-info-primary,var(--daw-accent));background:var(--dsw-alias-bg-layer-3,var(--daw-panel))}
|
|
31464
|
+
.daw-advanced-body .daw-card-header{padding:12px 14px}
|
|
31465
|
+
.daw-advanced-body .daw-card-name{font-size:14px}
|
|
31466
|
+
@media (max-width:720px){.daw-form-row,.daw-listener-form{grid-template-columns:1fr}.daw-admin{padding:12px}.daw-qr-grid{grid-template-columns:1fr}.daw-qr-image{width:150px;height:150px}.daw-inline-command{margin:8px 0 0}}
|
|
31467
|
+
`;
|
|
31468
|
+
function installWorkspaceStyles() {
|
|
31469
|
+
if (document.getElementById(STYLE_ID) !== null) return;
|
|
31470
|
+
const style = document.createElement("style");
|
|
31471
|
+
style.id = STYLE_ID;
|
|
31472
|
+
style.dataset.plugin = "dsh-workspace";
|
|
31473
|
+
style.textContent = WORKSPACE_CSS;
|
|
31474
|
+
document.head.appendChild(style);
|
|
31475
|
+
}
|
|
31476
|
+
//#endregion
|
|
31477
|
+
//#region src/client/app-qr.ts
|
|
31478
|
+
const APP_QR_CODES = [{
|
|
31479
|
+
"key": "github",
|
|
31480
|
+
"label": "GitHub",
|
|
31481
|
+
"url": "https://github.com/westanke/dsh-companion/releases",
|
|
31482
|
+
"dataUrl": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNjQiIGhlaWdodD0iMjY0IiB2aWV3Qm94PSIwIDAgNDEgNDEiIHNoYXBlLXJlbmRlcmluZz0iY3Jpc3BFZGdlcyI+PHBhdGggZmlsbD0iI2ZmZmZmZiIgZD0iTTAgMGg0MXY0MUgweiIvPjxwYXRoIHN0cm9rZT0iIzAwMDAwMCIgZD0iTTQgNC41aDdtMSAwaDFtMiAwaDFtMyAwaDNtMyAwaDFtMSAwaDFtMiAwaDdNNCA1LjVoMW01IDBoMW0zIDBoM200IDBoMW0xIDBoMW0yIDBoMW0zIDBoMW01IDBoMU00IDYuNWgxbTEgMGgzbTEgMGgxbTMgMGgybTEgMGgxbTEgMGgxbTQgMGg1bTEgMGgxbTEgMGgzbTEgMGgxTTQgNy41aDFtMSAwaDNtMSAwaDFtMSAwaDJtMiAwaDFtMSAwaDJtMSAwaDJtMSAwaDFtMiAwaDFtMiAwaDFtMSAwaDNtMSAwaDFNNCA4LjVoMW0xIDBoM20xIDBoMW0xIDBoNG0xIDBoMm0zIDBoM20yIDBoMW0yIDBoMW0xIDBoM20xIDBoMU00IDkuNWgxbTUgMGgxbTEgMGgxbTEgMGgxbTMgMGgxbTEgMGg0bTEgMGgxbTQgMGgxbTUgMGgxTTQgMTAuNWg3bTEgMGgxbTEgMGgxbTEgMGgxbTEgMGgxbTEgMGgxbTEgMGgxbTEgMGgxbTEgMGgxbTEgMGgxbTEgMGg3TTEyIDExLjVoMW01IDBoNW0yIDBoNE00IDEyLjVoMW0zIDBoMW0xIDBoNG03IDBoMW0xIDBoM20zIDBoNW0yIDBoMU00IDEzLjVoMW02IDBoMW0zIDBoMW0xIDBoNG02IDBoMW0xIDBoMW0zIDBoM000IDE0LjVoMm00IDBoNm0zIDBoMW0xIDBoMW0yIDBoMm0xIDBoMW0yIDBoMW0yIDBoMW0xIDBoMU00IDE1LjVoMW0yIDBoMm0zIDBoMW0xIDBoM20xIDBoMW0yIDBoNW0xIDBoMW0xIDBoMW01IDBoMk01IDE2LjVoMW0xIDBoMW0yIDBoMW0xIDBoMW0yIDBoMW0xIDBoM20yIDBoN20xIDBoMW0xIDBoMm0xIDBoMk00IDE3LjVoMW02IDBoMW0xIDBoNW0yIDBoMW0yIDBoMW0xIDBoMW0xIDBoMW0xIDBoMW0xIDBoMW0xIDBoMU02IDE4LjVoMm0xIDBoM20xIDBoMm01IDBoNG0zIDBoMW0xIDBoM20yIDBoMk00IDE5LjVoMW0yIDBoMm00IDBoMm0zIDBoMW0yIDBoMW0xIDBoNG0yIDBoMk02IDIwLjVoMW0yIDBoMm0yIDBoMm0yIDBoM20xIDBoMW0zIDBoMm0xIDBoMW0xIDBoMW0xIDBoMW0yIDBoMU00IDIxLjVoMW0xIDBoMW0yIDBoMW0yIDBoMW0xIDBoMm0yIDBoMm0xIDBoMm00IDBoMW0xIDBoMW0zIDBoM000IDIyLjVoNW0xIDBoMW0xIDBoMm0xIDBoM20xIDBoMW0yIDBoMW0xIDBoNG0xIDBoM20xIDBoMW0xIDBoMU01IDIzLjVoMm0xIDBoMm0yIDBoMW0yIDBoMm0yIDBoMW0zIDBoM20xIDBoNW0zIDBoMU01IDI0LjVoMW0xIDBoMm0xIDBoMW0zIDBoMm0xIDBoMm0xIDBoMW0zIDBoM20zIDBoMW0xIDBoMk00IDI1LjVoM20xIDBoMW0yIDBoMm05IDBoM20xIDBoMm0xIDBoMW0xIDBoMW0xIDBoMW0xIDBoMU02IDI2LjVoMW0xIDBoMW0xIDBoMW0xIDBoMW00IDBoM20xIDBoMW0yIDBoMW0xIDBoNG0yIDBoMW0yIDBoMU04IDI3LjVoMm0xIDBoMW0xIDBoMm0yIDBoMW0xIDBoNW0xIDBoMW0yIDBoNW0zIDBoMU00IDI4LjVoNW0xIDBoMW0yIDBoNG0xIDBoMW0yIDBoMW0xIDBoMTFtMSAwaDJNMTIgMjkuNWgybTIgMGgybTEgMGgybTEgMGgxbTEgMGgxbTIgMGgybTMgMGgxbTEgMGgyTTQgMzAuNWg3bTEgMGgxbTEgMGgxbTQgMGgxbTEgMGgxbTIgMGgxbTEgMGgxbTEgMGgxbTEgMGgxbTEgMGgyTTQgMzEuNWgxbTUgMGgxbTMgMGgxbTUgMGgzbTEgMGgybTIgMGgxbTMgMGgxTTQgMzIuNWgxbTEgMGgzbTEgMGgxbTEgMGgxbTQgMGgzbTIgMGgxbTIgMGgybTEgMGg2bTEgMGgxTTQgMzMuNWgxbTEgMGgzbTEgMGgxbTIgMGgxbTEgMGgybTMgMGgybTMgMGgxbTEgMGgybTEgMGgzbTEgMGgxTTQgMzQuNWgxbTEgMGgzbTEgMGgxbTIgMGgzbTQgMGgxbTEgMGgxbTEgMGgxbTIgMGgybTEgMGg0TTQgMzUuNWgxbTUgMGgxbTQgMGgxbTUgMGgxbTIgMGgybTEgMGg0TTQgMzYuNWg3bTEgMGgxbTEgMGgzbTEgMGgybTEgMGgxbTEgMGg0bTEgMGgybTYgMGgxIi8+PC9zdmc+Cg=="
|
|
31483
|
+
}, {
|
|
31484
|
+
"key": "gitee",
|
|
31485
|
+
"label": "Gitee",
|
|
31486
|
+
"url": "https://gitee.com/westanke/dsh-companion/releases",
|
|
31487
|
+
"dataUrl": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNjQiIGhlaWdodD0iMjY0IiB2aWV3Qm94PSIwIDAgNDEgNDEiIHNoYXBlLXJlbmRlcmluZz0iY3Jpc3BFZGdlcyI+PHBhdGggZmlsbD0iI2ZmZmZmZiIgZD0iTTAgMGg0MXY0MUgweiIvPjxwYXRoIHN0cm9rZT0iIzAwMDAwMCIgZD0iTTQgNC41aDdtMSAwaDFtMSAwaDJtMSAwaDRtNiAwaDJtMSAwaDdNNCA1LjVoMW01IDBoMW0zIDBoMm0xIDBoMm0xIDBoMW0xIDBoMm0yIDBoMW0zIDBoMW01IDBoMU00IDYuNWgxbTEgMGgzbTEgMGgxbTIgMGgybTEgMGgxbTEgMGgxbTEgMGg0bTEgMGgxbTEgMGgybTEgMGgxbTEgMGgzbTEgMGgxTTQgNy41aDFtMSAwaDNtMSAwaDFtMSAwaDVtMiAwaDNtNSAwaDFtMiAwaDFtMSAwaDNtMSAwaDFNNCA4LjVoMW0xIDBoM20xIDBoMW0xIDBoMW0xIDBoM20zIDBoMW0yIDBoMW0xIDBoMW0xIDBoMW0yIDBoMW0xIDBoM20xIDBoMU00IDkuNWgxbTUgMGgxbTEgMGgybTMgMGgzbTQgMGgzbTEgMGgxbTEgMGgxbTUgMGgxTTQgMTAuNWg3bTEgMGgxbTEgMGgxbTEgMGgxbTEgMGgxbTEgMGgxbTEgMGgxbTEgMGgxbTEgMGgxbTEgMGgxbTEgMGg3TTEyIDExLjVoNW00IDBoMW0xIDBoNk00IDEyLjVoMW0zIDBoMW0xIDBoNG0xIDBoMm0xIDBoNG0zIDBoOW0yIDBoMU00IDEzLjVoMW0xIDBoM201IDBoMW0xIDBoMW0xIDBoNW00IDBoMW0xIDBoMW0zIDBoM000IDE0LjVoMW0xIDBoMW0xIDBoMW0xIDBoNG0zIDBoNG0xIDBoMW0xIDBoN20yIDBoMW0xIDBoMU01IDE1LjVoMW0zIDBoMW0xIDBoMm0yIDBoMW0xIDBoMW0xIDBoMW00IDBoMm0xIDBoNG00IDBoMk00IDE2LjVoMm0yIDBoMW0xIDBoMW0yIDBoMm0yIDBoMW0yIDBoMW0yIDBoMW0xIDBoMm0xIDBoMW0xIDBoMW0xIDBoMk04IDE3LjVoMm0xIDBoMW0yIDBoM20xIDBoMW0zIDBoM20xIDBoMm0xIDBoMW0xIDBoMW0xIDBoMU00IDE4LjVoMW0xIDBoMW0xIDBoM20xIDBoMW0xIDBoMW0xIDBoMW0xIDBoMm0xIDBoMW0yIDBoMW0xIDBoNW0xIDBoMW0yIDBoMU00IDE5LjVoMW00IDBoMW0yIDBoMW0yIDBoM20xIDBoNG0yIDBoMW0xIDBoNU00IDIwLjVoNW0xIDBoNW0yIDBoMW0zIDBoMW0xIDBoNG0xIDBoMW0xIDBoMW0xIDBoMm0xIDBoMU01IDIxLjVoMW01IDBoMm01IDBoM20xIDBoMW00IDBoMW0xIDBoMW0zIDBoM001IDIyLjVoMm0yIDBoM20yIDBoMW0xIDBoMW0xIDBoMm0xIDBoMW0yIDBoMW0xIDBoMm0xIDBoM20xIDBoMW0xIDBoMU00IDIzLjVoMW0yIDBoMW0zIDBoNW01IDBoNW0xIDBoMm0xIDBoMm0zIDBoMk00IDI0LjVoNG0xIDBoMm0xIDBoMm0xIDBoMW0yIDBoMm0yIDBoNW0xIDBoM20xIDBoMm0xIDBoMk00IDI1LjVoNG00IDBoM20xIDBoMW0zIDBoMm0xIDBoMW0xIDBoMW0xIDBoMW0xIDBoMW0xIDBoMW0yIDBoMk04IDI2LjVoNW0yIDBoNG0xIDBoMW0xIDBoMm0zIDBoMW0xIDBoMW0xIDBoMW0xIDBoMW0xIDBoMU02IDI3LjVoMW0yIDBoMW00IDBoM200IDBoMW0xIDBoM20zIDBoMm00IDBoMU00IDI4LjVoM20zIDBoM200IDBoMW0xIDBoMW0xIDBoMW0zIDBoMm0xIDBoNW0yIDBoMk0xMiAyOS41aDFtMiAwaDFtMyAwaDFtMSAwaDJtNCAwaDJtMyAwaDFtMSAwaDJNNCAzMC41aDdtMSAwaDFtMyAwaDRtMiAwaDFtMSAwaDJtMiAwaDFtMSAwaDFtMSAwaDJtMSAwaDFNNCAzMS41aDFtNSAwaDFtMiAwaDFtMiAwaDFtMiAwaDFtMyAwaDNtMiAwaDFtMyAwaDFtMiAwaDFNNCAzMi41aDFtMSAwaDNtMSAwaDFtMSAwaDFtMSAwaDFtMSAwaDJtMiAwaDFtMyAwaDNtMSAwaDZNNCAzMy41aDFtMSAwaDNtMSAwaDFtMiAwaDFtMiAwaDFtNCAwaDhtMSAwaDNtMSAwaDFNNCAzNC41aDFtMSAwaDNtMSAwaDFtMiAwaDFtMSAwaDJtMSAwaDJtNCAwaDJtMSAwaDJtMSAwaDNNNCAzNS41aDFtNSAwaDFtNCAwaDNtMSAwaDVtMSAwaDFtMiAwaDNNNCAzNi41aDdtMSAwaDFtMiAwaDJtMSAwaDFtMiAwaDFtMSAwaDNtMSAwaDJtNyAwaDEiLz48L3N2Zz4K"
|
|
31488
|
+
}];
|
|
31489
|
+
//#endregion
|
|
31490
|
+
//#region src/client/cards/shell.tsx
|
|
31491
|
+
/**
|
|
31492
|
+
* 每张卡的图标。
|
|
31493
|
+
*
|
|
31494
|
+
* `remote` 与 `tunnel` 刻意不同款:前者是「在本机开个口子」(Upload),后者是「把机器搬到
|
|
31495
|
+
* 外网」(Cloud)。两者共用同一个图标会让用户以为它们是同一件事的两个开关 ——
|
|
31496
|
+
* 而那正是拆分前那张 123 行卡片的毛病。
|
|
31497
|
+
*/
|
|
31498
|
+
function tabIcon(tab) {
|
|
31499
|
+
if (tab === "roots") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.HardDrive, { size: 15 });
|
|
31500
|
+
if (tab === "devices") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Smartphone, { size: 15 });
|
|
31501
|
+
if (tab === "image") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Image, { size: 15 });
|
|
31502
|
+
if (tab === "trash") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Trash2, { size: 15 });
|
|
31503
|
+
if (tab === "plugins") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Package, { size: 15 });
|
|
31504
|
+
if (tab === "remote") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Upload, { size: 15 });
|
|
31505
|
+
if (tab === "tunnel") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Cloud, { size: 15 });
|
|
31506
|
+
if (tab === "app-download") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Download, { size: 15 });
|
|
31507
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.File, { size: 15 });
|
|
31508
|
+
}
|
|
31509
|
+
function SettingsCard(props) {
|
|
31510
|
+
const { t } = useWorkspaceI18n();
|
|
31511
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
31512
|
+
className: `daw-card${props.open ? " daw-card-open" : ""}`,
|
|
31513
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
31514
|
+
type: "button",
|
|
31515
|
+
className: "daw-card-header",
|
|
31516
|
+
"aria-expanded": props.open,
|
|
31517
|
+
onClick: props.onToggle,
|
|
31518
|
+
children: [
|
|
31519
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
31520
|
+
className: "daw-card-icon",
|
|
31521
|
+
"aria-hidden": "true",
|
|
31522
|
+
children: tabIcon(props.tab)
|
|
31523
|
+
}),
|
|
31524
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
31525
|
+
className: "daw-card-headtext",
|
|
31526
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
31527
|
+
className: "daw-card-name",
|
|
31528
|
+
children: t(props.nameKey)
|
|
31529
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
31530
|
+
className: "daw-card-desc",
|
|
31531
|
+
children: t(props.descKey)
|
|
31532
|
+
})]
|
|
31533
|
+
}),
|
|
31534
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
31535
|
+
className: `daw-card-chevron${props.open ? " daw-card-chevron-open" : ""}`,
|
|
31536
|
+
"aria-hidden": "true",
|
|
31537
|
+
children: "▾"
|
|
31538
|
+
})
|
|
31539
|
+
]
|
|
31540
|
+
}), props.open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31541
|
+
className: "daw-card-body",
|
|
31542
|
+
children: props.children
|
|
31543
|
+
})]
|
|
31544
|
+
});
|
|
31545
|
+
}
|
|
31546
|
+
/**
|
|
31547
|
+
* 「高级设置」组:把平时用不到的分区收进一个可折叠的容器。
|
|
31548
|
+
*
|
|
31549
|
+
* 关键点:**分组只影响显隐,不影响功能**。组里的每一张仍是完整的 `SettingsCard`,
|
|
31550
|
+
* 该拉的请求照拉、该写的接口照写 —— `image` 的懒加载挂在 `openCards` 上、
|
|
31551
|
+
* `plugins` 的懒加载挂在它自己的 `open` 上,两者都不经过这一层。
|
|
31552
|
+
*
|
|
31553
|
+
* 组本身**默认收起**:设置页一进来先看到主列表那几张常用的卡。
|
|
31554
|
+
*/
|
|
31555
|
+
function AdvancedGroup(props) {
|
|
31556
|
+
const { t } = useWorkspaceI18n();
|
|
31557
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
31558
|
+
className: `daw-card${props.open ? " daw-card-open" : ""} daw-advanced-group`,
|
|
31559
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
31560
|
+
type: "button",
|
|
31561
|
+
className: "daw-card-header",
|
|
31562
|
+
"aria-expanded": props.open,
|
|
31563
|
+
onClick: props.onToggle,
|
|
31564
|
+
children: [
|
|
31565
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
31566
|
+
className: "daw-card-icon",
|
|
31567
|
+
"aria-hidden": "true",
|
|
31568
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.SlidersHorizontal, { size: 15 })
|
|
31569
|
+
}),
|
|
31570
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
31571
|
+
className: "daw-card-headtext",
|
|
31572
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
31573
|
+
className: "daw-card-name",
|
|
31574
|
+
children: t("advancedGroupTitle")
|
|
31575
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
31576
|
+
className: "daw-card-desc",
|
|
31577
|
+
children: t("advancedGroupDesc")
|
|
31578
|
+
})]
|
|
31579
|
+
}),
|
|
31580
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
31581
|
+
className: `daw-card-chevron${props.open ? " daw-card-chevron-open" : ""}`,
|
|
31582
|
+
"aria-hidden": "true",
|
|
31583
|
+
children: "▾"
|
|
31584
|
+
})
|
|
31585
|
+
]
|
|
31586
|
+
}), props.open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31587
|
+
className: "daw-card-body daw-advanced-body",
|
|
31588
|
+
children: props.children
|
|
31589
|
+
})]
|
|
31590
|
+
});
|
|
31591
|
+
}
|
|
31592
|
+
/**
|
|
31593
|
+
* 卡片标题的 i18n 键。
|
|
31594
|
+
*
|
|
31595
|
+
* 大多数分区是 `${tab}Tab`,但 `app-download` 带连字符,裸模板会拼出 `app-downloadTab`
|
|
31596
|
+
* —— 没人该去记那个拼法的键名。显式映射,而不是把命名规则编码进模板。
|
|
31597
|
+
*/
|
|
31598
|
+
function tabLabelKey(tab) {
|
|
31599
|
+
return tab === "app-download" ? "appDownloadTab" : `${tab}Tab`;
|
|
31600
|
+
}
|
|
31601
|
+
/**
|
|
31602
|
+
* 卡片头部的描述行。与 `tabLabelKey` 同理:唯一含连字符的分区显式映射。
|
|
31603
|
+
*
|
|
31604
|
+
* 键名是 `${tab}TabDesc`(**带 Tab**),不是 `${tab}Desc`。这个中间段不是冗余:它让
|
|
31605
|
+
* 描述键与标题键 `rootsTab` / `rootsTabDesc` 成对,读的人一眼能看出这两个属于同一张卡。
|
|
31606
|
+
*
|
|
31607
|
+
* 踩过的坑:早先这里写成 `${tab}Desc`,`rootsDesc` 之类根本不在字典里 —— `translate` 直接
|
|
31608
|
+
* `dictionaries[locale][key].replace(...)` 抛 TypeError,整个设置面板白屏。`as MessageKey`
|
|
31609
|
+
* 把类型检查骗过去了,所以单测全绿也照样白屏。**键名靠模板拼出来时,类型系统是最后一道
|
|
31610
|
+
* 防线,这里偏偏被断言关掉了。** 现在改成显式查表,见下。
|
|
31611
|
+
*/
|
|
31612
|
+
const TAB_DESC_KEYS = {
|
|
31613
|
+
roots: "rootsTabDesc",
|
|
31614
|
+
remote: "remoteTabDesc",
|
|
31615
|
+
tunnel: "tunnelTabDesc",
|
|
31616
|
+
devices: "devicesTabDesc",
|
|
31617
|
+
image: "imageTabDesc",
|
|
31618
|
+
trash: "trashTabDesc",
|
|
31619
|
+
plugins: "pluginsTabDesc",
|
|
31620
|
+
"app-download": "appDownloadTabDesc"
|
|
31621
|
+
};
|
|
31622
|
+
function tabDescKey(tab) {
|
|
31623
|
+
return TAB_DESC_KEYS[tab];
|
|
31624
|
+
}
|
|
31625
|
+
//#endregion
|
|
31626
|
+
//#region src/client/cards/app-download.tsx
|
|
31627
|
+
/**
|
|
31628
|
+
* 「App 下载」卡:手机客户端的下载二维码与地址。
|
|
31629
|
+
*
|
|
31630
|
+
* 这张卡不碰任何服务端状态 —— 二维码是构建期生成的常量(`app-qr.ts`,由
|
|
31631
|
+
* `scripts/generate-app-qr.mjs` 产出)。它唯一的问题是「二维码指向的地址是否还有效」,
|
|
31632
|
+
* 那由生成脚本负责,不在这里。
|
|
31633
|
+
*/
|
|
31634
|
+
function AppDownloadCard(props) {
|
|
31635
|
+
const { t } = useWorkspaceI18n();
|
|
31636
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(SettingsCard, {
|
|
31637
|
+
tab: "app-download",
|
|
31638
|
+
nameKey: tabLabelKey("app-download"),
|
|
31639
|
+
descKey: tabDescKey("app-download"),
|
|
31640
|
+
open: props.open,
|
|
31641
|
+
onToggle: props.onToggle,
|
|
31642
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
31643
|
+
className: "daw-list-meta",
|
|
31644
|
+
children: t("appDownloadHint")
|
|
31645
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31646
|
+
className: "daw-qr-grid",
|
|
31647
|
+
children: APP_QR_CODES.map((code) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("figure", {
|
|
31648
|
+
className: "daw-qr-card",
|
|
31649
|
+
children: [
|
|
31650
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
31651
|
+
className: "daw-qr-image",
|
|
31652
|
+
src: code.dataUrl,
|
|
31653
|
+
alt: t("appDownloadQrAlt", { site: code.label }),
|
|
31654
|
+
width: 180,
|
|
31655
|
+
height: 180
|
|
31656
|
+
}),
|
|
31657
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("figcaption", {
|
|
31658
|
+
className: "daw-qr-site",
|
|
31659
|
+
children: code.label
|
|
31660
|
+
}),
|
|
31661
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
31662
|
+
className: "daw-qr-url",
|
|
31663
|
+
href: code.url,
|
|
31664
|
+
target: "_blank",
|
|
31665
|
+
rel: "noreferrer",
|
|
31666
|
+
children: code.url
|
|
31667
|
+
})
|
|
31668
|
+
]
|
|
31669
|
+
}, code.key))
|
|
31670
|
+
})]
|
|
31671
|
+
});
|
|
31672
|
+
}
|
|
31673
|
+
//#endregion
|
|
31674
|
+
//#region src/client/cards/context.ts
|
|
31675
|
+
/**
|
|
31676
|
+
* 卡片共享上下文:`AdminPanel` 拆成 `cards/*.tsx` 之后,各卡不再共用一个巨型函数作用域,
|
|
31677
|
+
* 而是从这里取「大家都要用」的东西。
|
|
31678
|
+
*
|
|
31679
|
+
* ## 为什么用 context 而不是逐层传 props
|
|
31680
|
+
*
|
|
31681
|
+
* 七张卡共享的是**同一份状态**,不是各自的副本:`error` 红条要跨卡显示、`refresh()` 一次
|
|
31682
|
+
* 刷新所有分区、`openCards` 决定展开态。逐层传 props 会让 `AdminPanel` 重新长成一张巨大的
|
|
31683
|
+
* 中转表(每个 prop 出现七八遍),跟没拆一样。context 把「谁给的」这件事收敛到一处。
|
|
31684
|
+
*
|
|
31685
|
+
* ## 放进来的判据
|
|
31686
|
+
*
|
|
31687
|
+
* **至少两张卡要用**,才进这里。只有一张卡用的状态留在那个文件里,不往上提 ——
|
|
31688
|
+
* 否则 context 会变成第二个巨型组件,只是换了个写法。
|
|
31689
|
+
*
|
|
31690
|
+
* ## 不放进来的东西
|
|
31691
|
+
*
|
|
31692
|
+
* 各卡自己的草稿态(根目录输入框里还没提交的路径、识图目录名草稿)刻意留在卡片内:
|
|
31693
|
+
* 它们不需要跨卡共享,提上来只会让「这张卡正在编辑什么」泄漏到全局。
|
|
31694
|
+
*/
|
|
31695
|
+
const CardsContext = (0, react.createContext)(void 0);
|
|
31696
|
+
const CardsProvider = CardsContext.Provider;
|
|
31697
|
+
function useCards() {
|
|
31698
|
+
const value = (0, react.useContext)(CardsContext);
|
|
31699
|
+
if (value === void 0) throw new Error("useCards 必须在 CardsProvider 内使用 —— 卡片没有拿到共享上下文");
|
|
31700
|
+
return value;
|
|
31701
|
+
}
|
|
31702
|
+
//#endregion
|
|
31703
|
+
//#region src/shared/contracts.ts
|
|
31704
|
+
const DEVICE_SCOPES = [
|
|
31705
|
+
"chat.read",
|
|
31706
|
+
"chat.write",
|
|
31707
|
+
"files.read",
|
|
31708
|
+
"files.write",
|
|
31709
|
+
"files.delete",
|
|
31710
|
+
"settings.read",
|
|
31711
|
+
"settings.write"
|
|
31712
|
+
];
|
|
31713
|
+
/**
|
|
31714
|
+
* 授予新设备时的默认权限(**五个非破坏性权限**)。
|
|
31715
|
+
*
|
|
31716
|
+
* 刻意不含 `files.delete` 与 `settings.write`:这两个是破坏性权限,应当是用户显式
|
|
31717
|
+
* 要求才授予。`settings.write` 尤其危险 —— 拿到它就能改供应商 `baseURL`,把模型
|
|
31718
|
+
* 请求连同真实凭据引到任意地址(SEC-H11),所以它绝不能是默认值。
|
|
31719
|
+
*
|
|
31720
|
+
* 三条路径必须共用这一份,否则「同一个产品里三处默认权限不一样」这种缺陷会反复
|
|
31721
|
+
* 复发:服务端生成配置文本时的缺省、Web UI 配对码面板的初始勾选、隧道一键路径。
|
|
31722
|
+
*/
|
|
31723
|
+
const DEFAULT_DEVICE_SCOPES = [
|
|
31724
|
+
"chat.read",
|
|
31725
|
+
"chat.write",
|
|
31726
|
+
"files.read",
|
|
31727
|
+
"files.write",
|
|
31728
|
+
"settings.read"
|
|
31729
|
+
];
|
|
31730
|
+
new Set(DEVICE_SCOPES);
|
|
31731
|
+
//#endregion
|
|
31732
|
+
//#region src/client/cards/devices.tsx
|
|
31733
|
+
/**
|
|
31734
|
+
* 「设备」卡:查看已配对的手机、改名、调权限,以及一键清理长期不动的设备。
|
|
31735
|
+
*
|
|
31736
|
+
* 从 `workspace.tsx` 的 `AdminPanel` 搬出来。搬的是**整张卡连同它的 `DeviceRow`**:
|
|
31737
|
+
* 设备行的编辑态(editing / scopes / rootIds / name)只服务这一张卡,留在卡外没有意义。
|
|
31738
|
+
*
|
|
31739
|
+
* ## 一个「保存」按钮同时提交改名与权限
|
|
31740
|
+
*
|
|
31741
|
+
* 理由(原样保留):界面上它们在同一块编辑区里,并排两个「保存」必然有人点错。分成两次
|
|
31742
|
+
* 请求(而不是一个 PATCH 带全部字段)是因为它们是两件不同的事:改名不需要权限校验通过,
|
|
31743
|
+
* 权限校验失败也不该把名字回滚。
|
|
31744
|
+
*
|
|
31745
|
+
* 顺序:**先改名,后权限**。名字是无害的装饰性字段;万一权限请求失败,留下一个已改的
|
|
31746
|
+
* 名字比「权限已变但界面报错」好。
|
|
31747
|
+
*/
|
|
31748
|
+
function DevicesCard(props) {
|
|
31749
|
+
const { api, t, status, managementLocked, refresh, notify, setError } = useCards();
|
|
31750
|
+
const [pruneDays, setPruneDays] = (0, react.useState)(30);
|
|
31751
|
+
const [pruneBusy, setPruneBusy] = (0, react.useState)(false);
|
|
31752
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(SettingsCard, {
|
|
31753
|
+
tab: "devices",
|
|
31754
|
+
nameKey: tabLabelKey("devices"),
|
|
31755
|
+
descKey: tabDescKey("devices"),
|
|
31756
|
+
open: props.open,
|
|
31757
|
+
onToggle: props.onToggle,
|
|
31758
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
31759
|
+
className: "daw-inline-command",
|
|
31760
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
31761
|
+
className: "daw-field",
|
|
31762
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("pruneInactiveDays") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
31763
|
+
className: "daw-input",
|
|
31764
|
+
type: "number",
|
|
31765
|
+
min: 1,
|
|
31766
|
+
max: 3650,
|
|
31767
|
+
value: pruneDays,
|
|
31768
|
+
disabled: managementLocked || pruneBusy,
|
|
31769
|
+
onChange: (event) => setPruneDays(Math.max(1, Math.min(3650, Number(event.target.value) || 1)))
|
|
31770
|
+
})]
|
|
31771
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
31772
|
+
className: "daw-command",
|
|
31773
|
+
disabled: managementLocked || pruneBusy,
|
|
31774
|
+
onClick: () => {
|
|
31775
|
+
if (!confirm(t("pruneInactiveConfirm", { days: String(pruneDays) }))) return;
|
|
31776
|
+
setPruneBusy(true);
|
|
31777
|
+
setError(void 0);
|
|
31778
|
+
api.pruneInactiveDevices(pruneDays).then((result) => {
|
|
31779
|
+
if (result.pruned === 0) notify("pruneInactiveNone");
|
|
31780
|
+
else notify("pruneInactiveDone", { count: String(result.pruned) });
|
|
31781
|
+
return refresh();
|
|
31782
|
+
}).catch(setError).finally(() => setPruneBusy(false));
|
|
31783
|
+
},
|
|
31784
|
+
children: pruneBusy ? t("pruneInactiveBusy") : t("pruneInactiveAction", { days: String(pruneDays) })
|
|
31785
|
+
})]
|
|
31786
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31787
|
+
className: "daw-list",
|
|
31788
|
+
children: (status?.devices ?? []).map((device) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DeviceRow, { device }, device.id))
|
|
31789
|
+
})]
|
|
31790
|
+
});
|
|
31791
|
+
}
|
|
31792
|
+
/**
|
|
31793
|
+
* 一台设备的行:名字 + 身份摘要 + 可展开的权限编辑区。
|
|
31794
|
+
*
|
|
31795
|
+
* 共享的东西(api / roots / refresh / notify / setError)从 `useCards()` 取,而不是
|
|
31796
|
+
* 一层层往下传 —— 这张行的调用点只有一处,多传六个 prop 只是把巨型 props 表挪个地方。
|
|
31797
|
+
*/
|
|
31798
|
+
function DeviceRow(props) {
|
|
31799
|
+
const { api, roots, refresh, notify, setError } = useCards();
|
|
31800
|
+
const { locale, t } = useWorkspaceI18n();
|
|
31801
|
+
const [editing, setEditing] = (0, react.useState)(false);
|
|
31802
|
+
const [scopes, setScopes] = (0, react.useState)(props.device.scopes);
|
|
31803
|
+
const [rootIds, setRootIds] = (0, react.useState)(props.device.rootIds);
|
|
31804
|
+
const [name, setName] = (0, react.useState)(props.device.name);
|
|
31805
|
+
const toggle = (list, value) => list.includes(value) ? list.filter((item) => item !== value) : [...list, value];
|
|
31806
|
+
const shortId = props.device.id.length > 14 ? `${props.device.id.slice(0, 8)}…${props.device.id.slice(-4)}` : props.device.id;
|
|
31807
|
+
const lastSeen = props.device.lastSeenAt === void 0 ? t("deviceNeverSeen") : t("deviceLastSeen", { time: new Date(props.device.lastSeenAt).toLocaleString(locale) });
|
|
31808
|
+
const autoNamed = props.device.nameSource === "default" || props.device.nameSource === "detected";
|
|
31809
|
+
const saveAll = () => {
|
|
31810
|
+
const trimmed = name.trim();
|
|
31811
|
+
const rename = trimmed !== "" && trimmed !== props.device.name;
|
|
31812
|
+
(rename ? api.renameDevice(props.device.id, trimmed) : Promise.resolve()).then(() => api.updateDevice(props.device.id, scopes, rootIds)).then(() => refresh()).then(() => {
|
|
31813
|
+
setEditing(false);
|
|
31814
|
+
notify(rename ? "deviceNameSaved" : "permissionsSaved");
|
|
31815
|
+
}).catch(setError);
|
|
31816
|
+
};
|
|
31817
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
31818
|
+
className: "daw-list-row",
|
|
31819
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
|
|
31820
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
31821
|
+
className: "daw-list-title",
|
|
31822
|
+
children: [
|
|
31823
|
+
props.device.name,
|
|
31824
|
+
props.device.revokedAt !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
31825
|
+
className: "daw-pill",
|
|
31826
|
+
children: t("revoked")
|
|
31776
31827
|
}),
|
|
31777
|
-
|
|
31828
|
+
autoNamed && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
31829
|
+
className: "daw-pill",
|
|
31830
|
+
title: t("deviceRenameHint"),
|
|
31831
|
+
children: t("deviceDetectedName")
|
|
31832
|
+
})
|
|
31778
31833
|
]
|
|
31779
|
-
}
|
|
31780
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(
|
|
31781
|
-
|
|
31782
|
-
nameKey: tabLabelKey("devices"),
|
|
31783
|
-
descKey: tabDescKey("devices"),
|
|
31784
|
-
open: isOpen("devices"),
|
|
31785
|
-
onToggle: () => toggleCard("devices"),
|
|
31786
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
31787
|
-
className: "daw-inline-command",
|
|
31788
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
31789
|
-
className: "daw-field",
|
|
31790
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("pruneInactiveDays") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
31791
|
-
className: "daw-input",
|
|
31792
|
-
type: "number",
|
|
31793
|
-
min: 1,
|
|
31794
|
-
max: 3650,
|
|
31795
|
-
value: pruneDays,
|
|
31796
|
-
disabled: managementLocked || pruneBusy,
|
|
31797
|
-
onChange: (event) => setPruneDays(Math.max(1, Math.min(3650, Number(event.target.value) || 1)))
|
|
31798
|
-
})]
|
|
31799
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
31800
|
-
className: "daw-command",
|
|
31801
|
-
disabled: managementLocked || pruneBusy,
|
|
31802
|
-
onClick: () => {
|
|
31803
|
-
if (!confirm(t("pruneInactiveConfirm", { days: String(pruneDays) }))) return;
|
|
31804
|
-
setPruneBusy(true);
|
|
31805
|
-
setError(void 0);
|
|
31806
|
-
props.api.pruneInactiveDevices(pruneDays).then((result) => {
|
|
31807
|
-
if (result.pruned === 0) notify("pruneInactiveNone");
|
|
31808
|
-
else notify("pruneInactiveDone", { count: String(result.pruned) });
|
|
31809
|
-
return refresh();
|
|
31810
|
-
}).catch(setError).finally(() => setPruneBusy(false));
|
|
31811
|
-
},
|
|
31812
|
-
children: pruneBusy ? t("pruneInactiveBusy") : t("pruneInactiveAction", { days: String(pruneDays) })
|
|
31813
|
-
})]
|
|
31814
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31815
|
-
className: "daw-list",
|
|
31816
|
-
children: (status?.devices ?? []).map((device) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DeviceRow, {
|
|
31817
|
-
api: props.api,
|
|
31818
|
-
device,
|
|
31819
|
-
roots,
|
|
31820
|
-
onChanged: refresh,
|
|
31821
|
-
onError: setError,
|
|
31822
|
-
onNotice: notify
|
|
31823
|
-
}, device.id))
|
|
31824
|
-
})]
|
|
31825
|
-
}, "devices"),
|
|
31826
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(SettingsCard, {
|
|
31827
|
-
tab: "image",
|
|
31828
|
-
nameKey: tabLabelKey("image"),
|
|
31829
|
-
descKey: tabDescKey("image"),
|
|
31830
|
-
open: isOpen("image"),
|
|
31831
|
-
onToggle: () => toggleCard("image"),
|
|
31834
|
+
}),
|
|
31835
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
31836
|
+
className: "daw-list-meta",
|
|
31832
31837
|
children: [
|
|
31833
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
31834
|
-
|
|
31835
|
-
children:
|
|
31836
|
-
}),
|
|
31837
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
31838
|
-
className: "daw-field",
|
|
31839
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("imageSubdir") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
31840
|
-
className: "daw-input",
|
|
31841
|
-
value: imageDraft,
|
|
31842
|
-
spellCheck: false,
|
|
31843
|
-
disabled: imageBusy || image === void 0,
|
|
31844
|
-
onChange: (event) => {
|
|
31845
|
-
setImageDraft(event.target.value);
|
|
31846
|
-
setImageSaved(false);
|
|
31847
|
-
}
|
|
31848
|
-
})]
|
|
31849
|
-
}),
|
|
31850
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31851
|
-
className: "daw-list-meta",
|
|
31852
|
-
style: { marginTop: 5 },
|
|
31853
|
-
children: t("imageSubdirHint")
|
|
31854
|
-
}),
|
|
31855
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
31856
|
-
className: "daw-field",
|
|
31857
|
-
style: { marginTop: 14 },
|
|
31858
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("imageDefault") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
31859
|
-
className: "daw-input",
|
|
31860
|
-
value: image?.defaultSubdir ?? "",
|
|
31861
|
-
readOnly: true,
|
|
31862
|
-
disabled: true
|
|
31863
|
-
})]
|
|
31864
|
-
}),
|
|
31865
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31866
|
-
className: "daw-list-meta",
|
|
31867
|
-
style: { marginTop: 5 },
|
|
31868
|
-
children: t("imageDefaultHint")
|
|
31869
|
-
}),
|
|
31870
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
31871
|
-
className: "daw-field",
|
|
31872
|
-
style: { marginTop: 14 },
|
|
31873
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("imageFallback") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
31874
|
-
className: "daw-input",
|
|
31875
|
-
value: image?.fallbackDir ?? "",
|
|
31876
|
-
readOnly: true,
|
|
31877
|
-
disabled: true
|
|
31878
|
-
})]
|
|
31879
|
-
}),
|
|
31880
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31881
|
-
className: "daw-list-meta",
|
|
31882
|
-
style: { marginTop: 5 },
|
|
31883
|
-
children: t("imageFallbackHint")
|
|
31884
|
-
}),
|
|
31885
|
-
image !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31886
|
-
className: "daw-list-meta",
|
|
31887
|
-
style: { marginTop: 12 },
|
|
31888
|
-
children: t("imageExample", { subdir: image.subdir })
|
|
31838
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
31839
|
+
title: props.device.id,
|
|
31840
|
+
children: shortId
|
|
31889
31841
|
}),
|
|
31890
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(
|
|
31891
|
-
|
|
31892
|
-
|
|
31893
|
-
|
|
31894
|
-
|
|
31895
|
-
|
|
31896
|
-
|
|
31897
|
-
|
|
31898
|
-
|
|
31899
|
-
disabled: imageBusy || image === void 0 || imageDraft.trim() === "" || imageDraft.trim() === image.subdir,
|
|
31900
|
-
onClick: () => {
|
|
31901
|
-
setImageBusy(true);
|
|
31902
|
-
setError(void 0);
|
|
31903
|
-
props.api.saveImageSubdir(imageDraft.trim()).then((next) => {
|
|
31904
|
-
setImage(next);
|
|
31905
|
-
setImageDraft(next.subdir);
|
|
31906
|
-
setImageSaved(true);
|
|
31907
|
-
}).catch(setError).finally(() => setImageBusy(false));
|
|
31908
|
-
},
|
|
31909
|
-
children: t("save")
|
|
31910
|
-
}), imageSaved && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
31911
|
-
className: "daw-pill on",
|
|
31912
|
-
children: t("imageSaved")
|
|
31913
|
-
})]
|
|
31914
|
-
})
|
|
31842
|
+
props.device.platform !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
31843
|
+
" · ",
|
|
31844
|
+
props.device.platform,
|
|
31845
|
+
props.device.model === void 0 ? "" : ` · ${props.device.model}`
|
|
31846
|
+
] }),
|
|
31847
|
+
" · ",
|
|
31848
|
+
lastSeen,
|
|
31849
|
+
" · ",
|
|
31850
|
+
props.device.scopes.length === 0 ? t("noScopes") : props.device.scopes.map((scope) => scopeLabel(locale, scope)).join(" · ")
|
|
31915
31851
|
]
|
|
31916
|
-
}, "image"),
|
|
31917
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(SettingsCard, {
|
|
31918
|
-
tab: "trash",
|
|
31919
|
-
nameKey: tabLabelKey("trash"),
|
|
31920
|
-
descKey: tabDescKey("trash"),
|
|
31921
|
-
open: isOpen("trash"),
|
|
31922
|
-
onToggle: () => toggleCard("trash"),
|
|
31923
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
31924
|
-
className: "daw-list-meta",
|
|
31925
|
-
children: t("pluginTrashHint")
|
|
31926
|
-
}), trash.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31927
|
-
className: "daw-list-meta daw-empty",
|
|
31928
|
-
children: t("trashEmpty")
|
|
31929
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31930
|
-
className: "daw-list",
|
|
31931
|
-
children: trash.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
31932
|
-
className: "daw-list-row",
|
|
31933
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31934
|
-
className: "daw-list-title",
|
|
31935
|
-
children: item.path
|
|
31936
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
31937
|
-
className: "daw-list-meta",
|
|
31938
|
-
children: [
|
|
31939
|
-
item.kind,
|
|
31940
|
-
" · ",
|
|
31941
|
-
formatBytes(item.size),
|
|
31942
|
-
" · ",
|
|
31943
|
-
new Date(item.createdAt).toLocaleString(locale)
|
|
31944
|
-
]
|
|
31945
|
-
})] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
31946
|
-
className: "daw-command",
|
|
31947
|
-
onClick: () => {
|
|
31948
|
-
setError(void 0);
|
|
31949
|
-
props.api.restoreTrash(item.id).then(() => {
|
|
31950
|
-
notify("trashRestored");
|
|
31951
|
-
return refresh();
|
|
31952
|
-
}).catch(setError);
|
|
31953
|
-
},
|
|
31954
|
-
children: t("restore")
|
|
31955
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
31956
|
-
className: "daw-icon danger",
|
|
31957
|
-
title: t("deletePermanently"),
|
|
31958
|
-
"aria-label": t("deletePermanently"),
|
|
31959
|
-
onClick: () => {
|
|
31960
|
-
if (confirm(t("purgeConfirm"))) props.api.purgeTrash(item.id).then(() => {
|
|
31961
|
-
notify("trashPurged");
|
|
31962
|
-
return refresh();
|
|
31963
|
-
}).catch(setError);
|
|
31964
|
-
},
|
|
31965
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Trash2, { size: 15 })
|
|
31966
|
-
})] })]
|
|
31967
|
-
}, item.id))
|
|
31968
|
-
})]
|
|
31969
|
-
}, "trash"),
|
|
31970
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PluginsSection, {
|
|
31971
|
-
api: props.api,
|
|
31972
|
-
open: isOpen("plugins"),
|
|
31973
|
-
onToggle: () => toggleCard("plugins"),
|
|
31974
|
-
onOpenPluginList: props.onOpenPluginList
|
|
31975
31852
|
}),
|
|
31976
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
31977
|
-
|
|
31978
|
-
|
|
31853
|
+
editing && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
31854
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
31855
|
+
className: "daw-field",
|
|
31856
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("deviceRename") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
31857
|
+
className: "daw-input",
|
|
31858
|
+
value: name,
|
|
31859
|
+
placeholder: t("deviceRenamePlaceholder"),
|
|
31860
|
+
title: t("deviceRenameHint"),
|
|
31861
|
+
onChange: (event) => setName(event.target.value),
|
|
31862
|
+
onKeyDown: (event) => {
|
|
31863
|
+
if (event.key === "Enter") saveAll();
|
|
31864
|
+
}
|
|
31865
|
+
})]
|
|
31866
|
+
}),
|
|
31867
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31868
|
+
className: "daw-scope-grid",
|
|
31869
|
+
children: DEVICE_SCOPES.map((scope) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
31870
|
+
className: "daw-check",
|
|
31871
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
31872
|
+
type: "checkbox",
|
|
31873
|
+
checked: scopes.includes(scope),
|
|
31874
|
+
onChange: () => setScopes((current) => toggle(current, scope))
|
|
31875
|
+
}), scopeLabel(locale, scope)]
|
|
31876
|
+
}, scope))
|
|
31877
|
+
}),
|
|
31878
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31879
|
+
className: "daw-scope-grid",
|
|
31880
|
+
children: roots.map((root) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
31881
|
+
className: "daw-check",
|
|
31882
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
31883
|
+
type: "checkbox",
|
|
31884
|
+
checked: rootIds.includes(root.id),
|
|
31885
|
+
onChange: () => setRootIds((current) => toggle(current, root.id))
|
|
31886
|
+
}), root.label]
|
|
31887
|
+
}, root.id))
|
|
31888
|
+
})
|
|
31889
|
+
] })
|
|
31890
|
+
] }), props.device.revokedAt === void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
|
|
31891
|
+
editing && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
31892
|
+
className: "daw-command primary",
|
|
31893
|
+
onClick: saveAll,
|
|
31894
|
+
children: t("save")
|
|
31895
|
+
}),
|
|
31896
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
31897
|
+
className: "daw-command",
|
|
31898
|
+
onClick: () => {
|
|
31899
|
+
setName(props.device.name);
|
|
31900
|
+
setEditing((value) => !value);
|
|
31901
|
+
},
|
|
31902
|
+
children: editing ? t("cancel") : t("permissions")
|
|
31903
|
+
}),
|
|
31904
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
31905
|
+
className: "daw-icon danger",
|
|
31906
|
+
title: t("revokeDevice"),
|
|
31907
|
+
"aria-label": t("revokeDevice"),
|
|
31908
|
+
onClick: () => {
|
|
31909
|
+
if (!window.confirm(t("revokeConfirm", { name: props.device.name }))) return;
|
|
31910
|
+
api.revokeDevice(props.device.id).then(() => refresh()).then(() => notify("deviceRevoked")).catch(setError);
|
|
31911
|
+
},
|
|
31912
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Trash2, { size: 15 })
|
|
31979
31913
|
})
|
|
31980
|
-
]
|
|
31914
|
+
] })]
|
|
31981
31915
|
});
|
|
31982
31916
|
}
|
|
31917
|
+
//#endregion
|
|
31918
|
+
//#region src/client/cards/image.tsx
|
|
31983
31919
|
/**
|
|
31984
|
-
*
|
|
31920
|
+
* 「识图」卡:设置图片交给识图工具的落盘目录名。
|
|
31921
|
+
*
|
|
31922
|
+
* 上半截可改(当前值,存数据库、改完立即生效,不必重启);下半截「当前默认」**只读**
|
|
31923
|
+
* —— 那是出厂字面量 `.dsh-images`,历史交接消息和下游工具都按它写过路径,它不是一个
|
|
31924
|
+
* 跟着用户改的偏好项。兜底目录同样只读:它是解析不出会话工作目录时的落点。
|
|
31985
31925
|
*
|
|
31986
|
-
* ##
|
|
31926
|
+
* ## 数据是懒加载的,判据是 `open` 而不是「组开没开」
|
|
31987
31927
|
*
|
|
31988
|
-
*
|
|
31989
|
-
*
|
|
31990
|
-
*
|
|
31928
|
+
* 原实现把 `imageSettings()` 放在 `AdminPanel.refresh()` 里,条件是 `openCards.has('image')`。
|
|
31929
|
+
* 拆出来之后这条判据变成这张卡自己的 `open` prop —— **语义完全等价**:`open` 就是
|
|
31930
|
+
* `isOpen('image')` 的返回值,同一个 Set、同一个值。请求仍然只在真正展开这一节时才发,
|
|
31931
|
+
* 不会因为设置页一进来就打一个没人看的往返。
|
|
31932
|
+
*
|
|
31933
|
+
* 反过来,如果把这层懒加载丢掉、改成挂载即拉,那就是行为变更:整整多一个请求。
|
|
31934
|
+
*/
|
|
31935
|
+
function ImageCard(props) {
|
|
31936
|
+
const { api, t, setError } = useCards();
|
|
31937
|
+
const [image, setImage] = (0, react.useState)();
|
|
31938
|
+
const [draft, setDraft] = (0, react.useState)("");
|
|
31939
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
31940
|
+
const [saved, setSaved] = (0, react.useState)(false);
|
|
31941
|
+
(0, react.useEffect)(() => {
|
|
31942
|
+
if (!props.open) return;
|
|
31943
|
+
let cancelled = false;
|
|
31944
|
+
api.imageSettings().then((settings) => {
|
|
31945
|
+
if (cancelled) return;
|
|
31946
|
+
setImage(settings);
|
|
31947
|
+
setDraft(settings.subdir);
|
|
31948
|
+
setSaved(false);
|
|
31949
|
+
}).catch((cause) => {
|
|
31950
|
+
if (!cancelled) setError(cause);
|
|
31951
|
+
});
|
|
31952
|
+
return () => {
|
|
31953
|
+
cancelled = true;
|
|
31954
|
+
};
|
|
31955
|
+
}, [
|
|
31956
|
+
props.open,
|
|
31957
|
+
api,
|
|
31958
|
+
setError
|
|
31959
|
+
]);
|
|
31960
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(SettingsCard, {
|
|
31961
|
+
tab: "image",
|
|
31962
|
+
nameKey: tabLabelKey("image"),
|
|
31963
|
+
descKey: tabDescKey("image"),
|
|
31964
|
+
open: props.open,
|
|
31965
|
+
onToggle: props.onToggle,
|
|
31966
|
+
children: [
|
|
31967
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
31968
|
+
className: "daw-list-meta",
|
|
31969
|
+
children: t("imageHint")
|
|
31970
|
+
}),
|
|
31971
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
31972
|
+
className: "daw-field",
|
|
31973
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("imageSubdir") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
31974
|
+
className: "daw-input",
|
|
31975
|
+
value: draft,
|
|
31976
|
+
spellCheck: false,
|
|
31977
|
+
disabled: busy || image === void 0,
|
|
31978
|
+
onChange: (event) => {
|
|
31979
|
+
setDraft(event.target.value);
|
|
31980
|
+
setSaved(false);
|
|
31981
|
+
}
|
|
31982
|
+
})]
|
|
31983
|
+
}),
|
|
31984
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31985
|
+
className: "daw-list-meta",
|
|
31986
|
+
style: { marginTop: 5 },
|
|
31987
|
+
children: t("imageSubdirHint")
|
|
31988
|
+
}),
|
|
31989
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
31990
|
+
className: "daw-field",
|
|
31991
|
+
style: { marginTop: 14 },
|
|
31992
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("imageDefault") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
31993
|
+
className: "daw-input",
|
|
31994
|
+
value: image?.defaultSubdir ?? "",
|
|
31995
|
+
readOnly: true,
|
|
31996
|
+
disabled: true
|
|
31997
|
+
})]
|
|
31998
|
+
}),
|
|
31999
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
32000
|
+
className: "daw-list-meta",
|
|
32001
|
+
style: { marginTop: 5 },
|
|
32002
|
+
children: t("imageDefaultHint")
|
|
32003
|
+
}),
|
|
32004
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
32005
|
+
className: "daw-field",
|
|
32006
|
+
style: { marginTop: 14 },
|
|
32007
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("imageFallback") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
32008
|
+
className: "daw-input",
|
|
32009
|
+
value: image?.fallbackDir ?? "",
|
|
32010
|
+
readOnly: true,
|
|
32011
|
+
disabled: true
|
|
32012
|
+
})]
|
|
32013
|
+
}),
|
|
32014
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
32015
|
+
className: "daw-list-meta",
|
|
32016
|
+
style: { marginTop: 5 },
|
|
32017
|
+
children: t("imageFallbackHint")
|
|
32018
|
+
}),
|
|
32019
|
+
image !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
32020
|
+
className: "daw-list-meta",
|
|
32021
|
+
style: { marginTop: 12 },
|
|
32022
|
+
children: t("imageExample", { subdir: image.subdir })
|
|
32023
|
+
}),
|
|
32024
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
32025
|
+
className: "daw-form-row",
|
|
32026
|
+
style: {
|
|
32027
|
+
marginTop: 12,
|
|
32028
|
+
gridTemplateColumns: "auto auto minmax(0,1fr)",
|
|
32029
|
+
alignItems: "center"
|
|
32030
|
+
},
|
|
32031
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
32032
|
+
className: "daw-command primary",
|
|
32033
|
+
disabled: busy || image === void 0 || draft.trim() === "" || draft.trim() === image.subdir,
|
|
32034
|
+
onClick: () => {
|
|
32035
|
+
setBusy(true);
|
|
32036
|
+
setError(void 0);
|
|
32037
|
+
api.saveImageSubdir(draft.trim()).then((next) => {
|
|
32038
|
+
setImage(next);
|
|
32039
|
+
setDraft(next.subdir);
|
|
32040
|
+
setSaved(true);
|
|
32041
|
+
}).catch(setError).finally(() => setBusy(false));
|
|
32042
|
+
},
|
|
32043
|
+
children: t("save")
|
|
32044
|
+
}), saved && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
32045
|
+
className: "daw-pill on",
|
|
32046
|
+
children: t("imageSaved")
|
|
32047
|
+
})]
|
|
32048
|
+
})
|
|
32049
|
+
]
|
|
32050
|
+
});
|
|
32051
|
+
}
|
|
32052
|
+
//#endregion
|
|
32053
|
+
//#region src/client/cards/plugins.tsx
|
|
32054
|
+
/**
|
|
32055
|
+
* 「插件」卡:这台机器 profile 里装了哪些插件、能不能停用、有没有更新。
|
|
31991
32056
|
*
|
|
31992
|
-
* ##
|
|
32057
|
+
* ## 为什么它收在「高级设置」组里
|
|
31993
32058
|
*
|
|
31994
|
-
*
|
|
31995
|
-
*
|
|
31996
|
-
*
|
|
31997
|
-
* node_modules 下,早期实现把它误判成「声明加载但缺失」,真机上一次冒出 5 条假警报。
|
|
31998
|
-
* 3. **改完要重启**。bundles 只在启动时读一次,所以成功提示不是「已停用」而是
|
|
31999
|
-
* 「已写进 package.json,重启后生效」—— 否则用户会以为点了没反应,再点几次。
|
|
32059
|
+
* DSH 内核自己有插件页(侧栏「插件」),这张卡与它是**同一条车道**:看清单、启停、检查更新。
|
|
32060
|
+
* 桥不跑内核、也不该重做内核的 UI,所以把它从主列表收进高级组 —— 功能照留(手机端与
|
|
32061
|
+
* 桌面端都能从这里改),但不再占日常视线。
|
|
32000
32062
|
*
|
|
32001
|
-
* ##
|
|
32063
|
+
* ## 为什么还要留这一张
|
|
32002
32064
|
*
|
|
32003
|
-
*
|
|
32004
|
-
*
|
|
32065
|
+
* 手机 App 那边没有内核的插件页可点,`pluginInventory` 是它唯一能看到「这台机器装了什么」的
|
|
32066
|
+
* 通道。删掉这张卡等于把这个能力从移动端拿走,所以是隐藏而不是删除。
|
|
32005
32067
|
*/
|
|
32006
|
-
function
|
|
32068
|
+
function PluginsCard(props) {
|
|
32007
32069
|
const { t } = useWorkspaceI18n();
|
|
32070
|
+
const { api } = useCards();
|
|
32008
32071
|
const [inventory, setInventory] = (0, react.useState)(void 0);
|
|
32009
32072
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
32010
32073
|
const [checking, setChecking] = (0, react.useState)(false);
|
|
@@ -32017,14 +32080,14 @@ window.__ModuleLoader__.load({
|
|
|
32017
32080
|
else setBusy(true);
|
|
32018
32081
|
setError(void 0);
|
|
32019
32082
|
try {
|
|
32020
|
-
setInventory(await
|
|
32083
|
+
setInventory(await api.pluginInventory(checkUpdates));
|
|
32021
32084
|
} catch (cause) {
|
|
32022
32085
|
setError(messageOf(cause, t));
|
|
32023
32086
|
} finally {
|
|
32024
32087
|
if (checkUpdates) setChecking(false);
|
|
32025
32088
|
else setBusy(false);
|
|
32026
32089
|
}
|
|
32027
|
-
}, [
|
|
32090
|
+
}, [api, t]);
|
|
32028
32091
|
(0, react.useEffect)(() => {
|
|
32029
32092
|
if (!props.open || inventory !== void 0) return;
|
|
32030
32093
|
load(false);
|
|
@@ -32037,15 +32100,15 @@ window.__ModuleLoader__.load({
|
|
|
32037
32100
|
setToggling(entry.name);
|
|
32038
32101
|
setError(void 0);
|
|
32039
32102
|
try {
|
|
32040
|
-
await
|
|
32041
|
-
setInventory(await
|
|
32103
|
+
await api.togglePlugin(entry.name, !entry.loaded);
|
|
32104
|
+
setInventory(await api.pluginInventory(false));
|
|
32042
32105
|
setRestartRequired(true);
|
|
32043
32106
|
} catch (cause) {
|
|
32044
32107
|
setError(messageOf(cause, t));
|
|
32045
32108
|
} finally {
|
|
32046
32109
|
setToggling(void 0);
|
|
32047
32110
|
}
|
|
32048
|
-
}, [
|
|
32111
|
+
}, [api, t]);
|
|
32049
32112
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(SettingsCard, {
|
|
32050
32113
|
tab: "plugins",
|
|
32051
32114
|
nameKey: tabLabelKey("plugins"),
|
|
@@ -32157,667 +32220,981 @@ window.__ModuleLoader__.load({
|
|
|
32157
32220
|
]
|
|
32158
32221
|
});
|
|
32159
32222
|
}
|
|
32160
|
-
|
|
32161
|
-
|
|
32162
|
-
if (state === "loaded") return "pluginsStateLoaded";
|
|
32163
|
-
if (state === "runtime-provided") return "pluginsStateRuntimeProvided";
|
|
32164
|
-
if (state === "installed-not-loaded") return "pluginsStateInstalledNotLoaded";
|
|
32165
|
-
return "pluginsStateDeclaredMissing";
|
|
32166
|
-
}
|
|
32167
|
-
/**
|
|
32168
|
-
* The App download page: one QR card per release host.
|
|
32169
|
-
*
|
|
32170
|
-
* The codes come from `APP_QR_CODES` as inlined SVG data URLs, so this page makes **no network
|
|
32171
|
-
* request** and adds no runtime dependency — the bytes are in the bundle. Both point at a Releases
|
|
32172
|
-
* *listing* page rather than a fixed file, so publishing a new APK needs no change here.
|
|
32173
|
-
*
|
|
32174
|
-
* The address is rendered as a real link next to the image on purpose: a phone cannot scan its own
|
|
32175
|
-
* screen, so this page is mostly used by the desktop admin to send a code to someone else. Without
|
|
32176
|
-
* the text there would be no way to type the address in by hand when a camera is unavailable.
|
|
32177
|
-
*/
|
|
32178
|
-
function AppDownloadSection(props) {
|
|
32179
|
-
const { t } = useWorkspaceI18n();
|
|
32180
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(SettingsCard, {
|
|
32181
|
-
tab: "app-download",
|
|
32182
|
-
nameKey: tabLabelKey("app-download"),
|
|
32183
|
-
descKey: tabDescKey("app-download"),
|
|
32184
|
-
open: props.open,
|
|
32185
|
-
onToggle: props.onToggle,
|
|
32186
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
32187
|
-
className: "daw-list-meta",
|
|
32188
|
-
children: t("appDownloadHint")
|
|
32189
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
32190
|
-
className: "daw-qr-grid",
|
|
32191
|
-
children: APP_QR_CODES.map((code) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("figure", {
|
|
32192
|
-
className: "daw-qr-card",
|
|
32193
|
-
children: [
|
|
32194
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
32195
|
-
className: "daw-qr-image",
|
|
32196
|
-
src: code.dataUrl,
|
|
32197
|
-
alt: t("appDownloadQrAlt", { site: code.label }),
|
|
32198
|
-
width: 180,
|
|
32199
|
-
height: 180
|
|
32200
|
-
}),
|
|
32201
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("figcaption", {
|
|
32202
|
-
className: "daw-qr-site",
|
|
32203
|
-
children: code.label
|
|
32204
|
-
}),
|
|
32205
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
32206
|
-
className: "daw-qr-url",
|
|
32207
|
-
href: code.url,
|
|
32208
|
-
target: "_blank",
|
|
32209
|
-
rel: "noreferrer",
|
|
32210
|
-
children: code.url
|
|
32211
|
-
})
|
|
32212
|
-
]
|
|
32213
|
-
}, code.key))
|
|
32214
|
-
})]
|
|
32215
|
-
});
|
|
32216
|
-
}
|
|
32223
|
+
//#endregion
|
|
32224
|
+
//#region src/client/cards/remote.tsx
|
|
32217
32225
|
/**
|
|
32218
|
-
*
|
|
32226
|
+
* 「远程访问」卡:在本机开一个口子 —— 绑定 IP/端口、开关远程访问、生成配对码与令牌配置。
|
|
32219
32227
|
*
|
|
32220
|
-
*
|
|
32221
|
-
*
|
|
32222
|
-
*
|
|
32223
|
-
*
|
|
32228
|
+
* ## 这张卡为什么还是最长的一张
|
|
32229
|
+
*
|
|
32230
|
+
* 它把三件相邻但不同的事收在一处:**监听器**(绑哪个地址)、**配对码**(把手机接进来)、
|
|
32231
|
+
* **权限勾选**(接进来之后能干什么)。三者是同一条流程的三步(勾权限 → 生成访问码 →
|
|
32232
|
+
* App 输入码 → 权限随设备落库),拆开会让人在两张卡之间来回跳,所以留在一起。
|
|
32233
|
+
*
|
|
32234
|
+
* 「外网隧道」已经从这张卡里独立出去了(见 `tunnel.tsx`)—— 那不是同一条流程,
|
|
32235
|
+
* 是另一条通道。当年挤在一张卡里,用户会以为隧道是远程访问的一个开关。
|
|
32236
|
+
*
|
|
32237
|
+
* ## 哪些东西不在这里
|
|
32238
|
+
*
|
|
32239
|
+
* `refresh()` 与隧道状态的拉取**不搬**:隧道状态必须**先于、独立于** `/manage/status`
|
|
32240
|
+
* 拉取(隧道运行期 status 会 403 中断整个 try),那属于 `AdminPanel`。
|
|
32241
|
+
* 本卡只消费它的结果(`tunnel` / `managementLocked` 等经 `context.ts` 下发)。
|
|
32242
|
+
*
|
|
32243
|
+
* ## 权限勾选的默认值为什么是「五个非破坏性权限」
|
|
32244
|
+
*
|
|
32245
|
+
* 见下方 `grantedScopes` 的初始化处 —— 这是 SEC-H11 的修复点,默认值偏松不是小事。
|
|
32224
32246
|
*/
|
|
32225
|
-
function
|
|
32226
|
-
const { t } = useWorkspaceI18n();
|
|
32227
|
-
const
|
|
32228
|
-
const
|
|
32229
|
-
const
|
|
32230
|
-
|
|
32231
|
-
|
|
32232
|
-
|
|
32233
|
-
|
|
32234
|
-
|
|
32235
|
-
|
|
32236
|
-
|
|
32237
|
-
if (
|
|
32238
|
-
|
|
32239
|
-
|
|
32247
|
+
function RemoteCard(props) {
|
|
32248
|
+
const { t, locale } = useWorkspaceI18n();
|
|
32249
|
+
const { api, status, roots, managementLocked, refresh, notify, copyText, setError, setStatus, tunnel, tunnelBusy, configText, configTextBusy, setConfigText, setConfigTextBusy, configEndpoint, setConfigEndpoint, tunnelConfigClick } = useCards();
|
|
32250
|
+
const [listenerHost, setListenerHost] = (0, react.useState)("0.0.0.0");
|
|
32251
|
+
const [listenerPort, setListenerPort] = (0, react.useState)("3090");
|
|
32252
|
+
const [pairing, setPairing] = (0, react.useState)();
|
|
32253
|
+
const [now, setNow] = (0, react.useState)(() => Date.now());
|
|
32254
|
+
(0, react.useEffect)(() => {
|
|
32255
|
+
const timer = window.setInterval(() => setNow(Date.now()), 3e4);
|
|
32256
|
+
return () => window.clearInterval(timer);
|
|
32257
|
+
}, []);
|
|
32258
|
+
(0, react.useEffect)(() => {
|
|
32259
|
+
if (status === void 0) return;
|
|
32260
|
+
setListenerHost(status.configuredHost);
|
|
32261
|
+
setListenerPort(String(status.configuredPort));
|
|
32262
|
+
}, [status?.configuredHost, status?.configuredPort]);
|
|
32263
|
+
const [grantedScopes, setGrantedScopes] = (0, react.useState)([...DEFAULT_DEVICE_SCOPES]);
|
|
32264
|
+
const toggleScope = (0, react.useCallback)((scope) => {
|
|
32265
|
+
setGrantedScopes((current) => current.includes(scope) ? current.filter((item) => item !== scope) : [...current, scope]);
|
|
32266
|
+
}, []);
|
|
32267
|
+
const grantNotice = grantedScopes.length === 0 ? t("scopeGrantNoticeEmpty") : (status?.devices?.length ?? 0) === 0 ? t("scopeGrantNotice") : t("scopeGrantNoticeExisting");
|
|
32268
|
+
const parsedListenerPort = Number(listenerPort);
|
|
32269
|
+
const listenerPortValid = Number.isInteger(parsedListenerPort) && parsedListenerPort >= 1 && parsedListenerPort <= 65535;
|
|
32270
|
+
const listenerDirty = status !== void 0 && (listenerHost.trim() !== status.configuredHost || parsedListenerPort !== status.configuredPort);
|
|
32271
|
+
const poll = async (id) => {
|
|
32272
|
+
for (let attempt = 0; attempt < 30; attempt += 1) {
|
|
32273
|
+
await delay(300);
|
|
32274
|
+
const next = await api.operation(id);
|
|
32275
|
+
setStatus(next);
|
|
32276
|
+
const operation = next.operation;
|
|
32277
|
+
if (operation?.state === "succeeded") {
|
|
32278
|
+
if (operation.pairingCode !== void 0) setPairing({
|
|
32279
|
+
code: operation.pairingCode,
|
|
32280
|
+
...operation.pairingExpiresAt === void 0 ? {} : { expiresAt: operation.pairingExpiresAt }
|
|
32281
|
+
});
|
|
32282
|
+
return;
|
|
32283
|
+
}
|
|
32284
|
+
if (operation?.state === "failed") throw new Error(listenerOperationError(operation.code, operation.message, t));
|
|
32240
32285
|
}
|
|
32241
|
-
|
|
32242
|
-
}
|
|
32243
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(
|
|
32244
|
-
|
|
32286
|
+
throw new Error(t("listenerTimedOut"));
|
|
32287
|
+
};
|
|
32288
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(SettingsCard, {
|
|
32289
|
+
tab: "remote",
|
|
32290
|
+
nameKey: tabLabelKey("remote"),
|
|
32291
|
+
descKey: tabDescKey("remote"),
|
|
32292
|
+
open: props.open,
|
|
32293
|
+
onToggle: props.onToggle,
|
|
32245
32294
|
children: [
|
|
32246
32295
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
32247
32296
|
className: "daw-warning",
|
|
32248
|
-
children: t("
|
|
32249
|
-
}),
|
|
32250
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
32251
|
-
className: "daw-list-meta",
|
|
32252
|
-
children: t("configTextHint")
|
|
32297
|
+
children: t("httpWarning")
|
|
32253
32298
|
}),
|
|
32254
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
32255
|
-
|
|
32256
|
-
|
|
32257
|
-
style: {
|
|
32258
|
-
width: "100%",
|
|
32259
|
-
height: "auto",
|
|
32260
|
-
minHeight: 76,
|
|
32261
|
-
marginTop: 8,
|
|
32262
|
-
padding: "8px 9px",
|
|
32263
|
-
fontFamily: "ui-monospace,SFMono-Regular,Consolas,monospace",
|
|
32264
|
-
fontSize: 12,
|
|
32265
|
-
lineHeight: 1.5,
|
|
32266
|
-
resize: "vertical"
|
|
32267
|
-
},
|
|
32268
|
-
readOnly: true,
|
|
32269
|
-
rows: 4,
|
|
32270
|
-
spellCheck: false,
|
|
32271
|
-
value: props.result.text,
|
|
32272
|
-
onFocus: (event) => event.currentTarget.select()
|
|
32299
|
+
roots.length === 0 && !managementLocked && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
32300
|
+
className: "daw-warning",
|
|
32301
|
+
children: t("addRootBeforeRemote")
|
|
32273
32302
|
}),
|
|
32274
32303
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
32275
|
-
className: "daw-form
|
|
32276
|
-
style: {
|
|
32277
|
-
marginTop: 8,
|
|
32278
|
-
marginBottom: 10,
|
|
32279
|
-
gridTemplateColumns: "auto auto minmax(0,1fr)",
|
|
32280
|
-
alignItems: "center"
|
|
32281
|
-
},
|
|
32304
|
+
className: "daw-listener-form",
|
|
32282
32305
|
children: [
|
|
32306
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
32307
|
+
className: "daw-field",
|
|
32308
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("bindIp") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
32309
|
+
className: "daw-input",
|
|
32310
|
+
value: listenerHost,
|
|
32311
|
+
disabled: status?.remoteEnabled || managementLocked,
|
|
32312
|
+
spellCheck: false,
|
|
32313
|
+
placeholder: "0.0.0.0",
|
|
32314
|
+
onChange: (event) => setListenerHost(event.target.value)
|
|
32315
|
+
})]
|
|
32316
|
+
}),
|
|
32317
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
32318
|
+
className: "daw-field",
|
|
32319
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("bindPort") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
32320
|
+
className: "daw-input",
|
|
32321
|
+
type: "number",
|
|
32322
|
+
min: 1,
|
|
32323
|
+
max: 65535,
|
|
32324
|
+
inputMode: "numeric",
|
|
32325
|
+
value: listenerPort,
|
|
32326
|
+
disabled: status?.remoteEnabled || managementLocked,
|
|
32327
|
+
onChange: (event) => setListenerPort(event.target.value)
|
|
32328
|
+
})]
|
|
32329
|
+
}),
|
|
32283
32330
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
32284
|
-
className: "daw-command
|
|
32331
|
+
className: "daw-command",
|
|
32332
|
+
disabled: managementLocked || status?.remoteEnabled || listenerHost.trim() === "" || !listenerPortValid || !listenerDirty,
|
|
32285
32333
|
onClick: () => {
|
|
32286
|
-
|
|
32334
|
+
setError(void 0);
|
|
32335
|
+
api.configureListener(listenerHost.trim(), parsedListenerPort).then((result) => poll(result.id)).then(() => {
|
|
32336
|
+
notify("listenerSaved");
|
|
32337
|
+
return refresh();
|
|
32338
|
+
}).catch(setError);
|
|
32287
32339
|
},
|
|
32288
|
-
children: t("
|
|
32289
|
-
}),
|
|
32290
|
-
copyState === "done" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
32291
|
-
className: "daw-pill on",
|
|
32292
|
-
children: t("copied")
|
|
32293
|
-
}),
|
|
32294
|
-
copyState === "manual" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
32295
|
-
className: "daw-list-meta",
|
|
32296
|
-
children: t("copyManual")
|
|
32340
|
+
children: t("saveListener")
|
|
32297
32341
|
})
|
|
32298
32342
|
]
|
|
32299
32343
|
}),
|
|
32300
32344
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
32301
|
-
className: "daw-
|
|
32302
|
-
|
|
32303
|
-
|
|
32304
|
-
|
|
32305
|
-
|
|
32306
|
-
|
|
32307
|
-
"
|
|
32308
|
-
|
|
32309
|
-
|
|
32310
|
-
|
|
32311
|
-
style: { fontFamily: "ui-monospace,SFMono-Regular,Consolas,monospace" },
|
|
32312
|
-
children: props.result.deviceId
|
|
32313
|
-
})
|
|
32314
|
-
]
|
|
32315
|
-
}),
|
|
32316
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
32317
|
-
className: "daw-list-meta",
|
|
32318
|
-
style: { marginBottom: 10 },
|
|
32319
|
-
children: t("configTextScopes")
|
|
32345
|
+
className: "daw-listener-status",
|
|
32346
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
32347
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("strong", { children: [t("configuredListener"), ":"] }),
|
|
32348
|
+
" ",
|
|
32349
|
+
formatEndpoint(status?.configuredHost ?? listenerHost, status?.configuredPort ?? parsedListenerPort)
|
|
32350
|
+
] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
32351
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("strong", { children: [t("actualListener"), ":"] }),
|
|
32352
|
+
" ",
|
|
32353
|
+
formatEndpoint(status?.host ?? "127.0.0.1", status?.port ?? parsedListenerPort)
|
|
32354
|
+
] })]
|
|
32320
32355
|
}),
|
|
32321
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
32322
|
-
|
|
32323
|
-
|
|
32324
|
-
style: { marginTop: 8 },
|
|
32325
|
-
children: t("configTextNoEndpoints")
|
|
32326
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
32327
|
-
className: "daw-list",
|
|
32328
|
-
children: props.result.endpoints.map((endpoint) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
32329
|
-
className: "daw-list-row",
|
|
32330
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
32331
|
-
className: "daw-list-title",
|
|
32332
|
-
children: endpoint.label
|
|
32333
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
32334
|
-
className: "daw-list-meta",
|
|
32335
|
-
children: [endpoint.baseUrl, endpoint.kind === void 0 ? "" : ` · ${endpoint.kind}`]
|
|
32336
|
-
})] })
|
|
32337
|
-
}, `${endpoint.label}:${endpoint.baseUrl}`))
|
|
32338
|
-
})
|
|
32339
|
-
]
|
|
32340
|
-
});
|
|
32341
|
-
}
|
|
32342
|
-
function DeviceRow(props) {
|
|
32343
|
-
const { locale, t } = useWorkspaceI18n();
|
|
32344
|
-
const [editing, setEditing] = (0, react.useState)(false);
|
|
32345
|
-
const [scopes, setScopes] = (0, react.useState)(props.device.scopes);
|
|
32346
|
-
const [rootIds, setRootIds] = (0, react.useState)(props.device.rootIds);
|
|
32347
|
-
const [name, setName] = (0, react.useState)(props.device.name);
|
|
32348
|
-
const toggle = (list, value) => list.includes(value) ? list.filter((item) => item !== value) : [...list, value];
|
|
32349
|
-
const shortId = props.device.id.length > 14 ? `${props.device.id.slice(0, 8)}…${props.device.id.slice(-4)}` : props.device.id;
|
|
32350
|
-
const lastSeen = props.device.lastSeenAt === void 0 ? t("deviceNeverSeen") : t("deviceLastSeen", { time: new Date(props.device.lastSeenAt).toLocaleString(locale) });
|
|
32351
|
-
const autoNamed = props.device.nameSource === "default" || props.device.nameSource === "detected";
|
|
32352
|
-
const saveAll = () => {
|
|
32353
|
-
const trimmed = name.trim();
|
|
32354
|
-
const rename = trimmed !== "" && trimmed !== props.device.name;
|
|
32355
|
-
(rename ? props.api.renameDevice(props.device.id, trimmed) : Promise.resolve()).then(() => props.api.updateDevice(props.device.id, scopes, rootIds)).then(props.onChanged).then(() => {
|
|
32356
|
-
setEditing(false);
|
|
32357
|
-
props.onNotice(rename ? "deviceNameSaved" : "permissionsSaved");
|
|
32358
|
-
}).catch(props.onError);
|
|
32359
|
-
};
|
|
32360
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
32361
|
-
className: "daw-list-row",
|
|
32362
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
|
|
32363
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
32364
|
-
className: "daw-list-title",
|
|
32365
|
-
children: [
|
|
32366
|
-
props.device.name,
|
|
32367
|
-
props.device.revokedAt !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
32368
|
-
className: "daw-pill",
|
|
32369
|
-
children: t("revoked")
|
|
32370
|
-
}),
|
|
32371
|
-
autoNamed && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
32372
|
-
className: "daw-pill",
|
|
32373
|
-
title: t("deviceRenameHint"),
|
|
32374
|
-
children: t("deviceDetectedName")
|
|
32375
|
-
})
|
|
32376
|
-
]
|
|
32356
|
+
status?.remoteEnabled && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
32357
|
+
className: "daw-list-meta daw-listener-hint",
|
|
32358
|
+
children: t("disableToEditListener")
|
|
32377
32359
|
}),
|
|
32378
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
32379
|
-
className: "daw-list-meta",
|
|
32380
|
-
children:
|
|
32381
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
32382
|
-
title: props.device.id,
|
|
32383
|
-
children: shortId
|
|
32384
|
-
}),
|
|
32385
|
-
props.device.platform !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
32386
|
-
" · ",
|
|
32387
|
-
props.device.platform,
|
|
32388
|
-
props.device.model === void 0 ? "" : ` · ${props.device.model}`
|
|
32389
|
-
] }),
|
|
32390
|
-
" · ",
|
|
32391
|
-
lastSeen,
|
|
32392
|
-
" · ",
|
|
32393
|
-
props.device.scopes.length === 0 ? t("noScopes") : props.device.scopes.map((scope) => scopeLabel(locale, scope)).join(" · ")
|
|
32394
|
-
]
|
|
32360
|
+
!status?.remoteEnabled && listenerDirty && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
32361
|
+
className: "daw-list-meta daw-listener-hint",
|
|
32362
|
+
children: t("saveBeforeEnable")
|
|
32395
32363
|
}),
|
|
32396
|
-
|
|
32397
|
-
|
|
32398
|
-
|
|
32399
|
-
|
|
32400
|
-
|
|
32401
|
-
|
|
32402
|
-
|
|
32403
|
-
|
|
32404
|
-
|
|
32405
|
-
|
|
32406
|
-
|
|
32407
|
-
|
|
32408
|
-
})]
|
|
32409
|
-
}),
|
|
32410
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
32364
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
32365
|
+
className: `daw-pill${status?.remoteEnabled ? " on" : ""}`,
|
|
32366
|
+
children: status?.remoteEnabled ? t("remoteEnabled") : t("loopbackOnly")
|
|
32367
|
+
}) }),
|
|
32368
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("fieldset", {
|
|
32369
|
+
disabled: managementLocked,
|
|
32370
|
+
style: {
|
|
32371
|
+
border: "none",
|
|
32372
|
+
margin: 0,
|
|
32373
|
+
padding: 0
|
|
32374
|
+
},
|
|
32375
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
32411
32376
|
className: "daw-scope-grid",
|
|
32412
32377
|
children: DEVICE_SCOPES.map((scope) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
32413
32378
|
className: "daw-check",
|
|
32414
|
-
children: [
|
|
32415
|
-
|
|
32416
|
-
|
|
32417
|
-
|
|
32418
|
-
|
|
32419
|
-
|
|
32420
|
-
|
|
32421
|
-
|
|
32422
|
-
|
|
32423
|
-
|
|
32424
|
-
className: "daw-check",
|
|
32425
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
32426
|
-
type: "checkbox",
|
|
32427
|
-
checked: rootIds.includes(root.id),
|
|
32428
|
-
onChange: () => setRootIds((current) => toggle(current, root.id))
|
|
32429
|
-
}), root.label]
|
|
32430
|
-
}, root.id))
|
|
32379
|
+
children: [
|
|
32380
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
32381
|
+
type: "checkbox",
|
|
32382
|
+
checked: grantedScopes.includes(scope),
|
|
32383
|
+
onChange: () => toggleScope(scope)
|
|
32384
|
+
}),
|
|
32385
|
+
" ",
|
|
32386
|
+
scopeLabel(locale, scope)
|
|
32387
|
+
]
|
|
32388
|
+
}, scope))
|
|
32431
32389
|
})
|
|
32432
|
-
] })
|
|
32433
|
-
] }), props.device.revokedAt === void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
|
|
32434
|
-
editing && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
32435
|
-
className: "daw-command primary",
|
|
32436
|
-
onClick: saveAll,
|
|
32437
|
-
children: t("save")
|
|
32438
32390
|
}),
|
|
32439
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
32440
|
-
className: "daw-
|
|
32391
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
32392
|
+
className: "daw-list-meta",
|
|
32393
|
+
children: grantNotice
|
|
32394
|
+
}),
|
|
32395
|
+
status?.remoteEnabled ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
32396
|
+
className: "daw-command danger",
|
|
32397
|
+
disabled: managementLocked,
|
|
32398
|
+
title: managementLocked ? t("managementLockedWhileTunnel") : void 0,
|
|
32441
32399
|
onClick: () => {
|
|
32442
|
-
|
|
32443
|
-
|
|
32400
|
+
if (!window.confirm(t("disableRemoteConfirm"))) return;
|
|
32401
|
+
setError(void 0);
|
|
32402
|
+
api.disableRemote().then((result) => poll(result.id)).then(() => {
|
|
32403
|
+
notify("remoteDisabled");
|
|
32404
|
+
return refresh();
|
|
32405
|
+
}).catch(setError);
|
|
32444
32406
|
},
|
|
32445
|
-
children:
|
|
32446
|
-
}),
|
|
32447
|
-
|
|
32448
|
-
|
|
32449
|
-
title: t("revokeDevice"),
|
|
32450
|
-
"aria-label": t("revokeDevice"),
|
|
32407
|
+
children: t("disableRemote")
|
|
32408
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
32409
|
+
className: "daw-command primary",
|
|
32410
|
+
disabled: managementLocked || roots.length === 0 || listenerDirty || !listenerPortValid || listenerHost.trim() === "" || grantedScopes.length === 0,
|
|
32451
32411
|
onClick: () => {
|
|
32452
|
-
|
|
32453
|
-
|
|
32412
|
+
setError(void 0);
|
|
32413
|
+
api.enableRemote(roots.map((root) => root.id), grantedScopes).then((result) => poll(result.id)).catch(setError);
|
|
32454
32414
|
},
|
|
32455
|
-
children:
|
|
32456
|
-
})
|
|
32457
|
-
|
|
32415
|
+
children: t("enablePairing")
|
|
32416
|
+
}),
|
|
32417
|
+
(status?.remoteEnabled === true || managementLocked) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
32418
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
32419
|
+
className: "daw-command daw-inline-command",
|
|
32420
|
+
disabled: managementLocked || grantedScopes.length === 0,
|
|
32421
|
+
title: managementLocked ? t("managementLockedWhileTunnel") : void 0,
|
|
32422
|
+
onClick: () => {
|
|
32423
|
+
setError(void 0);
|
|
32424
|
+
api.createPairing(roots.map((root) => root.id), grantedScopes).then((value) => {
|
|
32425
|
+
setPairing(value);
|
|
32426
|
+
notify("pairingCreated");
|
|
32427
|
+
}).catch(setError);
|
|
32428
|
+
},
|
|
32429
|
+
children: t("newPairingCode")
|
|
32430
|
+
}),
|
|
32431
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
32432
|
+
className: "daw-command daw-inline-command",
|
|
32433
|
+
disabled: managementLocked || configTextBusy,
|
|
32434
|
+
title: managementLocked ? t("managementLockedWhileTunnel") : void 0,
|
|
32435
|
+
onClick: () => {
|
|
32436
|
+
const manual = configEndpoint.trim();
|
|
32437
|
+
setConfigTextBusy(true);
|
|
32438
|
+
setError(void 0);
|
|
32439
|
+
api.createConfigText({
|
|
32440
|
+
...roots.length === 0 ? {} : { rootIds: roots.map((root) => root.id) },
|
|
32441
|
+
...manual === "" ? {} : { endpoints: [{
|
|
32442
|
+
label: t("configTextManualLabel"),
|
|
32443
|
+
baseUrl: manual
|
|
32444
|
+
}] }
|
|
32445
|
+
}).then((value) => {
|
|
32446
|
+
setConfigText(value);
|
|
32447
|
+
notify("configCreated");
|
|
32448
|
+
}).catch(setError).finally(() => setConfigTextBusy(false));
|
|
32449
|
+
},
|
|
32450
|
+
children: t("newConfigText")
|
|
32451
|
+
}),
|
|
32452
|
+
tunnel !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
32453
|
+
className: "daw-command daw-inline-command",
|
|
32454
|
+
disabled: configTextBusy || tunnelBusy,
|
|
32455
|
+
onClick: () => {
|
|
32456
|
+
tunnelConfigClick();
|
|
32457
|
+
},
|
|
32458
|
+
children: tunnel.running !== true ? t("tunnelStartAndGenConfig") : t("tunnelRestartAndGenConfig")
|
|
32459
|
+
}),
|
|
32460
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
32461
|
+
className: "daw-list-meta daw-listener-hint",
|
|
32462
|
+
children: t("configTextScopeNote")
|
|
32463
|
+
})
|
|
32464
|
+
] }),
|
|
32465
|
+
pairing !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
32466
|
+
className: "daw-code-row",
|
|
32467
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
32468
|
+
className: "daw-code",
|
|
32469
|
+
children: pairing.code
|
|
32470
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopyButton, {
|
|
32471
|
+
className: "daw-copy-inline",
|
|
32472
|
+
text: pairing.code,
|
|
32473
|
+
title: t("copyPairing"),
|
|
32474
|
+
copiedText: t("copied"),
|
|
32475
|
+
manualText: t("copyManual"),
|
|
32476
|
+
copyText
|
|
32477
|
+
})]
|
|
32478
|
+
}), pairing.expiresAt !== void 0 && pairing.expiresAt > now && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
32479
|
+
className: "daw-list-meta",
|
|
32480
|
+
style: { marginTop: -4 },
|
|
32481
|
+
children: t("pairingExpiresAt", { minutes: pairingMinutesLeft(pairing.expiresAt, now) })
|
|
32482
|
+
})] }),
|
|
32483
|
+
(status?.remoteEnabled === true || managementLocked) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
32484
|
+
style: {
|
|
32485
|
+
marginTop: 12,
|
|
32486
|
+
maxWidth: 560
|
|
32487
|
+
},
|
|
32488
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
32489
|
+
className: "daw-field",
|
|
32490
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("configTextManual") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
32491
|
+
className: "daw-input",
|
|
32492
|
+
value: configEndpoint,
|
|
32493
|
+
disabled: managementLocked,
|
|
32494
|
+
spellCheck: false,
|
|
32495
|
+
placeholder: t("configTextManualPlaceholder"),
|
|
32496
|
+
onChange: (event) => setConfigEndpoint(event.target.value)
|
|
32497
|
+
})]
|
|
32498
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
32499
|
+
className: "daw-list-meta",
|
|
32500
|
+
style: { marginTop: 5 },
|
|
32501
|
+
children: t("configTextManualHint")
|
|
32502
|
+
})]
|
|
32503
|
+
}),
|
|
32504
|
+
configText !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfigTextPanel, { result: configText })
|
|
32505
|
+
]
|
|
32458
32506
|
});
|
|
32459
32507
|
}
|
|
32460
|
-
|
|
32461
|
-
|
|
32462
|
-
|
|
32463
|
-
|
|
32464
|
-
|
|
32465
|
-
|
|
32466
|
-
|
|
32467
|
-
|
|
32468
|
-
|
|
32469
|
-
|
|
32470
|
-
|
|
32471
|
-
|
|
32472
|
-
|
|
32473
|
-
|
|
32474
|
-
|
|
32475
|
-
|
|
32476
|
-
|
|
32477
|
-
|
|
32478
|
-
|
|
32479
|
-
|
|
32480
|
-
|
|
32481
|
-
|
|
32482
|
-
|
|
32483
|
-
|
|
32484
|
-
|
|
32485
|
-
|
|
32486
|
-
|
|
32487
|
-
|
|
32488
|
-
|
|
32489
|
-
|
|
32490
|
-
|
|
32491
|
-
|
|
32492
|
-
|
|
32493
|
-
|
|
32494
|
-
|
|
32495
|
-
|
|
32496
|
-
|
|
32497
|
-
|
|
32498
|
-
|
|
32499
|
-
|
|
32500
|
-
|
|
32501
|
-
|
|
32502
|
-
|
|
32503
|
-
|
|
32504
|
-
|
|
32505
|
-
|
|
32506
|
-
|
|
32507
|
-
|
|
32508
|
-
|
|
32509
|
-
|
|
32510
|
-
|
|
32511
|
-
|
|
32512
|
-
|
|
32513
|
-
|
|
32508
|
+
//#endregion
|
|
32509
|
+
//#region src/client/cards/roots.tsx
|
|
32510
|
+
/**
|
|
32511
|
+
* 「根目录」卡:授权手机访问这台机器上的哪些目录。
|
|
32512
|
+
*
|
|
32513
|
+
* 为什么它收在「高级设置」组里而不是主列表:它一次配置完就基本不动,而主列表要留给
|
|
32514
|
+
* 日常会看的东西。但它在组内**排第一位** —— 没有授权根目录,启用远程访问与创建隧道
|
|
32515
|
+
* 都会被 `roots.length === 0` 锁死,它是所有远程能力的前置。
|
|
32516
|
+
*/
|
|
32517
|
+
function RootsCard(props) {
|
|
32518
|
+
const { api, t, roots, managementLocked, refresh, notify, setError } = useCards();
|
|
32519
|
+
const [path, setPath] = (0, react.useState)("");
|
|
32520
|
+
const [label, setLabel] = (0, react.useState)("");
|
|
32521
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(SettingsCard, {
|
|
32522
|
+
tab: "roots",
|
|
32523
|
+
nameKey: tabLabelKey("roots"),
|
|
32524
|
+
descKey: tabDescKey("roots"),
|
|
32525
|
+
open: props.open,
|
|
32526
|
+
onToggle: props.onToggle,
|
|
32527
|
+
children: [
|
|
32528
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
32529
|
+
className: "daw-form-row",
|
|
32530
|
+
children: [
|
|
32531
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
32532
|
+
className: "daw-input",
|
|
32533
|
+
placeholder: t("absolutePath"),
|
|
32534
|
+
value: path,
|
|
32535
|
+
onChange: (event) => setPath(event.target.value)
|
|
32536
|
+
}),
|
|
32537
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
32538
|
+
className: "daw-input",
|
|
32539
|
+
placeholder: t("displayLabel"),
|
|
32540
|
+
value: label,
|
|
32541
|
+
onChange: (event) => setLabel(event.target.value)
|
|
32542
|
+
}),
|
|
32543
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
32544
|
+
className: "daw-command primary",
|
|
32545
|
+
disabled: managementLocked,
|
|
32546
|
+
title: managementLocked ? t("managementLockedWhileTunnel") : void 0,
|
|
32547
|
+
onClick: () => {
|
|
32548
|
+
setError(void 0);
|
|
32549
|
+
api.addRoot(path, label || void 0).then(() => {
|
|
32550
|
+
setPath("");
|
|
32551
|
+
setLabel("");
|
|
32552
|
+
notify("rootAdded");
|
|
32553
|
+
return refresh();
|
|
32554
|
+
}).catch(setError);
|
|
32555
|
+
},
|
|
32556
|
+
children: t("addRoot")
|
|
32557
|
+
})
|
|
32558
|
+
]
|
|
32559
|
+
}),
|
|
32560
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
32561
|
+
className: "daw-list",
|
|
32562
|
+
children: roots.map((root) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
32563
|
+
className: "daw-list-row",
|
|
32564
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
32565
|
+
className: "daw-list-title",
|
|
32566
|
+
children: [root.label, root.builtin === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
32567
|
+
className: "daw-pill on",
|
|
32568
|
+
children: t("builtinRootBadge")
|
|
32569
|
+
})]
|
|
32570
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
32571
|
+
className: "daw-list-meta",
|
|
32572
|
+
children: root.realPath
|
|
32573
|
+
})] }), root.builtin === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
32574
|
+
className: "daw-list-meta daw-builtin-hint",
|
|
32575
|
+
children: t("builtinRootHint")
|
|
32576
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
32577
|
+
className: "daw-icon danger",
|
|
32578
|
+
title: t("removeRoot"),
|
|
32579
|
+
"aria-label": t("removeRoot"),
|
|
32580
|
+
disabled: managementLocked,
|
|
32581
|
+
onClick: () => {
|
|
32582
|
+
if (!window.confirm(t("removeRootConfirm", { label: root.label }))) return;
|
|
32583
|
+
setError(void 0);
|
|
32584
|
+
api.removeRoot(root.id).then(() => {
|
|
32585
|
+
notify("rootRemoved");
|
|
32586
|
+
return refresh();
|
|
32587
|
+
}).catch(setError);
|
|
32588
|
+
},
|
|
32589
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Trash2, { size: 15 })
|
|
32590
|
+
})]
|
|
32591
|
+
}, root.id))
|
|
32592
|
+
}),
|
|
32593
|
+
roots.some((root) => root.builtin === true) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
32594
|
+
className: "daw-list-meta daw-builtin-notice",
|
|
32595
|
+
children: t("builtinRootNotice")
|
|
32596
|
+
})
|
|
32597
|
+
]
|
|
32598
|
+
});
|
|
32514
32599
|
}
|
|
32515
|
-
|
|
32516
|
-
|
|
32600
|
+
//#endregion
|
|
32601
|
+
//#region src/client/cards/trash.tsx
|
|
32602
|
+
/**
|
|
32603
|
+
* 「回收站」卡:手机端删掉的文件留在这里,可以还原或彻底删除。
|
|
32604
|
+
*
|
|
32605
|
+
* 从 `workspace.tsx` 的 `AdminPanel` 搬出来。与 `image.tsx` 同样处理懒加载:
|
|
32606
|
+
*
|
|
32607
|
+
* ## 列表数据是懒加载的,判据是 `open`
|
|
32608
|
+
*
|
|
32609
|
+
* 原实现把 `trashItems()` 放在 `AdminPanel.refresh()` 里,条件是 `openCards.has('trash')`。
|
|
32610
|
+
* 拆出来后这条判据变成这张卡自己的 `open` prop —— **语义完全等价**:`open` 就是
|
|
32611
|
+
* `isOpen('trash')` 的返回值,同一个 Set、同一个值。
|
|
32612
|
+
*
|
|
32613
|
+
* 但有一个细节必须一起搬:原实现里还原/彻底删除之后走的是 `return refresh()`,而
|
|
32614
|
+
* `refresh()` 在「卡展开着」时会把回收站列表重新拉一遍 —— 所以删除动作的结果能立刻
|
|
32615
|
+
* 反映到列表上。列表状态现在住在这张卡里,`refresh()` 已经不知道它的存在,于是
|
|
32616
|
+
* 这里必须自己再拉一次,否则用户点了「还原」、文件从磁盘回来了、列表里那一行却还在。
|
|
32617
|
+
*/
|
|
32618
|
+
function TrashCard(props) {
|
|
32619
|
+
const { api, t, locale, refresh, notify, setError } = useCards();
|
|
32620
|
+
const [trash, setTrash] = (0, react.useState)([]);
|
|
32621
|
+
(0, react.useEffect)(() => {
|
|
32622
|
+
if (!props.open) return;
|
|
32623
|
+
let cancelled = false;
|
|
32624
|
+
api.trashItems().then((next) => {
|
|
32625
|
+
if (cancelled) return;
|
|
32626
|
+
setTrash(next.items);
|
|
32627
|
+
}).catch((cause) => {
|
|
32628
|
+
if (!cancelled) setError(cause);
|
|
32629
|
+
});
|
|
32630
|
+
return () => {
|
|
32631
|
+
cancelled = true;
|
|
32632
|
+
};
|
|
32633
|
+
}, [
|
|
32634
|
+
props.open,
|
|
32635
|
+
api,
|
|
32636
|
+
setError
|
|
32637
|
+
]);
|
|
32638
|
+
/**
|
|
32639
|
+
* 还原/删除之后的重新拉取。
|
|
32640
|
+
*
|
|
32641
|
+
* 两件事都要做:`refresh()` 让设置页其余分区跟着更新(原来的 `return refresh()` 就是这么
|
|
32642
|
+
* 干的),再单独把本卡的列表换掉 —— 后者正是拆卡之后必须自己补上的那一步。
|
|
32643
|
+
*/
|
|
32644
|
+
const reloadAfterAction = () => Promise.all([refresh(), api.trashItems().then((next) => setTrash(next.items))]).then(() => void 0);
|
|
32645
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(SettingsCard, {
|
|
32646
|
+
tab: "trash",
|
|
32647
|
+
nameKey: tabLabelKey("trash"),
|
|
32648
|
+
descKey: tabDescKey("trash"),
|
|
32649
|
+
open: props.open,
|
|
32650
|
+
onToggle: props.onToggle,
|
|
32651
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
32652
|
+
className: "daw-list-meta",
|
|
32653
|
+
children: t("pluginTrashHint")
|
|
32654
|
+
}), trash.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
32655
|
+
className: "daw-list-meta daw-empty",
|
|
32656
|
+
children: t("trashEmpty")
|
|
32657
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
32658
|
+
className: "daw-list",
|
|
32659
|
+
children: trash.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
32660
|
+
className: "daw-list-row",
|
|
32661
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
32662
|
+
className: "daw-list-title",
|
|
32663
|
+
children: item.path
|
|
32664
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
32665
|
+
className: "daw-list-meta",
|
|
32666
|
+
children: [
|
|
32667
|
+
item.kind,
|
|
32668
|
+
" · ",
|
|
32669
|
+
formatBytes(item.size),
|
|
32670
|
+
" · ",
|
|
32671
|
+
new Date(item.createdAt).toLocaleString(locale)
|
|
32672
|
+
]
|
|
32673
|
+
})] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
32674
|
+
className: "daw-command",
|
|
32675
|
+
onClick: () => {
|
|
32676
|
+
setError(void 0);
|
|
32677
|
+
api.restoreTrash(item.id).then(() => {
|
|
32678
|
+
notify("trashRestored");
|
|
32679
|
+
return reloadAfterAction();
|
|
32680
|
+
}).catch(setError);
|
|
32681
|
+
},
|
|
32682
|
+
children: t("restore")
|
|
32683
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
32684
|
+
className: "daw-icon danger",
|
|
32685
|
+
title: t("deletePermanently"),
|
|
32686
|
+
"aria-label": t("deletePermanently"),
|
|
32687
|
+
onClick: () => {
|
|
32688
|
+
if (confirm(t("purgeConfirm"))) api.purgeTrash(item.id).then(() => {
|
|
32689
|
+
notify("trashPurged");
|
|
32690
|
+
return reloadAfterAction();
|
|
32691
|
+
}).catch(setError);
|
|
32692
|
+
},
|
|
32693
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Trash2, { size: 15 })
|
|
32694
|
+
})] })]
|
|
32695
|
+
}, item.id))
|
|
32696
|
+
})]
|
|
32697
|
+
});
|
|
32517
32698
|
}
|
|
32518
32699
|
//#endregion
|
|
32519
|
-
//#region src/client/
|
|
32700
|
+
//#region src/client/cards/tunnel.tsx
|
|
32520
32701
|
/**
|
|
32521
|
-
*
|
|
32702
|
+
* 「外网隧道」卡:Cloudflare Quick Tunnel —— 公网连入的兜底通道。
|
|
32522
32703
|
*
|
|
32523
|
-
* ##
|
|
32704
|
+
* ## 为什么从 `remote` 里拆出来
|
|
32524
32705
|
*
|
|
32525
|
-
*
|
|
32526
|
-
*
|
|
32527
|
-
*
|
|
32528
|
-
*
|
|
32706
|
+
* 拆之前这张卡挤在「远程访问」卡的末尾,一共 123 行里它占 37 行,语义却是两件事:
|
|
32707
|
+
* 前者是「在本机开个口子」(绑 IP/端口 + 配对码),后者是「把机器搬到外网」。
|
|
32708
|
+
* 用户看到同一个卡里既绑端口又开隧道,会以为隧道是远程访问的一个开关 ——
|
|
32709
|
+
* 它不是,它是**独立的一条通道**,与「远程访问已启用」和「选择权限」并列。
|
|
32529
32710
|
*
|
|
32530
|
-
*
|
|
32531
|
-
* definition,并在 `sidebar.right.pane.tab` / `.title` 两个 keyed seat 上按 `definition.id`
|
|
32532
|
-
* 挂上 body 与标题。这样 guide 页会列出这张卡片,用户能在 rc.3 的 More 菜单里把它
|
|
32533
|
-
* 固定 / 取消固定(`guide-pins.d.ts` 的 `FIXED_GUIDE_KIND = "files"` 只豁免文件卡片,
|
|
32534
|
-
* 桥的 kind 不在豁免名单里)。
|
|
32711
|
+
* ## 状态为什么住在 `AdminPanel` 而不是这里
|
|
32535
32712
|
*
|
|
32536
|
-
*
|
|
32713
|
+
* 这张卡用到的 `tunnel` / `tunnelBusy` / `activity` / `createTunnel` / `stopTunnel`
|
|
32714
|
+
* 有一半要供主壳算 `managementLocked`(隧道运行 ⇒ 除隧道自身外所有管理写操作 403),
|
|
32715
|
+
* 另一半还要被 `remote` 卡的「一键开隧道并出令牌」按钮消费。状态搬进这个文件,
|
|
32716
|
+
* 主壳就没有依据去禁用别的卡片了。所以它们经 `context.ts` 下发,见那里的长注释。
|
|
32537
32717
|
*
|
|
32538
|
-
*
|
|
32539
|
-
*
|
|
32540
|
-
*
|
|
32718
|
+
* ## 必须保住的一处判据
|
|
32719
|
+
*
|
|
32720
|
+
* 「停止」按钮**绝不能**被 `roots.length === 0` 锁死。隧道运行期间管理面关门,
|
|
32721
|
+
* `status` / `roots` 可能刷新失败为空;若停止也要求有根目录,用户就再也关不掉隧道了。
|
|
32722
|
+
* 只有「创建」需要至少一个授权根(配置文本要有落点)。
|
|
32723
|
+
*/
|
|
32724
|
+
function TunnelCard(props) {
|
|
32725
|
+
const { t } = useWorkspaceI18n();
|
|
32726
|
+
const { tunnel, tunnelBusy, activity, roots, copyText, createTunnel, stopTunnel, configText, generateConfigForUrl, configTextBusy, managementLocked, setError } = useCards();
|
|
32727
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(SettingsCard, {
|
|
32728
|
+
tab: "tunnel",
|
|
32729
|
+
nameKey: tabLabelKey("tunnel"),
|
|
32730
|
+
descKey: tabDescKey("tunnel"),
|
|
32731
|
+
open: props.open,
|
|
32732
|
+
onToggle: props.onToggle,
|
|
32733
|
+
children: [
|
|
32734
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
32735
|
+
className: "daw-tunnel",
|
|
32736
|
+
children: [
|
|
32737
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
32738
|
+
className: "daw-tunnel-head",
|
|
32739
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t("tunnelTitle") }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
32740
|
+
className: `daw-pill${tunnel?.running ? " on" : ""}`,
|
|
32741
|
+
children: tunnel?.running ? t("tunnelRunning") : t("tunnelStopped")
|
|
32742
|
+
})]
|
|
32743
|
+
}),
|
|
32744
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
32745
|
+
className: "daw-list-meta",
|
|
32746
|
+
children: t("tunnelHint")
|
|
32747
|
+
}),
|
|
32748
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
32749
|
+
className: "daw-warning",
|
|
32750
|
+
children: t("tunnelSecurityWarning")
|
|
32751
|
+
}),
|
|
32752
|
+
tunnel?.errorKey !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
32753
|
+
className: "daw-warning",
|
|
32754
|
+
role: "alert",
|
|
32755
|
+
children: t(tunnel.errorKey, tunnel.errorParams ?? {})
|
|
32756
|
+
}),
|
|
32757
|
+
tunnel?.errorKey === void 0 && tunnel?.error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
32758
|
+
className: "daw-warning",
|
|
32759
|
+
role: "alert",
|
|
32760
|
+
children: tunnel.error
|
|
32761
|
+
}),
|
|
32762
|
+
tunnel?.url !== void 0 && tunnel.url !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
32763
|
+
className: "daw-url-row",
|
|
32764
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
32765
|
+
className: "daw-tunnel-url",
|
|
32766
|
+
children: tunnel.url
|
|
32767
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopyButton, {
|
|
32768
|
+
text: tunnel.url,
|
|
32769
|
+
title: t("copyUrl"),
|
|
32770
|
+
copiedText: t("copied"),
|
|
32771
|
+
manualText: t("copyManual"),
|
|
32772
|
+
copyText
|
|
32773
|
+
})]
|
|
32774
|
+
}),
|
|
32775
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
32776
|
+
className: "daw-command primary",
|
|
32777
|
+
style: { marginTop: 10 },
|
|
32778
|
+
disabled: tunnelBusy || tunnel?.running !== true && roots.length === 0,
|
|
32779
|
+
onClick: () => {
|
|
32780
|
+
if (tunnel?.running === true) stopTunnel();
|
|
32781
|
+
else createTunnel({ thenGenConfig: true });
|
|
32782
|
+
},
|
|
32783
|
+
children: tunnel?.running === true ? t("tunnelStop") : t("tunnelCreate")
|
|
32784
|
+
}),
|
|
32785
|
+
activity !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
32786
|
+
className: "daw-tunnel-progress",
|
|
32787
|
+
style: { marginTop: 10 },
|
|
32788
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
32789
|
+
className: "daw-progress-track",
|
|
32790
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
32791
|
+
className: `daw-progress-fill${activity.percent === null ? " indeterminate" : ""}`,
|
|
32792
|
+
style: activity.percent === null ? void 0 : { width: `${activity.percent}%` }
|
|
32793
|
+
})
|
|
32794
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
32795
|
+
className: "daw-list-meta",
|
|
32796
|
+
style: { marginTop: 4 },
|
|
32797
|
+
children: activity.labelKey === "" ? "" : t(activity.labelKey, activity.labelParams ?? {})
|
|
32798
|
+
})]
|
|
32799
|
+
})
|
|
32800
|
+
]
|
|
32801
|
+
}),
|
|
32802
|
+
tunnel?.running === true && tunnel.url != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
32803
|
+
className: "daw-command",
|
|
32804
|
+
style: { marginTop: 10 },
|
|
32805
|
+
disabled: configTextBusy,
|
|
32806
|
+
onClick: () => {
|
|
32807
|
+
generateConfigForUrl(tunnel.url, "tunnelConfigAuto").catch(setError);
|
|
32808
|
+
},
|
|
32809
|
+
children: t("tunnelGenConfigByUrl")
|
|
32810
|
+
}),
|
|
32811
|
+
configText !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfigTextPanel, { result: configText })
|
|
32812
|
+
]
|
|
32813
|
+
});
|
|
32814
|
+
}
|
|
32815
|
+
//#endregion
|
|
32816
|
+
//#region src/client/workspace.tsx
|
|
32817
|
+
/**
|
|
32818
|
+
* 收进「高级设置」的分区:主列表不显示,但**每一个都照旧可用**。
|
|
32541
32819
|
*
|
|
32542
|
-
*
|
|
32820
|
+
* 顺序即展开后的显示顺序。要加一张卡回主列表,把它从 `ADVANCED_TABS` 移回
|
|
32821
|
+
* `ADMIN_TABS` 即可 —— `TAB_DESC_KEYS` 是 `Record<AdminTab, …>` 全覆盖的,
|
|
32822
|
+
* 两处都能编译通过,不需要再补别的。
|
|
32543
32823
|
*
|
|
32544
|
-
* `
|
|
32545
|
-
*
|
|
32546
|
-
* 点开也正常,只是**不能 pin**。这是可接受的降级:能力随宿主版本生长,桥不做版本判断。
|
|
32824
|
+
* `roots` 排第一位:它是所有远程能力的**前置**(没有授权根目录,启用远程访问与创建隧道
|
|
32825
|
+
* 都会被 `roots.length === 0` 锁死)。放在组首,用户一展开就先看到它。
|
|
32547
32826
|
*/
|
|
32548
|
-
|
|
32549
|
-
|
|
32827
|
+
const ADVANCED_TABS = [
|
|
32828
|
+
"roots",
|
|
32829
|
+
"image",
|
|
32830
|
+
"plugins"
|
|
32831
|
+
];
|
|
32550
32832
|
/**
|
|
32551
|
-
*
|
|
32833
|
+
* 进设置页时先展开哪一张。
|
|
32552
32834
|
*
|
|
32553
|
-
*
|
|
32554
|
-
*
|
|
32555
|
-
*
|
|
32835
|
+
* 不能指向 `ADVANCED_TABS` 里的卡:那样进来看到的是一张收起的卡(甚至整组是收起的),
|
|
32836
|
+
* 用户不会知道该先授权根目录。见 `AdminPanel` 里 `advancedOpen` 的初始化 ——
|
|
32837
|
+
* 万一以后有人把它改成组内卡,那组会自动展开,不会出现「默认卡在关着的组里」。
|
|
32556
32838
|
*/
|
|
32557
|
-
const
|
|
32839
|
+
const DEFAULT_PANEL_SECTION = "remote";
|
|
32558
32840
|
/**
|
|
32559
|
-
*
|
|
32841
|
+
* The single desktop dialog: a vertical section rail on the left, one content pane on the right.
|
|
32560
32842
|
*
|
|
32561
|
-
*
|
|
32562
|
-
*
|
|
32563
|
-
|
|
32843
|
+
* Settings only — the five admin sections. The file tree used to live here as a sixth row; it does
|
|
32844
|
+
* not any more, so this is the old `AdminOverlay` body with a rail, not a workspace host.
|
|
32845
|
+
*/
|
|
32846
|
+
/**
|
|
32847
|
+
* 导航栏 + 内容区,**不含任何对话框外壳**。
|
|
32564
32848
|
*
|
|
32565
|
-
*
|
|
32566
|
-
*
|
|
32849
|
+
* 抽出来是为了让 DSH 设置页能把它**内联**渲染。
|
|
32850
|
+
*
|
|
32851
|
+
* 早先的版本做不到这一点,于是设置页里只放了一句说明加一个「打开手机设置面板」按钮,
|
|
32852
|
+
* 真正的设置藏在按钮后面的模态框里。用户的反应很直接:「其他的可以设置的东西那去了」——
|
|
32853
|
+
* 这就是把弹窗嵌进设置页的代价:入口看起来是空的,因为内容在下一层。
|
|
32854
|
+
* 设置页本身就是一层容器,没有任何理由再套一层。
|
|
32567
32855
|
*/
|
|
32568
|
-
function pluginsTabDefinition() {
|
|
32569
|
-
const label = () => translate(getWorkspaceLocale(), "pluginsTab");
|
|
32570
|
-
const description = () => translate(getWorkspaceLocale(), "pluginsTabDesc");
|
|
32571
|
-
return {
|
|
32572
|
-
id: PLUGINS_TAB_ID,
|
|
32573
|
-
kind: PLUGINS_TAB_KIND,
|
|
32574
|
-
title: label,
|
|
32575
|
-
guide: [{
|
|
32576
|
-
id: "plugins",
|
|
32577
|
-
order: 40,
|
|
32578
|
-
title: label,
|
|
32579
|
-
description,
|
|
32580
|
-
icon: import_lucide_react.Package
|
|
32581
|
-
}]
|
|
32582
|
-
};
|
|
32583
|
-
}
|
|
32584
32856
|
/**
|
|
32585
|
-
*
|
|
32857
|
+
* 设置面板主体:**七张卡片平铺,各自独立开合,没有导航栏**。
|
|
32586
32858
|
*
|
|
32587
|
-
*
|
|
32588
|
-
*
|
|
32859
|
+
* 上一版留了个分区导航栏、一次只显示一张卡。对照 Vision Router 的实际长相才发现差得远:
|
|
32860
|
+
* 它是全部卡片一起列出来、想开哪张开哪张,标题栏自己就是入口。那句提示词写得很直白 ——
|
|
32861
|
+
* 「多张卡可以同时保持展开」。所以导航栏这一层是多余的,用户还得先点导航、再点卡片头,
|
|
32862
|
+
* 两次点击才走一步。
|
|
32863
|
+
*
|
|
32864
|
+
* 抽出来是为了让 DSH 设置页能把它**内联**渲染。
|
|
32865
|
+
*
|
|
32866
|
+
* 早先的版本做不到这一点,于是设置页里只放了一句说明加一个「打开手机设置面板」按钮,
|
|
32867
|
+
* 真正的设置藏在按钮后面的模态框里。用户的反应很直接:「其他的可以设置的东西那去了」——
|
|
32868
|
+
* 这就是把弹窗嵌进设置页的代价:入口看起来是空的,因为内容在下一层。
|
|
32869
|
+
* 设置页本身就是一层容器,没有任何理由再套一层。
|
|
32870
|
+
*
|
|
32871
|
+
* `section` / `onSection` 保留是为了兼容旧调用方,但现在**不再决定显示哪一张卡**,
|
|
32872
|
+
* 只用来决定默认展开哪一张。
|
|
32589
32873
|
*/
|
|
32590
|
-
function
|
|
32591
|
-
|
|
32592
|
-
|
|
32874
|
+
function PhoneSettingsBody(props) {
|
|
32875
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AdminPanel, {
|
|
32876
|
+
api: props.api,
|
|
32877
|
+
initialOpen: props.section,
|
|
32878
|
+
onOpenPluginList: props.onOpenPluginList
|
|
32879
|
+
});
|
|
32593
32880
|
}
|
|
32594
32881
|
/**
|
|
32595
|
-
*
|
|
32596
|
-
*
|
|
32597
|
-
|
|
32598
|
-
|
|
32599
|
-
*
|
|
32882
|
+
* The settings content of one admin section, extracted from `AdminOverlay` so the unified panel and
|
|
32883
|
+
* the standalone `/dsh-workspace` page render exactly the same body.
|
|
32884
|
+
*/
|
|
32885
|
+
/**
|
|
32886
|
+
* 设置面板:卡片平铺,各自独立开合。
|
|
32600
32887
|
*
|
|
32601
|
-
*
|
|
32602
|
-
*
|
|
32888
|
+
* `initialOpen` 决定进页面时先展开哪一张(默认第一张),之后完全由用户点卡片头控制。
|
|
32889
|
+
* 展开状态存 Set —— 这样「同时展开两张」是自然结果,不需要额外的互斥逻辑。
|
|
32603
32890
|
*/
|
|
32604
|
-
function
|
|
32605
|
-
|
|
32606
|
-
|
|
32607
|
-
|
|
32608
|
-
|
|
32609
|
-
|
|
32610
|
-
|
|
32611
|
-
|
|
32612
|
-
|
|
32613
|
-
|
|
32614
|
-
|
|
32615
|
-
|
|
32616
|
-
|
|
32891
|
+
function AdminPanel(props) {
|
|
32892
|
+
const { locale, t } = useWorkspaceI18n();
|
|
32893
|
+
const [status, setStatus] = (0, react.useState)();
|
|
32894
|
+
const [configText, setConfigText] = (0, react.useState)();
|
|
32895
|
+
const [configEndpoint, setConfigEndpoint] = (0, react.useState)("");
|
|
32896
|
+
const [configTextBusy, setConfigTextBusy] = (0, react.useState)(false);
|
|
32897
|
+
const [openCards, setOpenCards] = (0, react.useState)(() => /* @__PURE__ */ new Set([props.initialOpen]));
|
|
32898
|
+
const [advancedOpen, setAdvancedOpen] = (0, react.useState)(() => ADVANCED_TABS.includes(props.initialOpen));
|
|
32899
|
+
const [error, setError] = (0, react.useState)();
|
|
32900
|
+
const [notice, setNotice] = (0, react.useState)();
|
|
32901
|
+
const [tunnel, setTunnel] = (0, react.useState)();
|
|
32902
|
+
const [tunnelBusy, setTunnelBusy] = (0, react.useState)(false);
|
|
32903
|
+
const [activity, setActivity] = (0, react.useState)();
|
|
32904
|
+
const notify = (0, react.useCallback)((key, params) => {
|
|
32905
|
+
setNotice(params === void 0 ? {
|
|
32906
|
+
id: Date.now() + Math.random(),
|
|
32907
|
+
key
|
|
32908
|
+
} : {
|
|
32909
|
+
id: Date.now() + Math.random(),
|
|
32910
|
+
key,
|
|
32911
|
+
params
|
|
32912
|
+
});
|
|
32913
|
+
}, []);
|
|
32914
|
+
const copyText = (0, react.useCallback)(async (text) => {
|
|
32915
|
+
const clipboard = globalThis.navigator?.clipboard;
|
|
32916
|
+
if (clipboard !== void 0) try {
|
|
32917
|
+
await clipboard.writeText(text);
|
|
32918
|
+
return "done";
|
|
32919
|
+
} catch {}
|
|
32920
|
+
try {
|
|
32921
|
+
const node = document.createElement("textarea");
|
|
32922
|
+
node.value = text;
|
|
32923
|
+
node.style.position = "fixed";
|
|
32924
|
+
node.style.top = "-9999px";
|
|
32925
|
+
node.setAttribute("readonly", "");
|
|
32926
|
+
document.body.append(node);
|
|
32927
|
+
node.select();
|
|
32928
|
+
const ok = document.execCommand("copy");
|
|
32929
|
+
node.remove();
|
|
32930
|
+
if (ok) return "done";
|
|
32931
|
+
} catch {}
|
|
32932
|
+
return "manual";
|
|
32933
|
+
}, []);
|
|
32934
|
+
const isOpen = (0, react.useCallback)((card) => openCards.has(card), [openCards]);
|
|
32935
|
+
const toggleCard = (0, react.useCallback)((card) => {
|
|
32936
|
+
setOpenCards((previous) => {
|
|
32937
|
+
const next = new Set(previous);
|
|
32938
|
+
if (!next.delete(card)) next.add(card);
|
|
32939
|
+
return next;
|
|
32940
|
+
});
|
|
32941
|
+
}, []);
|
|
32942
|
+
const refresh = (0, react.useCallback)(async () => {
|
|
32943
|
+
try {
|
|
32944
|
+
setTunnel(await props.api.tunnelStatus());
|
|
32945
|
+
} catch {}
|
|
32946
|
+
try {
|
|
32947
|
+
const next = await props.api.status();
|
|
32948
|
+
setStatus(next);
|
|
32949
|
+
} catch (cause) {
|
|
32950
|
+
if (!(cause instanceof WorkspaceApiError && cause.status === 403 && (cause.code === "TUNNEL_MODE_ADMIN_DISABLED" || cause.code === "ADMIN_PROXY_FORWARDED"))) setError(cause);
|
|
32951
|
+
}
|
|
32952
|
+
}, [props.api, openCards]);
|
|
32953
|
+
const tunnelErrorText = (0, react.useCallback)((s) => {
|
|
32954
|
+
if (s.errorKey !== void 0) return t(s.errorKey, s.errorParams ?? {});
|
|
32955
|
+
return s.error ?? t("listenerFailed");
|
|
32956
|
+
}, [t]);
|
|
32957
|
+
const pollActivityOnce = (0, react.useCallback)(async (active) => {
|
|
32958
|
+
while (active()) {
|
|
32617
32959
|
try {
|
|
32618
|
-
|
|
32619
|
-
|
|
32620
|
-
|
|
32621
|
-
}
|
|
32622
|
-
|
|
32623
|
-
|
|
32960
|
+
const a = await props.api.tunnelActivity();
|
|
32961
|
+
if (!active()) return;
|
|
32962
|
+
setActivity(a.stage === "idle" ? void 0 : a);
|
|
32963
|
+
} catch {}
|
|
32964
|
+
await delay(400);
|
|
32965
|
+
}
|
|
32966
|
+
}, [props.api]);
|
|
32967
|
+
const waitForTunnelUrl = (0, react.useCallback)(async (active) => {
|
|
32968
|
+
for (let attempt = 0; attempt < 40 && active(); attempt += 1) {
|
|
32969
|
+
await delay(500);
|
|
32970
|
+
const s = await props.api.tunnelStatus();
|
|
32971
|
+
if (!active()) return null;
|
|
32972
|
+
setTunnel(s);
|
|
32973
|
+
if (s.errorKey !== void 0 || s.error !== void 0) throw new Error(tunnelErrorText(s));
|
|
32974
|
+
if (s.running && s.url !== null) return s.url;
|
|
32975
|
+
}
|
|
32976
|
+
return null;
|
|
32977
|
+
}, [props.api, tunnelErrorText]);
|
|
32978
|
+
const generateConfigForUrl = (0, react.useCallback)(async (baseUrl, noticeKey = "configCreated") => {
|
|
32979
|
+
setConfigTextBusy(true);
|
|
32980
|
+
setError(void 0);
|
|
32981
|
+
try {
|
|
32982
|
+
const currentRoots = status?.roots ?? [];
|
|
32983
|
+
const value = await props.api.createConfigText({
|
|
32984
|
+
...currentRoots.length === 0 ? {} : { rootIds: currentRoots.map((root) => root.id) },
|
|
32985
|
+
endpoints: [{
|
|
32986
|
+
label: t("tunnelEndpointLabel"),
|
|
32987
|
+
baseUrl
|
|
32988
|
+
}]
|
|
32989
|
+
});
|
|
32990
|
+
setConfigText(value);
|
|
32991
|
+
notify(noticeKey);
|
|
32992
|
+
return true;
|
|
32993
|
+
} catch (cause) {
|
|
32994
|
+
setError(cause);
|
|
32995
|
+
return false;
|
|
32996
|
+
} finally {
|
|
32997
|
+
setConfigTextBusy(false);
|
|
32998
|
+
}
|
|
32999
|
+
}, [
|
|
33000
|
+
props.api,
|
|
33001
|
+
status,
|
|
33002
|
+
t,
|
|
33003
|
+
notify
|
|
33004
|
+
]);
|
|
33005
|
+
const createTunnel = (0, react.useCallback)(async (opts = {}) => {
|
|
33006
|
+
if (!window.confirm(t("tunnelStartConfirm"))) return;
|
|
33007
|
+
const wantConfig = opts.thenGenConfig === true;
|
|
33008
|
+
let alive = true;
|
|
33009
|
+
setTunnelBusy(true);
|
|
33010
|
+
setError(void 0);
|
|
33011
|
+
setConfigText(void 0);
|
|
33012
|
+
setActivity({
|
|
33013
|
+
stage: "locating",
|
|
33014
|
+
labelKey: "tunnelStageLocating",
|
|
33015
|
+
percent: null
|
|
33016
|
+
});
|
|
33017
|
+
pollActivityOnce(() => alive);
|
|
33018
|
+
try {
|
|
33019
|
+
const currentRoots = status?.roots ?? [];
|
|
33020
|
+
const created = await props.api.tunnelCreate(wantConfig, { ...currentRoots.length === 0 ? {} : { rootIds: currentRoots.map((root) => root.id) } });
|
|
33021
|
+
setTunnel(created);
|
|
33022
|
+
setActivity(void 0);
|
|
33023
|
+
if (created.errorKey !== void 0 || created.error !== void 0) throw new Error(tunnelErrorText(created));
|
|
33024
|
+
if (wantConfig && created.configText !== void 0) {
|
|
33025
|
+
setConfigText(created.configText);
|
|
33026
|
+
await refresh();
|
|
33027
|
+
notify("tunnelConfigAuto");
|
|
33028
|
+
return;
|
|
32624
33029
|
}
|
|
32625
|
-
|
|
32626
|
-
|
|
32627
|
-
|
|
32628
|
-
|
|
32629
|
-
|
|
32630
|
-
|
|
33030
|
+
const url = created.url ?? await waitForTunnelUrl(() => alive);
|
|
33031
|
+
await refresh();
|
|
33032
|
+
if (url === null) {
|
|
33033
|
+
notify("tunnelUrlPending");
|
|
33034
|
+
return;
|
|
33035
|
+
}
|
|
33036
|
+
if (wantConfig) await generateConfigForUrl(url, "tunnelConfigAuto");
|
|
33037
|
+
else notify("tunnelStarted");
|
|
33038
|
+
} catch (cause) {
|
|
33039
|
+
setError(cause);
|
|
33040
|
+
} finally {
|
|
33041
|
+
alive = false;
|
|
33042
|
+
setActivity(void 0);
|
|
33043
|
+
setTunnelBusy(false);
|
|
33044
|
+
}
|
|
33045
|
+
}, [
|
|
33046
|
+
props.api,
|
|
33047
|
+
refresh,
|
|
33048
|
+
status,
|
|
33049
|
+
t,
|
|
33050
|
+
notify,
|
|
33051
|
+
pollActivityOnce,
|
|
33052
|
+
waitForTunnelUrl,
|
|
33053
|
+
tunnelErrorText,
|
|
33054
|
+
generateConfigForUrl
|
|
33055
|
+
]);
|
|
33056
|
+
const stopTunnel = (0, react.useCallback)(async () => {
|
|
33057
|
+
if (!window.confirm(t("tunnelStopConfirm"))) return;
|
|
33058
|
+
setTunnelBusy(true);
|
|
33059
|
+
setError(void 0);
|
|
33060
|
+
try {
|
|
33061
|
+
const next = await props.api.tunnelStop();
|
|
33062
|
+
setTunnel(next);
|
|
33063
|
+
await refresh();
|
|
33064
|
+
notify("tunnelStopped");
|
|
33065
|
+
} catch (cause) {
|
|
33066
|
+
setError(cause);
|
|
33067
|
+
} finally {
|
|
33068
|
+
setTunnelBusy(false);
|
|
33069
|
+
}
|
|
33070
|
+
}, [
|
|
33071
|
+
props.api,
|
|
33072
|
+
refresh,
|
|
33073
|
+
t,
|
|
33074
|
+
notify
|
|
33075
|
+
]);
|
|
33076
|
+
const tunnelConfigClick = (0, react.useCallback)(async () => {
|
|
33077
|
+
if (tunnel?.running === true) {
|
|
33078
|
+
if (!window.confirm(t("tunnelStopConfirm"))) return;
|
|
33079
|
+
setTunnelBusy(true);
|
|
32631
33080
|
setError(void 0);
|
|
32632
33081
|
try {
|
|
32633
|
-
await api.
|
|
32634
|
-
|
|
32635
|
-
|
|
33082
|
+
const stopped = await props.api.tunnelStop();
|
|
33083
|
+
setTunnel(stopped);
|
|
33084
|
+
await refresh();
|
|
32636
33085
|
} catch (cause) {
|
|
32637
|
-
setError(
|
|
33086
|
+
setError(cause);
|
|
32638
33087
|
} finally {
|
|
32639
|
-
|
|
33088
|
+
setTunnelBusy(false);
|
|
32640
33089
|
}
|
|
32641
|
-
|
|
32642
|
-
|
|
32643
|
-
|
|
33090
|
+
await createTunnel({ thenGenConfig: true });
|
|
33091
|
+
} else await createTunnel({ thenGenConfig: true });
|
|
33092
|
+
}, [
|
|
33093
|
+
props.api,
|
|
33094
|
+
tunnel,
|
|
33095
|
+
t,
|
|
33096
|
+
createTunnel,
|
|
33097
|
+
refresh,
|
|
33098
|
+
setError
|
|
33099
|
+
]);
|
|
33100
|
+
(0, react.useEffect)(() => {
|
|
33101
|
+
refresh();
|
|
33102
|
+
}, [refresh]);
|
|
33103
|
+
const roots = status?.roots ?? [];
|
|
33104
|
+
const managementLocked = tunnel?.running === true;
|
|
33105
|
+
const cards = {
|
|
33106
|
+
api: props.api,
|
|
33107
|
+
t,
|
|
33108
|
+
locale,
|
|
33109
|
+
status,
|
|
33110
|
+
roots,
|
|
33111
|
+
managementLocked,
|
|
33112
|
+
refresh,
|
|
33113
|
+
notify,
|
|
33114
|
+
copyText,
|
|
33115
|
+
error,
|
|
33116
|
+
setError,
|
|
33117
|
+
setStatus,
|
|
33118
|
+
tunnel,
|
|
33119
|
+
tunnelBusy,
|
|
33120
|
+
activity,
|
|
33121
|
+
configText,
|
|
33122
|
+
configTextBusy,
|
|
33123
|
+
setConfigText,
|
|
33124
|
+
setConfigTextBusy,
|
|
33125
|
+
configEndpoint,
|
|
33126
|
+
setConfigEndpoint,
|
|
33127
|
+
createTunnel,
|
|
33128
|
+
stopTunnel,
|
|
33129
|
+
tunnelConfigClick,
|
|
33130
|
+
generateConfigForUrl
|
|
33131
|
+
};
|
|
33132
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardsProvider, {
|
|
33133
|
+
value: cards,
|
|
33134
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("main", {
|
|
33135
|
+
className: "daw-admin",
|
|
32644
33136
|
children: [
|
|
32645
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
32646
|
-
className: "daw-
|
|
32647
|
-
children:
|
|
33137
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
33138
|
+
className: "daw-admin-topbar",
|
|
33139
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LanguageToggle, {})
|
|
32648
33140
|
}),
|
|
32649
33141
|
error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
32650
33142
|
className: "daw-warning",
|
|
32651
33143
|
role: "alert",
|
|
32652
|
-
children: error
|
|
33144
|
+
children: messageOf(error, t)
|
|
32653
33145
|
}),
|
|
32654
|
-
|
|
33146
|
+
notice !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
32655
33147
|
className: "daw-success",
|
|
32656
33148
|
role: "status",
|
|
32657
|
-
children: t(
|
|
33149
|
+
children: t(notice.key, notice.params)
|
|
32658
33150
|
}),
|
|
32659
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
32660
|
-
className: "daw-
|
|
32661
|
-
|
|
32662
|
-
|
|
32663
|
-
className: "daw-command",
|
|
32664
|
-
disabled: busy,
|
|
32665
|
-
onClick: () => {
|
|
32666
|
-
load(false);
|
|
32667
|
-
},
|
|
32668
|
-
children: [
|
|
32669
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.RefreshCw, { size: 14 }),
|
|
32670
|
-
" ",
|
|
32671
|
-
busy ? t("loading") : t("refresh")
|
|
32672
|
-
]
|
|
32673
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
32674
|
-
type: "button",
|
|
32675
|
-
className: "daw-command",
|
|
32676
|
-
disabled: checking,
|
|
32677
|
-
onClick: () => {
|
|
32678
|
-
load(true);
|
|
32679
|
-
},
|
|
32680
|
-
children: checking ? t("pluginsChecking") : t("pluginsCheckUpdates")
|
|
32681
|
-
})]
|
|
33151
|
+
managementLocked && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
33152
|
+
className: "daw-warning",
|
|
33153
|
+
role: "status",
|
|
33154
|
+
children: t("managementLockedWhileTunnel")
|
|
32682
33155
|
}),
|
|
32683
|
-
|
|
32684
|
-
|
|
32685
|
-
|
|
32686
|
-
})
|
|
32687
|
-
|
|
32688
|
-
|
|
32689
|
-
|
|
32690
|
-
|
|
33156
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RemoteCard, {
|
|
33157
|
+
open: isOpen("remote"),
|
|
33158
|
+
onToggle: () => toggleCard("remote")
|
|
33159
|
+
}),
|
|
33160
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TunnelCard, {
|
|
33161
|
+
open: isOpen("tunnel"),
|
|
33162
|
+
onToggle: () => toggleCard("tunnel")
|
|
33163
|
+
}),
|
|
33164
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DevicesCard, {
|
|
33165
|
+
open: isOpen("devices"),
|
|
33166
|
+
onToggle: () => toggleCard("devices")
|
|
33167
|
+
}),
|
|
33168
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TrashCard, {
|
|
33169
|
+
open: isOpen("trash"),
|
|
33170
|
+
onToggle: () => toggleCard("trash")
|
|
33171
|
+
}),
|
|
33172
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppDownloadCard, {
|
|
33173
|
+
open: isOpen("app-download"),
|
|
33174
|
+
onToggle: () => toggleCard("app-download")
|
|
33175
|
+
}),
|
|
33176
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(AdvancedGroup, {
|
|
33177
|
+
open: advancedOpen,
|
|
33178
|
+
onToggle: () => setAdvancedOpen((v) => !v),
|
|
32691
33179
|
children: [
|
|
32692
|
-
|
|
32693
|
-
|
|
32694
|
-
|
|
32695
|
-
|
|
32696
|
-
|
|
33180
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RootsCard, {
|
|
33181
|
+
open: isOpen("roots"),
|
|
33182
|
+
onToggle: () => toggleCard("roots")
|
|
33183
|
+
}),
|
|
33184
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageCard, {
|
|
33185
|
+
open: isOpen("image"),
|
|
33186
|
+
onToggle: () => toggleCard("image")
|
|
33187
|
+
}),
|
|
33188
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PluginsCard, {
|
|
33189
|
+
open: isOpen("plugins"),
|
|
33190
|
+
onToggle: () => toggleCard("plugins"),
|
|
33191
|
+
onOpenPluginList: props.onOpenPluginList
|
|
33192
|
+
})
|
|
32697
33193
|
]
|
|
32698
|
-
})
|
|
32699
|
-
className: "daw-list-meta daw-empty",
|
|
32700
|
-
children: t("pluginsEmpty")
|
|
32701
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
32702
|
-
className: "daw-list",
|
|
32703
|
-
children: inventory.items.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
32704
|
-
className: "daw-list-row",
|
|
32705
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
|
|
32706
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
32707
|
-
className: "daw-list-title",
|
|
32708
|
-
children: [entry.name, entry.installed !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
32709
|
-
className: "daw-list-meta",
|
|
32710
|
-
children: [" · ", entry.installed]
|
|
32711
|
-
})]
|
|
32712
|
-
}),
|
|
32713
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
32714
|
-
className: "daw-list-meta",
|
|
32715
|
-
children: entry.description ?? t("pluginsNoDescription")
|
|
32716
|
-
}),
|
|
32717
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
32718
|
-
className: "daw-list-meta",
|
|
32719
|
-
children: [
|
|
32720
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
32721
|
-
className: `daw-pill${entry.loaded ? " on" : ""}`,
|
|
32722
|
-
children: t(pluginStateKey(entry.state))
|
|
32723
|
-
}),
|
|
32724
|
-
entry.updateAvailable === true && entry.latestVersion !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [" ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
32725
|
-
className: "daw-pill",
|
|
32726
|
-
children: t("pluginsUpdateAvailable", { version: entry.latestVersion })
|
|
32727
|
-
})] }),
|
|
32728
|
-
entry.updateAvailable === false && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [" · ", t("pluginsLatest")] })
|
|
32729
|
-
]
|
|
32730
|
-
})
|
|
32731
|
-
] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: entry.toggleable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
32732
|
-
type: "button",
|
|
32733
|
-
className: "daw-command",
|
|
32734
|
-
disabled: toggling === entry.name,
|
|
32735
|
-
onClick: () => {
|
|
32736
|
-
toggle(entry);
|
|
32737
|
-
},
|
|
32738
|
-
children: entry.loaded ? t("pluginsDisable") : t("pluginsEnable")
|
|
32739
|
-
}) })]
|
|
32740
|
-
}, entry.name))
|
|
32741
|
-
})] })
|
|
33194
|
+
})
|
|
32742
33195
|
]
|
|
32743
|
-
})
|
|
32744
|
-
};
|
|
32745
|
-
}
|
|
32746
|
-
//#endregion
|
|
32747
|
-
//#region src/client/styles.ts
|
|
32748
|
-
const STYLE_ID = "dsh-workspace-style";
|
|
32749
|
-
/**
|
|
32750
|
-
* 插件的全部样式。
|
|
32751
|
-
*
|
|
32752
|
-
* 导出出来是为了让测试能守住一条容易失守的不变量:**CSS 变量块必须同时覆盖
|
|
32753
|
-
* `.daw-root` 与 `.daw-settings-section`**。
|
|
32754
|
-
*
|
|
32755
|
-
* 这条防线来自一个真实发生过的隐形 bug:设置页内联渲染时,那一节没有 `.daw-root` 祖先,
|
|
32756
|
-
* 于是 `var(--daw-accent)` 解析失败 → `.daw-command.primary` 的 `background` 失效变透明、
|
|
32757
|
-
* 而 `color` 仍是 `#fff` → **白字落在白底上,按钮彻底看不见**。
|
|
32758
|
-
* 类型检查、单测、构建全都发现不了,只有肉眼看渲染或量计算样式才抓得到。
|
|
32759
|
-
*/
|
|
32760
|
-
const WORKSPACE_CSS = `
|
|
32761
|
-
.daw-root,.daw-settings-section,.daw-plugins-tab{--daw-bg:var(--dsw-alias-bg-base,var(--dsw-alias-bg-layer-2,#f7f8fa));--daw-panel:var(--dsw-alias-bg-layer-3,var(--dsw-alias-bg-layer-2,#fff));--daw-ink:var(--dsw-alias-label-primary,#18202a);--daw-muted:var(--dsw-alias-label-tertiary,#687381);--daw-line:var(--dsw-alias-border-l2,#dfe4e8);--daw-accent:var(--dsw-alias-brand-primary,#087f8c);--daw-accent-soft:var(--dsw-alias-bg-module-platform,#e5f5f6);--daw-good:var(--dsw-alias-state-success-primary,#2f7d4a);--daw-danger:var(--dsw-alias-state-error-primary,#b33a3a);color:var(--daw-ink);background:transparent;font:13px/1.45 Inter,ui-sans-serif,system-ui,sans-serif;letter-spacing:0;min-width:0}
|
|
32762
|
-
.daw-root *,.daw-settings-section *,.daw-plugins-tab *{box-sizing:border-box;letter-spacing:0}
|
|
32763
|
-
/* 输入控件基线(UI-04):此前只有 :focus-visible 一条,非聚焦态全靠浏览器默认(2px inset/Arial/灰底),
|
|
32764
|
-
深色令牌下实测白底黑字与面板割裂。这里给齐边框/底/字/内边距,聚焦态只换描边。 */
|
|
32765
|
-
.daw-input{width:100%;min-width:0;height:30px;padding:4px 9px;border:1px solid var(--dsw-alias-border-l2,var(--daw-line));border-radius:6px;background:var(--dsw-alias-bg-layer-3,var(--daw-panel));color:var(--dsw-alias-label-primary,var(--daw-ink));font:inherit}
|
|
32766
|
-
.daw-input::placeholder{color:var(--dsw-alias-label-tertiary,var(--daw-muted))}
|
|
32767
|
-
.daw-input:focus-visible{border-color:var(--dsw-alias-brand-primary,var(--daw-accent));outline:none}
|
|
32768
|
-
.daw-input:disabled{opacity:.55;cursor:not-allowed}.daw-icon{width:30px;padding:0}
|
|
32769
|
-
.daw-icon:hover{background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft))}
|
|
32770
|
-
.daw-icon.danger{color:var(--daw-danger);border-color:transparent;background:transparent}
|
|
32771
|
-
.daw-icon:disabled{opacity:.45;cursor:not-allowed}
|
|
32772
|
-
/* 普通命令按钮基线(UI-04):旧版写死 background:#fff/border:#cbd2d8,深色令牌下成了白块黑字。
|
|
32773
|
-
全部改走 DSH 令牌;.primary 沿用面板底色做次按钮、真正的主操作由调用方加 .primary 配合。 */
|
|
32774
|
-
.daw-command{height:30px;padding:0 12px;border-radius:6px;border:1px solid var(--dsw-alias-border-l2,var(--daw-line));background:var(--dsw-alias-bg-layer-3,var(--daw-panel));color:var(--dsw-alias-label-primary,var(--daw-ink));font:inherit;cursor:pointer}
|
|
32775
|
-
.daw-command:hover:not(:disabled){border-color:var(--dsw-alias-label-dimmed,var(--daw-line))}
|
|
32776
|
-
.daw-command:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,var(--daw-accent));outline-offset:1px}
|
|
32777
|
-
/* 禁用态(UI-05):此前无任何 :disabled 样式,只剩浏览器默认的 30% 文字且仍是箭头手型。 */
|
|
32778
|
-
.daw-command:disabled{opacity:.5;cursor:not-allowed}
|
|
32779
|
-
.daw-command.primary{color:var(--dsw-alias-label-primary,var(--daw-ink));background:var(--dsw-alias-bg-module-platform,var(--daw-panel));border-color:var(--dsw-alias-border-l2,var(--daw-line));font-weight:500}
|
|
32780
|
-
.daw-command.primary:hover:not(:disabled){background:var(--dsw-alias-bg-layer-2,var(--daw-panel));border-color:var(--dsw-alias-label-dimmed,var(--daw-line))}
|
|
32781
|
-
.daw-command.danger{color:var(--daw-danger)}
|
|
32782
|
-
.daw-command.danger:hover:not(:disabled){border-color:var(--daw-danger)}
|
|
32783
|
-
/* 图标+文字的小复制按钮:图标和文字对齐,窄屏只露图标也不改变按钮高度。 */
|
|
32784
|
-
.daw-command.icon-as-command{display:inline-flex;align-items:center;gap:6px;padding:0 10px;white-space:nowrap}
|
|
32785
|
-
.daw-copy-label{font-size:12px}
|
|
32786
|
-
.daw-empty{padding:14px 2px;text-align:center}
|
|
32787
|
-
.daw-admin{overflow:auto;padding:18px;display:flex;flex-direction:column;gap:10px}.daw-admin h3{font-size:14px;margin:0 0 12px}
|
|
32788
|
-
.daw-card{border:1px solid var(--dsw-alias-border-l2,var(--daw-line));background:var(--dsw-alias-bg-layer-3,var(--daw-panel));border-radius:12px;list-style:none;transition:border-color .16s,background .16s}
|
|
32789
|
-
.daw-card:hover{border-color:var(--dsw-alias-label-dimmed,var(--daw-line))}
|
|
32790
|
-
.daw-card-open{background:var(--dsw-alias-bg-layer-2,var(--daw-panel));border-color:var(--dsw-alias-label-dimmed,var(--daw-line))}
|
|
32791
|
-
.daw-card-header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}
|
|
32792
|
-
.daw-card-header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,var(--daw-accent));outline-offset:-2px}
|
|
32793
|
-
.daw-card-icon{color:var(--dsw-alias-label-tertiary,var(--daw-muted));flex:none;display:flex;align-items:center}
|
|
32794
|
-
.daw-card-headtext{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}
|
|
32795
|
-
.daw-card-name{color:var(--dsw-alias-label-primary,var(--daw-ink));font-size:15px;font-weight:600;line-height:1.4}
|
|
32796
|
-
.daw-card-desc{color:var(--dsw-alias-label-tertiary,var(--daw-muted));font-size:13px;line-height:1.5}
|
|
32797
|
-
.daw-card-chevron{color:var(--dsw-alias-label-tertiary,var(--daw-muted));flex:none;transition:transform .16s;font-size:12px;line-height:1}
|
|
32798
|
-
.daw-card-chevron-open{transform:rotate(180deg)}
|
|
32799
|
-
.daw-card-body{border-top:1px solid var(--dsw-alias-border-l2,var(--daw-line));margin:0 16px;padding-bottom:8px}.daw-settings-section{display:grid;grid-template-rows:auto minmax(0,1fr);gap:10px;min-width:0;min-height:0}.daw-plugins-tab{overflow:auto;padding:14px 16px;display:flex;flex-direction:column;gap:10px;min-width:0;min-height:0}.daw-settings-section>.daw-list-meta{margin:0}.daw-settings-section .daw-admin{padding:14px 2px}.daw-qr-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px;margin-top:12px}.daw-qr-card{margin:0;display:grid;justify-items:center;gap:8px;padding:16px;border:1px solid var(--daw-line);border-radius:6px;background:var(--daw-panel)}.daw-qr-image{width:180px;height:180px;background:var(--dsw-alias-bg-layer-3,#fff);border-radius:4px}.daw-qr-site{font-weight:600}.daw-qr-url{font:12px ui-monospace,SFMono-Regular,Consolas,monospace;color:var(--daw-accent);text-decoration:none;overflow-wrap:anywhere;text-align:center}.daw-qr-url:hover{text-decoration:underline}.daw-form-row{display:grid;grid-template-columns:minmax(0,1fr) minmax(120px,.45fr) auto;gap:8px;margin-bottom:14px}.daw-listener-form{display:grid;grid-template-columns:minmax(180px,1fr) 130px auto;gap:8px;align-items:end}.daw-field{display:grid;gap:5px;color:var(--dsw-alias-label-secondary,var(--daw-muted));font-size:12px}.daw-listener-status{display:flex;flex-wrap:wrap;gap:5px 18px;padding:9px 0;color:var(--daw-muted);font:12px ui-monospace,SFMono-Regular,Consolas,monospace}.daw-listener-hint{margin:0 0 8px}.daw-list{border-top:1px solid var(--daw-line)}.daw-list-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:12px;align-items:center;min-height:48px;border-bottom:1px solid var(--daw-line);padding:7px 2px}.daw-list-title{font-weight:600;overflow-wrap:anywhere}
|
|
32800
|
-
.daw-list-title .daw-pill{margin-left:8px;vertical-align:middle}
|
|
32801
|
-
.daw-builtin-hint{text-align:right;align-self:center}
|
|
32802
|
-
.daw-builtin-notice{margin-top:10px;padding:8px 10px;border-radius:8px;background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft))}.daw-list-meta{font-size:12px;color:var(--daw-muted);overflow-wrap:anywhere}.daw-scope-grid{display:flex;flex-wrap:wrap;gap:6px 12px;padding:8px 0}.daw-check{display:inline-flex;align-items:center;gap:5px;color:var(--dsw-alias-label-secondary,var(--daw-muted))}.daw-code{font:600 22px ui-monospace,SFMono-Regular,Consolas,monospace;letter-spacing:2px!important;padding:14px;border:1px solid var(--dsw-alias-border-l2,var(--daw-line));background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));border-radius:8px;text-align:center;margin:12px 0}.daw-warning{flex:none;border:1px solid var(--dsw-alias-border-l2,var(--daw-line));border-radius:8px;background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));padding:9px 11px;color:var(--daw-muted);margin-bottom:0}.daw-pill{display:inline-flex;align-items:center;gap:5px;border-radius:999px;padding:1px 8px;background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));color:var(--dsw-alias-label-secondary,var(--daw-muted));font-size:11px;font-weight:500;line-height:17px}.daw-pill.on{background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));color:var(--dsw-alias-state-success-primary,var(--daw-good))}.daw-tunnel{margin-top:14px;border-top:1px dashed var(--dsw-alias-border-l2,var(--daw-line));padding-top:12px}.daw-tunnel-head{display:flex;align-items:center;gap:8px;justify-content:space-between}.daw-tunnel-url{font:500 13px ui-monospace,SFMono-Regular,Consolas,monospace;overflow-wrap:anywhere;padding:9px 11px;border:1px solid var(--dsw-alias-border-l2,var(--daw-line));background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));border-radius:8px}.daw-tunnel-progress{max-width:360px}.daw-progress-track{height:6px;border-radius:999px;background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));overflow:hidden}.daw-progress-fill{height:100%;border-radius:999px;background:var(--dsw-alias-state-info-primary,var(--daw-accent,var(--daw-line)));transition:width .25s ease}.daw-inline-command{margin-left:8px}
|
|
32803
|
-
/* 成功反馈条(UI-07):与 .daw-warning 同形,但走成功色,操作成功不再静默。 */
|
|
32804
|
-
.daw-success{flex:none;border:1px solid var(--dsw-alias-state-success-primary,var(--daw-good));border-radius:8px;background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));padding:9px 11px;color:var(--dsw-alias-state-success-primary,var(--daw-good));margin-bottom:0}
|
|
32805
|
-
.daw-admin-topbar{display:flex;justify-content:flex-end;margin-bottom:8px}
|
|
32806
|
-
.daw-url-row{display:flex;align-items:center;gap:8px;margin-top:8px}.daw-url-row .daw-tunnel-url{flex:1 1 auto;margin:0;min-width:0}
|
|
32807
|
-
.daw-code-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:12px 0}.daw-code-row .daw-code{margin:0;flex:1 1 220px;min-width:0}.daw-copy-inline{flex:none}
|
|
32808
|
-
/* 隧道/监听进行中的流动条(UI-08):percent=null 时铺一段高光在轨道里循环滑动,
|
|
32809
|
-
不再把「未知进度」伪装成恒定 30%。 */
|
|
32810
|
-
.daw-progress-fill.indeterminate{width:40%;background:linear-gradient(90deg,transparent,var(--dsw-alias-state-info-primary,var(--daw-accent,var(--daw-line))),transparent);animation:daw-indeterminate 1.1s ease-in-out infinite}
|
|
32811
|
-
@keyframes daw-indeterminate{0%{transform:translateX(-120%)}100%{transform:translateX(280%)}}
|
|
32812
|
-
@media (max-width:720px){.daw-form-row,.daw-listener-form{grid-template-columns:1fr}.daw-admin{padding:12px}.daw-qr-grid{grid-template-columns:1fr}.daw-qr-image{width:150px;height:150px}.daw-inline-command{margin:8px 0 0}}
|
|
32813
|
-
`;
|
|
32814
|
-
function installWorkspaceStyles() {
|
|
32815
|
-
if (document.getElementById(STYLE_ID) !== null) return;
|
|
32816
|
-
const style = document.createElement("style");
|
|
32817
|
-
style.id = STYLE_ID;
|
|
32818
|
-
style.dataset.plugin = "dsh-workspace";
|
|
32819
|
-
style.textContent = WORKSPACE_CSS;
|
|
32820
|
-
document.head.appendChild(style);
|
|
33196
|
+
})
|
|
33197
|
+
});
|
|
32821
33198
|
}
|
|
32822
33199
|
//#endregion
|
|
32823
33200
|
//#region src/client/index.tsx
|