cicy-desktop 2.1.332 → 2.1.334
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 +20 -0
- package/src/backends/homepage-react/panel/facebook-matrix.html +381 -0
- package/src/backends/homepage-react/panel/redroid-matrix.html +668 -0
- package/src/backends/homepage-react/panel/split-panel.html +832 -0
- package/src/backends/homepage-react/panel/telegram-matrix.html +521 -0
- package/src/backends/homepage-window.js +18 -18
- package/src/tabbrowser/newtab-protocol.js +36 -3
- package/test/homepage-remote.test.js +26 -13
- package/test/panel-remote-source.test.js +73 -0
- package/workers/render/public/panel/facebook-matrix.html +381 -0
- package/workers/render/public/panel/redroid-matrix.html +668 -0
- package/workers/render/public/panel/split-panel.html +832 -0
- package/workers/render/public/panel/telegram-matrix.html +521 -0
package/package.json
CHANGED
|
@@ -28,6 +28,26 @@ function run(cmd, cwd) {
|
|
|
28
28
|
execSync(cmd, { cwd, stdio: "inherit" });
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
// 0) panel pages: src/tabbrowser is the SINGLE SOURCE. cicyui://panel now
|
|
32
|
+
// fetches them from the Worker (newtab-protocol) with the bundled copy as
|
|
33
|
+
// the fallback, so the Worker needs its own copy — generated here, never
|
|
34
|
+
// hand-maintained. Two hand-kept copies is exactly how the stale homepage
|
|
35
|
+
// shipped before; this one is regenerated on every build and gitignored.
|
|
36
|
+
const PANEL_SRC = path.join(ROOT, "src", "tabbrowser");
|
|
37
|
+
const PANEL_OUT = path.join(RENDER, "public", "panel");
|
|
38
|
+
const PANEL_PAGES = [
|
|
39
|
+
"telegram-matrix.html",
|
|
40
|
+
"redroid-matrix.html",
|
|
41
|
+
"facebook-matrix.html",
|
|
42
|
+
"split-panel.html",
|
|
43
|
+
];
|
|
44
|
+
fs.rmSync(PANEL_OUT, { recursive: true, force: true });
|
|
45
|
+
fs.mkdirSync(PANEL_OUT, { recursive: true });
|
|
46
|
+
for (const page of PANEL_PAGES) {
|
|
47
|
+
fs.copyFileSync(path.join(PANEL_SRC, page), path.join(PANEL_OUT, page));
|
|
48
|
+
}
|
|
49
|
+
console.log(`[build-homepage] panel pages synced: ${PANEL_PAGES.join(", ")}`);
|
|
50
|
+
|
|
31
51
|
// 1) build the SPA from source (install deps on a cold runner)
|
|
32
52
|
if (!fs.existsSync(path.join(RENDER, "node_modules"))) {
|
|
33
53
|
run("npm install --no-audit --no-fund", RENDER);
|
|
@@ -0,0 +1,381 @@
|
|
|
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
|
+
<title>Facebook 矩阵</title>
|
|
7
|
+
<style>
|
|
8
|
+
: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; }
|
|
9
|
+
* { box-sizing: border-box; }
|
|
10
|
+
html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; background: var(--bg); color: var(--fg); }
|
|
11
|
+
body { display: grid; grid-template-columns: minmax(320px, 34%) 1fr; }
|
|
12
|
+
aside { min-width: 0; border-right: 1px solid var(--line); background: var(--bg); display: flex; flex-direction: column; }
|
|
13
|
+
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); }
|
|
14
|
+
h1 { margin: 0; font-size: 15px; display: flex; align-items: baseline; gap: 8px; }
|
|
15
|
+
h1 small { color: var(--muted); font-weight: 500; font-size: 12px; }
|
|
16
|
+
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; }
|
|
17
|
+
button:hover { filter: brightness(1.08); }
|
|
18
|
+
button:disabled { opacity: .55; cursor: wait; }
|
|
19
|
+
button.ghost { background: var(--head); color: var(--fg); border: 1px solid var(--line); }
|
|
20
|
+
button.sm { padding: 4px 8px; font-size: 12px; }
|
|
21
|
+
.icon { background: transparent; color: #b4b4bb; padding: 3px 6px; font-size: 13px; border-radius: 6px; }
|
|
22
|
+
.icon:hover { background: var(--hover); color: #fff; }
|
|
23
|
+
.list { flex: 1; overflow: auto; padding: 8px; }
|
|
24
|
+
#rows { display: flex; flex-direction: column; gap: 4px; }
|
|
25
|
+
.row { padding: 9px 10px; border-radius: 10px; cursor: pointer; border: 1px solid transparent; min-width: 0; }
|
|
26
|
+
.row:hover { background: var(--hover); }
|
|
27
|
+
.row.active { background: var(--cell); border-color: rgba(59,130,246,.55); }
|
|
28
|
+
.row.failed .name { color: #ff9e9e; }
|
|
29
|
+
.line1 { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
30
|
+
.dot { width: 8px; height: 8px; border-radius: 50%; background: #3a3a40; flex: none; }
|
|
31
|
+
.dot.loading { background: #f0b429; animation: pulse 1s infinite alternate; }
|
|
32
|
+
.dot.ok { background: #3fb950; }
|
|
33
|
+
.dot.failed { background: #ff5c5c; }
|
|
34
|
+
@keyframes pulse { from { opacity: .35; } to { opacity: 1; } }
|
|
35
|
+
.name { font-weight: 650; font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
|
|
36
|
+
.chip { flex: none; padding: 1px 6px; border-radius: 999px; background: var(--head); color: #b4b4bb; font: 11px ui-monospace, monospace; }
|
|
37
|
+
.st { flex: none; color: var(--muted); font-size: 11px; white-space: nowrap; margin-left: auto; }
|
|
38
|
+
.line2 { display: flex; align-items: center; gap: 8px; margin-top: 4px; padding-left: 16px; font-size: 11.5px; color: var(--muted); min-width: 0; }
|
|
39
|
+
.line2 .tg { flex: none; max-width: 55%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: #b4b4bb; }
|
|
40
|
+
.line2 .tg b { color: var(--fg); font-weight: 600; }
|
|
41
|
+
.line2 .tg.none { color: #6b6b72; }
|
|
42
|
+
/* 编辑态就地变成可输入,尺寸/位置与只读态完全一致,不发生位移 */
|
|
43
|
+
.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; cursor: pointer; }
|
|
44
|
+
.line2 .ip.none { color: #6b6b72; font-family: inherit; }
|
|
45
|
+
.line2 .ip.busy { color: #f0b429; }
|
|
46
|
+
.line2 .ip.bad { color: #ff8a8a; }
|
|
47
|
+
.line2 .ip:hover { color: #fff; }
|
|
48
|
+
.cfg { display: none; margin: 8px 0 2px 16px; padding: 8px 10px; border: 1px solid var(--line); border-radius: 8px; background: var(--head); }
|
|
49
|
+
.row.cfg-open .cfg { display: block; }
|
|
50
|
+
.row.cfg-open .gear { color: var(--accent); }
|
|
51
|
+
.cfg .f { display: flex; align-items: center; gap: 6px; }
|
|
52
|
+
.cfg .f + .f { margin-top: 6px; }
|
|
53
|
+
.cfg label { flex: none; width: 34px; font-size: 11px; color: var(--muted); }
|
|
54
|
+
.cfg input { flex: 1; min-width: 0; height: 26px; border: 1px solid var(--line); border-radius: 6px; padding: 0 8px; background: #111113; color: var(--fg); font: 11.5px ui-monospace, monospace; outline: 0; }
|
|
55
|
+
.cfg input:focus { border-color: var(--accent); }
|
|
56
|
+
.cfg .hint { font-size: 11px; color: var(--muted); padding-left: 40px; }
|
|
57
|
+
.cfg .hint b { color: #b4b4bb; font-weight: 500; }
|
|
58
|
+
.cfg .danger { display: flex; align-items: center; gap: 6px; margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--line); font-size: 11px; color: var(--muted); }
|
|
59
|
+
.cfg .danger .sp { flex: 1; }
|
|
60
|
+
.cfg .confirm { display: none; align-items: center; gap: 6px; margin-top: 8px; padding: 6px 8px; border-radius: 6px; background: rgba(255,92,92,.12); color: #ffb3b3; font-size: 11.5px; }
|
|
61
|
+
.cfg .confirm.on { display: flex; }
|
|
62
|
+
.cfg .confirm .sp { flex: 1; }
|
|
63
|
+
button.del { background: #b3261e; }
|
|
64
|
+
.row-err { padding: 4px 0 0 16px; color: #ff8a8a; font-size: 11px; display: none; }
|
|
65
|
+
.row-err.on { display: block; }
|
|
66
|
+
#empty { display: none; padding: 40px 24px; color: var(--muted); text-align: center; line-height: 1.8; }
|
|
67
|
+
#empty.on { display: block; }
|
|
68
|
+
.hint { flex: none; padding: 8px 16px; color: var(--muted); font-size: 11px; border-top: 1px solid var(--line); }
|
|
69
|
+
main { min-width: 0; display: flex; flex-direction: column; align-items: center; padding: 14px 18px; background: var(--bg); }
|
|
70
|
+
#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; }
|
|
71
|
+
#preview-title .left { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
72
|
+
#selected-name { color: var(--fg); font-weight: 600; font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
73
|
+
#status { color: var(--muted); white-space: nowrap; }
|
|
74
|
+
.actions { display: flex; gap: 6px; flex: none; }
|
|
75
|
+
.actions button { padding: 5px 9px; font-size: 12px; }
|
|
76
|
+
#phone { position: relative; width: min(410px, calc(100% - 20px)); height: min(760px, calc(100vh - 92px)); 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); }
|
|
77
|
+
#phone-preview { width: 100%; height: 100%; overflow: hidden; border-radius: 19px; background: #fff; }
|
|
78
|
+
.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; }
|
|
79
|
+
.overlay.on { display: grid; }
|
|
80
|
+
.overlay h2 { margin: 0 0 8px; font-size: 16px; color: #ff8a8a; }
|
|
81
|
+
.overlay p { margin: 0 0 14px; font-size: 13px; line-height: 1.6; color: var(--muted); word-break: break-all; }
|
|
82
|
+
.overlay code { color: var(--fg); font: 12px ui-monospace, monospace; }
|
|
83
|
+
.overlay .btns { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; }
|
|
84
|
+
.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; }
|
|
85
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
86
|
+
</style>
|
|
87
|
+
</head>
|
|
88
|
+
<body>
|
|
89
|
+
<aside>
|
|
90
|
+
<header>
|
|
91
|
+
<h1>Facebook 矩阵 <small id="count"></small></h1>
|
|
92
|
+
<button id="add-profile" title="新建一个独立会话(persist:sandbox-N)">+ 添加 Profile</button>
|
|
93
|
+
</header>
|
|
94
|
+
<div class="list">
|
|
95
|
+
<div id="rows"></div>
|
|
96
|
+
<div id="empty">还没有 Profile<br>点击右上角「添加 Profile」创建第一个 Facebook 会话</div>
|
|
97
|
+
</div>
|
|
98
|
+
<div class="hint">点击行切换预览;点代理地址可修改(留空 = 直连)。已打开过的会话会在后台保持登录。</div>
|
|
99
|
+
</aside>
|
|
100
|
+
<main>
|
|
101
|
+
<div id="preview-title">
|
|
102
|
+
<div class="left"><span class="dot" id="sel-dot"></span><span id="selected-name">未选择 Profile</span><span id="status"></span></div>
|
|
103
|
+
<div class="actions"><button class="ghost" id="reload" title="重新加载 Facebook">重新加载</button></div>
|
|
104
|
+
</div>
|
|
105
|
+
<div id="phone">
|
|
106
|
+
<div id="phone-preview"></div>
|
|
107
|
+
<div class="overlay on" id="ov-empty"><div>选择左侧一个 Profile 预览 Facebook</div></div>
|
|
108
|
+
<div class="overlay" id="ov-loading"><div><div class="spinner"></div><div>正在加载 Facebook…</div></div></div>
|
|
109
|
+
<div class="overlay" id="ov-error">
|
|
110
|
+
<div>
|
|
111
|
+
<h2>无法连接 Facebook</h2>
|
|
112
|
+
<p id="err-detail"></p>
|
|
113
|
+
<div class="btns"><button id="err-retry">重试</button><button class="ghost" id="err-cfg">代理设置</button></div>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</main>
|
|
118
|
+
<script>
|
|
119
|
+
const SITE_URL = 'https://www.facebook.com/';
|
|
120
|
+
const rowsEl = document.getElementById('rows');
|
|
121
|
+
const empty = document.getElementById('empty');
|
|
122
|
+
const countEl = document.getElementById('count');
|
|
123
|
+
const addButton = document.getElementById('add-profile');
|
|
124
|
+
const preview = document.getElementById('phone-preview');
|
|
125
|
+
const selectedName = document.getElementById('selected-name');
|
|
126
|
+
const selDot = document.getElementById('sel-dot');
|
|
127
|
+
const statusEl = document.getElementById('status');
|
|
128
|
+
const ov = { empty: document.getElementById('ov-empty'), loading: document.getElementById('ov-loading'), error: document.getElementById('ov-error') };
|
|
129
|
+
const errDetail = document.getElementById('err-detail');
|
|
130
|
+
let profiles = [];
|
|
131
|
+
let selected = null;
|
|
132
|
+
const openedProfiles = new Set(); // 打开过的会话保留在后台(登录态不丢),只有选中的贴到预览区
|
|
133
|
+
const cellState = new Map(); // accountIdx -> { loading, failed, error, url, title }
|
|
134
|
+
const rows = new Map(); // accountIdx -> row element
|
|
135
|
+
|
|
136
|
+
const esc = (value) => String(value || '').replace(/[&<>"']/g, (c) => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
|
137
|
+
const cellId = (idx) => `facebook-preview-${idx}`;
|
|
138
|
+
const stateOf = (idx) => cellState.get(Number(idx)) || {};
|
|
139
|
+
const dotClass = (st) => st.loading ? 'loading' : st.failed ? 'failed' : (st.url ? 'ok' : '');
|
|
140
|
+
const stateText = (st, opened) => st.loading ? '加载中…' : st.failed ? '连接失败' : st.url ? '已加载' : (opened ? '' : '未打开');
|
|
141
|
+
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)} 天前`; };
|
|
142
|
+
function renderIp(p, mode) {
|
|
143
|
+
const row = rows.get(p.accountIdx); if (!row) return;
|
|
144
|
+
const el = row.querySelector('.ip');
|
|
145
|
+
el.className = 'ip';
|
|
146
|
+
if (mode === 'busy') { el.classList.add('busy'); el.textContent = '测 IP…'; el.title = ''; return; }
|
|
147
|
+
if (mode && mode.error) { el.classList.add('bad'); el.textContent = 'IP 探测失败'; el.title = mode.error + '(点击重试)'; return; }
|
|
148
|
+
const info = p.ipInfo;
|
|
149
|
+
if (info && info.ip) {
|
|
150
|
+
el.textContent = info.area ? `${info.area} · ${info.ip}` : info.ip;
|
|
151
|
+
el.title = `出口 IP ${info.ip}${info.area ? ',' + info.area : ''},${fmtAgo(info.probedAt) || '未知时间'}探测(点击重测)`;
|
|
152
|
+
} else { el.classList.add('none'); el.textContent = '测 IP'; el.title = '经该 Profile 的代理探测出口 IP / 地区'; }
|
|
153
|
+
}
|
|
154
|
+
async function probeIp(p) {
|
|
155
|
+
renderIp(p, 'busy');
|
|
156
|
+
try {
|
|
157
|
+
const r = await window.panelAPI.probeIp(p.accountIdx);
|
|
158
|
+
if (r && r.ipInfo) { p.ipInfo = r.ipInfo; renderIp(p); }
|
|
159
|
+
else renderIp(p, { error: (r && r.error) || '探测失败' });
|
|
160
|
+
} catch (e) { renderIp(p, { error: e.message || String(e) }); }
|
|
161
|
+
}
|
|
162
|
+
// 身份:优先用本次会话探到的(cellState.identity),否则用 profile 里存的(上次登录)。
|
|
163
|
+
const identityOf = (p) => stateOf(p.accountIdx).identity || p.facebook || null;
|
|
164
|
+
function renderIdentity(p) {
|
|
165
|
+
const row = rows.get(p.accountIdx); if (!row) return;
|
|
166
|
+
const el = row.querySelector('.tg');
|
|
167
|
+
const it = identityOf(p);
|
|
168
|
+
if (it && (it.username || it.displayName || it.phone)) {
|
|
169
|
+
el.classList.remove('none');
|
|
170
|
+
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)}` : '');
|
|
171
|
+
el.title = [it.username && `@${it.username}`, it.displayName, it.phone && `+${it.phone}`].filter(Boolean).join(' ');
|
|
172
|
+
} else {
|
|
173
|
+
el.classList.add('none');
|
|
174
|
+
el.textContent = openedProfiles.has(p.accountIdx) && stateOf(p.accountIdx).url ? '未登录' : '—';
|
|
175
|
+
el.title = '';
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function flash(text, ms = 2500) {
|
|
180
|
+
statusEl.textContent = text; statusEl.dataset.sticky = '1';
|
|
181
|
+
clearTimeout(flash.t);
|
|
182
|
+
flash.t = setTimeout(() => { delete statusEl.dataset.sticky; renderPreviewState(); }, ms);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function syncPreview() {
|
|
186
|
+
if (!window.panelAPI) return;
|
|
187
|
+
const rect = preview.getBoundingClientRect();
|
|
188
|
+
const cells = [...openedProfiles].map((profileId) => ({
|
|
189
|
+
id: cellId(profileId),
|
|
190
|
+
url: SITE_URL,
|
|
191
|
+
profile: profileId,
|
|
192
|
+
visible: !!selected && profileId === selected.accountIdx,
|
|
193
|
+
rect: selected && profileId === selected.accountIdx ? { x: rect.x, y: rect.y, w: rect.width, h: rect.height } : null,
|
|
194
|
+
}));
|
|
195
|
+
try { window.panelAPI.sync(cells); } catch (e) {}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function describeError(p, st) {
|
|
199
|
+
const e = st.error || {};
|
|
200
|
+
const desc = e.description ? `<code>${esc(e.description)}</code>` : '';
|
|
201
|
+
if (p.proxy) return `代理 <code>${esc(p.proxy)}</code> 不可用或无法访问 Facebook。${desc}<br>点 ⚙ 检查或更换该 Profile 的代理,可用「测 IP」验证。`;
|
|
202
|
+
return `当前为直连,网络无法访问 Facebook。${desc}<br>点 ⚙ 为该 Profile 设置代理。`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function renderPreviewState() {
|
|
206
|
+
const st = selected ? stateOf(selected.accountIdx) : {};
|
|
207
|
+
ov.empty.classList.toggle('on', !selected);
|
|
208
|
+
ov.loading.classList.toggle('on', !!selected && !!st.loading && !st.url);
|
|
209
|
+
ov.error.classList.toggle('on', !!selected && !st.loading && !!st.failed);
|
|
210
|
+
if (selected && st.failed) errDetail.innerHTML = describeError(selected, st);
|
|
211
|
+
selDot.className = `dot ${selected ? dotClass(st) : ''}`;
|
|
212
|
+
if (selected && !statusEl.dataset.sticky) statusEl.textContent = stateText(st, true);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function renderRowState(idx) {
|
|
216
|
+
const row = rows.get(idx); if (!row) return;
|
|
217
|
+
const st = stateOf(idx);
|
|
218
|
+
row.classList.toggle('failed', !!st.failed && !st.loading);
|
|
219
|
+
row.querySelector('.dot').className = `dot ${dotClass(st)}`;
|
|
220
|
+
row.querySelector('.st').textContent = stateText(st, openedProfiles.has(idx));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function buildRow(p) {
|
|
224
|
+
const row = document.createElement('div');
|
|
225
|
+
row.className = 'row'; row.dataset.id = String(p.accountIdx); row.title = `persist:sandbox-${p.accountIdx}`;
|
|
226
|
+
row.innerHTML = `
|
|
227
|
+
<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>
|
|
228
|
+
<div class="line2"><span class="tg none">—</span><span class="ip none" title="经该 Profile 的代理探测出口 IP / 地区">测 IP</span></div>
|
|
229
|
+
<div class="cfg">
|
|
230
|
+
<div class="f"><label>代理</label><input data-role="proxy" placeholder="留空 = 直连,例如 http://127.0.0.1:20001" spellcheck="false"><button class="sm save">保存</button><button class="sm ghost test">测 IP</button></div>
|
|
231
|
+
<div class="hint">保存后已打开的会话会重新加载;<b>测 IP</b> 经该代理访问 IP 服务,结果显示在上方。</div>
|
|
232
|
+
<div class="danger"><span>删除此 Profile 会一并清除它的 Facebook 登录态和缓存。</span><span class="sp"></span><button class="sm ghost rm">删除</button></div>
|
|
233
|
+
<div class="confirm"><span>确认删除 <b>${esc(p.name)} #${p.accountIdx}</b>?不可恢复。</span><span class="sp"></span><button class="sm del rm-yes">确认删除</button><button class="sm ghost rm-no">取消</button></div>
|
|
234
|
+
</div>
|
|
235
|
+
<div class="row-err"></div>`;
|
|
236
|
+
const errEl = row.querySelector('.row-err');
|
|
237
|
+
// 备注功能已按需求移除(列表只保留 名称 / @用户名 / 状态 / IP)
|
|
238
|
+
// 代理设置抽屉(⚙):不常用,默认收起,列表保持干净。
|
|
239
|
+
const proxyInput = row.querySelector('[data-role="proxy"]');
|
|
240
|
+
const saveBtn = row.querySelector('.cfg .save');
|
|
241
|
+
const toggleCfg = (e) => { e.stopPropagation(); const open = !row.classList.contains('cfg-open'); row.classList.toggle('cfg-open', open); if (open) { proxyInput.value = p.proxy || ''; proxyInput.focus(); } };
|
|
242
|
+
const saveProxy = async () => {
|
|
243
|
+
saveBtn.disabled = true; errEl.classList.remove('on');
|
|
244
|
+
try {
|
|
245
|
+
const updated = await window.panelAPI.setProfileProxy(p.accountIdx, proxyInput.value.trim());
|
|
246
|
+
p.proxy = updated.proxy; p.ipInfo = null; renderIp(p);
|
|
247
|
+
if (openedProfiles.has(p.accountIdx)) { reloadProfile(p.accountIdx); flash(`#${p.accountIdx} 代理已应用,重新加载中`); }
|
|
248
|
+
else flash(`#${p.accountIdx} 代理已保存,打开时生效`);
|
|
249
|
+
row.classList.remove('cfg-open');
|
|
250
|
+
} catch (e) { errEl.textContent = e.message || String(e); errEl.classList.add('on'); }
|
|
251
|
+
finally { saveBtn.disabled = false; }
|
|
252
|
+
};
|
|
253
|
+
row.querySelector('.gear').onclick = toggleCfg;
|
|
254
|
+
row.querySelector('.cfg').onclick = (e) => e.stopPropagation();
|
|
255
|
+
saveBtn.onclick = saveProxy;
|
|
256
|
+
proxyInput.onkeydown = (e) => { e.stopPropagation(); if (e.key === 'Enter') saveProxy(); if (e.key === 'Escape') row.classList.remove('cfg-open'); };
|
|
257
|
+
row.querySelector('.cfg .test').onclick = () => probeIp(p);
|
|
258
|
+
row.querySelector('.ip').onclick = (e) => { e.stopPropagation(); probeIp(p); };
|
|
259
|
+
const confirmEl = row.querySelector('.cfg .confirm');
|
|
260
|
+
row.querySelector('.rm').onclick = () => { confirmEl.classList.add('on'); row.querySelector('.rm-yes').focus(); };
|
|
261
|
+
row.querySelector('.rm-no').onclick = () => confirmEl.classList.remove('on');
|
|
262
|
+
row.querySelector('.rm-yes').onclick = async () => {
|
|
263
|
+
const btn = row.querySelector('.rm-yes'); btn.disabled = true;
|
|
264
|
+
try { await removeProfile(p); }
|
|
265
|
+
catch (e) { errEl.textContent = e.message || String(e); errEl.classList.add('on'); btn.disabled = false; }
|
|
266
|
+
};
|
|
267
|
+
row._openCfg = () => { if (!row.classList.contains('cfg-open')) toggleCfg(new Event('click')); row.scrollIntoView({ block: 'nearest' }); };
|
|
268
|
+
row.onclick = () => selectProfile(p.accountIdx);
|
|
269
|
+
row.querySelector('.reload').onclick = (e) => { e.stopPropagation(); reloadProfile(p.accountIdx); };
|
|
270
|
+
return row;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function reloadProfile(idx) {
|
|
274
|
+
const fresh = !openedProfiles.has(idx);
|
|
275
|
+
openedProfiles.add(idx);
|
|
276
|
+
cellState.set(idx, { loading: true }); renderRowState(idx);
|
|
277
|
+
if (selected && selected.accountIdx === idx) renderPreviewState();
|
|
278
|
+
syncPreview();
|
|
279
|
+
// 首次打开:上面的 sync() 已经会建视图并加载 URL,再 reload 只是白加载一次。
|
|
280
|
+
if (!fresh) { try { window.panelAPI.reload(cellId(idx)); } catch (e) {} }
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function render() {
|
|
284
|
+
empty.classList.toggle('on', profiles.length === 0);
|
|
285
|
+
countEl.textContent = profiles.length ? `${profiles.length} 个会话` : '';
|
|
286
|
+
for (const p of profiles) {
|
|
287
|
+
if (!rows.has(p.accountIdx)) { const row = buildRow(p); rows.set(p.accountIdx, row); rowsEl.appendChild(row); }
|
|
288
|
+
rows.get(p.accountIdx).classList.toggle('active', !!selected && selected.accountIdx === p.accountIdx);
|
|
289
|
+
renderRowState(p.accountIdx);
|
|
290
|
+
renderIdentity(p);
|
|
291
|
+
renderIp(p);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function selectProfile(accountIdx) {
|
|
296
|
+
selected = profiles.find((p) => p.accountIdx === Number(accountIdx)) || null;
|
|
297
|
+
if (selected) { openedProfiles.add(selected.accountIdx); localStorage.setItem('facebook-matrix-profile', String(selected.accountIdx)); }
|
|
298
|
+
selectedName.textContent = selected ? `${selected.name} · #${selected.accountIdx}` : '未选择 Profile';
|
|
299
|
+
selectedName.title = selected ? `persist:sandbox-${selected.accountIdx}` : '';
|
|
300
|
+
delete statusEl.dataset.sticky;
|
|
301
|
+
render();
|
|
302
|
+
renderPreviewState();
|
|
303
|
+
syncPreview();
|
|
304
|
+
if (selected && !cellState.has(selected.accountIdx)) setTimeout(pullStates, 300);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function applyCellState(state) {
|
|
308
|
+
const m = /^facebook-preview-(\d+)$/.exec(state.id || '');
|
|
309
|
+
if (!m) return;
|
|
310
|
+
const idx = Number(m[1]);
|
|
311
|
+
const prev = stateOf(idx);
|
|
312
|
+
const failed = !!state.failed || /^chrome-error:/.test(state.url || '');
|
|
313
|
+
const next = { ...prev, loading: !!state.loading };
|
|
314
|
+
if (state.url !== undefined) { next.url = failed ? '' : state.url; next.title = state.title || ''; }
|
|
315
|
+
if (!state.loading) { next.failed = failed; next.error = state.error || (failed ? prev.error : null); }
|
|
316
|
+
if (state.identity) next.identity = state.identity;
|
|
317
|
+
cellState.set(idx, next);
|
|
318
|
+
renderRowState(idx);
|
|
319
|
+
{ const p = profiles.find((x) => x.accountIdx === idx); if (p) renderIdentity(p); }
|
|
320
|
+
if (selected && selected.accountIdx === idx) renderPreviewState();
|
|
321
|
+
}
|
|
322
|
+
async function pullStates({ adopt = false } = {}) {
|
|
323
|
+
let list = [];
|
|
324
|
+
try { list = await window.panelAPI.states(); } catch (e) { return; }
|
|
325
|
+
for (const st of list || []) {
|
|
326
|
+
// 面板页刷新后 openedProfiles 是空的,而主进程里那些会话视图还活着。
|
|
327
|
+
// 不先认领回来,第一次 syncPreview() 就会把它们当多余格子销毁,后台
|
|
328
|
+
// 登录着的会话全没(实测:刷新后 #1 的 webContents 被销毁)。
|
|
329
|
+
if (adopt) {
|
|
330
|
+
const m = /^facebook-preview-(\d+)$/.exec(st.id || '');
|
|
331
|
+
if (m) openedProfiles.add(Number(m[1]));
|
|
332
|
+
}
|
|
333
|
+
applyCellState(st);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function loadProfiles(preferredId) {
|
|
338
|
+
profiles = await window.panelAPI.profiles();
|
|
339
|
+
profiles.sort((a, b) => a.accountIdx - b.accountIdx);
|
|
340
|
+
const remembered = Number(localStorage.getItem('facebook-matrix-profile'));
|
|
341
|
+
const target = preferredId || (profiles.some((p) => p.accountIdx === remembered) ? remembered : 0) || (profiles[0] && profiles[0].accountIdx);
|
|
342
|
+
selectProfile(target);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function removeProfile(p) {
|
|
346
|
+
const idx = p.accountIdx;
|
|
347
|
+
await window.panelAPI.removeProfile(idx);
|
|
348
|
+
openedProfiles.delete(idx); cellState.delete(idx);
|
|
349
|
+
const row = rows.get(idx); if (row) row.remove(); rows.delete(idx);
|
|
350
|
+
profiles = profiles.filter((x) => x.accountIdx !== idx);
|
|
351
|
+
if (selected && selected.accountIdx === idx) {
|
|
352
|
+
const next = profiles[0] ? profiles[0].accountIdx : 0;
|
|
353
|
+
if (next) selectProfile(next); else { selected = null; localStorage.removeItem('facebook-matrix-profile'); selectedName.textContent = '未选择 Profile'; render(); renderPreviewState(); syncPreview(); }
|
|
354
|
+
} else render();
|
|
355
|
+
flash(`Profile #${idx} 已删除`);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
addButton.onclick = async () => {
|
|
359
|
+
addButton.disabled = true; flash('正在创建…', 60000);
|
|
360
|
+
try {
|
|
361
|
+
const profile = await window.panelAPI.addProfile();
|
|
362
|
+
await loadProfiles(profile.accountIdx);
|
|
363
|
+
flash(`Profile #${profile.accountIdx} 已创建`);
|
|
364
|
+
const row = rows.get(profile.accountIdx); if (row) row.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
365
|
+
} catch (e) { flash(e.message || String(e), 6000); }
|
|
366
|
+
finally { addButton.disabled = false; }
|
|
367
|
+
};
|
|
368
|
+
document.getElementById('reload').onclick = () => { if (selected) reloadProfile(selected.accountIdx); };
|
|
369
|
+
document.getElementById('err-retry').onclick = () => { if (selected) reloadProfile(selected.accountIdx); };
|
|
370
|
+
document.getElementById('err-cfg').onclick = () => { const row = selected && rows.get(selected.accountIdx); if (row) row._openCfg(); };
|
|
371
|
+
|
|
372
|
+
new ResizeObserver(() => requestAnimationFrame(syncPreview)).observe(preview);
|
|
373
|
+
window.addEventListener('resize', syncPreview);
|
|
374
|
+
try { window.panelAPI.onCellState(applyCellState); } catch (e) {}
|
|
375
|
+
pullStates({ adopt: true })
|
|
376
|
+
.catch(() => {})
|
|
377
|
+
.then(() => loadProfiles())
|
|
378
|
+
.catch((e) => { flash(e.message || String(e), 8000); render(); });
|
|
379
|
+
</script>
|
|
380
|
+
</body>
|
|
381
|
+
</html>
|