dsh-gufa-code 0.0.0-stage → 1.0.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/LICENSE +21 -0
- package/README.md +93 -2
- package/client.js +11548 -0
- package/cordis.patch.yml +8 -0
- package/docs/icon-180.png +0 -0
- package/docs/icon.svg +18 -0
- package/docs/market-submission.md +32 -0
- package/docs/screenshot-ide.jpg +0 -0
- package/host/assets.js +31 -0
- package/host/chat.js +523 -0
- package/host/context.js +438 -0
- package/host/dev.js +35 -0
- package/host/fs.js +306 -0
- package/host/git.js +532 -0
- package/host/project.js +305 -0
- package/host/ssh.js +54 -0
- package/host/terminal-pty.js +237 -0
- package/host/terminal.js +947 -0
- package/icon.svg +18 -0
- package/index.js +63 -0
- package/locale/en.json +121 -0
- package/locale/zh.json +121 -0
- package/package.json +67 -4
- package/screenshots.json +3 -0
- package/scripts/build-client.mjs +38 -0
- package/scripts/selftest-client.mjs +457 -0
- package/scripts/selftest-host.mjs +197 -0
- package/scripts/selftest-http.mjs +284 -0
- package/scripts/selftest-router.mjs +101 -0
- package/scripts/selftest-terminal-client.mjs +314 -0
- package/scripts/selftest-terminal-pty.mjs +190 -0
- package/scripts/selftest-terminal.mjs +351 -0
- package/scripts/test-chat-render.mjs +702 -0
- package/scripts/test-chat.mjs +761 -0
- package/scripts/test-editor.mjs +516 -0
- package/scripts/verify-workspace-sessions.mjs +135 -0
- package/src/client/00-core.js +1686 -0
- package/src/client/10-icons.js +205 -0
- package/src/client/20-editor.js +1318 -0
- package/src/client/30-terminal.js +1510 -0
- package/src/client/40-chat.js +1184 -0
- package/src/client/46-gitgraph.js +278 -0
- package/src/client/50-panels.js +916 -0
- package/src/client/55-backdrop.js +508 -0
- package/src/client/60-dock.js +662 -0
- package/src/client/85-shortcuts.js +516 -0
- package/src/client/90-app.js +2272 -0
- package/src/client/99-main.js +449 -0
- package/vendor/monaco/LICENSE +21 -0
- package/vendor/monaco/ThirdPartyNotices.txt +448 -0
- package/vendor/monaco/vs/base/browser/ui/codicons/codicon/codicon.ttf +0 -0
- package/vendor/monaco/vs/base/worker/workerMain.js +31 -0
- package/vendor/monaco/vs/basic-languages/abap/abap.js +10 -0
- package/vendor/monaco/vs/basic-languages/apex/apex.js +10 -0
- package/vendor/monaco/vs/basic-languages/azcli/azcli.js +10 -0
- package/vendor/monaco/vs/basic-languages/bat/bat.js +10 -0
- package/vendor/monaco/vs/basic-languages/bicep/bicep.js +11 -0
- package/vendor/monaco/vs/basic-languages/cameligo/cameligo.js +10 -0
- package/vendor/monaco/vs/basic-languages/clojure/clojure.js +10 -0
- package/vendor/monaco/vs/basic-languages/coffee/coffee.js +10 -0
- package/vendor/monaco/vs/basic-languages/cpp/cpp.js +10 -0
- package/vendor/monaco/vs/basic-languages/csharp/csharp.js +10 -0
- package/vendor/monaco/vs/basic-languages/csp/csp.js +10 -0
- package/vendor/monaco/vs/basic-languages/css/css.js +12 -0
- package/vendor/monaco/vs/basic-languages/cypher/cypher.js +10 -0
- package/vendor/monaco/vs/basic-languages/dart/dart.js +10 -0
- package/vendor/monaco/vs/basic-languages/dockerfile/dockerfile.js +10 -0
- package/vendor/monaco/vs/basic-languages/ecl/ecl.js +10 -0
- package/vendor/monaco/vs/basic-languages/elixir/elixir.js +10 -0
- package/vendor/monaco/vs/basic-languages/flow9/flow9.js +10 -0
- package/vendor/monaco/vs/basic-languages/freemarker2/freemarker2.js +12 -0
- package/vendor/monaco/vs/basic-languages/fsharp/fsharp.js +10 -0
- package/vendor/monaco/vs/basic-languages/go/go.js +10 -0
- package/vendor/monaco/vs/basic-languages/graphql/graphql.js +10 -0
- package/vendor/monaco/vs/basic-languages/handlebars/handlebars.js +10 -0
- package/vendor/monaco/vs/basic-languages/hcl/hcl.js +10 -0
- package/vendor/monaco/vs/basic-languages/html/html.js +10 -0
- package/vendor/monaco/vs/basic-languages/ini/ini.js +10 -0
- package/vendor/monaco/vs/basic-languages/java/java.js +10 -0
- package/vendor/monaco/vs/basic-languages/javascript/javascript.js +10 -0
- package/vendor/monaco/vs/basic-languages/julia/julia.js +10 -0
- package/vendor/monaco/vs/basic-languages/kotlin/kotlin.js +10 -0
- package/vendor/monaco/vs/basic-languages/less/less.js +11 -0
- package/vendor/monaco/vs/basic-languages/lexon/lexon.js +10 -0
- package/vendor/monaco/vs/basic-languages/liquid/liquid.js +10 -0
- package/vendor/monaco/vs/basic-languages/lua/lua.js +10 -0
- package/vendor/monaco/vs/basic-languages/m3/m3.js +10 -0
- package/vendor/monaco/vs/basic-languages/markdown/markdown.js +10 -0
- package/vendor/monaco/vs/basic-languages/mdx/mdx.js +10 -0
- package/vendor/monaco/vs/basic-languages/mips/mips.js +10 -0
- package/vendor/monaco/vs/basic-languages/msdax/msdax.js +10 -0
- package/vendor/monaco/vs/basic-languages/mysql/mysql.js +10 -0
- package/vendor/monaco/vs/basic-languages/objective-c/objective-c.js +10 -0
- package/vendor/monaco/vs/basic-languages/pascal/pascal.js +10 -0
- package/vendor/monaco/vs/basic-languages/pascaligo/pascaligo.js +10 -0
- package/vendor/monaco/vs/basic-languages/perl/perl.js +10 -0
- package/vendor/monaco/vs/basic-languages/pgsql/pgsql.js +10 -0
- package/vendor/monaco/vs/basic-languages/php/php.js +10 -0
- package/vendor/monaco/vs/basic-languages/pla/pla.js +10 -0
- package/vendor/monaco/vs/basic-languages/postiats/postiats.js +10 -0
- package/vendor/monaco/vs/basic-languages/powerquery/powerquery.js +10 -0
- package/vendor/monaco/vs/basic-languages/powershell/powershell.js +10 -0
- package/vendor/monaco/vs/basic-languages/protobuf/protobuf.js +11 -0
- package/vendor/monaco/vs/basic-languages/pug/pug.js +10 -0
- package/vendor/monaco/vs/basic-languages/python/python.js +10 -0
- package/vendor/monaco/vs/basic-languages/qsharp/qsharp.js +10 -0
- package/vendor/monaco/vs/basic-languages/r/r.js +10 -0
- package/vendor/monaco/vs/basic-languages/razor/razor.js +10 -0
- package/vendor/monaco/vs/basic-languages/redis/redis.js +10 -0
- package/vendor/monaco/vs/basic-languages/redshift/redshift.js +10 -0
- package/vendor/monaco/vs/basic-languages/restructuredtext/restructuredtext.js +10 -0
- package/vendor/monaco/vs/basic-languages/ruby/ruby.js +10 -0
- package/vendor/monaco/vs/basic-languages/rust/rust.js +10 -0
- package/vendor/monaco/vs/basic-languages/sb/sb.js +10 -0
- package/vendor/monaco/vs/basic-languages/scala/scala.js +10 -0
- package/vendor/monaco/vs/basic-languages/scheme/scheme.js +10 -0
- package/vendor/monaco/vs/basic-languages/scss/scss.js +12 -0
- package/vendor/monaco/vs/basic-languages/shell/shell.js +10 -0
- package/vendor/monaco/vs/basic-languages/solidity/solidity.js +10 -0
- package/vendor/monaco/vs/basic-languages/sophia/sophia.js +10 -0
- package/vendor/monaco/vs/basic-languages/sparql/sparql.js +10 -0
- package/vendor/monaco/vs/basic-languages/sql/sql.js +10 -0
- package/vendor/monaco/vs/basic-languages/st/st.js +10 -0
- package/vendor/monaco/vs/basic-languages/swift/swift.js +13 -0
- package/vendor/monaco/vs/basic-languages/systemverilog/systemverilog.js +10 -0
- package/vendor/monaco/vs/basic-languages/tcl/tcl.js +10 -0
- package/vendor/monaco/vs/basic-languages/twig/twig.js +10 -0
- package/vendor/monaco/vs/basic-languages/typescript/typescript.js +10 -0
- package/vendor/monaco/vs/basic-languages/typespec/typespec.js +10 -0
- package/vendor/monaco/vs/basic-languages/vb/vb.js +10 -0
- package/vendor/monaco/vs/basic-languages/wgsl/wgsl.js +307 -0
- package/vendor/monaco/vs/basic-languages/xml/xml.js +10 -0
- package/vendor/monaco/vs/basic-languages/yaml/yaml.js +10 -0
- package/vendor/monaco/vs/editor/editor.main.css +8 -0
- package/vendor/monaco/vs/editor/editor.main.js +798 -0
- package/vendor/monaco/vs/language/css/cssMode.js +13 -0
- package/vendor/monaco/vs/language/css/cssWorker.js +77 -0
- package/vendor/monaco/vs/language/html/htmlMode.js +13 -0
- package/vendor/monaco/vs/language/html/htmlWorker.js +454 -0
- package/vendor/monaco/vs/language/json/jsonMode.js +19 -0
- package/vendor/monaco/vs/language/json/jsonWorker.js +42 -0
- package/vendor/monaco/vs/language/typescript/tsMode.js +20 -0
- package/vendor/monaco/vs/language/typescript/tsWorker.js +51328 -0
- package/vendor/monaco/vs/loader.js +11 -0
- package/vendor/monaco/vs/nls.messages.de.js +21 -0
- package/vendor/monaco/vs/nls.messages.es.js +21 -0
- package/vendor/monaco/vs/nls.messages.fr.js +19 -0
- package/vendor/monaco/vs/nls.messages.it.js +19 -0
- package/vendor/monaco/vs/nls.messages.ja.js +21 -0
- package/vendor/monaco/vs/nls.messages.ko.js +19 -0
- package/vendor/monaco/vs/nls.messages.ru.js +21 -0
- package/vendor/monaco/vs/nls.messages.zh-cn.js +21 -0
- package/vendor/monaco/vs/nls.messages.zh-tw.js +19 -0
- package/vendor/xterm/LICENSE +21 -0
- package/vendor/xterm/LICENSE.addon-fit +19 -0
- package/vendor/xterm/addon-fit.js +2 -0
- package/vendor/xterm/xterm.css +285 -0
- package/vendor/xterm/xterm.js +2 -0
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 55-backdrop:把「梁圣」皮肤(@leon___/dsh-client-liang-intensity-skin)的背景图渐变
|
|
3
|
+
* 复刻到古法编程覆盖层里。
|
|
4
|
+
*
|
|
5
|
+
* 皮肤的做法(见其 client.js):
|
|
6
|
+
* - 在中心列挂一个 `.liang-skin-backdrop`,背景 = `var(--liang-page)`;
|
|
7
|
+
* - 内部放一个 `<img class="liang-skin-sequence-frame">`,src 指向皮肤 assets 里的人像
|
|
8
|
+
* (按强度档位切换 stage-XX.webp),右侧 `min(42vw,720px)`、`object-fit:cover`,
|
|
9
|
+
* 并用 `mask-image: linear-gradient(90deg, transparent → black)` 做「左透明→右实」的渐变;
|
|
10
|
+
* - 透明度由 `--liang-portrait-opacity` 控制,可选 `data-mask="on"` 时再叠一层渐变遮罩。
|
|
11
|
+
*
|
|
12
|
+
* 古法编程是 `shell.overlay` 覆盖层,会盖住皮肤背景。本模块读取皮肤当前的背景节点/人像/透明度/
|
|
13
|
+
* 遮罩状态,在覆盖层里用同样的渐变规则重建一份背景,让 IDE 也能看到同一张背景图与渐变,
|
|
14
|
+
* 并通过标题栏的「背景」按钮随时开关(偏好持久化)。
|
|
15
|
+
*
|
|
16
|
+
* 皮肤关闭 / 找不到背景节点时不渲染(IDE 保持纯净实色)。
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** 背景开关偏好('0' = 关闭)。 */
|
|
20
|
+
const GUFA_BACKDROP_PREF_KEY = 'gufa-code:backdrop:v1'
|
|
21
|
+
|
|
22
|
+
G.backdropPref = {
|
|
23
|
+
get() {
|
|
24
|
+
try {
|
|
25
|
+
return localStorage.getItem(GUFA_BACKDROP_PREF_KEY) !== '0'
|
|
26
|
+
} catch {
|
|
27
|
+
return true
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
set(value) {
|
|
31
|
+
try {
|
|
32
|
+
localStorage.setItem(GUFA_BACKDROP_PREF_KEY, value === true ? '1' : '0')
|
|
33
|
+
} catch {
|
|
34
|
+
/* 隐私模式忽略 */
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 背景浓度偏好:null = 跟随皮肤自身的透明度;数字 = 0–100 的独立无级浓度。 */
|
|
40
|
+
const GUFA_BACKDROP_STRENGTH_KEY = 'gufa-code:backdrop-strength:v1'
|
|
41
|
+
|
|
42
|
+
G.backdropStrengthPref = {
|
|
43
|
+
get() {
|
|
44
|
+
try {
|
|
45
|
+
const raw = localStorage.getItem(GUFA_BACKDROP_STRENGTH_KEY)
|
|
46
|
+
if (raw === null || raw === '') return null
|
|
47
|
+
const value = Number.parseFloat(raw)
|
|
48
|
+
return Number.isFinite(value) ? Math.max(0, Math.min(100, value)) : null
|
|
49
|
+
} catch {
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
set(value) {
|
|
54
|
+
try {
|
|
55
|
+
if (value === null || value === undefined) localStorage.removeItem(GUFA_BACKDROP_STRENGTH_KEY)
|
|
56
|
+
else localStorage.setItem(GUFA_BACKDROP_STRENGTH_KEY, String(Math.max(0, Math.min(100, Number(value)))))
|
|
57
|
+
} catch {
|
|
58
|
+
/* 隐私模式忽略 */
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 滑动变祖皮肤的偏好存储(皮肤源码里的常量),用于开启「31 档细分」。 */
|
|
64
|
+
const SKIN_SETTINGS_KEY = 'dsh-liang-intensity-skin.settings.v1'
|
|
65
|
+
const SKIN_SETTINGS_MARKER = 'gufa-code:skin-fine-stops:v1'
|
|
66
|
+
const SKIN_DEFAULT_VALUES = Object.freeze({
|
|
67
|
+
bindEffort: true,
|
|
68
|
+
maskEnabled: true,
|
|
69
|
+
xianxiaPortraits: false,
|
|
70
|
+
particlesEnabled: false,
|
|
71
|
+
motionEnabled: false,
|
|
72
|
+
fineStopsEnabled: false,
|
|
73
|
+
hideSlider: false,
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 开启滑动变祖皮肤的「31 档细分」(fineStopsEnabled)。
|
|
78
|
+
*
|
|
79
|
+
* 皮肤默认行为:绑定思考等级时,松手会把**外观滑块**吸附到模型真实推理档位
|
|
80
|
+
* (5 档就是 0/7.5/15/22.5/30),所以进度条和背景会「弹」到档位上。
|
|
81
|
+
* 打开 31 档细分后:外观可停在 0–30 的任意整数级(松手只吸附到最近整数级),
|
|
82
|
+
* 进度条与背景停在松手位置,模型仍提交最近的真实推理档位 —— 正是想要的行为。
|
|
83
|
+
*
|
|
84
|
+
* 只干预一次(写 marker,之后用户在皮肤设置里再关掉不会被反复打开);
|
|
85
|
+
* 写入后派发 StorageEvent,让皮肤的内存 store 立即生效,无需刷新。
|
|
86
|
+
* @returns {{applied:boolean, reason?:string}}
|
|
87
|
+
*/
|
|
88
|
+
G.ensureSkinFineStops = function ensureSkinFineStops() {
|
|
89
|
+
try {
|
|
90
|
+
if (localStorage.getItem(SKIN_SETTINGS_MARKER) === '1') return { applied: false, reason: 'marked' }
|
|
91
|
+
let record = null
|
|
92
|
+
try {
|
|
93
|
+
const raw = localStorage.getItem(SKIN_SETTINGS_KEY)
|
|
94
|
+
record = raw === null ? null : JSON.parse(raw)
|
|
95
|
+
} catch {
|
|
96
|
+
record = null
|
|
97
|
+
}
|
|
98
|
+
const versioned = record !== null && typeof record === 'object' && record.version === 1 && record.values !== null && typeof record.values === 'object'
|
|
99
|
+
const legacyBind = localStorage.getItem('dsh-liang-intensity-skin.bind-effort') !== '0'
|
|
100
|
+
const legacyMask = localStorage.getItem('dsh-liang-intensity-skin.mask') !== '0'
|
|
101
|
+
const values = versioned
|
|
102
|
+
? { ...SKIN_DEFAULT_VALUES, ...record.values }
|
|
103
|
+
: { ...SKIN_DEFAULT_VALUES, bindEffort: legacyBind, maskEnabled: legacyMask }
|
|
104
|
+
localStorage.setItem(SKIN_SETTINGS_MARKER, '1')
|
|
105
|
+
if (values.fineStopsEnabled === true) return { applied: false, reason: 'already-enabled' }
|
|
106
|
+
values.fineStopsEnabled = true
|
|
107
|
+
const payload = JSON.stringify({ version: 1, values })
|
|
108
|
+
const previous = localStorage.getItem(SKIN_SETTINGS_KEY)
|
|
109
|
+
localStorage.setItem(SKIN_SETTINGS_KEY, payload)
|
|
110
|
+
try {
|
|
111
|
+
if (typeof window !== 'undefined' && typeof window.dispatchEvent === 'function' && typeof StorageEvent === 'function') {
|
|
112
|
+
window.dispatchEvent(new StorageEvent('storage', {
|
|
113
|
+
key: SKIN_SETTINGS_KEY,
|
|
114
|
+
oldValue: previous,
|
|
115
|
+
newValue: payload,
|
|
116
|
+
storageArea: localStorage,
|
|
117
|
+
}))
|
|
118
|
+
}
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.error('[gufa-code] 通知皮肤设置失败(刷新后仍会生效)', error)
|
|
121
|
+
}
|
|
122
|
+
return { applied: true }
|
|
123
|
+
} catch (error) {
|
|
124
|
+
return { applied: false, reason: String(error) }
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** 读取当前「31 档细分」是否开启(诊断探针用)。 */
|
|
129
|
+
G.readSkinFineStops = function readSkinFineStops() {
|
|
130
|
+
try {
|
|
131
|
+
const raw = localStorage.getItem(SKIN_SETTINGS_KEY)
|
|
132
|
+
const record = raw === null ? null : JSON.parse(raw)
|
|
133
|
+
return record !== null && typeof record === 'object' && record.values !== null && typeof record.values === 'object' && record.values.fineStopsEnabled === true
|
|
134
|
+
} catch {
|
|
135
|
+
return false
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** 解析 transform 矩阵里的缩放(取 a 分量;非矩阵时返回 1)。 */
|
|
140
|
+
function gufaTransformScale(transform) {
|
|
141
|
+
if (typeof transform !== 'string' || transform === '' || transform === 'none') return 1
|
|
142
|
+
const matrix = /matrix\(([^)]+)\)/.exec(transform)
|
|
143
|
+
if (matrix === null) return 1
|
|
144
|
+
const parts = matrix[1].split(',').map((part) => Number.parseFloat(part))
|
|
145
|
+
return parts.length >= 4 && Number.isFinite(parts[0]) && parts[0] > 0 ? parts[0] : 1
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* 读取当前皮肤背景状态。纯 DOM 读取,不修改皮肤节点。
|
|
150
|
+
* @returns {{available:boolean, pageColor?:string, pageImage?:string, portraitSrc?:string, portraitOpacity?:number, portraitScale?:number, mask?:boolean, media?:string}}
|
|
151
|
+
*/
|
|
152
|
+
G.readSkinBackdropMeta = function readSkinBackdropMeta() {
|
|
153
|
+
if (typeof document === 'undefined' || typeof window === 'undefined') return { available: false }
|
|
154
|
+
const body = document.body
|
|
155
|
+
if (body === null || body === undefined || typeof body.hasAttribute !== 'function') return { available: false }
|
|
156
|
+
if (!body.hasAttribute('data-liang-skin')) return { available: false }
|
|
157
|
+
if (typeof document.querySelector !== 'function') return { available: false }
|
|
158
|
+
const node = document.querySelector('.liang-skin-backdrop')
|
|
159
|
+
if (node === null || node === undefined) return { available: false }
|
|
160
|
+
let style
|
|
161
|
+
try {
|
|
162
|
+
style = window.getComputedStyle(node)
|
|
163
|
+
} catch {
|
|
164
|
+
return { available: false }
|
|
165
|
+
}
|
|
166
|
+
const pageColor = style.backgroundColor !== '' && style.backgroundColor !== 'rgba(0, 0, 0, 0)' ? style.backgroundColor : ''
|
|
167
|
+
const pageImage = style.backgroundImage !== 'none' && style.backgroundImage !== '' ? style.backgroundImage : ''
|
|
168
|
+
const img = typeof node.querySelector === 'function' ? node.querySelector('img') : null
|
|
169
|
+
let portraitSrc = ''
|
|
170
|
+
let portraitOpacity = 1
|
|
171
|
+
let portraitScale = 1
|
|
172
|
+
if (img !== null && img !== undefined) {
|
|
173
|
+
portraitSrc = img.currentSrc || img.src || ''
|
|
174
|
+
try {
|
|
175
|
+
const imgStyle = window.getComputedStyle(img)
|
|
176
|
+
if (imgStyle.display === 'none') portraitSrc = ''
|
|
177
|
+
const opacity = Number.parseFloat(imgStyle.opacity)
|
|
178
|
+
if (Number.isFinite(opacity)) portraitOpacity = Math.max(0, Math.min(1, opacity))
|
|
179
|
+
portraitScale = Math.max(0.1, Math.min(3, gufaTransformScale(imgStyle.transform)))
|
|
180
|
+
} catch {
|
|
181
|
+
/* 忽略样式读取失败 */
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
available: true,
|
|
186
|
+
pageColor,
|
|
187
|
+
pageImage,
|
|
188
|
+
portraitSrc,
|
|
189
|
+
portraitOpacity,
|
|
190
|
+
portraitScale,
|
|
191
|
+
mask: typeof node.getAttribute === 'function' && node.getAttribute('data-mask') === 'on',
|
|
192
|
+
media: (typeof node.getAttribute === 'function' && node.getAttribute('data-media')) || 'color',
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* 订阅皮肤背景状态:皮肤强度变化会换人像、改透明度,这里每 1.5s 轮询一次并在变化时重渲染。
|
|
198
|
+
* @returns {ReturnType<typeof G.readSkinBackdropMeta>}
|
|
199
|
+
*/
|
|
200
|
+
G.useSkinBackdrop = function useSkinBackdrop() {
|
|
201
|
+
const [meta, setMeta] = useState(() => G.readSkinBackdropMeta())
|
|
202
|
+
useEffect(() => {
|
|
203
|
+
let disposed = false
|
|
204
|
+
const sync = () => {
|
|
205
|
+
if (disposed) return
|
|
206
|
+
const next = G.readSkinBackdropMeta()
|
|
207
|
+
setMeta((previous) => (JSON.stringify(previous) === JSON.stringify(next) ? previous : next))
|
|
208
|
+
}
|
|
209
|
+
sync()
|
|
210
|
+
const timer = window.setInterval(sync, 350)
|
|
211
|
+
window.addEventListener('resize', sync)
|
|
212
|
+
return () => {
|
|
213
|
+
disposed = true
|
|
214
|
+
window.clearInterval(timer)
|
|
215
|
+
window.removeEventListener('resize', sync)
|
|
216
|
+
}
|
|
217
|
+
}, [])
|
|
218
|
+
return meta
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* 背景层:页面底色 + 右侧人像(带左→右渐变遮罩)+ 可选遮罩。
|
|
223
|
+
* 同时注册一个空 overlay 探针(`gufa-code-backdrop-live-<state>`),便于 Client Inspect
|
|
224
|
+
* 验证「背景层已挂载 / 人像已加载」,无需截图。
|
|
225
|
+
*/
|
|
226
|
+
G.SkinBackdrop = function SkinBackdrop(props) {
|
|
227
|
+
const meta = props.meta
|
|
228
|
+
const [imgState, setImgState] = useState('pending')
|
|
229
|
+
const [layers, setLayers] = useState(() => ({
|
|
230
|
+
current: meta !== null && meta !== undefined && meta.available === true && typeof meta.portraitSrc === 'string' ? meta.portraitSrc : '',
|
|
231
|
+
previous: '',
|
|
232
|
+
}))
|
|
233
|
+
const enabled = props.enabled !== false
|
|
234
|
+
const active = meta !== null && meta !== undefined && meta.available === true && enabled === true
|
|
235
|
+
const portraitSrc = active && typeof meta.portraitSrc === 'string' ? meta.portraitSrc : ''
|
|
236
|
+
|
|
237
|
+
// 人像源变化(皮肤换档)时保留旧图层做交叉淡出,避免硬切
|
|
238
|
+
useEffect(() => {
|
|
239
|
+
setLayers((previous) => (previous.current === portraitSrc ? previous : { current: portraitSrc, previous: previous.current }))
|
|
240
|
+
}, [portraitSrc])
|
|
241
|
+
useEffect(() => {
|
|
242
|
+
if (layers.previous === '') return undefined
|
|
243
|
+
const timer = window.setTimeout(() => setLayers((previous) => (previous.previous === '' ? previous : { ...previous, previous: '' })), 520)
|
|
244
|
+
return () => window.clearTimeout(timer)
|
|
245
|
+
}, [layers.previous])
|
|
246
|
+
useEffect(() => {
|
|
247
|
+
if (!active) return undefined
|
|
248
|
+
if (G.ctx === null || G.ctx === undefined || typeof G.ctx.inject !== 'function') return undefined
|
|
249
|
+
const state = imgState === 'ok' ? 'img-ok' : imgState === 'fail' ? 'img-fail' : meta.portraitSrc ? 'img-loading' : 'color-only'
|
|
250
|
+
let dispose = () => {}
|
|
251
|
+
let disposeScope
|
|
252
|
+
try {
|
|
253
|
+
disposeScope = G.ctx.inject(['slots'], (scope) => {
|
|
254
|
+
try {
|
|
255
|
+
const removal = scope.slots.register({ name: 'shell.overlay', id: `gufa-code-backdrop-live-${state}`, order: 996 }, () => null)
|
|
256
|
+
dispose = typeof removal === 'function' ? removal : () => {}
|
|
257
|
+
} catch (error) {
|
|
258
|
+
console.error('[gufa-code] 注册背景探针失败', error)
|
|
259
|
+
}
|
|
260
|
+
})
|
|
261
|
+
} catch (error) {
|
|
262
|
+
console.error('[gufa-code] 注入背景探针失败', error)
|
|
263
|
+
return undefined
|
|
264
|
+
}
|
|
265
|
+
return () => {
|
|
266
|
+
try {
|
|
267
|
+
dispose()
|
|
268
|
+
} catch {
|
|
269
|
+
/* ignore */
|
|
270
|
+
}
|
|
271
|
+
try {
|
|
272
|
+
disposeScope?.()
|
|
273
|
+
} catch {
|
|
274
|
+
/* ignore */
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}, [active, imgState, meta])
|
|
278
|
+
if (!active) return null
|
|
279
|
+
const pageStyle = {}
|
|
280
|
+
if (typeof meta.pageColor === 'string' && meta.pageColor !== '') pageStyle.backgroundColor = meta.pageColor
|
|
281
|
+
if (typeof meta.pageImage === 'string' && meta.pageImage !== '') pageStyle.backgroundImage = meta.pageImage
|
|
282
|
+
pageStyle.backgroundSize = 'cover'
|
|
283
|
+
pageStyle.backgroundPosition = 'center'
|
|
284
|
+
// 浓度:用户拖动过滑杆就用无级值,否则跟随皮肤自身透明度
|
|
285
|
+
const strength = typeof props.strength === 'number' && Number.isFinite(props.strength)
|
|
286
|
+
? Math.max(0, Math.min(1, props.strength / 100))
|
|
287
|
+
: Math.max(0, Math.min(1, meta.portraitOpacity))
|
|
288
|
+
const portraitStyle = {
|
|
289
|
+
opacity: strength,
|
|
290
|
+
transform: `translateY(-50%) scale(${meta.portraitScale})`,
|
|
291
|
+
}
|
|
292
|
+
const leavingStyle = {
|
|
293
|
+
opacity: 0,
|
|
294
|
+
transform: `translateY(-50%) scale(${meta.portraitScale})`,
|
|
295
|
+
}
|
|
296
|
+
return h(
|
|
297
|
+
'div',
|
|
298
|
+
{ className: 'gufa-backdrop', 'aria-hidden': 'true' },
|
|
299
|
+
h('div', { className: 'gufa-backdrop-page', style: pageStyle }),
|
|
300
|
+
h('div', { className: 'gufa-backdrop-glow', 'aria-hidden': 'true' }),
|
|
301
|
+
layers.previous !== ''
|
|
302
|
+
? h('img', { key: `prev:${layers.previous}`, className: 'gufa-backdrop-portrait', src: layers.previous, alt: '', draggable: false, style: leavingStyle, 'data-leaving': '' })
|
|
303
|
+
: null,
|
|
304
|
+
layers.current !== ''
|
|
305
|
+
? h('img', {
|
|
306
|
+
key: `cur:${layers.current}`,
|
|
307
|
+
className: 'gufa-backdrop-portrait',
|
|
308
|
+
src: layers.current,
|
|
309
|
+
alt: '',
|
|
310
|
+
draggable: false,
|
|
311
|
+
style: portraitStyle,
|
|
312
|
+
onLoad: () => setImgState('ok'),
|
|
313
|
+
onError: () => setImgState('fail'),
|
|
314
|
+
})
|
|
315
|
+
: null,
|
|
316
|
+
h('div', { className: 'gufa-backdrop-fade' }),
|
|
317
|
+
meta.mask === true ? h('div', { className: 'gufa-backdrop-scrim' }) : null,
|
|
318
|
+
h(G.BackdropParticles, { strength }),
|
|
319
|
+
)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* 背景粒子层:按「滑动变祖」原版粒子参数复刻(小颗粒、数量多、加色发光、从右下方涌向人像)。
|
|
324
|
+
* 原版用 WebGL 点精灵 + 加色混合;这里用 2D canvas + 预渲染光点 sprite + 'lighter' 混合近似,
|
|
325
|
+
* 性能足够(每帧 drawImage,30fps)。
|
|
326
|
+
*/
|
|
327
|
+
G.BackdropParticles = function BackdropParticles(props) {
|
|
328
|
+
const canvasRef = useRef(null)
|
|
329
|
+
const strengthRaw = typeof props.strength === 'number' && Number.isFinite(props.strength) ? props.strength : 1
|
|
330
|
+
const strength = Math.max(0, Math.min(1, strengthRaw))
|
|
331
|
+
useEffect(() => {
|
|
332
|
+
const canvas = canvasRef.current
|
|
333
|
+
if (canvas === null || canvas === undefined) return undefined
|
|
334
|
+
const context = canvas.getContext('2d')
|
|
335
|
+
if (context === null || context === undefined) return undefined
|
|
336
|
+
let reduced = false
|
|
337
|
+
try {
|
|
338
|
+
reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
339
|
+
} catch {
|
|
340
|
+
reduced = false
|
|
341
|
+
}
|
|
342
|
+
if (reduced) return undefined
|
|
343
|
+
|
|
344
|
+
const dpr = Math.min(1.5, window.devicePixelRatio || 1)
|
|
345
|
+
let width = 0
|
|
346
|
+
let height = 0
|
|
347
|
+
const resize = () => {
|
|
348
|
+
width = Math.max(1, Math.floor(canvas.clientWidth * dpr))
|
|
349
|
+
height = Math.max(1, Math.floor(canvas.clientHeight * dpr))
|
|
350
|
+
canvas.width = width
|
|
351
|
+
canvas.height = height
|
|
352
|
+
sprite = makeSprite()
|
|
353
|
+
for (const particle of particles) spawn(particle, true)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/* 预渲染光点 sprite(暖金色核心 + 外圈微光) */
|
|
357
|
+
let sprite = null
|
|
358
|
+
const makeSprite = () => {
|
|
359
|
+
const size = 64
|
|
360
|
+
const off = document.createElement('canvas')
|
|
361
|
+
off.width = size
|
|
362
|
+
off.height = size
|
|
363
|
+
const ctx = off.getContext('2d')
|
|
364
|
+
if (ctx === null) return null
|
|
365
|
+
const gradient = ctx.createRadialGradient(size / 2, size / 2, 0, size / 2, size / 2, size / 2)
|
|
366
|
+
gradient.addColorStop(0, 'rgba(255,241,214,1)')
|
|
367
|
+
gradient.addColorStop(0.25, 'rgba(240,196,120,0.85)')
|
|
368
|
+
gradient.addColorStop(0.55, 'rgba(210,150,80,0.35)')
|
|
369
|
+
gradient.addColorStop(1, 'rgba(150,90,50,0)')
|
|
370
|
+
ctx.fillStyle = gradient
|
|
371
|
+
ctx.fillRect(0, 0, size, size)
|
|
372
|
+
return off
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const TAU = Math.PI * 2
|
|
376
|
+
/* 与皮肤原版一致的群体参数:数量随「背景浓度」放大,尺寸 2~5.2 CSS px,寿命 3~7s */
|
|
377
|
+
const activeCount = Math.round(300 + strength * 720)
|
|
378
|
+
const anchorX = 0.58
|
|
379
|
+
const anchorY = 0.92
|
|
380
|
+
const targetX = 0.5
|
|
381
|
+
const targetY = 0.4
|
|
382
|
+
const particles = Array.from({ length: activeCount }, () => ({ x: 0, y: 0, vx: 0, vy: 0, size: 0, life: 0, maxLife: 1, phase: 0 }))
|
|
383
|
+
const clamp01 = (value) => Math.max(0, Math.min(1, value))
|
|
384
|
+
const spawn = (particle, randomLife) => {
|
|
385
|
+
const spread = width * 0.05
|
|
386
|
+
particle.x = anchorX * width + (Math.random() - 0.5) * spread * 2
|
|
387
|
+
particle.y = anchorY * height + (Math.random() - 0.5) * spread * 1.4
|
|
388
|
+
particle.vx = (Math.random() - 0.5) * 90 * dpr
|
|
389
|
+
particle.vy = (-30 - Math.random() * 110) * dpr
|
|
390
|
+
particle.maxLife = 3 + Math.random() * 4
|
|
391
|
+
particle.life = randomLife ? Math.random() * particle.maxLife : 0
|
|
392
|
+
particle.size = (2 + Math.random() * 3.2) * dpr
|
|
393
|
+
particle.phase = Math.random() * TAU
|
|
394
|
+
}
|
|
395
|
+
for (const particle of particles) spawn(particle, true)
|
|
396
|
+
|
|
397
|
+
const pointer = { x: -1e4, y: -1e4, down: false }
|
|
398
|
+
let last = 0
|
|
399
|
+
let raf = 0
|
|
400
|
+
const tick = (now) => {
|
|
401
|
+
raf = window.requestAnimationFrame(tick)
|
|
402
|
+
if (now - last < 33) return
|
|
403
|
+
const dt = Math.min(0.048, (last === 0 ? 0.016 : (now - last) / 1000))
|
|
404
|
+
last = now
|
|
405
|
+
context.clearRect(0, 0, width, height)
|
|
406
|
+
context.globalCompositeOperation = 'lighter'
|
|
407
|
+
const levelFactor = 0.45 + 0.55 * strength
|
|
408
|
+
for (const particle of particles) {
|
|
409
|
+
particle.life += dt
|
|
410
|
+
if (particle.life > particle.maxLife) spawn(particle, false)
|
|
411
|
+
const curl = Math.sin(particle.y * 0.0016 + now * 0.00025 + particle.phase)
|
|
412
|
+
particle.vx += curl * width * 0.02 * dt + (Math.random() - 0.5) * width * 0.05 * dt
|
|
413
|
+
particle.vy -= height * 0.008 * dt
|
|
414
|
+
particle.vy += (Math.random() - 0.5) * height * 0.03 * dt
|
|
415
|
+
const dxp = pointer.x - particle.x
|
|
416
|
+
const dyp = pointer.y - particle.y
|
|
417
|
+
const distance = Math.hypot(dxp, dyp)
|
|
418
|
+
if (pointer.x > -1e3 && distance < width * 0.34 && distance > 0.001) {
|
|
419
|
+
const falloff = 1 - distance / (width * 0.34)
|
|
420
|
+
const sign = pointer.down === true ? -1 : 1
|
|
421
|
+
particle.vx -= (dxp / distance) * falloff * 900 * dt * sign
|
|
422
|
+
particle.vy -= (dyp / distance) * falloff * 900 * dt * sign
|
|
423
|
+
}
|
|
424
|
+
const toX = targetX * width - particle.x
|
|
425
|
+
const toY = targetY * height - particle.y
|
|
426
|
+
const toDistance = Math.hypot(toX, toY) || 1
|
|
427
|
+
const pull = 60 * dt * levelFactor * dpr
|
|
428
|
+
particle.vx += (toX / toDistance) * pull
|
|
429
|
+
particle.vy += (toY / toDistance) * pull
|
|
430
|
+
const damping = Math.max(0, 1 - 0.8 * dt)
|
|
431
|
+
particle.vx *= damping
|
|
432
|
+
particle.vy *= damping
|
|
433
|
+
particle.x += particle.vx * dt
|
|
434
|
+
particle.y += particle.vy * dt
|
|
435
|
+
if (particle.y < -height * 0.15 || particle.x < -width * 0.1 || particle.x > width * 1.1) spawn(particle, false)
|
|
436
|
+
const lifeRatio = particle.life / particle.maxLife
|
|
437
|
+
const fade = Math.min(1, lifeRatio / 0.18) * Math.pow(Math.sin(Math.PI * clamp01(lifeRatio)), 0.7)
|
|
438
|
+
const twinkle = 0.55 + 0.45 * Math.sin(particle.phase + now * 0.0016)
|
|
439
|
+
const alpha = Math.max(0, Math.min(1, (0.3 + 0.42 * levelFactor) * twinkle * fade))
|
|
440
|
+
if (alpha <= 0.01 || sprite === null) continue
|
|
441
|
+
const size = particle.size * (0.8 + 0.4 * levelFactor)
|
|
442
|
+
context.globalAlpha = alpha * 0.9
|
|
443
|
+
context.drawImage(sprite, particle.x - size * 1.6, particle.y - size * 1.6, size * 3.2, size * 3.2)
|
|
444
|
+
}
|
|
445
|
+
context.globalAlpha = 1
|
|
446
|
+
context.globalCompositeOperation = 'source-over'
|
|
447
|
+
}
|
|
448
|
+
const onPointerMove = (event) => {
|
|
449
|
+
const rect = canvas.getBoundingClientRect()
|
|
450
|
+
pointer.x = (event.clientX - rect.left) * dpr
|
|
451
|
+
pointer.y = (event.clientY - rect.top) * dpr
|
|
452
|
+
}
|
|
453
|
+
const onPointerLeave = () => {
|
|
454
|
+
pointer.x = -1e4
|
|
455
|
+
pointer.y = -1e4
|
|
456
|
+
}
|
|
457
|
+
const onPointerDown = () => { pointer.down = true }
|
|
458
|
+
const onPointerUp = () => { pointer.down = false }
|
|
459
|
+
window.addEventListener('pointermove', onPointerMove)
|
|
460
|
+
window.addEventListener('pointerdown', onPointerDown)
|
|
461
|
+
window.addEventListener('pointerup', onPointerUp)
|
|
462
|
+
window.addEventListener('pointerleave', onPointerLeave)
|
|
463
|
+
window.addEventListener('resize', resize)
|
|
464
|
+
resize()
|
|
465
|
+
raf = window.requestAnimationFrame(tick)
|
|
466
|
+
try {
|
|
467
|
+
void G.api('dev.log', { message: `particles-start count=${activeCount} size=2-5.2css dpr=${dpr} canvas=${width}x${height}` }).catch(() => {})
|
|
468
|
+
} catch {
|
|
469
|
+
/* ignore */
|
|
470
|
+
}
|
|
471
|
+
return () => {
|
|
472
|
+
if (raf !== 0) window.cancelAnimationFrame(raf)
|
|
473
|
+
window.removeEventListener('pointermove', onPointerMove)
|
|
474
|
+
window.removeEventListener('pointerdown', onPointerDown)
|
|
475
|
+
window.removeEventListener('pointerup', onPointerUp)
|
|
476
|
+
window.removeEventListener('pointerleave', onPointerLeave)
|
|
477
|
+
window.removeEventListener('resize', resize)
|
|
478
|
+
}
|
|
479
|
+
}, [strength])
|
|
480
|
+
return h('canvas', { ref: canvasRef, className: 'gufa-backdrop-particles', 'aria-hidden': 'true', style: { mixBlendMode: 'screen' } })
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/** 背景浓度滑杆(无级、连续;双击恢复「跟随皮肤」)。 */
|
|
484
|
+
G.BackdropStrengthSlider = function BackdropStrengthSlider(props) {
|
|
485
|
+
const t = G.useT()
|
|
486
|
+
const raw = typeof props.value === 'number' && Number.isFinite(props.value) ? props.value : 100
|
|
487
|
+
const value = Math.max(0, Math.min(100, Math.round(raw)))
|
|
488
|
+
return h(
|
|
489
|
+
'label',
|
|
490
|
+
{ className: 'gufa-strength', title: `${t('app.backdropStrength')} — ${t('app.backdropStrengthHint')}` },
|
|
491
|
+
h(G.Icon, { name: 'image', size: 12 }),
|
|
492
|
+
h('input', {
|
|
493
|
+
className: 'gufa-strength-range',
|
|
494
|
+
type: 'range',
|
|
495
|
+
min: 0,
|
|
496
|
+
max: 100,
|
|
497
|
+
step: 1,
|
|
498
|
+
value,
|
|
499
|
+
'aria-label': t('app.backdropStrength'),
|
|
500
|
+
onChange: (event) => props.onChange?.(Number(event.target.value)),
|
|
501
|
+
onDoubleClick: () => props.onReset?.(),
|
|
502
|
+
}),
|
|
503
|
+
h('span', { className: 'gufa-strength-value' }, `${value}%`),
|
|
504
|
+
)
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/** 纯函数暴露(测试/控制台排查)。 */
|
|
508
|
+
G.__backdrop = { read: G.readSkinBackdropMeta }
|