cicy-desktop 2.1.78 → 2.1.79
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/bin/cicy-desktop +7 -7
- package/package.json +6 -6
- package/src/backends/homepage-preload.js +22 -0
- package/src/backends/homepage-react/assets/index-CKpaMBKz.css +1 -0
- package/src/backends/homepage-react/assets/index-CSsNZgC5.js +365 -0
- package/src/backends/homepage-react/index.html +2 -2
- package/src/backends/homepage-window.js +52 -7
- package/src/backends/ipc.js +57 -0
- package/src/backends/local-teams.js +73 -26
- package/src/backends/sidecar-ipc.js +11 -0
- package/src/backends/webview-preload.js +5 -3
- package/src/backends/window-manager.js +13 -3
- package/src/chrome/chrome-launcher.js +5 -4
- package/src/chrome/debugger-port-resolver.js +1 -1
- package/src/cloud/cloud-client.js +237 -41
- package/src/cluster/types.js +0 -5
- package/src/extension/inject.js +1 -1
- package/src/main.js +282 -88
- package/src/master/chrome-config.js +2 -2
- package/src/preload-rpc.js +1 -1
- package/src/profiles/profile-store.js +321 -0
- package/src/profiles/trusted-origins-store.js +95 -0
- package/src/server/worker-observability-routes.js +0 -2
- package/src/sidecar/cicy-code.js +84 -23
- package/src/sidecar/localbin.js +20 -3
- package/src/sidecar/native.js +3 -3
- package/src/sidecar/version.js +45 -0
- package/src/tabbrowser/newtab-protocol.js +54 -0
- package/src/tabbrowser/tab-browser.html +151 -0
- package/src/tabbrowser/tab-shell-preload.js +28 -0
- package/src/tabbrowser/tab-shell.html +227 -0
- package/src/tools/account-tools.js +191 -25
- package/src/tools/chrome-tools.js +173 -37
- package/src/tools/device-tools.js +25 -0
- package/src/tools/index.js +2 -0
- package/src/tools/tab-browser-tools.js +453 -0
- package/src/tools/window-tools.js +64 -7
- package/src/utils/brand-host-electron.js +25 -0
- package/src/utils/context-menu-options.js +80 -0
- package/src/utils/cookie-logins.js +58 -0
- package/src/utils/ip-probe.js +50 -0
- package/src/utils/rpc-audit.js +53 -0
- package/src/utils/rpc-guard.js +189 -0
- package/src/utils/window-monitor.js +5 -15
- package/src/utils/window-registry.js +210 -0
- package/src/utils/window-thumbnails.js +126 -0
- package/src/utils/window-utils.js +146 -109
- package/workers/render/package-lock.json +6 -6
- package/workers/render/src/App.css +36 -2
- package/workers/render/src/App.jsx +587 -103
- package/src/backends/artifact-ipc.js +0 -142
- package/src/backends/homepage-react/assets/index-DE9m6JTn.css +0 -1
- package/src/backends/homepage-react/assets/index-DLYMzgf5.js +0 -365
- package/src/cluster/artifact-registry.js +0 -61
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// cicy-code 版本的唯一读法。三个概念一个模块,别处不准再自己解析 /api/health、
|
|
2
|
+
// 也不准再直连 npm 拿版本(主人令:"拿版本就一个方法")。
|
|
3
|
+
// running(port) → 活着的 daemon 在 /api/health 报的版本(“正在跑什么”的唯一真相)
|
|
4
|
+
// latest() → npm 上最新版(和 update() 升级到的同一个号)
|
|
5
|
+
// installed() → 磁盘 binary 版本(localbin manifest,诊断用)
|
|
6
|
+
const http = require("http");
|
|
7
|
+
|
|
8
|
+
const DEFAULT_PORT = 8008;
|
|
9
|
+
|
|
10
|
+
// The ONE running-version reader. GET /api/health → version. Returns the version
|
|
11
|
+
// string, or null on any failure / missing field. Used by the update flow's
|
|
12
|
+
// post-restart verification AND surfaced to the UI via the sidecar:versions IPC.
|
|
13
|
+
//
|
|
14
|
+
// timeout=4000 (not 1500): on Windows Node's http.get to 127.0.0.1:8008 routinely
|
|
15
|
+
// takes 1.5–5s (localhost goes through 360/Defender socket inspection) even though
|
|
16
|
+
// curl is instant — a 1500ms cap returned null while the daemon was fine. Connection:
|
|
17
|
+
// close makes the Go server end the response promptly instead of holding keep-alive.
|
|
18
|
+
function running(port = DEFAULT_PORT, timeoutMs = 4000) {
|
|
19
|
+
return new Promise((resolve) => {
|
|
20
|
+
const req = http.get({ host: "127.0.0.1", port, path: "/api/health", timeout: timeoutMs, headers: { Connection: "close" } }, (res) => {
|
|
21
|
+
let body = "";
|
|
22
|
+
res.on("data", (d) => { body += d; if (body.length > 8192) body = body.slice(0, 8192); });
|
|
23
|
+
res.on("end", () => { resolve(parseHealthVersion(body)); });
|
|
24
|
+
});
|
|
25
|
+
req.on("error", () => resolve(null));
|
|
26
|
+
req.on("timeout", () => { req.destroy(); resolve(null); });
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// The ONE way to pull the version out of a /api/health body — shared so every
|
|
31
|
+
// caller (this module + local-teams' liveness probe) parses identically.
|
|
32
|
+
function parseHealthVersion(body) {
|
|
33
|
+
try { const j = JSON.parse(body); return String(j?.version || j?.data?.version || "").trim() || null; }
|
|
34
|
+
catch { return null; }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function latest() {
|
|
38
|
+
try { return await require("./localbin").latestVersion(); } catch { return null; }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function installed() {
|
|
42
|
+
try { return require("./localbin").currentVersion(); } catch { return null; }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = { running, latest, installed, parseHealthVersion, DEFAULT_PORT };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// cicy://newtab — the tab browser's start page, served via a custom scheme so a
|
|
2
|
+
// new tab's URL is a clean "cicy://newtab" instead of a giant inline data: URL
|
|
3
|
+
// (主人令: url 不要那串 data: 天书). Chrome's chrome://newtab analog.
|
|
4
|
+
const { protocol, session } = require("electron");
|
|
5
|
+
const _handled = new WeakSet(); // sessions that already have the cicyui handler
|
|
6
|
+
|
|
7
|
+
// NOTE: scheme is "cicyui", NOT "cicy" — "cicy" is already an OS deep-link
|
|
8
|
+
// protocol client (setAsDefaultProtocolClient), so navigating a webContents to
|
|
9
|
+
// cicy://… gets dispatched externally (open-url) and the page never renders.
|
|
10
|
+
// A dedicated scheme avoids that collision.
|
|
11
|
+
const NEWTAB_URL = "cicyui://newtab";
|
|
12
|
+
|
|
13
|
+
function startPageHtml() {
|
|
14
|
+
// <title> → document.title = 起始页 (so the tab isn't titled by its URL).
|
|
15
|
+
return `<!doctype html><meta charset=utf-8><title>起始页</title><style>html,body{height:100%;margin:0;display:flex;align-items:center;justify-content:center;background:#202124;color:#e8eaed;font-family:-apple-system,sans-serif}.w{text-align:center}.l{width:54px;height:54px;border-radius:16px;margin:0 auto 16px;background:linear-gradient(135deg,#3b82f6,#8b5cf6);display:flex;align-items:center;justify-content:center;color:#fff;font-size:26px}h1{font-size:16px;margin-bottom:6px}p{color:#9aa0a6;font-size:13px}</style><div class=w><div class=l>✦</div><h1>CiCy Browser</h1><p>新标签页</p></div>`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// MUST be called BEFORE app 'ready' (registerSchemesAsPrivileged requirement).
|
|
19
|
+
function registerScheme() {
|
|
20
|
+
try {
|
|
21
|
+
protocol.registerSchemesAsPrivileged([
|
|
22
|
+
{ scheme: "cicyui", privileges: { standard: true, secure: true, supportFetchAPI: true } },
|
|
23
|
+
]);
|
|
24
|
+
} catch (e) {}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Register the cicyui handler on a given session. protocol.handle on the global
|
|
28
|
+
// (default) session does NOT cover persist:sandbox-N partition sessions — the
|
|
29
|
+
// tab BrowserViews run in those — so each partition session needs its own.
|
|
30
|
+
function handlerFor(ses) {
|
|
31
|
+
if (!ses || _handled.has(ses)) return;
|
|
32
|
+
try {
|
|
33
|
+
ses.protocol.handle("cicyui", (request) => {
|
|
34
|
+
let host = "";
|
|
35
|
+
try { host = new URL(request.url).hostname; } catch (e) {}
|
|
36
|
+
if (host === "newtab") {
|
|
37
|
+
return new Response(startPageHtml(), { headers: { "content-type": "text/html; charset=utf-8" } });
|
|
38
|
+
}
|
|
39
|
+
return new Response("not found", { status: 404 });
|
|
40
|
+
});
|
|
41
|
+
_handled.add(ses);
|
|
42
|
+
} catch (e) {}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// MUST be called AFTER app 'ready' — default session (homepage etc.).
|
|
46
|
+
function installHandler() { handlerFor(session.defaultSession); }
|
|
47
|
+
|
|
48
|
+
// Per-partition session for the tab-browser sandboxes — call before a tab in
|
|
49
|
+
// that partition loads cicyui://newtab.
|
|
50
|
+
function ensureForPartition(partition) {
|
|
51
|
+
try { handlerFor(session.fromPartition(partition)); } catch (e) {}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = { NEWTAB_URL, registerScheme, installHandler, ensureForPartition, startPageHtml };
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>CiCy Browser</title>
|
|
6
|
+
<style>
|
|
7
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
8
|
+
html,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif}
|
|
9
|
+
body{display:flex;flex-direction:column;background:#202124;color:#e8eaed}
|
|
10
|
+
#tabs{display:flex;align-items:center;gap:2px;padding:6px 6px 0;overflow-x:auto;flex-wrap:nowrap}
|
|
11
|
+
#tabs::-webkit-scrollbar{height:0}
|
|
12
|
+
.tab{display:flex;align-items:center;gap:6px;max-width:220px;min-width:110px;padding:8px 10px;background:#35363a;border-radius:9px 9px 0 0;font-size:12px;white-space:nowrap;cursor:default}
|
|
13
|
+
.tab.active{background:#5f6368}
|
|
14
|
+
.tab .t{flex:1;overflow:hidden;text-overflow:ellipsis}
|
|
15
|
+
.tab .x{opacity:.55;cursor:pointer;padding:0 3px;border-radius:3px}
|
|
16
|
+
.tab .x:hover{background:#0004;opacity:1}
|
|
17
|
+
#newtab{padding:4px 11px;cursor:pointer;font-size:18px;color:#9aa0a6}
|
|
18
|
+
#newtab:hover{color:#fff}
|
|
19
|
+
#bar{display:flex;align-items:center;gap:8px;padding:8px 10px;background:#35363a}
|
|
20
|
+
#bar button{background:none;border:none;color:#9aa0a6;font-size:17px;cursor:pointer;padding:4px 9px;border-radius:6px}
|
|
21
|
+
#bar button:hover{background:#ffffff18;color:#fff}
|
|
22
|
+
#url{flex:1;background:#202124;border:1px solid #5f6368;border-radius:18px;padding:8px 14px;color:#e8eaed;font-size:13px;outline:none}
|
|
23
|
+
#url:focus{border-color:#8ab4f8}
|
|
24
|
+
#views{flex:1;position:relative;background:#fff}
|
|
25
|
+
webview{position:absolute;inset:0;width:100%;height:100%}
|
|
26
|
+
webview.hidden{display:none}
|
|
27
|
+
</style>
|
|
28
|
+
</head>
|
|
29
|
+
<body>
|
|
30
|
+
<div id="tabs"></div>
|
|
31
|
+
<div id="bar">
|
|
32
|
+
<button id="back" title="后退">‹</button>
|
|
33
|
+
<button id="fwd" title="前进">›</button>
|
|
34
|
+
<button id="reload" title="刷新">↻</button>
|
|
35
|
+
<input id="url" placeholder="输入网址或搜索">
|
|
36
|
+
</div>
|
|
37
|
+
<div id="views"></div>
|
|
38
|
+
<script>
|
|
39
|
+
// partition (profile) comes from the query: ?p=<accountIdx>
|
|
40
|
+
const Q = new URLSearchParams(location.search);
|
|
41
|
+
const ACCT = Q.get('p') || '0';
|
|
42
|
+
const PARTITION = 'persist:sandbox-' + ACCT;
|
|
43
|
+
// cicy's webview preload → trusted tabs (cicy-code team app) get
|
|
44
|
+
// window.electronRPC + cicy bridges so they keep working as a tab.
|
|
45
|
+
const PRELOAD = new URL('../backends/webview-preload.js', location.href).href;
|
|
46
|
+
// homepage uses its OWN preload (window.cicy.localTeams/auth/...). When ?home=
|
|
47
|
+
// is set, the first tab loads the homepage with that preload.
|
|
48
|
+
const HOMEPAGE_PRELOAD = new URL('../backends/homepage-preload.js', location.href).href;
|
|
49
|
+
const HOME = Q.get('home') || '';
|
|
50
|
+
// Built-in start page (a real page, not about:blank) for new tabs.
|
|
51
|
+
const START = 'data:text/html;charset=utf-8,' + encodeURIComponent(
|
|
52
|
+
`<!doctype html><meta charset=utf-8><style>html,body{height:100%;margin:0;display:flex;align-items:center;justify-content:center;background:#202124;color:#e8eaed;font-family:-apple-system,sans-serif}.w{text-align:center}.l{width:54px;height:54px;border-radius:16px;margin:0 auto 16px;background:linear-gradient(135deg,#3b82f6,#8b5cf6);display:flex;align-items:center;justify-content:center;color:#fff;font-size:26px}h1{font-size:16px;margin-bottom:6px}p{color:#9aa0a6;font-size:13px}</style><div class=w><div class=l>✦</div><h1>CiCy Browser</h1><p>profile sandbox-${ACCT}</p></div>`);
|
|
53
|
+
|
|
54
|
+
const tabsEl = document.getElementById('tabs');
|
|
55
|
+
const viewsEl = document.getElementById('views');
|
|
56
|
+
const urlEl = document.getElementById('url');
|
|
57
|
+
let tabs = []; // { wv, title, url }
|
|
58
|
+
let active = -1;
|
|
59
|
+
|
|
60
|
+
function render() {
|
|
61
|
+
tabsEl.innerHTML = '';
|
|
62
|
+
tabs.forEach((t, i) => {
|
|
63
|
+
const d = document.createElement('div');
|
|
64
|
+
d.className = 'tab' + (i === active ? ' active' : '');
|
|
65
|
+
d.innerHTML = '<span class="t"></span><span class="x">✕</span>';
|
|
66
|
+
d.querySelector('.t').textContent = t.title || '新标签页';
|
|
67
|
+
d.title = t.url || '';
|
|
68
|
+
d.onclick = () => select(i);
|
|
69
|
+
d.querySelector('.x').onclick = (e) => { e.stopPropagation(); closeAt(i); };
|
|
70
|
+
tabsEl.appendChild(d);
|
|
71
|
+
});
|
|
72
|
+
const add = document.createElement('div');
|
|
73
|
+
add.id = 'newtab'; add.textContent = '+';
|
|
74
|
+
add.onclick = () => newTab();
|
|
75
|
+
tabsEl.appendChild(add);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function select(i) {
|
|
79
|
+
active = i;
|
|
80
|
+
tabs.forEach((t, j) => t.wv.classList.toggle('hidden', j !== i));
|
|
81
|
+
const t = tabs[i];
|
|
82
|
+
if (t) { try { urlEl.value = t.wv.getURL() || t.url || ''; } catch (e) { urlEl.value = t.url || ''; } }
|
|
83
|
+
render();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function closeAt(i) {
|
|
87
|
+
const t = tabs[i]; if (!t) return;
|
|
88
|
+
t.wv.remove(); tabs.splice(i, 1);
|
|
89
|
+
if (tabs.length === 0) { newTab(); return; }
|
|
90
|
+
if (active >= tabs.length) active = tabs.length - 1;
|
|
91
|
+
select(active);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Public: open a new tab. opts.trusted=true → inject the electronRPC bridge
|
|
95
|
+
// (ONLY for trusted pages like the cicy-code team app — never random sites).
|
|
96
|
+
window.newTab = function (u, opts) {
|
|
97
|
+
u = (u && String(u).trim()) || START;
|
|
98
|
+
const wv = document.createElement('webview');
|
|
99
|
+
wv.setAttribute('partition', PARTITION);
|
|
100
|
+
wv.setAttribute('allowpopups', '');
|
|
101
|
+
if (opts && opts.trusted) { try { wv.setAttribute('preload', PRELOAD); } catch (e) {} }
|
|
102
|
+
wv.setAttribute('src', u);
|
|
103
|
+
viewsEl.appendChild(wv);
|
|
104
|
+
const t = { wv, title: '', url: u };
|
|
105
|
+
tabs.push(t);
|
|
106
|
+
wv.addEventListener('page-title-updated', (e) => { t.title = e.title; render(); });
|
|
107
|
+
wv.addEventListener('did-navigate', (e) => { t.url = e.url; if (tabs[active] === t) urlEl.value = e.url; });
|
|
108
|
+
wv.addEventListener('did-navigate-in-page', (e) => { if (tabs[active] === t) urlEl.value = e.url; });
|
|
109
|
+
// popups / window.open → open as a new tab instead of a new window
|
|
110
|
+
wv.addEventListener('new-window', (e) => { try { newTab(e.url); } catch (x) {} });
|
|
111
|
+
select(tabs.length - 1);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// Public: close the tab whose webview has this webContentsId (used by main).
|
|
115
|
+
window.closeTabByWcId = function (id) {
|
|
116
|
+
const i = tabs.findIndex((t) => { try { return t.wv.getWebContentsId() === id; } catch (e) { return false; } });
|
|
117
|
+
if (i >= 0) closeAt(i);
|
|
118
|
+
return i >= 0;
|
|
119
|
+
};
|
|
120
|
+
// Public: activate (foreground) a tab by webContentsId.
|
|
121
|
+
window.activateTabByWcId = function (id) {
|
|
122
|
+
const i = tabs.findIndex((t) => { try { return t.wv.getWebContentsId() === id; } catch (e) { return false; } });
|
|
123
|
+
if (i >= 0) select(i);
|
|
124
|
+
return i >= 0;
|
|
125
|
+
};
|
|
126
|
+
// origin+pathname only (token/hash vary per session) — for tab reuse.
|
|
127
|
+
function stripVol(u) { try { const x = new URL(u); return x.origin + x.pathname; } catch (e) { return u || ''; } }
|
|
128
|
+
// Public: focus an existing tab whose url matches (origin+pathname); else false.
|
|
129
|
+
window.activateTabByUrl = function (key) {
|
|
130
|
+
const i = tabs.findIndex((t) => { try { return stripVol(t.wv.getURL() || t.url) === key; } catch (e) { return false; } });
|
|
131
|
+
if (i >= 0) { select(i); return true; }
|
|
132
|
+
return false;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
function go() {
|
|
136
|
+
let v = urlEl.value.trim(); if (!v) return;
|
|
137
|
+
if (!/^[a-z]+:\/\//i.test(v)) {
|
|
138
|
+
v = (/\.\w/.test(v) && !/\s/.test(v)) ? 'https://' + v : 'https://www.google.com/search?q=' + encodeURIComponent(v);
|
|
139
|
+
}
|
|
140
|
+
if (tabs[active]) tabs[active].wv.loadURL(v);
|
|
141
|
+
}
|
|
142
|
+
urlEl.addEventListener('keydown', (e) => { if (e.key === 'Enter') go(); });
|
|
143
|
+
document.getElementById('back').onclick = () => { const w = tabs[active] && tabs[active].wv; if (w && w.canGoBack()) w.goBack(); };
|
|
144
|
+
document.getElementById('fwd').onclick = () => { const w = tabs[active] && tabs[active].wv; if (w && w.canGoForward()) w.goForward(); };
|
|
145
|
+
document.getElementById('reload').onclick = () => { tabs[active] && tabs[active].wv.reload(); };
|
|
146
|
+
|
|
147
|
+
// first tab = start page
|
|
148
|
+
newTab();
|
|
149
|
+
</script>
|
|
150
|
+
</body>
|
|
151
|
+
</html>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Preload for the BrowserView tab-browser shell (tab strip + toolbar).
|
|
2
|
+
// Exposes window.tabAPI so the chrome UI drives the main-process TabManager,
|
|
3
|
+
// and receives tab state pushes. Tabs themselves are BrowserViews (managed in
|
|
4
|
+
// main) — this preload is ONLY for the thin chrome UI, never the tab content.
|
|
5
|
+
const { contextBridge, ipcRenderer } = require("electron");
|
|
6
|
+
|
|
7
|
+
contextBridge.exposeInMainWorld("tabAPI", {
|
|
8
|
+
newTab: (url) => ipcRenderer.send("tabwin:new", { url: url || "" }),
|
|
9
|
+
activate: (id) => ipcRenderer.send("tabwin:activate", { id }),
|
|
10
|
+
close: (id) => ipcRenderer.send("tabwin:close", { id }),
|
|
11
|
+
navigate: (url) => ipcRenderer.send("tabwin:navigate", { url }),
|
|
12
|
+
back: () => ipcRenderer.send("tabwin:back"),
|
|
13
|
+
fwd: () => ipcRenderer.send("tabwin:fwd"),
|
|
14
|
+
reload: () => ipcRenderer.send("tabwin:reload"),
|
|
15
|
+
ready: () => ipcRenderer.send("tabwin:ready"),
|
|
16
|
+
onState: (cb) => {
|
|
17
|
+
const h = (_e, s) => { try { cb(s); } catch (e) {} };
|
|
18
|
+
ipcRenderer.on("tabwin:state", h);
|
|
19
|
+
return () => ipcRenderer.removeListener("tabwin:state", h);
|
|
20
|
+
},
|
|
21
|
+
// mac native fullscreen toggles the traffic lights → the strip reclaims the
|
|
22
|
+
// reserved left gutter. cb(isFullScreen:boolean).
|
|
23
|
+
onFullscreen: (cb) => {
|
|
24
|
+
const h = (_e, fs) => { try { cb(!!fs); } catch (e) {} };
|
|
25
|
+
ipcRenderer.on("window:fullscreen", h);
|
|
26
|
+
return () => ipcRenderer.removeListener("window:fullscreen", h);
|
|
27
|
+
},
|
|
28
|
+
});
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>CiCy Browser</title>
|
|
6
|
+
<style>
|
|
7
|
+
/* Chrome MUST total CHROME_H (80) in tab-browser-tools.js: strip 40 + bar 40. */
|
|
8
|
+
:root{
|
|
9
|
+
/* match cicy-code's #0A0A0A main bg so the tab strip blends into the
|
|
10
|
+
content below — no color seam (断层). */
|
|
11
|
+
/* The top tab-strip is a clearly different (lighter) bar; the ACTIVE tab is
|
|
12
|
+
the same color as the toolbar/content (#0A0A0A) so it merges into the
|
|
13
|
+
page, carved out of the lighter strip with outward bottom curves.
|
|
14
|
+
(content is near-black, so a darker strip would be invisible → go lighter.) */
|
|
15
|
+
--bg:#0A0A0A; --strip:#1c1c20; --toolbar:#0A0A0A;
|
|
16
|
+
--fg:#e6e6e9; --muted:#8b8b92; --line:rgba(255,255,255,.06);
|
|
17
|
+
--accent:#3b82f6; --omni:#1a1a1c;
|
|
18
|
+
}
|
|
19
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
20
|
+
html,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:var(--bg);color:var(--fg);overflow:hidden;-webkit-user-select:none;user-select:none}
|
|
21
|
+
svg{display:block}
|
|
22
|
+
|
|
23
|
+
/* ── tab strip ── */
|
|
24
|
+
/* Chrome Refresh tabs: floating rounded pills, gaps between; active = a filled
|
|
25
|
+
lighter surface (NO bottom border — Chrome doesn't use one). */
|
|
26
|
+
/* gap >= the active tab's ear width so the ear sits in the inter-tab gap
|
|
27
|
+
(over the strip) instead of spilling onto / occluding the neighbor tab. */
|
|
28
|
+
#tabs{height:40px;display:flex;align-items:flex-end;gap:6px;padding:8px 8px 0;background:var(--strip);overflow-x:auto;white-space:nowrap;-webkit-app-region:drag}
|
|
29
|
+
/* titleBarStyle:hidden — the strip IS the top of the window: make empty areas
|
|
30
|
+
draggable, and reserve room for the native window controls (mac traffic
|
|
31
|
+
lights on the left, win min/max/close overlay on the right). Tabs/buttons
|
|
32
|
+
stay clickable. */
|
|
33
|
+
body.is-mac #tabs{padding-left:78px}
|
|
34
|
+
body.is-win #tabs{padding-right:140px}
|
|
35
|
+
/* fullscreen → OS window controls hidden → reclaim the reserved gutter
|
|
36
|
+
(mac: traffic lights on the left, win: min/max/close overlay on the right) */
|
|
37
|
+
body.is-mac.is-fullscreen #tabs{padding-left:8px}
|
|
38
|
+
body.is-win.is-fullscreen #tabs{padding-right:8px}
|
|
39
|
+
.tab,#newtab{-webkit-app-region:no-drag}
|
|
40
|
+
#tabs::-webkit-scrollbar{height:0}
|
|
41
|
+
.tab{position:relative;display:flex;align-items:center;gap:9px;height:32px;min-width:54px;max-width:240px;padding:0 12px;border-radius:10px 10px 0 0;font-size:12.5px;color:var(--muted);background:transparent;cursor:default;flex:1 1 0;transition:background .12s,color .12s}
|
|
42
|
+
.tab:hover{background:rgba(255,255,255,.06);color:var(--fg)}
|
|
43
|
+
/* home = the resident homepage tab: pinned first, fixed width, user icon, no
|
|
44
|
+
title text, no close — like Chrome's profile/avatar button. */
|
|
45
|
+
/* start-page tab: pinned, sizes to its "我的团队" label (not a flex-grow tab) */
|
|
46
|
+
.tab.home{flex:0 0 auto;min-width:0;max-width:none;padding:0 12px 0 10px}
|
|
47
|
+
.tab.home .ttl{flex:0 0 auto}
|
|
48
|
+
.tab.home .fav{overflow:visible}
|
|
49
|
+
/* CiCy brand mark — gradient rounded square + ✦, matches the start page logo */
|
|
50
|
+
.cicy-logo{width:17px;height:17px;border-radius:5px;background:linear-gradient(135deg,#3b82f6,#8b5cf6);display:flex;align-items:center;justify-content:center;color:#fff;font-size:11px;line-height:1;box-shadow:0 1px 2px rgba(0,0,0,.3)}
|
|
51
|
+
/* the homepage tab has no toolbar beneath it (its content covers y=40 down),
|
|
52
|
+
so drop the outward ears — they'd otherwise meet the homepage top with a seam. */
|
|
53
|
+
.tab.home.active::before,.tab.home.active::after{display:none}
|
|
54
|
+
/* active tab = toolbar/content color, merged into the page, with outward bottom curves */
|
|
55
|
+
.tab.active{background:var(--toolbar);color:#fff;z-index:1}
|
|
56
|
+
/* outward bottom "ears" carved from the strip into the toolbar. gradient goes
|
|
57
|
+
strip→toolbar (NOT transparent→toolbar): a transparent→opaque ramp passes
|
|
58
|
+
through semi-transparent black and leaves a dark fringe on the lighter strip;
|
|
59
|
+
using the solid strip color makes the inner quarter blend seamlessly. */
|
|
60
|
+
/* ear width (5px) < the inter-tab gap (6px) so the ear never reaches the
|
|
61
|
+
neighbor tab — no overlap/occlusion on hover. */
|
|
62
|
+
.tab.active::before,.tab.active::after{content:"";position:absolute;bottom:0;width:5px;height:5px}
|
|
63
|
+
.tab.active::before{left:-5px;background:radial-gradient(circle at top left,var(--strip) 4.5px,var(--toolbar) 5px)}
|
|
64
|
+
.tab.active::after{right:-5px;background:radial-gradient(circle at top right,var(--strip) 4.5px,var(--toolbar) 5px)}
|
|
65
|
+
.fav{width:16px;height:16px;flex:0 0 16px;border-radius:4px;display:flex;align-items:center;justify-content:center;overflow:hidden}
|
|
66
|
+
.fav img{width:16px;height:16px;object-fit:contain}
|
|
67
|
+
.spin{width:14px;height:14px;border:2px solid rgba(255,255,255,.25);border-top-color:var(--accent);border-radius:50%;animation:sp .7s linear infinite}
|
|
68
|
+
@keyframes sp{to{transform:rotate(360deg)}}
|
|
69
|
+
.ttl{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
70
|
+
.cls{flex:0 0 16px;width:16px;height:16px;border-radius:50%;display:flex;align-items:center;justify-content:center;opacity:0;color:var(--muted);transition:opacity .1s,background .1s}
|
|
71
|
+
.tab:hover .cls,.tab.active .cls{opacity:.85}
|
|
72
|
+
.cls:hover{background:rgba(255,255,255,.16);color:#fff;opacity:1}
|
|
73
|
+
#newtab{flex:0 0 30px;width:30px;height:30px;margin:2px 0 2px 2px;border-radius:50%;display:flex;align-items:center;justify-content:center;color:var(--muted);cursor:default}
|
|
74
|
+
#newtab:hover{background:rgba(255,255,255,.08);color:#fff}
|
|
75
|
+
|
|
76
|
+
/* ── toolbar ── */
|
|
77
|
+
#bar{position:relative;height:40px;display:flex;align-items:center;gap:4px;padding:0 8px;background:var(--toolbar)}
|
|
78
|
+
.nav{width:32px;height:32px;flex:0 0 32px;border-radius:50%;display:flex;align-items:center;justify-content:center;color:var(--fg);cursor:default}
|
|
79
|
+
.nav:hover{background:rgba(255,255,255,.10)}
|
|
80
|
+
.nav.off{color:var(--muted);opacity:.35;pointer-events:none}
|
|
81
|
+
#omni{flex:1;height:30px;display:flex;align-items:center;gap:8px;margin-left:6px;padding:0 12px;background:var(--omni);border:1px solid var(--line);border-radius:16px;transition:border-color .12s,background .12s}
|
|
82
|
+
#omni.focus{background:#111113;border-color:var(--accent)}
|
|
83
|
+
#lead{flex:0 0 16px;width:16px;height:16px;color:var(--muted);display:flex;align-items:center;justify-content:center}
|
|
84
|
+
#url{flex:1;height:100%;background:transparent;border:none;outline:none;color:var(--fg);font-size:13px;-webkit-user-select:text;user-select:text}
|
|
85
|
+
#url::placeholder{color:var(--muted)}
|
|
86
|
+
|
|
87
|
+
/* ── top load progress ── */
|
|
88
|
+
#prog{position:absolute;left:0;right:0;bottom:-1px;height:2px;overflow:hidden;pointer-events:none;opacity:0;transition:opacity .2s}
|
|
89
|
+
#prog.on{opacity:1}
|
|
90
|
+
#prog::before{content:"";position:absolute;left:0;top:0;height:100%;width:35%;background:linear-gradient(90deg,transparent,var(--accent),transparent);animation:load 1.05s ease-in-out infinite}
|
|
91
|
+
@keyframes load{0%{left:-35%}100%{left:100%}}
|
|
92
|
+
</style>
|
|
93
|
+
</head>
|
|
94
|
+
<body>
|
|
95
|
+
<div id="tabs"></div>
|
|
96
|
+
<div id="bar">
|
|
97
|
+
<div class="nav off" id="back" title="后退">
|
|
98
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>
|
|
99
|
+
</div>
|
|
100
|
+
<div class="nav off" id="fwd" title="前进">
|
|
101
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
|
|
102
|
+
</div>
|
|
103
|
+
<div class="nav" id="reload" title="刷新">
|
|
104
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
|
|
105
|
+
</div>
|
|
106
|
+
<div id="omni">
|
|
107
|
+
<span id="lead"></span>
|
|
108
|
+
<input id="url" placeholder="搜索或输入网址" spellcheck="false" autocomplete="off">
|
|
109
|
+
</div>
|
|
110
|
+
<div id="prog"></div>
|
|
111
|
+
</div>
|
|
112
|
+
<script>
|
|
113
|
+
const ICON_GLOBE = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#9aa0a6" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>';
|
|
114
|
+
const CICY_LOGO = '<span class="cicy-logo">✦</span>';
|
|
115
|
+
const ICON_LOCK = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>';
|
|
116
|
+
|
|
117
|
+
const tabsEl = document.getElementById('tabs');
|
|
118
|
+
const urlEl = document.getElementById('url');
|
|
119
|
+
const omniEl = document.getElementById('omni');
|
|
120
|
+
const leadEl = document.getElementById('lead');
|
|
121
|
+
const backEl = document.getElementById('back');
|
|
122
|
+
const fwdEl = document.getElementById('fwd');
|
|
123
|
+
const progEl = document.getElementById('prog');
|
|
124
|
+
const NO_NEW = new URLSearchParams(location.search).get('noNew') === '1'; // profile 0
|
|
125
|
+
const PLAT = new URLSearchParams(location.search).get('plat') || ''; // darwin|win32|linux
|
|
126
|
+
if (PLAT === 'darwin') document.body.classList.add('is-mac');
|
|
127
|
+
else if (PLAT === 'win32') document.body.classList.add('is-win');
|
|
128
|
+
try { window.tabAPI.onFullscreen((fs) => document.body.classList.toggle('is-fullscreen', !!fs)); } catch (e) {}
|
|
129
|
+
let state = { tabs: [], nav: {} };
|
|
130
|
+
let urlFocused = false;
|
|
131
|
+
|
|
132
|
+
function faviconNode(t) {
|
|
133
|
+
const w = document.createElement('span');
|
|
134
|
+
w.className = 'fav';
|
|
135
|
+
if (t.loading) { w.innerHTML = '<span class="spin"></span>'; return w; }
|
|
136
|
+
if (t.favicon) {
|
|
137
|
+
const img = document.createElement('img');
|
|
138
|
+
img.src = t.favicon;
|
|
139
|
+
img.onerror = () => { w.innerHTML = ICON_GLOBE; };
|
|
140
|
+
w.appendChild(img);
|
|
141
|
+
} else { w.innerHTML = ICON_GLOBE; }
|
|
142
|
+
return w;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function render() {
|
|
146
|
+
tabsEl.innerHTML = '';
|
|
147
|
+
const hasHome = state.tabs.some((x) => x.home);
|
|
148
|
+
const nonHome = state.tabs.filter((x) => !x.home).length;
|
|
149
|
+
state.tabs.forEach((t) => {
|
|
150
|
+
const d = document.createElement('div');
|
|
151
|
+
d.className = 'tab' + (t.home ? ' home' : '') + (t.active ? ' active' : '');
|
|
152
|
+
d.title = t.home ? '我的团队' : (t.url || '');
|
|
153
|
+
d.onclick = () => window.tabAPI.activate(t.id);
|
|
154
|
+
// resident start-page tab: pinned first, user icon only, no title, no close
|
|
155
|
+
if (t.home) {
|
|
156
|
+
const ic = document.createElement('span');
|
|
157
|
+
ic.className = 'fav'; ic.innerHTML = CICY_LOGO;
|
|
158
|
+
d.appendChild(ic);
|
|
159
|
+
const ttl = document.createElement('span');
|
|
160
|
+
ttl.className = 'ttl'; ttl.textContent = '我的团队';
|
|
161
|
+
d.appendChild(ttl);
|
|
162
|
+
tabsEl.appendChild(d);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
d.appendChild(faviconNode(t));
|
|
166
|
+
const ttl = document.createElement('span');
|
|
167
|
+
ttl.className = 'ttl'; ttl.textContent = t.title || '新标签页';
|
|
168
|
+
d.appendChild(ttl);
|
|
169
|
+
// closable unless it's the only tab and there's no resident homepage to
|
|
170
|
+
// keep the window alive (don't let the user empty a plain browser window).
|
|
171
|
+
if (hasHome || nonHome > 1) {
|
|
172
|
+
const cls = document.createElement('span');
|
|
173
|
+
cls.className = 'cls';
|
|
174
|
+
cls.innerHTML = '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"><line x1="5" y1="5" x2="19" y2="19"/><line x1="19" y1="5" x2="5" y2="19"/></svg>';
|
|
175
|
+
cls.onclick = (e) => { e.stopPropagation(); window.tabAPI.close(t.id); };
|
|
176
|
+
d.appendChild(cls);
|
|
177
|
+
}
|
|
178
|
+
tabsEl.appendChild(d);
|
|
179
|
+
});
|
|
180
|
+
if (!NO_NEW) {
|
|
181
|
+
const add = document.createElement('div');
|
|
182
|
+
add.id = 'newtab'; add.title = '新建标签';
|
|
183
|
+
add.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>';
|
|
184
|
+
add.onclick = () => window.tabAPI.newTab();
|
|
185
|
+
tabsEl.appendChild(add);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const nav = state.nav || {};
|
|
189
|
+
backEl.classList.toggle('off', !nav.canBack);
|
|
190
|
+
fwdEl.classList.toggle('off', !nav.canFwd);
|
|
191
|
+
progEl.classList.toggle('on', !!nav.loading);
|
|
192
|
+
const activeTab = state.tabs.find((x) => x.active);
|
|
193
|
+
const isHome = !!(activeTab && activeTab.home);
|
|
194
|
+
const u = nav.url || '';
|
|
195
|
+
const secure = /^https:\/\//i.test(u);
|
|
196
|
+
leadEl.style.color = secure ? '#81c995' : '#9aa0a6';
|
|
197
|
+
leadEl.innerHTML = secure ? ICON_LOCK : ICON_GLOBE;
|
|
198
|
+
// homepage tab → empty omnibox (like Chrome's new-tab page), don't expose the file:// path
|
|
199
|
+
if (!urlFocused) urlEl.value = isHome ? '' : displayUrl(u);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function displayUrl(u) {
|
|
203
|
+
// start pages (cicy://newtab) and data: URLs → empty omnibox (like Chrome NTP)
|
|
204
|
+
if (!u || u.startsWith('data:') || u.startsWith('cicyui://')) return '';
|
|
205
|
+
return u;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
window.tabAPI.onState((s) => { state = s || { tabs: [], nav: {} }; render(); });
|
|
209
|
+
|
|
210
|
+
function go() {
|
|
211
|
+
let v = urlEl.value.trim(); if (!v) return;
|
|
212
|
+
if (!/^[a-z]+:\/\//i.test(v)) {
|
|
213
|
+
v = (/\.\w/.test(v) && !/\s/.test(v)) ? 'https://' + v : 'https://www.google.com/search?q=' + encodeURIComponent(v);
|
|
214
|
+
}
|
|
215
|
+
window.tabAPI.navigate(v);
|
|
216
|
+
}
|
|
217
|
+
urlEl.addEventListener('focus', () => { urlFocused = true; omniEl.classList.add('focus'); urlEl.select(); });
|
|
218
|
+
urlEl.addEventListener('blur', () => { urlFocused = false; omniEl.classList.remove('focus'); render(); });
|
|
219
|
+
urlEl.addEventListener('keydown', (e) => { if (e.key === 'Enter') { go(); urlEl.blur(); } });
|
|
220
|
+
backEl.onclick = () => window.tabAPI.back();
|
|
221
|
+
fwdEl.onclick = () => window.tabAPI.fwd();
|
|
222
|
+
document.getElementById('reload').onclick = () => window.tabAPI.reload();
|
|
223
|
+
|
|
224
|
+
window.tabAPI.ready();
|
|
225
|
+
</script>
|
|
226
|
+
</body>
|
|
227
|
+
</html>
|