zcode-pro 1.3.1 → 1.4.1
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/README.en.md +57 -30
- package/README.md +57 -30
- package/dist/inject.js +1195 -66
- package/dist/zcode_pro-1.4.0-py3-none-any.whl +0 -0
- package/dist/zcode_pro-1.4.0.tar.gz +0 -0
- package/package.json +1 -1
- package/src/host/helper.mjs +492 -57
- package/src/host/webdav.mjs +19 -11
- package/src/inject/core.js +132 -20
- package/src/inject/features/settings-dialog.js +400 -44
- package/src/inject/features/status-bar.js +17 -4
- package/src/inject/features/theme.js +500 -0
- package/src/inject/index.js +2 -0
- package/src/inject/ui.js +33 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// “ZCode Pro 增强设置”弹窗:功能开关 +
|
|
1
|
+
// “ZCode Pro 增强设置”弹窗:功能开关 + 外观 + 运行状态。
|
|
2
2
|
// 顶部标签页切换(视觉参考侧栏“分组/项目”切换);配置写入 helper(~/.zcode/zcodepro.json)。
|
|
3
3
|
import { h, t, rpc, getConfig, clearConfigCache, errText, HELPER_URL, isZhLocale, isNewProjectSidebar, DEFAULT_SHORTCUTS, DEFAULT_STATUS_ITEMS, DEFAULT_TURN_ITEMS, STATUS_SESSION_IDS, STATUS_GLOBAL_IDS } from '../core.js';
|
|
4
4
|
import { openDialog, dialogFooter, btnPrimary, btnSecondary, btnSmall, settingRow, ensureStyle, showToast, numberField, unitField } from '../ui.js';
|
|
@@ -6,6 +6,7 @@ import { comboField, providerOptionsOf, modelOptionsOf, modelPlaceholderOf, isMa
|
|
|
6
6
|
import { refreshAliases } from './alias.js';
|
|
7
7
|
import { migrationCard } from './alias-migration.js';
|
|
8
8
|
import { applyStyles, STYLE_DEFAULTS, showStyleHighlight, hideStyleHighlight } from './styles.js';
|
|
9
|
+
import { THEME_SKINS, CUSTOM_DEFAULTS } from './theme.js';
|
|
9
10
|
import { copyToClipboard, hCopyIcon } from './copy-path.js';
|
|
10
11
|
|
|
11
12
|
// 调宿主桥在系统浏览器打开外链;宿主没暴露该能力时静默忽略
|
|
@@ -205,17 +206,21 @@ export function openSettingsDialog() {
|
|
|
205
206
|
}, '✕');
|
|
206
207
|
titleEl.replaceChildren(titleLeft, closeX);
|
|
207
208
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const agentsRes = await
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
209
|
+
// 八个只读请求相互独立:并行取(串行时 headroom 的版本探测一项就约
|
|
210
|
+
// 700ms,弹窗打开明显卡顿)
|
|
211
|
+
const [health, config, agentsRes, visionRes, titleGenRes, titleGenProvidersRes, rtkRes, headroomRes] = await Promise.all([
|
|
212
|
+
rpc('/health'),
|
|
213
|
+
getConfig(true),
|
|
214
|
+
rpc('/agents'),
|
|
215
|
+
rpc('/vision'),
|
|
216
|
+
rpc('/title-gen'),
|
|
217
|
+
rpc('/title-gen/providers'),
|
|
218
|
+
rpc('/rtk'),
|
|
219
|
+
rpc('/headroom'),
|
|
220
|
+
]);
|
|
214
221
|
// 供应商表(含各供应商可选模型):视觉代理的模型下拉也用这份
|
|
215
222
|
const titleGenProviders = titleGenProvidersRes.ok && Array.isArray(titleGenProvidersRes.providers)
|
|
216
223
|
? titleGenProvidersRes.providers : [];
|
|
217
|
-
const rtkRes = await rpc('/rtk');
|
|
218
|
-
const headroomRes = await rpc('/headroom');
|
|
219
224
|
|
|
220
225
|
const setFeature = async (key, value) => {
|
|
221
226
|
const res = await rpc('/config', { method: 'POST', body: { features: { [key]: value } } });
|
|
@@ -438,7 +443,7 @@ export function openSettingsDialog() {
|
|
|
438
443
|
if (await copyToClipboard(WECHAT_ID)) showToast(L.wechatIdCopied);
|
|
439
444
|
}));
|
|
440
445
|
|
|
441
|
-
// 标签页切换:功能(现有内容)/
|
|
446
|
+
// 标签页切换:功能(现有内容)/ 外观 / 全局提示词 / 视觉代理
|
|
442
447
|
let activeTab = 'features';
|
|
443
448
|
// 插件更新卡片:显示 duanluan-zcode-plugins 市场状态,一键更新(helper /plugins/* 端点);
|
|
444
449
|
// 状态异步加载,打开弹窗不等待
|
|
@@ -587,7 +592,7 @@ export function openSettingsDialog() {
|
|
|
587
592
|
};
|
|
588
593
|
renderTabs();
|
|
589
594
|
|
|
590
|
-
//
|
|
595
|
+
// “样式”:一行两项、相关项同行;无描述文字,悬停名称显示 tip;
|
|
591
596
|
// 数字框滚轮/手输调节,改完即存即生效
|
|
592
597
|
const savedStyles = config.styles || {};
|
|
593
598
|
let saveTimer = null;
|
|
@@ -738,9 +743,12 @@ export function openSettingsDialog() {
|
|
|
738
743
|
for (const c of fontCells) c.setOptions(list);
|
|
739
744
|
})();
|
|
740
745
|
renderToolbarSwitch();
|
|
741
|
-
//
|
|
746
|
+
// “外观”标签页的二级结构:样式(原内容)/ 状态栏(统计项顺序与显隐)/
|
|
747
|
+
// 主题(皮肤与自定义主题)/ 背景图(图库、文件夹轮换、网址)
|
|
742
748
|
const paneStylesSub1 = h('div', { class: 'mt-4' });
|
|
743
749
|
const paneStylesSub2 = h('div', { class: 'mt-4', style: 'display:none' });
|
|
750
|
+
const paneStylesSub3 = h('div', { class: 'mt-4', style: 'display:none' });
|
|
751
|
+
const paneStylesSub4 = h('div', { class: 'mt-4', style: 'display:none' });
|
|
744
752
|
const ITEM_NAMES = {
|
|
745
753
|
ctx: 'sbItemCtx', questions: 'sbItemQuestions',
|
|
746
754
|
todayTokens: 'sbItemTodayTokens', totalTokens: 'sbItemTotalTokens', tasks: 'sbItemTasks',
|
|
@@ -858,11 +866,362 @@ export function openSettingsDialog() {
|
|
|
858
866
|
sbGlobalList,
|
|
859
867
|
h('div', { class: 'mt-3 mb-1 text-ui-sm font-medium' }, L.sbGroupTurn),
|
|
860
868
|
turnList);
|
|
869
|
+
// —— “颜色”子页(paneStylesSub3):预设皮肤卡片 + 自定义主题编辑 ——
|
|
870
|
+
// —— “背景图”子页(paneStylesSub4):无背景 / 图库 / 文件夹轮换 / 网址 ——
|
|
871
|
+
let themeCfg = structuredClone(config.theme || {});
|
|
872
|
+
const numOrBg = (v, d) => (typeof v === 'number' && Number.isFinite(v) ? v : d);
|
|
873
|
+
themeCfg.skin = themeCfg.skin || 'default';
|
|
874
|
+
themeCfg.custom = {
|
|
875
|
+
light: (themeCfg.custom && themeCfg.custom.light) || {},
|
|
876
|
+
dark: (themeCfg.custom && themeCfg.custom.dark) || {},
|
|
877
|
+
};
|
|
878
|
+
themeCfg.bg = {
|
|
879
|
+
...(themeCfg.bg || {}),
|
|
880
|
+
mode: themeCfg.bg?.mode || 'none',
|
|
881
|
+
maskOpacity: numOrBg(themeCfg.bg?.maskOpacity, 0.82),
|
|
882
|
+
blurPx: numOrBg(themeCfg.bg?.blurPx, 0),
|
|
883
|
+
panelOpacity: numOrBg(themeCfg.bg?.panelOpacity, 0.3),
|
|
884
|
+
};
|
|
885
|
+
// file/folder/url/fit/folderRandom/intervalMin/regions 由 helper 清洗保证形态,原样保留
|
|
886
|
+
themeCfg.bg.regions = (themeCfg.bg.regions && typeof themeCfg.bg.regions === 'object') ? themeCfg.bg.regions : {};
|
|
887
|
+
let bgFiles = [];
|
|
888
|
+
let bgLoading = false;
|
|
889
|
+
let folderInfo = { count: 0, dir: '' };
|
|
890
|
+
const themeInputCls = 'h-8 w-full rounded-lg border border-border bg-input px-2.5 text-ui-sm text-foreground outline-none transition-shadow placeholder:text-foreground-subtle focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/40';
|
|
891
|
+
const numField = (val, min, max, commit, title) => {
|
|
892
|
+
const n = h('input', {
|
|
893
|
+
type: 'number', min: String(min), max: String(max), step: '1', title,
|
|
894
|
+
class: 'h-7 w-24 rounded-lg border border-border bg-input px-2 text-right text-ui-xs tabular-nums text-foreground outline-none focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/40',
|
|
895
|
+
});
|
|
896
|
+
n.value = String(val);
|
|
897
|
+
n.addEventListener('change', () => {
|
|
898
|
+
const v = Math.max(min, Math.min(max, Math.round(Number(n.value) || 0)));
|
|
899
|
+
n.value = String(v);
|
|
900
|
+
commit(v);
|
|
901
|
+
});
|
|
902
|
+
return n;
|
|
903
|
+
};
|
|
904
|
+
const bgUrl = (name) => `${HELPER_URL}/bg/file/${encodeURIComponent(name)}`;
|
|
905
|
+
const saveTheme = async (patch) => {
|
|
906
|
+
themeCfg = {
|
|
907
|
+
skin: patch.skin !== undefined ? patch.skin : themeCfg.skin,
|
|
908
|
+
custom: patch.custom !== undefined ? patch.custom : themeCfg.custom,
|
|
909
|
+
bg: { ...themeCfg.bg, ...(patch.bg || {}) },
|
|
910
|
+
};
|
|
911
|
+
const res = await rpc('/config', { method: 'POST', body: { theme: themeCfg } });
|
|
912
|
+
clearConfigCache();
|
|
913
|
+
window.dispatchEvent(new CustomEvent('zcodepro:config-changed'));
|
|
914
|
+
if (!res.ok) showToast(L.failed + ': ' + errText(res), 'error');
|
|
915
|
+
renderThemeColors();
|
|
916
|
+
renderThemeBg();
|
|
917
|
+
};
|
|
918
|
+
const refreshBgFiles = async () => {
|
|
919
|
+
bgLoading = true;
|
|
920
|
+
renderThemeBg();
|
|
921
|
+
const res = await rpc('/bg');
|
|
922
|
+
bgLoading = false;
|
|
923
|
+
if (res.ok && Array.isArray(res.files)) bgFiles = res.files;
|
|
924
|
+
// 图库模式下当前文件可能已被删(同步/手动删除):自动回无背景并提示一次
|
|
925
|
+
if (themeCfg.bg.mode === 'library' && themeCfg.bg.file
|
|
926
|
+
&& !bgFiles.some((f) => f.name === themeCfg.bg.file)) {
|
|
927
|
+
themeCfg = { ...themeCfg, bg: { ...themeCfg.bg, file: '' } };
|
|
928
|
+
await saveTheme({ bg: { file: '' } });
|
|
929
|
+
showToast(L.themeBgDeletedActive, 'success');
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
renderThemeBg();
|
|
933
|
+
};
|
|
934
|
+
const refreshFolderInfo = async () => {
|
|
935
|
+
if (!themeCfg.bg.folder) { folderInfo = { count: 0, dir: '' }; renderThemeBg(); return; }
|
|
936
|
+
const res = await rpc('/bg/folder/list');
|
|
937
|
+
folderInfo = res.ok && !res.missing && Array.isArray(res.files)
|
|
938
|
+
? { count: res.files.length, dir: res.dir || themeCfg.bg.folder }
|
|
939
|
+
: { count: 0, dir: themeCfg.bg.folder };
|
|
940
|
+
renderThemeBg();
|
|
941
|
+
};
|
|
942
|
+
// 上传:多选文件逐张转 base64 传 helper;第一张上传且当前无背景时自动选为当前
|
|
943
|
+
const bgInput = h('input', { type: 'file', accept: '.png,.jpg,.jpeg,.webp,.gif,.bmp,.avif', multiple: '', style: 'display:none' });
|
|
944
|
+
bgInput.addEventListener('change', () => {
|
|
945
|
+
const files = [...bgInput.files || []];
|
|
946
|
+
bgInput.value = '';
|
|
947
|
+
if (!files.length) return;
|
|
948
|
+
void (async () => {
|
|
949
|
+
let first = themeCfg.bg.file;
|
|
950
|
+
for (const f of files) {
|
|
951
|
+
const dataUrl = await new Promise((resolve) => {
|
|
952
|
+
const r = new FileReader();
|
|
953
|
+
r.onload = () => resolve(String(r.result || ''));
|
|
954
|
+
r.onerror = () => resolve('');
|
|
955
|
+
r.readAsDataURL(f);
|
|
956
|
+
});
|
|
957
|
+
if (!dataUrl) continue;
|
|
958
|
+
const res = await rpc('/bg', { method: 'POST', body: { name: f.name, data: dataUrl } });
|
|
959
|
+
if (!res.ok) { showToast(L.failed + ': ' + errText(res), 'error'); continue; }
|
|
960
|
+
if (Array.isArray(res.files)) bgFiles = res.files;
|
|
961
|
+
if (!first) first = f.name;
|
|
962
|
+
}
|
|
963
|
+
if (first && first !== themeCfg.bg.file) await saveTheme({ bg: { file: first } });
|
|
964
|
+
else renderThemeBg();
|
|
965
|
+
})();
|
|
966
|
+
});
|
|
967
|
+
// 自定义主题:改色即保存并切到自定义皮肤(实时生效);250ms 防抖收拖拽连发
|
|
968
|
+
let customSaveTimer = null;
|
|
969
|
+
const CUSTOM_FIELDS = [
|
|
970
|
+
['bg', 'themeCustomBg'], ['card', 'themeCustomCard'], ['fg', 'themeCustomFg'],
|
|
971
|
+
['muted', 'themeCustomMuted'], ['accent', 'themeCustomAccent'],
|
|
972
|
+
];
|
|
973
|
+
const customVal = (mode, k) => (themeCfg.custom[mode] && /^#[0-9a-f]{6}$/i.test(themeCfg.custom[mode][k] || '')
|
|
974
|
+
? themeCfg.custom[mode][k] : CUSTOM_DEFAULTS[mode][k]);
|
|
975
|
+
const customInput = (mode, k, label) => {
|
|
976
|
+
const inp = h('input', {
|
|
977
|
+
type: 'color', value: customVal(mode, k), title: label,
|
|
978
|
+
style: 'width:30px;height:22px;padding:0;border:1px solid var(--color-card-border);border-radius:5px;background:transparent;cursor:pointer',
|
|
979
|
+
});
|
|
980
|
+
inp.addEventListener('input', () => {
|
|
981
|
+
const next = {
|
|
982
|
+
light: { ...themeCfg.custom.light },
|
|
983
|
+
dark: { ...themeCfg.custom.dark },
|
|
984
|
+
};
|
|
985
|
+
next[mode][k] = inp.value.toLowerCase();
|
|
986
|
+
clearTimeout(customSaveTimer);
|
|
987
|
+
customSaveTimer = setTimeout(() => { void saveTheme({ skin: 'custom', custom: next }); }, 250);
|
|
988
|
+
});
|
|
989
|
+
return h('label', { class: 'flex items-center gap-1.5 text-ui-xs text-foreground-subtle' }, inp, label);
|
|
990
|
+
};
|
|
991
|
+
const customSwatch = (mode) => [
|
|
992
|
+
customVal(mode, 'bg'), customVal(mode, 'card'), customVal(mode, 'fg'),
|
|
993
|
+
customVal(mode, 'muted'), customVal(mode, 'accent'),
|
|
994
|
+
];
|
|
995
|
+
const renderThemeColors = () => {
|
|
996
|
+
paneStylesSub3.replaceChildren(
|
|
997
|
+
h('div', { class: 'mb-1 text-ui-sm font-medium' }, L.themeSkinTitle),
|
|
998
|
+
h('p', { class: 'mb-2 text-ui-xs/relaxed text-foreground-subtle' }, L.themeSkinDesc),
|
|
999
|
+
// 4 列 × 3 行可视,超出滚动;色条 8 块(底/卡片/加深面/边框/强调×2/次级文字/正文)
|
|
1000
|
+
h('div', { class: 'grid grid-cols-4 gap-2', style: 'max-height:13.5rem;overflow-y:auto;' },
|
|
1001
|
+
...THEME_SKINS.map((skin) => h('button', {
|
|
1002
|
+
type: 'button', class: 'zcodepro-skin-card',
|
|
1003
|
+
'data-state': themeCfg.skin === skin.id ? 'active' : 'inactive',
|
|
1004
|
+
onClick: () => void saveTheme({ skin: skin.id }),
|
|
1005
|
+
},
|
|
1006
|
+
h('div', { class: 'zcodepro-skin-swatches' },
|
|
1007
|
+
...skin.preview.map((c) => h('span', { style: `background:${c}` }))),
|
|
1008
|
+
h('span', { class: 'text-ui-xs text-foreground' }, isZhLocale() ? skin.zh : skin.en))),
|
|
1009
|
+
h('button', {
|
|
1010
|
+
type: 'button', class: 'zcodepro-skin-card',
|
|
1011
|
+
'data-state': themeCfg.skin === 'custom' ? 'active' : 'inactive',
|
|
1012
|
+
onClick: () => void saveTheme({ skin: 'custom' }),
|
|
1013
|
+
},
|
|
1014
|
+
h('div', { class: 'zcodepro-skin-swatches' },
|
|
1015
|
+
...customSwatch('light').map((c) => h('span', { style: `background:${c}` })),
|
|
1016
|
+
...customSwatch('dark').map((c) => h('span', { style: `background:${c}` }))),
|
|
1017
|
+
h('span', { class: 'text-ui-xs text-foreground' }, L.themeSkinCustom))),
|
|
1018
|
+
h('div', { class: 'mt-3 mb-1 text-ui-sm font-medium' }, L.themeCustomTitle),
|
|
1019
|
+
h('p', { class: 'mb-2 text-ui-xs/relaxed text-foreground-subtle' }, L.themeCustomHint),
|
|
1020
|
+
h('div', { class: 'grid grid-cols-2 gap-3' },
|
|
1021
|
+
...['light', 'dark'].map((mode) => h('div', { class: 'rounded-xl border border-border p-2' },
|
|
1022
|
+
h('div', { class: 'mb-1.5 text-ui-xs font-medium text-foreground-subtle' },
|
|
1023
|
+
mode === 'light' ? L.themeCustomLight : L.themeCustomDark),
|
|
1024
|
+
h('div', { class: 'flex flex-col gap-1.5' },
|
|
1025
|
+
...CUSTOM_FIELDS.map(([k, key]) => customInput(mode, k, L[key])))))),
|
|
1026
|
+
);
|
|
1027
|
+
};
|
|
1028
|
+
// 分区背景行:跟随全局 / 图库(下拉选图)/ 文件夹 / 网址
|
|
1029
|
+
const setRegion = (region, value) => {
|
|
1030
|
+
void saveTheme({ bg: { regions: { ...themeCfg.bg.regions, [region]: value } } });
|
|
1031
|
+
};
|
|
1032
|
+
const regionRow = (region, label) => {
|
|
1033
|
+
const cur = themeCfg.bg.regions[region] || null;
|
|
1034
|
+
const mode = cur ? cur.mode : null;
|
|
1035
|
+
const rBtn = (id, text) => h('button', {
|
|
1036
|
+
type: 'button', class: 'zcodepro-tab',
|
|
1037
|
+
'data-state': mode === id ? 'active' : 'inactive',
|
|
1038
|
+
onClick: () => {
|
|
1039
|
+
if (id === null) { setRegion(region, null); return; }
|
|
1040
|
+
const keep = themeCfg.bg.regions[region];
|
|
1041
|
+
setRegion(region, { mode: id, file: keep && keep.file || '', folder: keep && keep.folder || '', url: keep && keep.url || '' });
|
|
1042
|
+
},
|
|
1043
|
+
}, text);
|
|
1044
|
+
let valueCtrl = null;
|
|
1045
|
+
if (mode === 'library') {
|
|
1046
|
+
const sel = h('select', { class: 'h-7 max-w-40 rounded-lg border border-border bg-input px-1.5 text-ui-xs text-foreground outline-none' });
|
|
1047
|
+
sel.append(h('option', { value: '' }, '—'));
|
|
1048
|
+
for (const f of bgFiles) sel.append(h('option', { value: f.name }, f.name));
|
|
1049
|
+
sel.value = (cur && cur.file) || '';
|
|
1050
|
+
sel.addEventListener('change', () => setRegion(region, { ...cur, file: sel.value }));
|
|
1051
|
+
valueCtrl = sel;
|
|
1052
|
+
} else if (mode === 'folder') {
|
|
1053
|
+
const inp = h('input', { type: 'text', value: (cur && cur.folder) || '', class: themeInputCls.replace('h-8', 'h-7'), placeholder: '/path/to/wallpapers' });
|
|
1054
|
+
inp.addEventListener('change', () => { if (inp.value.trim()) setRegion(region, { ...cur, folder: inp.value.trim() }); });
|
|
1055
|
+
valueCtrl = inp;
|
|
1056
|
+
} else if (mode === 'url') {
|
|
1057
|
+
const inp = h('input', { type: 'url', value: (cur && cur.url) || '', class: themeInputCls.replace('h-8', 'h-7'), placeholder: 'https://...' });
|
|
1058
|
+
inp.addEventListener('change', () => { if (/^https?:\/\/\S+$/.test(inp.value.trim())) setRegion(region, { ...cur, url: inp.value.trim() }); });
|
|
1059
|
+
valueCtrl = inp;
|
|
1060
|
+
}
|
|
1061
|
+
return h('div', { class: 'flex flex-wrap items-center gap-2' },
|
|
1062
|
+
h('span', { class: 'w-16 shrink-0 text-ui-xs text-foreground' }, label),
|
|
1063
|
+
h('div', { class: 'flex flex-wrap gap-1' },
|
|
1064
|
+
rBtn(null, L.themeBgRegionFollow), rBtn('library', L.themeBgModeLibrary),
|
|
1065
|
+
rBtn('folder', L.themeBgModeFolder), rBtn('url', L.themeBgModeUrl)),
|
|
1066
|
+
valueCtrl || h('span', { class: 'text-ui-xs text-foreground-subtle' }, ''));
|
|
1067
|
+
};
|
|
1068
|
+
const renderThemeBg = () => {
|
|
1069
|
+
// 三个调节参数(滑杆行构造器:label + 范围 + 单位 + 提示 + 防抖保存)
|
|
1070
|
+
const sliderRow = (label, title, min, max, step, val, unit, commit, hint) => {
|
|
1071
|
+
// 标签列固定宽(容最长标签六字;内联样式——应用的 Tailwind 按需编译
|
|
1072
|
+
// 不包含 w-[72px] 这类任意值类):多行滑杆的轨道起点对齐同一竖线
|
|
1073
|
+
const lab = h('span', { class: 'shrink-0 whitespace-nowrap text-ui-xs font-medium text-foreground-subtle', title, style: 'width:72px' }, label);
|
|
1074
|
+
const valLabel = h('span', { class: 'w-12 text-right text-ui-xs tabular-nums text-foreground-subtle' }, val + unit);
|
|
1075
|
+
const range = h('input', {
|
|
1076
|
+
type: 'range', min: String(min), max: String(max), step: String(step), value: String(val),
|
|
1077
|
+
class: 'zcodepro-range', 'aria-label': label, style: 'flex:1',
|
|
1078
|
+
});
|
|
1079
|
+
let timer = null;
|
|
1080
|
+
range.addEventListener('input', () => {
|
|
1081
|
+
const v = Number(range.value);
|
|
1082
|
+
valLabel.textContent = v + unit;
|
|
1083
|
+
clearTimeout(timer);
|
|
1084
|
+
timer = setTimeout(() => commit(v), 300);
|
|
1085
|
+
});
|
|
1086
|
+
return h('div', { class: 'flex flex-col gap-0.5' },
|
|
1087
|
+
h('div', { class: 'flex items-center gap-3' }, lab, range, valLabel),
|
|
1088
|
+
hint ? h('span', { class: 'text-ui-xs/relaxed text-foreground-subtle' }, hint) : null);
|
|
1089
|
+
};
|
|
1090
|
+
// 数值兜底:null(旧配置带入)与 undefined 都要落默认,?? 只兜 undefined
|
|
1091
|
+
const numOr = (v, d) => (typeof v === 'number' && Number.isFinite(v) ? v : d);
|
|
1092
|
+
const maskRow = () => sliderRow(L.themeBgMask, L.themeBgMaskHint, 0, 99, 1, Math.round(numOr(themeCfg.bg.maskOpacity, 0.82) * 100), '%',
|
|
1093
|
+
(v) => { void saveTheme({ bg: { maskOpacity: v / 100 } }); }, L.themeBgMaskHint2);
|
|
1094
|
+
const blurRow = () => sliderRow(L.themeBgBlur, '', 0, 24, 1, numOr(themeCfg.bg.blurPx, 0), 'px',
|
|
1095
|
+
(v) => { void saveTheme({ bg: { blurPx: v } }); }, '');
|
|
1096
|
+
const panelRow = () => sliderRow(L.themeBgPanel, '', 0, 100, 1, Math.round(numOr(themeCfg.bg.panelOpacity, 0.3) * 100), '%',
|
|
1097
|
+
(v) => { void saveTheme({ bg: { panelOpacity: v / 100 } }); }, L.themeBgPanelHint);
|
|
1098
|
+
const modeBtn = (id, label) => h('button', {
|
|
1099
|
+
type: 'button', class: 'zcodepro-tab',
|
|
1100
|
+
'data-state': themeCfg.bg.mode === id ? 'active' : 'inactive',
|
|
1101
|
+
onClick: () => void saveTheme({ bg: { mode: id } }),
|
|
1102
|
+
}, label);
|
|
1103
|
+
// 文件夹路径输入 + 浏览(系统对话框);确认时先校验目录再保存
|
|
1104
|
+
const folderInput = h('input', { type: 'text', value: themeCfg.bg.folder, class: themeInputCls, placeholder: '/path/to/wallpapers' });
|
|
1105
|
+
const applyFolder = async (dir) => {
|
|
1106
|
+
const v = (dir || '').trim();
|
|
1107
|
+
if (!v) return;
|
|
1108
|
+
const res = await rpc('/bg/folder', { method: 'POST', body: { dir: v } });
|
|
1109
|
+
if (!res.ok) {
|
|
1110
|
+
showToast(L.failed + ': ' + errText(res), 'error');
|
|
1111
|
+
renderThemeBg();
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
folderInfo = { count: (res.files || []).length, dir: res.dir || v };
|
|
1115
|
+
await saveTheme({ bg: { mode: 'folder', folder: v } });
|
|
1116
|
+
};
|
|
1117
|
+
folderInput.addEventListener('change', () => { void applyFolder(folderInput.value); });
|
|
1118
|
+
const browseBtn = btnSmall(L.themeBgFolderPick, () => {
|
|
1119
|
+
void (async () => {
|
|
1120
|
+
browseBtn.disabled = true;
|
|
1121
|
+
try {
|
|
1122
|
+
const res = await rpc('/pick-folder?start=' + encodeURIComponent(themeCfg.bg.folder || '') + '&title=' + encodeURIComponent(L.themeBgFolderPickTitle));
|
|
1123
|
+
if (res && res.ok && res.path) {
|
|
1124
|
+
folderInput.value = res.path;
|
|
1125
|
+
await applyFolder(res.path);
|
|
1126
|
+
}
|
|
1127
|
+
} finally { browseBtn.disabled = false; }
|
|
1128
|
+
})();
|
|
1129
|
+
});
|
|
1130
|
+
const randomCheck = h('input', { type: 'checkbox' });
|
|
1131
|
+
randomCheck.checked = themeCfg.bg.folderRandom !== false;
|
|
1132
|
+
randomCheck.addEventListener('change', () => { void saveTheme({ bg: { folderRandom: randomCheck.checked } }); });
|
|
1133
|
+
// 网址输入:change 提交;空值回无背景
|
|
1134
|
+
const urlInput = h('input', { type: 'url', value: themeCfg.bg.url, class: themeInputCls, placeholder: 'https://...' });
|
|
1135
|
+
urlInput.addEventListener('change', () => {
|
|
1136
|
+
const v = urlInput.value.trim();
|
|
1137
|
+
if (!v) { void saveTheme({ bg: { mode: 'none', url: '' } }); return; }
|
|
1138
|
+
if (!/^https?:\/\/\S+$/.test(v)) { showToast(L.themeBgUrlInvalid, 'error'); renderThemeBg(); return; }
|
|
1139
|
+
void saveTheme({ bg: { mode: 'url', url: v } });
|
|
1140
|
+
});
|
|
1141
|
+
const libCard = (f) => {
|
|
1142
|
+
const del = h('button', {
|
|
1143
|
+
type: 'button', class: 'zcodepro-bg-del', title: L.themeBgDelete,
|
|
1144
|
+
onClick: (ev) => {
|
|
1145
|
+
ev.stopPropagation();
|
|
1146
|
+
void (async () => {
|
|
1147
|
+
const res = await rpc('/bg/delete', { method: 'POST', body: { name: f.name } });
|
|
1148
|
+
if (!res.ok) { showToast(L.failed + ': ' + errText(res), 'error'); return; }
|
|
1149
|
+
if (Array.isArray(res.files)) bgFiles = res.files;
|
|
1150
|
+
await refreshBgFiles();
|
|
1151
|
+
})();
|
|
1152
|
+
},
|
|
1153
|
+
}, '✕');
|
|
1154
|
+
return h('button', {
|
|
1155
|
+
type: 'button', class: 'zcodepro-bg-card',
|
|
1156
|
+
'data-state': themeCfg.bg.mode === 'library' && themeCfg.bg.file === f.name ? 'active' : 'inactive',
|
|
1157
|
+
title: f.name,
|
|
1158
|
+
onClick: () => void saveTheme({ bg: { mode: 'library', file: themeCfg.bg.file === f.name ? '' : f.name } }),
|
|
1159
|
+
}, h('img', { src: bgUrl(f.name), alt: '', loading: 'lazy' }), del);
|
|
1160
|
+
};
|
|
1161
|
+
const section = (titleText, body) => h('div', { class: 'mt-3' },
|
|
1162
|
+
h('div', { class: 'mb-1.5 text-ui-xs font-medium text-foreground-subtle' }, titleText), body);
|
|
1163
|
+
const modeArea = themeCfg.bg.mode === 'library' ? [
|
|
1164
|
+
section(L.themeBgLibrary + (bgFiles.length ? `(${bgFiles.length})` : ''), h('div', { class: 'flex flex-col gap-2' },
|
|
1165
|
+
h('div', { class: 'flex items-center justify-between gap-2' },
|
|
1166
|
+
h('span', { class: 'text-ui-xs/relaxed text-foreground-subtle' }, bgLoading ? '…' : (bgFiles.length ? '' : L.themeBgEmpty)),
|
|
1167
|
+
btnSmall(L.themeBgUpload, () => bgInput.click())),
|
|
1168
|
+
bgFiles.length ? h('div', { class: 'grid grid-cols-4 gap-2', style: 'max-height:13.5rem;overflow-y:auto;' },
|
|
1169
|
+
...bgFiles.map(libCard)) : null)),
|
|
1170
|
+
] : themeCfg.bg.mode === 'folder' ? [
|
|
1171
|
+
section(L.themeBgModeFolder, h('div', { class: 'flex flex-col gap-2' },
|
|
1172
|
+
h('div', { class: 'flex items-center gap-2' }, folderInput, browseBtn),
|
|
1173
|
+
h('div', { class: 'flex items-center gap-2' },
|
|
1174
|
+
h('label', { class: 'flex items-center gap-1.5 text-ui-xs text-foreground-subtle' }, randomCheck, L.themeBgFolderRandom),
|
|
1175
|
+
numField(themeCfg.bg.intervalMin, 0, 1440, (v) => { void saveTheme({ bg: { intervalMin: v } }); }, L.themeBgIntervalHint)
|
|
1176
|
+
, h('span', { class: 'text-ui-xs text-foreground-subtle' }, L.themeBgIntervalUnit)),
|
|
1177
|
+
h('p', { class: 'text-ui-xs/relaxed text-foreground-subtle' },
|
|
1178
|
+
themeCfg.bg.folder
|
|
1179
|
+
? (folderInfo.count > 0 ? L.themeBgFolderCount.replaceAll('{n}', String(folderInfo.count)) : L.themeBgFolderMissing)
|
|
1180
|
+
: L.themeBgFolderHint))),
|
|
1181
|
+
] : themeCfg.bg.mode === 'url' ? [
|
|
1182
|
+
section(L.themeBgModeUrl, h('div', { class: 'flex flex-col gap-2' },
|
|
1183
|
+
urlInput,
|
|
1184
|
+
h('div', { class: 'flex items-center gap-2' },
|
|
1185
|
+
numField(themeCfg.bg.intervalMin, 0, 1440, (v) => { void saveTheme({ bg: { intervalMin: v } }); }, L.themeBgUrlIntervalHint),
|
|
1186
|
+
h('span', { class: 'text-ui-xs text-foreground-subtle' }, L.themeBgIntervalUnit)),
|
|
1187
|
+
h('p', { class: 'text-ui-xs/relaxed text-foreground-subtle' }, L.themeBgUrlHint))),
|
|
1188
|
+
] : [h('p', { class: 'mt-3 text-ui-xs/relaxed text-foreground-subtle' }, L.themeBgNoneHint)];
|
|
1189
|
+
const fitBtn = (id, label) => h('button', {
|
|
1190
|
+
type: 'button', class: 'zcodepro-tab',
|
|
1191
|
+
'data-state': (themeCfg.bg.fit || 'cover') === id ? 'active' : 'inactive',
|
|
1192
|
+
onClick: () => void saveTheme({ bg: { fit: id } }),
|
|
1193
|
+
}, label);
|
|
1194
|
+
paneStylesSub4.replaceChildren(
|
|
1195
|
+
h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' }, L.themeBgDesc),
|
|
1196
|
+
h('div', { class: 'mt-2 flex flex-wrap gap-1' },
|
|
1197
|
+
modeBtn('none', L.themeBgModeNone), modeBtn('library', L.themeBgModeLibrary),
|
|
1198
|
+
modeBtn('folder', L.themeBgModeFolder), modeBtn('url', L.themeBgModeUrl)),
|
|
1199
|
+
...modeArea,
|
|
1200
|
+
section(L.themeBgRegions, h('div', { class: 'flex flex-col gap-2' },
|
|
1201
|
+
regionRow('left', L.themeBgRegionLeft),
|
|
1202
|
+
regionRow('center', L.themeBgRegionCenter),
|
|
1203
|
+
regionRow('right', L.themeBgRegionRight))),
|
|
1204
|
+
h('div', { class: 'mt-3 flex flex-wrap items-center gap-2' },
|
|
1205
|
+
h('span', { class: 'text-ui-xs font-medium text-foreground-subtle' }, L.themeBgFit),
|
|
1206
|
+
fitBtn('cover', L.themeBgFitCover), fitBtn('contain', L.themeBgFitContain),
|
|
1207
|
+
fitBtn('fill', L.themeBgFitFill), fitBtn('tile', L.themeBgFitTile)),
|
|
1208
|
+
h('div', { class: 'mt-3 flex flex-col gap-2' },
|
|
1209
|
+
maskRow(), blurRow(), panelRow()),
|
|
1210
|
+
bgInput,
|
|
1211
|
+
);
|
|
1212
|
+
};
|
|
1213
|
+
renderThemeColors();
|
|
1214
|
+
renderThemeBg();
|
|
1215
|
+
void refreshBgFiles();
|
|
1216
|
+
void refreshFolderInfo();
|
|
1217
|
+
|
|
861
1218
|
let stylesSubTab = 'styles';
|
|
862
1219
|
const stylesSubTablist = h('div', { role: 'tablist', 'aria-orientation': 'horizontal', class: 'zcodepro-tablist' });
|
|
863
1220
|
const renderStylesSubTabs = () => stylesSubTablist.replaceChildren(...[
|
|
864
1221
|
['styles', L.tabStylesPane],
|
|
865
1222
|
['status', L.tabStatusPane],
|
|
1223
|
+
['colors', L.themeColorsPane],
|
|
1224
|
+
['bgimg', L.themeBgPane],
|
|
866
1225
|
].map(([id, label]) => h('button', {
|
|
867
1226
|
type: 'button', role: 'tab', class: 'zcodepro-tab',
|
|
868
1227
|
'aria-selected': String(stylesSubTab === id),
|
|
@@ -871,11 +1230,13 @@ export function openSettingsDialog() {
|
|
|
871
1230
|
stylesSubTab = id;
|
|
872
1231
|
paneStylesSub1.style.display = id === 'styles' ? '' : 'none';
|
|
873
1232
|
paneStylesSub2.style.display = id === 'status' ? '' : 'none';
|
|
1233
|
+
paneStylesSub3.style.display = id === 'colors' ? '' : 'none';
|
|
1234
|
+
paneStylesSub4.style.display = id === 'bgimg' ? '' : 'none';
|
|
874
1235
|
renderStylesSubTabs();
|
|
875
1236
|
},
|
|
876
1237
|
}, label)));
|
|
877
1238
|
renderStylesSubTabs();
|
|
878
|
-
paneStyles.append(stylesSubTablist, paneStylesSub1, paneStylesSub2);
|
|
1239
|
+
paneStyles.append(stylesSubTablist, paneStylesSub1, paneStylesSub2, paneStylesSub3, paneStylesSub4);
|
|
879
1240
|
paneStylesSub1.append(
|
|
880
1241
|
toolbarSwitchWrap,
|
|
881
1242
|
h('div', { class: 'mt-2 flex flex-col gap-0.5 rounded-xl border border-border p-1' },
|
|
@@ -1468,18 +1829,11 @@ export function openSettingsDialog() {
|
|
|
1468
1829
|
h('p', { class: 'mt-2 text-ui-xs/relaxed text-foreground-subtle' }, L.syncHint),
|
|
1469
1830
|
);
|
|
1470
1831
|
|
|
1471
|
-
//
|
|
1472
|
-
//
|
|
1473
|
-
//
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
if (!res.ok) {
|
|
1477
|
-
showToast(L.failed + ': ' + errText(res), 'error');
|
|
1478
|
-
return false;
|
|
1479
|
-
}
|
|
1480
|
-
if (res.config) config.devMode = res.config.devMode;
|
|
1481
|
-
return true;
|
|
1482
|
-
};
|
|
1832
|
+
// “调试”:重载按钮。重载 = 构建 → 换新启动器进程(新 helper 代码与新注入
|
|
1833
|
+
// 脚本)→ 各 ZCode 窗口检测到换代自动整页刷新重新注入;ZCode 本体与所有
|
|
1834
|
+
// 会话不受影响,不用重启 ZCode。标签页本身只在开发模式生效时显示(环境
|
|
1835
|
+
// 变量 ZCODEPRO_DEV=1 或配置文件 devMode),不放开关——关掉开关标签页就
|
|
1836
|
+
// 消失,再也点不回来,没意义
|
|
1483
1837
|
const devReloadBtn = btnSmall(L.devReload, () => { void runDevReload(); }, 'shrink-0', 'primary');
|
|
1484
1838
|
const runDevReload = async () => {
|
|
1485
1839
|
devReloadBtn.disabled = true;
|
|
@@ -1490,25 +1844,15 @@ export function openSettingsDialog() {
|
|
|
1490
1844
|
// 就自己刷新,宁可多刷一次也不能停在旧代码 + 失效 token 上
|
|
1491
1845
|
setTimeout(() => { try { location.reload(); } catch { /* ignore */ } }, 2000);
|
|
1492
1846
|
};
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
...(dev ? [h('div', { class: 'flex items-center justify-between gap-2 px-2 py-1.5' },
|
|
1503
|
-
h('div', { class: 'min-w-0' },
|
|
1504
|
-
h('div', { class: 'text-ui-sm font-medium text-foreground' }, L.devReload),
|
|
1505
|
-
h('div', { class: 'mt-0.5 text-ui-xs/relaxed text-foreground-subtle' }, L.devReloadDesc)),
|
|
1506
|
-
devReloadBtn)] : []),
|
|
1507
|
-
),
|
|
1508
|
-
);
|
|
1509
|
-
};
|
|
1510
|
-
renderDevPane();
|
|
1511
|
-
paneDebug.append(devModeWrap);
|
|
1847
|
+
paneDebug.append(
|
|
1848
|
+
h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' }, L.devModeDesc),
|
|
1849
|
+
h('div', { class: 'mt-3 rounded-xl border border-border' },
|
|
1850
|
+
h('div', { class: 'flex items-center justify-between gap-2 px-2 py-1.5' },
|
|
1851
|
+
h('div', { class: 'min-w-0' },
|
|
1852
|
+
h('div', { class: 'text-ui-sm font-medium text-foreground' }, L.devReload),
|
|
1853
|
+
h('div', { class: 'mt-0.5 text-ui-xs/relaxed text-foreground-subtle' }, L.devReloadDesc)),
|
|
1854
|
+
devReloadBtn)),
|
|
1855
|
+
);
|
|
1512
1856
|
|
|
1513
1857
|
// “视觉代理”:编辑 ~/.zcode/zcode-vision.json(zcode-vision 插件与 /vision-* 命令共用同一文件)。
|
|
1514
1858
|
// 代理列表顺序即执行链;结构性改动(开关/模式/排序/删增)即时保存,文本输入防抖保存。
|
|
@@ -1534,6 +1878,8 @@ export function openSettingsDialog() {
|
|
|
1534
1878
|
let visionCfg = visionRes.ok && visionRes.config && typeof visionRes.config === 'object'
|
|
1535
1879
|
? structuredClone(visionRes.config)
|
|
1536
1880
|
: null;
|
|
1881
|
+
// 自动初始化只试一次(每次打开弹窗):写盘失败回退手动“添加代理”卡
|
|
1882
|
+
let visionAutoInitTried = false;
|
|
1537
1883
|
let visionSaveTimer = null;
|
|
1538
1884
|
const persistVision = async () => {
|
|
1539
1885
|
visionCfg.chain = visionCfg.proxies.map((p) => (p.name || '').trim()).filter(Boolean);
|
|
@@ -1600,6 +1946,16 @@ export function openSettingsDialog() {
|
|
|
1600
1946
|
return;
|
|
1601
1947
|
}
|
|
1602
1948
|
if (!visionCfg) {
|
|
1949
|
+
// 插件已装但配置文件未建(刚装完第一次打开的面板状态):自动写入默认链,
|
|
1950
|
+
// 不再要求手动点一次“添加代理”;先照常渲染手动卡兜底,写盘成功后整面重绘
|
|
1951
|
+
if (!visionAutoInitTried && visionRes.hook) {
|
|
1952
|
+
visionAutoInitTried = true;
|
|
1953
|
+
void (async () => {
|
|
1954
|
+
const res = await rpc('/vision', { method: 'POST', body: { config: VISION_DEFAULT_CFG } });
|
|
1955
|
+
if (res.ok) visionCfg = structuredClone(res.config);
|
|
1956
|
+
renderVision();
|
|
1957
|
+
})();
|
|
1958
|
+
}
|
|
1603
1959
|
const initBtn = btnSmall(L.visionAddProxy, async () => {
|
|
1604
1960
|
const res = await rpc('/vision', { method: 'POST', body: { config: VISION_DEFAULT_CFG } });
|
|
1605
1961
|
if (res.ok) { visionCfg = structuredClone(res.config); renderVision(); }
|
|
@@ -10,6 +10,7 @@ let enabled = true;
|
|
|
10
10
|
let items = DEFAULT_STATUS_ITEMS.map((x) => ({ ...x }));
|
|
11
11
|
let barEl = null;
|
|
12
12
|
let lastStats = null; // /stats/usage 响应缓存(失败保旧值)
|
|
13
|
+
let lastRenderedHtml = null; // renderBar 去重:内容没变不重写 DOM
|
|
13
14
|
|
|
14
15
|
// —— 工具 ——
|
|
15
16
|
|
|
@@ -21,7 +22,7 @@ function readCtx() {
|
|
|
21
22
|
if (el.closest('.zcodepro-statusbar')) continue;
|
|
22
23
|
const text = el.getAttribute('title') || el.getAttribute('aria-label') || '';
|
|
23
24
|
if (!/上下文|context/i.test(text)) continue;
|
|
24
|
-
//
|
|
25
|
+
// 格式为“已用 497,101 / 总量 1,000,000”:斜杠后可能带“总量”等文字再接数字
|
|
25
26
|
const m = text.match(/([\d][\d,]*)\s*\/[^0-9]*([\d][\d,]*)/);
|
|
26
27
|
if (!m) continue;
|
|
27
28
|
const used = Number(m[1].replace(/,/g, ''));
|
|
@@ -187,12 +188,14 @@ function renderBar() {
|
|
|
187
188
|
if (!enabled || !host || !host.parentElement) {
|
|
188
189
|
if (barEl) barEl.remove();
|
|
189
190
|
barEl = null;
|
|
191
|
+
lastRenderedHtml = null; // 元素没了,去重基线同步清空,重建后首渲染不被跳过
|
|
190
192
|
return;
|
|
191
193
|
}
|
|
192
194
|
if (!barEl || !barEl.isConnected) {
|
|
193
195
|
if (!barEl) {
|
|
194
196
|
barEl = document.createElement('div');
|
|
195
197
|
barEl.className = 'zcodepro-statusbar';
|
|
198
|
+
lastRenderedHtml = null; // 新元素:内容基线作废
|
|
196
199
|
}
|
|
197
200
|
host.parentElement.insertBefore(barEl, host.nextSibling);
|
|
198
201
|
}
|
|
@@ -219,19 +222,29 @@ function renderBar() {
|
|
|
219
222
|
// 组内项之间用 ·,跨组竖线两侧不再叠加 ·
|
|
220
223
|
const SEP = '<span class="zcodepro-statusbar-sep">·</span>';
|
|
221
224
|
const GROUPSEP = '<span class="zcodepro-statusbar-groupsep"></span>';
|
|
222
|
-
|
|
225
|
+
// 数值没变就不重写 DOM:每 2 秒 tick 全量重建会在流式渲染期持续触发布局
|
|
226
|
+
//(CPU 剖析中 renderBar 是我们唯一上榜的函数)
|
|
227
|
+
const html = parts.reduce((acc, p, i) => {
|
|
223
228
|
if (i === 0) return p;
|
|
224
229
|
if (p === GROUPSEP || parts[i - 1] === GROUPSEP) return acc + p;
|
|
225
230
|
return acc + SEP + p;
|
|
226
231
|
}, '');
|
|
227
232
|
barEl.style.display = parts.length ? '' : 'none';
|
|
233
|
+
if (html === lastRenderedHtml) return;
|
|
234
|
+
lastRenderedHtml = html;
|
|
235
|
+
barEl.innerHTML = html;
|
|
228
236
|
}
|
|
229
237
|
|
|
230
238
|
async function tick() {
|
|
231
239
|
if (!enabled || document.hidden) return;
|
|
232
240
|
refreshOfficial();
|
|
233
|
-
|
|
234
|
-
|
|
241
|
+
// 全局项优先取官方快照(5 分钟一更):就绪后不再每 2 秒拉本地聚合——
|
|
242
|
+
// helper 侧聚合每次都要遍历全部 rollout 记录(稳态实测约 13ms/次),
|
|
243
|
+
// 官方不可用(刚启动/接口失败)时才回退本地轮询
|
|
244
|
+
if (!official) {
|
|
245
|
+
const res = await rpc('/stats/usage').catch(() => null);
|
|
246
|
+
if (res && res.ok) lastStats = res;
|
|
247
|
+
}
|
|
235
248
|
renderBar();
|
|
236
249
|
}
|
|
237
250
|
|