Compare commits

..

5 Commits

Author SHA1 Message Date
b4f0b1838e Rename Chaturbate thumbnail userscript 2026-06-06 12:50:08 +08:00
73d9d380f5 Add Chaturbate thumbnail controls and chat hiding 2026-06-06 12:17:53 +08:00
5d6a0d7d41 Make Chaturbate thumbnail grids responsive 2026-06-06 10:18:54 +08:00
bde00f27d3 Translate Chaturbate tag pages 2026-06-03 13:33:16 +08:00
893fe47ace Fix discover carousel scaling 2026-06-02 01:36:29 +08:00
3 changed files with 3186 additions and 1043 deletions

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@ const assert = require("node:assert/strict");
const fs = require("node:fs");
const vm = require("node:vm");
const source = fs.readFileSync("Chaturbate/chaturbate-thumbnails-2x.user.js", "utf8");
const source = fs.readFileSync("Chaturbate/chaturbate-thumbnails-custom.user.js", "utf8");
let capturedCss = "";
let gmAddStyleCalls = 0;
let menuCommandName = "";
@@ -86,43 +86,43 @@ assert.match(
);
assert.match(
capturedCss,
/data-tm-thumb-scale-following-list="1"[\s\S]*--tm-thumb-following-list-columns/,
"followed-cams list should be separated from homepage list column count",
/data-tm-thumb-scale-following-list="1"[\s\S]*--tm-thumb-following-list-min-width/,
"followed-cams list should use its own detected minimum column width",
);
assert.match(
capturedCss,
/data-tm-thumb-scale-following-list="1"[\s\S]*grid-template-columns:\s*repeat\(var\(--tm-thumb-following-list-columns\),\s*minmax\(0,\s*1fr\)\)\s*!important/,
"followed-cams list should reduce the original column count and keep 1fr alignment",
/data-tm-thumb-scale-following-list="1"[\s\S]*grid-template-columns:\s*repeat\(auto-fill,\s*minmax\(var\(--tm-thumb-following-list-min-width\),\s*1fr\)\)\s*!important/,
"followed-cams list should reduce columns responsively using scaled minimum width",
);
assert.match(
capturedCss,
/#main\.roomPage\s+ul\.list:has\(li\.roomCard\)\s+\.room_thumbnail_container[\s\S]*width:\s*100%\s*!important[\s\S]*height:\s*var\(--tm-thumb-related-height\)\s*!important/,
"room page related rooms thumbnail containers should follow card width and use detected height",
/#main\.roomPage\s+ul\.list:has\(li\.roomCard\)\s+\.room_thumbnail_container[\s\S]*width:\s*100%\s*!important[\s\S]*height:\s*auto\s*!important/,
"room page related rooms thumbnail containers should follow card width and automatic height",
);
assert.match(
capturedCss,
/\.BaseRoomContents\s+ul\.list:has\(li\.roomCard\)\s+\.room_thumbnail[\s\S]*width:\s*100%\s*!important[\s\S]*height:\s*var\(--tm-thumb-related-height\)\s*!important/,
"base room related thumbnails should follow card width and use detected height",
/\.BaseRoomContents\s+ul\.list:has\(li\.roomCard\)\s+\.room_thumbnail[\s\S]*width:\s*100%\s*!important[\s\S]*height:\s*auto\s*!important/,
"base room related thumbnails should follow card width and automatic height",
);
assert.match(
capturedCss,
/\.BaseRoomContents\s+ul\.RoomCardGrid[\s\S]*grid-template-columns:\s*repeat\(auto-fill,\s*var\(--tm-thumb-related-width\)\)\s*!important/,
"base room RoomCardGrid related rooms should use detected card width",
/\.BaseRoomContents\s+ul\.RoomCardGrid[\s\S]*grid-template-columns:\s*repeat\(auto-fill,\s*minmax\(var\(--tm-thumb-related-width\),\s*1fr\)\)\s*!important/,
"base room RoomCardGrid related rooms should fill the row using detected scaled minimum width",
);
assert.match(
capturedCss,
/\.BaseRoomContents\s+ul\.RoomCardGrid\s+\.RoomCardThumbnail[\s\S]*width:\s*100%\s*!important[\s\S]*height:\s*var\(--tm-thumb-related-height\)\s*!important/,
"base room RoomCardGrid thumbnails should follow card width and use detected height",
/\.BaseRoomContents\s+ul\.RoomCardGrid\s+\.RoomCardThumbnail[\s\S]*width:\s*100%\s*!important[\s\S]*height:\s*auto\s*!important/,
"base room RoomCardGrid thumbnails should follow card width and automatic height",
);
assert.match(
capturedCss,
/\.BaseRoomContents\s+ul\.RoomCardGrid\s*>\s*li\.RoomCard[\s\S]*height:\s*var\(--tm-thumb-related-card-height\)\s*!important/,
"base room RoomCardGrid related cards should use detected card height",
/\.BaseRoomContents\s+ul\.RoomCardGrid\s*>\s*li\.RoomCard[\s\S]*height:\s*auto\s*!important[\s\S]*max-height:\s*none\s*!important/,
"base room RoomCardGrid related cards should use automatic height",
);
assert.match(
capturedCss,
/\.BaseRoomContents\s+ul\.RoomCardGrid\s*>\s*li\.RoomCard[\s\S]*width:\s*var\(--tm-thumb-related-width\)\s*!important/,
"base room RoomCardGrid related cards should use detected card width",
/\.BaseRoomContents\s+ul\.RoomCardGrid\s*>\s*li\.RoomCard[\s\S]*width:\s*auto\s*!important[\s\S]*justify-self:\s*stretch\s*!important[\s\S]*max-width:\s*none\s*!important/,
"base room RoomCardGrid related cards should stretch to fill responsive columns",
);
assert.doesNotMatch(capturedCss, /--tm-thumb-min-root:\s*\d+px;/, "script should not hard-code a root thumbnail fallback width");
assert.doesNotMatch(capturedCss, /--tm-thumb-height-root:\s*\d+px;/, "script should not hard-code a root thumbnail fallback height");
@@ -136,6 +136,11 @@ assert.match(
/#discover_root\s+\.room-list-carousel\s+\.room_thumbnail_container\s+img/,
"discover carousel thumbnail images should fill enlarged items",
);
assert.match(
capturedCss,
/#discover_root\s+\.room-list-carousel\s+\.room_thumbnail_container[\s\S]*height:\s*var\(--tm-thumb-discover-height\)\s*!important/,
"discover carousel thumbnail containers should use detected scaled height",
);
assert.match(
capturedCss,
/height:\s*var\(--tm-thumb-discover-triple-ul\)\s*!important/,
@@ -146,6 +151,36 @@ assert.match(
/height:\s*var\(--tm-thumb-discover-double-arrow\)\s*!important/,
"discover carousel arrow heights should use detected original heights",
);
assert.match(
capturedCss,
/data-tm-thumb-scale-followed-page="1"[\s\S]*\.carousel-root\s+\.single-row\s+ul\.list[\s\S]*height:\s*auto\s*!important[\s\S]*max-height:\s*none\s*!important[\s\S]*overflow:\s*visible\s*!important/,
"followed-cams recommendations should release carousel list height after scaling",
);
assert.match(
capturedCss,
/data-tm-thumb-scale-followed-page="1"[\s\S]*grid-template-columns:\s*repeat\(auto-fill,\s*minmax\(var\(--tm-thumb-discover-width\),\s*1fr\)\)\s*!important/,
"followed-cams legacy recommendations should use responsive scaled minimum width",
);
assert.doesNotMatch(
capturedCss,
/data-tm-thumb-scale-followed-page="1"[\s\S]*grid-template-columns:\s*repeat\(2,/,
"followed-cams recommendations should not force a hard-coded two-column layout",
);
assert.match(
capturedCss,
/data-tm-thumb-scale-followed-recommendations="1"[\s\S]*data-tm-thumb-followed-recommendations-list="1"[\s\S]*grid-template-columns:\s*repeat\(auto-fill,\s*minmax\(var\(--tm-thumb-followed-recommendations-width\),\s*1fr\)\)\s*!important[\s\S]*grid-auto-rows:\s*auto\s*!important/,
"title-detected followed recommendations should use responsive scaled minimum columns and automatic row height",
);
assert.match(
capturedCss,
/data-tm-thumb-scale-followed-recommendations="1"[\s\S]*data-tm-thumb-followed-recommendations-card="1"[\s\S]*width:\s*auto\s*!important[\s\S]*max-width:\s*none\s*!important[\s\S]*height:\s*auto\s*!important[\s\S]*max-height:\s*none\s*!important/,
"title-detected followed recommendation cards should stretch responsively and grow naturally",
);
assert.match(
capturedCss,
/data-tm-thumb-scale-followed-recommendations="1"[\s\S]*data-tm-thumb-followed-recommendations-thumb="1"[\s\S]*height:\s*auto\s*!important/,
"title-detected followed recommendation thumbnails should keep automatic height",
);
assert.match(
capturedCss,
/#discover_root\s+\.room-list-carousel\s+\.room_thumbnail[\s\S]*height:\s*var\(--tm-thumb-discover-height\)\s*!important/,
@@ -161,6 +196,9 @@ assert.match(
/\.FollowedDropdown__room-image[\s\S]*width:\s*100%\s*!important[\s\S]*height:\s*var\(--tm-thumb-follow-thumb-height\)\s*!important/,
"follow dropdown images should follow fixed card width and use detected height",
);
assert.doesNotMatch(capturedCss, /translateX\(/, "follow dropdown should not use a fixed horizontal offset");
assert.match(source, /adjustFollowDropdownPosition/, "script should adjust follow dropdown position only when it overflows");
assert.match(source, /viewportPadding - rect\.left/, "follow dropdown adjustment should be based on left viewport overflow");
assert.match(
capturedCss,
/react-tooltip[\s\S]*width:\s*var\(--tm-thumb-tooltip-width\)\s*!important[\s\S]*height:\s*var\(--tm-thumb-tooltip-height\)\s*!important/,
@@ -174,9 +212,20 @@ assert.match(
assert.doesNotMatch(source, /withOwnStyleDisabled/, "script should not disable its own style while measuring");
assert.match(source, /detectAndApplySizes/, "script should detect original sizes before applying scaled variables");
assert.match(source, /moduleReady/, "script should lock each module after detecting its original size");
assert.match(source, /CARD_HEIGHT_SCALE/, "script should use a separate scale for card heights");
assert.match(source, /setCardHeightVar/, "script should apply card height scaling separately from thumbnail scaling");
assert.doesNotMatch(source, /localStorage\.setItem/, "script should not write detected sizes to localStorage");
assert.match(source, /DEFAULT_RESPONSIVE_SCALE/, "script should expose a default responsive scale");
assert.match(source, /DEFAULT_FIXED_SCALE/, "script should expose a default fixed scale");
assert.match(source, /tmThumbScaleSetScales/, "script should expose a debug API for changing both scales");
assert.match(capturedCss, /#tm-thumb-scale-controls[\s\S]*position:\s*fixed\s*!important/, "script should render a fixed page control panel");
assert.match(capturedCss, /data-tm-thumb-scale-auto-hide-room-chat="1"[\s\S]*data-tm-thumb-room-chat-overlay="1"[\s\S]*display:\s*none\s*!important/, "script should hide marked room chat overlays when enabled");
assert.match(source, /hideRoomChat/, "script should persist the room chat auto-hide setting");
assert.match(source, /applyRoomChatAutoHide/, "script should apply automatic room chat hiding");
assert.match(source, /setDiscoverStackHeightVars/, "discover carousel stack heights should be derived from scaled card rows");
assert.match(source, /setCardHeightFromThumbVar\("--tm-thumb-discover-card-height"/, "discover card height should scale the thumbnail area and keep metadata height");
assert.match(source, /stackHeight == null\) return true/, "discover should not require every carousel row type to exist before enabling");
assert.match(source, /TAG_TRANSLATIONS/, "script should include a centralized tag translation table");
assert.match(source, /translateTagsPage/, "script should translate the Chaturbate tags page");
assert.doesNotMatch(source, /localStorage\.setItem\([^,]*LEGACY_CACHE_PREFIX/, "script should not write detected sizes to localStorage");
assert.match(source, /SETTINGS_KEY/, "script may persist user-adjustable scale settings");
assert.match(source, /LEGACY_CACHE_PREFIX/, "script should only keep legacy cache cleanup support");
assert.match(source, /scheduleMeasure\("after-800ms"\)/, "script should delay the first page measurement until original layout can render");
assert.doesNotMatch(
@@ -230,6 +279,144 @@ assert.equal(earlyAppendedTarget, null, "GM_addStyle should avoid manual documen
assert.equal(typeof earlyDomContentLoadedHandler, "function", "script should reinject after head becomes available");
assert.equal(earlyObservedTarget, null, "script should not observe a missing head");
const tagAnchors = [
{
href: "https://zh-hans.chaturbate.com/tag/asian/",
textContent: "#asian",
dataset: {},
title: "",
getAttribute(name) {
return name === "href" ? this.href : "";
},
},
{
href: "https://zh-hans.chaturbate.com/tag/bigboobs/",
textContent: "#bigboobs",
dataset: {},
title: "",
getAttribute(name) {
return name === "href" ? this.href : "";
},
},
{
href: "https://zh-hans.chaturbate.com/tag/ebony/female/",
textContent: "#ebony",
dataset: {},
title: "",
getAttribute(name) {
return name === "href" ? this.href : "";
},
},
{
href: "https://zh-hans.chaturbate.com/tag/not-in-table/",
textContent: "Not In Table",
dataset: {},
title: "",
getAttribute(name) {
return name === "href" ? this.href : "";
},
},
{
href: "https://zh-hans.chaturbate.com/tag/new-custom-tag/",
textContent: "#new-custom-tag",
dataset: {},
title: "",
getAttribute(name) {
return name === "href" ? this.href : "";
},
},
{
href: "https://zh-hans.chaturbate.com/tag/colombiana/female/",
textContent: "#colombiana",
dataset: {},
title: "",
getAttribute(name) {
return name === "href" ? this.href : "";
},
},
{
href: "https://zh-hans.chaturbate.com/tag/untranslated-example/",
textContent: "#untranslated-example",
dataset: {},
title: "",
getAttribute(name) {
return name === "href" ? this.href : "";
},
},
{
href: "https://zh-hans.chaturbate.com/tags/female/?page=2",
textContent: "2",
dataset: {},
title: "",
getAttribute(name) {
return name === "href" ? this.href : "";
},
},
];
const tagFakeDocument = {
documentElement: {
setAttribute() {},
},
head: {
appendChild() {},
},
body: {},
createElement() {
return { id: "", textContent: "", setAttribute() {} };
},
getElementById() {
return null;
},
querySelectorAll(selector) {
return selector.includes("/tag") ? tagAnchors : [];
},
addEventListener() {},
};
const runTagsPageTranslationTest = (pathname = "/tags/") => vm.runInNewContext(source, {
console,
document: tagFakeDocument,
location: {
href: `https://zh-hans.chaturbate.com${pathname}`,
pathname,
},
URL,
GM_addStyle() {
return { id: "", textContent: "", setAttribute() {} };
},
GM_registerMenuCommand() {},
MutationObserver: class {
observe() {}
},
});
runTagsPageTranslationTest();
assert.equal(tagAnchors[0].textContent, "亚洲", "tags page should translate known tag link text");
assert.equal(tagAnchors[0].dataset.tmTagOriginal, "#asian", "translated tags should keep the original label in dataset");
assert.equal(tagAnchors[0].title, "#asian", "translated tags should expose the original label in title");
assert.equal(tagAnchors[1].textContent, "大胸", "tags page should translate compact multi-word tag slugs");
assert.equal(tagAnchors[2].textContent, "黑人", "gender-scoped tag links should translate by the first slug segment");
assert.equal(tagAnchors[3].textContent, "Not In Table", "unknown tags should remain unchanged");
assert.equal(tagAnchors[4].textContent, "新自定义标签", "untranslated hashtag-style tags should use slug word translation");
assert.equal(tagAnchors[5].textContent, "哥伦比亚女性", "known gendered nationality tags should be translated");
assert.equal(tagAnchors[6].textContent, "#untranslated-example", "untranslated hashtag-style tags should keep the original slug");
assert.equal(tagAnchors[7].textContent, "2", "tags pagination links should not be translated");
tagAnchors.forEach((anchor, index) => {
anchor.textContent = ["#asian", "#bigboobs", "#ebony", "Not In Table", "#new-custom-tag", "#colombiana", "#untranslated-example", "2"][index];
anchor.dataset = {};
anchor.title = "";
});
runTagsPageTranslationTest("/tags/female/");
assert.equal(tagAnchors[0].textContent, "亚洲", "tag category pages under /tags/ should also translate links");
assert.equal(tagAnchors[1].textContent, "大胸", "tag category pages should translate compact slugs too");
assert.equal(tagAnchors[2].textContent, "黑人", "tag category pages should translate /tag/slug/gender/ links");
assert.equal(tagAnchors[5].textContent, "哥伦比亚女性", "tag category pages should translate gendered nationality links");
assert.equal(tagAnchors[6].textContent, "#untranslated-example", "tag category pages should preserve unknown hashtag-style tags");
assert.equal(tagAnchors[7].textContent, "2", "tag category page pagination should keep page numbers");
const followingVars = new Map();
const followingAttrs = new Map();
const followingFakeDocument = {
@@ -272,12 +459,12 @@ vm.runInNewContext(source, {
cardHeightScale: 1.55,
modules: {
home: {
homeColumns: 7,
homeMinWidth: 190,
homeCard: { width: 174, height: 120 },
homeThumb: { width: 174, height: 98 },
},
followingList: {
followingListColumns: 7,
followingListMinWidth: 190,
followingListCard: { width: 190, height: 120 },
followingListThumb: { width: 188, height: 106 },
},
@@ -305,8 +492,8 @@ vm.runInNewContext(source, {
assert.equal(followingAttrs.get("data-tm-thumb-scale-following-list"), undefined, "followed-cams should not apply legacy cached list module");
assert.equal(followingAttrs.get("data-tm-thumb-scale-home"), undefined, "followed-cams should not apply legacy homepage cache");
assert.equal(followingVars.get("--tm-thumb-following-list-columns"), undefined);
assert.equal(followingVars.get("--tm-thumb-following-list-min-width"), undefined);
assert.equal(followingVars.get("--tm-thumb-following-list-card-height"), undefined);
assert.equal(followingVars.get("--tm-thumb-following-list-thumb-width"), undefined);
assert.equal(followingVars.get("--tm-thumb-following-list-thumb-height"), undefined);
assert.equal(followingVars.get("--tm-thumb-home-columns"), undefined);
assert.equal(followingVars.get("--tm-thumb-home-min-width"), undefined);