Compare commits

...

4 Commits

Author SHA1 Message Date
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
d078339288 Fix thumbnail scaling heights 2026-06-02 01:19:17 +08:00
ac8e88fe66 Prevent double scaling on home thumbnails 2026-06-01 13:35:45 +08:00
2 changed files with 1562 additions and 452 deletions

View File

File diff suppressed because it is too large Load Diff

View File

@@ -86,39 +86,44 @@ assert.match(
);
assert.match(
capturedCss,
/data-tm-thumb-scale-following-list="1"[\s\S]*--tm-thumb-following-list-width/,
"followed-cams list should be separated from homepage list sizing",
/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",
);
assert.match(
capturedCss,
/data-tm-thumb-scale-following-list="1"[\s\S]*grid-template-columns:\s*repeat\(auto-fill,\s*minmax\(var\(--tm-thumb-following-list-width\),\s*1fr\)\)\s*!important/,
"followed-cams list should keep the site's stretching grid alignment while using detected minimum width",
/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",
);
assert.match(
capturedCss,
/#main\.roomPage\s+ul\.list:has\(li\.roomCard\)\s+\.room_thumbnail_container[\s\S]*width:\s*var\(--tm-thumb-related-thumb-width\)\s*!important[\s\S]*height:\s*var\(--tm-thumb-related-height\)\s*!important/,
"room page related rooms thumbnail containers should use detected thumb width and height",
/#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",
);
assert.match(
capturedCss,
/\.BaseRoomContents\s+ul\.list:has\(li\.roomCard\)\s+\.room_thumbnail[\s\S]*width:\s*var\(--tm-thumb-related-thumb-width\)\s*!important[\s\S]*height:\s*var\(--tm-thumb-related-height\)\s*!important/,
"base room related thumbnails should use detected thumb width and height",
/\.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",
);
assert.match(
capturedCss,
/\.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 use detected width",
/\.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",
);
assert.match(
capturedCss,
/\.BaseRoomContents\s+ul\.RoomCardGrid\s+\.RoomCardThumbnail[\s\S]*width:\s*var\(--tm-thumb-related-thumb-width\)\s*!important[\s\S]*height:\s*var\(--tm-thumb-related-height\)\s*!important/,
"base room RoomCardGrid thumbnails should use detected thumb width and height",
/\.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",
);
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",
);
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",
);
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");
assert.match(
@@ -131,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/,
@@ -153,8 +163,8 @@ assert.match(
);
assert.match(
capturedCss,
/\.FollowedDropdown__room-image[\s\S]*width:\s*var\(--tm-thumb-follow-thumb-width\)\s*!important[\s\S]*height:\s*var\(--tm-thumb-follow-thumb-height\)\s*!important/,
"follow dropdown images should use detected original thumb width and height",
/\.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.match(
capturedCss,
@@ -169,11 +179,15 @@ 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, /CACHE_KEY/, "script should cache detected original sizes");
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.match(source, /localStorage\.setItem\(CACHE_KEY/, "script should save detected sizes in localStorage");
assert.match(source, /localStorage\.removeItem\(CACHE_KEY/, "script should clear cached sizes from the menu command");
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/, "script should not write detected sizes to localStorage");
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(
capturedCss,
@@ -226,23 +240,87 @@ 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 styleVars = new Map();
const attrs = new Map();
const cacheFakeDocument = {
documentElement: {
style: {
setProperty(name, value) {
styleVars.set(name, value);
},
},
setAttribute(name, value) {
attrs.set(name, value);
},
removeAttribute(name) {
attrs.delete(name);
const tagAnchors = [
{
href: "https://zh-hans.chaturbate.com/tag/asian/",
textContent: "#asian",
dataset: {},
title: "",
getAttribute(name) {
return name === "href" ? this.href : "";
},
},
head: {},
{
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() {} };
@@ -250,52 +328,20 @@ const cacheFakeDocument = {
getElementById() {
return null;
},
querySelectorAll() {
return [];
querySelectorAll(selector) {
return selector.includes("/tag") ? tagAnchors : [];
},
addEventListener() {},
};
const cacheStore = new Map([
["tm-thumb-scale:size-cache:v9", JSON.stringify({
schema: 9,
scale: 2,
cardHeightScale: 1.55,
modules: {
discover: {
discoverCard: { width: 182, height: 176 },
discoverThumb: { width: 180, height: 101 },
discoverTripleUl: { width: 1904, height: 550 },
discoverTripleArrow: { width: 35, height: 534 },
discoverDoubleUl: { width: 1904, height: 366 },
discoverDoubleArrow: { width: 35, height: 350 },
discoverSingleUl: { width: 2127, height: 182 },
discoverSingleArrow: { width: 35, height: 166 },
},
},
})],
]);
vm.runInNewContext(source, {
const runTagsPageTranslationTest = (pathname = "/tags/") => vm.runInNewContext(source, {
console,
document: cacheFakeDocument,
localStorage: {
getItem(key) {
return cacheStore.get(key) || null;
},
setItem(key, value) {
cacheStore.set(key, value);
},
removeItem(key) {
cacheStore.delete(key);
},
},
getComputedStyle() {
return {
getPropertyValue(name) {
return styleVars.get(name) || "";
},
};
document: tagFakeDocument,
location: {
href: `https://zh-hans.chaturbate.com${pathname}`,
pathname,
},
URL,
GM_addStyle() {
return { id: "", textContent: "", setAttribute() {} };
},
@@ -305,13 +351,32 @@ vm.runInNewContext(source, {
},
});
assert.equal(attrs.get("data-tm-thumb-scale-discover"), "1", "discover should be enabled directly from cached original sizes");
assert.equal(styleVars.get("--tm-thumb-discover-width"), "364px");
assert.equal(styleVars.get("--tm-thumb-discover-card-height"), "273px");
assert.equal(styleVars.get("--tm-thumb-discover-thumb-width"), "360px");
assert.equal(styleVars.get("--tm-thumb-discover-height"), "202px");
assert.equal(styleVars.get("--tm-thumb-discover-single-ul"), "282px");
assert.equal(styleVars.get("--tm-thumb-discover-triple-ul"), "853px");
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();
@@ -350,15 +415,19 @@ vm.runInNewContext(source, {
localStorage: {
getItem() {
return JSON.stringify({
schema: 9,
schema: 15,
scale: 2,
cardHeightScale: 1.55,
modules: {
home: {
homeColumns: 7,
homeCard: { width: 174, height: 120 },
homeThumb: { width: 174, height: 98 },
},
followingList: {
followingListColumns: 7,
followingListCard: { width: 190, height: 120 },
followingListThumb: { width: 188, height: 106 },
},
},
});
@@ -382,10 +451,10 @@ vm.runInNewContext(source, {
},
});
assert.equal(followingAttrs.get("data-tm-thumb-scale-following-list"), "1", "followed-cams should use its own cached list module");
assert.equal(followingAttrs.get("data-tm-thumb-scale-home"), undefined, "followed-cams should not apply homepage cache");
assert.equal(followingVars.get("--tm-thumb-following-list-width"), "380px");
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-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-width"), undefined);
assert.equal(followingVars.get("--tm-thumb-home-columns"), undefined);