cicy-desktop 2.1.342 → 2.1.358
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/scripts/build-homepage.cjs +16 -1
- package/src/app-updater.js +30 -0
- package/src/backends/homepage-preload.js +3 -1
- package/src/backends/homepage-react/assets/index-Buyk0RlW.js +655 -0
- package/src/backends/homepage-react/assets/index-CVk0uc2g.css +1 -0
- package/src/backends/homepage-react/index.html +5 -3
- package/src/backends/homepage-react/panel/facebook-matrix.html +305 -87
- package/src/backends/homepage-react/panel/ipcheck.html +112 -0
- package/src/backends/homepage-react/panel/matrix.html +595 -0
- package/src/backends/homepage-react/panel/telegram-matrix.html +96 -22
- package/src/backends/homepage-react/panel/tiktok-matrix.html +599 -0
- package/src/backends/hub-client.js +108 -2
- package/src/backends/sidecar-ipc.js +33 -6
- package/src/main.js +67 -11
- package/src/sidecar/host-mihomo.js +47 -2
- package/src/tabbrowser/facebook-identity.js +12 -1
- package/src/tabbrowser/facebook-matrix.html +305 -87
- package/src/tabbrowser/fb-login.js +115 -0
- package/src/tabbrowser/newtab-protocol.js +6 -1
- package/src/tabbrowser/panel-cells.js +14 -1
- package/src/tabbrowser/telegram-matrix.html +96 -22
- package/src/tabbrowser/telegram-web-preferences.js +5 -1
- package/src/tabbrowser/tiktok-matrix.html +305 -87
- package/src/tools/tab-browser-tools.js +21 -100
- package/src/tray.js +22 -0
- package/test/bootstrap-failure-reporting.test.js +1 -1
- package/test/facebook-identity.test.js +3 -1
- package/test/facebook-matrix.test.js +2 -1
- package/test/telegram-matrix.test.js +2 -2
- package/test/tiktok-matrix.test.js +2 -1
- package/tools/arm-fblogin.js +32 -0
- package/tools/wsd-ws +114 -0
- package/workers/render/index.html +3 -1
- package/workers/render/package.json +2 -3
- package/workers/render/public/panel/facebook-matrix.html +305 -87
- package/workers/render/public/panel/ipcheck.html +112 -0
- package/workers/render/public/panel/matrix.html +595 -0
- package/workers/render/public/panel/telegram-matrix.html +96 -22
- package/workers/render/public/panel/tiktok-matrix.html +599 -0
- package/workers/render/src/App.css +4 -0
- package/workers/render/src/App.jsx +917 -52
- package/workers/render/stamp-panel-version.mjs +16 -0
- package/workers/render/worker.js +1 -1
- package/src/backends/homepage-react/assets/index-C6Zrdgc0.css +0 -1
- package/src/backends/homepage-react/assets/index-OG6JB73F.js +0 -411
- package/src/tabbrowser/cicy-code-tab-restore.js +0 -10
- package/test/cicy-code-tab-restore.test.js +0 -21
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<meta name="pv" content="15e64cc77c1d">
|
|
7
|
+
<title>TikTok 矩阵</title>
|
|
8
|
+
<style>
|
|
9
|
+
:root { color-scheme: dark; --bg:#0A0A0A; --cell:#141416; --head:#1a1a1c; --line:rgba(255,255,255,.08); --fg:#e6e6e9; --muted:#8b8b92; --accent:#3b82f6; --hover:rgba(255,255,255,.06); font-family: Inter, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; }
|
|
10
|
+
* { box-sizing: border-box; }
|
|
11
|
+
html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; background: var(--bg); color: var(--fg); }
|
|
12
|
+
body { display: grid; grid-template-columns: minmax(320px, 34%) 1fr; }
|
|
13
|
+
aside { min-width: 0; border-right: 1px solid var(--line); background: var(--bg); display: flex; flex-direction: column; }
|
|
14
|
+
header { flex: none; height: 56px; padding: 0 16px; display: flex; align-items: center; justify-content: space-between; gap: 12px; border-bottom: 1px solid var(--line); }
|
|
15
|
+
h1 { margin: 0; font-size: 15px; display: flex; align-items: baseline; gap: 8px; }
|
|
16
|
+
h1 small { color: var(--muted); font-weight: 500; font-size: 12px; }
|
|
17
|
+
button { border: 0; border-radius: 8px; padding: 7px 12px; color: white; background: var(--accent); cursor: pointer; font-weight: 600; font-size: 13px; white-space: nowrap; line-height: 1.2; }
|
|
18
|
+
button:hover { filter: brightness(1.08); }
|
|
19
|
+
button:disabled { opacity: .55; cursor: wait; }
|
|
20
|
+
button.ghost { background: var(--head); color: var(--fg); border: 1px solid var(--line); }
|
|
21
|
+
button.sm { padding: 4px 8px; font-size: 12px; }
|
|
22
|
+
.icon { background: transparent; color: #b4b4bb; padding: 3px 6px; font-size: 13px; border-radius: 6px; }
|
|
23
|
+
.icon:hover { background: var(--hover); color: #fff; }
|
|
24
|
+
.list { flex: 1; overflow: auto; padding: 8px; }
|
|
25
|
+
#rows { display: flex; flex-direction: column; gap: 4px; }
|
|
26
|
+
.row { padding: 9px 10px; border-radius: 10px; cursor: pointer; border: 1px solid transparent; min-width: 0; }
|
|
27
|
+
.row:hover { background: var(--hover); }
|
|
28
|
+
.row.active { background: var(--cell); border-color: rgba(59,130,246,.55); }
|
|
29
|
+
.row.failed .name { color: #ff9e9e; }
|
|
30
|
+
.line1 { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
31
|
+
.dot { width: 8px; height: 8px; border-radius: 50%; background: #3a3a40; flex: none; }
|
|
32
|
+
.dot.loading { background: #f0b429; animation: pulse 1s infinite alternate; }
|
|
33
|
+
.dot.ok { background: #3fb950; }
|
|
34
|
+
.dot.failed { background: #ff5c5c; }
|
|
35
|
+
@keyframes pulse { from { opacity: .35; } to { opacity: 1; } }
|
|
36
|
+
.name { font-weight: 650; font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
|
|
37
|
+
.chip { flex: none; padding: 1px 6px; border-radius: 999px; background: var(--head); color: #b4b4bb; font: 11px ui-monospace, monospace; }
|
|
38
|
+
.st { flex: none; color: var(--muted); font-size: 11px; white-space: nowrap; margin-left: auto; }
|
|
39
|
+
.line2 { display: flex; align-items: center; gap: 8px; margin-top: 4px; padding-left: 16px; font-size: 11.5px; color: var(--muted); min-width: 0; }
|
|
40
|
+
.line2 .tg { flex: none; max-width: 55%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: #b4b4bb; }
|
|
41
|
+
.line2 .tg b { color: var(--fg); font-weight: 600; }
|
|
42
|
+
.line2 .tg.none { color: #6b6b72; }
|
|
43
|
+
/* 编辑态就地变成可输入,尺寸/位置与只读态完全一致,不发生位移 */
|
|
44
|
+
.line2 .ip { flex: none; max-width: 45%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; padding: 0 6px; border-radius: 999px; background: var(--head); color: #b4b4bb; font: 11px ui-monospace, monospace; line-height: 16px; }
|
|
45
|
+
.line2 .ip.none { display: none; }
|
|
46
|
+
.line2 .phone { flex: none; color: #b4b4bb; font: 11px ui-monospace, monospace; white-space: nowrap; }
|
|
47
|
+
.line2 .phone.none { display: none; }
|
|
48
|
+
button.code { background: #1f6feb; padding: 3px 8px; font-size: 11px; }
|
|
49
|
+
button.code.on { background: #c0142e; }
|
|
50
|
+
.ghead .phone { color: #b4b4bb; font: 11px ui-monospace, monospace; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
|
|
51
|
+
/* 代理设置弹窗 */
|
|
52
|
+
.modal { position: fixed; inset: 0; z-index: 60; display: flex; align-items: center; justify-content: center; background: rgba(0,0,0,.55); }
|
|
53
|
+
.modal[hidden] { display: none; }
|
|
54
|
+
.modal-card { width: 380px; max-width: 92vw; background: var(--cell); border: 1px solid var(--line); border-radius: 14px; padding: 18px; box-shadow: 0 24px 70px rgba(0,0,0,.5); }
|
|
55
|
+
.modal-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; font-size: 15px; font-weight: 650; }
|
|
56
|
+
.modal-head .icon { background: transparent; color: #b4b4bb; padding: 2px 6px; font-size: 14px; border-radius: 6px; }
|
|
57
|
+
.modal-head .icon:hover { background: var(--hover); color: #fff; }
|
|
58
|
+
.modal-label { display: block; font-size: 12px; color: var(--muted); margin: 4px 0 6px; }
|
|
59
|
+
.modal-row { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
|
|
60
|
+
.modal-row input { flex: 1; min-width: 0; height: 32px; border: 1px solid var(--line); border-radius: 8px; padding: 0 10px; background: #111113; color: var(--fg); font: 12px ui-monospace, monospace; outline: 0; }
|
|
61
|
+
.modal-row input:focus { border-color: var(--accent); }
|
|
62
|
+
.modal-ip { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; color: #b4b4bb; }
|
|
63
|
+
.modal-err { color: #ff8a8a; font-size: 12px; min-height: 16px; margin-bottom: 4px; word-break: break-all; }
|
|
64
|
+
.modal-danger { margin-top: 8px; padding-top: 12px; border-top: 1px solid var(--line); }
|
|
65
|
+
button.del { background: #b3261e; }
|
|
66
|
+
.row-err { padding: 4px 0 0 16px; color: #ff8a8a; font-size: 11px; display: none; }
|
|
67
|
+
.row-err.on { display: block; }
|
|
68
|
+
#empty { display: none; padding: 40px 24px; color: var(--muted); text-align: center; line-height: 1.8; }
|
|
69
|
+
#empty.on { display: block; }
|
|
70
|
+
.hint { flex: none; padding: 8px 16px; color: var(--muted); font-size: 11px; border-top: 1px solid var(--line); }
|
|
71
|
+
main { min-width: 0; display: flex; flex-direction: column; align-items: center; padding: 14px 18px; background: var(--bg); }
|
|
72
|
+
#preview-title { width: min(410px, calc(100% - 20px)); margin-bottom: 8px; display: flex; align-items: center; justify-content: space-between; gap: 8px; color: #b4b4bb; font-size: 12px; }
|
|
73
|
+
#preview-title .left { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
74
|
+
#selected-name { color: var(--fg); font-weight: 600; font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
75
|
+
#status { color: var(--muted); white-space: nowrap; }
|
|
76
|
+
.actions { display: flex; gap: 6px; flex: none; }
|
|
77
|
+
.actions button { padding: 5px 9px; font-size: 12px; }
|
|
78
|
+
#phone { position: relative; width: min(410px, calc(100% - 20px)); flex: 1 1 auto; min-height: 420px; padding: 10px; overflow: hidden; border: 1px solid var(--line); border-radius: 28px; background: var(--cell); box-shadow: 0 24px 70px rgba(0,0,0,.45); }
|
|
79
|
+
#phone-preview { width: 100%; height: 100%; overflow: hidden; border-radius: 19px; background: #fff; }
|
|
80
|
+
.overlay { position: absolute; inset: 10px; border-radius: 19px; display: none; place-items: center; text-align: center; padding: 28px; color: var(--fg); background: var(--head); z-index: 2; }
|
|
81
|
+
.overlay.on { display: grid; }
|
|
82
|
+
.overlay h2 { margin: 0 0 8px; font-size: 16px; color: #ff8a8a; }
|
|
83
|
+
.overlay p { margin: 0 0 14px; font-size: 13px; line-height: 1.6; color: var(--muted); word-break: break-all; }
|
|
84
|
+
.overlay code { color: var(--fg); font: 12px ui-monospace, monospace; }
|
|
85
|
+
.overlay .btns { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; }
|
|
86
|
+
/* 批量登录:未登录的会话一次全部铺开(最多 10 格),每格一个独立 BrowserView */
|
|
87
|
+
#grid-title { display: none; width: 100%; margin-bottom: 6px; align-items: center; justify-content: space-between; gap: 8px; color: #b4b4bb; font-size: 12px; }
|
|
88
|
+
#grid-title b { color: var(--fg); font-size: 13px; }
|
|
89
|
+
#grid { display: none; width: 100%; flex: 1 1 auto; min-height: 0; gap: 8px; flex-direction: row; flex-wrap: wrap; align-content: flex-start; overflow-x: hidden; overflow-y: auto; }
|
|
90
|
+
/* 批量模式 = 整页只有 webview:侧栏整个隐藏,顶部一条细工具栏,其余全是格子 */
|
|
91
|
+
body.batch { grid-template-columns: 1fr; }
|
|
92
|
+
body.batch aside { display: none; }
|
|
93
|
+
main.batch { padding: 8px 10px 10px; }
|
|
94
|
+
main.batch #preview-title, main.batch #phone { display: none; }
|
|
95
|
+
main.batch #grid-title { display: flex; }
|
|
96
|
+
main.batch #grid { display: flex; }
|
|
97
|
+
.gcell { display: flex; flex-direction: column; flex: none; width: 360px; height: 50vh; min-height: 0; border: 1px solid var(--line); border-radius: 14px; background: var(--cell); overflow: hidden; }
|
|
98
|
+
.gcell.done { border-color: rgba(63,185,80,.6); }
|
|
99
|
+
.gcell.failed { border-color: rgba(255,92,92,.6); }
|
|
100
|
+
.ghead { flex: none; height: 30px; padding: 0 8px; display: flex; align-items: center; gap: 6px; font-size: 12px; border-bottom: 1px solid var(--line); background: var(--head); min-width: 0; }
|
|
101
|
+
.ghead .name { font-size: 12px; }
|
|
102
|
+
.ghead .st { margin-left: auto; }
|
|
103
|
+
.gcell.done .ghead .st { color: #3fb950; font-weight: 600; }
|
|
104
|
+
.gbody { flex: 1 1 auto; min-height: 0; width: 100%; background: #fff; }
|
|
105
|
+
#grid-empty { grid-column: 1 / -1; display: grid; place-items: center; color: var(--muted); }
|
|
106
|
+
.spinner { width: 28px; height: 28px; border: 3px solid rgba(255,255,255,.12); border-top-color: var(--accent); border-radius: 50%; animation: spin .9s linear infinite; margin: 0 auto 12px; }
|
|
107
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
108
|
+
/* TikTok 矩阵:接码/手机号是 Telegram 专属,这里隐藏 */
|
|
109
|
+
button.code, .line2 .phone, .ghead .phone, #cfg-login, #open-code { display: none !important; }
|
|
110
|
+
/* 机器 id 徽章:机器多了一眼认出这是哪台。列表/批量两种模式都显示。 */
|
|
111
|
+
.mid{display:inline-block;margin-right:8px;padding:2px 9px;border-radius:7px;font:700 13px ui-monospace,monospace;
|
|
112
|
+
background:#1f6feb;color:#fff;letter-spacing:.5px;vertical-align:middle;box-shadow:0 1px 4px rgba(0,0,0,.4)}
|
|
113
|
+
</style>
|
|
114
|
+
</head>
|
|
115
|
+
<body>
|
|
116
|
+
<aside>
|
|
117
|
+
<header>
|
|
118
|
+
<h1><span id="mid-list" class="mid" hidden></span>TikTok 矩阵 <small id="count"></small></h1>
|
|
119
|
+
<div class="actions"><button class="ghost" id="batch-login" title="平铺所有 Profile 的会话到网格(最多 10 个)">批量网格</button><button id="add-profile" title="新建一个独立会话(persist:sandbox-N)">+ 添加 Profile</button></div>
|
|
120
|
+
</header>
|
|
121
|
+
<div class="list">
|
|
122
|
+
<div id="rows"></div>
|
|
123
|
+
<div id="empty">还没有 Profile<br>点击右上角「添加 Profile」创建第一个 TikTok 会话</div>
|
|
124
|
+
</div>
|
|
125
|
+
<div class="hint">点击行切换预览;点 ⚙ 设置代理 / 测 IP / 删除。已打开过的会话会在后台保持登录。</div>
|
|
126
|
+
</aside>
|
|
127
|
+
<main>
|
|
128
|
+
<div id="grid-title">
|
|
129
|
+
<div class="left"><span id="mid-batch" class="mid" hidden></span><b>批量登录</b><span id="grid-count"></span></div>
|
|
130
|
+
<div class="actions"><button class="ghost" id="grid-reload" title="重新加载所有格子">全部重载</button><button class="ghost" id="grid-exit">退出批量</button></div>
|
|
131
|
+
</div>
|
|
132
|
+
<div id="grid"></div>
|
|
133
|
+
<div id="preview-title">
|
|
134
|
+
<div class="left"><span class="dot" id="sel-dot"></span><span id="selected-name">未选择 Profile</span><span id="status"></span></div>
|
|
135
|
+
<div class="actions"><button class="code" id="open-code" title="在格子上叠加/关闭接码 webview(同代理出口)">接码</button><button class="ghost" id="probe-ip" title="经该 Profile 的代理探测出口 IP / 地区">测 IP</button><button class="ghost" id="reload" title="重新加载 TikTok">重新加载</button></div>
|
|
136
|
+
</div>
|
|
137
|
+
<div id="phone">
|
|
138
|
+
<div id="phone-preview"></div>
|
|
139
|
+
<div class="overlay on" id="ov-empty"><div>选择左侧一个 Profile 预览 TikTok</div></div>
|
|
140
|
+
<div class="overlay" id="ov-loading"><div><div class="spinner"></div><div>正在加载 TikTok…</div></div></div>
|
|
141
|
+
<div class="overlay" id="ov-error">
|
|
142
|
+
<div>
|
|
143
|
+
<h2>无法连接 TikTok</h2>
|
|
144
|
+
<p id="err-detail"></p>
|
|
145
|
+
<div class="btns"><button id="err-retry">重试</button><button class="ghost" id="err-cfg">代理设置</button></div>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
</main>
|
|
150
|
+
<div id="cfg-modal" class="modal" hidden>
|
|
151
|
+
<div class="modal-card">
|
|
152
|
+
<div class="modal-head"><span id="cfg-title">Profile 设置</span><button class="icon" id="cfg-x" title="关闭">✕</button></div>
|
|
153
|
+
<div id="cfg-login">
|
|
154
|
+
<label class="modal-label">手机号</label>
|
|
155
|
+
<div class="modal-row"><input id="cfg-phone" placeholder="+8801700000000" spellcheck="false"></div>
|
|
156
|
+
<label class="modal-label">接码 URL</label>
|
|
157
|
+
<div class="modal-row"><input id="cfg-code" placeholder="https://…/getcode?id=…" spellcheck="false"><button class="ghost" id="cfg-code-open" title="在该 Profile 的 tab 里打开">打开</button></div>
|
|
158
|
+
</div>
|
|
159
|
+
<label class="modal-label">代理地址</label>
|
|
160
|
+
<div class="modal-row"><input id="cfg-proxy" placeholder="留空 = 直连,例如 http://127.0.0.1:20001" spellcheck="false"><button id="cfg-save">保存</button></div>
|
|
161
|
+
<div class="modal-row"><button class="ghost" id="cfg-test">测 IP</button><span class="modal-ip" id="cfg-ip"></span></div>
|
|
162
|
+
<div class="modal-err" id="cfg-err"></div>
|
|
163
|
+
<div class="modal-danger"><button class="ghost del" id="cfg-del">删除此 Profile</button></div>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
<script>
|
|
167
|
+
const SITE_URL = 'https://www.tiktok.com/';
|
|
168
|
+
const rowsEl = document.getElementById('rows');
|
|
169
|
+
const empty = document.getElementById('empty');
|
|
170
|
+
const countEl = document.getElementById('count');
|
|
171
|
+
const addButton = document.getElementById('add-profile');
|
|
172
|
+
const preview = document.getElementById('phone-preview');
|
|
173
|
+
const selectedName = document.getElementById('selected-name');
|
|
174
|
+
const selDot = document.getElementById('sel-dot');
|
|
175
|
+
const statusEl = document.getElementById('status');
|
|
176
|
+
const ov = { empty: document.getElementById('ov-empty'), loading: document.getElementById('ov-loading'), error: document.getElementById('ov-error') };
|
|
177
|
+
const errDetail = document.getElementById('err-detail');
|
|
178
|
+
let profiles = [];
|
|
179
|
+
let selected = null;
|
|
180
|
+
const openedProfiles = new Set(); // 打开过的会话保留在后台(登录态不丢),只有选中的贴到预览区
|
|
181
|
+
const codeOpen = new Set(); // 已叠开接码 overlay 的 accountIdx(同 partition 的接码 webview 覆盖在格子上)
|
|
182
|
+
const cellState = new Map(); // accountIdx -> { loading, failed, error, url, title }
|
|
183
|
+
const rows = new Map(); // accountIdx -> row element
|
|
184
|
+
const BATCH_MAX = 10; // 一次最多铺开多少个登录格
|
|
185
|
+
const mainEl = document.querySelector('main');
|
|
186
|
+
const gridEl = document.getElementById('grid');
|
|
187
|
+
const gridCount = document.getElementById('grid-count');
|
|
188
|
+
let batchMode = false;
|
|
189
|
+
const batch = new Set(); // 批量模式下铺开的 accountIdx
|
|
190
|
+
const gcells = new Map(); // accountIdx -> { el, body }
|
|
191
|
+
|
|
192
|
+
const esc = (value) => String(value || '').replace(/[&<>"']/g, (c) => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
|
193
|
+
// 机器 id:app 打开面板时把本机 id 拼进 URL(?team=xs-3007)。两处徽章都填上。
|
|
194
|
+
(function(){ try {
|
|
195
|
+
const t = new URLSearchParams(location.search).get('team');
|
|
196
|
+
if (t) for (const id of ['mid-list','mid-batch']) {
|
|
197
|
+
const el = document.getElementById(id); if (el) { el.textContent = t; el.hidden = false; el.title = '本机: ' + t; }
|
|
198
|
+
}
|
|
199
|
+
if (t) document.title = t + ' · TikTok 矩阵';
|
|
200
|
+
} catch (e) {} })();
|
|
201
|
+
const cellId = (idx) => `tiktok-preview-${idx}`;
|
|
202
|
+
const stateOf = (idx) => cellState.get(Number(idx)) || {};
|
|
203
|
+
const dotClass = (st) => st.loading ? 'loading' : st.failed ? 'failed' : (st.url ? 'ok' : '');
|
|
204
|
+
const stateText = (st, opened) => st.loading ? '加载中…' : st.failed ? '连接失败' : st.url ? '已加载' : (opened ? '' : '未打开');
|
|
205
|
+
const fmtAgo = (iso) => { const t = Date.parse(iso || ''); if (!t) return ''; const m = Math.round((Date.now() - t) / 60000); return m < 1 ? '刚刚' : m < 60 ? `${m} 分钟前` : m < 1440 ? `${Math.round(m / 60)} 小时前` : `${Math.round(m / 1440)} 天前`; };
|
|
206
|
+
// 列表只显示一个简短地区(取最具体的一段:优先省/州,否则国家),完整信息在 tooltip。
|
|
207
|
+
const shortArea = (area) => { const seg = String(area || '').split('·').map((x) => x.trim()).filter(Boolean); return seg[1] || seg[0] || ''; };
|
|
208
|
+
function renderIp(p) {
|
|
209
|
+
const row = rows.get(p.accountIdx); if (!row) return;
|
|
210
|
+
const el = row.querySelector('.ip'); el.className = 'ip';
|
|
211
|
+
const info = p.ipInfo;
|
|
212
|
+
if (info && info.ip) {
|
|
213
|
+
const a = shortArea(info.area);
|
|
214
|
+
el.textContent = a || info.ip;
|
|
215
|
+
el.title = `出口 IP ${info.ip}${info.area ? ',' + info.area : ''},${fmtAgo(info.probedAt) || '未知时间'}探测`;
|
|
216
|
+
} else { el.classList.add('none'); el.textContent = ''; el.title = ''; }
|
|
217
|
+
}
|
|
218
|
+
// 经该 Profile 的代理探测出口 IP;成功后更新它的列表地区。返回 {ok, info|error}。
|
|
219
|
+
async function probeIp(p) {
|
|
220
|
+
try {
|
|
221
|
+
const r = await window.panelAPI.probeIp(p.accountIdx);
|
|
222
|
+
if (r && r.ipInfo) { p.ipInfo = r.ipInfo; renderIp(p); return { ok: true, info: r.ipInfo }; }
|
|
223
|
+
return { ok: false, error: (r && r.error) || '探测失败' };
|
|
224
|
+
} catch (e) { return { ok: false, error: e.message || String(e) }; }
|
|
225
|
+
}
|
|
226
|
+
// 身份:优先用本次会话探到的(cellState.identity),否则用 profile 里存的(上次登录)。
|
|
227
|
+
const identityOf = (p) => stateOf(p.accountIdx).identity || p.tiktok || null;
|
|
228
|
+
const phoneOf = (p) => (p.login && p.login.phone) || '';
|
|
229
|
+
const codeUrlOf = (p) => (p.login && p.login.codeUrl) || '';
|
|
230
|
+
function renderPhone(p) {
|
|
231
|
+
const row = rows.get(p.accountIdx); if (!row) return;
|
|
232
|
+
const el = row.querySelector('.phone'); const ph = phoneOf(p);
|
|
233
|
+
el.classList.toggle('none', !ph); el.textContent = ph; el.title = codeUrlOf(p) ? `接码:${codeUrlOf(p)}` : '';
|
|
234
|
+
const g = gcells.get(p.accountIdx); if (g) { g.el.querySelector('.phone').textContent = ph; g.el.querySelector('.code').disabled = !codeUrlOf(p); }
|
|
235
|
+
}
|
|
236
|
+
// 接码 = 在当前格子的 Telegram webview 上方叠一个同尺寸、同 partition(persist:sandbox-idx,
|
|
237
|
+
// 走该 profile 的代理出口)、更高 z 层的接码 webview。再点一次关闭。与 tg-autologin 自动化里
|
|
238
|
+
// 的 overlay 完全一致的呈现,只是重载/关闭放在格子 header(BrowserView 之上无法放页面按钮)。
|
|
239
|
+
function openCode(p) {
|
|
240
|
+
if (!codeUrlOf(p)) { flash(`#${p.accountIdx} 还没有接码 URL,点 ⚙ 填写`, 4000); return; }
|
|
241
|
+
if (!openedProfiles.has(p.accountIdx)) { openedProfiles.add(p.accountIdx); cellState.set(p.accountIdx, { loading: true }); }
|
|
242
|
+
if (codeOpen.has(p.accountIdx)) { codeOpen.delete(p.accountIdx); flash(`#${p.accountIdx} 已关闭接码`); }
|
|
243
|
+
else { codeOpen.add(p.accountIdx); flash(`#${p.accountIdx} 接码已叠加在格子上`); }
|
|
244
|
+
updateCodeButtons(p.accountIdx);
|
|
245
|
+
syncPreview();
|
|
246
|
+
}
|
|
247
|
+
function updateCodeButtons(idx) {
|
|
248
|
+
const on = codeOpen.has(idx);
|
|
249
|
+
const g = gcells.get(idx);
|
|
250
|
+
if (g) { const b = g.el.querySelector('.code'); if (b) { b.classList.toggle('on', on); b.textContent = on ? '关闭接码' : '接码'; } }
|
|
251
|
+
if (selected && selected.accountIdx === idx) { const b = document.getElementById('open-code'); if (b) { b.classList.toggle('on', on); b.textContent = on ? '关闭接码' : '接码'; } }
|
|
252
|
+
}
|
|
253
|
+
function renderIdentity(p) {
|
|
254
|
+
renderPhone(p);
|
|
255
|
+
const row = rows.get(p.accountIdx); if (!row) return;
|
|
256
|
+
const el = row.querySelector('.tg');
|
|
257
|
+
const it = identityOf(p);
|
|
258
|
+
if (it && (it.username || it.displayName || it.phone)) {
|
|
259
|
+
el.classList.remove('none');
|
|
260
|
+
el.innerHTML = (it.username ? `<b>@${esc(it.username)}</b>` : '') + (it.displayName ? `${it.username ? ' · ' : ''}${esc(it.displayName)}` : '') + (!it.username && !it.displayName && it.phone ? `+${esc(it.phone)}` : '');
|
|
261
|
+
el.title = [it.username && `@${it.username}`, it.displayName, it.phone && `+${it.phone}`].filter(Boolean).join(' ');
|
|
262
|
+
} else {
|
|
263
|
+
el.classList.add('none');
|
|
264
|
+
el.textContent = openedProfiles.has(p.accountIdx) && stateOf(p.accountIdx).url ? '未登录' : '—';
|
|
265
|
+
el.title = '';
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function flash(text, ms = 2500) {
|
|
270
|
+
statusEl.textContent = text; statusEl.dataset.sticky = '1';
|
|
271
|
+
clearTimeout(flash.t);
|
|
272
|
+
flash.t = setTimeout(() => { delete statusEl.dataset.sticky; renderPreviewState(); }, ms);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function syncPreview() {
|
|
276
|
+
if (!window.panelAPI) return;
|
|
277
|
+
const rect = preview.getBoundingClientRect();
|
|
278
|
+
const box = (r) => ({ x: r.x, y: r.y, w: r.width, h: r.height });
|
|
279
|
+
const cells = []; const overlays = [];
|
|
280
|
+
for (const profileId of openedProfiles) {
|
|
281
|
+
let visible = false, r = null;
|
|
282
|
+
if (batchMode) {
|
|
283
|
+
const g = gcells.get(profileId);
|
|
284
|
+
if (g && batch.has(profileId)) { visible = true; r = box(g.body.getBoundingClientRect()); }
|
|
285
|
+
} else if (selected && profileId === selected.accountIdx) { visible = true; r = box(rect); }
|
|
286
|
+
cells.push({ id: cellId(profileId), url: SITE_URL, profile: profileId, visible, rect: r });
|
|
287
|
+
// 接码叠加层:同 rect、同 profile(=同 partition/代理),排在 tg 格子之后 → attach 更晚 → 叠最上层
|
|
288
|
+
if (visible && codeOpen.has(profileId)) {
|
|
289
|
+
const p = profiles.find((x) => x.accountIdx === profileId); const cu = p && codeUrlOf(p);
|
|
290
|
+
if (cu) overlays.push({ id: 'code-' + profileId, url: cu, profile: profileId, visible: true, rect: r });
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
try { window.panelAPI.sync(cells.concat(overlays)); } catch (e) {}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function describeError(p, st) {
|
|
297
|
+
const e = st.error || {};
|
|
298
|
+
const desc = e.description ? `<code>${esc(e.description)}</code>` : '';
|
|
299
|
+
if (p.proxy) return `代理 <code>${esc(p.proxy)}</code> 不可用或无法访问 TikTok。${desc}<br>点 ⚙ 检查或更换该 Profile 的代理,可用「测 IP」验证。`;
|
|
300
|
+
return `当前为直连,网络无法访问 TikTok。${desc}<br>点 ⚙ 为该 Profile 设置代理。`;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function renderPreviewState() {
|
|
304
|
+
const st = selected ? stateOf(selected.accountIdx) : {};
|
|
305
|
+
ov.empty.classList.toggle('on', !selected);
|
|
306
|
+
ov.loading.classList.toggle('on', !!selected && !!st.loading && !st.url);
|
|
307
|
+
ov.error.classList.toggle('on', !!selected && !st.loading && !!st.failed);
|
|
308
|
+
if (selected && st.failed) errDetail.innerHTML = describeError(selected, st);
|
|
309
|
+
selDot.className = `dot ${selected ? dotClass(st) : ''}`;
|
|
310
|
+
if (selected && !statusEl.dataset.sticky) statusEl.textContent = stateText(st, true);
|
|
311
|
+
if (selected) updateCodeButtons(selected.accountIdx);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function renderRowState(idx) {
|
|
315
|
+
const row = rows.get(idx); if (!row) return;
|
|
316
|
+
const st = stateOf(idx);
|
|
317
|
+
row.classList.toggle('failed', !!st.failed && !st.loading);
|
|
318
|
+
row.querySelector('.dot').className = `dot ${dotClass(st)}`;
|
|
319
|
+
row.querySelector('.st').textContent = stateText(st, openedProfiles.has(idx));
|
|
320
|
+
renderGridCell(idx);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ── 批量登录:把未登录的 Profile 一次全部铺开 ──
|
|
324
|
+
const hasIdentity = (p) => { const it = identityOf(p); return !!(it && (it.username || it.displayName || it.phone)); };
|
|
325
|
+
const gridCols = (n) => n <= 1 ? 1 : n <= 4 ? 2 : n <= 6 ? 3 : n <= 8 ? 4 : 5;
|
|
326
|
+
function buildGridCell(p) {
|
|
327
|
+
const el = document.createElement('div');
|
|
328
|
+
el.className = 'gcell'; el.dataset.id = String(p.accountIdx);
|
|
329
|
+
el.innerHTML = `<div class="ghead"><span class="dot"></span><span class="name">${esc(p.name)}</span><span class="chip">#${p.accountIdx}</span><span class="phone">${esc(phoneOf(p))}</span><span class="st"></span><button class="code" title="在格子上叠加/关闭接码 webview(同代理出口)">接码</button><button class="icon reload" title="重新加载">⟳</button></div><div class="gbody"></div>`;
|
|
330
|
+
el.querySelector('.reload').onclick = () => reloadProfile(p.accountIdx);
|
|
331
|
+
el.querySelector('.code').onclick = () => openCode(p);
|
|
332
|
+
el.querySelector('.code').disabled = !codeUrlOf(p);
|
|
333
|
+
el.querySelector('.ghead').onclick = (e) => { if (!e.target.closest('button')) { exitBatch(); selectProfile(p.accountIdx); } };
|
|
334
|
+
return { el, body: el.querySelector('.gbody') };
|
|
335
|
+
}
|
|
336
|
+
function renderGridCell(idx) {
|
|
337
|
+
const g = gcells.get(idx); if (!g) return;
|
|
338
|
+
const p = profiles.find((x) => x.accountIdx === idx); const st = stateOf(idx);
|
|
339
|
+
const done = !!p && hasIdentity(p);
|
|
340
|
+
g.el.classList.toggle('done', done);
|
|
341
|
+
g.el.classList.toggle('failed', !!st.failed && !st.loading);
|
|
342
|
+
g.el.querySelector('.dot').className = `dot ${dotClass(st)}`;
|
|
343
|
+
g.el.querySelector('.st').textContent = done ? '✓ 已登录' : stateText(st, true);
|
|
344
|
+
updateCodeButtons(idx);
|
|
345
|
+
if (batchMode) { const n = [...batch].filter((i) => { const q = profiles.find((x) => x.accountIdx === i); return q && hasIdentity(q); }).length; gridCount.textContent = `${batch.size} 个会话 · ${n} 已登录`; }
|
|
346
|
+
}
|
|
347
|
+
function enterBatch() {
|
|
348
|
+
// 默认网格 = 平铺全部 profile 的会话(不再只挑未登录的);仍受 BATCH_MAX 上限保护。
|
|
349
|
+
const todo = profiles.slice(0, BATCH_MAX);
|
|
350
|
+
if (!todo.length) { flash('没有 Profile'); return; }
|
|
351
|
+
batch.clear(); gridEl.innerHTML = ''; gcells.clear();
|
|
352
|
+
for (const p of todo) {
|
|
353
|
+
batch.add(p.accountIdx);
|
|
354
|
+
const g = buildGridCell(p); gcells.set(p.accountIdx, g); gridEl.appendChild(g.el);
|
|
355
|
+
if (!openedProfiles.has(p.accountIdx)) { openedProfiles.add(p.accountIdx); cellState.set(p.accountIdx, { loading: true }); }
|
|
356
|
+
renderRowState(p.accountIdx);
|
|
357
|
+
}
|
|
358
|
+
gridEl.style.gridTemplateColumns = `repeat(${gridCols(todo.length)}, minmax(0, 1fr))`;
|
|
359
|
+
batchMode = true; mainEl.classList.add('batch'); document.body.classList.add('batch'); document.getElementById('batch-login').textContent = '退出批量';
|
|
360
|
+
render();
|
|
361
|
+
syncPreview(); // 后台 tab 里 rAF 不会触发,先直接同步一次把格子建出来
|
|
362
|
+
requestAnimationFrame(syncPreview);
|
|
363
|
+
setTimeout(pullStates, 300);
|
|
364
|
+
}
|
|
365
|
+
function exitBatch() {
|
|
366
|
+
if (!batchMode) return;
|
|
367
|
+
batchMode = false; mainEl.classList.remove('batch'); document.body.classList.remove('batch'); document.getElementById('batch-login').textContent = '批量网格';
|
|
368
|
+
batch.clear(); gcells.clear(); gridEl.innerHTML = '';
|
|
369
|
+
render(); renderPreviewState(); syncPreview(); // 会话留在后台,登录态不丢
|
|
370
|
+
}
|
|
371
|
+
document.getElementById('batch-login').onclick = () => (batchMode ? exitBatch() : enterBatch());
|
|
372
|
+
document.getElementById('grid-exit').onclick = exitBatch;
|
|
373
|
+
document.getElementById('grid-reload').onclick = () => { for (const idx of batch) reloadProfile(idx); };
|
|
374
|
+
new ResizeObserver(() => requestAnimationFrame(syncPreview)).observe(gridEl);
|
|
375
|
+
|
|
376
|
+
function buildRow(p) {
|
|
377
|
+
const row = document.createElement('div');
|
|
378
|
+
row.className = 'row'; row.dataset.id = String(p.accountIdx); row.title = `persist:sandbox-${p.accountIdx}`;
|
|
379
|
+
row.innerHTML = `
|
|
380
|
+
<div class="line1"><span class="dot"></span><span class="name">${esc(p.name)}</span><span class="chip">#${p.accountIdx}</span><span class="st"></span><button class="icon gear" title="代理设置">⚙</button><button class="icon reload" title="重新加载">⟳</button></div>
|
|
381
|
+
<div class="line2"><span class="tg none">—</span><span class="phone none"></span><span class="ip none"></span></div>`;
|
|
382
|
+
// ⚙ 打开设置弹窗(代理 / 测 IP / 删除),不再 inline。
|
|
383
|
+
row.querySelector('.gear').onclick = (e) => { e.stopPropagation(); openCfgModal(p); };
|
|
384
|
+
row.onclick = () => { exitBatch(); selectProfile(p.accountIdx); };
|
|
385
|
+
row.querySelector('.reload').onclick = (e) => { e.stopPropagation(); reloadProfile(p.accountIdx); };
|
|
386
|
+
return row;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function reloadProfile(idx) {
|
|
390
|
+
// 接码 overlay 开着时,重载指向接码 webview(用户看到的最上层就是它)
|
|
391
|
+
if (codeOpen.has(idx)) { try { window.panelAPI.reload('code-' + idx); } catch (e) {} return; }
|
|
392
|
+
const fresh = !openedProfiles.has(idx);
|
|
393
|
+
openedProfiles.add(idx);
|
|
394
|
+
cellState.set(idx, { loading: true }); renderRowState(idx);
|
|
395
|
+
if (selected && selected.accountIdx === idx) renderPreviewState();
|
|
396
|
+
syncPreview();
|
|
397
|
+
// 首次打开:上面的 sync() 已经会建视图并加载 URL,再 reload 只是白加载一次。
|
|
398
|
+
if (!fresh) { try { window.panelAPI.reload(cellId(idx)); } catch (e) {} }
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function render() {
|
|
402
|
+
empty.classList.toggle('on', profiles.length === 0);
|
|
403
|
+
countEl.textContent = profiles.length ? `${profiles.length} 个会话` : '';
|
|
404
|
+
for (const p of profiles) {
|
|
405
|
+
if (!rows.has(p.accountIdx)) { const row = buildRow(p); rows.set(p.accountIdx, row); rowsEl.appendChild(row); }
|
|
406
|
+
rows.get(p.accountIdx).classList.toggle('active', !!selected && selected.accountIdx === p.accountIdx);
|
|
407
|
+
renderRowState(p.accountIdx);
|
|
408
|
+
renderIdentity(p);
|
|
409
|
+
renderIp(p);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function selectProfile(accountIdx) {
|
|
414
|
+
selected = profiles.find((p) => p.accountIdx === Number(accountIdx)) || null;
|
|
415
|
+
if (selected) { openedProfiles.add(selected.accountIdx); localStorage.setItem('tiktok-matrix-profile', String(selected.accountIdx)); }
|
|
416
|
+
selectedName.textContent = selected ? `${selected.name} · #${selected.accountIdx}` : '未选择 Profile';
|
|
417
|
+
selectedName.title = selected ? `persist:sandbox-${selected.accountIdx}` : '';
|
|
418
|
+
delete statusEl.dataset.sticky;
|
|
419
|
+
render();
|
|
420
|
+
renderPreviewState();
|
|
421
|
+
syncPreview();
|
|
422
|
+
if (selected && !cellState.has(selected.accountIdx)) setTimeout(pullStates, 300);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function applyCellState(state) {
|
|
426
|
+
const m = /^tiktok-preview-(\d+)$/.exec(state.id || '');
|
|
427
|
+
if (!m) return;
|
|
428
|
+
const idx = Number(m[1]);
|
|
429
|
+
const prev = stateOf(idx);
|
|
430
|
+
const failed = !!state.failed || /^chrome-error:/.test(state.url || '');
|
|
431
|
+
const next = { ...prev, loading: !!state.loading };
|
|
432
|
+
if (state.url !== undefined) { next.url = failed ? '' : state.url; next.title = state.title || ''; }
|
|
433
|
+
if (!state.loading) { next.failed = failed; next.error = state.error || (failed ? prev.error : null); }
|
|
434
|
+
if (state.identity) next.identity = state.identity;
|
|
435
|
+
cellState.set(idx, next);
|
|
436
|
+
renderRowState(idx);
|
|
437
|
+
{ const p = profiles.find((x) => x.accountIdx === idx); if (p) renderIdentity(p); }
|
|
438
|
+
renderGridCell(idx);
|
|
439
|
+
if (selected && selected.accountIdx === idx) renderPreviewState();
|
|
440
|
+
}
|
|
441
|
+
async function pullStates({ adopt = false } = {}) {
|
|
442
|
+
let list = [];
|
|
443
|
+
try { list = await window.panelAPI.states(); } catch (e) { return; }
|
|
444
|
+
for (const st of list || []) {
|
|
445
|
+
// 面板页刷新后 openedProfiles 是空的,而主进程里那些会话视图还活着。
|
|
446
|
+
// 不先认领回来,第一次 syncPreview() 就会把它们当多余格子销毁,后台
|
|
447
|
+
// 登录着的会话全没(实测:刷新后 #1 的 webContents 被销毁)。
|
|
448
|
+
if (adopt) {
|
|
449
|
+
const m = /^tiktok-preview-(\d+)$/.exec(st.id || '');
|
|
450
|
+
if (m) openedProfiles.add(Number(m[1]));
|
|
451
|
+
}
|
|
452
|
+
applyCellState(st);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
async function loadProfiles(preferredId) {
|
|
457
|
+
profiles = await window.panelAPI.profiles();
|
|
458
|
+
profiles.sort((a, b) => a.accountIdx - b.accountIdx);
|
|
459
|
+
const remembered = Number(localStorage.getItem('tiktok-matrix-profile'));
|
|
460
|
+
const target = preferredId || (profiles.some((p) => p.accountIdx === remembered) ? remembered : 0) || (profiles[0] && profiles[0].accountIdx);
|
|
461
|
+
selectProfile(target);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
async function removeProfile(p) {
|
|
465
|
+
const idx = p.accountIdx;
|
|
466
|
+
await window.panelAPI.removeProfile(idx);
|
|
467
|
+
openedProfiles.delete(idx); cellState.delete(idx);
|
|
468
|
+
const row = rows.get(idx); if (row) row.remove(); rows.delete(idx);
|
|
469
|
+
profiles = profiles.filter((x) => x.accountIdx !== idx);
|
|
470
|
+
if (selected && selected.accountIdx === idx) {
|
|
471
|
+
const next = profiles[0] ? profiles[0].accountIdx : 0;
|
|
472
|
+
if (next) selectProfile(next); else { selected = null; localStorage.removeItem('tiktok-matrix-profile'); selectedName.textContent = '未选择 Profile'; render(); renderPreviewState(); syncPreview(); }
|
|
473
|
+
} else render();
|
|
474
|
+
flash(`Profile #${idx} 已删除`);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
addButton.onclick = async () => {
|
|
478
|
+
addButton.disabled = true; flash('正在创建…', 60000);
|
|
479
|
+
try {
|
|
480
|
+
const profile = await window.panelAPI.addProfile();
|
|
481
|
+
await loadProfiles(profile.accountIdx);
|
|
482
|
+
flash(`Profile #${profile.accountIdx} 已创建`);
|
|
483
|
+
const row = rows.get(profile.accountIdx); if (row) row.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
484
|
+
} catch (e) { flash(e.message || String(e), 6000); }
|
|
485
|
+
finally { addButton.disabled = false; }
|
|
486
|
+
};
|
|
487
|
+
document.getElementById('reload').onclick = () => { if (selected) reloadProfile(selected.accountIdx); };
|
|
488
|
+
document.getElementById('open-code').onclick = () => { if (selected) openCode(selected); };
|
|
489
|
+
document.getElementById('err-retry').onclick = () => { if (selected) reloadProfile(selected.accountIdx); };
|
|
490
|
+
document.getElementById('err-cfg').onclick = () => { if (selected) openCfgModal(selected); };
|
|
491
|
+
document.getElementById('probe-ip').onclick = async () => {
|
|
492
|
+
if (!selected) { flash('先选择一个 Profile'); return; }
|
|
493
|
+
const b = document.getElementById('probe-ip'); const o = b.textContent; b.disabled = true; b.textContent = '测 IP…';
|
|
494
|
+
const r = await probeIp(selected);
|
|
495
|
+
b.disabled = false; b.textContent = o;
|
|
496
|
+
flash(r.ok ? `#${selected.accountIdx} 出口:${shortArea(selected.ipInfo.area) || selected.ipInfo.ip}` : `IP 探测失败:${r.error}`, 6000);
|
|
497
|
+
};
|
|
498
|
+
// ── 设置弹窗(代理 / 测 IP / 删除) ──
|
|
499
|
+
const cfgModal = document.getElementById('cfg-modal');
|
|
500
|
+
const cfgProxy = document.getElementById('cfg-proxy');
|
|
501
|
+
const cfgErr = document.getElementById('cfg-err');
|
|
502
|
+
const cfgIp = document.getElementById('cfg-ip');
|
|
503
|
+
const cfgDel = document.getElementById('cfg-del');
|
|
504
|
+
let cfgP = null, delArmed = false;
|
|
505
|
+
const cfgPhone = document.getElementById('cfg-phone');
|
|
506
|
+
const cfgCode = document.getElementById('cfg-code');
|
|
507
|
+
document.getElementById('cfg-code-open').onclick = () => { if (cfgP) openCode({ ...cfgP, login: { phone: cfgPhone.value, codeUrl: cfgCode.value.trim() } }); };
|
|
508
|
+
const cfgIpText = (p) => p && p.ipInfo && p.ipInfo.ip ? (p.ipInfo.area ? `${p.ipInfo.area} · ${p.ipInfo.ip}` : p.ipInfo.ip) : '未探测';
|
|
509
|
+
function openCfgModal(p) {
|
|
510
|
+
if (!p) return; cfgP = p; delArmed = false;
|
|
511
|
+
document.getElementById('cfg-title').textContent = `${p.name} · #${p.accountIdx} 设置`;
|
|
512
|
+
cfgProxy.value = p.proxy || ''; cfgErr.textContent = ''; cfgIp.textContent = cfgIpText(p);
|
|
513
|
+
cfgPhone.value = phoneOf(p); cfgCode.value = codeUrlOf(p);
|
|
514
|
+
cfgDel.textContent = '删除此 Profile'; cfgDel.disabled = false;
|
|
515
|
+
cfgModal.hidden = false; cfgProxy.focus();
|
|
516
|
+
// BrowserView 预览永远浮在页面 DOM 之上,会盖住弹窗 —— 打开弹窗时把 cells 脱离(复用拖拽逻辑),关闭时贴回。
|
|
517
|
+
try { window.panelAPI.dragging(true); } catch (e) {}
|
|
518
|
+
}
|
|
519
|
+
function closeCfgModal() {
|
|
520
|
+
cfgModal.hidden = true; cfgP = null; delArmed = false;
|
|
521
|
+
try { window.panelAPI.dragging(false); } catch (e) {}
|
|
522
|
+
syncPreview();
|
|
523
|
+
}
|
|
524
|
+
cfgModal.onclick = (e) => { if (e.target === cfgModal) closeCfgModal(); };
|
|
525
|
+
document.getElementById('cfg-x').onclick = closeCfgModal;
|
|
526
|
+
document.getElementById('cfg-save').onclick = async () => {
|
|
527
|
+
if (!cfgP) return; const b = document.getElementById('cfg-save'); b.disabled = true; cfgErr.textContent = '';
|
|
528
|
+
try {
|
|
529
|
+
const lg = await window.panelAPI.setProfileLogin(cfgP.accountIdx, cfgPhone.value.trim(), cfgCode.value.trim());
|
|
530
|
+
cfgP.login = lg.login; renderPhone(cfgP);
|
|
531
|
+
const u = await window.panelAPI.setProfileProxy(cfgP.accountIdx, cfgProxy.value.trim());
|
|
532
|
+
cfgP.proxy = u.proxy; cfgP.ipInfo = null; renderIp(cfgP);
|
|
533
|
+
if (openedProfiles.has(cfgP.accountIdx)) { reloadProfile(cfgP.accountIdx); flash(`#${cfgP.accountIdx} 代理已应用,重新加载中`); }
|
|
534
|
+
else flash(`#${cfgP.accountIdx} 代理已保存,打开时生效`);
|
|
535
|
+
closeCfgModal();
|
|
536
|
+
} catch (e) { cfgErr.textContent = e.message || String(e); } finally { b.disabled = false; }
|
|
537
|
+
};
|
|
538
|
+
cfgProxy.onkeydown = (e) => { if (e.key === 'Enter') document.getElementById('cfg-save').onclick(); if (e.key === 'Escape') closeCfgModal(); };
|
|
539
|
+
document.getElementById('cfg-test').onclick = async () => {
|
|
540
|
+
if (!cfgP) return; const b = document.getElementById('cfg-test'); const o = b.textContent; b.disabled = true; b.textContent = '测 IP…'; cfgErr.textContent = '';
|
|
541
|
+
const r = await probeIp(cfgP);
|
|
542
|
+
b.disabled = false; b.textContent = o;
|
|
543
|
+
if (r.ok) cfgIp.textContent = cfgIpText(cfgP); else cfgErr.textContent = '探测失败:' + r.error;
|
|
544
|
+
};
|
|
545
|
+
cfgDel.onclick = async () => {
|
|
546
|
+
if (!cfgP) return;
|
|
547
|
+
if (!delArmed) { delArmed = true; cfgDel.textContent = '确认删除(不可恢复)'; setTimeout(() => { if (delArmed) { delArmed = false; cfgDel.textContent = '删除此 Profile'; } }, 4000); return; }
|
|
548
|
+
delArmed = false; cfgDel.disabled = true; cfgErr.textContent = '';
|
|
549
|
+
try { await removeProfile(cfgP); closeCfgModal(); }
|
|
550
|
+
catch (e) { cfgErr.textContent = e.message || String(e); cfgDel.disabled = false; cfgDel.textContent = '删除此 Profile'; }
|
|
551
|
+
};
|
|
552
|
+
document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && !cfgModal.hidden) closeCfgModal(); });
|
|
553
|
+
|
|
554
|
+
new ResizeObserver(() => requestAnimationFrame(syncPreview)).observe(preview);
|
|
555
|
+
window.addEventListener('resize', syncPreview);
|
|
556
|
+
document.addEventListener('visibilitychange', () => { if (!document.hidden) { syncPreview(); setTimeout(syncPreview, 500); setTimeout(pullStates, 800); } });
|
|
557
|
+
try { window.panelAPI.onCellState(applyCellState); } catch (e) {}
|
|
558
|
+
pullStates({ adopt: true })
|
|
559
|
+
.catch(() => {})
|
|
560
|
+
.then(() => loadProfiles())
|
|
561
|
+
// 打开面板即默认进批量网格:把所有 profile 的 webview 平铺出来。
|
|
562
|
+
// 崩溃循环保护:若上次开机自动批量后没在几秒内清除标记(说明渲染进程崩溃重载了),
|
|
563
|
+
// 这次跳过自动批量、停在列表,避免弱机反复「崩溃→自动批量→再崩」的刷新循环。
|
|
564
|
+
.then(() => {
|
|
565
|
+
try {
|
|
566
|
+
const K = 'tk-matrix-autobatch-pending';
|
|
567
|
+
let prev = null; try { prev = localStorage.getItem(K); } catch (e) {}
|
|
568
|
+
if (prev && (Date.now() - Number(prev)) < 20000) {
|
|
569
|
+
try { localStorage.removeItem(K); } catch (e) {}
|
|
570
|
+
flash('上次批量疑似导致崩溃,已跳过自动批量。点「批量网格」可手动打开。', 6000);
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
try { localStorage.setItem(K, String(Date.now())); } catch (e) {}
|
|
574
|
+
enterBatch();
|
|
575
|
+
setTimeout(() => { try { localStorage.removeItem(K); } catch (e) {} }, 8000);
|
|
576
|
+
} catch (e) {}
|
|
577
|
+
})
|
|
578
|
+
.catch((e) => { flash(e.message || String(e), 8000); render(); });
|
|
579
|
+
|
|
580
|
+
// 自动更新:构建时给本页打了内容版本号(<meta name="pv">)。每 60s 拉一次自身
|
|
581
|
+
// (同源 cicyui://panel,无跨域;cicyui handler 每次都从远端取最新),版本变了就 reload。
|
|
582
|
+
// 这样服务端一部署,所有开着的面板自己就切到新版,不用手动逐台重载。DEV 态不轮询。
|
|
583
|
+
(function autoUpdate() {
|
|
584
|
+
const meta = document.querySelector('meta[name="pv"]');
|
|
585
|
+
const PV = meta ? meta.content : '';
|
|
586
|
+
if (!PV || PV === 'DEV') return;
|
|
587
|
+
setInterval(async () => {
|
|
588
|
+
try {
|
|
589
|
+
const r = await fetch(location.href, { cache: 'no-store' });
|
|
590
|
+
if (!r.ok) return;
|
|
591
|
+
const t = await r.text();
|
|
592
|
+
const mm = t.match(/<meta name="pv" content="([^"]*)">/);
|
|
593
|
+
if (mm && mm[1] && mm[1] !== PV) location.reload();
|
|
594
|
+
} catch (e) {}
|
|
595
|
+
}, 60000);
|
|
596
|
+
})();
|
|
597
|
+
</script>
|
|
598
|
+
</body>
|
|
599
|
+
</html>
|