@xulongzhe/clawbench-desktop-linux-x64-payload 0.104.0 → 0.106.0
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/package.json +1 -1
- package/resources/app.asar +0 -0
- package/resources/login.html +467 -137
- package/resources/url-utils.js +58 -0
package/package.json
CHANGED
package/resources/app.asar
CHANGED
|
Binary file
|
package/resources/login.html
CHANGED
|
@@ -13,9 +13,10 @@
|
|
|
13
13
|
password_label: 'Password',
|
|
14
14
|
password_placeholder: 'Password',
|
|
15
15
|
connect: 'Connect',
|
|
16
|
-
protocol: 'Protocol',
|
|
17
16
|
server_address: 'Server Address',
|
|
18
|
-
|
|
17
|
+
add: 'Add',
|
|
18
|
+
save: 'Save',
|
|
19
|
+
edit_server: 'Edit Server',
|
|
19
20
|
cancel: 'Cancel',
|
|
20
21
|
add_server: 'Add Server',
|
|
21
22
|
confirm: 'Confirm',
|
|
@@ -24,8 +25,13 @@
|
|
|
24
25
|
delete_server_msg: 'Delete this server?',
|
|
25
26
|
saved_servers: 'Saved Servers',
|
|
26
27
|
authenticating: 'Authenticating...',
|
|
28
|
+
saving: 'Saving...',
|
|
29
|
+
name_label: 'Name (optional)',
|
|
30
|
+
name_placeholder: 'e.g. Home NAS',
|
|
31
|
+
duplicate_name: 'That name is already used',
|
|
27
32
|
enter_password: 'Please enter password',
|
|
28
|
-
|
|
33
|
+
enter_full_address: 'Enter a full address with http:// or https:// and a port, e.g. https://192.168.1.100:20000',
|
|
34
|
+
duplicate_server: 'That address is already saved',
|
|
29
35
|
connection_failed: 'Connection failed: ',
|
|
30
36
|
connect_error: 'Unable to connect. Check address and network.',
|
|
31
37
|
wrong_password: 'Wrong password',
|
|
@@ -36,7 +42,17 @@
|
|
|
36
42
|
splash_loading: 'Loading page…',
|
|
37
43
|
splash_rendering: 'Rendering…',
|
|
38
44
|
splash_initializing: 'Initializing app…',
|
|
39
|
-
splash_cancel: 'Cancel connection'
|
|
45
|
+
splash_cancel: 'Cancel connection',
|
|
46
|
+
window_close: 'Close',
|
|
47
|
+
// Version-mismatch gate. The desktop and server versions differ in either
|
|
48
|
+
// direction, so the wording must not assume the app is the older side —
|
|
49
|
+
// the two `_older`/`_newer` variants cover that.
|
|
50
|
+
version_mismatch_title: 'Version mismatch',
|
|
51
|
+
version_mismatch_body: 'Desktop version: %1\nServer version: %2\n\nThe two versions may be incompatible: some features may behave unexpectedly or stop working.',
|
|
52
|
+
version_mismatch_older: 'The desktop app is older than the server. Updating is recommended.',
|
|
53
|
+
version_mismatch_newer: 'The desktop app is newer than the server. Downloading will align it to the server version (a downgrade).',
|
|
54
|
+
version_mismatch_download: 'Download',
|
|
55
|
+
version_mismatch_continue: 'Continue anyway'
|
|
40
56
|
},
|
|
41
57
|
zh: {
|
|
42
58
|
slogan: '从终端到桌面',
|
|
@@ -44,9 +60,10 @@
|
|
|
44
60
|
password_label: '登录密码',
|
|
45
61
|
password_placeholder: '密码',
|
|
46
62
|
connect: '连 接',
|
|
47
|
-
protocol: '协议',
|
|
48
63
|
server_address: '服务器地址',
|
|
49
|
-
|
|
64
|
+
add: '添 加',
|
|
65
|
+
save: '保 存',
|
|
66
|
+
edit_server: '编辑服务器',
|
|
50
67
|
cancel: '取 消',
|
|
51
68
|
add_server: '添加服务器',
|
|
52
69
|
confirm: '确认',
|
|
@@ -55,8 +72,13 @@
|
|
|
55
72
|
delete_server_msg: '删除此服务器?',
|
|
56
73
|
saved_servers: '已保存服务器',
|
|
57
74
|
authenticating: '认证中...',
|
|
75
|
+
saving: '保存中...',
|
|
76
|
+
name_label: '名称(选填)',
|
|
77
|
+
name_placeholder: '例如 家里的 NAS',
|
|
78
|
+
duplicate_name: '该名称已被使用',
|
|
58
79
|
enter_password: '请输入密码',
|
|
59
|
-
|
|
80
|
+
enter_full_address: '请输入完整地址,包含 http:// 或 https:// 与端口,例如 https://192.168.1.100:20000',
|
|
81
|
+
duplicate_server: '该地址已存在',
|
|
60
82
|
connection_failed: '连接失败: ',
|
|
61
83
|
connect_error: '无法连接到服务器,请检查地址和网络。',
|
|
62
84
|
wrong_password: '密码错误',
|
|
@@ -67,7 +89,16 @@
|
|
|
67
89
|
splash_loading: '正在加载页面…',
|
|
68
90
|
splash_rendering: '正在渲染…',
|
|
69
91
|
splash_initializing: '正在初始化应用…',
|
|
70
|
-
splash_cancel: '取消连接'
|
|
92
|
+
splash_cancel: '取消连接',
|
|
93
|
+
window_close: '关闭',
|
|
94
|
+
// Version-mismatch gate. See the en block for why the wording is split by
|
|
95
|
+
// direction rather than assuming the app is older.
|
|
96
|
+
version_mismatch_title: '版本不一致',
|
|
97
|
+
version_mismatch_body: '桌面端版本: %1\n服务器版本: %2\n\n两者可能不兼容:部分功能可能异常或无法使用。',
|
|
98
|
+
version_mismatch_older: '桌面端版本低于服务器,建议更新。',
|
|
99
|
+
version_mismatch_newer: '桌面端版本高于服务器,下载将对齐到服务器版本(降级)。',
|
|
100
|
+
version_mismatch_download: '下载',
|
|
101
|
+
version_mismatch_continue: '仍然继续'
|
|
71
102
|
}
|
|
72
103
|
};
|
|
73
104
|
var LANG = 'en';
|
|
@@ -715,6 +746,76 @@
|
|
|
715
746
|
position: relative;
|
|
716
747
|
overflow: hidden;
|
|
717
748
|
-webkit-tap-highlight-color: transparent;
|
|
749
|
+
/* This page IS the window on first run and whenever the server is
|
|
750
|
+
unreachable, and the window is frameless on Windows/Linux — so without a
|
|
751
|
+
drag region the login window could not be moved at all. The whole page is
|
|
752
|
+
the handle (there is no title bar), which is why every interactive
|
|
753
|
+
element below has to opt back out. Ignored on macOS/Android/browser. */
|
|
754
|
+
-webkit-app-region: drag;
|
|
755
|
+
-webkit-user-select: none;
|
|
756
|
+
user-select: none;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/* Opt out of the page-wide drag handle. Without this the inputs and buttons
|
|
760
|
+
would be undraggable-but-unclickable — a click would start a window move.
|
|
761
|
+
`.dlg-box` is included because the confirmation dialog's message text
|
|
762
|
+
should be selectable, and `.server-item` because its rows are clickable. */
|
|
763
|
+
input,
|
|
764
|
+
button,
|
|
765
|
+
select,
|
|
766
|
+
a,
|
|
767
|
+
.form-card,
|
|
768
|
+
.server-list,
|
|
769
|
+
.field-group,
|
|
770
|
+
.server-item,
|
|
771
|
+
.dlg-box,
|
|
772
|
+
.dlg-actions {
|
|
773
|
+
-webkit-app-region: no-drag;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/* Text the user may want to select (server URLs, error messages) opts out of
|
|
777
|
+
both the drag region and the inherited user-select:none. */
|
|
778
|
+
input,
|
|
779
|
+
.server-url,
|
|
780
|
+
.error-message,
|
|
781
|
+
.splash-stage {
|
|
782
|
+
-webkit-user-select: text;
|
|
783
|
+
user-select: text;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/* Window close, for the frameless Windows/Linux shell. Pinned to the window's
|
|
787
|
+
top-right — the same corner the native controls occupy on those platforms. */
|
|
788
|
+
.window-close {
|
|
789
|
+
position: fixed;
|
|
790
|
+
top: 0;
|
|
791
|
+
right: 0;
|
|
792
|
+
width: 46px;
|
|
793
|
+
height: 32px;
|
|
794
|
+
display: flex;
|
|
795
|
+
align-items: center;
|
|
796
|
+
justify-content: center;
|
|
797
|
+
padding: 0;
|
|
798
|
+
border: none;
|
|
799
|
+
border-radius: 0 0 0 6px;
|
|
800
|
+
background: transparent;
|
|
801
|
+
color: var(--text-hint);
|
|
802
|
+
cursor: pointer;
|
|
803
|
+
z-index: 200;
|
|
804
|
+
transition: background 150ms ease, color 150ms ease;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
.window-close svg {
|
|
808
|
+
width: 15px;
|
|
809
|
+
height: 15px;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
.window-close:hover {
|
|
813
|
+
background: var(--color-red, #ef4444);
|
|
814
|
+
color: #fff;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
.window-close[hidden] {
|
|
818
|
+
display: none;
|
|
718
819
|
}
|
|
719
820
|
|
|
720
821
|
/* Decorative background — matches LoginView.vue */
|
|
@@ -922,6 +1023,16 @@
|
|
|
922
1023
|
font-weight: 500;
|
|
923
1024
|
}
|
|
924
1025
|
|
|
1026
|
+
/* Row actions: edit + delete, kept in one group so the info block can
|
|
1027
|
+
stretch without pushing the icons apart. */
|
|
1028
|
+
.server-item-actions {
|
|
1029
|
+
flex-shrink: 0;
|
|
1030
|
+
display: flex;
|
|
1031
|
+
align-items: center;
|
|
1032
|
+
gap: 2px;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
.server-edit-btn,
|
|
925
1036
|
.server-delete-btn {
|
|
926
1037
|
flex-shrink: 0;
|
|
927
1038
|
border: none;
|
|
@@ -936,15 +1047,26 @@
|
|
|
936
1047
|
transition: background 0.1s, color 0.1s;
|
|
937
1048
|
}
|
|
938
1049
|
|
|
1050
|
+
.server-edit-btn:hover {
|
|
1051
|
+
background: rgba(var(--accent-rgb), 0.12);
|
|
1052
|
+
color: var(--accent-color);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
939
1055
|
.server-delete-btn:hover {
|
|
940
1056
|
background: rgba(220, 38, 38, 0.1);
|
|
941
1057
|
color: var(--color-red);
|
|
942
1058
|
}
|
|
943
1059
|
|
|
1060
|
+
.server-item.active .server-edit-btn,
|
|
944
1061
|
.server-item.active .server-delete-btn {
|
|
945
1062
|
color: var(--text-muted);
|
|
946
1063
|
}
|
|
947
1064
|
|
|
1065
|
+
.server-item.active .server-edit-btn:hover {
|
|
1066
|
+
color: var(--accent-color);
|
|
1067
|
+
background: rgba(var(--accent-rgb), 0.15);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
948
1070
|
.server-item.active .server-delete-btn:hover {
|
|
949
1071
|
color: var(--color-red);
|
|
950
1072
|
background: rgba(220, 38, 38, 0.12);
|
|
@@ -965,69 +1087,12 @@
|
|
|
965
1087
|
margin-bottom: 6px;
|
|
966
1088
|
}
|
|
967
1089
|
|
|
968
|
-
/*
|
|
969
|
-
.
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
margin-bottom: 16px;
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
.protocol-group label {
|
|
976
|
-
display: flex;
|
|
977
|
-
align-items: center;
|
|
978
|
-
gap: 6px;
|
|
979
|
-
font-size: 14px;
|
|
1090
|
+
/* Title shown above the add form when it is reused for editing */
|
|
1091
|
+
.form-title {
|
|
1092
|
+
font-size: 15px;
|
|
1093
|
+
font-weight: 600;
|
|
980
1094
|
color: var(--text-primary);
|
|
981
|
-
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
.protocol-group input[type="radio"] {
|
|
985
|
-
appearance: none;
|
|
986
|
-
-webkit-appearance: none;
|
|
987
|
-
width: 18px;
|
|
988
|
-
height: 18px;
|
|
989
|
-
border: 2px solid var(--border-color);
|
|
990
|
-
border-radius: 50%;
|
|
991
|
-
position: relative;
|
|
992
|
-
cursor: pointer;
|
|
993
|
-
transition: border-color 0.2s;
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
.protocol-group input[type="radio"]:checked {
|
|
997
|
-
border-color: var(--accent-color);
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
.protocol-group input[type="radio"]:checked::after {
|
|
1001
|
-
content: '';
|
|
1002
|
-
position: absolute;
|
|
1003
|
-
top: 3px;
|
|
1004
|
-
left: 3px;
|
|
1005
|
-
width: 8px;
|
|
1006
|
-
height: 8px;
|
|
1007
|
-
border-radius: 50%;
|
|
1008
|
-
background: var(--accent-color);
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
/* Host + Port row */
|
|
1012
|
-
.host-row {
|
|
1013
|
-
display: flex;
|
|
1014
|
-
align-items: center;
|
|
1015
|
-
gap: 4px;
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
.host-row input:first-child {
|
|
1019
|
-
flex: 1;
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
.host-row .colon {
|
|
1023
|
-
color: var(--text-muted);
|
|
1024
|
-
font-size: 18px;
|
|
1025
|
-
line-height: 44px;
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
.host-row input:last-child {
|
|
1029
|
-
width: 90px;
|
|
1030
|
-
flex-shrink: 0;
|
|
1095
|
+
margin-bottom: 14px;
|
|
1031
1096
|
}
|
|
1032
1097
|
|
|
1033
1098
|
/* Input fields — matches LoginView.vue input style */
|
|
@@ -1386,6 +1451,94 @@
|
|
|
1386
1451
|
background: var(--bg-tertiary);
|
|
1387
1452
|
color: var(--text-primary);
|
|
1388
1453
|
}
|
|
1454
|
+
|
|
1455
|
+
/* ── Version-mismatch gate ──────────────────────────────────────────────
|
|
1456
|
+
Replaces the loading splash when the desktop/server versions differ. Like
|
|
1457
|
+
the splash it is hidden unless a body class is set (added by
|
|
1458
|
+
__versionMismatchShow), so the same document can serve the login form, the
|
|
1459
|
+
splash and the gate. Styled as a card, mirroring the app's DialogOverlay so
|
|
1460
|
+
the prompt reads as the same UI as the in-app dialogs. */
|
|
1461
|
+
.gate { display: none; }
|
|
1462
|
+
|
|
1463
|
+
body.gate-mode .gate {
|
|
1464
|
+
position: fixed;
|
|
1465
|
+
inset: 0;
|
|
1466
|
+
/* Above the splash: the gate replaces it, but both may be display:flex at
|
|
1467
|
+
once (the page is reused and the main process does not always clear
|
|
1468
|
+
splash-mode), so the gate must win the paint order. */
|
|
1469
|
+
z-index: 200;
|
|
1470
|
+
display: flex;
|
|
1471
|
+
align-items: center;
|
|
1472
|
+
justify-content: center;
|
|
1473
|
+
padding: 0 20px;
|
|
1474
|
+
background: var(--bg-primary);
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
body.gate-mode .content { display: none; }
|
|
1478
|
+
|
|
1479
|
+
.gate-card {
|
|
1480
|
+
width: 100%;
|
|
1481
|
+
max-width: 360px;
|
|
1482
|
+
padding: 18px 20px 14px;
|
|
1483
|
+
background: var(--bg-secondary);
|
|
1484
|
+
border-radius: 14px;
|
|
1485
|
+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.16), 0 14px 44px rgba(0, 0, 0, 0.3);
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
.gate-title {
|
|
1489
|
+
font-size: 15px;
|
|
1490
|
+
font-weight: 600;
|
|
1491
|
+
color: var(--text-primary);
|
|
1492
|
+
margin-bottom: 8px;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
.gate-body {
|
|
1496
|
+
font-size: 13px;
|
|
1497
|
+
line-height: 1.6;
|
|
1498
|
+
color: var(--text-secondary);
|
|
1499
|
+
white-space: pre-line;
|
|
1500
|
+
word-break: break-word;
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
.gate-direction {
|
|
1504
|
+
margin-top: 10px;
|
|
1505
|
+
font-size: 12px;
|
|
1506
|
+
line-height: 1.5;
|
|
1507
|
+
color: var(--text-muted);
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
.gate-actions {
|
|
1511
|
+
display: flex;
|
|
1512
|
+
justify-content: flex-end;
|
|
1513
|
+
gap: 8px;
|
|
1514
|
+
margin-top: 16px;
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
.gate-btn {
|
|
1518
|
+
padding: 7px 14px;
|
|
1519
|
+
font-size: 13px;
|
|
1520
|
+
font-family: inherit;
|
|
1521
|
+
border-radius: 6px;
|
|
1522
|
+
cursor: pointer;
|
|
1523
|
+
transition: opacity 150ms ease, background 150ms ease, color 150ms ease;
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
.gate-continue {
|
|
1527
|
+
color: var(--text-secondary);
|
|
1528
|
+
background: var(--bg-tertiary);
|
|
1529
|
+
border: 1px solid var(--border-color);
|
|
1530
|
+
}
|
|
1531
|
+
.gate-continue:hover {
|
|
1532
|
+
color: var(--text-primary);
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
.gate-download {
|
|
1536
|
+
color: #fff;
|
|
1537
|
+
background: var(--accent-color);
|
|
1538
|
+
border: none;
|
|
1539
|
+
font-weight: 600;
|
|
1540
|
+
}
|
|
1541
|
+
.gate-download:hover { opacity: 0.9; }
|
|
1389
1542
|
</style>
|
|
1390
1543
|
</head>
|
|
1391
1544
|
<body>
|
|
@@ -1402,9 +1555,38 @@
|
|
|
1402
1555
|
<button type="button" class="splash-cancel" id="splashCancel" data-i18n="splash_cancel">取消连接</button>
|
|
1403
1556
|
</div>
|
|
1404
1557
|
|
|
1558
|
+
<!-- Version-mismatch gate. Shown by the main process (see splash.ts /
|
|
1559
|
+
versionGate.ts) INSTEAD of the loading splash when the desktop and server
|
|
1560
|
+
versions differ in either direction. Blocking: it has no cancel button, so
|
|
1561
|
+
the only ways out are "download" and "continue". -->
|
|
1562
|
+
<div class="gate" id="versionGate">
|
|
1563
|
+
<div class="gate-card">
|
|
1564
|
+
<div class="gate-title" id="gateTitle" data-i18n="version_mismatch_title">版本不一致</div>
|
|
1565
|
+
<div class="gate-body" id="gateBody"></div>
|
|
1566
|
+
<div class="gate-direction" id="gateDirection"></div>
|
|
1567
|
+
<div class="gate-actions">
|
|
1568
|
+
<button type="button" class="gate-btn gate-continue" id="gateContinue" data-i18n="version_mismatch_continue">仍然继续</button>
|
|
1569
|
+
<button type="button" class="gate-btn gate-download" id="gateDownload" data-i18n="version_mismatch_download">下载</button>
|
|
1570
|
+
</div>
|
|
1571
|
+
</div>
|
|
1572
|
+
</div>
|
|
1573
|
+
|
|
1405
1574
|
<div class="bg-gradient"></div>
|
|
1406
1575
|
<div class="bg-grid"></div>
|
|
1407
1576
|
|
|
1577
|
+
<!-- Window close, for the frameless Windows/Linux shell.
|
|
1578
|
+
This document has no AppHeader, so it is the ONLY way to close the window
|
|
1579
|
+
on first run or when the server is unreachable — without it the window
|
|
1580
|
+
would have no close affordance at all. Hidden unless the host reports
|
|
1581
|
+
app-drawn controls (macOS keeps its traffic lights, and Android is not a
|
|
1582
|
+
window), so it never duplicates a native button. -->
|
|
1583
|
+
<button type="button" class="window-close" id="windowClose" data-i18n-aria-label="window_close" data-i18n-title="window_close" aria-label="Close" title="Close" hidden>
|
|
1584
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
|
1585
|
+
<line x1="6" y1="6" x2="18" y2="18"/>
|
|
1586
|
+
<line x1="18" y1="6" x2="6" y2="18"/>
|
|
1587
|
+
</svg>
|
|
1588
|
+
</button>
|
|
1589
|
+
|
|
1408
1590
|
<div class="content">
|
|
1409
1591
|
<!-- Brand -->
|
|
1410
1592
|
<div class="brand">
|
|
@@ -1453,31 +1635,20 @@
|
|
|
1453
1635
|
</div>
|
|
1454
1636
|
</form>
|
|
1455
1637
|
|
|
1456
|
-
<!-- Add server form (hidden by default) -->
|
|
1638
|
+
<!-- Add / edit server form (hidden by default) -->
|
|
1457
1639
|
<form id="addServerForm" onsubmit="return false;" style="display:none;">
|
|
1458
|
-
|
|
1640
|
+
<div class="form-title" id="addFormTitle" style="display:none;"></div>
|
|
1641
|
+
|
|
1642
|
+
<!-- Optional display name -->
|
|
1459
1643
|
<div class="field-group">
|
|
1460
|
-
<span class="field-label" data-i18n="
|
|
1461
|
-
<
|
|
1462
|
-
<label>
|
|
1463
|
-
<input type="radio" name="addProtocol" value="https" checked>
|
|
1464
|
-
HTTPS
|
|
1465
|
-
</label>
|
|
1466
|
-
<label>
|
|
1467
|
-
<input type="radio" name="addProtocol" value="http">
|
|
1468
|
-
HTTP
|
|
1469
|
-
</label>
|
|
1470
|
-
</div>
|
|
1644
|
+
<span class="field-label" data-i18n="name_label">名称(选填)</span>
|
|
1645
|
+
<input type="text" id="addName" data-i18n-placeholder="name_placeholder" placeholder="例如 家里的 NAS" maxlength="40" autocorrect="off" autocapitalize="sentences" spellcheck="false">
|
|
1471
1646
|
</div>
|
|
1472
1647
|
|
|
1473
|
-
<!--
|
|
1648
|
+
<!-- Server address (protocol + host + port in one field) -->
|
|
1474
1649
|
<div class="field-group">
|
|
1475
1650
|
<span class="field-label" data-i18n="server_address">服务器地址</span>
|
|
1476
|
-
<
|
|
1477
|
-
<input type="text" id="addHost" placeholder="192.168.1.100" autocorrect="off" autocapitalize="none" spellcheck="false">
|
|
1478
|
-
<span class="colon">:</span>
|
|
1479
|
-
<input type="number" id="addPort" value="20000" inputmode="numeric">
|
|
1480
|
-
</div>
|
|
1651
|
+
<input type="text" id="addAddress" placeholder="https://192.168.1.100:20000" autocorrect="off" autocapitalize="none" spellcheck="false" inputmode="url">
|
|
1481
1652
|
</div>
|
|
1482
1653
|
|
|
1483
1654
|
<!-- Password -->
|
|
@@ -1493,7 +1664,7 @@
|
|
|
1493
1664
|
</div>
|
|
1494
1665
|
|
|
1495
1666
|
<button type="submit" class="connect-btn" id="addConnectBtn">
|
|
1496
|
-
<span id="addBtnText" data-i18n="
|
|
1667
|
+
<span id="addBtnText" data-i18n="add">添 加</span>
|
|
1497
1668
|
</button>
|
|
1498
1669
|
<button type="button" class="cancel-btn" id="cancelAddBtn" data-i18n="cancel">取 消</button>
|
|
1499
1670
|
|
|
@@ -1537,6 +1708,7 @@
|
|
|
1537
1708
|
// SVG icons
|
|
1538
1709
|
var SVG_SERVER = '<svg class="server-item-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="20" height="8" rx="2"/><rect x="2" y="14" width="20" height="8" rx="2"/><circle cx="6" cy="6" r="1"/><circle cx="6" cy="18" r="1"/></svg>';
|
|
1539
1710
|
var SVG_X = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>';
|
|
1711
|
+
var SVG_PENCIL = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg>';
|
|
1540
1712
|
|
|
1541
1713
|
function applyI18n() {
|
|
1542
1714
|
document.querySelectorAll('[data-i18n]').forEach(function(el) {
|
|
@@ -1545,6 +1717,15 @@
|
|
|
1545
1717
|
document.querySelectorAll('[data-i18n-placeholder]').forEach(function(el) {
|
|
1546
1718
|
el.placeholder = t(el.getAttribute('data-i18n-placeholder'));
|
|
1547
1719
|
});
|
|
1720
|
+
// Attribute-valued strings (tooltips, accessible names). The icon-only
|
|
1721
|
+
// close button has no text to translate, so without these it would keep its
|
|
1722
|
+
// hard-coded English label in every language.
|
|
1723
|
+
document.querySelectorAll('[data-i18n-title]').forEach(function(el) {
|
|
1724
|
+
el.title = t(el.getAttribute('data-i18n-title'));
|
|
1725
|
+
});
|
|
1726
|
+
document.querySelectorAll('[data-i18n-aria-label]').forEach(function(el) {
|
|
1727
|
+
el.setAttribute('aria-label', t(el.getAttribute('data-i18n-aria-label')));
|
|
1728
|
+
});
|
|
1548
1729
|
}
|
|
1549
1730
|
|
|
1550
1731
|
// ── Splash overlay (?splash=1) ──────────────────────────────────────────
|
|
@@ -1585,10 +1766,60 @@
|
|
|
1585
1766
|
if (el) el.classList.remove('is-fading');
|
|
1586
1767
|
};
|
|
1587
1768
|
|
|
1769
|
+
// ── Version-mismatch gate ───────────────────────────────────────────────
|
|
1770
|
+
// Shown INSTEAD of the loading splash when the desktop and server versions
|
|
1771
|
+
// differ in either direction (see desktop/src/main/versionGate.ts). The main
|
|
1772
|
+
// process calls __versionMismatchShow with the two versions and which side is
|
|
1773
|
+
// newer; the buttons report the user's choice back through the native bridge.
|
|
1774
|
+
//
|
|
1775
|
+
// The direction decides the wording: "newer" means the desktop app is AHEAD,
|
|
1776
|
+
// where downloading aligns DOWN to the server — so "update" would be wrong.
|
|
1777
|
+
/** Fill the gate with the version details. Called from the main process. */
|
|
1778
|
+
window.__versionMismatchShow = function(info) {
|
|
1779
|
+
if (!info) return;
|
|
1780
|
+
var body = document.getElementById('gateBody');
|
|
1781
|
+
if (body) {
|
|
1782
|
+
body.textContent = t('version_mismatch_body')
|
|
1783
|
+
.replace('%1', info.clientVersion || '')
|
|
1784
|
+
.replace('%2', info.serverVersion || '');
|
|
1785
|
+
}
|
|
1786
|
+
var dir = document.getElementById('gateDirection');
|
|
1787
|
+
if (dir) {
|
|
1788
|
+
dir.textContent = t(info.direction === 'newer'
|
|
1789
|
+
? 'version_mismatch_newer'
|
|
1790
|
+
: 'version_mismatch_older');
|
|
1791
|
+
}
|
|
1792
|
+
document.body.classList.add('gate-mode');
|
|
1793
|
+
};
|
|
1794
|
+
|
|
1795
|
+
/** Clear the gate state. The page is REUSED, so this runs before every show. */
|
|
1796
|
+
window.__versionMismatchReset = function() {
|
|
1797
|
+
document.body.classList.remove('gate-mode');
|
|
1798
|
+
};
|
|
1799
|
+
|
|
1588
1800
|
if (SPLASH_MODE) {
|
|
1589
1801
|
document.body.classList.add('splash-mode');
|
|
1590
1802
|
}
|
|
1591
1803
|
|
|
1804
|
+
/**
|
|
1805
|
+
* Reveal and wire the window close button when the host draws its own
|
|
1806
|
+
* controls (frameless Windows/Linux). `hasCustomWindowControls` is absent on
|
|
1807
|
+
* older hosts, macOS and Android — the button stays hidden there, so it never
|
|
1808
|
+
* duplicates a native close button. Runs before DOMContentLoaded so the
|
|
1809
|
+
* button is usable even while the splash overlay is up.
|
|
1810
|
+
*/
|
|
1811
|
+
(function initWindowClose() {
|
|
1812
|
+
var btn = document.getElementById('windowClose');
|
|
1813
|
+
if (!btn) return;
|
|
1814
|
+
var supported = false;
|
|
1815
|
+
try { supported = ClawBenchNative.hasCustomWindowControls && ClawBenchNative.hasCustomWindowControls() === true; } catch(e) {}
|
|
1816
|
+
if (!supported) return;
|
|
1817
|
+
btn.hidden = false;
|
|
1818
|
+
btn.addEventListener('click', function() {
|
|
1819
|
+
try { ClawBenchNative.windowClose(); } catch(e) {}
|
|
1820
|
+
});
|
|
1821
|
+
})();
|
|
1822
|
+
|
|
1592
1823
|
window.addEventListener('DOMContentLoaded', function() {
|
|
1593
1824
|
// Apply i18n to all elements with data-i18n / data-i18n-placeholder
|
|
1594
1825
|
applyI18n();
|
|
@@ -1602,6 +1833,20 @@
|
|
|
1602
1833
|
try { ClawBenchNative.cancelSplash(); } catch(e) {}
|
|
1603
1834
|
});
|
|
1604
1835
|
}
|
|
1836
|
+
// Version-gate buttons. Wired here (not in the show hook) so they work
|
|
1837
|
+
// even if the gate is shown after the page has already loaded.
|
|
1838
|
+
var continueBtn = document.getElementById('gateContinue');
|
|
1839
|
+
if (continueBtn) {
|
|
1840
|
+
continueBtn.addEventListener('click', function() {
|
|
1841
|
+
try { ClawBenchNative.versionContinue(); } catch(e) {}
|
|
1842
|
+
});
|
|
1843
|
+
}
|
|
1844
|
+
var downloadBtn = document.getElementById('gateDownload');
|
|
1845
|
+
if (downloadBtn) {
|
|
1846
|
+
downloadBtn.addEventListener('click', function() {
|
|
1847
|
+
try { ClawBenchNative.versionDownload(); } catch(e) {}
|
|
1848
|
+
});
|
|
1849
|
+
}
|
|
1605
1850
|
return;
|
|
1606
1851
|
}
|
|
1607
1852
|
|
|
@@ -1655,64 +1900,81 @@
|
|
|
1655
1900
|
}
|
|
1656
1901
|
});
|
|
1657
1902
|
|
|
1658
|
-
// Event: add and
|
|
1903
|
+
// Event: save the add/edit form. Both "Add Server" and the row pencil land
|
|
1904
|
+
// here; the form's editUrl data attribute is what distinguishes them.
|
|
1905
|
+
// Saving never connects — the user picks the server and presses Connect.
|
|
1659
1906
|
document.getElementById('addServerForm').addEventListener('submit', function(e) {
|
|
1660
1907
|
e.preventDefault();
|
|
1661
1908
|
if (connecting) return;
|
|
1662
1909
|
hideError('addErrorMsg');
|
|
1663
1910
|
|
|
1664
|
-
var
|
|
1665
|
-
|
|
1911
|
+
var url = buildServerUrl(document.getElementById('addAddress').value);
|
|
1912
|
+
if (!url) {
|
|
1913
|
+
showError('addErrorMsg', t('enter_full_address'));
|
|
1914
|
+
document.getElementById('addAddress').focus();
|
|
1915
|
+
return;
|
|
1916
|
+
}
|
|
1666
1917
|
var password = document.getElementById('addPassword').value;
|
|
1667
|
-
var
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1918
|
+
var name = normalizeServerName(document.getElementById('addName').value);
|
|
1919
|
+
// The URL this form is editing ('' when adding). Captured before save so
|
|
1920
|
+
// the rename branch can retire the old entry afterwards.
|
|
1921
|
+
var editingUrl = document.getElementById('addServerForm').getAttribute('data-edit-url') || '';
|
|
1922
|
+
|
|
1923
|
+
// Renaming onto another saved server would silently overwrite that
|
|
1924
|
+
// server's password (saveServer keys on URL). Refuse instead.
|
|
1925
|
+
var clash = savedServers.some(function(s) {
|
|
1926
|
+
return s.url === url && s.url !== editingUrl;
|
|
1927
|
+
});
|
|
1928
|
+
if (clash) {
|
|
1929
|
+
showError('addErrorMsg', t('duplicate_server'));
|
|
1672
1930
|
return;
|
|
1673
1931
|
}
|
|
1674
|
-
|
|
1675
|
-
|
|
1932
|
+
// Names are unique labels. The edited entry is exempt from matching
|
|
1933
|
+
// itself; unnamed servers never collide with each other.
|
|
1934
|
+
if (findServerNameConflict(savedServers, name, editingUrl)) {
|
|
1935
|
+
showError('addErrorMsg', t('duplicate_name'));
|
|
1936
|
+
return;
|
|
1676
1937
|
}
|
|
1677
1938
|
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1939
|
+
setSaving(true);
|
|
1940
|
+
// Both calls go through ipcRenderer.invoke, so failures surface as
|
|
1941
|
+
// rejections. Chain them: the rename must delete the old entry only after
|
|
1942
|
+
// the new one is written, and the list must reload after both settle.
|
|
1682
1943
|
try {
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1944
|
+
Promise.resolve(ClawBenchNative.saveServerNamed(url, password, name))
|
|
1945
|
+
.then(function() {
|
|
1946
|
+
// Only when the address actually changed: deleting the same URL
|
|
1947
|
+
// would drop the entry we just saved.
|
|
1948
|
+
if (editingUrl && editingUrl !== url) {
|
|
1949
|
+
return Promise.resolve(ClawBenchNative.removeServer(editingUrl));
|
|
1950
|
+
}
|
|
1951
|
+
})
|
|
1952
|
+
.then(function() {
|
|
1953
|
+
setSaving(false);
|
|
1954
|
+
hideAddForm();
|
|
1955
|
+
})
|
|
1956
|
+
.catch(function(err) {
|
|
1957
|
+
showError('addErrorMsg', t('connection_failed') + (err && err.message ? err.message : ''));
|
|
1958
|
+
setSaving(false);
|
|
1959
|
+
});
|
|
1690
1960
|
} catch(e) {
|
|
1691
1961
|
showError('addErrorMsg', t('connection_failed') + e.message);
|
|
1692
|
-
|
|
1962
|
+
setSaving(false);
|
|
1693
1963
|
}
|
|
1694
1964
|
});
|
|
1695
1965
|
|
|
1696
|
-
// Event: blur on the
|
|
1697
|
-
// Blur, not paste/input: the field is only rewritten after the user leaves
|
|
1698
|
-
// so typing is never disturbed mid-keystroke. Reading .value (rather
|
|
1699
|
-
// clipboard data) also sidesteps WebView clipboard restrictions.
|
|
1700
|
-
document.getElementById('
|
|
1701
|
-
var
|
|
1702
|
-
//
|
|
1703
|
-
|
|
1966
|
+
// Event: blur on the address field -> normalize a full address in place.
|
|
1967
|
+
// Blur, not paste/input: the field is only rewritten after the user leaves
|
|
1968
|
+
// it, so typing is never disturbed mid-keystroke. Reading .value (rather
|
|
1969
|
+
// than clipboard data) also sidesteps WebView clipboard restrictions.
|
|
1970
|
+
document.getElementById('addAddress').addEventListener('blur', function() {
|
|
1971
|
+
var url = buildServerUrl(document.getElementById('addAddress').value);
|
|
1972
|
+
// Incomplete address -> leave the field exactly as the user typed it; the
|
|
1973
|
+
// submit handler reports what is missing.
|
|
1974
|
+
if (!url) return;
|
|
1704
1975
|
|
|
1705
1976
|
hideError('addErrorMsg');
|
|
1706
|
-
document.getElementById('
|
|
1707
|
-
// Only override the port when the URL carried one explicitly; the field
|
|
1708
|
-
// keeps its default (20000) otherwise.
|
|
1709
|
-
if (parsed.port !== null) {
|
|
1710
|
-
document.getElementById('addPort').value = parsed.port;
|
|
1711
|
-
}
|
|
1712
|
-
// No scheme in the input -> leave the radio untouched.
|
|
1713
|
-
if (parsed.protocol !== null) {
|
|
1714
|
-
document.querySelector('input[name="addProtocol"][value="' + parsed.protocol + '"]').checked = true;
|
|
1715
|
-
}
|
|
1977
|
+
document.getElementById('addAddress').value = url;
|
|
1716
1978
|
});
|
|
1717
1979
|
|
|
1718
1980
|
// Event: show add form
|
|
@@ -1768,11 +2030,19 @@
|
|
|
1768
2030
|
html += ' <div class="server-item-info">';
|
|
1769
2031
|
html += ' <span class="server-indicator"></span>';
|
|
1770
2032
|
html += ' ' + SVG_SERVER;
|
|
1771
|
-
|
|
2033
|
+
// A named server shows its name; an unnamed one falls back to the
|
|
2034
|
+
// address. The full address always stays in the title tooltip, so a
|
|
2035
|
+
// named row is still identifiable (and copying it is possible).
|
|
2036
|
+
html += ' <span class="server-item-url" title="' + escHtml(srv.url) + '">' + escHtml(normalizeServerName(srv.name) || formatHost(srv.url)) + '</span>';
|
|
2037
|
+
html += ' </div>';
|
|
2038
|
+
html += ' <div class="server-item-actions">';
|
|
2039
|
+
html += ' <button class="server-edit-btn" onclick="event.stopPropagation(); editServer(\'' + escJs(srv.url) + '\')" title="' + t('edit_server') + '">';
|
|
2040
|
+
html += ' ' + SVG_PENCIL;
|
|
2041
|
+
html += ' </button>';
|
|
2042
|
+
html += ' <button class="server-delete-btn" onclick="event.stopPropagation(); deleteServer(\'' + escJs(srv.url) + '\')" title="' + t('delete') + '">';
|
|
2043
|
+
html += ' ' + SVG_X;
|
|
2044
|
+
html += ' </button>';
|
|
1772
2045
|
html += ' </div>';
|
|
1773
|
-
html += ' <button class="server-delete-btn" onclick="event.stopPropagation(); deleteServer(\'' + escJs(srv.url) + '\')" title="' + t('delete') + '">';
|
|
1774
|
-
html += ' ' + SVG_X;
|
|
1775
|
-
html += ' </button>';
|
|
1776
2046
|
html += '</div>';
|
|
1777
2047
|
});
|
|
1778
2048
|
|
|
@@ -1886,17 +2156,59 @@
|
|
|
1886
2156
|
});
|
|
1887
2157
|
}
|
|
1888
2158
|
|
|
2159
|
+
/**
|
|
2160
|
+
* Show the form for adding a NEW server: empty fields, "Add" button.
|
|
2161
|
+
*/
|
|
1889
2162
|
function showAddForm() {
|
|
2163
|
+
var form = document.getElementById('addServerForm');
|
|
2164
|
+
// Empty data-edit-url is what marks this as an add, not an edit.
|
|
2165
|
+
form.setAttribute('data-edit-url', '');
|
|
2166
|
+
document.getElementById('addFormTitle').style.display = 'none';
|
|
2167
|
+
document.getElementById('addName').value = '';
|
|
2168
|
+
document.getElementById('addAddress').value = '';
|
|
2169
|
+
document.getElementById('addPassword').value = '';
|
|
2170
|
+
document.getElementById('addBtnText').textContent = t('add');
|
|
2171
|
+
|
|
2172
|
+
document.getElementById('serverListSection').style.display = 'none';
|
|
2173
|
+
document.getElementById('connectForm').style.display = 'none';
|
|
2174
|
+
document.getElementById('addServerBtn').style.display = 'none';
|
|
2175
|
+
form.style.display = 'block';
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
/**
|
|
2179
|
+
* Reuse the add form to edit an existing server. data-edit-url holds the
|
|
2180
|
+
* entry being edited so submit can tell the two modes apart and retire the
|
|
2181
|
+
* old URL when the address changes.
|
|
2182
|
+
*/
|
|
2183
|
+
function editServer(url) {
|
|
2184
|
+
var srv = savedServers.find(function(s) { return s.url === url; });
|
|
2185
|
+
if (!srv) return;
|
|
2186
|
+
|
|
2187
|
+
var form = document.getElementById('addServerForm');
|
|
2188
|
+
form.setAttribute('data-edit-url', url);
|
|
2189
|
+
var title = document.getElementById('addFormTitle');
|
|
2190
|
+
title.textContent = t('edit_server');
|
|
2191
|
+
title.style.display = 'block';
|
|
2192
|
+
document.getElementById('addName').value = normalizeServerName(srv.name);
|
|
2193
|
+
document.getElementById('addAddress').value = srv.url;
|
|
2194
|
+
// Prefill the saved password so saving without retyping keeps it.
|
|
2195
|
+
document.getElementById('addPassword').value = srv.password || '';
|
|
2196
|
+
document.getElementById('addBtnText').textContent = t('save');
|
|
2197
|
+
|
|
1890
2198
|
document.getElementById('serverListSection').style.display = 'none';
|
|
1891
2199
|
document.getElementById('connectForm').style.display = 'none';
|
|
1892
2200
|
document.getElementById('addServerBtn').style.display = 'none';
|
|
1893
|
-
|
|
2201
|
+
form.style.display = 'block';
|
|
2202
|
+
hideError('addErrorMsg');
|
|
2203
|
+
document.getElementById('addAddress').focus();
|
|
1894
2204
|
}
|
|
1895
2205
|
|
|
1896
2206
|
function hideAddForm() {
|
|
1897
2207
|
document.getElementById('addServerForm').style.display = 'none';
|
|
1898
2208
|
document.getElementById('addErrorMsg').classList.remove('visible');
|
|
1899
|
-
|
|
2209
|
+
// Reload from native rather than re-rendering the stale in-memory array:
|
|
2210
|
+
// saving changed the list (new entry, promoted head, renamed URL).
|
|
2211
|
+
loadServerList();
|
|
1900
2212
|
}
|
|
1901
2213
|
|
|
1902
2214
|
function formatHost(url) {
|
|
@@ -1937,13 +2249,31 @@
|
|
|
1937
2249
|
} else {
|
|
1938
2250
|
btn.disabled = false;
|
|
1939
2251
|
if (isAddForm) {
|
|
1940
|
-
|
|
2252
|
+
// The form doubles as add and edit, so restore whichever label its
|
|
2253
|
+
// current mode calls for rather than always "Add".
|
|
2254
|
+
var editing = document.getElementById('addServerForm').getAttribute('data-edit-url');
|
|
2255
|
+
btn.innerHTML = '<span id="addBtnText">' + t(editing ? 'save' : 'add') + '</span>';
|
|
1941
2256
|
} else {
|
|
1942
2257
|
btn.innerHTML = '<span id="btnText">' + t('connect') + '</span>';
|
|
1943
2258
|
}
|
|
1944
2259
|
}
|
|
1945
2260
|
}
|
|
1946
2261
|
|
|
2262
|
+
/**
|
|
2263
|
+
* Busy state for the add/edit form's own Save/Add button. Separate from
|
|
2264
|
+
* setConnecting because saving does not authenticate: showing "Authenticating"
|
|
2265
|
+
* would misdescribe what is happening.
|
|
2266
|
+
*/
|
|
2267
|
+
function setSaving(saving) {
|
|
2268
|
+
var btn = document.getElementById('addConnectBtn');
|
|
2269
|
+
if (saving) {
|
|
2270
|
+
btn.disabled = true;
|
|
2271
|
+
btn.innerHTML = '<div class="spinner"></div><span>' + t('saving') + '</span>';
|
|
2272
|
+
} else {
|
|
2273
|
+
setConnecting(false, true);
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
|
|
1947
2277
|
// Called by native code on connection failure.
|
|
1948
2278
|
// msg is the Chromium load-error description (e.g. "ERR_CONNECTION_REFUSED")
|
|
1949
2279
|
// delivered from the main process's did-fail-load handler; empty when the
|
package/resources/url-utils.js
CHANGED
|
@@ -46,3 +46,61 @@ function parseServerInput(text) {
|
|
|
46
46
|
|
|
47
47
|
return { protocol: protocol, host: host, port: port };
|
|
48
48
|
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Build the canonical server URL from a single free-text address.
|
|
52
|
+
*
|
|
53
|
+
* Strict on purpose: the login form no longer has a protocol radio or a port
|
|
54
|
+
* field, so the address must carry BOTH explicitly. A bare host or a
|
|
55
|
+
* scheme-less `host:port` returns null and the caller shows "enter a full
|
|
56
|
+
* address" rather than guessing a scheme/port the user never chose.
|
|
57
|
+
*
|
|
58
|
+
* Normalisation the parser already gives us: scheme lowercased, path/query/
|
|
59
|
+
* fragment dropped, surrounding whitespace trimmed.
|
|
60
|
+
*
|
|
61
|
+
* Returns 'scheme://host:port', or null when the input is not a complete
|
|
62
|
+
* server address. Callers must treat null as "reject, do not save".
|
|
63
|
+
*/
|
|
64
|
+
function buildServerUrl(text) {
|
|
65
|
+
var parsed = parseServerInput(text);
|
|
66
|
+
if (!parsed || !parsed.protocol || !parsed.port) return null;
|
|
67
|
+
return parsed.protocol + '://' + parsed.host + ':' + parsed.port;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Normalise a server's display name: trimmed. Empty means "no name", which the
|
|
72
|
+
* list renders as the address instead.
|
|
73
|
+
*
|
|
74
|
+
* Returns '' for anything that is not a string, so callers can pass the raw
|
|
75
|
+
* field value without guarding.
|
|
76
|
+
*/
|
|
77
|
+
function normalizeServerName(text) {
|
|
78
|
+
if (typeof text !== 'string') return '';
|
|
79
|
+
return text.trim();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Find the server whose NAME equals `name`, ignoring `exceptUrl`.
|
|
84
|
+
*
|
|
85
|
+
* Names are unique labels, so the form refuses to save a duplicate. The
|
|
86
|
+
* comparison is case-insensitive (users do not think of "Home" and "home" as
|
|
87
|
+
* different servers) and an empty `name` never matches — it means "no name",
|
|
88
|
+
* and unnamed servers must not collide with each other.
|
|
89
|
+
*
|
|
90
|
+
* `exceptUrl` is the entry currently being edited: a server must not collide
|
|
91
|
+
* with itself when its name is left unchanged.
|
|
92
|
+
*
|
|
93
|
+
* Returns the conflicting entry (an object with at least `.url`), or null.
|
|
94
|
+
*/
|
|
95
|
+
function findServerNameConflict(servers, name, exceptUrl) {
|
|
96
|
+
var wanted = normalizeServerName(name).toLowerCase();
|
|
97
|
+
if (!wanted) return null;
|
|
98
|
+
if (!Array.isArray(servers)) return null;
|
|
99
|
+
for (var i = 0; i < servers.length; i++) {
|
|
100
|
+
var srv = servers[i];
|
|
101
|
+
if (!srv) continue;
|
|
102
|
+
if (exceptUrl && srv.url === exceptUrl) continue;
|
|
103
|
+
if (normalizeServerName(srv.name).toLowerCase() === wanted) return srv;
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|