// ==UserScript==
// @run-at document-start
// @name MissAV去广告、影院模式
// @description 测试一下聊天室
// @icon https://missav.ws/img/favicon.ico
// @namespace loadingi.local
// @version 5.0.3
// @author chris
// @match *://*.missav.ws/*
// @match *://*.missav.ai/*
// @match *://*.missav123.com/*
// @match *://*/view_video.php?viewkey=*
// @match *://fuliba2025.net/*
// @grant GM_setValue
// @grant GM_getValue
// @grant unsafeWindow
// @grant GM_xmlhttpRequest
// @compatible chrome
// @compatible firefox
// @compatible edge
// @compatible safari
// @license GPL-3.0-only
// @require https://code.jquery.com/jquery-3.6.0.min.js
// ==/UserScript==
(function() {
'use strict';
/**
* 主要配置对象
* - selectors: DOM元素选择器配置
* - styles: 样式配置
* - player: 播放器功能配置
*/
const Config = {
// DOM 选择器
selectors: {
player: {
container: '.relative.-mx-4.sm\\:m-0.-mt-6',
wrapper: '.aspect-w-16.aspect-h-9',
video: 'video#player',
progress: 'div.sm\\:hidden.flex.justify-between.-mx-4.px-4.pt-3.pb-1.bg-black',
abLoop: 'div.flex.items-center.flex-nowrap.leading-5',
abLoopControls: '.theater-controls-abloop',
genres: '.absolute.bottom-1.left-1.rounded-lg.px-2.py-1.text-xs.text-nord5.bg-blue-800.bg-opacity-75',
uncensoredLink: "a[id^='option-menu-item'][href*='uncensored']",
qualityOptions: '.plyr__menu__container [data-plyr="quality"]'
},
ads: {
scripts: [
"script[src*='app.1aad5686.js']",
"script[src*='inpage.push.js']",
"script[src*='hartattenuate.com']",
"script[src*='ads']",
"script[src*='pop']",
"script[src*='banner']",
"script[src*='htmlAds']",
"script[src*='popAds']",
"script[src*='bannerAds']",
"script[src*='adsConfig']"
],
elements: [
// 'div.sm\\:container.mx-auto.mb-5.px-4',
'ul.mb-4.list-none.text-nord14.grid.grid-cols-2.gap-2',
'div.relative.ml-4',
'div.root--ujvuu',
'div.under_player',
'div.space-y-5.mb-5',
'div[class^="rootContent--"]',
'div[class^="fixed right-2 bottom-2"]',
'div[class^="space-y-6 mb-6"]',
'div.space-y-2.mb-4.ml-4.list-disc.text-nord14',
'div[id*="ads"]',
'div[id*="banner"]',
'div[class*="ads"]',
'div[class*="banner"]',
'.ad-container',
'#ad-container'
],
scriptPatterns: ['htmlAds', 'popAds', 'bannerAds', 'adsConfig']
}
},
styles: {
button: {
base: {
backgroundColor: '#222',
borderRadius: '15px',
borderColor: 'black',
borderWidth: '1px',
color: 'burlywood',
cursor: 'pointer',
transition: 'all 0.3s ease',
outline: 'none',
minWidth: '80px',
padding: '2px 4px',
marginBottom: '10px'
},
hover: {
backgroundColor: '#333'
}
},
theaterMode: `
.theater-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.95);
z-index: 9998;
display: none;
}
.theater-mode-container {
position: fixed !important;
top: 0 !important;
left: 0 !important;
width: 100vw !important;
height: 100vh !important;
transform: none !important;
z-index: 9999 !important;
margin: 0 !important;
padding: 0 !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
background: transparent !important;
pointer-events: auto !important;
}
.theater-mode-container .aspect-w-16.aspect-h-9 {
position: relative !important;
width: 100vw !important;
max-width: none !important;
height: 100vh !important;
margin: 0 auto !important;
pointer-events: auto !important;
}
.theater-mode-container video {
position: absolute !important;
top: 0 !important;
left: 0 !important;
width: 100% !important;
height: 100% !important;
object-fit: contain !important;
pointer-events: auto !important;
}
.theater-mode-container * {
max-width: none !important;
max-height: none !important;
pointer-events: auto !important;
}
.theater-mode-container .plyr__controls {
position: fixed !important;
bottom: 0 !important;
left: 0 !important;
width: 100% !important;
z-index: 10000 !important;
background: transparent !important;
padding: 10px !important;
opacity: 1 !important;
visibility: visible !important;
display: flex !important;
}
.fixed.z-max.w-full.bg-gradient-to-b.from-darkest {
z-index: 1 !important;
}
.theater-mode-container .fixed.z-max.w-full.bg-gradient-to-b.from-darkest {
display: none !important;
}
.theater-mode-container .plyr__time {
display: inline-block !important;
color: white !important;
opacity: 1 !important;
visibility: visible !important;
}
.theater-controls-progress {
position: fixed !important;
bottom: 104px !important;
z-index: 10000 !important;
background: transparent !important;
padding: 10px !important;
width: 100% !important;
max-width: none !important;
pointer-events: auto !important;
}
.theater-controls-abloop {
position: fixed !important;
bottom: 52px !important;
left: 0px !important;
width: 100% !important;
z-index: 10000 !important;
padding: 10px !important;
pointer-events: auto !important;
}
.theater-mode-container .plyr__controls__item.plyr__volume {
width: 40px !important;
}
.theater-mode-container .plyr__controls__item[data-plyr="rewind"],
.theater-mode-container .plyr__controls__item[data-plyr="fast-forward"],
.theater-mode-container .plyr__control[data-plyr="settings"],
.theater-mode-container .plyr__controls__item[data-plyr="pip"],
.theater-mode-container .plyr__controls__item[data-plyr="fullscreen"] {
display: none !important;
}
.theater-mode-container ~ div .theater-mode-button,
.theater-mode-container ~ div .ab-loop-button {
background-color: rgba(34, 34, 34, 0.5) !important;
border-color: rgba(0, 0, 0, 0.5) !important;
}
.theater-mode-container ~ div .theater-mode-button:hover,
.theater-mode-container ~ div .ab-loop-button:hover {
background-color: rgba(51, 51, 51, 0.7) !important;
}
`
},
player: {
autoHighestQuality: true,
preventFocusPause: true,
autoSwitchUncensored: true,
hideVideoGenres: true
}
};
/**
* 工具类
* - debounce: 函数防抖
* - createButton: 创建自定义按钮
* - addStyle: 添加自定义样式
*/
const Utils = {
debounce(func, wait) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
},
createButton(text, onClick) {
const button = document.createElement('button');
Object.assign(button.style, Config.styles.button.base);
button.innerText = text;
button.addEventListener('mouseover', () => Object.assign(button.style, Config.styles.button.hover));
button.addEventListener('mouseout', () => Object.assign(button.style, { backgroundColor: Config.styles.button.base.backgroundColor }));
button.addEventListener('click', onClick);
return button;
},
addStyle(css) {
const style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
}
};
/**
* 广告拦截器模块
* - 移除广告脚本和元素
* - 拦截动态加载的广告
* - 使用MutationObserver监听DOM变化
*/
const AdBlocker = {
init() {
this.blockAds = Utils.debounce(this.blockAds.bind(this), 100);
this.blockAds();
this.setupMutationObserver();
this.interceptDynamicScripts();
},
blockAds() {
const { scripts, elements, scriptPatterns } = Config.selectors.ads;
// 移除广告脚本和元素
[...scripts, ...elements].forEach(selector => {
document.querySelectorAll(selector).forEach(el => el?.remove());
});
// 移除 iframes
document.querySelectorAll('iframe').forEach(iframe => iframe.remove());
// 移除匹配模式的脚本
const scriptPattern = new RegExp(scriptPatterns.join('|'));
document.querySelectorAll('script').forEach(script => {
if (scriptPattern.test(script.innerText)) {
script.remove();
}
});
},
setupMutationObserver() {
const observer = new MutationObserver(
Utils.debounce(() => this.blockAds(), 100)
);
observer.observe(document.body, {
childList: true,
subtree: true
});
},
interceptDynamicScripts() {
const scriptPattern = new RegExp(Config.selectors.ads.scriptPatterns.join('|'));
const originalCreateElement = document.createElement.bind(document);
document.createElement = function(tagName) {
const element = originalCreateElement(tagName);
if (tagName.toLowerCase() === 'script') {
const originalSetAttribute = element.setAttribute.bind(element);
element.setAttribute = function(name, value) {
if (name === 'src' && scriptPattern.test(value)) {
return; // 阻止加载广告脚本
}
return originalSetAttribute(name, value);
};
}
return element;
};
}
};
/**
* 播放器增强模块
* 主要功能:
* - 影院模式
* - 进度控制
* - AB循环
* - 自动最高画质
* - 防止失焦暂停
* - 隐藏视频类型标签
*/
const PlayerEnhancer = {
init() {
this.setupPlayer();
this.createControls();
Utils.addStyle(Config.styles.theaterMode);
// 新增功能初始化
if(Config.player.hideVideoGenres) {
this.hideVideoGenres();
}
if(Config.player.autoSwitchUncensored) {
this.setupAutoUncensored();
}
if(Config.player.autoHighestQuality) {
this.setupAutoHighestQuality();
}
if(Config.player.preventFocusPause) {
this.preventFocusPause();
}
},
setupPlayer() {
// 移除点击事件
document.querySelectorAll('[\\@click="pop()"]').forEach(el => {
el.removeAttribute('@click');
});
// 移除窗口失焦暂停
const aspectElements = document.getElementsByClassName('aspect-w-16 aspect-h-9');
if (aspectElements[11]) {
aspectElements[11].removeAttribute('@click');
aspectElements[11].removeAttribute('@keyup.space.window');
}
},
createControls() {
const container = document.createElement('div');
Object.assign(container.style, {
position: 'fixed',
top: '14px',
right: '98px',
zIndex: '10001',
display: 'flex',
flexDirection: 'row',
gap: '10px'
});
// 创建影院模式按钮
const theaterButton = Utils.createButton('影院模式', () => this.toggleTheaterMode());
theaterButton.className = 'theater-mode-button';
container.appendChild(theaterButton);
// 创建进度控制按钮
const progressButton = Utils.createButton('进度控制', () => this.toggleProgressControls());
progressButton.className = 'progress-control-button';
progressButton.style.display = 'none'; // 初始隐藏
container.appendChild(progressButton);
// 创建 AB 循环按钮
const abLoopButton = Utils.createButton('A/B', () => this.toggleABLoopControls());
abLoopButton.className = 'ab-loop-button';
abLoopButton.style.display = 'none';
container.appendChild(abLoopButton);
document.body.appendChild(container);
},
toggleTheaterMode() {
const { player } = Config.selectors;
const playerContainer = document.querySelector(player.container);
const progress = document.querySelector(player.progress);
const abLoop = document.querySelector(player.abLoop);
let overlay = document.querySelector('.theater-overlay');
if (!overlay) {
overlay = document.createElement('div');
overlay.className = 'theater-overlay';
document.body.appendChild(overlay);
}
const isTheaterMode = overlay.style.display === 'none' || overlay.style.display === '';
const theaterButton = document.querySelector('.theater-mode-button');
const abLoopButton = document.querySelector('.ab-loop-button');
const progressButton = document.querySelector('.progress-control-button');
if (theaterButton) {
theaterButton.innerText = isTheaterMode ? '关闭影院' : '影院模式';
}
if (abLoopButton) {
abLoopButton.style.display = isTheaterMode ? 'block' : 'none';
}
if (progressButton) {
progressButton.style.display = isTheaterMode ? 'block' : 'none';
}
if (isTheaterMode) {
this.enterTheaterMode(overlay, playerContainer, progress, abLoop);
} else {
this.exitTheaterMode(overlay, playerContainer, progress, abLoop);
}
},
toggleProgressControls() {
const progress = document.querySelector('.theater-controls-progress');
const progressButton = document.querySelector('.progress-control-button');
if (progress) {
const isVisible = progress.style.display !== 'none';
progress.style.display = isVisible ? 'none' : 'flex';
if (progressButton) {
progressButton.innerText = isVisible ? '显示进度' : '隐藏进度';
}
}
},
enterTheaterMode(overlay, playerContainer, progress, abLoop) {
overlay.style.display = 'block';
if (playerContainer) {
playerContainer.classList.add('theater-mode-container');
this.adjustParentContainers(playerContainer);
}
if (progress) {
progress.classList.add('theater-controls-progress');
// 默认显示进度条
progress.style.display = 'flex';
const progressButton = document.querySelector('.progress-control-button');
if (progressButton) {
progressButton.innerText = '隐藏进度';
}
}
if (abLoop) {
abLoop.classList.add('theater-controls-abloop');
abLoop.style.display = 'none';
}
document.addEventListener('keydown', this.handleEscKey);
},
exitTheaterMode(overlay, playerContainer, progress, abLoop) {
overlay.style.display = 'none';
if (playerContainer) {
playerContainer.classList.remove('theater-mode-container');
this.resetParentContainers(playerContainer);
}
if (progress) {
progress.classList.remove('theater-controls-progress');
}
if (abLoop) {
abLoop.classList.remove('theater-controls-abloop');
}
document.removeEventListener('keydown', this.handleEscKey);
},
adjustParentContainers(element) {
let parent = element.parentElement;
while (parent && parent !== document.body) {
Object.assign(parent.style, {
maxWidth: 'none',
maxHeight: 'none',
overflow: 'visible',
zIndex: 'auto'
});
parent = parent.parentElement;
}
},
resetParentContainers(element) {
let parent = element.parentElement;
while (parent && parent !== document.body) {
['maxWidth', 'maxHeight', 'overflow', 'zIndex'].forEach(prop => {
parent.style.removeProperty(prop);
});
parent = parent.parentElement;
}
},
handleEscKey(e) {
if (e.key === 'Escape') {
PlayerEnhancer.toggleTheaterMode();
}
},
toggleABLoopControls() {
const abLoopControls = document.querySelector(Config.selectors.player.abLoopControls);
const abLoopButton = document.querySelector('.ab-loop-button');
if (abLoopControls) {
const isVisible = abLoopControls.style.display !== 'none';
abLoopControls.style.display = isVisible ? 'none' : 'flex';
if (abLoopButton) {
abLoopButton.innerText = isVisible ? 'A/B' : 'no A/B';
}
}
},
// 隐藏视频类型
hideVideoGenres() {
const genresElements = document.querySelectorAll(Config.selectors.player.genres);
genresElements.forEach(el => el.style.display = 'none');
},
// 自动切换无码版本beta
// setupAutoUncensored() {
// const uncensoredLink = document.querySelector(Config.selectors.player.uncensoredLink);
// if(uncensoredLink) {
// uncensoredLink.click();
// }
// },
// 自动设置最高画质
setupAutoHighestQuality() {
const setHighestQuality = () => {
const player = unsafeWindow.player;
if(!player?.config?.quality?.options) return;
const maxQuality = Math.max(...player.config.quality.options);
player.quality = maxQuality;
player.config.quality.default = maxQuality;
player.config.quality.selected = maxQuality;
};
// 等待播放器加载完成
const checkPlayer = setInterval(() => {
if(unsafeWindow.player) {
setHighestQuality();
clearInterval(checkPlayer);
}
}, 100);
},
// 防止失焦暂停
preventFocusPause() {
document.addEventListener('visibilitychange', () => {
const player = unsafeWindow.player;
if(document.hidden && player?.playing) {
player.play();
}
});
// 移除原有的失焦暂停事件
const playerContainer = document.querySelector(Config.selectors.player.container);
if(playerContainer) {
playerContainer.removeAttribute('@keyup.space.window');
}
}
};
// ↑----------------------------------------------------------------------
// 聊天室类
class ChatRoom {
constructor(config = {}) {
// 配置项
this.config = {
wsServer: config.wsServer || 'wss://topurl.cn:9001',
authToken: config.authToken || window.btoa(encodeURIComponent('https://news.topurl.cn/')),
maxHistory: config.maxHistory || 300,
...config
};
// 加密配置
this.PREFIX = '🔒';
this.CHINESE_RANGE = { start: 0x4E00, end: 0x9FA5 };
this.HALL_DOMAIN = "square.io";
// 全局变量
this.activeWebsockets = {};
this.activeDomain = null;
this.domainData = {};
this.isSending = false;
this.isReconnecting = false;
this.autoScroll = true;
this.heartbeatTimer = null;
this.userId = null;
this.userName = null;
this.domainList = [];
this.showAllDomains = false;
this.onlineUsers = null;
// URL解析相关
this.urlCache = {}; // 用于缓存已解析的URL
this.supportedDomains = ['missav.com', 'missav.ws', 'jable.tv']; // 支持预览的网站域名
// DOM 元素缓存
this.elements = {};
// 认证字符
this.authChar = this.config.authToken[1] +
this.config.authToken[3] +
this.config.authToken[7] +
this.config.authToken[9];
// 后台跟踪相关变量
this.trackingWs = null;
this.trackingInterval = null;
this.clientId = this.getOrCreateClientId();
}
// 生成或获取客户端唯一标识
getOrCreateClientId() {
// 尝试从localStorage获取已有的客户端ID
let clientId = localStorage.getItem('ctrm_client_id');
// 如果不存在,则生成一个基于设备特征的唯一ID
if (!clientId) {
// 收集设备特征信息
const deviceInfo = {
userAgent: navigator.userAgent,
screenWidth: screen.width,
screenHeight: screen.height,
colorDepth: screen.colorDepth,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
language: navigator.language,
cpuCores: navigator.hardwareConcurrency || 0,
deviceMemory: navigator.deviceMemory || 0,
platform: navigator.platform
};
// 将设备信息转换为字符串并生成哈希
const deviceString = JSON.stringify(deviceInfo);
let hash = 0;
for (let i = 0; i < deviceString.length; i++) {
const char = deviceString.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // 转换为32位整数
}
// 将哈希值转换为4位字母数字组合
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
clientId = '';
const absHash = Math.abs(hash);
for (let i = 0; i < 4; i++) {
// 使用哈希值的不同部分选择字符
const index = (absHash >> (i * 8)) % chars.length;
clientId += chars.charAt(index);
}
// 将新生成的ID保存到localStorage
localStorage.setItem('ctrm_client_id', clientId);
}
return clientId;
}
// 初始化后台跟踪连接
initBackgroundTracking() {
// 创建WebSocket连接
this.trackingWs = new WebSocket(this.config.wsServer);
this.trackingWs.onopen = () => {
console.log("后台跟踪连接已建立");
// 连接成功后先发送域名更新消息,指定连接到asshole.io
const updateMsg = {
type: 'update',
data: {
domainFrom: 'asshole.io'
},
char: this.authChar
};
this.trackingWs.send(JSON.stringify(updateMsg));
// 延迟1秒后发送第一次消息,确保更新已经处理
setTimeout(() => {
this.sendTrackingMessage();
// 设置定时器,每5分钟发送一次消息
this.trackingInterval = setInterval(() => {
this.sendTrackingMessage();
}, 5 * 60 * 1000); // 5分钟
}, 1000);
};
this.trackingWs.onclose = () => {
console.log("后台跟踪连接已关闭");
// 清除定时器
if (this.trackingInterval) {
clearInterval(this.trackingInterval);
this.trackingInterval = null;
}
// 2分钟后尝试重新连接
setTimeout(() => {
this.initBackgroundTracking();
}, 2 * 60 * 1000);
};
this.trackingWs.onerror = (error) => {
console.error("后台跟踪连接错误:", error);
};
}
// 发送跟踪消息
sendTrackingMessage() {
if (this.trackingWs && this.trackingWs.readyState === WebSocket.OPEN) {
// 获取当前URL
const currentUrl = window.location.href;
// 构建消息: 客户端ID + 空格 + @ + 当前URL
const message = {
type: 'chat',
data: {
msg: this.encrypt(`${this.clientId} at ${currentUrl}`)
// 移除domainTo字段,因为我们已经通过update消息设置了域名
},
char: this.authChar
};
// 发送消息
this.trackingWs.send(JSON.stringify(message));
console.log("已发送跟踪消息");
}
}
// 加密相关方法
encrypt(text) {
try {
const encrypted = this.compressEncrypt(text);
return this.PREFIX + encrypted;
} catch (e) {
console.error('加密失败:', e);
return text;
}
}
decrypt(text) {
if (!text.startsWith(this.PREFIX)) return text;
try {
const encryptedText = text.slice(this.PREFIX.length);
return this.compressDecrypt(encryptedText);
} catch (e) {
console.error('解密失败:', e);
return text;
}
}
compressEncrypt(text) {
const mapStart = this.CHINESE_RANGE.start;
const bytes = new TextEncoder().encode(text);
let encrypted = '';
// 添加长度标记确保解密精确
const lengthMark = String.fromCharCode(mapStart + bytes.length);
encrypted += lengthMark;
for (let i = 0; i < bytes.length; i += 2) {
const byte1 = bytes[i];
const byte2 = i + 1 < bytes.length ? bytes[i + 1] : 0;
const merged = (byte1 << 8) | byte2;
encrypted += String.fromCharCode(mapStart + 256 + merged);
}
return encrypted;
}
compressDecrypt(encrypted) {
if (encrypted.length <= 1) return "加密数据不完整";
const mapStart = this.CHINESE_RANGE.start;
const bytesLength = encrypted.charCodeAt(0) - mapStart;
const bytes = new Uint8Array(bytesLength);
for (let i = 1, byteIndex = 0; i < encrypted.length; i++) {
const merged = encrypted.charCodeAt(i) - mapStart - 256;
if (byteIndex < bytesLength) {
bytes[byteIndex++] = (merged >> 8) & 0xFF;
}
if (byteIndex < bytesLength) {
bytes[byteIndex++] = merged & 0xFF;
}
}
return new TextDecoder().decode(bytes);
}
// 工具方法
generateElegantColor(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
}
const hue = Math.abs(hash % 360);
const saturation = 60 + Math.abs(hash % 30);
const lightness = 40 + Math.abs(hash % 30);
return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
}
getContrastColor(hsl) {
const lightness = parseInt(hsl.match(/hsl\((\d+),\s*(\d+)%,\s*(\d+)%\)/)[3]);
return lightness > 60 ? 'rgba(0, 0, 0, 0.8)' : 'rgba(255, 255, 255, 0.9)';
}
// 防抖函数
debounce(func, wait) {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
// 初始化方法
init() {
// 防止重复注入
if (window._hasCtrmInjected) {
return document.querySelector('.chat-title').click();
}
window._hasCtrmInjected = true;
this.injectStyles();
this.injectHTML();
// 添加延时确保 DOM 已经渲染
setTimeout(() => {
this.cacheDOMElements();
this.bindEvents();
this.initAllConnections();
this.adjustUI();
// 初始化时折叠用户面板
this.elements.onlineUsersPanel.addClass('collapsed');
// 默认收起状态
setTimeout(() => {
this.elements.chatContainer.show();
this.elements.closeBtn.click();
}, 100);
// 初始化后台跟踪
this.initBackgroundTracking();
}, 0);
// 添加URL预览样式
const style = document.createElement('style');
style.textContent = `
.ctrm-url-preview {
margin-top: 5px;
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
background-color: #f8f8f8;
display: flex;
flex-direction: column;
max-width: 100%;
}
.ctrm-preview-image {
width: 100%;
height: auto;
max-height: 150px;
object-fit: cover;
background-color: #f1f1f1;
transition: transform 0.2s;
}
.ctrm-preview-image:hover {
transform: scale(1.02);
}
.ctrm-preview-content {
padding: 8px;
flex: 1;
min-width: 0;
}
.ctrm-preview-title {
font-size: 14px;
font-weight: bold;
color: #333;
margin-bottom: 5px;
word-break: break-word;
/* 移除单行截断,允许多行显示 */
white-space: normal;
overflow: visible;
line-height: 1.4;
}
.ctrm-preview-title a {
color: #0066cc;
text-decoration: none;
}
.ctrm-preview-title a:hover {
text-decoration: underline;
}
.ctrm-preview-domain {
font-size: 12px;
color: #666;
margin-top: 3px;
}
/* 消息中的URL链接样式 */
.ctrm-url-link, .message-text a {
color: #0066cc !important;
text-decoration: underline !important;
}
`;
document.head.appendChild(style);
}
// 注入样式
injectStyles() {
const styles = `
/*--------------------
CSS变量定义
--------------------*/
:root {
--primary-color: rgba(222, 184, 135, 0.8); /* 主色调 burlywood */
--primary-hover: rgba(222, 184, 135, 0.5); /* 悬停色 */
--bg-dark: rgba(0, 0, 0, 0.8); /* 深色背景 */
--bg-darker: rgba(0, 0, 0, 0.2); /* 更深色背景 */
--bg-lighter: rgba(135, 135, 135, 0.3); /* 较浅色背景 */
--text-primary: rgba(255, 255, 255, 0.9); /* 主要文字色 */
--text-secondary: rgba(255, 255, 255, 0.7); /* 次要文字色 */
--text-muted: rgba(255, 255, 255, 0.3); /* 弱化文字色 */
--border-color: rgba(255, 255, 255, 0.1); /* 边框色 */
--shadow-color: rgba(222, 184, 135, 0.5); /* 阴影色 */
--system-msg-bg: rgba(255, 152, 0, 0.5); /* 系统消息背景 */
--message-background-color: rgba(255, 255, 255, 0.95);
--primary-text-color: rgba(0, 0, 0, 0.9);
--peer-color-rgb: 0, 150, 135;
}
/* 原来的所有样式 */
${this.getChatStyles()}
`;
const styleElement = document.createElement('style');
styleElement.textContent = styles;
document.head.appendChild(styleElement);
}
// 注入HTML
injectHTML() {
const chatTemplate = `
<div id="ctrm_" style="display: none;">
<div class="chat">
<div class="chat-title">
<div class="chat-tabs">
<!-- 这里将动态添加标签 -->
</div>
<div class="chat-controls">
<button class="chat-add" title="添加聊天室">+</button>
<button class="chat-reconn" title="重生">
<svg fill="currentColor" viewBox="0 0 8 8" width="16" height="16" xmlns="http://www.w3.org/2000/svg">
<path d="M4 0c-1.65 0-3 1.35-3 3h-1l1.5 2 1.5-2h-1c0-1.11.89-2 2-2v-1zm2.5 1l-1.5 2h1c0 1.11-.89 2-2 2v1c1.65 0 3-1.35 3-3h1l-1.5-2z" transform="translate(0 1)" />
</svg>
</button>
<button class="chat-close" title="老板出没"></button>
</div>
</div>
<div class="custom-domain-popup">
<input type="text" placeholder="输入域名" class="custom-domain-input">
<div class="popup-buttons">
<button class="cancel">取消</button>
<button class="submit">连接</button>
</div>
</div>
<div class="messages">
<div class="messages-content"></div>
<div class="scroll-bottom">⇩</div>
<div class="online-users">
<div class="online-users-header">在线人数:0</div>
<div class="online-users-content"></div>
<div class="toggle-users-panel"></div>
</div>
</div>
<div class="message-box">
<textarea type="text" class="message-input" placeholder="说点什么吧..." maxlength="69"></textarea>
<button type="submit" class="message-submit">发送</button>
</div>
</div>
</div>
`;
const container = document.createElement('div');
container.innerHTML = chatTemplate;
document.body.appendChild(container.firstElementChild);
}
// 缓存DOM元素
cacheDOMElements() {
const elements = {
chatContainer: $('#ctrm_'),
chatTitle: $('#ctrm_ .chat-title'),
chatTabs: $('#ctrm_ .chat-tabs'),
chatMessagesContent: $('#ctrm_ .messages-content'),
scrollBottomBtn: $('#ctrm_ .scroll-bottom'),
messageInput: $('#ctrm_ .message-input'),
messageSubmitBtn: $('#ctrm_ .message-submit'),
onlineUsersHeader: $('#ctrm_ .online-users-header'),
onlineUsersContent: $('#ctrm_ .online-users-content'),
toggleUsersPanelBtn: $('#ctrm_ .toggle-users-panel'),
onlineUsersPanel: $('#ctrm_ .online-users'),
closeBtn: $('#ctrm_ .chat-close'),
reconnectBtn: $('#ctrm_ .chat-reconn'),
addRoomBtn: $('#ctrm_ .chat-add'),
customDomainPopup: $('#ctrm_ .custom-domain-popup'),
customDomainInput: $('#ctrm_ .custom-domain-input'),
customDomainSubmit: $('#ctrm_ .custom-domain-popup .submit'),
customDomainCancel: $('#ctrm_ .custom-domain-popup .cancel')
};
// 检查所有必需的元素是否存在
for (const [key, element] of Object.entries(elements)) {
if (!element.length) {
console.error(`Required element not found: ${key}`);
throw new Error(`Required element not found: ${key}`);
}
}
this.elements = elements;
}
// 绑定事件
bindEvents() {
// 点击空白处关闭聊天框
$(document.body).on('click', this.handleOutsideClick.bind(this));
window.addEventListener('popstate', this.handleOutsideClick.bind(this));
// 阻止事件冒泡
this.elements.chatContainer.on('click', e => e.stopPropagation());
this.elements.chatContainer.on('touchstart', e => e.stopPropagation());
this.elements.chatContainer.on('touchend', e => e.stopPropagation());
this.elements.chatContainer.on('touchmove', e => e.stopPropagation());
// 聊天面板点击事件
this.elements.chatContainer.find('.chat').on('click', e => {
if (this.elements.chatContainer.hasClass('ctrm-close')) {
this.elements.chatContainer.removeClass('ctrm-close');
this.adjustUI();
// 面板展开时,确保滚动到最新消息
this.autoScroll = true;
requestAnimationFrame(() => {
this.scrollToBottom();
});
e.stopPropagation();
}
});
// 关闭按钮事件
this.elements.closeBtn.click(e => {
this.elements.chatContainer.toggleClass('ctrm-close');
this.adjustUI();
e.stopPropagation();
});
// 重连按钮事件
this.elements.reconnectBtn.click(() => this.reconnect());
// 发送按钮事件
this.elements.messageSubmitBtn.click(() => this.sendMessage());
// 输入框事件
this.elements.messageInput.on('keydown', e => {
if (e.keyCode === 13 && !e.shiftKey) {
e.preventDefault();
this.sendMessage();
}
});
// 输入框自动调整高度
this.elements.messageInput.on('input', function() {
this.style.height = '36px';
const newHeight = Math.min(this.scrollHeight, 120);
this.style.height = newHeight + 'px';
});
// 滚动事件
this.elements.chatMessagesContent.on('scroll',
this.debounce(() => this.handleScroll(), 100)
);
// 滚动到底部按钮事件
this.elements.scrollBottomBtn.click(() => {
this.elements.scrollBottomBtn.hide();
this.autoScroll = true;
this.scrollToBottom();
});
// 用户面板切换按钮事件
this.elements.toggleUsersPanelBtn.click(() => {
this.elements.onlineUsersPanel.toggleClass('collapsed');
// 切换消息区域的宽度
if (this.elements.onlineUsersPanel.hasClass('collapsed')) {
this.elements.chatMessagesContent.addClass('full-width');
} else {
this.elements.chatMessagesContent.removeClass('full-width');
}
// 增加过渡效果后可能需要重新调整对话框
setTimeout(() => {
this.scrollToBottom();
}, 300);
});
// 移动端优化
this.setupMobileEvents();
// 添加聊天室按钮点击事件
this.elements.addRoomBtn.click(e => {
this.showCustomDomainPopup();
e.stopPropagation();
});
// 自定义域名弹窗中的提交按钮
this.elements.customDomainSubmit.click(() => {
this.connectCustomDomain();
});
// 自定义域名弹窗中的取消按钮
this.elements.customDomainCancel.click(() => {
this.hideCustomDomainPopup();
});
// 自定义域名输入框的回车事件
this.elements.customDomainInput.on('keydown', e => {
if (e.keyCode === 13) {
this.connectCustomDomain();
}
});
// 点击弹窗外部关闭弹窗
$(document).on('click', e => {
if (this.elements.customDomainPopup.is(':visible') &&
!this.elements.customDomainPopup.is(e.target) &&
this.elements.customDomainPopup.has(e.target).length === 0 &&
!this.elements.addRoomBtn.is(e.target)) {
this.hideCustomDomainPopup();
}
});
}
// 移动端事件优化
setupMobileEvents() {
document.addEventListener('touchstart', e => {
if($(e.target).closest('#ctrm_').length) {
e.preventDefault();
}
}, { passive: false });
this.elements.chatMessagesContent[0].addEventListener('scroll', e => {
e.stopPropagation();
}, { passive: true });
this.elements.onlineUsersContent[0].addEventListener('scroll', e => {
e.stopPropagation();
}, { passive: true });
}
// 获取聊天室样式
getChatStyles() {
return `
/*--------------------
CSS变量定义
--------------------*/
:root {
--primary-color: rgba(222, 184, 135, 0.8); /* 主色调 burlywood */
--primary-hover: rgba(222, 184, 135, 0.5); /* 悬停色 */
--bg-dark: rgba(0, 0, 0, 0.8); /* 深色背景 */
--bg-darker: rgba(0, 0, 0, 0.2); /* 更深色背景 */
--bg-lighter: rgba(135, 135, 135, 0.3); /* 较浅色背景 */
--text-primary: rgba(255, 255, 255, 0.9); /* 主要文字色 */
--text-secondary: rgba(255, 255, 255, 0.7); /* 次要文字色 */
--text-muted: rgba(255, 255, 255, 0.3); /* 弱化文字色 */
--border-color: rgba(255, 255, 255, 0.1); /* 边框色 */
--shadow-color: rgba(222, 184, 135, 0.5); /* 阴影色 */
--system-msg-bg: rgba(255, 152, 0, 0.5); /* 系统消息背景 */
--message-background-color: rgba(255, 255, 255, 0.95);
--primary-text-color: rgba(0, 0, 0, 0.9);
--peer-color-rgb: 0, 150, 135;
}
/*--------------------
基础样式
--------------------*/
#ctrm_ {
position: fixed;
z-index: 10001;
bottom: 0;
right: 0;
transition: all 0.3s ease;
font-family: 'Open Sans', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-size: 12px;
line-height: 1.3;
height: 0;
width: 0;
}
#ctrm_ .chat {
position: fixed;
bottom: 0;
right: 0;
width: 350px;
height: 500px;
z-index: 2;
overflow: hidden;
box-shadow: 0 0px 5px var(--shadow-color);
background: var(--bg-lighter);
backdrop-filter: blur(10px);
border-radius: 20px 0 0 0;
display: flex;
flex-direction: column;
transition: all 0.3s ease;
border: 1px solid var(--border-color);
--message-background-color: rgba(255, 255, 255, 0.95);
--primary-text-color: rgba(0, 0, 0, 0.9);
}
#ctrm_ .chat-title {
flex: 0 0 45px;
position: relative;
background: var(--bg-darker);
color: var(--text-primary);
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 10px;
cursor: pointer;
border-radius: 20px 0 0 0;
z-index: 3;
}
#ctrm_ .chat-title.glow {
background: color-mix(in srgb, var(--primary-color) 70%, transparent);
}
#ctrm_ .chat-tabs {
display: flex;
overflow-x: auto;
white-space: nowrap;
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */
padding: 5px 0;
max-width: calc(100% - 90px); /* 为右侧按钮留出空间 */
}
#ctrm_ .chat-tabs::-webkit-scrollbar {
display: none; /* Chrome, Safari and Opera */
}
#ctrm_ .chat-tab {
flex: 0 0 auto; /* 防止标签被压缩 */
padding: 6px 12px;
margin-right: 6px;
border-radius: 15px;
background: var(--bg-lighter);
cursor: pointer;
white-space: nowrap;
font-size: 12px;
transition: all 0.2s ease;
color: var(--text-secondary);
display: inline-block; /* 确保标签内联显示 */
}
#ctrm_ .chat-tab.active {
background: var(--primary-color);
color: var(--text-primary);
}
#ctrm_ .chat-tab .unread-indicator {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
background: #ff5252;
margin-left: 4px;
}
#ctrm_.ctrm-mobile .chat-tab {
padding: 6px 14px;
font-size: 14px;
}
/*--------------------
控制按钮
--------------------*/
#ctrm_ .chat-controls {
display: flex;
align-items: center;
}
#ctrm_ .chat-add,
#ctrm_ .chat-reconn,
#ctrm_ .chat-close {
display: flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
font-size: 14px;
border-radius: 50%;
cursor: pointer;
background: var(--bg-lighter);
color: var(--text-secondary);
margin-left: 6px;
border: none;
transition: all 0.2s ease;
}
#ctrm_ .chat-add {
font-size: 16px;
}
#ctrm_ .chat-add:hover,
#ctrm_ .chat-reconn:hover,
#ctrm_ .chat-close:hover {
background: var(--bg-darker);
color: var(--text-primary);
}
/*--------------------
消息区域
--------------------*/
#ctrm_ .messages {
flex: 1;
position: relative;
color: var(--text-secondary);
overflow: hidden;
}
#ctrm_ .messages-content {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
overflow-y: auto;
padding: 10px 15px;
scrollbar-width: none; /* Firefox */
overscroll-behavior: contain; /* 阻止滚动链 */
touch-action: pan-y; /* 仅允许垂直滚动 */
}
#ctrm_ .messages-content::-webkit-scrollbar {
display: none; /* Chrome/Safari */
}
/*--------------------
消息气泡
--------------------*/
#ctrm_ .message {
margin: 0;
clear: none;
float: none;
display: inline-block;
padding: 6px 10px 7px;
border-radius: 10px 10px 10px 0;
background: rgba(0, 0, 0, 0.3);
font-size: 12px;
line-height: 1.4;
position: relative;
box-shadow: 0 1px 2px rgba(16, 35, 47, 0.15);
max-width: 85%;
min-width: 50px;
word-wrap: break-word;
animation: fadeIn 0.2s ease;
border: none;
color: rgba(255, 255, 255, 0.9);
}
#ctrm_ .message .timestamp {
position: absolute;
right: 5px;
bottom: 2px;
font-size: 9px;
color: rgba(255, 255, 255, 0.5);
}
#ctrm_ .message .username {
display: block;
font-weight: 600;
color: var(--bg-color) !important;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
}
#ctrm_ .message::before {
content: '';
position: absolute;
left: -11px;
bottom: 0;
width: 11px;
height: 20px;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 11 20' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 20h11V0C11 5 8 10 0 20z' fill='rgba(0, 0, 0, 0.3)'/%3E%3C/svg%3E");
background-size: contain;
background-repeat: no-repeat;
}
#ctrm_ .message.message-personal::before {
left: auto;
right: -11px;
transform: scaleX(-1);
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 11 20' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 20h11V0C11 5 8 10 0 20z' fill='%23806e58'/%3E%3C/svg%3E"); /* 使用主题色 */
}
/*--------------------
头像样式
--------------------*/
#ctrm_ .messages .avatar {
position: absolute;
z-index: 1;
left: -6px; // 不要修改
bottom: 0;
transform: none;
border-radius: 30px;
width: 30px;
height: 30px;
margin: 0;
padding: 0;
border: none;
box-shadow: 0 1px 2px rgba(16, 35, 47, 0.15);
display: flex;
align-items: center;
justify-content: center;
background: var(--bg-color);
color: var(--text-color);
font-size: 14px;
font-weight: bold;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
}
#ctrm_ .messages .avatar span {
color: var(--text-primary);
text-shadow: 0 1px 2px var(--shadow-color);
font-size: 16px;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
#ctrm_ .message.message-personal {
margin-left: auto;
margin-right: 0;
color: rgba(255, 255, 255, 0.9);
text-align: left;
background: linear-gradient(120deg,
color-mix(in srgb, var(--primary-color) 90%, transparent),
color-mix(in srgb, var(--primary-hover) 90%, transparent)
);
border-radius: 10px 10px 0 10px;
border: none;
}
#ctrm_ .message.system-message {
background: var(--system-msg-bg);
text-align: center;
float: none;
margin: 8px auto;
clear: both;
color: var(--text-primary);
width: auto;
display: inline-block;
border-radius: 10px; // 四角统一圆角
padding: 6px 15px; // 增加内边距
}
#ctrm_ .message.system-message::before {
display: none;
}
/*--------------------
滚动到底部按钮
--------------------*/
#ctrm_ .scroll-bottom {
position: absolute;
bottom: 20px;
right: 20px;
width: 36px;
height: 36px;
background: color-mix(in srgb, var(--primary-color) 80%, transparent);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-primary);
font-size: 16px;
cursor: pointer;
box-shadow: 0 2px 5px color-mix(in srgb, var(--shadow-color) 20%, transparent);
transition: all 0.2s ease;
z-index: 2;
display: none;
text-align: center;
line-height: 36px;
}
#ctrm_ .scroll-bottom:hover {
background: var(--primary-color);
}
/*--------------------
输入框区域
--------------------*/
#ctrm_ .message-box {
flex: 0 0 auto; /* 改为固定高度 */
padding: 8px 10px;
position: relative;
background: var(--bg-darker);
min-height: 52px; /* 设置最小高度 = padding + input最小高度 */
}
#ctrm_ .message-input {
box-sizing: border-box;
min-height: 36px; /* 设置输入框最小高度 */
max-height: 120px; /* 设置最大高度限制 */
height: 36px; /* 默认高度等于最小高度 */
padding: 8px 10px;
line-height: 20px; /* 设置行高 */
width: calc(100% - 64px); /* 为发送按钮留出空间 */
border-radius: 18px;
resize: none;
background: var(--bg-darker);
border: none;
outline: none;
color: var(--text-primary);
overflow-y: auto; /* 允许垂直滚动 */
transition: height 0.1s ease; /* 添加高度变化动画 */
}
#ctrm_ .message-input::placeholder {
color: var(--text-muted);
}
/* 隐藏所有滚动条但保留滚动功能 */
#ctrm_ .message-input::-webkit-scrollbar {
display: none; /* Chrome/Safari */
}
#ctrm_ .message-input {
scrollbar-width: none; /* Firefox */
}
#ctrm_ .message-submit {
top: 50%;
transform: translateY(-50%);
right: 10px;
margin: 0;
position: absolute;
color: var(--text-primary);
border: none;
background: var(--primary-color);
font-size: 12px;
text-transform: uppercase;
line-height: 1;
padding: 8px 15px;
border-radius: 15px;
outline: none !important;
transition: background .2s ease;
cursor: pointer;
box-shadow: 0 2px 5px color-mix(in srgb, var(--shadow-color) 30%, transparent);
}
#ctrm_ .message-submit:hover {
background: var(--primary-hover);
}
/*--------------------
在线用户面板
--------------------*/
#ctrm_ .online-users {
position: absolute;
right: 0;
top: 0;
width: 130px;
height: 100%;
background: var(--bg-lighter);
transition: transform 0.3s ease;
z-index: 2;
border-left: 1px solid var(--border-color);
}
#ctrm_ .online-users-header {
text-align: center;
padding: 5px 5px;
font-size: 12px;
color: var(--text-secondary);
border-bottom: 1px solid var(--border-color);
}
#ctrm_ .online-users.collapsed {
transform: translateX(130px);
}
#ctrm_ .online-users-content {
height: calc(100% - 34px);
overflow-y: auto;
padding: 5px 5px;
scrollbar-width: none; /* Firefox */
overscroll-behavior: contain;
touch-action: pan-y;
}
#ctrm_ .messages-content.full-width {
width: 100%; /* 当用户面板折叠时使用全宽 */
}
#ctrm_ .online-user {
padding: 6px 10px;
border-radius: 15px;
margin: 4px 0;
font-size: 11px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
transition: all 0.2s ease;
color: var(--text-secondary);
background: var(--bg-lighter);
text-align: center;
}
#ctrm_ .online-user:hover {
transform: translateX(-2px);
}
#ctrm_ .online-user.self {
background: var(--primary-color);
color: var(--text-primary);
}
#ctrm_ .toggle-users-panel {
position: absolute;
left: -11px;
top: 50%;
transform: translateY(-50%);
width: 10px;
height: 50px;
background: var(--bg-darker);
border-radius: 4px 0 0 4px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 12px;
color: var(--text-secondary);
}
#ctrm_ .toggle-users-panel::after {
content: ">";
}
#ctrm_ .online-users.collapsed .toggle-users-panel::after {
content: "<";
}
#ctrm_ .toggle-users-panel:hover {
color: var(--text-primary);
}
/*--------------------
消息容器
--------------------*/
#ctrm_ .message-container {
position: relative;
min-height: 40px;
margin: 16px 0 20px;
clear: both;
padding-left: 35px;
display: flex;
align-items: flex-end;
width: 100%;
}
#ctrm_ .message-container .message {
margin-bottom: 0;
}
#ctrm_.ctrm-close .chat-close::after {
content: "▲";
}
#ctrm_ .chat-close::after {
content: "▼";
}
#ctrm_.ctrm-close .toggle-users-panel {
display: none;
}
#ctrm_ .message-text {
display: block;
margin-top: 4px;
padding-bottom: 2px;
}
#ctrm_ .message-container:has(.message-personal) {
justify-content: flex-end;
padding-left: 0;
padding-right: 35px;
}
#ctrm_ .message-container:has(.message-personal) .avatar {
left: auto;
right: -6px;
bottom: 0;
}
/* 修改收起状态样式 */
#ctrm_.ctrm-close .chat {
height: 45px !important; /* 仅显示标题高度 */
width: auto !important;
min-width: 120px;
}
#ctrm_.ctrm-close .chat-tabs {
max-width: 200px;
overflow: hidden;
}
#ctrm_.ctrm-close .messages,
#ctrm_.ctrm-close .message-box,
#ctrm_.ctrm-close .online-users {
display: none !important;
}
@keyframes tab-pulse {
0% { box-shadow: 0 0 0 0 rgba(255,82,82,0.4); }
70% { box-shadow: 0 0 0 6px rgba(255,82,82,0); }
100% { box-shadow: 0 0 0 0 rgba(255,82,82,0); }
}
#ctrm_ .chat-tab.unread-pulse {
animation: tab-pulse 1.5s infinite;
position: relative;
}
@keyframes fadeIn {
0% { opacity: 0; transform: translateY(10px); }
100% { opacity: 1; transform: translateY(0); }
}
/*--------------------
自定义域名弹窗
--------------------*/
#ctrm_ .custom-domain-popup {
position: absolute;
top: 50px;
right: 10px;
background: var(--bg-dark);
padding: 15px;
border-radius: 10px;
z-index: 10;
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
display: none; /* 默认隐藏 */
width: 250px;
}
#ctrm_ .custom-domain-popup input {
width: 100%;
padding: 8px 10px;
border-radius: 15px;
border: 1px solid var(--border-color);
background: var(--bg-lighter);
color: var(--text-primary);
outline: none;
box-sizing: border-box;
}
#ctrm_ .custom-domain-popup .popup-buttons {
display: flex;
justify-content: flex-end;
margin-top: 10px;
}
#ctrm_ .custom-domain-popup button {
padding: 5px 10px;
border-radius: 15px;
border: none;
background: var(--primary-color);
color: var(--text-primary);
cursor: pointer;
margin-left: 10px;
transition: all 0.2s ease;
}
#ctrm_ .custom-domain-popup button.cancel {
background: var(--bg-lighter);
}
#ctrm_ .custom-domain-popup button:hover {
opacity: 0.9;
}
#ctrm_ .chat-add {
font-size: 16px;
}
`;
}
// WebSocket 相关方法
initAllConnections() {
const domainInfo = this.getDomainInfo();
this.pendingConnections = [];
// 将需要连接的域名加入队列
if (domainInfo.isSpecialSite && domainInfo.specialCode) {
this.pendingConnections.push(domainInfo.specialCode);
}
this.pendingConnections.push(domainInfo.currentHostname);
this.pendingConnections.push(this.HALL_DOMAIN);
// 开始顺序连接
this.connectNextDomain();
}
connectNextDomain() {
if (this.pendingConnections.length === 0) {
console.log("所有聊天室连接尝试完成");
// 检查连接状态并报告
const connectedCount = Object.values(this.domainData).filter(d => d.connected).length;
const totalCount = Object.keys(this.domainData).length;
if (connectedCount === totalCount) {
console.log("所有聊天室连接成功!");
if (this.isReconnecting) {
this.appendSystemMessage("所有聊天室已重新连接成功!");
}
} else {
console.log(`部分聊天室连接失败,成功连接 ${connectedCount}/${totalCount} 个聊天室`);
if (this.isReconnecting) {
this.appendSystemMessage(`部分聊天室连接失败,成功连接 ${connectedCount}/${totalCount} 个聊天室`);
}
}
return;
}
const domain = this.pendingConnections.shift();
console.log(`正在连接到聊天室: ${domain}`);
this.initDomainConnection(domain, () => {
// 成功连接后,延迟一秒再连接下一个,避免并发压力
setTimeout(() => {
this.connectNextDomain();
}, 1000);
}, () => {
// 连接失败后,也继续连接下一个
console.log(`连接到 ${domain} 失败,继续尝试其他聊天室`);
// 在失败后稍微延迟久一点再连接下一个
setTimeout(() => {
this.connectNextDomain();
}, 2000);
});
}
initDomainConnection(domain, onSuccess, onFailure) {
if (!this.domainData[domain]) {
this.domainData[domain] = {
messages: [],
users: [],
unreadCount: 0,
connected: false,
userId: null,
userName: null
};
}
if (!this.activeWebsockets[domain]) {
console.log(`开始创建WebSocket连接: ${domain}`);
// 添加连接超时
let connectTimeout = setTimeout(() => {
console.log(`连接到 ${domain} 超时`);
if (onFailure) onFailure();
// 如果有正在建立的连接,关闭它
if (ws && ws.readyState === 0) {
ws.close();
}
}, 10000); // 10秒超时
const ws = new WebSocket(this.config.wsServer);
ws.onopen = () => {
clearTimeout(connectTimeout);
console.log(`WebSocket连接成功: ${domain}`);
const updateMsg = {
type: 'update',
data: {
domainFrom: domain
},
char: this.authChar
};
ws.send(JSON.stringify(updateMsg));
this.domainData[domain].connected = true;
if (!this.activeDomain) {
this.activeDomain = domain;
this.updateUI();
}
this.updateTabs();
if (onSuccess) onSuccess();
};
ws.onmessage = (event) => this.handleDomainMessage(domain, event);
ws.onclose = (event) => {
console.log(`WebSocket连接关闭: ${domain}, 状态码: ${event.code}, 原因: ${event.reason}`);
clearTimeout(connectTimeout);
if (!this.domainData[domain].connected) {
// 连接过程中被关闭,调用失败回调
if (onFailure) onFailure();
} else {
// 已建立的连接被关闭
this.handleDomainDisconnect(domain);
// 如果不是手动关闭的,尝试在短时间后自动重连(指数退避)
if (!this.isReconnecting && event.code !== 1000) {
const retryDelay = this.getReconnectDelay(domain);
console.log(`将在 ${retryDelay/1000} 秒后尝试重连 ${domain}`);
setTimeout(() => {
if (this.activeWebsockets[domain] === ws) {
this.activeWebsockets[domain] = null;
this.initDomainConnection(domain);
}
}, retryDelay);
}
}
};
ws.onerror = (error) => {
console.error(`WebSocket错误 (${domain}):`, error);
// 错误处理由 onclose 事件处理,这里不需要额外操作
};
this.activeWebsockets[domain] = ws;
this.addDomainTab(domain);
}
}
getReconnectDelay(domain) {
// 存储每个域名的重连次数
this.reconnectAttempts = this.reconnectAttempts || {};
this.reconnectAttempts[domain] = (this.reconnectAttempts[domain] || 0) + 1;
// 指数退避算法: 基础延迟 * 2^(尝试次数),设置上限为30秒
const baseDelay = 1000; // 1秒
const maxDelay = 30000; // 30秒
const reconnectDelay = Math.min(baseDelay * Math.pow(2, this.reconnectAttempts[domain] - 1), maxDelay);
// 添加一些随机抖动,避免多个连接同时重试
return reconnectDelay + Math.random() * 1000;
}
handleDomainMessage(domain, event) {
const message = JSON.parse(event.data);
const type = message.type;
const data = message.data;
switch (type) {
case 'identity':
this.domainData[domain].userId = data.id;
this.domainData[domain].userName = data.name;
if (data.history && data.history.length > 0) {
this.domainData[domain].messages = data.history.map(msg => ({
...msg,
msg: this.decrypt(msg.msg)
}));
if (domain === this.activeDomain) {
this.updateUI();
// 确保历史记录加载后滚动到底部
this.autoScroll = true;
// 使用requestAnimationFrame确保DOM渲染完成后再滚动
requestAnimationFrame(() => {
this.scrollToBottom();
});
}
}
break;
case 'memberList':
this.domainData[domain].users = data.filter(user =>
user.id !== "12523461428" && user.name !== "小尬"
);
// 无论当前活跃域名是什么,都更新标签显示
this.updateTabs();
if (domain === this.activeDomain) {
this.updateOnlineUsers();
}
break;
case 'chat':
data.msg = this.decrypt(data.msg);
this.domainData[domain].messages.push(data);
if (domain === this.activeDomain) {
this.appendMessage(data);
if (this.autoScroll) {
this.scrollToBottom();
}
} else {
this.domainData[domain].unreadCount++;
this.updateTabs();
}
if (this.elements.chatContainer.hasClass('ctrm-close') && domain === this.activeDomain) {
this.domainData[domain].unreadCount++;
this.updateTabs();
}
break;
case 'ack':
if (domain === this.activeDomain) {
this.elements.messageInput.val('');
}
break;
}
this.trimHistory();
}
handleDomainDisconnect(domain) {
this.domainData[domain].connected = false;
this.domainData[domain].users = [];
if (domain === this.activeDomain) {
this.appendSystemMessage("您已掉线,点击重生按钮重新连接...");
this.updateOnlineUsers();
}
this.updateTabs();
}
// 消息相关方法
sendMessage() {
const message = this.sanitizeMessage(
this.elements.messageInput.val().slice(0, 69).trim()
);
if (message.length === 0) {
return alert('消息不能为空');
}
if (!this.activeWebsockets[this.activeDomain] || !this.domainData[this.activeDomain].connected) {
return alert('当前聊天室未连接,请重新连接');
}
if (!this.isSending) {
const chatMessage = {
type: 'chat',
data: {
msg: this.encrypt(message)
},
char: this.authChar
};
try {
this.isSending = true;
this.activeWebsockets[this.activeDomain].send(JSON.stringify(chatMessage));
setTimeout(() => {
this.isSending = false;
}, 5000);
} catch (error) {
console.error('发送消息失败:', error);
alert('发送消息失败,请检查网络连接');
this.isSending = false;
}
}
}
// UI 更新相关方法
updateUI() {
try {
this.elements.chatMessagesContent.empty();
const messages = this.domainData[this.activeDomain]?.messages || [];
messages.forEach(msg => {
const element = this.createMessageElement(msg);
if (element) {
this.elements.chatMessagesContent.append(element);
}
});
// 历史记录加载时,总是滚动到底部
this.autoScroll = true;
// 使用requestAnimationFrame确保DOM渲染完成后再滚动
requestAnimationFrame(() => {
this.scrollToBottom();
});
this.updateOnlineUsers();
this.updateTabs();
} catch (error) {
console.error('Error updating UI:', error);
this.appendSystemMessage("更新界面时出现错误");
}
}
createMessageElement(data) {
// 检查所需数据是否存在
if (!data || !data.id) return null;
const time = new Date(data.time);
const timeStr = `${time.getHours().toString().padStart(2, '0')}:${time.getMinutes().toString().padStart(2, '0')}`;
const isMe = data.id === this.domainData[this.activeDomain].userId;
const isSystem = data.id === "system";
let $messageElement;
if (isSystem) {
// 系统消息保持简单
const messageText = this.sanitizeHTML(data.msg || '');
$messageElement = $(`<div class="message system-message"></div>`).html(this.convertUrlsToLinks(messageText));
} else {
// 处理消息内容,先解密再转换URL
const messageText = this.sanitizeHTML(data.msg || '');
const messageHtml = this.convertUrlsToLinks(messageText);
const firstChar = data.name.charAt(0);
const bgColor = this.generateElegantColor(data.name);
const textColor = this.getContrastColor(bgColor);
if (!isMe) {
$messageElement = $(`
<div class="message-container">
<figure class="avatar" style="--bg-color:${bgColor}; --text-color:${textColor}">
<span>${firstChar}</span>
</figure>
<div class="message" style="--bg-color:${bgColor}">
<div class="username">${data.name}</div>
<span class="message-text">${messageHtml}</span>
<div class="timestamp">${timeStr}</div>
</div>
</div>
`);
} else {
$messageElement = $(`
<div class="message-container">
<div class="message message-personal" style="--bg-color:${bgColor}">
<div class="username">${data.name}</div>
<span class="message-text">${messageHtml}</span>
<div class="timestamp">${timeStr}</div>
</div>
<figure class="avatar" style="--bg-color:${bgColor}; --text-color:${textColor}">
<span>${firstChar}</span>
</figure>
</div>
`);
}
// 异步处理URL预览
const messageElement = $messageElement[0];
if (data.msg) {
this.processMessageUrls(messageElement, data.msg);
}
}
return $messageElement[0];
}
// 域名检测和特殊站点处理
getDomainInfo() {
const currentUrl = window.location.href;
const currentHostname = location.hostname;
const result = {
currentHostname: currentHostname,
specialCode: null,
isSpecialSite: false
};
/* 不创建特定房间
if (currentUrl.includes('missav')) {
result.isSpecialSite = true;
const code = this.extractMissavCode(currentUrl);
if (code) {
result.specialCode = code + '.av';
} else {
console.log('未能提取到有效番号,将不连接特定房间');
}
} else if (currentUrl.includes('jable')) {
result.isSpecialSite = true;
const code = this.extractJableCode(currentUrl);
if (code) {
result.specialCode = code + '.av';
} else {
console.log('未能提取到有效番号,将不连接特定房间');
}
}
*/
return result;
}
// 提取 Missav 网站的番号
extractMissavCode(currentUrl) {
console.log("尝试提取 Missav 番号:", currentUrl);
// 首先确认是否为 Missav 域名
if (!/https?:\/\/(www\.)?missav\.(com|ai|ws|net)/i.test(currentUrl)) {
console.log('不是missav网站,无法提取');
return null;
}
// 去除URL中的锚点部分 (#时间戳等)
const urlWithoutHash = currentUrl.split('#')[0];
// 尝试从路径中提取番号
let potentialCode = null;
// 处理 /dm2/cn/miab-314 这类格式
const complexPathMatch = urlWithoutHash.match(/\/(?:dm2|ja|cn|en|ko|xxx)\/(?:[a-zA-Z]{2,}\/)?([a-zA-Z0-9]+-[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)?)/);
if (complexPathMatch && complexPathMatch[1]) {
potentialCode = complexPathMatch[1];
console.log('从复杂路径提取番号:', potentialCode);
} else {
// 从URL的最后一段提取
const urlParts = urlWithoutHash.split('/');
const lastPart = urlParts[urlParts.length - 1];
// 排除纯数字和过短的段落
if (/^\d+$/.test(lastPart) || lastPart.length < 4) {
console.log('忽略无效路径段:', lastPart);
return null;
}
// 特殊字符分隔的情况
if (lastPart.includes('-')) {
const segments = lastPart.split('-');
// FC2-PPV特殊处理
if (lastPart.toLowerCase().startsWith('fc2-ppv')) {
potentialCode = `fc2-ppv-${segments[2]}`;
}
// caribbeancom特殊处理
else if (lastPart.toLowerCase().startsWith('caribbeancom')) {
potentialCode = segments.slice(0, 3).join('-');
}
// 常规番号处理 (通常为 XXX-XXX 格式)
else if (/^[a-zA-Z]{2,5}-\d{3,6}/.test(lastPart)) {
potentialCode = segments.slice(0, 2).join('-');
}
// 其他横杠分隔的格式
else if (segments.length >= 2) {
potentialCode = segments.slice(0, 2).join('-');
}
}
// 没有连字符但看起来可能是有效番号(字母+数字组合)
else if (/^[a-zA-Z]+\d+$/.test(lastPart)) {
potentialCode = lastPart;
}
}
// 验证提取的番号是否有效
if (potentialCode) {
// 番号必须包含字母和数字,且有横杠,长度合理
if (/^[a-zA-Z0-9]+-[a-zA-Z0-9]+/.test(potentialCode) &&
potentialCode.length >= 4 && potentialCode.length <= 20) {
console.log('成功提取到有效番号:', potentialCode);
return potentialCode;
}
}
console.log('无法提取到有效番号');
return null;
}
// 提取 Jable 网站的番号
extractJableCode(currentUrl) {
console.log("尝试提取 Jable 番号:", currentUrl);
// 确认是否为 Jable 域名
if (!/https?:\/\/(www\.)?jable\.tv/i.test(currentUrl)) {
console.log('不是jable网站,无法提取');
return null;
}
// 从URL中提取番号
const urlParts = currentUrl.split('/');
let videoId = null;
// 查找videos后面的部分
for (let i = 0; i < urlParts.length; i++) {
if (urlParts[i] === 'videos' && i + 1 < urlParts.length) {
videoId = urlParts[i + 1];
break;
}
}
if (!videoId) {
console.log('无法从jable网址中提取视频ID');
return null;
}
// 移除可能的尾部斜杠和锚点
videoId = videoId.split('#')[0].replace(/\/$/, '');
// 如果videoId太短,可能不是有效番号
if (videoId.length < 4) {
console.log('提取的ID太短,可能不是有效番号:', videoId);
return null;
}
let processedCode = null;
// 处理不同情况
// 情况1: 纯数字带连字符 (如 011209-959) -> 转为 caribbeancom-011209-959
if (/^\d{6}-\d{3}$/.test(videoId)) {
processedCode = `caribbeancom-${videoId}`;
}
// 情况2: fc2ppv-数字 -> 转为 fc2-ppv-数字
else if (/^fc2ppv-\d+/.test(videoId.toLowerCase())) {
const fc2Num = videoId.split('-')[1];
processedCode = `fc2-ppv-${fc2Num}`;
}
// 情况3: 标准番号带后缀 (如 snis-420-c) -> 移除后缀
else if (/^[a-zA-Z]+-\d+(-[a-zA-Z])?$/.test(videoId)) {
const parts = videoId.split('-');
if (parts.length > 2 && parts[2].length <= 2) { // 假设后缀很短
processedCode = `${parts[0]}-${parts[1]}`;
} else {
processedCode = videoId;
}
}
// 情况4: 标准番号 (如 snis-420)
else if (/^[a-zA-Z]+-\d+$/.test(videoId)) {
processedCode = videoId;
}
// 其他未识别格式,返回null
else {
console.log('未知格式的jable视频ID:', videoId);
return null;
}
if (processedCode && processedCode.length >= 4) {
console.log('成功提取到有效番号:', processedCode);
return processedCode;
}
console.log('无法提取到有效番号');
return null;
}
// 标签管理相关方法
addDomainTab(domain) {
if (this.elements.chatTabs.find(`.chat-tab[data-domain="${domain}"]`).length === 0) {
let displayName = domain;
if (domain === this.HALL_DOMAIN) {
displayName = "大厅";
} else if (domain === location.hostname) {
displayName = "当前站点";
} else if (domain.endsWith('.av')) {
displayName = domain.replace('.av', '');
}
const tab = $(`
<div class="chat-tab" data-domain="${domain}">
${displayName} <span class="user-count">(0)</span>
</div>
`);
tab.on('click', () => this.switchDomain(domain));
this.elements.chatTabs.append(tab);
if (this.elements.chatTabs.find('.chat-tab').length === 1) {
tab.addClass('active');
}
}
}
updateTabs() {
this.elements.chatTabs.find('.chat-tab').each((_, tab) => {
const $tab = $(tab);
const domain = $tab.data('domain');
const domainInfo = this.domainData[domain];
$tab.removeClass('active disconnected unread-pulse');
$tab.find('.unread-indicator').remove();
// 更新在线人数
const userCount = domainInfo.users ? domainInfo.users.length : 0;
$tab.find('.user-count').text(`(${userCount})`);
if (domain === this.activeDomain) {
$tab.addClass('active');
if (!this.elements.chatContainer.hasClass('ctrm-close')) {
domainInfo.unreadCount = 0;
}
}
if (!domainInfo.connected) {
$tab.addClass('disconnected');
}
if (domainInfo.unreadCount > 0 &&
(this.elements.chatContainer.hasClass('ctrm-close') || domain !== this.activeDomain)) {
$tab.append(`<span class="unread-indicator"></span>`);
$tab.addClass('unread-pulse');
}
});
}
// 域名切换相关方法
switchDomain(domain) {
if (domain === this.activeDomain) return;
const previousTab = this.elements.chatTabs.find(`.chat-tab[data-domain="${this.activeDomain}"]`);
previousTab.removeClass('unread-pulse');
this.activeDomain = domain;
this.domainData[domain].unreadCount = 0;
this.elements.chatMessagesContent.empty();
if (this.domainData[domain] && this.domainData[domain].messages) {
this.domainData[domain].messages.forEach(msg => {
const element = this.createMessageElement(msg);
if (element) {
this.elements.chatMessagesContent.append(element);
}
});
}
this.updateOnlineUsers();
this.updateTabs();
// 切换域名时,确保滚动到底部
this.autoScroll = true;
// 使用requestAnimationFrame确保DOM渲染完成后再滚动
requestAnimationFrame(() => {
this.scrollToBottom();
});
}
// 重连相关方法
reconnect() {
if (!this.isReconnecting) {
this.isReconnecting = true;
// 关闭并清理所有聊天室连接
Object.keys(this.activeWebsockets).forEach(domain => {
if (this.activeWebsockets[domain]) {
this.activeWebsockets[domain].close();
this.activeWebsockets[domain] = null;
}
this.domainData[domain].messages = [];
this.domainData[domain].users = [];
this.domainData[domain].connected = false;
// 重置重连计数器
if (this.reconnectAttempts) {
this.reconnectAttempts[domain] = 0;
}
});
// 关闭后台跟踪连接
if (this.trackingWs) {
// 清除定时器
if (this.trackingInterval) {
clearInterval(this.trackingInterval);
this.trackingInterval = null;
}
// 关闭连接
this.trackingWs.close();
this.trackingWs = null;
}
this.elements.chatMessagesContent.empty();
this.elements.messageInput.val('');
this.appendSystemMessage("正在重新连接所有聊天室...");
// 使用新的顺序连接方法
this.initAllConnections();
// 重新初始化后台跟踪连接
setTimeout(() => {
this.initBackgroundTracking();
}, 1000);
setTimeout(() => {
this.isReconnecting = false;
}, 2000);
}
}
// 消息历史记录管理
trimHistory() {
if (this.domainData[this.activeDomain].messages.length > this.config.maxHistory) {
this.domainData[this.activeDomain].messages =
this.domainData[this.activeDomain].messages.slice(-this.config.maxHistory);
}
}
// 在线用户管理
updateOnlineUsers() {
this.elements.onlineUsersContent.empty();
const currentUsers = this.domainData[this.activeDomain]?.users || [];
const currentUserId = this.domainData[this.activeDomain]?.userId;
// 更新在线人数显示
this.elements.onlineUsersHeader.text(`在线人数:${currentUsers.length}`);
currentUsers.forEach(user => {
const isCurrentUser = user.id === currentUserId;
const userClass = isCurrentUser ? 'online-user self' : 'online-user';
const userElement = $(`
<div class="${userClass}" data-id="${user.id}">
${user.name}
</div>
`);
this.elements.onlineUsersContent.append(userElement);
});
}
// 滚动处理相关方法
handleScroll() {
const el = this.elements.chatMessagesContent[0];
const clientHeight = el.clientHeight;
const scrollTop = el.scrollTop;
this.autoScroll = clientHeight + scrollTop >= el.scrollHeight * 0.9;
this.elements.scrollBottomBtn.toggle(!this.autoScroll);
}
scrollToBottom() {
const el = this.elements.chatMessagesContent[0];
el.scrollTop = el.scrollHeight;
}
// 辅助方法
sanitizeMessage(message) {
return message.replace(/</g, '<').replace(/>/g, '>');
}
appendSystemMessage(message) {
const systemMsg = {
time: Date.now(),
id: "system",
name: "系统消息",
msg: message
};
this.appendMessage(systemMsg);
}
appendMessage(data, scroll = true) {
try {
const element = this.createMessageElement(data);
if (element) {
this.elements.chatMessagesContent.append(element);
if (scroll && this.autoScroll) {
requestAnimationFrame(() => this.scrollToBottom());
}
}
} catch (error) {
console.error('添加消息错误:', error);
}
}
handleOutsideClick(event) {
if (!this.elements.chatContainer.is(event.target) &&
this.elements.chatContainer.has(event.target).length === 0 &&
!this.elements.chatContainer.hasClass('ctrm-close')) {
this.elements.chatContainer.addClass('ctrm-close');
}
}
adjustUI() {
if (window.innerWidth < 768) {
this.elements.chatContainer.addClass('ctrm-mobile');
} else {
this.elements.chatContainer.removeClass('ctrm-mobile');
}
}
// URL识别和转换方法
convertUrlsToLinks(text) {
if (!text) return '';
const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/gi;
return text.replace(urlRegex, (url) => {
return `<a href="${url}" target="_blank" rel="noopener noreferrer" class="ctrm-url-link">${url}</a>`;
});
}
// 获取URL域名
extractDomain(url) {
try {
const urlObj = new URL(url);
return urlObj.hostname;
} catch (e) {
return null;
}
}
// 检查URL是否支持预览
isSupportedDomain(url) {
const domain = this.extractDomain(url);
if (!domain) return false;
return this.supportedDomains.some(supportedDomain =>
domain.includes(supportedDomain)
);
}
// 使用GM_xmlhttpRequest加载URL内容
loadUrlContent(url) {
return new Promise((resolve, reject) => {
// 如果已经缓存,直接返回缓存结果
if (this.urlCache[url]) {
resolve(this.urlCache[url]);
return;
}
GM_xmlhttpRequest({
method: 'GET',
url: url,
timeout: 10000, // 10秒超时
onload: (response) => {
if (response.status === 200) {
resolve(response.responseText);
} else {
reject(new Error(`请求失败,状态码: ${response.status}`));
}
},
onerror: (error) => {
reject(error);
},
ontimeout: () => {
reject(new Error('请求超时'));
}
});
});
}
// 解析MissAV网站内容
parseMissAV(html) {
try {
// 提取标题
const titleMatch = html.match(/<title>(.*?)<\/title>/i);
const title = titleMatch ? titleMatch[1].trim() : '';
// 提取封面图
const ogImageMatch = html.match(/<meta property="og:image" content="(.*?)"/i);
const imageUrl = ogImageMatch ? ogImageMatch[1] : '';
return { title, imageUrl };
} catch (e) {
console.error('解析MissAV内容失败:', e);
return { title: '', imageUrl: '' };
}
}
// 解析Jable网站内容
parseJable(html) {
try {
// 提取标题
const titleMatch = html.match(/<title>(.*?)<\/title>/i);
const title = titleMatch ? titleMatch[1].trim() : '';
// 提取封面图
const ogImageMatch = html.match(/<meta property="og:image" content="(.*?)"/i);
const imageUrl = ogImageMatch ? ogImageMatch[1] : '';
return { title, imageUrl };
} catch (e) {
console.error('解析Jable内容失败:', e);
return { title: '', imageUrl: '' };
}
}
// 创建预览元素
createPreviewElement(url, previewData) {
const domain = this.extractDomain(url);
const previewElement = document.createElement('div');
previewElement.className = 'ctrm-url-preview';
// 缓存解析结果
this.urlCache[url] = previewData;
// 标题和域名部分
let previewHtml = `
<div class="ctrm-preview-content">
<div class="ctrm-preview-title">
<a href="${url}" target="_blank" rel="noopener noreferrer">${previewData.title || '无标题'}</a>
</div>
<div class="ctrm-preview-domain">${domain || '未知来源'}</div>
</div>
`;
// 如果有图片,添加图片预览并使用GM_xmlhttpRequest避免CORS问题
if (previewData.imageUrl) {
// 添加一个占位图,稍后异步加载实际图片
const imgId = `img-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
// 图片在内容下方
previewHtml += `
<div class="ctrm-preview-image-container">
<img id="${imgId}" class="ctrm-preview-image" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100%25' height='150' viewBox='0 0 300 150'%3E%3Crect width='100%25' height='100%25' fill='%23f1f1f1'/%3E%3Ctext x='50%25' y='50%25' font-size='14' text-anchor='middle' dominant-baseline='middle' fill='%23999'%3E加载中...%3C/text%3E%3C/svg%3E" alt="预览图">
</div>
`;
// 异步加载图片,避免CORS问题
this.loadImageWithGM(previewData.imageUrl, imgId);
}
previewElement.innerHTML = previewHtml;
// 为图片添加点击事件,实现放大预览
setTimeout(() => {
const img = previewElement.querySelector('.ctrm-preview-image');
if (img) {
img.style.cursor = 'pointer';
img.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
this.showImagePreview(img.src);
});
}
}, 100);
return previewElement;
}
// 添加方法:使用GM_xmlhttpRequest加载图片避免CORS
loadImageWithGM(imageUrl, imgId) {
GM_xmlhttpRequest({
method: 'GET',
url: imageUrl,
responseType: 'blob',
onload: (response) => {
if (response.status === 200) {
const blob = response.response;
const imgElement = document.getElementById(imgId);
if (imgElement) {
const reader = new FileReader();
reader.onload = (e) => {
imgElement.src = e.target.result;
};
reader.readAsDataURL(blob);
}
} else {
console.error('加载预览图失败:', response.status);
}
},
onerror: (error) => {
console.error('加载预览图错误:', error);
}
});
}
// 处理消息中的URL,提取并添加预览
async processMessageUrls(messageElement, messageText) {
try {
// 确保处理解密后的文本
if (messageText.startsWith(this.PREFIX)) {
messageText = this.decrypt(messageText);
}
// 使用正则表达式找出所有URL
const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/gi;
const urls = messageText.match(urlRegex);
if (!urls || urls.length === 0) return;
// 只处理第一个URL
const url = urls[0];
// 检查是否是支持的域名
if (!this.isSupportedDomain(url)) return;
try {
// 找到消息文本元素
const messageTextElement = $(messageElement).find('.message-text');
if (messageTextElement.length === 0) return;
// 加载URL内容
const html = await this.loadUrlContent(url);
// 根据域名选择合适的解析方法
let previewData = { title: '', imageUrl: '' };
const domain = this.extractDomain(url);
if (domain.includes('missav')) {
previewData = this.parseMissAV(html);
} else if (domain.includes('jable.tv')) {
previewData = this.parseJable(html);
}
// 如果成功解析出标题或图片,创建预览元素
if (previewData.title || previewData.imageUrl) {
const previewElement = this.createPreviewElement(url, previewData);
// 将预览添加到消息文本元素内部
messageTextElement.append(previewElement);
// 如果自动滚动开启,滚动到底部
if (this.autoScroll) {
this.scrollToBottom();
}
}
} catch (error) {
console.error('处理URL预览失败:', error);
// 错误处理:当无法获取网页内容或解析失败时,不添加预览
}
} catch (e) {
console.error('处理消息URL错误:', e);
}
}
sanitizeHTML(message) {
if (!message) return '';
// 如果是加密消息,先解密
if (message.startsWith(this.PREFIX)) {
message = this.decrypt(message);
}
// 基本的HTML转义,防止XSS攻击
return message.replace(/</g, '<').replace(/>/g, '>');
}
// 添加图片放大预览方法
showImagePreview(imageSrc) {
// 如果已有预览窗口,先移除
let existingPreview = document.getElementById('ctrm-image-preview-modal');
if (existingPreview) {
existingPreview.remove();
}
// 创建预览窗口
const modal = document.createElement('div');
modal.id = 'ctrm-image-preview-modal';
modal.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
cursor: pointer;
`;
// 创建图片元素
const img = document.createElement('img');
img.src = imageSrc;
img.style.cssText = `
max-width: 90%;
max-height: 90%;
object-fit: contain;
border: 2px solid white;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
`;
// 创建关闭按钮
const closeBtn = document.createElement('div');
closeBtn.innerHTML = '×';
closeBtn.style.cssText = `
position: absolute;
top: 15px;
right: 25px;
font-size: 40px;
color: white;
cursor: pointer;
`;
// 添加关闭事件
modal.addEventListener('click', () => {
modal.remove();
});
// 添加元素到DOM
modal.appendChild(img);
modal.appendChild(closeBtn);
document.body.appendChild(modal);
}
// 添加以下方法到 ChatRoom 类中
showCustomDomainPopup() {
this.elements.customDomainInput.val('');
this.elements.customDomainPopup.show();
this.elements.customDomainInput.focus();
}
hideCustomDomainPopup() {
this.elements.customDomainPopup.hide();
}
connectCustomDomain() {
const domain = this.elements.customDomainInput.val().trim();
if (!domain) {
alert('请输入有效的域名');
return;
}
// 检查域名格式
if (!/^[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9](?:\.[a-zA-Z]{2,})+$/.test(domain) &&
!/^[a-zA-Z0-9-]+\.av$/.test(domain)) {
alert('请输入有效的域名格式');
return;
}
// 检查UI标签是否已存在
const existingTab = this.elements.chatTabs.find(`.chat-tab[data-domain="${domain}"]`);
// 如果已存在该聊天室(数据或UI标签已存在)
if (this.domainData[domain] || existingTab.length > 0) {
console.log(`聊天室 ${domain} 已存在,切换到该聊天室`);
this.switchDomain(domain);
this.hideCustomDomainPopup();
return;
}
// 初始化并连接到新域名
this.initDomainConnection(domain,
// 成功回调
() => {
this.appendSystemMessage(`成功连接到 ${domain} 聊天室`);
this.switchDomain(domain);
this.hideCustomDomainPopup();
},
// 失败回调
() => {
this.appendSystemMessage(`连接 ${domain} 聊天室失败`);
this.hideCustomDomainPopup();
}
);
}
}
// 主函数------------------------------------------------------------------
//
// 确保 jQuery 已加载
if (typeof jQuery === 'undefined') {
console.error('jQuery is required but not loaded!');
return;
}
const chatRoom = new ChatRoom({
wsServer: 'wss://topurl.cn:9001',
maxHistory: 300
});
/**
* 主程序入口
* - 初始化聊天室
* - 检测 missav 网站并开启额外功能
*/
const App = {
init() {
// 使用 MutationObserver 监听 DOM 变化
const observer = new MutationObserver((mutations, obs) => {
// 当 body 元素存在时初始化
if (document.body) {
obs.disconnect(); // 停止观察
this.initWithDomainCheck();
}
});
// 如果 body 已存在则直接初始化
if (document.body) {
this.initWithDomainCheck();
} else {
// 否则开始观察 DOM 变化
observer.observe(document.documentElement, {
childList: true,
subtree: true
});
}
},
initWithDomainCheck() {
// 初始化聊天室
chatRoom.init();
// 检查当前域名是否包含 missav
const isMissAV = window.location.hostname.includes('missav');
if (isMissAV) {
console.log('检测到 MissAV 网站,初始化额外功能...');
// 设置背景色
document.body.style.background = '#000';
// 初始化广告拦截器
AdBlocker.init();
// 初始化播放器增强
PlayerEnhancer.init();
}
}
};
// 启动程序
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => App.init());
} else {
App.init();
}
})();