diff --git a/Chaturbate/chaturbate-thumbnails-2x.user.js b/Chaturbate/chaturbate-thumbnails-2x.user.js
index 7d886df..18b0f51 100644
--- a/Chaturbate/chaturbate-thumbnails-2x.user.js
+++ b/Chaturbate/chaturbate-thumbnails-2x.user.js
@@ -1,7 +1,7 @@
// ==UserScript==
// @name Chaturbate 缩略图放大 2 倍
// @namespace https://chaturbate.com/
-// @version 0.13.9
+// @version 0.14.7
// @description 放大当前 Chaturbate 房间列表, 发现页轮播, 关注下拉与悬停预览缩略图, 并中文化标签页
// @match https://chaturbate.com/*
// @match https://*.chaturbate.com/*
@@ -26,7 +26,8 @@
*
* 1) 只做"站点原始尺寸 * 倍率"的放大.
* 不重写站点原有排版模型, 不用 object-fit / aspect-ratio / 自定义行高去替代站点布局.
- * 图片宽高和卡片宽度使用 THUMBNAIL_SCALE; 卡片整体高度使用 CARD_HEIGHT_SCALE.
+ * 响应式网格只放大最小列宽, 图片和卡片高度保持自动.
+ * 固定式模块放大卡片宽度, 图片宽高和轮播高度, 信息栏高度尽量保留原站尺寸.
*
* 2) 各模块必须自己探测原始值, 再乘倍率.
* 没有探测到原始值, 就不放大该模块; 不拿默认尺寸猜页面.
@@ -37,33 +38,39 @@
*
* 4) 不再缓存探测结果.
* Chaturbate 的列表宽度会随页面/窗口变化, 缓存旧尺寸容易污染新布局.
- * 当前版本每次页面加载都重新探测; 油猴菜单只用于清除旧版本遗留的 localStorage 缓存.
+ * 当前版本每次页面加载都重新探测; localStorage 只保存用户手动设置的两个倍率.
*
- * 5) 顶部"关注"弹窗的 FOLLOW_DROPDOWN_SHIFT_X 只是位置微调, 不属于缩略图倍率.
- *
- * 6) 标签页中文化只改页面显示文字, 不改链接地址.
+ * 5) 标签页中文化只改页面显示文字, 不改链接地址.
* 翻译表和分词词典里没有的标签保持原 #slug, 避免半英文保底文本影响观感.
*
- * 7) DEBUG 只用于控制台与 window.tmThumbScaleDebug() 诊断, 不应该弹窗或显示页面调试面板.
+ * 6) DEBUG 只用于控制台与 window.tmThumbScaleDebug() 诊断, 不应该弹窗或显示页面调试面板.
*/
(function () {
"use strict";
// ========== 配置项 ==========
- // 图片宽高和卡片宽度倍率: 2 表示按站点原始尺寸放大到 2 倍.
- const THUMBNAIL_SCALE = 2;
- // 卡片整体高度倍率: 信息栏, 边距等不需要和图片一样放大到 2 倍, 过大会产生空白.
- const CARD_HEIGHT_SCALE = 1.55;
- // 顶部"关注"弹出层水平偏移; 只调位置, 不参与缩略图放大倍率.
- const FOLLOW_DROPDOWN_SHIFT_X = 10;
+ // 响应式倍率默认值: 影响首页, 关注页列表, 频道页更多房间等 auto-fill 网格的最小列宽.
+ const DEFAULT_RESPONSIVE_SCALE = 2;
+ // 固定式倍率默认值: 影响发现页轮播, 顶部关注弹窗和悬浮预览这类固定尺寸区域.
+ const DEFAULT_FIXED_SCALE = 2;
+ // 控件步进: 页面输入框按 0.1 调整倍率.
+ const SCALE_STEP = 0.1;
+ // 恢复原始尺寸时两个倍率都回到 1.0.
+ const ORIGINAL_SCALE = 1;
// 调试开关: 控制控制台日志, html data-tm-* 标记和 window.tmThumbScaleDebug().
const DEBUG = true;
// ===========================
- const VER = "0.13.9";
- // 旧版本用过 localStorage 尺寸缓存.当前版本不再读写缓存, 避免旧探测值污染新布局.
+ const VER = "0.14.7";
+ // 旧版本用过 localStorage 尺寸缓存.当前版本不再读写尺寸缓存, 避免旧探测值污染新布局.
const LEGACY_CACHE_PREFIX = "tm-thumb-scale:size-cache:";
+ // 当前版本只保存用户手动调节的倍率, 不保存任何页面尺寸.
+ const SETTINGS_KEY = "tm-thumb-scale:settings:v1";
+ // 页面内倍率控制条的 id, 便于重复检查时复用同一个节点.
+ const CONTROL_ID = "tm-thumb-scale-controls";
+ // 频道页悬浮聊天窗隐藏标记.勾选设置时 JS 找到聊天窗后写入这个 data 属性, CSS 负责隐藏.
+ const ROOM_CHAT_OVERLAY_ATTR = "data-tm-thumb-room-chat-overlay";
// 标签页中文化表: key 使用站点 tag slug 或英文显示文本归一化后的结果.
// 只在 /tags/ 页面替换显示文字, 不改变 href, 所以点击后的站点行为保持原样.
const TAG_TRANSLATIONS = Object.freeze({
@@ -946,7 +953,7 @@
* home 模块:
* - 适用范围: 主页, 分类页等普通 #roomlist_root 房间列表.
* - 探测值: 站点 CSS 里的 grid 最小列宽 + 卡片原始高度 + 缩略图原始高度.
- * - 放大方式: 只把 grid 最小列宽乘 THUMBNAIL_SCALE; 列宽仍用 1fr 自适应.
+ * - 放大方式: 只把 grid 最小列宽乘响应式倍率; 列宽仍用 1fr 自适应.
* - 这样窗口缩小时会自动减列, 不会把固定列数硬压窄.
* - 注意: 关注页列表单独用 followingList 模块, 不和 home 共用探测结果.
*/
@@ -1044,7 +1051,7 @@
* related 模块:
* - 适用范围: 直播间页面底部"更多这样的房间".
* - 探测值: 卡片原始宽度 + 卡片原始高度 + 缩略图原始高度.
- * - 放大方式: 把卡片原始宽度乘 THUMBNAIL_SCALE 作为最小列宽, 列宽仍用 1fr 均分填满.
+ * - 放大方式: 把卡片原始宽度乘响应式倍率作为最小列宽, 列宽仍用 1fr 均分填满.
* - 这样右侧不会留空, 窗口变窄时也会自动减列.
*/
html[data-tm-thumb-scale-related="1"] .BaseRoomContents ul.RoomCardGrid,
@@ -1136,10 +1143,6 @@
max-width: none !important;
box-sizing: border-box !important;
}
- html[data-tm-thumb-scale-follow="1"] .HeaderNavBar__following .FollowedDropdown {
- /* 只做用户要求的右移微调, 不参与缩略图尺寸计算. */
- transform: translateX(${FOLLOW_DROPDOWN_SHIFT_X}px) !important;
- }
/*
* 悬停"关注"星星等出现的浮层预览(多为 react-tooltip, portal 挂在 body)
* 仅放大直播间缩略图域名, 避免误伤图标类小图
@@ -1162,8 +1165,8 @@
* 1. li 卡片宽高;
* 2. 缩略图容器高度;
* 3. triple/double/single 三种轮播 ul 高度.
- * - 放大方式: 图片宽高和卡片宽度乘 THUMBNAIL_SCALE.
- * - 卡片高度: 只把缩略图区域乘 THUMBNAIL_SCALE, 用户名/人数等信息栏保留原始高度.
+ * - 放大方式: 图片宽高和卡片宽度乘固定式倍率.
+ * - 卡片高度: 只把缩略图区域乘固定式倍率, 用户名/人数等信息栏保留原始高度.
* - 轮播高度: triple/double/single 按 3/2/1 行卡片高度重新计算, 保留原始行间距.
* - 箭头高度: 跟对应轮播高度同步, 避免右侧箭头与轮播内容错位.
* - 启用条件: 卡片和缩略图探测到就先启用; 某种行数的轮播不存在时跳过对应高度变量.
@@ -1414,6 +1417,111 @@
max-width: none !important;
box-sizing: border-box !important;
}
+
+ /*
+ * 页面倍率控制条:
+ * - 这是用户交互控件, 不是调试面板.
+ * - 响应式倍率和固定式倍率都通过 JS 写回 localStorage, 然后重新计算 CSS 变量.
+ * - 控件尽量贴边且小尺寸, 避免遮挡主内容.
+ */
+ #tm-thumb-scale-controls {
+ position: fixed !important;
+ right: 14px !important;
+ bottom: 14px !important;
+ z-index: 2147483647 !important;
+ display: flex !important;
+ align-items: center !important;
+ gap: 6px !important;
+ padding: 6px !important;
+ border: 1px solid rgba(255, 116, 28, 0.34) !important;
+ border-radius: 8px !important;
+ background: rgba(255, 255, 255, 0.9) !important;
+ box-shadow: 0 8px 24px rgba(31, 24, 18, 0.18), 0 1px 0 rgba(255, 255, 255, 0.85) inset !important;
+ color: #2f2f2f !important;
+ font: 12px/1.2 Arial, sans-serif !important;
+ backdrop-filter: blur(8px) saturate(1.15) !important;
+ -webkit-backdrop-filter: blur(8px) saturate(1.15) !important;
+ }
+ #tm-thumb-scale-controls label {
+ display: inline-flex !important;
+ align-items: center !important;
+ gap: 6px !important;
+ margin: 0 !important;
+ padding: 4px 6px !important;
+ border: 1px solid rgba(0, 0, 0, 0.08) !important;
+ border-radius: 6px !important;
+ background: rgba(255, 255, 255, 0.72) !important;
+ white-space: nowrap !important;
+ }
+ #tm-thumb-scale-controls .tm-thumb-scale-label {
+ color: #5d5149 !important;
+ font-weight: 700 !important;
+ letter-spacing: 0 !important;
+ }
+ #tm-thumb-scale-controls input {
+ width: 50px !important;
+ min-width: 50px !important;
+ height: 26px !important;
+ margin: 0 !important;
+ padding: 2px 5px !important;
+ border: 1px solid rgba(255, 116, 28, 0.42) !important;
+ border-radius: 5px !important;
+ background: #fffdfb !important;
+ box-sizing: border-box !important;
+ color: #1f1f1f !important;
+ font: 700 13px/1.2 Arial, sans-serif !important;
+ text-align: center !important;
+ outline: none !important;
+ }
+ #tm-thumb-scale-controls input:focus {
+ border-color: #f47321 !important;
+ box-shadow: 0 0 0 2px rgba(244, 115, 33, 0.16) !important;
+ }
+ #tm-thumb-scale-controls button {
+ height: 34px !important;
+ margin: 0 !important;
+ padding: 0 10px !important;
+ border: 1px solid rgba(244, 115, 33, 0.38) !important;
+ border-radius: 6px !important;
+ background: rgba(255, 247, 241, 0.96) !important;
+ color: #b84d10 !important;
+ cursor: pointer !important;
+ font: 700 12px/1.2 Arial, sans-serif !important;
+ white-space: nowrap !important;
+ }
+ #tm-thumb-scale-controls button:hover {
+ background: #fff0e5 !important;
+ border-color: rgba(244, 115, 33, 0.62) !important;
+ }
+ #tm-thumb-scale-controls button:active {
+ transform: translateY(1px) !important;
+ }
+ #tm-thumb-scale-controls .tm-thumb-scale-check {
+ cursor: pointer !important;
+ }
+ #tm-thumb-scale-controls input[type="checkbox"] {
+ width: 15px !important;
+ min-width: 15px !important;
+ height: 15px !important;
+ margin: 0 !important;
+ padding: 0 !important;
+ accent-color: #f47321 !important;
+ cursor: pointer !important;
+ }
+ html[data-tm-thumb-scale-auto-hide-room-chat="1"] [data-tm-thumb-room-chat-overlay="1"] {
+ display: none !important;
+ visibility: hidden !important;
+ pointer-events: none !important;
+ }
+ @media (max-width: 560px) {
+ #tm-thumb-scale-controls {
+ right: 8px !important;
+ left: 8px !important;
+ bottom: 8px !important;
+ justify-content: center !important;
+ flex-wrap: wrap !important;
+ }
+ }
`;
const STYLE_ID = "tm-thumb-scale-style";
@@ -1445,6 +1553,16 @@
tooltip: false,
discover: false,
};
+ // 保存每个模块第一次探测到的原始尺寸.倍率变化时只用这些原始值重算 CSS 变量.
+ const originalMetrics = {
+ home: null,
+ followingList: null,
+ followedRecommendations: null,
+ related: null,
+ follow: null,
+ tooltip: null,
+ discover: null,
+ };
const setDebugAttr = (name, value) => {
// 调试属性写到 html 上, 方便在 Elements 面板确认脚本是否运行.
if (!DEBUG) return;
@@ -1453,15 +1571,52 @@
} catch (_) {}
};
- const scaledPx = (value) => `${Math.round(value * THUMBNAIL_SCALE)}px`;
- const scaledCardHeightPx = (value) => `${Math.round(value * CARD_HEIGHT_SCALE)}px`;
- const scaledCardHeightFromThumbPx = (cardHeight, thumbHeight) => {
- const nonThumbHeight = Math.max(0, cardHeight - thumbHeight);
- return `${Math.round((thumbHeight * THUMBNAIL_SCALE) + nonThumbHeight)}px`;
+ const normalizeScale = (value, fallback) => {
+ // 倍率只保留一位小数; 过小会变成缩小, 过大容易把页面挤爆, 所以限制在 1.0 到 4.0.
+ const number = Number.parseFloat(value);
+ const safe = Number.isFinite(number) && number >= ORIGINAL_SCALE ? number : fallback;
+ const rounded = Math.round(safe / SCALE_STEP) * SCALE_STEP;
+ return Math.min(4, Math.max(ORIGINAL_SCALE, Number(rounded.toFixed(1))));
};
- const scaledCardHeightFromThumbValue = (cardHeight, thumbHeight) => {
+ const defaultScaleSettings = () => ({
+ responsive: normalizeScale(DEFAULT_RESPONSIVE_SCALE, DEFAULT_RESPONSIVE_SCALE),
+ fixed: normalizeScale(DEFAULT_FIXED_SCALE, DEFAULT_FIXED_SCALE),
+ hideRoomChat: false,
+ });
+ const loadScaleSettings = () => {
+ const defaults = defaultScaleSettings();
+ try {
+ if (typeof localStorage === "undefined") return defaults;
+ const parsed = JSON.parse(localStorage.getItem(SETTINGS_KEY) || "{}");
+ return {
+ responsive: normalizeScale(parsed.responsive, defaults.responsive),
+ fixed: normalizeScale(parsed.fixed, defaults.fixed),
+ hideRoomChat: Boolean(parsed.hideRoomChat),
+ };
+ } catch (_) {
+ return defaults;
+ }
+ };
+ const saveScaleSettings = () => {
+ try {
+ if (typeof localStorage !== "undefined") {
+ localStorage.setItem(SETTINGS_KEY, JSON.stringify(scaleSettings));
+ }
+ } catch (_) {}
+ };
+ let scaleSettings = loadScaleSettings();
+ const responsiveScale = () => scaleSettings.responsive;
+ const fixedScale = () => scaleSettings.fixed;
+ const shouldHideRoomChat = () => Boolean(scaleSettings.hideRoomChat);
+ const scaledPx = (value, scale = fixedScale()) => `${Math.round(value * scale)}px`;
+ const scaledResponsivePx = (value) => scaledPx(value, responsiveScale());
+ const scaledCardHeightFromThumbPx = (cardHeight, thumbHeight, scale = fixedScale()) => {
const nonThumbHeight = Math.max(0, cardHeight - thumbHeight);
- return Math.round((thumbHeight * THUMBNAIL_SCALE) + nonThumbHeight);
+ return `${Math.round((thumbHeight * scale) + nonThumbHeight)}px`;
+ };
+ const scaledCardHeightFromThumbValue = (cardHeight, thumbHeight, scale = fixedScale()) => {
+ const nonThumbHeight = Math.max(0, cardHeight - thumbHeight);
+ return Math.round((thumbHeight * scale) + nonThumbHeight);
};
const scaledStackHeightFromCardValue = (stackHeight, cardHeight, thumbHeight, rows) => {
if (
@@ -1477,10 +1632,6 @@
const originalNonCardSpace = Math.max(0, stackHeight - (cardHeight * rows));
return Math.round((scaledCardHeightFromThumbValue(cardHeight, thumbHeight) * rows) + originalNonCardSpace);
};
- const scaledCardHeightFromThumbWithScalePx = (cardHeight, thumbHeight, scale) => {
- const nonThumbHeight = Math.max(0, cardHeight - thumbHeight);
- return `${Math.round((thumbHeight * scale) + nonThumbHeight)}px`;
- };
const rootStyle = () => document.documentElement && document.documentElement.style;
const isFollowingListPage = () => {
try {
@@ -1498,6 +1649,15 @@
return false;
}
};
+ const isRoomPage = () => {
+ try {
+ const path = typeof location === "undefined" ? "" : location.pathname;
+ if (!/^\/[^/]+\/?$/i.test(path)) return false;
+ return !/^\/(?:$|discover|tags|followed-cams|followed|accounts|auth|login|support|privacy|terms|static)\b/i.test(path);
+ } catch (_) {
+ return false;
+ }
+ };
const normalizeTagKey = (value) => String(value || "")
.trim()
.replace(/^#/, "")
@@ -1629,23 +1789,18 @@
}, delay);
};
// 所有 CSS 变量都在这里统一乘倍率.调用方传入的必须是页面原始尺寸.
- const setVar = (name, value) => {
+ const setVar = (name, value, scale = fixedScale()) => {
const style = rootStyle();
if (!style || !Number.isFinite(value) || value <= 0) return false;
- style.setProperty(name, scaledPx(value));
+ style.setProperty(name, scaledPx(value, scale));
return true;
};
- // 只给 card 整体高度使用.图片宽高仍然走 setVar(), 继续乘 THUMBNAIL_SCALE.
- const setCardHeightVar = (name, value) => {
- const style = rootStyle();
- if (!style || !Number.isFinite(value) || value <= 0) return false;
- style.setProperty(name, scaledCardHeightPx(value));
- return true;
- };
- const setCardHeightFromThumbVar = (name, cardHeight, thumbHeight) => {
+ const setResponsiveVar = (name, value) => setVar(name, value, responsiveScale());
+ const setFixedVar = (name, value) => setVar(name, value, fixedScale());
+ const setCardHeightFromThumbVar = (name, cardHeight, thumbHeight, scale = fixedScale()) => {
const style = rootStyle();
if (!style || !Number.isFinite(cardHeight) || !Number.isFinite(thumbHeight) || cardHeight <= 0 || thumbHeight <= 0) return false;
- style.setProperty(name, scaledCardHeightFromThumbPx(cardHeight, thumbHeight));
+ style.setProperty(name, scaledCardHeightFromThumbPx(cardHeight, thumbHeight, scale));
return true;
};
const setDiscoverStackHeightVars = (ulName, arrowName, stackHeight, cardHeight, thumbHeight, rows) => {
@@ -1675,9 +1830,9 @@
const originalMinWidth = Number.isFinite(metrics.minColumn) && metrics.minColumn > 0
? metrics.minColumn
: cardWidth;
- style.setProperty(minWidthName, scaledPx(originalMinWidth));
- style.setProperty(cardHeightName, scaledCardHeightFromThumbPx(cardHeight, thumbHeight));
- style.setProperty(thumbHeightName, scaledPx(thumbHeight));
+ style.setProperty(minWidthName, scaledResponsivePx(originalMinWidth));
+ style.setProperty(cardHeightName, "auto");
+ style.setProperty(thumbHeightName, scaledResponsivePx(thumbHeight));
return true;
};
const clearFollowedRecommendationMarkers = () => {
@@ -1788,12 +1943,134 @@
const cardRect = rectOf(marked.cards[0]);
const thumbRect = rectOf(marked.thumbs[0]);
if (!isRect(cardRect) || !isRect(thumbRect)) return false;
- style.setProperty("--tm-thumb-followed-recommendations-width", scaledPx(cardRect.width));
+ style.setProperty("--tm-thumb-followed-recommendations-width", scaledResponsivePx(cardRect.width));
style.setProperty("--tm-thumb-followed-recommendations-card-height", "auto");
- style.setProperty("--tm-thumb-followed-recommendations-thumb-width", scaledPx(thumbRect.width));
+ style.setProperty("--tm-thumb-followed-recommendations-thumb-width", scaledResponsivePx(thumbRect.width));
style.setProperty("--tm-thumb-followed-recommendations-thumb-height", "auto");
return true;
};
+ const applyHomeMetrics = (metrics) => setListScaleVars(
+ "--tm-thumb-home-min-width",
+ "--tm-thumb-home-card-height",
+ "--tm-thumb-home-thumb-height",
+ metrics && metrics.grid,
+ metrics && metrics.cardWidth,
+ metrics && metrics.cardHeight,
+ metrics && metrics.thumbHeight,
+ );
+ const applyFollowingListMetrics = (metrics) => setListScaleVars(
+ "--tm-thumb-following-list-min-width",
+ "--tm-thumb-following-list-card-height",
+ "--tm-thumb-following-list-thumb-height",
+ metrics && metrics.grid,
+ metrics && metrics.cardWidth,
+ metrics && metrics.cardHeight,
+ metrics && metrics.thumbHeight,
+ );
+ const applyFollowedRecommendationsMetrics = (metrics) => {
+ const style = rootStyle();
+ if (!style || !metrics || !Number.isFinite(metrics.cardWidth) || metrics.cardWidth <= 0) return false;
+ style.setProperty("--tm-thumb-followed-recommendations-width", scaledResponsivePx(metrics.cardWidth));
+ style.setProperty("--tm-thumb-followed-recommendations-card-height", "auto");
+ style.setProperty("--tm-thumb-followed-recommendations-thumb-width", scaledResponsivePx(metrics.thumbWidth || metrics.cardWidth));
+ style.setProperty("--tm-thumb-followed-recommendations-thumb-height", "auto");
+ return true;
+ };
+ const applyRelatedMetrics = (metrics) => ([
+ setResponsiveVar("--tm-thumb-related-width", metrics && metrics.cardWidth),
+ (() => {
+ const style = rootStyle();
+ if (!style) return false;
+ style.setProperty("--tm-thumb-related-card-height", "auto");
+ return true;
+ })(),
+ setResponsiveVar("--tm-thumb-related-height", metrics && metrics.thumbHeight),
+ ].every(Boolean));
+ const applyFollowMetrics = (metrics) => {
+ const style = rootStyle();
+ const ready = [
+ setFixedVar("--tm-thumb-follow-card-width", metrics && metrics.cardWidth),
+ setCardHeightFromThumbVar("--tm-thumb-follow-card-height", metrics && metrics.cardHeight, metrics && metrics.thumbHeight, fixedScale()),
+ setFixedVar("--tm-thumb-follow-thumb-height", metrics && metrics.thumbHeight),
+ ].every(Boolean);
+ if (ready && style) {
+ style.setProperty("--tm-thumb-follow-width", `calc(${Math.round(metrics.cardWidth * fixedScale() * 2)}px + 2em)`);
+ }
+ return ready;
+ };
+ const adjustFollowDropdownPosition = (stage = "follow-position") => {
+ /*
+ * 原站负责计算顶部"关注"弹窗的锚点位置.
+ * 脚本只在放大后发现左侧越出视口时, 临时补一个 margin-left.
+ * 这样正常情况下完全保持原站定位, 只有越界时才向右推回屏幕内.
+ */
+ try {
+ const dropdown = firstElement(".FollowedDropdown");
+ if (!dropdown || !dropdown.getBoundingClientRect || !dropdown.style) return false;
+ dropdown.style.removeProperty("margin-left");
+ const rect = dropdown.getBoundingClientRect();
+ const viewportPadding = 8;
+ const shift = Math.ceil(viewportPadding - rect.left);
+ if (shift > 0) {
+ dropdown.style.setProperty("margin-left", `${shift}px`, "important");
+ if (DEBUG) log("follow dropdown shifted to avoid left overflow", { stage, left: rect.left, shift });
+ return true;
+ }
+ return false;
+ } catch (_) {
+ return false;
+ }
+ };
+ const applyTooltipMetrics = (metrics) => ([
+ setFixedVar("--tm-thumb-tooltip-width", metrics && metrics.width),
+ setFixedVar("--tm-thumb-tooltip-height", metrics && metrics.height),
+ ].every(Boolean));
+ const applyDiscoverMetrics = (metrics) => {
+ if (!metrics) return false;
+ const discoverWidthReady = setFixedVar("--tm-thumb-discover-width", metrics.cardWidth);
+ const discoverCardHeightReady = setCardHeightFromThumbVar("--tm-thumb-discover-card-height", metrics.cardHeight, metrics.thumbHeight, fixedScale());
+ const discoverThumbWidthReady = setFixedVar("--tm-thumb-discover-thumb-width", metrics.thumbWidth);
+ const discoverHeightReady = setFixedVar("--tm-thumb-discover-height", metrics.thumbHeight);
+ const discoverTripleReady = setDiscoverStackHeightVars(
+ "--tm-thumb-discover-triple-ul",
+ "--tm-thumb-discover-triple-arrow",
+ metrics.tripleHeight,
+ metrics.cardHeight,
+ metrics.thumbHeight,
+ 3,
+ );
+ const discoverDoubleReady = setDiscoverStackHeightVars(
+ "--tm-thumb-discover-double-ul",
+ "--tm-thumb-discover-double-arrow",
+ metrics.doubleHeight,
+ metrics.cardHeight,
+ metrics.thumbHeight,
+ 2,
+ );
+ const discoverSingleReady = setDiscoverStackHeightVars(
+ "--tm-thumb-discover-single-ul",
+ "--tm-thumb-discover-single-arrow",
+ metrics.singleHeight,
+ metrics.cardHeight,
+ metrics.thumbHeight,
+ 1,
+ );
+ return discoverWidthReady && discoverCardHeightReady && discoverThumbWidthReady && discoverHeightReady
+ && discoverTripleReady && discoverDoubleReady && discoverSingleReady;
+ };
+ const reapplyScaleVars = (stage = "scale-change") => {
+ // 倍率变化时不重新读取 DOM, 只使用第一次探测到的原始值重新写 CSS 变量.
+ if (originalMetrics.home) moduleReady.home = applyHomeMetrics(originalMetrics.home);
+ if (originalMetrics.followingList) moduleReady.followingList = applyFollowingListMetrics(originalMetrics.followingList);
+ if (originalMetrics.followedRecommendations) moduleReady.followedRecommendations = applyFollowedRecommendationsMetrics(originalMetrics.followedRecommendations);
+ if (originalMetrics.related) moduleReady.related = applyRelatedMetrics(originalMetrics.related);
+ if (originalMetrics.follow) moduleReady.follow = applyFollowMetrics(originalMetrics.follow);
+ adjustFollowDropdownPosition(stage);
+ if (originalMetrics.tooltip) moduleReady.tooltip = applyTooltipMetrics(originalMetrics.tooltip);
+ if (originalMetrics.discover) moduleReady.discover = applyDiscoverMetrics(originalMetrics.discover);
+ Object.keys(moduleReady).forEach((name) => setModuleReady(name, moduleReady[name]));
+ reportDiagnostics(stage);
+ };
// 每个模块的 CSS 都由 html[data-tm-thumb-scale-模块名="1"] 控制.
// 未探测到原始值时不设置开关, 对应模块完全不放大, 避免用错误尺寸猜页面.
const setModuleReady = (name, ready) => {
@@ -1829,6 +2106,216 @@
}
} catch (_) {}
};
+ const clearRoomChatOverlayMarkers = () => {
+ try {
+ if (!document.querySelectorAll) return;
+ document.querySelectorAll(`[${ROOM_CHAT_OVERLAY_ATTR}]`).forEach((el) => {
+ el.removeAttribute(ROOM_CHAT_OVERLAY_ATTR);
+ });
+ } catch (_) {}
+ };
+ const isSmallRoomChatOverlayRect = (rect) => {
+ if (!rect) return false;
+ // 只允许截图里这种浮在视频上的小聊天窗; 直播主容器通常比这个范围大很多.
+ const maxWidth = Math.min(760, Math.max(560, window.innerWidth * 0.86));
+ const maxHeight = Math.min(720, Math.max(360, window.innerHeight * 0.82));
+ return rect.width >= 260
+ && rect.height >= 160
+ && rect.width <= maxWidth
+ && rect.height <= maxHeight
+ && (rect.width * rect.height) <= 520000;
+ };
+ const roomChatOverlayScore = (el, anchor) => {
+ try {
+ if (!el || !el.textContent || !el.getBoundingClientRect) return 0;
+ const rect = el.getBoundingClientRect();
+ if (!isSmallRoomChatOverlayRect(rect)) return 0;
+ if (anchor && !el.contains(anchor)) return 0;
+ const text = el.textContent.replace(/\s+/g, " ").trim();
+ let score = 0;
+ if (anchor) score += 3;
+ if (/发送消息|send message/i.test(text)) score += 3;
+ if (/规则[::]/.test(text)) score += 3;
+ if (/聊天/.test(text)) score += 2;
+ if (/直播间私信/.test(text)) score += 3;
+ if (/打赏/.test(text)) score += 2;
+ if (/Notice:|The Menu|Welcome/i.test(text)) score += 2;
+ if (/Ctrl\+S|Ctrl\+\//i.test(text)) score += 2;
+ if (el.querySelector && el.querySelector('button, [role="button"], input, textarea, [contenteditable="true"]')) score += 1;
+ return score;
+ } catch (_) {
+ return 0;
+ }
+ };
+ const roomChatInputs = () => {
+ try {
+ return Array.from(document.querySelectorAll('input, textarea, [contenteditable="true"]'))
+ .filter((el) => {
+ const text = `${el.getAttribute("placeholder") || ""} ${el.getAttribute("data-placeholder") || ""} ${el.getAttribute("aria-label") || ""} ${el.value || ""} ${el.textContent || ""}`;
+ return /发送消息|send message/i.test(text);
+ });
+ } catch (_) {
+ return [];
+ }
+ };
+ const findRoomChatOverlay = () => {
+ if (!isRoomPage() || !document.querySelectorAll) return null;
+ try {
+ // 保存后的 veliasai 页面显示, 频道页浮动聊天窗有稳定标识:
+ // #draggableCanvasChatWindow / [data-testid="chat-floating-window"].
+ // 优先用这个精确节点, 并仍做小窗尺寸检查, 避免误伤直播主容器.
+ const direct = firstElement('#draggableCanvasChatWindow, [data-testid="chat-floating-window"]');
+ if (direct && isSmallRoomChatOverlayRect(direct.getBoundingClientRect())) return direct;
+ const candidates = [];
+ roomChatInputs().forEach((anchor) => {
+ let current = anchor.parentElement;
+ while (current && current !== document.body && current !== document.documentElement) {
+ const score = roomChatOverlayScore(current, anchor);
+ if (score >= 8) candidates.push({ el: current, score });
+ current = current.parentElement;
+ }
+ });
+ Array.from(document.querySelectorAll("div,section,aside,form")).forEach((el) => {
+ const score = roomChatOverlayScore(el, null);
+ if (score >= 7) candidates.push({ el, score });
+ });
+ candidates.sort((a, b) => {
+ const ar = a.el.getBoundingClientRect();
+ const br = b.el.getBoundingClientRect();
+ const areaDiff = (ar.width * ar.height) - (br.width * br.height);
+ if (b.score !== a.score && Math.abs(areaDiff) < 8000) return b.score - a.score;
+ return areaDiff;
+ });
+ return candidates[0] && candidates[0].el;
+ } catch (_) {
+ return null;
+ }
+ };
+ const inspectRoomChatOverlay = () => {
+ const rectInfo = (el) => {
+ if (!el || !el.getBoundingClientRect) return null;
+ const rect = el.getBoundingClientRect();
+ return {
+ x: Math.round(rect.x),
+ y: Math.round(rect.y),
+ width: Math.round(rect.width),
+ height: Math.round(rect.height),
+ };
+ };
+ const nodeInfo = (el, score) => ({
+ score,
+ tag: el && el.tagName,
+ id: el && el.id,
+ className: el && String(el.className || "").slice(0, 160),
+ rect: rectInfo(el),
+ text: el && (el.textContent || "").replace(/\s+/g, " ").trim().slice(0, 240),
+ });
+ const candidates = [];
+ try {
+ roomChatInputs().forEach((anchor) => {
+ let current = anchor.parentElement;
+ while (current && current !== document.body && current !== document.documentElement) {
+ const score = roomChatOverlayScore(current, anchor);
+ if (score > 0) candidates.push(nodeInfo(current, score));
+ current = current.parentElement;
+ }
+ });
+ Array.from(document.querySelectorAll("div,section,aside,form")).forEach((el) => {
+ const score = roomChatOverlayScore(el, null);
+ if (score > 0) candidates.push(nodeInfo(el, score));
+ });
+ } catch (_) {}
+ const selected = findRoomChatOverlay();
+ const report = {
+ ver: VER,
+ href: typeof location === "undefined" ? "" : location.href,
+ enabled: shouldHideRoomChat(),
+ isRoomPage: isRoomPage(),
+ selected: selected ? nodeInfo(selected, roomChatOverlayScore(selected, null)) : null,
+ candidates: candidates
+ .sort((a, b) => (b.score - a.score) || ((a.rect.width * a.rect.height) - (b.rect.width * b.rect.height)))
+ .slice(0, 20),
+ };
+ console.log("[tm-thumb-scale room chat]", report);
+ return report;
+ };
+ const applyRoomChatAutoHide = (stage = "room-chat-auto-hide") => {
+ try {
+ setModuleReady("autoHideRoomChat", shouldHideRoomChat() && isRoomPage());
+ if (!shouldHideRoomChat() || !isRoomPage()) {
+ clearRoomChatOverlayMarkers();
+ return false;
+ }
+ const overlay = findRoomChatOverlay();
+ if (!overlay) return false;
+ overlay.setAttribute(ROOM_CHAT_OVERLAY_ATTR, "1");
+ if (DEBUG) log("room chat overlay hidden", { stage });
+ return true;
+ } catch (_) {
+ return false;
+ }
+ };
+ const updateControlValues = (root) => {
+ if (!root || !root.querySelector) return;
+ const responsiveInput = root.querySelector('[data-tm-thumb-control="responsive"]');
+ const fixedInput = root.querySelector('[data-tm-thumb-control="fixed"]');
+ const hideChatInput = root.querySelector('[data-tm-thumb-control="hide-room-chat"]');
+ if (responsiveInput) responsiveInput.value = responsiveScale().toFixed(1);
+ if (fixedInput) fixedInput.value = fixedScale().toFixed(1);
+ if (hideChatInput) hideChatInput.checked = shouldHideRoomChat();
+ };
+ const applyScaleSettings = (next, stage) => {
+ scaleSettings = {
+ responsive: normalizeScale(next && next.responsive, responsiveScale()),
+ fixed: normalizeScale(next && next.fixed, fixedScale()),
+ hideRoomChat: next && Object.prototype.hasOwnProperty.call(next, "hideRoomChat")
+ ? Boolean(next.hideRoomChat)
+ : shouldHideRoomChat(),
+ };
+ saveScaleSettings();
+ updateControlValues(document.getElementById(CONTROL_ID));
+ applyRoomChatAutoHide(stage);
+ reapplyScaleVars(stage);
+ scheduleMeasure(stage, 50);
+ };
+ const ensureScaleControls = () => {
+ if (!document.body || !document.createElement) return;
+ let root = document.getElementById(CONTROL_ID);
+ if (!root) {
+ root = document.createElement("div");
+ root.id = CONTROL_ID;
+ root.innerHTML = [
+ '',
+ '',
+ '',
+ '',
+ ].join("");
+ if (root.addEventListener) {
+ root.addEventListener("input", (event) => {
+ const target = event && event.target;
+ if (!target || !target.getAttribute) return;
+ const kind = target.getAttribute("data-tm-thumb-control");
+ if (kind === "responsive") {
+ applyScaleSettings({ responsive: target.value, fixed: fixedScale(), hideRoomChat: shouldHideRoomChat() }, "control-responsive");
+ } else if (kind === "fixed") {
+ applyScaleSettings({ responsive: responsiveScale(), fixed: target.value, hideRoomChat: shouldHideRoomChat() }, "control-fixed");
+ }
+ });
+ root.addEventListener("change", (event) => {
+ const target = event && event.target;
+ if (!target || !target.getAttribute || target.getAttribute("data-tm-thumb-control") !== "hide-room-chat") return;
+ applyScaleSettings({ responsive: responsiveScale(), fixed: fixedScale(), hideRoomChat: target.checked }, "control-hide-room-chat");
+ });
+ root.addEventListener("click", (event) => {
+ const target = event && event.target;
+ if (!target || !target.getAttribute || target.getAttribute("data-tm-thumb-control") !== "reset") return;
+ applyScaleSettings({ responsive: ORIGINAL_SCALE, fixed: ORIGINAL_SCALE, hideRoomChat: shouldHideRoomChat() }, "control-reset-original");
+ });
+ }
+ if (document.body.appendChild) document.body.appendChild(root);
+ }
+ updateControlValues(root);
+ };
const rectOf = (el) => {
// getBoundingClientRect 能拿到 CSS 布局后的实际渲染尺寸.
if (!el || !el.getBoundingClientRect) return null;
@@ -1995,34 +2482,40 @@
if (!moduleReady.home && !followingListPage) {
// home 依赖当前页面的自然列数和原始高度; 每次加载重新探测.
- moduleReady.home = setListScaleVars(
- "--tm-thumb-home-min-width",
- "--tm-thumb-home-card-height",
- "--tm-thumb-home-thumb-height",
- measured.homeMetrics,
- measured.homeCard && measured.homeCard.width,
- measured.homeCard && measured.homeCard.height,
- measured.homeThumb && measured.homeThumb.height,
- );
+ const metrics = {
+ grid: measured.homeMetrics,
+ cardWidth: measured.homeCard && measured.homeCard.width,
+ cardHeight: measured.homeCard && measured.homeCard.height,
+ thumbHeight: measured.homeThumb && measured.homeThumb.height,
+ };
+ moduleReady.home = applyHomeMetrics(metrics);
+ if (moduleReady.home) originalMetrics.home = metrics;
setModuleReady("home", moduleReady.home);
}
if (!moduleReady.followingList && followingListPage) {
// followingList 单独探测, 避免关注页拿到首页的列数.
- moduleReady.followingList = setListScaleVars(
- "--tm-thumb-following-list-min-width",
- "--tm-thumb-following-list-card-height",
- "--tm-thumb-following-list-thumb-height",
- measured.followingListMetrics,
- measured.followingListCard && measured.followingListCard.width,
- measured.followingListCard && measured.followingListCard.height,
- measured.followingListThumb && measured.followingListThumb.height,
- );
+ const metrics = {
+ grid: measured.followingListMetrics,
+ cardWidth: measured.followingListCard && measured.followingListCard.width,
+ cardHeight: measured.followingListCard && measured.followingListCard.height,
+ thumbHeight: measured.followingListThumb && measured.followingListThumb.height,
+ };
+ moduleReady.followingList = applyFollowingListMetrics(metrics);
+ if (moduleReady.followingList) originalMetrics.followingList = metrics;
setModuleReady("followingList", moduleReady.followingList);
}
if (!moduleReady.followedRecommendations && followingListPage) {
moduleReady.followedRecommendations = setFollowedRecommendationVars(followedRecommendations);
+ if (moduleReady.followedRecommendations) {
+ const cardRect = rectOf(followedRecommendations.cards[0]);
+ const thumbRect = rectOf(followedRecommendations.thumbs[0]);
+ originalMetrics.followedRecommendations = {
+ cardWidth: cardRect && cardRect.width,
+ thumbWidth: thumbRect && thumbRect.width,
+ };
+ }
setModuleReady("followedRecommendations", moduleReady.followedRecommendations);
} else if (moduleReady.followedRecommendations && followingListPage) {
setModuleReady("followedRecommendations", true);
@@ -2030,76 +2523,59 @@
if (!moduleReady.related) {
// related 同时依赖卡片宽度, 卡片高度和缩略图高度; 三者缺一不可.
- moduleReady.related = [
- setVar("--tm-thumb-related-width", measured.relatedCard && measured.relatedCard.width),
- setCardHeightFromThumbVar("--tm-thumb-related-card-height", measured.relatedCard && measured.relatedCard.height, measured.relatedThumb && measured.relatedThumb.height),
- setVar("--tm-thumb-related-height", measured.relatedThumb && measured.relatedThumb.height),
- ].every(Boolean);
+ const metrics = {
+ cardWidth: measured.relatedCard && measured.relatedCard.width,
+ cardHeight: measured.relatedCard && measured.relatedCard.height,
+ thumbHeight: measured.relatedThumb && measured.relatedThumb.height,
+ };
+ moduleReady.related = applyRelatedMetrics(metrics);
+ if (moduleReady.related) originalMetrics.related = metrics;
setModuleReady("related", moduleReady.related);
}
if (!moduleReady.follow) {
// follow 的总弹窗宽度按两列计算: 单卡原始宽度 * 倍率 * 2 + 2em 间距余量.
- const followReady = [
- setVar("--tm-thumb-follow-card-width", measured.followCard && measured.followCard.width),
- setCardHeightFromThumbVar("--tm-thumb-follow-card-height", measured.followCard && measured.followCard.height, measured.followThumb && measured.followThumb.height),
- setVar("--tm-thumb-follow-thumb-height", measured.followThumb && measured.followThumb.height),
- ].every(Boolean);
- moduleReady.follow = followReady;
- const style = rootStyle();
- if (followReady && style) {
- style.setProperty("--tm-thumb-follow-width", `calc(${Math.round(measured.followCard.width * THUMBNAIL_SCALE * 2)}px + 2em)`);
- }
+ const metrics = {
+ cardWidth: measured.followCard && measured.followCard.width,
+ cardHeight: measured.followCard && measured.followCard.height,
+ thumbHeight: measured.followThumb && measured.followThumb.height,
+ };
+ moduleReady.follow = applyFollowMetrics(metrics);
+ if (moduleReady.follow) originalMetrics.follow = metrics;
setModuleReady("follow", moduleReady.follow);
+ if (moduleReady.follow) adjustFollowDropdownPosition(stage);
+ } else if (moduleReady.follow) {
+ adjustFollowDropdownPosition(stage);
}
if (!moduleReady.tooltip) {
// tooltip 出现时机不固定, 所以通常由 body mutation 触发探测.
- moduleReady.tooltip = [
- setVar("--tm-thumb-tooltip-width", measured.tooltipThumb && measured.tooltipThumb.width),
- setVar("--tm-thumb-tooltip-height", measured.tooltipThumb && measured.tooltipThumb.height),
- ].every(Boolean);
+ const metrics = {
+ width: measured.tooltipThumb && measured.tooltipThumb.width,
+ height: measured.tooltipThumb && measured.tooltipThumb.height,
+ };
+ moduleReady.tooltip = applyTooltipMetrics(metrics);
+ if (moduleReady.tooltip) originalMetrics.tooltip = metrics;
setModuleReady("tooltip", moduleReady.tooltip);
}
if (!moduleReady.discover) {
// 发现页要求更严格: 少一个高度都不开启, 宁可不放大, 也不要错位.
- const discoverWidthReady = setVar("--tm-thumb-discover-width", measured.discoverCard && measured.discoverCard.width);
- const discoverCardHeightReady = setCardHeightFromThumbVar("--tm-thumb-discover-card-height", measured.discoverCard && measured.discoverCard.height, measured.discoverThumb && measured.discoverThumb.height);
- const discoverThumbWidthReady = setVar("--tm-thumb-discover-thumb-width", measured.discoverThumb && measured.discoverThumb.width);
- const discoverHeightReady = setVar("--tm-thumb-discover-height", measured.discoverThumb && measured.discoverThumb.height);
- const discoverTripleReady = setDiscoverStackHeightVars(
- "--tm-thumb-discover-triple-ul",
- "--tm-thumb-discover-triple-arrow",
- measured.discoverTripleUl && measured.discoverTripleUl.height,
- measured.discoverCard && measured.discoverCard.height,
- measured.discoverThumb && measured.discoverThumb.height,
- 3,
- );
- const discoverDoubleReady = setDiscoverStackHeightVars(
- "--tm-thumb-discover-double-ul",
- "--tm-thumb-discover-double-arrow",
- measured.discoverDoubleUl && measured.discoverDoubleUl.height,
- measured.discoverCard && measured.discoverCard.height,
- measured.discoverThumb && measured.discoverThumb.height,
- 2,
- );
- const discoverSingleReady = setDiscoverStackHeightVars(
- "--tm-thumb-discover-single-ul",
- "--tm-thumb-discover-single-arrow",
- measured.discoverSingleUl && measured.discoverSingleUl.height,
- measured.discoverCard && measured.discoverCard.height,
- measured.discoverThumb && measured.discoverThumb.height,
- 1,
- );
- moduleReady.discover = discoverWidthReady && discoverCardHeightReady && discoverThumbWidthReady && discoverHeightReady
- && discoverTripleReady
- && discoverDoubleReady
- && discoverSingleReady;
+ const metrics = {
+ cardWidth: measured.discoverCard && measured.discoverCard.width,
+ cardHeight: measured.discoverCard && measured.discoverCard.height,
+ thumbWidth: measured.discoverThumb && measured.discoverThumb.width,
+ thumbHeight: measured.discoverThumb && measured.discoverThumb.height,
+ tripleHeight: measured.discoverTripleUl && measured.discoverTripleUl.height,
+ doubleHeight: measured.discoverDoubleUl && measured.discoverDoubleUl.height,
+ singleHeight: measured.discoverSingleUl && measured.discoverSingleUl.height,
+ };
+ moduleReady.discover = applyDiscoverMetrics(metrics);
+ if (moduleReady.discover) originalMetrics.discover = metrics;
setModuleReady("discover", moduleReady.discover);
}
- lastMeasure = { stage, moduleReady: { ...moduleReady }, measured };
+ lastMeasure = { stage, moduleReady: { ...moduleReady }, scaleSettings: { ...scaleSettings }, measured };
if (DEBUG) log("measured original sizes", lastMeasure);
return lastMeasure;
};
@@ -2164,6 +2640,7 @@
const computed = [
computedInfo("#roomlist_root ul.RoomCardGrid"),
computedInfo("#roomlist_root ul.list:has(li.roomCard)"),
+ computedInfo(".FollowedDropdown"),
computedInfo(".FollowedDropdown__rooms"),
computedInfo(".FollowedDropdown__room-image"),
computedInfo("#discover_root .room-list-carousel ul.list"),
@@ -2173,6 +2650,7 @@
stage,
ver: VER,
debug: DEBUG,
+ scaleSettings: { ...scaleSettings },
href: typeof location === "undefined" ? "" : location.href,
readyState: document.readyState,
stylePresent,
@@ -2353,6 +2831,8 @@
try {
target.tmThumbScaleDebug = api;
target.tmThumbScaleInspectRecommendations = inspectFollowedRecommendations;
+ target.tmThumbScaleInspectRoomChat = inspectRoomChatOverlay;
+ target.tmThumbScaleSetScales = (responsive, fixed) => applyScaleSettings({ responsive, fixed }, "api-set-scales");
target.tmThumbScaleLastReport = lastReport;
} catch (_) {}
});
@@ -2434,9 +2914,13 @@
// 观察 body 后, 每次 DOM 变化都延迟处理一次尚未 ready 的模块.
if (!document.body || pageObserverStarted || typeof MutationObserver !== "function") return;
pageObserverStarted = true;
+ ensureScaleControls();
+ applyRoomChatAutoHide("body-observer-start");
const mo = new MutationObserver(() => {
scheduleMeasure("mutation", 300);
scheduleTagsTranslation("mutation", 300);
+ ensureScaleControls();
+ applyRoomChatAutoHide("mutation");
});
mo.observe(document.body, { childList: true, subtree: true });
};
@@ -2468,6 +2952,8 @@
document.addEventListener("DOMContentLoaded", () => {
inject();
exposeDebugApi();
+ ensureScaleControls();
+ applyRoomChatAutoHide("domcontentloaded");
// DOMContentLoaded 后再等 600ms, 让 React/图片容器有时间完成初始布局.
scheduleMeasure("domcontentloaded", 600);
scheduleTagsTranslation("domcontentloaded", 600);
@@ -2479,11 +2965,11 @@
if (typeof setTimeout === "function") {
// 固定时间点探测用于覆盖: React 首屏慢, 图片容器稍后才出现等情况.
- setTimeout(() => { scheduleMeasure("after-800ms"); scheduleTagsTranslation("after-800ms"); reportDiagnostics("after-800ms"); startPageObserver(); }, 800);
+ setTimeout(() => { ensureScaleControls(); applyRoomChatAutoHide("after-800ms"); scheduleMeasure("after-800ms"); scheduleTagsTranslation("after-800ms"); reportDiagnostics("after-800ms"); startPageObserver(); }, 800);
// 第二次探测给慢一点的页面留余量.
- setTimeout(() => { scheduleMeasure("after-2000ms"); scheduleTagsTranslation("after-2000ms"); reportDiagnostics("after-2000ms"); startPageObserver(); }, 2000);
+ setTimeout(() => { ensureScaleControls(); applyRoomChatAutoHide("after-2000ms"); scheduleMeasure("after-2000ms"); scheduleTagsTranslation("after-2000ms"); reportDiagnostics("after-2000ms"); startPageObserver(); }, 2000);
// 最后一次兜底探测; 成功的模块会被 moduleReady 锁住, 不会重复相乘.
- setTimeout(() => { scheduleMeasure("after-5000ms"); scheduleTagsTranslation("after-5000ms"); reportDiagnostics("after-5000ms"); startPageObserver(); }, 5000);
+ setTimeout(() => { ensureScaleControls(); applyRoomChatAutoHide("after-5000ms"); scheduleMeasure("after-5000ms"); scheduleTagsTranslation("after-5000ms"); reportDiagnostics("after-5000ms"); startPageObserver(); }, 5000);
}
})();
diff --git a/tests/followed-dropdown-css.test.js b/tests/followed-dropdown-css.test.js
index 8348a3e..3cf282a 100644
--- a/tests/followed-dropdown-css.test.js
+++ b/tests/followed-dropdown-css.test.js
@@ -196,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/,
@@ -209,14 +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.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/, "script should not write detected sizes to localStorage");
+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(