fix: restore skin controls and desktop portrait compatibility

Respect host root layout, draw the scrim once, discover appearance controls without build hashes, and recover failed portraits independently. Add unit and real-browser regression coverage.

Refs #8, #12, #15, #10, #13. Native Desktop acceptance remains pending; no release.
This commit is contained in:
Leon
2026-10-16 15:02:25 +08:00
parent a25d4c8360
commit d0b7849c02
13 changed files with 1690 additions and 399 deletions

View File

@@ -29,7 +29,7 @@ var import_react = require("react");
// src/client/skin.css
var skin_default = `.liang-skin-backdrop {
position: fixed;
z-index: 0;
z-index: -1;
inset: 0;
overflow: hidden;
pointer-events: none;
@@ -48,7 +48,7 @@ body[data-liang-skin="on"] .liang-skin-backdrop {
right: 0;
display: block;
width: min(42vw, 720px);
height: 100vh;
height: 100%;
object-fit: cover;
object-position: 50% 42%;
opacity: var(--liang-portrait-opacity, 0.45);
@@ -75,11 +75,16 @@ body[data-liang-skin="on"] .liang-skin-backdrop {
position: absolute;
inset: 0;
content: "";
background: linear-gradient(90deg, var(--liang-page, transparent) 0 28%, transparent 57% 100%);
background:
linear-gradient(90deg, var(--liang-page, transparent) 0 28%, transparent 57% 100%),
linear-gradient(var(--liang-bg-base, transparent), var(--liang-bg-base, transparent));
}
body[data-liang-skin="on"] {
--dsw-alias-bg-base: var(--liang-bg-base) !important;
/* Keep the backdrop above the canvas but below content and native portals.
The host owns root positioning, dimensions, and its own stacking order. */
isolation: isolate;
--dsw-alias-bg-base: transparent !important;
--dsw-alias-bg-layer-1: var(--liang-layer-1) !important;
--dsw-alias-bg-layer-2: var(--liang-layer-2) !important;
--dsw-alias-bg-layer-3: var(--liang-layer-3) !important;
@@ -239,11 +244,16 @@ body[data-liang-skin="on"]:is(
}
body[data-liang-skin="on"] > #root {
position: relative;
z-index: 1;
background: transparent !important;
}
body[data-liang-skin="on"]:is(
[data-dsh-desktop-mode="compatibility"],
[data-dsh-desktop-mode="extended"]
) .liang-skin-backdrop {
top: var(--dsh-desktop-frame-height, 0px);
}
/* Windows paints the sidebar fill across the host frame, covering the portrait.
Match macOS's transparent frame while keeping the sidebar, caption, and
conversation surfaces painted by their own host rules. */
@@ -480,7 +490,7 @@ body[data-liang-skin="on"] .liang-effort-control__range::-webkit-slider-thumb {
display: flex;
}
[class*="_8HJdBW_cubeRow"]:has(.liang-appearance-choice) {
[class*="_cubeRow"]:has(.liang-appearance-choice) {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
}
@@ -626,6 +636,324 @@ body[data-liang-skin="on"] .liang-effort-control__range::-webkit-slider-thumb {
}
`;
// src/client/appearance.ts
var NATIVE_APPEARANCE_ROW = '[class*="_cubeRow"]';
var LIANG_APPEARANCE_BUTTON = "liang-appearance-choice";
var LIANG_BINDING_CONTROL = "liang-appearance-binding";
var LIANG_BINDING_INPUT = "liang-appearance-binding__input";
function hasClassSuffix(node, suffix) {
return [...node.classList].some((name) => name.endsWith(suffix));
}
function findAppearanceRow() {
for (const row of document.querySelectorAll(NATIVE_APPEARANCE_ROW)) {
if (!hasClassSuffix(row, "_cubeRow")) continue;
const nativeButtons = [...row.children].filter(
(node) => node instanceof HTMLButtonElement && hasClassSuffix(node, "_themeCube")
);
if (nativeButtons.length < 3) continue;
let group = row.parentElement;
while (group !== null && !hasClassSuffix(group, "_group")) {
group = group.parentElement;
}
group ??= row.parentElement;
if (group !== null) return { row, group, nativeButtons };
}
return null;
}
function installLiangAppearanceButton(scope, presenter, packageId) {
let pending = false;
let disposed = false;
let mounted = null;
const nativeClickHandlers = /* @__PURE__ */ new Map();
const unmount = () => {
for (const [button, handler] of nativeClickHandlers) {
button.removeEventListener("click", handler, { capture: true });
}
nativeClickHandlers.clear();
if (mounted === null) return;
mounted.button.removeEventListener("click", mounted.handleCustomClick);
mounted.bindingInput.removeEventListener("change", mounted.handleBindingChange);
mounted.button.remove();
mounted.binding.remove();
mounted = null;
};
const choose = async (enabled) => {
if (disposed || pending || scope.getSnapshot().enabled === enabled) return;
pending = true;
sync();
try {
await presenter.choose(enabled);
} catch {
} finally {
pending = false;
if (!disposed) sync();
}
};
const createMount = (row, group) => {
const english = document.documentElement.lang.toLowerCase().startsWith("en");
const button = document.createElement("button");
button.className = LIANG_APPEARANCE_BUTTON;
button.type = "button";
button.dataset.plugin = packageId;
button.setAttribute("aria-label", "\u6ED1\u52A8\u53D8\u7956");
const icon = document.createElement("span");
icon.className = "liang-appearance-choice__icon";
icon.setAttribute("aria-hidden", "true");
icon.textContent = "\u25C8";
const label = document.createElement("span");
label.className = "liang-appearance-choice__label";
label.textContent = english ? "Slider" : "\u6ED1\u52A8\u53D8\u7956";
button.append(icon, label);
const handleCustomClick = () => {
void choose(true);
};
button.addEventListener("click", handleCustomClick);
const binding = document.createElement("label");
binding.className = LIANG_BINDING_CONTROL;
binding.dataset.plugin = packageId;
const bindingCopy = document.createElement("span");
bindingCopy.className = "liang-appearance-binding__copy";
const bindingLabel = document.createElement("span");
bindingLabel.className = "liang-appearance-binding__label";
const bindingText = english ? "Bind slider to reasoning level" : "\u6ED1\u52A8\u53D8\u7956\u7ED1\u5B9A\u601D\u8003\u7B49\u7EA7";
bindingLabel.textContent = bindingText;
const bindingDescription = document.createElement("span");
bindingDescription.className = "liang-appearance-binding__description";
bindingDescription.id = "liang-appearance-binding-description";
bindingDescription.textContent = english ? "When off, the slider does not change the reasoning level." : "\u5173\u95ED\u4E4B\u540E\u6ED1\u5757\u4E0D\u8054\u52A8\u601D\u8003\u7B49\u7EA7";
bindingCopy.append(bindingLabel, bindingDescription);
const bindingInput = document.createElement("input");
bindingInput.className = LIANG_BINDING_INPUT;
bindingInput.type = "checkbox";
bindingInput.setAttribute("role", "switch");
bindingInput.setAttribute("aria-label", bindingText);
bindingInput.setAttribute("aria-describedby", bindingDescription.id);
const updateBinding = async () => {
if (disposed || pending) return;
try {
await scope.setBindEffort(bindingInput.checked);
} catch {
} finally {
if (!disposed) sync();
}
};
const handleBindingChange = () => {
void updateBinding();
};
bindingInput.addEventListener("change", handleBindingChange);
binding.append(bindingCopy, bindingInput);
return { row, group, button, binding, bindingInput, handleCustomClick, handleBindingChange };
};
const sync = () => {
if (disposed) return;
const target = findAppearanceRow();
if (target === null) {
unmount();
return;
}
if (mounted !== null && (mounted.row !== target.row || mounted.group !== target.group)) {
unmount();
}
mounted ??= createMount(target.row, target.group);
const { row, group, button, binding, bindingInput } = mounted;
if (button.parentElement !== row) row.append(button);
const snapshot = scope.getSnapshot();
button.disabled = pending;
button.setAttribute("aria-pressed", String(snapshot.enabled));
if (snapshot.enabled) {
if (binding.parentElement !== group) group.append(binding);
bindingInput.checked = snapshot.bindEffort;
bindingInput.setAttribute("aria-checked", String(snapshot.bindEffort));
bindingInput.disabled = pending;
} else {
binding.remove();
}
for (const [nativeButton, handler] of nativeClickHandlers) {
if (target.nativeButtons.includes(nativeButton)) continue;
nativeButton.removeEventListener("click", handler, { capture: true });
nativeClickHandlers.delete(nativeButton);
}
for (const nativeButton of target.nativeButtons) {
if (nativeClickHandlers.has(nativeButton)) continue;
const handleNativeClick = () => {
void choose(false);
};
nativeClickHandlers.set(nativeButton, handleNativeClick);
nativeButton.addEventListener("click", handleNativeClick, { capture: true });
}
};
const observer = new MutationObserver(sync);
observer.observe(document.body, { childList: true, subtree: true });
const unsubscribe = scope.subscribe(sync);
sync();
return () => {
if (disposed) return;
disposed = true;
observer.disconnect();
unsubscribe();
unmount();
};
}
// src/client/portrait-loader.ts
var RETRY_DELAYS = [500, 1500, 3e3];
function retryUrl(source, retry) {
if (retry === 0) return source;
const hashIndex = source.indexOf("#");
const hash = hashIndex < 0 ? "" : source.slice(hashIndex);
const base = hashIndex < 0 ? source : source.slice(0, hashIndex);
const queryIndex = base.indexOf("?");
const path = queryIndex < 0 ? base : base.slice(0, queryIndex);
const query = new URLSearchParams(queryIndex < 0 ? "" : base.slice(queryIndex + 1));
query.set("retry", String(retry));
return `${path}?${query}${hash}`;
}
function createPortraitLoader(options) {
const sources = options.sources.map((source) => ({ ...source }));
const assets = /* @__PURE__ */ new Map();
for (const { url } of sources) {
if (!assets.has(url)) {
assets.set(url, {
url,
status: "idle",
loadedUrl: null,
retries: 0,
image: null,
timer: null,
detach: null
});
}
}
const createImage = options.createImage ?? (() => new Image());
let onChange = options.onChange;
let active = false;
let disposed = false;
let generation = 0;
let targetLevel = sources[0]?.level ?? 0;
let displayedIndex = null;
let lastState = null;
const isCurrent = (run) => active && !disposed && generation === run;
const nearestIndex = (available) => {
let nearest = null;
for (let index = 0; index < sources.length; index += 1) {
if (!available(index)) continue;
const distance = Math.abs(sources[index].level - targetLevel);
const previous = nearest === null ? Infinity : Math.abs(sources[nearest].level - targetLevel);
if (nearest === null || distance < previous || distance === previous && sources[index].level > sources[nearest].level) {
nearest = index;
}
}
return nearest;
};
const isReady = (index) => assets.get(sources[index].url)?.status === "ready";
const emit = (state) => {
if (lastState?.index === state.index && lastState.url === state.url && lastState.status === state.status) return;
lastState = state;
onChange?.(state);
};
const reconcile = () => {
if (!active || disposed) return;
const target = nearestIndex(() => true);
if (target !== null && isReady(target)) displayedIndex = target;
else if (displayedIndex === null) displayedIndex = nearestIndex(isReady);
if (displayedIndex !== null) {
const asset = assets.get(sources[displayedIndex].url);
emit({ index: displayedIndex, url: asset.loadedUrl, image: asset.image, status: "ready" });
} else {
const exhausted = [...assets.values()].every((asset) => asset.status === "failed");
emit({ index: null, url: null, image: null, status: exhausted ? "failed" : "loading" });
}
};
const request = (asset, run) => {
if (!isCurrent(run)) return;
asset.status = "loading";
const image = createImage();
asset.image = image;
image.loading = "eager";
image.decoding = "async";
const url = retryUrl(asset.url, asset.retries);
let settled = false;
let decoding = false;
const current = () => !settled && isCurrent(run) && asset.image === image;
const detach = () => {
image.removeEventListener("load", handleLoad);
image.removeEventListener("error", handleError);
asset.detach = null;
};
const handleError = () => {
if (!current()) return;
settled = true;
detach();
image.removeAttribute("src");
asset.image = null;
if (asset.retries < RETRY_DELAYS.length) {
const delay = RETRY_DELAYS[asset.retries];
asset.retries += 1;
asset.timer = setTimeout(() => {
if (!isCurrent(run)) return;
asset.timer = null;
request(asset, run);
}, delay);
} else {
asset.status = "failed";
reconcile();
}
};
const handleLoad = () => {
if (!current() || decoding) return;
decoding = true;
void image.decode().then(() => {
if (!current()) return;
settled = true;
detach();
asset.loadedUrl = url;
asset.status = "ready";
reconcile();
}, handleError);
};
asset.detach = detach;
image.addEventListener("load", handleLoad);
image.addEventListener("error", handleError);
image.src = url;
};
const stop = () => {
active = false;
generation += 1;
for (const asset of assets.values()) {
if (asset.timer !== null) clearTimeout(asset.timer);
asset.timer = null;
asset.detach?.();
asset.image?.removeAttribute("src");
asset.image = null;
asset.status = "idle";
asset.loadedUrl = null;
asset.retries = 0;
}
displayedIndex = null;
lastState = null;
};
return {
start() {
if (disposed || active) return;
active = true;
const run = ++generation;
reconcile();
for (const asset of assets.values()) request(asset, run);
},
select(level) {
if (disposed || !Number.isFinite(level)) return;
targetLevel = level;
reconcile();
},
stop,
dispose() {
stop();
disposed = true;
onChange = null;
}
};
}
// src/client/logic.ts
var PREVIEW_MAX_FRAME = 240;
var MAX_LEVEL = 30;
@@ -665,19 +993,6 @@ function selectedEffortIndex(efforts, selectedId, defaultId) {
const id = selectedId ?? defaultId;
return id === void 0 ? -1 : efforts.findIndex((effort) => effort.id === id);
}
function portraitBlendForLevel(level, anchors) {
if (anchors.length === 0) return { lowerIndex: -1, upperIndex: -1, mix: 0 };
const safeLevel = Number.isFinite(level) ? Math.min(anchors[anchors.length - 1], Math.max(anchors[0], level)) : anchors[0];
let upperIndex = anchors.findIndex((anchor) => anchor >= safeLevel);
if (upperIndex < 0) upperIndex = anchors.length - 1;
const lowerIndex = Math.max(0, upperIndex - (anchors[upperIndex] > safeLevel ? 1 : 0));
const span = anchors[upperIndex] - anchors[lowerIndex];
return {
lowerIndex,
upperIndex,
mix: span === 0 ? 0 : (safeLevel - anchors[lowerIndex]) / span
};
}
function liangRankForFrame(rawFrame) {
const level = clampFrame(rawFrame) / PREVIEW_MAX_FRAME * MAX_LEVEL;
const index = level >= MAX_LEVEL ? LIANG_RANKS.length - 1 : Math.floor(level / 6);
@@ -750,7 +1065,6 @@ var import_jsx_runtime = require("react/jsx-runtime");
var PACKAGE_ID = "dsh-client-liang-intensity-skin";
var LOCALE_NAMESPACE = "liang.skin";
var ASSET_PREFIX = `/plugins/${PACKAGE_ID}/assets`;
var FIRST_PORTRAIT_FILE = "portrait-source-v2/stage-00.webp";
var BIND_EFFORT_KEY = "dsh-liang-intensity-skin.bind-effort";
var PORTRAIT_ANCHORS = [
{ level: 0, file: "stage-00.webp" },
@@ -778,7 +1092,6 @@ var PORTRAIT_ANCHORS = [
{ level: 29, file: "level-29.webp" },
{ level: 30, file: "stage-30.webp" }
];
var ANCHOR_LEVELS = PORTRAIT_ANCHORS.map((anchor) => anchor.level);
var cssVariables = [
"--liang-strength",
"--liang-page",
@@ -800,9 +1113,7 @@ var SkinPresenter = class {
scope;
theme;
root;
portrait;
preloads;
portraitReady = false;
portraits;
enabled = true;
// Default to the max frame so the first paint after load is the dark shell;
// starting at 0 flashed the light palette before the directory resolved.
@@ -817,46 +1128,31 @@ var SkinPresenter = class {
this.root = document.createElement("div");
this.root.className = "liang-skin-backdrop";
this.root.dataset.plugin = PACKAGE_ID;
this.root.dataset.media = "sequence";
this.root.dataset.media = "color";
this.root.setAttribute("aria-hidden", "true");
this.portrait = document.createElement("img");
this.portrait.className = "liang-skin-sequence-frame";
this.portrait.alt = "";
this.portrait.draggable = false;
this.portrait.decoding = "async";
this.portrait.src = `${ASSET_PREFIX}/${FIRST_PORTRAIT_FILE}`;
this.portrait.addEventListener("error", this.handleSequenceError);
this.preloads = PORTRAIT_ANCHORS.map(({ file }) => {
const image = new Image();
image.loading = "eager";
image.decoding = "async";
image.src = `${ASSET_PREFIX}/portrait-source-v2/${file}`;
return image;
});
void Promise.all(this.preloads.map((image) => image.decode())).then(
() => {
if (this.disposed) return;
this.portraitReady = true;
this.portraits = createPortraitLoader({
sources: PORTRAIT_ANCHORS.map(({ level, file }) => ({
level,
url: `${ASSET_PREFIX}/portrait-source-v2/${file}`
})),
onChange: ({ image }) => {
if (this.disposed || !this.enabled) return;
if (image === null) {
this.root.dataset.media = "color";
this.root.replaceChildren();
return;
}
const portrait = image;
portrait.className = "liang-skin-sequence-frame";
portrait.alt = "";
portrait.draggable = false;
if (this.root.firstChild !== portrait) this.root.replaceChildren(portrait);
this.root.dataset.media = "sequence";
this.updatePortrait(paletteForFrame(this.frame).level);
},
() => {
}
);
this.root.append(this.portrait);
document.body.prepend(this.root);
});
this.unsubscribe = scope.subscribe(() => this.syncSettings());
this.syncSettings();
}
handleSequenceError = () => {
if (this.portrait.src.endsWith(FIRST_PORTRAIT_FILE)) {
this.root.dataset.media = "color";
return;
}
this.portraitReady = false;
this.portrait.src = `${ASSET_PREFIX}/${FIRST_PORTRAIT_FILE}`;
this.root.dataset.media = "sequence";
};
syncSettings() {
this.setEnabled(this.scope.getSnapshot().enabled);
}
@@ -867,19 +1163,27 @@ var SkinPresenter = class {
return this.frame;
}
setEnabled(enabled) {
if (this.disposed) return;
this.enabled = enabled;
if (enabled) {
if (!this.root.isConnected) document.body.prepend(this.root);
document.body.dataset.liangSkin = "on";
this.applyFrame();
this.portraits.start();
} else {
if (this.raf !== 0) cancelAnimationFrame(this.raf);
this.raf = 0;
this.root.remove();
this.portraits.stop();
this.root.replaceChildren();
this.root.dataset.media = "color";
delete document.body.dataset.liangSkin;
delete document.body.dataset.liangStage;
for (const name of cssVariables) document.body.style.removeProperty(name);
}
}
setFrame(frame) {
if (this.disposed) return;
this.pendingFrame = Math.min(PREVIEW_MAX_FRAME, Math.max(0, Math.round(frame)));
this.frame = this.pendingFrame;
if (!this.enabled) return;
@@ -891,6 +1195,7 @@ var SkinPresenter = class {
});
}
applyFrame() {
if (this.disposed || !this.enabled) return;
const palette = paletteForFrame(this.frame);
const body = document.body;
this.syncNativeTheme(palette.stage === 5 ? "dark" : "light");
@@ -917,16 +1222,7 @@ var SkinPresenter = class {
this.theme.setTheme(theme);
}
updatePortrait(level) {
if (!this.portraitReady) return;
const { lowerIndex, upperIndex, mix: mix2 } = portraitBlendForLevel(
level,
ANCHOR_LEVELS
);
const lower = PORTRAIT_ANCHORS[lowerIndex];
const upper = PORTRAIT_ANCHORS[upperIndex];
const selected = mix2 >= 0.5 ? upper : lower;
const source = `${ASSET_PREFIX}/portrait-source-v2/${selected.file}`;
if (this.portrait.getAttribute("src") !== source) this.portrait.src = source;
this.portraits.select(level);
}
async choose(enabled) {
this.setEnabled(enabled);
@@ -942,8 +1238,7 @@ var SkinPresenter = class {
this.disposed = true;
this.unsubscribe();
if (this.raf !== 0) cancelAnimationFrame(this.raf);
this.portrait.removeEventListener("error", this.handleSequenceError);
for (const image of this.preloads) image.src = "";
this.portraits.dispose();
this.root.remove();
document.body.removeAttribute("data-liang-skin");
delete document.body.dataset.liangStage;
@@ -1110,117 +1405,6 @@ function LiangEffortSlider({ directory, load, select, presenter, scope }) {
}
);
}
var NATIVE_APPEARANCE_GROUP = '[class*="_8HJdBW_group"]';
var NATIVE_APPEARANCE_ROW = '[class*="_8HJdBW_cubeRow"]';
var LIANG_APPEARANCE_BUTTON = "liang-appearance-choice";
var LIANG_BINDING_CONTROL = "liang-appearance-binding";
var LIANG_BINDING_INPUT = "liang-appearance-binding__input";
function installLiangAppearanceButton(scope, presenter) {
let pending = false;
const hookedNativeButtons = /* @__PURE__ */ new Set();
const nativeClickHandlers = /* @__PURE__ */ new Map();
const sync = () => {
const group = [...document.querySelectorAll(NATIVE_APPEARANCE_GROUP)].find((node) => node.querySelector('[class*="_8HJdBW_themeCube"]'));
const row = group?.querySelector(NATIVE_APPEARANCE_ROW);
if (row === void 0 || row === null) return;
let customButton = row.querySelector(`.${LIANG_APPEARANCE_BUTTON}`);
if (customButton === null) {
customButton = document.createElement("button");
customButton.className = LIANG_APPEARANCE_BUTTON;
customButton.type = "button";
customButton.dataset.plugin = PACKAGE_ID;
customButton.setAttribute("aria-label", "\u6ED1\u52A8\u53D8\u7956");
const icon = document.createElement("span");
icon.className = "liang-appearance-choice__icon";
icon.setAttribute("aria-hidden", "true");
icon.textContent = "\u25C8";
const label = document.createElement("span");
label.className = "liang-appearance-choice__label";
label.textContent = document.documentElement.lang.toLowerCase().startsWith("en") ? "Slider" : "\u6ED1\u52A8\u53D8\u7956";
customButton.append(icon, label);
customButton.addEventListener("click", () => {
if (pending || scope.getSnapshot().enabled) return;
pending = true;
sync();
void presenter.choose(true).finally(() => {
pending = false;
sync();
});
});
}
if (customButton.parentElement !== row) row.append(customButton);
const snapshot = scope.getSnapshot();
customButton.disabled = pending;
customButton.setAttribute("aria-pressed", String(snapshot.enabled));
let bindingControl = group.querySelector(`.${LIANG_BINDING_CONTROL}`);
if (!snapshot.enabled) {
bindingControl?.remove();
bindingControl = null;
} else if (bindingControl === null) {
bindingControl = document.createElement("label");
bindingControl.className = LIANG_BINDING_CONTROL;
bindingControl.dataset.plugin = PACKAGE_ID;
const bindingCopy = document.createElement("span");
bindingCopy.className = "liang-appearance-binding__copy";
const bindingLabel = document.createElement("span");
bindingLabel.className = "liang-appearance-binding__label";
const bindingText = document.documentElement.lang.toLowerCase().startsWith("en") ? "Bind slider to reasoning level" : "\u6ED1\u52A8\u53D8\u7956\u7ED1\u5B9A\u601D\u8003\u7B49\u7EA7";
bindingLabel.textContent = bindingText;
const bindingDescription = document.createElement("span");
bindingDescription.className = "liang-appearance-binding__description";
bindingDescription.id = "liang-appearance-binding-description";
bindingDescription.textContent = document.documentElement.lang.toLowerCase().startsWith("en") ? "When off, the slider does not change the reasoning level." : "\u5173\u95ED\u4E4B\u540E\u6ED1\u5757\u4E0D\u8054\u52A8\u601D\u8003\u7B49\u7EA7";
bindingCopy.append(bindingLabel, bindingDescription);
const bindingInput = document.createElement("input");
bindingInput.className = LIANG_BINDING_INPUT;
bindingInput.type = "checkbox";
bindingInput.setAttribute("role", "switch");
bindingInput.setAttribute("aria-label", bindingText);
bindingInput.setAttribute("aria-describedby", bindingDescription.id);
bindingInput.addEventListener("change", () => {
void scope.setBindEffort(bindingInput.checked).catch(() => sync());
});
bindingControl.append(bindingCopy, bindingInput);
group.append(bindingControl);
}
if (bindingControl !== null) {
const bindingInput = bindingControl.querySelector(`.${LIANG_BINDING_INPUT}`);
if (bindingInput !== null) {
bindingInput.checked = snapshot.bindEffort;
bindingInput.setAttribute("aria-checked", String(snapshot.bindEffort));
bindingInput.disabled = pending;
}
}
for (const nativeButton of row.querySelectorAll('[class*="_8HJdBW_themeCube"]')) {
if (hookedNativeButtons.has(nativeButton)) continue;
hookedNativeButtons.add(nativeButton);
const handleNativeClick = () => {
if (pending || !scope.getSnapshot().enabled) return;
pending = true;
sync();
void presenter.choose(false).finally(() => {
pending = false;
sync();
});
};
nativeClickHandlers.set(nativeButton, handleNativeClick);
nativeButton.addEventListener("click", handleNativeClick, { capture: true });
}
};
const observer = new MutationObserver(sync);
observer.observe(document.body, { childList: true, subtree: true });
const unsubscribe = scope.subscribe(sync);
sync();
return () => {
observer.disconnect();
unsubscribe();
for (const [nativeButton, handleNativeClick] of nativeClickHandlers) {
nativeButton.removeEventListener("click", handleNativeClick, { capture: true });
}
document.querySelectorAll(`.${LIANG_APPEARANCE_BUTTON}`).forEach((button) => button.remove());
document.querySelectorAll(`.${LIANG_BINDING_CONTROL}`).forEach((control) => control.remove());
};
}
var inject = [
"slots",
"sessions",
@@ -1284,7 +1468,7 @@ function apply(ctx) {
const presenter = new SkinPresenter(scope, theme);
ctx.effect(() => () => presenter.dispose(), "liang-intensity-skin: backdrop presenter");
ctx.effect(
() => installLiangAppearanceButton(scope, presenter),
() => installLiangAppearanceButton(scope, presenter, PACKAGE_ID),
"liang-intensity-skin: native appearance extension"
);
ctx.effect(

File diff suppressed because one or more lines are too long