Add Chaturbate thumbnail controls and chat hiding
This commit is contained in:
@@ -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 = [
|
||||
'<label title="影响首页, 关注页列表, 频道页更多房间等自适应网格"><span class="tm-thumb-scale-label">响应式</span><input data-tm-thumb-control="responsive" type="number" min="1.0" max="4.0" step="0.1"></label>',
|
||||
'<label title="影响发现页轮播, 顶部关注弹窗和悬浮预览"><span class="tm-thumb-scale-label">固定式</span><input data-tm-thumb-control="fixed" type="number" min="1.0" max="4.0" step="0.1"></label>',
|
||||
'<label class="tm-thumb-scale-check" title="在主播频道页自动隐藏视频上方的悬浮聊天窗"><input data-tm-thumb-control="hide-room-chat" type="checkbox"><span class="tm-thumb-scale-label">隐藏聊天窗</span></label>',
|
||||
'<button type="button" data-tm-thumb-control="reset" title="把两个倍率都设为 1.0">恢复原始尺寸</button>',
|
||||
].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);
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user