feat: add Liang skin effort binding toggle
This commit is contained in:
192
lib/client.js
192
lib/client.js
@@ -560,6 +560,93 @@ body[data-liang-skin="on"] .liang-effort-control__range::-webkit-slider-thumb {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.liang-appearance-binding {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 28px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 4px 0 0;
|
||||
color: var(--dsw-alias-label-primary);
|
||||
font: inherit;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.liang-appearance-binding__label {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.liang-appearance-binding__copy {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.liang-appearance-binding__description {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.liang-appearance-binding__input {
|
||||
position: relative;
|
||||
flex: 0 0 36px;
|
||||
width: 36px;
|
||||
height: 20px;
|
||||
margin: 0;
|
||||
border: 1px solid var(--dsw-alias-border-l2);
|
||||
border-radius: 999px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
background: var(--dsw-alias-interactive-bg-hover, var(--dsw-alias-bg-module-platform));
|
||||
}
|
||||
|
||||
.liang-appearance-binding__input::before {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
content: "";
|
||||
background: var(--dsw-alias-label-primary);
|
||||
box-shadow: 0 1px 3px rgb(0 0 0 / 18%);
|
||||
transition: transform 160ms ease, background-color 160ms ease;
|
||||
}
|
||||
|
||||
.liang-appearance-binding__input:checked {
|
||||
border-color: var(--dsw-static-neutral-bluish-400);
|
||||
background: var(--dsw-static-neutral-bluish-400);
|
||||
}
|
||||
|
||||
.liang-appearance-binding__input:checked::before {
|
||||
background: var(--dsw-alias-bg-layer-1, #fff);
|
||||
transform: translateX(16px);
|
||||
}
|
||||
|
||||
.liang-appearance-binding__input:focus-visible {
|
||||
box-shadow: 0 0 0 2px var(--dsw-alias-interactive-bg-hover);
|
||||
}
|
||||
|
||||
.liang-appearance-binding__input:disabled {
|
||||
cursor: progress;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.liang-effort-control {
|
||||
width: 92px;
|
||||
@@ -712,6 +799,7 @@ var LOCALE_NAMESPACE = "liang.skin";
|
||||
var ASSET_PREFIX = `/plugins/${PACKAGE_ID}/assets`;
|
||||
var VIDEO_DURATION = 8.033;
|
||||
var PREFERENCE_KEY = "dsh-liang-intensity-skin.enabled";
|
||||
var BIND_EFFORT_KEY = "dsh-liang-intensity-skin.bind-effort";
|
||||
var PORTRAIT_ANCHORS = [
|
||||
{ level: 0, file: "stage-00.png" },
|
||||
{ level: 1, file: "level-01.png" },
|
||||
@@ -829,6 +917,9 @@ var SkinPresenter = class {
|
||||
isEnabled() {
|
||||
return this.enabled;
|
||||
}
|
||||
getFrame() {
|
||||
return this.frame;
|
||||
}
|
||||
setEnabled(enabled) {
|
||||
this.enabled = enabled;
|
||||
if (enabled) {
|
||||
@@ -952,26 +1043,35 @@ function LiangEffortSlider({ directory, load, select, presenter, scope }) {
|
||||
reasoning?.defaultEffort
|
||||
);
|
||||
const committedFrame = committedIndex < 0 ? 0 : frameForEffort(committedIndex, efforts.length);
|
||||
const [frame, setFrame] = (0, import_react.useState)(committedFrame);
|
||||
const bindEffort = skin.bindEffort;
|
||||
const [frame, setFrame] = (0, import_react.useState)(() => bindEffort ? committedFrame : presenter.getFrame());
|
||||
const [pending, setPending] = (0, import_react.useState)(false);
|
||||
const [failed, setFailed] = (0, import_react.useState)(false);
|
||||
const [interacting, setInteracting] = (0, import_react.useState)(false);
|
||||
const dragging = (0, import_react.useRef)(false);
|
||||
const dragStartFrame = (0, import_react.useRef)(frame);
|
||||
const enabled = skin.enabled;
|
||||
(0, import_react.useEffect)(() => {
|
||||
if (enabled) load();
|
||||
}, [enabled, load]);
|
||||
(0, import_react.useEffect)(() => {
|
||||
if (dragging.current || pending) return;
|
||||
if (!bindEffort || dragging.current || pending) return;
|
||||
setFrame(committedFrame);
|
||||
presenter.setFrame(committedFrame);
|
||||
}, [committedFrame, pending, presenter]);
|
||||
}, [bindEffort, committedFrame, pending, presenter]);
|
||||
if (!enabled) return null;
|
||||
if (reasoning === null || efforts.length < 2) return null;
|
||||
if (bindEffort && (reasoning === null || efforts.length < 2)) return null;
|
||||
const previewIndex = nearestEffortIndex(frame, efforts);
|
||||
const previewEffort = efforts[previewIndex];
|
||||
const progressRatio = frame / PREVIEW_MAX_FRAME;
|
||||
const tooltipLabel = indicatorLabel(frame, bindEffort ? efforts : []);
|
||||
const commit = async (rawFrame) => {
|
||||
if (!bindEffort) {
|
||||
dragging.current = false;
|
||||
setFrame(rawFrame);
|
||||
presenter.setFrame(rawFrame);
|
||||
return;
|
||||
}
|
||||
const targetIndex = nearestEffortIndex(rawFrame, efforts);
|
||||
const target = efforts[targetIndex];
|
||||
if (target === void 0) return;
|
||||
@@ -1000,14 +1100,14 @@ function LiangEffortSlider({ directory, load, select, presenter, scope }) {
|
||||
className: "liang-effort-control",
|
||||
"data-plugin": PACKAGE_ID,
|
||||
"data-state": failed ? "error" : pending ? "pending" : "ready",
|
||||
title: previewEffort?.name,
|
||||
title: bindEffort ? previewEffort?.name : void 0,
|
||||
children: [
|
||||
interacting && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
||||
"output",
|
||||
{
|
||||
className: "liang-effort-control__tooltip",
|
||||
style: { "--liang-slider-ratio": progressRatio },
|
||||
children: indicatorLabel(frame, efforts)
|
||||
children: tooltipLabel
|
||||
}
|
||||
),
|
||||
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "liang-effort-control__ticks", "aria-hidden": "true", children: efforts.map((effort, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
||||
@@ -1028,10 +1128,11 @@ function LiangEffortSlider({ directory, load, select, presenter, scope }) {
|
||||
step: 1,
|
||||
value: frame,
|
||||
disabled: pending || state.status === "selecting",
|
||||
"aria-label": "\u601D\u8003\u7B49\u7EA7",
|
||||
"aria-valuetext": previewEffort?.name ?? "",
|
||||
"aria-label": bindEffort ? "\u601D\u8003\u7B49\u7EA7" : "\u76AE\u80A4\u8FDB\u5EA6",
|
||||
"aria-valuetext": bindEffort ? previewEffort?.name ?? "" : tooltipLabel,
|
||||
onPointerDown: () => {
|
||||
dragging.current = true;
|
||||
dragStartFrame.current = frame;
|
||||
setInteracting(true);
|
||||
},
|
||||
onInput: (event) => {
|
||||
@@ -1047,8 +1148,8 @@ function LiangEffortSlider({ directory, load, select, presenter, scope }) {
|
||||
onPointerCancel: () => {
|
||||
setInteracting(false);
|
||||
dragging.current = false;
|
||||
setFrame(committedFrame);
|
||||
presenter.setFrame(committedFrame);
|
||||
setFrame(dragStartFrame.current);
|
||||
presenter.setFrame(dragStartFrame.current);
|
||||
},
|
||||
onKeyUp: (event) => {
|
||||
setInteracting(false);
|
||||
@@ -1063,8 +1164,8 @@ function LiangEffortSlider({ directory, load, select, presenter, scope }) {
|
||||
if (event.key === "Escape" && !pending) {
|
||||
setInteracting(false);
|
||||
dragging.current = false;
|
||||
setFrame(committedFrame);
|
||||
presenter.setFrame(committedFrame);
|
||||
setFrame(dragStartFrame.current);
|
||||
presenter.setFrame(dragStartFrame.current);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1076,6 +1177,8 @@ 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();
|
||||
@@ -1110,8 +1213,48 @@ function installLiangAppearanceButton(scope, presenter) {
|
||||
});
|
||||
}
|
||||
if (customButton.parentElement !== row) row.append(customButton);
|
||||
const snapshot = scope.getSnapshot();
|
||||
customButton.disabled = pending;
|
||||
customButton.setAttribute("aria-pressed", String(scope.getSnapshot().enabled));
|
||||
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);
|
||||
@@ -1139,6 +1282,7 @@ function installLiangAppearanceButton(scope, presenter) {
|
||||
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 = [
|
||||
@@ -1150,14 +1294,18 @@ var inject = [
|
||||
];
|
||||
function createPreferenceStore() {
|
||||
let snapshot = {
|
||||
enabled: localStorage.getItem(PREFERENCE_KEY) !== "0"
|
||||
enabled: localStorage.getItem(PREFERENCE_KEY) !== "0",
|
||||
bindEffort: localStorage.getItem(BIND_EFFORT_KEY) !== "0"
|
||||
};
|
||||
const listeners = /* @__PURE__ */ new Set();
|
||||
const onStorage = (event) => {
|
||||
if (event.key !== PREFERENCE_KEY) return;
|
||||
const enabled = event.newValue !== "0";
|
||||
if (enabled === snapshot.enabled) return;
|
||||
snapshot = { enabled };
|
||||
if (event.key !== PREFERENCE_KEY && event.key !== BIND_EFFORT_KEY) return;
|
||||
const next = {
|
||||
enabled: event.key === PREFERENCE_KEY ? event.newValue !== "0" : snapshot.enabled,
|
||||
bindEffort: event.key === BIND_EFFORT_KEY ? event.newValue !== "0" : snapshot.bindEffort
|
||||
};
|
||||
if (next.enabled === snapshot.enabled && next.bindEffort === snapshot.bindEffort) return;
|
||||
snapshot = next;
|
||||
for (const listener of listeners) listener();
|
||||
};
|
||||
window.addEventListener("storage", onStorage);
|
||||
@@ -1170,7 +1318,13 @@ function createPreferenceStore() {
|
||||
async set(enabled) {
|
||||
if (enabled === snapshot.enabled) return;
|
||||
localStorage.setItem(PREFERENCE_KEY, enabled ? "1" : "0");
|
||||
snapshot = { enabled };
|
||||
snapshot = { ...snapshot, enabled };
|
||||
for (const listener of listeners) listener();
|
||||
},
|
||||
async setBindEffort(bindEffort) {
|
||||
if (bindEffort === snapshot.bindEffort) return;
|
||||
localStorage.setItem(BIND_EFFORT_KEY, bindEffort ? "1" : "0");
|
||||
snapshot = { ...snapshot, bindEffort };
|
||||
for (const listener of listeners) listener();
|
||||
},
|
||||
dispose() {
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user