cicy-desktop 2.1.199 → 2.1.201
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 +6 -6
- package/src/app-updater.js +164 -94
- package/src/backends/homepage-preload.js +1 -0
- package/src/backends/homepage-react/assets/index-BShnCn32.css +1 -0
- package/src/backends/homepage-react/assets/index-DTbVwDSc.js +377 -0
- package/src/backends/homepage-react/index.html +2 -2
- package/src/backends/ipc.js +1 -0
- package/src/i18n/locales/en.json +11 -1
- package/src/i18n/locales/fr.json +11 -1
- package/src/i18n/locales/ja.json +11 -1
- package/src/i18n/locales/zh-CN.json +11 -1
- package/src/main.js +2 -2
- package/src/sidecar/mirrors.js +10 -5
- package/src/tabbrowser/tab-shell.html +5 -2
- package/src/utils/context-menu-options.js +8 -0
- package/src/utils/window-utils.js +8 -0
- package/workers/render/src/App.css +74 -0
- package/workers/render/src/App.jsx +86 -7
- package/src/backends/homepage-react/assets/index-Dnp76yv5.js +0 -377
- package/src/backends/homepage-react/assets/index-jCKMeNVk.css +0 -1
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
<link rel="icon" type="image/svg+xml" href="./favicon.svg" />
|
|
7
7
|
<link rel="icon" type="image/png" sizes="256x256" href="./favicon-256.png" />
|
|
8
8
|
<title>CiCy Desktop</title>
|
|
9
|
-
<script type="module" crossorigin src="./assets/index-
|
|
10
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
9
|
+
<script type="module" crossorigin src="./assets/index-DTbVwDSc.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="./assets/index-BShnCn32.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
<div id="root"></div>
|
package/src/backends/ipc.js
CHANGED
|
@@ -172,6 +172,7 @@ function register(opts = {}) {
|
|
|
172
172
|
const appUpdater = require("../app-updater");
|
|
173
173
|
ipcMain.handle("app:update-state", () => appUpdater.getState());
|
|
174
174
|
ipcMain.handle("app:check-update", async () => { await appUpdater.check(); return appUpdater.getState(); });
|
|
175
|
+
ipcMain.handle("app:download-update", async () => { await appUpdater.downloadUpdate(); return appUpdater.getState(); }); // 用户点「下载」→ 按平台/网络下安装包(带进度,可重试)
|
|
175
176
|
ipcMain.handle("app:install-update", () => { appUpdater.installNow(); return true; });
|
|
176
177
|
|
|
177
178
|
// Static version info: cicy-desktop's own version + the cicy-code version we
|
package/src/i18n/locales/en.json
CHANGED
|
@@ -444,7 +444,9 @@
|
|
|
444
444
|
"saveVideoAs": "Save Video As…",
|
|
445
445
|
"copyLink": "Copy Link",
|
|
446
446
|
"saveLinkAs": "Save Link As…",
|
|
447
|
-
"copied": "Skill command copied"
|
|
447
|
+
"copied": "Skill command copied — paste & send it to your Agent",
|
|
448
|
+
"goBack": "Back",
|
|
449
|
+
"goForward": "Forward"
|
|
448
450
|
},
|
|
449
451
|
"dockerOpen": {
|
|
450
452
|
"tokVolHit": "✓ Got token from volume (attempt {{n}})",
|
|
@@ -482,5 +484,13 @@
|
|
|
482
484
|
"noContainerConfig": "Could not read container mihomo config",
|
|
483
485
|
"binMissing": "mihomo binary missing (run ensureBinary first)",
|
|
484
486
|
"configMissing": "host config missing (run writeConfig first)"
|
|
487
|
+
},
|
|
488
|
+
"updateBanner": {
|
|
489
|
+
"available": "New version v{{v}} available",
|
|
490
|
+
"downloading": "Downloading update",
|
|
491
|
+
"ready": "Update downloaded — restart to install",
|
|
492
|
+
"error": "Update failed",
|
|
493
|
+
"downloadBtn": "Download",
|
|
494
|
+
"installBtn": "Install now"
|
|
485
495
|
}
|
|
486
496
|
}
|
package/src/i18n/locales/fr.json
CHANGED
|
@@ -444,7 +444,9 @@
|
|
|
444
444
|
"saveVideoAs": "Enregistrer la vidéo sous…",
|
|
445
445
|
"copyLink": "Copier le lien",
|
|
446
446
|
"saveLinkAs": "Enregistrer le lien sous…",
|
|
447
|
-
"copied": "Commande Skill copiée"
|
|
447
|
+
"copied": "Commande Skill copiée — collez-la et envoyez-la à votre Agent",
|
|
448
|
+
"goBack": "Précédent",
|
|
449
|
+
"goForward": "Suivant"
|
|
448
450
|
},
|
|
449
451
|
"dockerOpen": {
|
|
450
452
|
"tokVolHit": "✓ Token obtenu depuis le volume (tentative {{n}})",
|
|
@@ -482,5 +484,13 @@
|
|
|
482
484
|
"noContainerConfig": "Impossible de lire la config mihomo du conteneur",
|
|
483
485
|
"binMissing": "Binaire mihomo manquant (exécutez dabord ensureBinary)",
|
|
484
486
|
"configMissing": "Config host manquante (exécutez dabord writeConfig)"
|
|
487
|
+
},
|
|
488
|
+
"updateBanner": {
|
|
489
|
+
"available": "Nouvelle version v{{v}} disponible",
|
|
490
|
+
"downloading": "Téléchargement de la mise à jour",
|
|
491
|
+
"ready": "Téléchargé — redémarrez pour installer",
|
|
492
|
+
"error": "Échec de la mise à jour",
|
|
493
|
+
"downloadBtn": "Télécharger",
|
|
494
|
+
"installBtn": "Installer"
|
|
485
495
|
}
|
|
486
496
|
}
|
package/src/i18n/locales/ja.json
CHANGED
|
@@ -444,7 +444,9 @@
|
|
|
444
444
|
"saveVideoAs": "動画を別名で保存…",
|
|
445
445
|
"copyLink": "リンクをコピー",
|
|
446
446
|
"saveLinkAs": "リンクを別名で保存…",
|
|
447
|
-
"copied": "Skill コマンドをコピーしました"
|
|
447
|
+
"copied": "Skill コマンドをコピーしました — Agent に貼り付けて送ってください",
|
|
448
|
+
"goBack": "戻る",
|
|
449
|
+
"goForward": "進む"
|
|
448
450
|
},
|
|
449
451
|
"dockerOpen": {
|
|
450
452
|
"tokVolHit": "✓ volume から token 取得(試行 {{n}})",
|
|
@@ -482,5 +484,13 @@
|
|
|
482
484
|
"noContainerConfig": "コンテナの mihomo 設定を取得できませんでした",
|
|
483
485
|
"binMissing": "mihomo バイナリがありません(先に ensureBinary)",
|
|
484
486
|
"configMissing": "host 設定がありません(先に writeConfig)"
|
|
487
|
+
},
|
|
488
|
+
"updateBanner": {
|
|
489
|
+
"available": "新しいバージョン v{{v}} が利用可能",
|
|
490
|
+
"downloading": "アップデートをダウンロード中",
|
|
491
|
+
"ready": "ダウンロード完了 — 再起動でインストール",
|
|
492
|
+
"error": "アップデート失敗",
|
|
493
|
+
"downloadBtn": "ダウンロード",
|
|
494
|
+
"installBtn": "今すぐインストール"
|
|
485
495
|
}
|
|
486
496
|
}
|
|
@@ -444,7 +444,9 @@
|
|
|
444
444
|
"saveVideoAs": "视频另存为…",
|
|
445
445
|
"copyLink": "复制链接",
|
|
446
446
|
"saveLinkAs": "链接另存为…",
|
|
447
|
-
"copied": "已复制 Skill
|
|
447
|
+
"copied": "已复制 Skill 指令,请粘贴发给 Agent",
|
|
448
|
+
"goBack": "后退",
|
|
449
|
+
"goForward": "前进"
|
|
448
450
|
},
|
|
449
451
|
"dockerOpen": {
|
|
450
452
|
"tokVolHit": "✓ 从 volume 读到 token(第 {{n}} 次)",
|
|
@@ -482,5 +484,13 @@
|
|
|
482
484
|
"noContainerConfig": "没有拿到容器 mihomo 配置",
|
|
483
485
|
"binMissing": "mihomo 二进制不存在(先 ensureBinary)",
|
|
484
486
|
"configMissing": "host 配置不存在(先 writeConfig)"
|
|
487
|
+
},
|
|
488
|
+
"updateBanner": {
|
|
489
|
+
"available": "发现新版本 v{{v}}",
|
|
490
|
+
"downloading": "正在下载更新",
|
|
491
|
+
"ready": "新版本已下载,重启即可安装",
|
|
492
|
+
"error": "更新失败",
|
|
493
|
+
"downloadBtn": "下载",
|
|
494
|
+
"installBtn": "立即安装"
|
|
485
495
|
}
|
|
486
496
|
}
|
package/src/main.js
CHANGED
|
@@ -1205,10 +1205,10 @@ electronApp.whenReady().then(async () => {
|
|
|
1205
1205
|
// already run via the existing event handlers in app-updater.init() — we
|
|
1206
1206
|
// just surface the verdict so the user knows their click had effect.
|
|
1207
1207
|
async function onCheckForUpdatesClicked() {
|
|
1208
|
-
const result = await appUpdater.
|
|
1208
|
+
const result = await appUpdater.check();
|
|
1209
1209
|
const currentVersion = electronApp.getVersion();
|
|
1210
1210
|
if (result.status === "available") {
|
|
1211
|
-
const v =
|
|
1211
|
+
const v = result.version || "?";
|
|
1212
1212
|
dialog.showMessageBox({
|
|
1213
1213
|
type: "info",
|
|
1214
1214
|
message: i18n.t("menu.updateAvailable", { version: v }),
|
package/src/sidecar/mirrors.js
CHANGED
|
@@ -8,12 +8,15 @@
|
|
|
8
8
|
//
|
|
9
9
|
// All current mirrors are prepend-style. Add domain-style entries as needed.
|
|
10
10
|
|
|
11
|
+
// 自更新 CN feed 首选 OSS(自建,稳)。gh-proxy.com 反代已删(不稳/有问题);
|
|
12
|
+
// ghproxy.net 留作 OSS 不可达时的兜底镜像。非 CN 走 GitHub 直连(见 app-updater)。
|
|
13
|
+
const OSS_RELEASES_BASE = process.env.CICY_OSS_RELEASES_BASE
|
|
14
|
+
|| "https://cicy-1372193042-cn.oss-cn-shanghai.aliyuncs.com/releases";
|
|
15
|
+
|
|
11
16
|
const MIRRORS = [
|
|
12
|
-
// gh.llkk.cc
|
|
13
|
-
//
|
|
14
|
-
// are healthy. Re-add only after re-validating reachability.
|
|
17
|
+
// gh.llkk.cc / gh-proxy.com removed — unreachable / unstable from APAC.
|
|
18
|
+
// Re-add only after re-validating reachability.
|
|
15
19
|
{ url: "https://ghproxy.net/", type: "prepend" },
|
|
16
|
-
{ url: "https://gh-proxy.com/", type: "prepend" },
|
|
17
20
|
// { url: "hub.gitmirror.com", type: "domain" }, // example domain-replace
|
|
18
21
|
];
|
|
19
22
|
|
|
@@ -57,6 +60,8 @@ async function resolveFeedUrl(feedBase) {
|
|
|
57
60
|
req.end();
|
|
58
61
|
});
|
|
59
62
|
|
|
63
|
+
// CN 首选 OSS:OSS 上有 electron-updater feed(latest*.yml)才用它。
|
|
64
|
+
if (await probe(OSS_RELEASES_BASE + "/latest.yml")) return OSS_RELEASES_BASE;
|
|
60
65
|
for (const m of MIRRORS) {
|
|
61
66
|
const base = mirrorUrl(feedBase, m);
|
|
62
67
|
if (await probe(base + "/latest.yml")) return base;
|
|
@@ -64,4 +69,4 @@ async function resolveFeedUrl(feedBase) {
|
|
|
64
69
|
return feedBase; // fallback: direct
|
|
65
70
|
}
|
|
66
71
|
|
|
67
|
-
module.exports = { MIRRORS, mirrorUrl, buildUrlList, resolveFeedUrl };
|
|
72
|
+
module.exports = { MIRRORS, OSS_RELEASES_BASE, mirrorUrl, buildUrlList, resolveFeedUrl };
|
|
@@ -123,8 +123,11 @@
|
|
|
123
123
|
<div id="prog"></div>
|
|
124
124
|
</div>
|
|
125
125
|
<script>
|
|
126
|
-
|
|
127
|
-
|
|
126
|
+
// 禁止 tab-shell 的右键菜单(标签栏/地址栏 chrome 不弹系统 contextmenu)。
|
|
127
|
+
window.addEventListener('contextmenu', (e) => e.preventDefault());
|
|
128
|
+
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>';
|
|
129
|
+
// 「我的团队」标签的品牌标识 = 与首页 brand-mark 同款 BrandGlyph(白色六芒星)在蓝→紫渐变方块上。
|
|
130
|
+
const CICY_LOGO = '<span class="cicy-logo"><svg width="12" height="12" viewBox="0 0 96 96" fill="none"><path d="M48 11L39.5 33.3L16 29.5L31 48L16 66.5L39.5 62.7L48 85L56.5 62.7L80 66.5L65 48L80 29.5L56.5 33.3Z" fill="white" stroke="white" stroke-width="8" stroke-linejoin="round" stroke-linecap="round"/></svg></span>';
|
|
128
131
|
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>';
|
|
129
132
|
|
|
130
133
|
const tabsEl = document.getElementById('tabs');
|
|
@@ -63,6 +63,12 @@ const OPTIONS = {
|
|
|
63
63
|
const profile = part.startsWith("persist:sandbox-") ? (parseInt(part.replace("persist:sandbox-", ""), 10) || 0) : 0;
|
|
64
64
|
// 给 agent 用的一句话指令:带 webContents id + profile id + url + title,让它用 agent-electron 操作。
|
|
65
65
|
const skillPrompt = t("ctxMenu.skillPrompt", { id: wcId, profile, url, title });
|
|
66
|
+
// 导航:优先用 Electron 新 navigationHistory API,旧版兜底 wc.canGoBack/goBack。
|
|
67
|
+
const nav = (wc && wc.navigationHistory) || null;
|
|
68
|
+
const canBack = (() => { try { return nav ? nav.canGoBack() : (wc && wc.canGoBack && wc.canGoBack()); } catch (e) { return false; } })();
|
|
69
|
+
const canFwd = (() => { try { return nav ? nav.canGoForward() : (wc && wc.canGoForward && wc.canGoForward()); } catch (e) { return false; } })();
|
|
70
|
+
const goBack = () => { try { nav ? nav.goBack() : (wc && wc.goBack && wc.goBack()); } catch (e) {} };
|
|
71
|
+
const goForward = () => { try { nav ? nav.goForward() : (wc && wc.goForward && wc.goForward()); } catch (e) {} };
|
|
66
72
|
return [
|
|
67
73
|
{ label: t("ctxMenu.webviewId", { id: wcId }), enabled: false },
|
|
68
74
|
{ label: t("ctxMenu.copySkillCmd"), click: () => {
|
|
@@ -70,6 +76,8 @@ const OPTIONS = {
|
|
|
70
76
|
injectToast(wc, t("ctxMenu.copied"));
|
|
71
77
|
} },
|
|
72
78
|
{ type: "separator" },
|
|
79
|
+
{ label: t("ctxMenu.goBack"), enabled: !!canBack, click: goBack },
|
|
80
|
+
{ label: t("ctxMenu.goForward"), enabled: !!canFwd, click: goForward },
|
|
73
81
|
{ label: t("ctxMenu.reload"), click: () => { try { if (wc) wc.reload(); } catch (e) {} } },
|
|
74
82
|
{ type: "separator" },
|
|
75
83
|
];
|
|
@@ -83,6 +83,14 @@ function setupWindowHandlers(win) {
|
|
|
83
83
|
if (!ses._autoDownloadEnabled) {
|
|
84
84
|
ses._autoDownloadEnabled = true;
|
|
85
85
|
ses.on("will-download", (event, item, webContents) => {
|
|
86
|
+
// File-explorer downloads (/api/fs/download) should let the user choose
|
|
87
|
+
// where to save: leave savePath unset so Electron shows the native Save As
|
|
88
|
+
// dialog. Everything else keeps the silent auto-save to ~/Downloads/electron.
|
|
89
|
+
const dlUrl = (() => { try { return item.getURL() || ""; } catch { return ""; } })();
|
|
90
|
+
if (dlUrl.includes("/api/fs/download")) {
|
|
91
|
+
log.info(`[Download] save-as dialog for ${item.getFilename()}`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
86
94
|
// 如果没有设置 savePath,自动保存
|
|
87
95
|
setTimeout(() => {
|
|
88
96
|
if (!item.getSavePath()) {
|
|
@@ -191,6 +191,9 @@ body {
|
|
|
191
191
|
Removed in fullscreen where the buttons hide. */
|
|
192
192
|
[data-platform="darwin"][data-fullscreen="0"] .topbar { padding-top: 34px; }
|
|
193
193
|
.brand-mini { display: inline-flex; align-items: center; gap: 10px; }
|
|
194
|
+
/* brand-mini 已移除(logo 在 tab-shell 的「我的团队」标签):topbar 只剩 user-chip,
|
|
195
|
+
推到右侧(space-between 单子元素会贴左)。 */
|
|
196
|
+
.topbar .user-chip { margin-left: auto; }
|
|
194
197
|
.brand-mini .brand-name { font-size: 14px; }
|
|
195
198
|
|
|
196
199
|
.user-chip {
|
|
@@ -324,6 +327,77 @@ body {
|
|
|
324
327
|
}
|
|
325
328
|
.main::-webkit-scrollbar-thumb:hover { background-color: rgba(125,135,150,.55); }
|
|
326
329
|
|
|
330
|
+
/* ── 全局自定义滚动条 ──────────────────────────────────────────────────────────
|
|
331
|
+
Windows 默认滚动条又宽又丑(灰色方块),homepage 里所有滚动区(菜单/弹窗/抽屉/
|
|
332
|
+
审计日志/列表…)统一成细圆角半透明滚动条。Electron=Chromium,::-webkit-scrollbar
|
|
333
|
+
生效;scrollbar-* 是兜底。 */
|
|
334
|
+
*::-webkit-scrollbar { width: 8px; height: 8px; }
|
|
335
|
+
*::-webkit-scrollbar-track { background: transparent; }
|
|
336
|
+
*::-webkit-scrollbar-thumb {
|
|
337
|
+
background: rgba(125,135,150,.30);
|
|
338
|
+
border-radius: 999px;
|
|
339
|
+
border: 2px solid transparent;
|
|
340
|
+
background-clip: padding-box;
|
|
341
|
+
}
|
|
342
|
+
*::-webkit-scrollbar-thumb:hover { background: rgba(125,135,150,.55); background-clip: padding-box; }
|
|
343
|
+
*::-webkit-scrollbar-corner { background: transparent; }
|
|
344
|
+
* { scrollbar-width: thin; scrollbar-color: rgba(125,135,150,.35) transparent; }
|
|
345
|
+
|
|
346
|
+
/* ── Skeleton(首次加载占位)──────────────────────────────────────────────────── */
|
|
347
|
+
@keyframes cicy-shimmer {
|
|
348
|
+
0% { background-position: -240px 0; }
|
|
349
|
+
100% { background-position: calc(240px + 100%) 0; }
|
|
350
|
+
}
|
|
351
|
+
.skel {
|
|
352
|
+
background-color: rgba(255,255,255,.05);
|
|
353
|
+
background-image: linear-gradient(90deg, rgba(255,255,255,0) 0, rgba(255,255,255,.08) 50%, rgba(255,255,255,0) 100%);
|
|
354
|
+
background-size: 240px 100%;
|
|
355
|
+
background-repeat: no-repeat;
|
|
356
|
+
animation: cicy-shimmer 1.25s ease-in-out infinite;
|
|
357
|
+
border-radius: 8px;
|
|
358
|
+
}
|
|
359
|
+
.skel--avatar { width: 30px; height: 30px; border-radius: 50%; flex: 0 0 auto; }
|
|
360
|
+
.skel--name { width: 72px; height: 12px; border-radius: 6px; }
|
|
361
|
+
.skel--line { height: 12px; border-radius: 6px; }
|
|
362
|
+
.skel--cta { height: 34px; border-radius: 9px; margin-top: 16px; }
|
|
363
|
+
.user-chip__trigger--skel { display: inline-flex; align-items: center; gap: 10px; cursor: default; }
|
|
364
|
+
.bcard--skeleton { pointer-events: none; }
|
|
365
|
+
.bcard--skeleton__head { display: flex; align-items: center; gap: 12px; }
|
|
366
|
+
.bcard--skeleton__lines { flex: 1; min-width: 0; }
|
|
367
|
+
|
|
368
|
+
/* ── App 自更新 banner(顶部细条)──────────────────────────────────────────────── */
|
|
369
|
+
.update-banner {
|
|
370
|
+
position: relative; flex: 0 0 auto;
|
|
371
|
+
padding: 9px 14px 9px 16px; font-size: 13px;
|
|
372
|
+
border-bottom: 1px solid rgba(255,255,255,.06);
|
|
373
|
+
background: rgba(59,130,246,.10); /* available: accent tint */
|
|
374
|
+
}
|
|
375
|
+
.update-banner--ready { background: rgba(34,197,94,.12); }
|
|
376
|
+
.update-banner--error { background: rgba(239,68,68,.12); }
|
|
377
|
+
.update-banner--downloading { background: rgba(255,255,255,.05); }
|
|
378
|
+
.update-banner__row { display: flex; align-items: center; gap: 10px; }
|
|
379
|
+
.update-banner__icon {
|
|
380
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
381
|
+
width: 18px; height: 18px; border-radius: 50%; flex: 0 0 auto;
|
|
382
|
+
font-size: 12px; font-weight: 700; color: #fff;
|
|
383
|
+
background: linear-gradient(135deg,#3b82f6,#8b5cf6);
|
|
384
|
+
}
|
|
385
|
+
.update-banner--ready .update-banner__icon { background: #22c55e; }
|
|
386
|
+
.update-banner--error .update-banner__icon { background: #ef4444; }
|
|
387
|
+
.update-banner--downloading .update-banner__icon { background: transparent; color: #7aa2f7; }
|
|
388
|
+
.update-banner__text { flex: 1; min-width: 0; color: #e6edf3; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
389
|
+
.update-banner__actions { display: flex; align-items: center; gap: 6px; flex: 0 0 auto; }
|
|
390
|
+
.update-banner__btn {
|
|
391
|
+
border: none; border-radius: 7px; padding: 5px 14px; font-size: 12px; font-weight: 600;
|
|
392
|
+
cursor: pointer; color: #fff; background: rgba(255,255,255,.12);
|
|
393
|
+
}
|
|
394
|
+
.update-banner__btn.is-accent { background: linear-gradient(135deg,#3b82f6,#8b5cf6); }
|
|
395
|
+
.update-banner__btn:hover { filter: brightness(1.12); }
|
|
396
|
+
.update-banner__x { border: none; background: transparent; color: #9aa0a6; font-size: 18px; line-height: 1; cursor: pointer; padding: 0 4px; border-radius: 6px; }
|
|
397
|
+
.update-banner__x:hover { color: #e6edf3; background: rgba(255,255,255,.08); }
|
|
398
|
+
.update-banner__bar { position: absolute; left: 0; right: 0; bottom: 0; height: 3px; background: rgba(255,255,255,.08); overflow: hidden; }
|
|
399
|
+
.update-banner__bar > span { display: block; height: 100%; background: linear-gradient(90deg,#3b82f6,#8b5cf6); transition: width .2s ease; }
|
|
400
|
+
|
|
327
401
|
/* ── Tabs row ── */
|
|
328
402
|
.app__tabs {
|
|
329
403
|
display: inline-flex;
|
|
@@ -891,6 +891,8 @@ export default function App() {
|
|
|
891
891
|
const showLocal = tab === "all" || tab === "local";
|
|
892
892
|
const showCustom = tab === "all" || tab === "custom";
|
|
893
893
|
const showCloud = tab === "all" || tab === "cloud";
|
|
894
|
+
// 首次打开:本地团队与云端团队都还没拉到(都为 null)→ grid 显示 skeleton 占位卡。
|
|
895
|
+
const firstLoading = localTeams === null && teams === null;
|
|
894
896
|
|
|
895
897
|
return (
|
|
896
898
|
<div className="shell shell--app">
|
|
@@ -898,6 +900,7 @@ export default function App() {
|
|
|
898
900
|
<div className="shell__left">
|
|
899
901
|
<Header me={me} welcome={welcome} onLogout={handleLogout}
|
|
900
902
|
mitmTeam={localList.length > 0 ? localList[0] : null} />
|
|
903
|
+
<UpdateBanner />
|
|
901
904
|
<main className="main">
|
|
902
905
|
{/* 整行:左边 tab 药丸,右边「新加团队」顶到行尾 */}
|
|
903
906
|
<div className="app__tabsrow">
|
|
@@ -992,12 +995,13 @@ export default function App() {
|
|
|
992
995
|
)}
|
|
993
996
|
|
|
994
997
|
<div className="app__grid">
|
|
995
|
-
{
|
|
998
|
+
{firstLoading && [0, 1, 2].map((i) => <SkeletonCard key={"skc" + i} />)}
|
|
999
|
+
{!firstLoading && showLocal && localList.map((t) => (
|
|
996
1000
|
<LocalTeamCard key={"local:" + t.id} team={t} cloudCode={cloudCodeFor(t.cloud_team_id)} onOpen={() => openLocalTeam(t.id)} onRename={renameLocalTeam} onRefresh={fetchLocalTeams} />
|
|
997
1001
|
))}
|
|
998
1002
|
{/* native :8008 退役 —— 不再有"本地团队 正在启动"占位卡(native 已删,
|
|
999
1003
|
:8008 永远不会起来,占位会一直转)。cicy-code 用下面的 Docker 卡(:8008)。 */}
|
|
1000
|
-
{showLocal && (
|
|
1004
|
+
{!firstLoading && showLocal && (
|
|
1001
1005
|
<DockerCard
|
|
1002
1006
|
dockerTeam={dockerTeam}
|
|
1003
1007
|
onOpen={async () => {
|
|
@@ -1333,12 +1337,16 @@ function Header({ me, welcome, onLogout, mitmTeam }) {
|
|
|
1333
1337
|
return (
|
|
1334
1338
|
<>
|
|
1335
1339
|
<header className="topbar">
|
|
1336
|
-
|
|
1337
|
-
<div className="brand-mark sm"><BrandGlyph /></div>
|
|
1338
|
-
<span className="brand-name">CiCy Desktop</span>
|
|
1339
|
-
</div>
|
|
1340
|
+
{/* logo 移到 tab-shell 的「我的团队」标签(CICY_LOGO),topbar 不再重复显示品牌 */}
|
|
1340
1341
|
<div className="user-chip" data-id="UserChip" ref={wrap}>
|
|
1341
1342
|
{welcome && <span className="welcome">{welcome}</span>}
|
|
1343
|
+
{!me ? (
|
|
1344
|
+
// 首次打开:profile 还没拉到 → avatar/名字用 skeleton 占位
|
|
1345
|
+
<div className="user-chip__trigger user-chip__trigger--skel" data-id="UserChip-skeleton" aria-hidden>
|
|
1346
|
+
<div className="skel skel--avatar" />
|
|
1347
|
+
<div className="skel skel--name" />
|
|
1348
|
+
</div>
|
|
1349
|
+
) : (
|
|
1342
1350
|
<button
|
|
1343
1351
|
type="button"
|
|
1344
1352
|
data-id="UserChip-trigger"
|
|
@@ -1349,6 +1357,7 @@ function Header({ me, welcome, onLogout, mitmTeam }) {
|
|
|
1349
1357
|
<span className="user-name">{name}</span>
|
|
1350
1358
|
<span className="user-chip__caret" aria-hidden>▾</span>
|
|
1351
1359
|
</button>
|
|
1360
|
+
)}
|
|
1352
1361
|
{open && (
|
|
1353
1362
|
<div className="user-chip__menu" data-id="UserChip-menu" role="menu">
|
|
1354
1363
|
<button type="button" data-id="UserChip-wallet" className="user-chip__menu-item" onClick={() => goDash("?view=wallet")}>
|
|
@@ -2045,7 +2054,14 @@ function DockerCard({ dockerTeam, cloudTitle, cloudCode, onOpen, onRename, onRef
|
|
|
2045
2054
|
try {
|
|
2046
2055
|
const r = await window.cicy?.docker?.appUpdate?.();
|
|
2047
2056
|
dockerDrawer.finish({ ok: !!r?.ok, message: r?.ok ? tr("docker.updated", "cicy-code 已更新到最新") : (r?.error || tr("docker.updateFailed", "更新失败")) });
|
|
2048
|
-
if (r?.ok)
|
|
2057
|
+
if (r?.ok) {
|
|
2058
|
+
onRefresh?.();
|
|
2059
|
+
// update() 返回时 :8008 已健康(waitUntil probeHealth)。立刻:① reload 已打开的
|
|
2060
|
+
// docker tab(reloadIgnoringCache,拿新版 SPA)② 强制重探,卡片版本马上更新(不等
|
|
2061
|
+
// 60s reconcile / 缓存)。
|
|
2062
|
+
try { window.cicy?.tabs?.reloadIfOpen?.("http://127.0.0.1:8008", tr("docker.teamTab", "Docker 团队")); } catch {}
|
|
2063
|
+
try { const s = await window.cicy?.docker?.appRedetect?.(); if (s) setStatus(s); } catch {}
|
|
2064
|
+
}
|
|
2049
2065
|
} catch (e) {
|
|
2050
2066
|
dockerDrawer.finish({ ok: false, message: e.message });
|
|
2051
2067
|
} finally {
|
|
@@ -3113,6 +3129,69 @@ function Brand() {
|
|
|
3113
3129
|
);
|
|
3114
3130
|
}
|
|
3115
3131
|
|
|
3132
|
+
// app 自更新 banner(产品级):顶部细条,跟随 app:update-state。
|
|
3133
|
+
// available → 「发现新版本 vX [下载]」(可关闭)
|
|
3134
|
+
// downloading→ 进度条 + 「45% · 32.1/72.4 MB」(不可关,可后台)
|
|
3135
|
+
// ready → 「已下载,重启安装 [立即安装] [稍后]」
|
|
3136
|
+
// error → 「更新失败:… [重试]」(可关闭)
|
|
3137
|
+
function UpdateBanner() {
|
|
3138
|
+
const [st, setSt] = useState(null);
|
|
3139
|
+
const [dismissed, setDismissed] = useState(false);
|
|
3140
|
+
useEffect(() => {
|
|
3141
|
+
let alive = true;
|
|
3142
|
+
window.cicy?.app?.updateState?.().then((s) => { if (alive) setSt(s); }).catch(() => {});
|
|
3143
|
+
const unsub = window.cicy?.app?.onUpdateState?.((s) => setSt(s));
|
|
3144
|
+
return () => { alive = false; try { unsub && unsub(); } catch {} };
|
|
3145
|
+
}, []);
|
|
3146
|
+
const status = st?.status;
|
|
3147
|
+
// 每次状态变化都重置「已关闭」——新状态(如下载完成)要重新出现。
|
|
3148
|
+
useEffect(() => { setDismissed(false); }, [status]);
|
|
3149
|
+
const IMPORTANT = ["available", "downloading", "ready", "error"];
|
|
3150
|
+
if (!IMPORTANT.includes(status) || dismissed) return null;
|
|
3151
|
+
const p = st?.progress || {};
|
|
3152
|
+
const mb = (b) => (b ? (b / 1048576).toFixed(1) : "0");
|
|
3153
|
+
const download = () => { window.cicy?.app?.downloadUpdate?.(); };
|
|
3154
|
+
const install = () => { window.cicy?.app?.installUpdate?.(); };
|
|
3155
|
+
return (
|
|
3156
|
+
<div className={`update-banner update-banner--${status}`} data-id="UpdateBanner" data-status={status}>
|
|
3157
|
+
<div className="update-banner__row">
|
|
3158
|
+
<span className="update-banner__icon" aria-hidden>
|
|
3159
|
+
{status === "downloading" ? <Spinner /> : status === "ready" ? "✓" : status === "error" ? "!" : "↑"}
|
|
3160
|
+
</span>
|
|
3161
|
+
<span className="update-banner__text" data-id="UpdateBanner-text">
|
|
3162
|
+
{status === "available" && tr("updateBanner.available", "发现新版本 v{{v}}", { v: st.version })}
|
|
3163
|
+
{status === "downloading" && `${tr("updateBanner.downloading", "正在下载更新")} ${p.percent || 0}%${p.total ? ` · ${mb(p.transferred)}/${mb(p.total)} MB` : ""}`}
|
|
3164
|
+
{status === "ready" && tr("updateBanner.ready", "新版本已下载,重启即可安装")}
|
|
3165
|
+
{status === "error" && `${tr("updateBanner.error", "更新失败")}${st.error ? `:${st.error}` : ""}`}
|
|
3166
|
+
</span>
|
|
3167
|
+
<div className="update-banner__actions">
|
|
3168
|
+
{status === "available" && <button type="button" className="update-banner__btn is-accent" data-id="UpdateBanner-download" onClick={download}>{tr("updateBanner.downloadBtn", "下载")}</button>}
|
|
3169
|
+
{status === "ready" && <button type="button" className="update-banner__btn is-accent" data-id="UpdateBanner-install" onClick={install}>{tr("updateBanner.installBtn", "立即安装")}</button>}
|
|
3170
|
+
{status === "error" && <button type="button" className="update-banner__btn is-accent" data-id="UpdateBanner-retry" onClick={download}>{tr("common.retry", "重试")}</button>}
|
|
3171
|
+
{status !== "downloading" && <button type="button" className="update-banner__x" data-id="UpdateBanner-dismiss" onClick={() => setDismissed(true)} aria-label="dismiss">×</button>}
|
|
3172
|
+
</div>
|
|
3173
|
+
</div>
|
|
3174
|
+
{status === "downloading" && (
|
|
3175
|
+
<div className="update-banner__bar" data-id="UpdateBanner-bar"><span style={{ width: `${p.percent || 0}%` }} /></div>
|
|
3176
|
+
)}
|
|
3177
|
+
</div>
|
|
3178
|
+
);
|
|
3179
|
+
}
|
|
3180
|
+
// 首次加载的团队卡占位骨架(与 .bcard 大致同形:头像圈 + 两行标题 + CTA 条)。
|
|
3181
|
+
function SkeletonCard() {
|
|
3182
|
+
return (
|
|
3183
|
+
<div className="bcard bcard--skeleton" data-id="SkeletonCard" aria-hidden>
|
|
3184
|
+
<div className="bcard--skeleton__head">
|
|
3185
|
+
<div className="skel skel--avatar" />
|
|
3186
|
+
<div className="bcard--skeleton__lines">
|
|
3187
|
+
<div className="skel skel--line" style={{ width: "58%" }} />
|
|
3188
|
+
<div className="skel skel--line" style={{ width: "34%", height: 10, marginTop: 8 }} />
|
|
3189
|
+
</div>
|
|
3190
|
+
</div>
|
|
3191
|
+
<div className="skel skel--cta" />
|
|
3192
|
+
</div>
|
|
3193
|
+
);
|
|
3194
|
+
}
|
|
3116
3195
|
function BrandGlyph() {
|
|
3117
3196
|
// New CiCy mark (六芒星). Rendered white here because it sits on the brand
|
|
3118
3197
|
// chip's blue→violet gradient square; the full-color gradient version is the
|