dsh-folia 2.9.2 → 3.0.2
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/lib/client.js +18 -4
- package/lib/routes/skins.js +160 -4
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -16256,7 +16256,13 @@ window.__ModuleLoader__.load({
|
|
|
16256
16256
|
const viaHost = await apiFetch("/skins/scoped-css?id=" + enc, { parse: false, cache: "no-store" });
|
|
16257
16257
|
if (viaHost && viaHost.ok && viaHost.response) {
|
|
16258
16258
|
const text = await viaHost.response.text();
|
|
16259
|
-
if (text && text.trim())
|
|
16259
|
+
if (text && text.trim()) {
|
|
16260
|
+
// 响应头说明这份是谁作用域化的:
|
|
16261
|
+
// skin-center = 拿到了皮肤中心加工的那份;local = 宿主自己做的(等价结果)
|
|
16262
|
+
let via = "local";
|
|
16263
|
+
try { via = viaHost.response.headers.get("x-folia-scoped") || "local"; } catch { /* 读不到按 local */ }
|
|
16264
|
+
return { css: text, source: via === "skin-center" ? "skin-center" : "local-scoped" };
|
|
16265
|
+
}
|
|
16260
16266
|
}
|
|
16261
16267
|
} catch { /* 代理不可用 ⇒ 继续走下面两条 */ }
|
|
16262
16268
|
|
|
@@ -16691,11 +16697,13 @@ window.__ModuleLoader__.load({
|
|
|
16691
16697
|
? weT("已切换为「") + label + weT("」,但样式注入失败:") + applyErr
|
|
16692
16698
|
: weT("已切换到「") + label + weT("」—— 样式 ")
|
|
16693
16699
|
+ Math.round(info.bytes / 1024) + " KB / " + info.rules + weT(" 条规则")
|
|
16694
|
-
+ (info.source === "skin-center" ? weT("(已作用域化)") : weT("(未作用域化,可能显示不全)"))
|
|
16700
|
+
+ ((info.source === "skin-center" || info.source === "local-scoped") ? weT("(已作用域化)") : weT("(未作用域化,可能显示不全)"))
|
|
16695
16701
|
+ (bgKind === "none" ? weT(",无背景媒体") : weT(",背景:") + bgKind)
|
|
16696
16702
|
+ (hooksInfo.ran ? weT(",动效已启动") : (hooksInfo.reason === "no-hooks" ? weT(",无动效") : weT(",动效未启动(") + hooksInfo.reason + ")"))
|
|
16703
|
+
+ weT(",主题:") + (skinThemeNow() === "dark" ? weT("深色") : weT("浅色"))
|
|
16697
16704
|
+ (typeof onClearWallpaper === "function" ? weT(",壁纸已关闭") : ""),
|
|
16698
|
-
//
|
|
16705
|
+
// 主题与背景变体写进提示:皮肤有 light/dark 两套素材,不写清就会"图对不上"。
|
|
16706
|
+
// hooks 已经跑过了;reload 只是"整页重载"的可选入口(极少数皮肤依赖启动期布局)
|
|
16699
16707
|
reload: true,
|
|
16700
16708
|
});
|
|
16701
16709
|
// 不再整表重拉:本地标记已经对了,省掉那 600 ms 的"没反应"窗口。
|
|
@@ -16863,7 +16871,13 @@ window.__ModuleLoader__.load({
|
|
|
16863
16871
|
if (shown.length > 0) {
|
|
16864
16872
|
children.push(React.createElement("div", { key: "grid", style: SKINS_STYLE.grid },
|
|
16865
16873
|
shown.map((s) => {
|
|
16866
|
-
|
|
16874
|
+
// 预览图**跟随当前主题**:每套皮肤都有 light / dark 两份预览(例:居家模型娘的
|
|
16875
|
+
// 白天房间 vs 夜晚房间)。之前一律取 light,于是"深色模式下看到的效果图"与
|
|
16876
|
+
// "实际渲染的 dark 变体"完全是两张画 —— 用户自然会觉得对不上。
|
|
16877
|
+
const darkNow = skinThemeNow() === "dark";
|
|
16878
|
+
const previewRel = (darkNow
|
|
16879
|
+
? (s.previewDark || s.previewLight)
|
|
16880
|
+
: (s.previewLight || s.previewDark)) || "";
|
|
16867
16881
|
const previewSrc = previewRel
|
|
16868
16882
|
? apiUrl("/skins/preview?p=" + encodeURIComponent(previewRel))
|
|
16869
16883
|
: "";
|
package/lib/routes/skins.js
CHANGED
|
@@ -595,7 +595,16 @@ export function registerSkinRoutes(webServer, c) {
|
|
|
595
595
|
id,
|
|
596
596
|
name: typeof doc.name === 'string' ? doc.name : id,
|
|
597
597
|
backgroundMedia: media ? { light: shape(media.light), dark: shape(media.dark) } : null,
|
|
598
|
-
|
|
598
|
+
// ⚠️ hooks 的声明位置是 **`facets.client.entry`**,不是 `contributes.hooks`
|
|
599
|
+
//(契约见皮肤中心 contracts/hooks-api.d.ts:`facets.client.entry (hooks.mjs)`)。
|
|
600
|
+
// 之前读 contributes.hooks ⇒ 28 套带动效的皮肤全部"无动效",而那正是它们
|
|
601
|
+
// 看起来"显示不全"的部分(粒子/漂浮/交互)。这里两个位置都认,以 facets 为准。
|
|
602
|
+
hooks: (() => {
|
|
603
|
+
const facets = (doc && doc.facets) || null;
|
|
604
|
+
const client = facets && facets.client;
|
|
605
|
+
if (client && typeof client.entry === 'string' && client.entry) return client.entry;
|
|
606
|
+
return typeof c.hooks === 'string' ? c.hooks : '';
|
|
607
|
+
})(),
|
|
599
608
|
});
|
|
600
609
|
},
|
|
601
610
|
}));
|
|
@@ -680,6 +689,125 @@ export function registerSkinRoutes(webServer, c) {
|
|
|
680
689
|
},
|
|
681
690
|
}));
|
|
682
691
|
|
|
692
|
+
/* ── 自己实现「作用域化」,不再依赖皮肤中心的接口 ────────────────────────────
|
|
693
|
+
*
|
|
694
|
+
* 皮肤中心的 `/api/skin-center/v2/skins/<id>/stylesheet` 会走 `transformSkinCss()`:
|
|
695
|
+
* 把**每条选择器**包进 `html[data-dsh-skin="<id>"]`。这一步决定皮肤能不能压住应用
|
|
696
|
+
* 自身的面板/令牌规则 —— 作用域化之后特异性更高。
|
|
697
|
+
*
|
|
698
|
+
* 但那条路由在网关后面:浏览器直连 401,宿主代发(哪怕透传全部请求头)也可能 401。
|
|
699
|
+
* 而"让每个用户把自己的会话凭证交给插件"不是可交付的方案。所以这里**自己算**:
|
|
700
|
+
* 读皮肤目录里的 CSS,做同一件事,输出等价结果。
|
|
701
|
+
*
|
|
702
|
+
* 转换规则:
|
|
703
|
+
* · 普通规则:`a, b` → `html[data-dsh-skin="x"] a, html[data-dsh-skin="x"] b`
|
|
704
|
+
* · 以 `:root` 开头的:开头换成作用域本身
|
|
705
|
+
* · 以 `html` 开头的:原样保留(作者自己已作用域化,别套两层)
|
|
706
|
+
* · `@media/@supports/@layer/@container`:**递归进去**处理内部规则
|
|
707
|
+
* · `@keyframes/@font-face/@page/@property/@import`:原样保留
|
|
708
|
+
* (关键帧内部是 `0% {}` 这种选择器,套作用域会直接毁掉动画)
|
|
709
|
+
*/
|
|
710
|
+
|
|
711
|
+
/** 从 `openIdx`(指向 `{`)找配对的 `}`,跳过注释与字符串。找不到返回 -1。 */
|
|
712
|
+
function matchBrace(css, openIdx) {
|
|
713
|
+
let depth = 0;
|
|
714
|
+
for (let i = openIdx; i < css.length; i++) {
|
|
715
|
+
const ch = css[i];
|
|
716
|
+
if (ch === '/' && css[i + 1] === '*') {
|
|
717
|
+
const end = css.indexOf('*/', i + 2);
|
|
718
|
+
if (end < 0) return -1;
|
|
719
|
+
i = end + 1;
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
if (ch === '"' || ch === "'") {
|
|
723
|
+
const quote = ch;
|
|
724
|
+
for (i++; i < css.length; i++) {
|
|
725
|
+
if (css[i] === '\\') { i++; continue; }
|
|
726
|
+
if (css[i] === quote) break;
|
|
727
|
+
}
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
if (ch === '{') depth++;
|
|
731
|
+
else if (ch === '}') {
|
|
732
|
+
depth--;
|
|
733
|
+
if (depth === 0) return i;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
return -1;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/** 单个选择器 → 作用域化后的选择器。 */
|
|
740
|
+
function scopeSelector(sel, scope) {
|
|
741
|
+
const t = sel.trim();
|
|
742
|
+
if (!t) return t;
|
|
743
|
+
if (/^:root\b/.test(t)) return t.replace(/^:root/, scope);
|
|
744
|
+
if (/^html\b/i.test(t)) return t; // 作者已作用域化,别套两层
|
|
745
|
+
return scope + ' ' + t;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/** 选择器列表 → 逐项作用域化。**不切括号内的逗号**(:is()/:not()/[a="b,c"])。 */
|
|
749
|
+
function scopeSelectorList(list, scope) {
|
|
750
|
+
const out = [];
|
|
751
|
+
let depth = 0;
|
|
752
|
+
let cur = '';
|
|
753
|
+
for (const ch of list) {
|
|
754
|
+
if (ch === '(' || ch === '[') depth++;
|
|
755
|
+
else if (ch === ')' || ch === ']') depth = Math.max(0, depth - 1);
|
|
756
|
+
if (ch === ',' && depth === 0) { out.push(cur); cur = ''; continue; }
|
|
757
|
+
cur += ch;
|
|
758
|
+
}
|
|
759
|
+
out.push(cur);
|
|
760
|
+
return out.map((s) => scopeSelector(s, scope)).join(', ');
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
const GROUP_AT = /^@(media|supports|layer|container|scope)\b/i;
|
|
764
|
+
const KEEP_AT = /^@(keyframes|-webkit-keyframes|font-face|page|property|counter-style|import|charset|namespace)\b/i;
|
|
765
|
+
|
|
766
|
+
/** 把整段 CSS 作用域化。看不懂的结构就原样跳过该段(宁可少转,不可转坏)。 */
|
|
767
|
+
function scopeCss(css, id) {
|
|
768
|
+
const scope = `html[data-dsh-skin="${id}"]`;
|
|
769
|
+
let out = '';
|
|
770
|
+
let i = 0;
|
|
771
|
+
while (i < css.length) {
|
|
772
|
+
if (css[i] === '/' && css[i + 1] === '*') { // 注释原样保留
|
|
773
|
+
const end = css.indexOf('*/', i + 2);
|
|
774
|
+
if (end < 0) { out += css.slice(i); break; }
|
|
775
|
+
out += css.slice(i, end + 2);
|
|
776
|
+
i = end + 2;
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
if (/\s/.test(css[i])) { out += css[i++]; continue; }
|
|
780
|
+
|
|
781
|
+
if (css[i] === '@') {
|
|
782
|
+
const headEnd = css.indexOf('{', i);
|
|
783
|
+
const semi = css.indexOf(';', i);
|
|
784
|
+
if (semi >= 0 && (headEnd < 0 || semi < headEnd)) { // 无块 at-rule
|
|
785
|
+
out += css.slice(i, semi + 1);
|
|
786
|
+
i = semi + 1;
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
if (headEnd < 0) { out += css.slice(i); break; }
|
|
790
|
+
const head = css.slice(i, headEnd).trim();
|
|
791
|
+
const close = matchBrace(css, headEnd);
|
|
792
|
+
if (close < 0) { out += css.slice(i); break; }
|
|
793
|
+
const inner = css.slice(headEnd + 1, close);
|
|
794
|
+
// 分组 at-rule 递归进去;其余(关键帧/字体面/未知)原样保留
|
|
795
|
+
out += head + '{' + (GROUP_AT.test(head) ? scopeCss(inner, id) : inner) + '}';
|
|
796
|
+
i = close + 1;
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
const braceIdx = css.indexOf('{', i); // 普通规则
|
|
801
|
+
if (braceIdx < 0) { out += css.slice(i); break; }
|
|
802
|
+
const selector = css.slice(i, braceIdx);
|
|
803
|
+
const close = matchBrace(css, braceIdx);
|
|
804
|
+
if (close < 0) { out += css.slice(i); break; }
|
|
805
|
+
out += scopeSelectorList(selector, scope) + '{' + css.slice(braceIdx + 1, close) + '}';
|
|
806
|
+
i = close + 1;
|
|
807
|
+
}
|
|
808
|
+
return out;
|
|
809
|
+
}
|
|
810
|
+
|
|
683
811
|
/* ── 皮肤中心「加工后 CSS」的宿主端代理 ──────────────────────────────────────
|
|
684
812
|
* 皮肤中心的 `/skins/<id>/stylesheet|patches` 是 `transformSkinCss()` 的产物:
|
|
685
813
|
* **每条规则被强制包进 `html[data-dsh-skin="<id>"]`**,特异性高于应用自身规则 ——
|
|
@@ -733,13 +861,41 @@ export function registerSkinRoutes(webServer, c) {
|
|
|
733
861
|
if (!text || !text.trim()) continue;
|
|
734
862
|
res.setHeader('Content-Type', 'text/css; charset=utf-8');
|
|
735
863
|
res.setHeader('Cache-Control', 'no-store');
|
|
736
|
-
res.setHeader('X-Folia-Scoped', '
|
|
864
|
+
res.setHeader('X-Folia-Scoped', 'skin-center');
|
|
737
865
|
res.end(text);
|
|
738
866
|
return;
|
|
739
867
|
} catch { /* 这一段失败就试下一段 */ }
|
|
740
868
|
}
|
|
741
|
-
|
|
742
|
-
|
|
869
|
+
|
|
870
|
+
// 代理拿不到(网关拦截 / 没装皮肤中心)⇒ **自己作用域化**。
|
|
871
|
+
// 这条路不依赖任何凭证,也不依赖对方的进程,对所有用户一致成立。
|
|
872
|
+
const harness = harnessRoot();
|
|
873
|
+
if (!harness) { res.statusCode = 503; res.end(); return; }
|
|
874
|
+
const dir = join(harness, SKINS_DIR_NAME, id);
|
|
875
|
+
const sheet = skinStylesheetOf(dir);
|
|
876
|
+
if (!sheet) { res.statusCode = 404; res.end(); return; }
|
|
877
|
+
const raw = [];
|
|
878
|
+
try { raw.push(readFileSync(join(dir, ...sheet.rel.split('/')), 'utf8')); } catch { /* 主样式读不到就不能算成功 */ }
|
|
879
|
+
if (raw.length === 0) { res.statusCode = 404; res.end(); return; }
|
|
880
|
+
const patches = skinAuxFileOf(dir, 'patches');
|
|
881
|
+
if (patches) {
|
|
882
|
+
try { raw.push(readFileSync(join(dir, ...patches.split('/')), 'utf8')); } catch { /* 补丁缺失可接受 */ }
|
|
883
|
+
}
|
|
884
|
+
// url() 先改写成我们的素材路由(作用域化只动选择器,不动声明)。
|
|
885
|
+
const baseDir = sheet.rel.includes('/') ? sheet.rel.slice(0, sheet.rel.lastIndexOf('/')) : '';
|
|
886
|
+
const absUrl = (p) => {
|
|
887
|
+
if (/^(?:[a-z]+:|\/|#|data:)/i.test(p)) return p;
|
|
888
|
+
const rel = (baseDir ? baseDir + '/' : '') + p;
|
|
889
|
+
return `${base}/skins/asset?id=${encodeURIComponent(id)}&p=${encodeURIComponent(rel)}`;
|
|
890
|
+
};
|
|
891
|
+
const merged = raw.join('\n/* ---- patches ---- */\n')
|
|
892
|
+
.replace(/url\(\s*(['"]?)([^'")]+)\1\s*\)/gi, (m, q, p) => `url("${absUrl(p.trim())}")`);
|
|
893
|
+
const scoped = scopeCss(merged, id);
|
|
894
|
+
note('[skins] scoped locally: ' + id + ' (' + scoped.length + ' bytes)');
|
|
895
|
+
res.setHeader('Content-Type', 'text/css; charset=utf-8');
|
|
896
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
897
|
+
res.setHeader('X-Folia-Scoped', 'local');
|
|
898
|
+
res.end(scoped);
|
|
743
899
|
},
|
|
744
900
|
}));
|
|
745
901
|
|