// ==UserScript==
// @name Enhanced_Media_Helper
// @version 2.7.6
// @description Enhanced media downloader with multiple site support
// @author cores
// @match https://jable.tv/videos/*
// @match https://tokyolib.com/*
// @match https://javgg.net/tag/to-be-release/*
// @match https://javgg.net/featured/*
// @match https://javgg.net/
// @match https://javgg.net/new-post/*
// @match https://javgg.net/jav/*
// @match https://javgg.net/star/*
// @match https://javgg.net/trending/*
// @match https://www.javbus.com/*
// @include /.*javtxt.[a-z]+\/v/.*$/
// @include /.*javtxt.[a-z]+\/.*$/
// @include /.*javtext.[a-z]+\/v/.*$/
// @match https://cableav.tv/?p=*
// @require https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js
// @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @connect api-shoulei-ssl.xunlei.com
// @connect subtitle.v.geilijiasu.com
// @license MPL
// @namespace cdn.bootcss.com
// ==/UserScript==
(function () {
'use strict';
let EMH_currentVideoCode = null;
function updateGlobalVideoCode(code) {
if (code) {
EMH_currentVideoCode = code;
console.log("EMH: Global video code updated:", EMH_currentVideoCode);
// The draggable button no longer relies on this for being enabled,
// but EMH_currentVideoCode is used as a default in its prompt.
}
}
const CONFIG = {
serverMode: 2,
serverPort: 9909,
alternateUrl: {
av123:'https://123av.com/zh/v/',
jable:'https://jable.tv/videos/',
cili1: 'https://1cili.com/search?q=',
},
subtitleApiUrl: 'https://api-shoulei-ssl.xunlei.com/oracle/subtitle',
elementCheckInterval: 200,
elementCheckTimeout: 7000,
searchHistoryKey: 'emh_subtitle_search_history',
maxHistoryItems: 10,
animationDuration: 300,
toastDuration: 3000,
// 字幕文件名处理选项
subtitleFilenameOptions: {
useOriginalName: true, // 使用API返回的原始name属性,不做修改
addCodePrefix: false, // 已弃用:是否添加影片编码作为前缀(当useOriginalName=false时使用)
removeIllegalChars: true, // 是否移除非法字符
maxLength: 100 // 文件名最大长度
},
// 番号管理相关配置
codeManager: {
storageKey: 'emh_code_library',
trashStorageKey: 'emh_code_trash',
trashRetentionDays: 7, // 回收站保留天数
autoAddDetected: true, // 自动添加检测到的番号
defaultPage: 'all', // 默认页面: all, favorite, watched, trash
statusColors: {
unmarked: '#909090', // 未标记 - 灰色
favorite: '#ff4757', // 关注 - 红色
watched: '#2ed573' // 已看 - 绿色
}
}
};
// 番号管理库
const CODE_LIBRARY = {
// 数据结构
data: null,
trash: null,
initialized: false,
// 初始化库
init: function() {
if (this.initialized) return true;
try {
// 主库
const savedData = GM_getValue(CONFIG.codeManager.storageKey);
this.data = savedData ? JSON.parse(savedData) : {
items: [],
lastUpdated: new Date().toISOString()
};
// 回收站
const savedTrash = GM_getValue(CONFIG.codeManager.trashStorageKey);
this.trash = savedTrash ? JSON.parse(savedTrash) : {
items: [],
lastUpdated: new Date().toISOString()
};
// 清理过期回收站条目
this.cleanupTrash();
this.initialized = true;
return true;
} catch (e) {
console.error('番号库初始化失败:', e);
this.data = { items: [], lastUpdated: new Date().toISOString() };
this.trash = { items: [], lastUpdated: new Date().toISOString() };
this.initialized = true;
return false;
}
},
// 保存数据
save: function() {
try {
// 更新时间戳
this.data.lastUpdated = new Date().toISOString();
const dataString = JSON.stringify(this.data);
GM_setValue(CONFIG.codeManager.storageKey, dataString);
this.trash.lastUpdated = new Date().toISOString();
GM_setValue(CONFIG.codeManager.trashStorageKey, JSON.stringify(this.trash));
// 触发自定义事件
const event = new CustomEvent('emh_library_updated', {
detail: {
type: 'library_update',
data: this.data
}
});
window.dispatchEvent(event);
// 同步更新所有打开的标签页
if (typeof GM_setValue !== 'undefined') {
// 使用时间戳作为更新标记
GM_setValue('emh_sync_timestamp', Date.now().toString());
}
return true;
} catch (e) {
console.error('保存番号库失败:', e);
UTILS.showToast('保存番号库失败', 'error');
return false;
}
},
// 获取所有番号
getAll: function() {
if (!this.initialized) this.init();
return [...this.data.items];
},
// 获取关注列表
getFavorites: function() {
if (!this.initialized) this.init();
return this.data.items.filter(item => item.status === 'favorite');
},
// 获取已看记录
getWatched: function() {
if (!this.initialized) this.init();
return this.data.items.filter(item => item.status === 'watched');
},
// 获取回收站内容
getTrash: function() {
if (!this.initialized) this.init();
return [...this.trash.items];
},
// 添加新番号
add: function(code, title = '', remarks = '') {
if (!this.initialized) this.init();
if (!code) return false;
// 标准化番号格式(大写)
const normalizedCode = code.toUpperCase();
// 检查是否已存在
if (this.getItem(normalizedCode)) {
UTILS.showToast(`番号 ${normalizedCode} 已存在于番号库中`, 'warning');
return false;
}
// 创建新条目
const newItem = {
code: normalizedCode,
title: title || normalizedCode,
status: 'unmarked',
remarks: remarks || '',
tags: [],
createdDate: new Date().toISOString(),
modifiedDate: new Date().toISOString()
};
this.data.items.unshift(newItem);
this.save();
return true;
},
// 删除番号(移至回收站)
delete: function(code) {
if (!this.initialized) this.init();
if (!code) return false;
// 标准化番号格式
const normalizedCode = code.toUpperCase();
// 查找条目
const itemIndex = this.data.items.findIndex(item => item.code.toUpperCase() === normalizedCode);
if (itemIndex === -1) return false; // 不存在
// 添加删除日期并移至回收站
const item = this.data.items[itemIndex];
item.deleteDate = new Date().toISOString();
// 从主库中删除
this.data.items.splice(itemIndex, 1);
// 添加到回收站
this.trash.items.unshift(item);
return this.save();
},
// 清理回收站中过期的条目
cleanupTrash: function() {
if (!this.trash || !this.trash.items || !this.trash.items.length) return;
const now = new Date();
const retentionPeriod = CONFIG.codeManager.trashRetentionDays * 24 * 60 * 60 * 1000; // 转换为毫秒
this.trash.items = this.trash.items.filter(item => {
const deleteDate = new Date(item.deleteDate);
return (now - deleteDate) < retentionPeriod;
});
this.save();
},
// 获取单个番号的信息
getItem: function(code) {
if (!this.initialized) this.init();
if (!code) return null;
// 标准化番号格式(大写)
const normalizedCode = code.toUpperCase();
return this.data.items.find(item => item.code.toUpperCase() === normalizedCode);
},
// 获取番号状态
getStatus: function(code) {
const item = this.getItem(code);
return item ? item.status : 'unmarked';
},
// 标记番号
markItem: function(code, status, title = '', remark = '') {
if (!this.initialized) this.init();
if (!code) return false;
// 标准化番号格式(大写)
const normalizedCode = code.toUpperCase();
// 检查状态是否有效
if (!['unmarked', 'favorite', 'watched'].includes(status)) {
status = 'unmarked';
}
// 检查是否已存在
const existingIndex = this.data.items.findIndex(item => item.code.toUpperCase() === normalizedCode);
if (existingIndex >= 0) {
// 更新现有条目
this.data.items[existingIndex].status = status;
// 只在提供了新值时更新这些字段
if (title) this.data.items[existingIndex].title = title;
if (remark !== undefined) this.data.items[existingIndex].remark = remark;
// 更新修改时间
this.data.items[existingIndex].modifiedDate = new Date().toISOString();
} else {
// 创建新条目
const newItem = {
code: normalizedCode,
title: title || normalizedCode,
status: status,
remark: remark || '',
tags: [],
createdDate: new Date().toISOString(),
modifiedDate: new Date().toISOString()
};
this.data.items.unshift(newItem); // 添加到数组开头
}
return this.save();
},
// 导出数据
exportData: function(filter = 'all') {
if (!this.initialized) this.init();
let exportData = {
version: "1.0",
exportDate: new Date().toISOString(),
filter: filter,
items: []
};
// 确定导出的数据
if (filter === 'trash') {
exportData.items = [...this.trash.items];
} else if (filter === 'all') {
exportData.items = [...this.data.items];
} else {
exportData.items = this.data.items.filter(item => item.status === filter);
}
return exportData;
},
// 导入数据
importData: function(data, mode = 'merge') {
if (!this.initialized) this.init();
try {
// 验证数据格式
if (!data.items || !Array.isArray(data.items)) {
throw new Error('导入的数据格式不正确');
}
if (mode === 'replace') {
// 替换模式:完全覆盖现有数据
this.data.items = data.items;
} else if (mode === 'merge') {
// 合并模式:更新已有条目,添加新条目
for (const importedItem of data.items) {
if (!importedItem.code) continue;
const normalizedCode = importedItem.code.toUpperCase();
const existingIndex = this.data.items.findIndex(item =>
item.code.toUpperCase() === normalizedCode
);
if (existingIndex >= 0) {
// 更新现有条目
this.data.items[existingIndex] = {
...this.data.items[existingIndex],
...importedItem,
code: normalizedCode,
modifiedDate: new Date().toISOString()
};
} else {
// 添加新条目
const newItem = {
...importedItem,
code: normalizedCode,
createdDate: importedItem.createdDate || new Date().toISOString(),
modifiedDate: new Date().toISOString()
};
this.data.items.unshift(newItem);
}
}
}
this.save();
return {
success: true,
message: `成功导入 ${data.items.length} 个番号条目`
};
} catch (e) {
console.error('导入番号数据失败:', e);
return {
success: false,
message: '导入失败: ' + e.message
};
}
}
};
// 获取搜索历史
function getSearchHistory() {
try {
const history = localStorage.getItem(CONFIG.searchHistoryKey);
return history ? JSON.parse(history) : [];
} catch (e) {
console.error('读取搜索历史失败:', e);
return [];
}
}
// 保存搜索历史
function saveSearchHistory(term) {
if (!term || term.trim() === '') return;
try {
let history = getSearchHistory();
// 移除已存在的相同条目
history = history.filter(item => item.toLowerCase() !== term.toLowerCase());
// 添加到开头
history.unshift(term);
// 限制数量
if (history.length > CONFIG.maxHistoryItems) {
history = history.slice(0, CONFIG.maxHistoryItems);
}
localStorage.setItem(CONFIG.searchHistoryKey, JSON.stringify(history));
} catch (e) {
console.error('保存搜索历史失败:', e);
}
}
// 清除搜索历史
function clearSearchHistory() {
try {
localStorage.removeItem(CONFIG.searchHistoryKey);
return true;
} catch (e) {
console.error('清除搜索历史失败:', e);
return false;
}
}
// 字幕管理模块
const SUBTITLE_MANAGER = {
// 获取字幕列表
fetchSubtitles: (searchTerm) => {
if (!searchTerm || searchTerm.trim() === "") {
UTILS.showToast("请输入有效的字幕搜索关键字", "error");
return;
}
const searchTermTrimmed = searchTerm.trim();
UTILS.showToast(`正在为 "${searchTermTrimmed}" 获取字幕信息...`, "info");
const buttonsToDisable = [
document.getElementById('emh-getSubtitles'), // Main auto-detect button
...document.querySelectorAll(`.emh-subtitle-button-small[data-video-code]`), // All small per-item buttons
document.getElementById('emh-draggable-custom-subtitle-btn') // Draggable custom search button
].filter(Boolean);
buttonsToDisable.forEach(btn => {
btn.disabled = true;
if (btn.classList.contains('btn')) {
btn.classList.add('btn-disabled');
}
});
const apiUrl = `${CONFIG.subtitleApiUrl}?name=${encodeURIComponent(searchTermTrimmed)}`;
const reEnableButtons = () => {
buttonsToDisable.forEach(btn => {
btn.disabled = false;
if (btn.classList.contains('btn')) {
btn.classList.remove('btn-disabled');
}
});
};
const handleResponse = (responseText) => {
reEnableButtons();
try {
const data = JSON.parse(responseText);
SUBTITLE_MANAGER.createSubtitleModal(data, searchTermTrimmed); // Pass searchTerm to modal
if (data.data && data.data.length > 0) {
UTILS.showToast(`"${searchTermTrimmed}" 的字幕信息获取成功`, "success");
} else {
UTILS.showToast(`未找到 "${searchTermTrimmed}" 的字幕`, "info");
}
} catch (e) {
console.error("解析字幕数据时出错:", e);
UTILS.showToast("解析字幕数据时出错", "error");
SUBTITLE_MANAGER.createSubtitleModal(null, searchTermTrimmed);
}
};
const handleError = (error) => {
reEnableButtons();
console.error("获取字幕时出错:", error);
UTILS.showToast("获取字幕时出错", "error");
SUBTITLE_MANAGER.createSubtitleModal(null, searchTermTrimmed);
};
// 设置超时处理
let timeoutId = setTimeout(() => {
reEnableButtons();
UTILS.showToast("获取字幕超时", "error");
SUBTITLE_MANAGER.createSubtitleModal(null, searchTermTrimmed);
// 清理可能的JSONP回调
if (window.emhJsonpCallback) {
delete window.emhJsonpCallback;
}
// 清理可能添加的script标签
const jsonpScript = document.getElementById('emh-jsonp-script');
if (jsonpScript) {
jsonpScript.remove();
}
}, 15000);
if (typeof GM_xmlhttpRequest !== 'undefined') {
// 使用油猴API,它能自动绕过CORS限制
GM_xmlhttpRequest({
method: 'GET',
url: apiUrl,
timeout: 15000,
onload: (response) => {
clearTimeout(timeoutId);
handleResponse(response.responseText);
},
onerror: (error) => {
clearTimeout(timeoutId);
handleError(error);
},
ontimeout: () => {
clearTimeout(timeoutId);
reEnableButtons();
UTILS.showToast("获取字幕超时", "error");
SUBTITLE_MANAGER.createSubtitleModal(null, searchTermTrimmed);
}
});
} else {
// 尝试使用CORS代理
const corsProxies = [
`https://api.allorigins.win/raw?url=${encodeURIComponent(apiUrl)}`,
`https://corsproxy.io/?${encodeURIComponent(apiUrl)}`,
`https://cors-anywhere.herokuapp.com/${apiUrl}`
];
// 创建一个Promise数组,对每个代理进行尝试
const fetchRequests = corsProxies.map(proxyUrl => {
return fetch(proxyUrl, {
method: 'GET',
headers: {
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => {
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
return response.text();
});
});
// 使用Promise.any来获取第一个成功的结果
Promise.any(fetchRequests)
.then(text => {
clearTimeout(timeoutId);
if (!text) {
handleResponse('{"data": []}');
} else {
handleResponse(text);
}
})
.catch(error => {
// 所有代理都失败时,尝试使用JSONP方法
console.warn("所有CORS代理失败,尝试JSONP方法");
// 清理之前可能存在的回调和脚本
if (window.emhJsonpCallback) {
delete window.emhJsonpCallback;
}
const oldScript = document.getElementById('emh-jsonp-script');
if (oldScript) {
oldScript.remove();
}
// 创建JSONP回调
window.emhJsonpCallback = function(data) {
clearTimeout(timeoutId);
handleResponse(JSON.stringify(data));
delete window.emhJsonpCallback;
};
// 尝试直接请求,某些服务器可能支持JSONP
const jsonpUrl = `${CONFIG.subtitleApiUrl}?name=${encodeURIComponent(searchTermTrimmed)}&callback=emhJsonpCallback`;
const script = document.createElement('script');
script.id = 'emh-jsonp-script';
script.src = jsonpUrl;
script.onerror = () => {
// JSONP失败时,创建一个空结果并处理
if (!document.getElementById('emh-subtitle-modal')) {
clearTimeout(timeoutId);
handleResponse('{"data": []}');
UTILS.showToast("无法连接到字幕API,请稍后重试", "error");
}
};
document.head.appendChild(script);
});
}
},
// 创建字幕模态框
createSubtitleModal: (subtitleContent = null, videoCode = null) => {
const existingModal = document.getElementById('emh-subtitle-modal');
if (existingModal) existingModal.remove();
const modal = document.createElement('div');
modal.id = 'emh-subtitle-modal';
modal.className = 'emh-modal';
const modalContent = document.createElement('div');
modalContent.className = 'emh-modal-content';
const modalHeader = document.createElement('div');
modalHeader.className = 'emh-modal-header';
modalHeader.innerHTML = `<h3>字幕列表 (搜索关键字: ${videoCode || '未知'})</h3><span class="emh-modal-close">×</span>`;
modalContent.appendChild(modalHeader);
const modalBody = document.createElement('div');
modalBody.className = 'emh-modal-body';
if (subtitleContent && subtitleContent.data && subtitleContent.data.length > 0) {
const list = document.createElement('ul');
list.className = 'emh-subtitle-list';
// 调试用:输出字幕数据结构
console.log("字幕数据:", subtitleContent.data);
subtitleContent.data.forEach((subtitle) => {
SUBTITLE_MANAGER.createSubtitleItem(list, subtitle, videoCode);
});
modalBody.appendChild(list);
} else {
modalBody.innerHTML = `<p class="emh-no-subtitle-message">未找到 "${videoCode}" 的相关字幕</p>`;
}
modalContent.appendChild(modalBody);
modal.appendChild(modalContent);
document.body.appendChild(modal);
modal.querySelector('.emh-modal-close').onclick = () => modal.remove();
modal.onclick = (event) => {
if (event.target === modal) {
modal.remove();
}
};
setTimeout(() => modal.classList.add('show'), 10);
return modal;
},
// 创建单个字幕项
createSubtitleItem: (listElement, subtitle, videoCode) => {
const item = document.createElement('li');
item.className = 'emh-subtitle-item';
// 获取原始文件名(直接从API返回)
let originalFilename = subtitle.name || '';
// 确保文件名有扩展名
if (originalFilename && !originalFilename.toLowerCase().endsWith(`.${subtitle.ext}`)) {
originalFilename = `${originalFilename}.${subtitle.ext || 'srt'}`;
} else if (!originalFilename) {
originalFilename = `subtitle.${subtitle.ext || 'srt'}`;
}
// 清理文件名中的非法字符
if (CONFIG.subtitleFilenameOptions.removeIllegalChars) {
originalFilename = UTILS.sanitizeFilename(originalFilename);
}
// 保存最终的下载文件名
const downloadFilename = originalFilename;
item.innerHTML = `
<div class="emh-subtitle-info">
<h4>${subtitle.name || '未命名字幕'}</h4>
<p>格式: ${subtitle.ext || '未知'} | 语言: ${subtitle.languages?.length ? subtitle.languages.join(', ') : '未知'} ${subtitle.extra_name ? '| 来源: ' + subtitle.extra_name : ''}</p>
</div>
<div class="emh-subtitle-actions">
${subtitle.url ? `
<button class="btn my-btn-primary emh-download-subtitle-btn" data-url="${subtitle.url}" data-filename="${downloadFilename}">缓存下载</button>
<a href="${subtitle.url}" target="_blank" class="btn btn-outline" download="${downloadFilename}">直接下载</a>
` : ''}
</div>
`;
listElement.appendChild(item);
return item;
},
// 下载字幕文件
downloadSubtitle: async (url, defaultFilename) => {
try {
UTILS.showToast('正在获取字幕文件...', 'info');
// 处理可能的跨域问题
if (typeof GM_xmlhttpRequest !== 'undefined') {
// 使用GM_xmlhttpRequest获取字幕内容(可绕过跨域限制)
GM_xmlhttpRequest({
method: 'GET',
url: url,
responseType: 'blob',
onload: function(response) {
if (response.status >= 200 && response.status < 300) {
const blob = response.response;
SUBTITLE_MANAGER.processSubtitleDownload(blob, defaultFilename);
} else {
UTILS.showToast(`获取字幕失败: ${response.status}`, 'error');
}
},
onerror: function(error) {
console.error('字幕下载失败:', error);
UTILS.showToast('字幕下载失败,请尝试直接下载', 'error');
}
});
} else {
// 使用标准fetch API
try {
const corsProxies = [
url, // 先尝试直接访问
`https://corsproxy.io/?${encodeURIComponent(url)}`,
`https://api.allorigins.win/raw?url=${encodeURIComponent(url)}`
];
// 尝试所有代理URL
let success = false;
for (const proxyUrl of corsProxies) {
try {
const response = await fetch(proxyUrl, {
method: 'GET',
headers: {
'Accept': 'text/plain, application/octet-stream'
}
});
if (response.ok) {
const blob = await response.blob();
SUBTITLE_MANAGER.processSubtitleDownload(blob, defaultFilename);
success = true;
break;
}
} catch (err) {
console.warn(`尝试使用代理 ${proxyUrl} 失败:`, err);
// 继续尝试下一个代理
}
}
if (!success) {
throw new Error('所有代理都失败');
}
} catch (error) {
console.error('字幕下载失败:', error);
UTILS.showToast('字幕下载失败,请尝试直接下载', 'error');
// 如果所有方法都失败,尝试打开新标签页直接下载
if (confirm('自动下载失败,是否尝试在新标签页中直接打开字幕链接?')) {
window.open(url, '_blank');
}
}
}
} catch (error) {
console.error('字幕下载处理失败:', error);
UTILS.showToast('字幕下载处理失败', 'error');
}
},
// 处理字幕下载的通用流程
processSubtitleDownload: (blob, defaultFilename) => {
try {
// 创建一个临时URL
const objectUrl = URL.createObjectURL(blob);
// 直接使用提供的文件名,无需用户确认
const downloadLink = document.createElement('a');
downloadLink.href = objectUrl;
downloadLink.download = defaultFilename;
downloadLink.style.display = 'none';
// 添加到文档中并点击
document.body.appendChild(downloadLink);
downloadLink.click();
// 清理
setTimeout(() => {
document.body.removeChild(downloadLink);
URL.revokeObjectURL(objectUrl);
}, 100);
UTILS.showToast(`字幕文件 "${defaultFilename}" 下载已开始`, 'success');
} catch (error) {
console.error('字幕下载处理失败:', error);
UTILS.showToast('字幕下载处理失败', 'error');
}
}
};
const UTILS = {
getDomain: () => document.domain,
getCodeFromUrl: (url) => {
const match = url.match(/\/([a-z0-9-]+)\/?$/i);
return match ? match[1] : null;
},
getPosterImage: () => {
const videoContainer = document.querySelector('.video-player-container, .player-container, #player');
if (videoContainer) {
const posterElem = videoContainer.querySelector('.plyr__poster, [poster]');
if (posterElem) {
if (posterElem.hasAttribute('poster')) {
return posterElem.getAttribute('poster');
}
const backgroundImageStyle = window.getComputedStyle(posterElem).getPropertyValue('background-image');
const matches = /url\("(.+)"\)/.exec(backgroundImageStyle);
return matches ? matches[1] : null;
}
}
const metaPoster = document.querySelector('meta[property="og:image"], meta[name="twitter:image"]');
return metaPoster ? metaPoster.content : null;
},
getActressNames: () => {
const actressLinks = document.querySelectorAll('.video-info .info-item a[href*="/actress/"], .models-list .model a, .attributes a[href*="/star/"]');
return Array.from(actressLinks)
.map(link => link.getAttribute('title') || link.textContent.trim())
.filter(name => name)
.filter((value, index, self) => self.indexOf(value) === index)
.join(',');
},
buildApiUrl: (domain, options) => {
const queryParams = Object.keys(options.query || {})
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(options.query[key])}`)
.join("&");
const query = queryParams.length > 0 ? `?${queryParams}` : "";
return `http://${domain}${options.path || ''}${query}`;
},
showToast: (message, type = 'info') => {
let toastContainer = document.getElementById('custom-toast-container');
if (!toastContainer) {
toastContainer = document.createElement('div');
toastContainer.id = 'custom-toast-container';
document.body.appendChild(toastContainer);
}
const toast = document.createElement('div');
toast.className = `custom-toast custom-toast-${type}`;
toast.textContent = message;
toastContainer.appendChild(toast);
setTimeout(() => toast.classList.add('show'), 10);
setTimeout(() => {
toast.classList.remove('show');
toast.addEventListener('transitionend', () => toast.remove());
}, 3000);
},
copyToClipboard: async (text) => {
if (!text) {
UTILS.showToast("没有可复制的内容", "error");
return false;
}
try {
await navigator.clipboard.writeText(text);
UTILS.showToast("内容已成功复制到剪贴板", "success");
return true;
} catch (error) {
UTILS.showToast("复制失败,请检查浏览器权限", "error");
console.error("Copy error:", error);
try {
const textArea = document.createElement("textarea");
textArea.value = text;
textArea.style.position = "fixed";
textArea.style.top = "0";
textArea.style.left = "0";
textArea.style.opacity = "0";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
const successful = document.execCommand('copy');
document.body.removeChild(textArea);
if (successful) {
UTILS.showToast("内容已复制 (fallback)", "success");
return true;
} else {
throw new Error('execCommand failed');
}
} catch (fallbackError) {
UTILS.showToast("复制到剪贴板时出错", "error");
console.error("Fallback copy error:", fallbackError);
return false;
}
}
},
addActionButtons: (container, videoUrl, videoCode) => {
const buttonContainer = document.createElement("div");
buttonContainer.className = "emh-action-buttons";
// Add code status indicator if we have a valid code
if (videoCode) {
createCodeStatusIndicator(buttonContainer, videoCode);
// Auto-add to library if enabled
if (CONFIG.codeManager.autoAddDetected && CODE_LIBRARY.initialized) {
const existingItem = CODE_LIBRARY.getItem(videoCode);
if (!existingItem) {
// Get title from page if possible
let title = '';
const titleElement = document.querySelector("h4.title, h1.post-title, .video-info h4, meta[property='og:title']");
if (titleElement) {
title = titleElement.content || titleElement.innerText.trim();
if (title.includes(videoCode)) {
title = title.split(videoCode).pop().trim().replace(/^[-–—\s]+/, '');
}
}
// Add to library with "unmarked" status
CODE_LIBRARY.markItem(videoCode, 'unmarked', title);
}
}
}
const copyButton = document.createElement("button");
copyButton.id = "emh-copyLink";
copyButton.className = "btn my-btn-primary";
copyButton.innerHTML = "<span>📋 复制链接</span>";
copyButton.title = videoUrl || "无有效视频链接";
copyButton.dataset.videoUrl = videoUrl || '';
buttonContainer.appendChild(copyButton);
const sendButton = document.createElement("button");
sendButton.id = "emh-sendData";
sendButton.className = "btn my-btn-danger";
sendButton.innerHTML = "<span>💾 发送到服务器</span>";
sendButton.dataset.videoUrl = videoUrl || '';
sendButton.dataset.videoCode = videoCode || '';
buttonContainer.appendChild(sendButton);
const subtitleButton = document.createElement("button");
subtitleButton.id = "emh-getSubtitles"; // This is for auto-detected code
subtitleButton.className = "btn my-btn-success";
subtitleButton.innerHTML = "<span>📄 获取字幕</span>";
subtitleButton.dataset.videoCode = videoCode || '';
buttonContainer.appendChild(subtitleButton);
// Add code manager button
const codeManagerButton = document.createElement("button");
codeManagerButton.id = "emh-code-manager-btn";
codeManagerButton.className = "btn btn-info";
codeManagerButton.innerHTML = "<span>📋 番号库</span>";
codeManagerButton.title = "打开番号管理面板";
codeManagerButton.addEventListener('click', () => {
if (window.CodeManagerPanel) {
window.CodeManagerPanel.togglePanel();
}
});
buttonContainer.appendChild(codeManagerButton);
container.appendChild(buttonContainer);
return buttonContainer;
},
// 注意:下面的字幕相关函数已移至SUBTITLE_MANAGER模块,保留API兼容性
createSubtitleModal: (subtitleContent, videoCode) => {
return SUBTITLE_MANAGER.createSubtitleModal(subtitleContent, videoCode);
},
fetchSubtitles: (searchTerm) => {
return SUBTITLE_MANAGER.fetchSubtitles(searchTerm);
},
downloadSubtitle: (url, defaultFilename) => {
return SUBTITLE_MANAGER.downloadSubtitle(url, defaultFilename);
},
createDraggableSubtitleButton: () => {
const button = document.createElement('button');
button.id = 'emh-draggable-custom-subtitle-btn'; // New ID
button.className = 'btn btn-info emh-draggable-btn';
button.innerHTML = '<span>🔍 高级搜索</span>'; // Updated text
button.title = '拖动我 | 点击打开高级字幕搜索';
let isDragging = false;
let offsetX, offsetY;
let hasDragged = false;
let startX, startY;
button.onmousedown = (e) => {
if (e.button !== 0) return;
e.preventDefault();
isDragging = true;
hasDragged = false;
button.style.cursor = 'grabbing';
startX = e.clientX;
startY = e.clientY;
const rect = button.getBoundingClientRect();
offsetX = e.clientX - rect.left;
offsetY = e.clientY - rect.top;
button.style.position = 'fixed';
document.onmousemove = (moveEvent) => {
if (!isDragging) return;
if (Math.abs(moveEvent.clientX - startX) > 3 || Math.abs(moveEvent.clientY - startY) > 3) {
hasDragged = true;
}
let newX = moveEvent.clientX - offsetX;
let newY = moveEvent.clientY - offsetY;
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const buttonWidth = button.offsetWidth;
const buttonHeight = button.offsetHeight;
if (newX < 0) newX = 0;
if (newY < 0) newY = 0;
if (newX + buttonWidth > viewportWidth) newX = viewportWidth - buttonWidth;
if (newY + buttonHeight > viewportHeight) newY = viewportHeight - buttonHeight;
button.style.left = `${newX}px`;
button.style.top = `${newY}px`;
button.style.bottom = 'auto';
button.style.right = 'auto';
};
document.onmouseup = () => {
if (!isDragging) return;
isDragging = false;
button.style.cursor = 'grab';
document.onmousemove = null;
document.onmouseup = null;
if (!hasDragged) { // Click action
// 使用高级搜索模态框替代简单的 prompt
const defaultSearchTerm = EMH_currentVideoCode || "";
UTILS.createSearchModal(defaultSearchTerm);
}
};
};
button.onclick = (e) => { // Prevent click if drag occurred
if (hasDragged) {
e.preventDefault();
e.stopPropagation();
}
};
document.body.appendChild(button);
if (!button.style.left && !button.style.top) {
button.style.position = 'fixed';
button.style.bottom = '70px';
button.style.right = '20px';
}
return button;
},
// 创建高级搜索模态框
createSearchModal: (defaultSearchTerm = '') => {
// 移除已存在的模态框
const existingModal = document.getElementById('emh-search-modal');
if (existingModal) existingModal.remove();
// 创建模态框基本结构
const modal = document.createElement('div');
modal.id = 'emh-search-modal';
modal.className = 'emh-modal';
const modalContent = document.createElement('div');
modalContent.className = 'emh-modal-content emh-search-modal-content';
// 创建模态框头部
const modalHeader = document.createElement('div');
modalHeader.className = 'emh-modal-header';
modalHeader.innerHTML = `
<h3>高级字幕搜索</h3>
<span class="emh-modal-close">×</span>
`;
// 创建模态框主体
const modalBody = document.createElement('div');
modalBody.className = 'emh-modal-body';
// 搜索表单
const searchForm = document.createElement('form');
searchForm.className = 'emh-search-form';
searchForm.addEventListener('submit', (e) => {
e.preventDefault();
const searchInput = document.getElementById('emh-subtitle-search-input');
const searchTerm = searchInput.value.trim();
if (searchTerm) {
saveSearchHistory(searchTerm);
modal.remove();
UTILS.fetchSubtitles(searchTerm);
}
});
// 搜索输入区域
const searchInputGroup = document.createElement('div');
searchInputGroup.className = 'emh-search-input-group';
searchInputGroup.innerHTML = `
<div class="emh-input-wrapper">
<input type="text" id="emh-subtitle-search-input" class="emh-search-input"
placeholder="输入字幕关键词..." value="${defaultSearchTerm}" autofocus>
<button type="button" class="emh-search-clear-btn" title="清除输入">×</button>
</div>
<button type="submit" class="emh-search-btn">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
搜索
</button>
`;
searchForm.appendChild(searchInputGroup);
// 搜索历史
const historySection = document.createElement('div');
historySection.className = 'emh-search-history-section';
const historyHeader = document.createElement('div');
historyHeader.className = 'emh-search-history-header';
historyHeader.innerHTML = `
<h4>搜索历史</h4>
<button type="button" class="emh-clear-history-btn">清除历史</button>
`;
const historyList = document.createElement('div');
historyList.className = 'emh-search-history-list';
UTILS.updateHistoryList(historyList);
historySection.appendChild(historyHeader);
historySection.appendChild(historyList);
// 热门搜索(可选功能 - 如果有API支持)
const trendingSection = document.createElement('div');
trendingSection.className = 'emh-trending-section';
trendingSection.innerHTML = `
<h4>热门推荐</h4>
<div class="emh-trending-tags">
<span class="emh-trending-tag">中文字幕</span>
<span class="emh-trending-tag">4K高清</span>
<span class="emh-trending-tag">双语字幕</span>
<span class="emh-trending-tag">特效字幕</span>
<span class="emh-trending-tag">日语字幕</span>
</div>
`;
// 添加设置选项
const settingsSection = document.createElement('div');
settingsSection.className = 'emh-settings-section';
settingsSection.innerHTML = `
<h4>设置选项</h4>
<div class="emh-setting-item">
<label for="emh-original-name-setting" class="emh-setting-label">
<span>使用原始文件名下载字幕</span>
<input type="checkbox" id="emh-original-name-setting" class="emh-toggle-checkbox" ${CONFIG.subtitleFilenameOptions.useOriginalName ? 'checked' : ''} disabled>
<span class="emh-toggle-switch"></span>
</label>
</div>
`;
// 添加到主体
modalBody.appendChild(searchForm);
modalBody.appendChild(historySection);
modalBody.appendChild(trendingSection);
modalBody.appendChild(settingsSection);
// 添加到模态框
modalContent.appendChild(modalHeader);
modalContent.appendChild(modalBody);
modal.appendChild(modalContent);
// 添加到文档
document.body.appendChild(modal);
// 绑定事件
UTILS.setupSearchModalEvents(modal);
// 显示模态框
setTimeout(() => modal.classList.add('show'), 10);
return modal;
},
// 更新历史列表
updateHistoryList: (historyList) => {
const history = getSearchHistory();
if (history.length === 0) {
historyList.innerHTML = '<div class="emh-empty-history">暂无搜索历史</div>';
return;
}
historyList.innerHTML = '';
history.forEach(term => {
const historyItem = document.createElement('div');
historyItem.className = 'emh-history-item';
historyItem.innerHTML = `
<span class="emh-history-text">${term}</span>
<button class="emh-history-use-btn" data-term="${term}" title="使用该关键词">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="9 10 4 15 9 20"></polyline>
<path d="M20 4v7a4 4 0 0 1-4 4H4"></path>
</svg>
</button>
`;
historyList.appendChild(historyItem);
});
},
// 设置搜索模态框事件
setupSearchModalEvents: (modal) => {
// 关闭按钮
const closeBtn = modal.querySelector('.emh-modal-close');
if (closeBtn) {
closeBtn.addEventListener('click', () => {
modal.classList.remove('show');
setTimeout(() => modal.remove(), CONFIG.animationDuration);
});
}
// 点击模态框背景关闭
modal.addEventListener('click', (e) => {
if (e.target === modal) {
modal.classList.remove('show');
setTimeout(() => modal.remove(), CONFIG.animationDuration);
}
});
// 清除输入按钮
const clearInputBtn = modal.querySelector('.emh-search-clear-btn');
const searchInput = modal.querySelector('#emh-subtitle-search-input');
if (clearInputBtn && searchInput) {
clearInputBtn.addEventListener('click', () => {
searchInput.value = '';
searchInput.focus();
});
// 根据输入框内容显示/隐藏清除按钮
searchInput.addEventListener('input', () => {
if (searchInput.value) {
clearInputBtn.style.visibility = 'visible';
} else {
clearInputBtn.style.visibility = 'hidden';
}
});
// 初始状态
if (searchInput.value) {
clearInputBtn.style.visibility = 'visible';
} else {
clearInputBtn.style.visibility = 'hidden';
}
}
// 清除历史按钮
const clearHistoryBtn = modal.querySelector('.emh-clear-history-btn');
if (clearHistoryBtn) {
clearHistoryBtn.addEventListener('click', () => {
if (confirm('确定要清除所有搜索历史吗?')) {
const success = clearSearchHistory();
if (success) {
const historyList = modal.querySelector('.emh-search-history-list');
if (historyList) {
UTILS.updateHistoryList(historyList);
}
UTILS.showToast('搜索历史已清除', 'success');
} else {
UTILS.showToast('清除历史失败', 'error');
}
}
});
}
// 历史项使用按钮
modal.querySelectorAll('.emh-history-use-btn').forEach(btn => {
btn.addEventListener('click', () => {
const term = btn.getAttribute('data-term');
if (term && searchInput) {
searchInput.value = term;
searchInput.focus();
}
});
});
// 热门标签点击
modal.querySelectorAll('.emh-trending-tag').forEach(tag => {
tag.addEventListener('click', () => {
if (searchInput) {
searchInput.value = tag.textContent;
searchInput.focus();
}
});
});
},
// 创建悬浮搜索按钮
createFloatingSearchButton: () => {
const button = document.createElement('button');
button.id = 'emh-floating-search-btn';
button.className = 'emh-floating-btn';
button.title = '高级字幕搜索';
button.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
`;
button.addEventListener('click', () => {
// 使用当前视频代码作为默认搜索词
let defaultSearchTerm = '';
if (typeof EMH_currentVideoCode !== 'undefined' && EMH_currentVideoCode) {
defaultSearchTerm = EMH_currentVideoCode;
}
UTILS.createSearchModal(defaultSearchTerm);
});
document.body.appendChild(button);
return button;
},
// 清理文件名,移除非法字符
sanitizeFilename: (filename) => {
if (!filename) return '字幕';
// 移除Windows/通用文件系统中的非法字符
let sanitized = filename.replace(/[<>:"\/\\|?*\x00-\x1F]/g, '');
// 替换连续空格为单个空格
sanitized = sanitized.replace(/\s+/g, ' ').trim();
// 如果为空,返回默认名称
return sanitized || '字幕';
},
// 下载字幕文件(先缓存再下载)
downloadSubtitle: async (url, defaultFilename) => {
try {
UTILS.showToast('正在获取字幕文件...', 'info');
// 处理可能的跨域问题
if (typeof GM_xmlhttpRequest !== 'undefined') {
// 使用GM_xmlhttpRequest获取字幕内容(可绕过跨域限制)
GM_xmlhttpRequest({
method: 'GET',
url: url,
responseType: 'blob',
onload: function(response) {
if (response.status >= 200 && response.status < 300) {
const blob = response.response;
SUBTITLE_MANAGER.processSubtitleDownload(blob, defaultFilename);
} else {
UTILS.showToast(`获取字幕失败: ${response.status}`, 'error');
}
},
onerror: function(error) {
console.error('字幕下载失败:', error);
UTILS.showToast('字幕下载失败,请尝试直接下载', 'error');
}
});
} else {
// 使用标准fetch API
try {
const corsProxies = [
url, // 先尝试直接访问
`https://corsproxy.io/?${encodeURIComponent(url)}`,
`https://api.allorigins.win/raw?url=${encodeURIComponent(url)}`
];
// 尝试所有代理URL
let success = false;
for (const proxyUrl of corsProxies) {
try {
const response = await fetch(proxyUrl, {
method: 'GET',
headers: {
'Accept': 'text/plain, application/octet-stream'
}
});
if (response.ok) {
const blob = await response.blob();
SUBTITLE_MANAGER.processSubtitleDownload(blob, defaultFilename);
success = true;
break;
}
} catch (err) {
console.warn(`尝试使用代理 ${proxyUrl} 失败:`, err);
// 继续尝试下一个代理
}
}
if (!success) {
throw new Error('所有代理都失败');
}
} catch (error) {
console.error('字幕下载失败:', error);
UTILS.showToast('字幕下载失败,请尝试直接下载', 'error');
// 如果所有方法都失败,尝试打开新标签页直接下载
if (confirm('自动下载失败,是否尝试在新标签页中直接打开字幕链接?')) {
window.open(url, '_blank');
}
}
}
} catch (error) {
console.error('字幕下载处理失败:', error);
UTILS.showToast('字幕下载失败,请尝试直接下载', 'error');
}
},
// 处理字幕下载的通用流程
processSubtitleDownload: (blob, defaultFilename) => {
try {
// 创建一个临时URL
const objectUrl = URL.createObjectURL(blob);
// 直接使用提供的文件名,无需用户确认
const downloadLink = document.createElement('a');
downloadLink.href = objectUrl;
downloadLink.download = defaultFilename;
downloadLink.style.display = 'none';
// 添加到文档中并点击
document.body.appendChild(downloadLink);
downloadLink.click();
// 清理
setTimeout(() => {
document.body.removeChild(downloadLink);
URL.revokeObjectURL(objectUrl);
}, 100);
UTILS.showToast(`字幕文件 "${defaultFilename}" 下载已开始`, 'success');
} catch (error) {
console.error('字幕下载处理失败:', error);
UTILS.showToast('字幕下载处理失败', 'error');
}
},
};
const SITE_HANDLERS = {
javtxt: {
isMatch: () => UTILS.getDomain().includes('javtxt') || UTILS.getDomain().includes('tokyolib') || UTILS.getDomain().includes('javtext'),
targetSelector: 'body > div.main > div.info > div.attributes > dl > dt:nth-child(2)',
process: (targetElement) => {
if (!targetElement) {
console.error("JavTXT: Target element not found.");
return;
}
const config = {
links: [
{ urlTemplate: 'https://123av.com/zh/v/$code', target: '_blank', displayText: '123av' },
{ urlTemplate: 'https://jable.tv/videos/$code/', target: '_blank', displayText: 'Jable' }
]
};
const cleanedCode = extractCode(targetElement.innerText);
if (!cleanedCode) {
console.error("JavTXT: Failed to extract code.");
return;
}
updateGlobalVideoCode(cleanedCode);
// 创建状态指示器容器
const statusContainer = document.createElement('div');
statusContainer.className = 'emh-code-status-container';
statusContainer.style.display = 'inline-block';
statusContainer.style.marginLeft = '10px';
createCodeStatusIndicator(statusContainer, cleanedCode);
// 将状态指示器添加到番号文本后面
targetElement.appendChild(statusContainer);
const controlsContainer = document.createElement('div');
controlsContainer.className = 'emh-controls-container';
config.links.forEach(linkConfig => {
const link = document.createElement('a');
link.href = linkConfig.urlTemplate.replace('$code', cleanedCode);
link.target = linkConfig.target;
link.className = 'btn btn-outline';
link.innerText = linkConfig.displayText;
controlsContainer.appendChild(link);
});
const subtitleButton = document.createElement('button');
subtitleButton.id = 'emh-getSubtitles';
subtitleButton.className = 'btn my-btn-success';
subtitleButton.innerHTML = '<span>📄 获取字幕</span>';
subtitleButton.dataset.videoCode = cleanedCode;
controlsContainer.appendChild(subtitleButton);
targetElement.parentNode.insertBefore(controlsContainer, targetElement.nextSibling);
}
},
javgg: {
isMatch: () => UTILS.getDomain().includes('javgg'),
targetSelector: 'article.item.movies .data, h1.post-title, .videoinfo .meta',
process: (targetElement) => {
if (document.querySelector("article.item.movies")) {
const sidebar = document.querySelector("#contenedor > div > div.sidebar.right.scrolling");
if (sidebar) sidebar.remove();
const linkProviders = [
{ code: "njav", url: CONFIG.alternateUrl.av123 + "$p", target: "_blank" },
{ code: "jable", url: CONFIG.alternateUrl.jable+"$p/", target: "_blank" },
{ code: "1cili", url: CONFIG.alternateUrl.cili1+"$p", target: "_blank" }
];
document.querySelectorAll("article.item.movies").forEach(entry => {
const dataElement = entry.querySelector(".data");
const anchorTag = dataElement ? dataElement.querySelector("h3 a") : null;
if (anchorTag) {
const videoCode = anchorTag.textContent.trim();
if (!videoCode) return;
if (dataElement.querySelector('.emh-javgg-controls')) return;
// 创建状态指示器容器
const statusContainer = document.createElement('div');
statusContainer.className = 'emh-code-status-container';
statusContainer.style.display = 'inline-block';
statusContainer.style.marginLeft = '10px';
createCodeStatusIndicator(statusContainer, videoCode);
// 将状态指示器添加到标题后面
anchorTag.parentNode.appendChild(statusContainer);
const controlsDiv = document.createElement('div');
controlsDiv.className = 'emh-javgg-controls';
linkProviders.forEach(provider => {
const newAnchorTag = document.createElement("a");
newAnchorTag.href = provider.url.replace("$p", videoCode);
newAnchorTag.target = provider.target;
newAnchorTag.className = 'btn btn-outline';
newAnchorTag.style.padding = '4px 8px';
newAnchorTag.style.fontSize = '12px';
newAnchorTag.textContent = provider.code;
controlsDiv.appendChild(newAnchorTag);
});
const subtitleButton = document.createElement('button');
subtitleButton.className = 'btn my-btn-success emh-subtitle-button-small';
subtitleButton.style.padding = '4px 8px';
subtitleButton.style.fontSize = '12px';
subtitleButton.innerHTML = '<span>字幕</span>';
subtitleButton.dataset.videoCode = videoCode;
controlsDiv.appendChild(subtitleButton);
dataElement.appendChild(controlsDiv);
}
});
}
}
},
jable: {
isMatch: () => UTILS.getDomain().includes('jable') || UTILS.getDomain().includes('cableav') || UTILS.getDomain().includes('fs1.app'),
targetSelector: '.video-toolbar, .video-info .level, .video-info .row, .text-center, #detail-container .pb-3, .container .mt-4, .player-container + div',
process: (targetElement) => {
if (!targetElement) {
console.error("Jable-like: Target container not found or page structure mismatch.");
return;
}
if (targetElement.querySelector('.emh-ui-container') || document.querySelector('.emh-ui-container')) {
return;
}
const isCableAv = UTILS.getDomain() === "cableav.tv";
let videoUrl = '';
let videoCode = UTILS.getCodeFromUrl(window.location.href);
if (!videoCode) {
const titleCodeMatch = document.title.match(/^([A-Z0-9-]+)/i);
if (titleCodeMatch) videoCode = titleCodeMatch[1].toUpperCase();
}
if (!videoCode) {
const ogTitle = document.querySelector("meta[property='og:title']");
if (ogTitle && ogTitle.content) {
const titleMatch = ogTitle.content.match(/^([A-Z0-9-]+)/i);
if (titleMatch) videoCode = titleMatch[1].toUpperCase();
}
}
if (!isCableAv) {
if (typeof hlsUrl !== 'undefined' && hlsUrl) {
videoUrl = hlsUrl;
} else {
const scripts = document.querySelectorAll('script');
for (let script of scripts) {
if (script.textContent.includes('player.src({')) {
const match = script.textContent.match(/src:\s*['"]([^'"]+\.m3u8[^'"]*)['"]/);
if (match && match[1]) {
videoUrl = match[1];
break;
}
}
}
}
if (videoUrl && videoCode) {
videoUrl += "#" + videoCode;
} else if (videoUrl && !videoCode) {
if (videoCode) videoUrl += "#" + videoCode;
}
} else {
const metaTag = document.head.querySelector("meta[property~='og:video:url'][content]");
if (metaTag) videoUrl = metaTag.content;
}
if (videoCode) {
updateGlobalVideoCode(videoCode);
} else {
console.warn("Jable-like: Video code could not be determined for this page.");
}
const uiContainer = document.createElement("div");
uiContainer.className = "emh-ui-container";
if (videoCode) {
const dataElement = document.createElement("span");
dataElement.id = "emh-dataElement";
dataElement.className = "btn btn-outline";
dataElement.style.cursor = 'pointer';
dataElement.innerHTML = `番号: ${videoCode}`;
dataElement.title = "点击搜索番号 (1cili)";
dataElement.dataset.videoCode = videoCode;
// 创建状态指示器容器
const statusContainer = document.createElement('div');
statusContainer.className = 'emh-code-status-container';
statusContainer.style.display = 'inline-block';
statusContainer.style.marginLeft = '10px';
createCodeStatusIndicator(statusContainer, videoCode);
// 将状态指示器添加到番号文本后面
dataElement.appendChild(statusContainer);
uiContainer.appendChild(dataElement);
}
UTILS.addActionButtons(uiContainer, videoUrl, videoCode);
targetElement.appendChild(uiContainer);
console.log("EMH: Added UI buttons to Jable-like page via target:", targetElement);
}
},
javbus:{
isMatch: () => UTILS.getDomain().includes('javbus.com'),
targetSelector: '.item.masonry-brick', // 目标元素选择器
// 处理函数:对每个由targetSelector选中的元素执行的操作
process: (targetElement) => {
// 遍历所有masonry布局的视频项
document.querySelectorAll('.item.masonry-brick').forEach(entry => {
// 获取视频信息容器元素
const photoInfoElement = entry.querySelector(".photo-info");
if (!photoInfoElement) return;
// 防止重复处理
// 检查 photoInfoElement 是否已经处理过
if (photoInfoElement.classList.contains('emh-indicator-processed')) return;
// 从span元素中提取第一个date元素(视频代码)
const dateElements = photoInfoElement.querySelectorAll("span > date");
const videoCodeElement = dateElements.length > 0 ? dateElements[0] : null;
if (videoCodeElement) {
const videoCode = videoCodeElement.textContent.trim();
if (!videoCode) return; // 跳过空代码
// --- 创建状态指示器的容器 ---
const statusContainer = document.createElement('div');
// statusContainer.className = 'emh-indicator-wrapper';
createCodeStatusIndicator(statusContainer, videoCode); // 填充 statusContainer
// 检查 statusContainer 是否真的包含了子元素 (指示器)
if (statusContainer.firstChild) {
// --- 插入到 photoInfoElement 的最前面 ---
photoInfoElement.insertBefore(statusContainer.firstChild, photoInfoElement.firstChild);
// photoInfoElement.insertBefore(indicatorElement, photoInfoElement.firstChild);
// 添加标记,表示这个 photoInfoElement 已经处理过
photoInfoElement.classList.add('emh-indicator-processed');
}
}
});
}
},
};
const VIDEO_MANAGER = {
sendVideoData: (button) => {
const videoUrl = button.dataset.videoUrl || '';
const videoCode = button.dataset.videoCode || EMH_currentVideoCode || UTILS.getCodeFromUrl(window.location.href);
const titleElement = document.querySelector("h4.title, h1.post-title, .video-info h4, meta[property='og:title']");
let title = titleElement ? (titleElement.content || titleElement.innerText.trim()) : document.title;
if (videoCode && title.includes(videoCode)) {
title = title.split(videoCode).pop().trim().replace(/^[-–—\s]+/, '');
}
const posterImage = UTILS.getPosterImage();
const actress = UTILS.getActressNames();
const videoData = {
code: videoCode || 'UNKNOWN',
name: title || 'Untitled',
img: posterImage || '',
url: window.location.href,
actress: actress || '',
video: videoUrl || ''
};
if (!videoData.code || videoData.code === 'UNKNOWN') {
UTILS.showToast("无法获取视频代码,发送中止", "warning");
console.warn("Send data aborted, missing video code.", videoData);
return;
}
console.log("Data to send:", videoData);
const serverDomain = (CONFIG.serverMode === 1) ? `localhost:${CONFIG.serverPort}` : `YOUR_SERVER_IP:${CONFIG.serverPort}`;
if (CONFIG.serverMode === 2 && serverDomain.includes('YOUR_SERVER_IP')) {
UTILS.showToast("请先在脚本中配置服务器IP地址", "error");
console.error("Server IP not configured in script for serverMode 2.");
return;
}
const apiUrl = UTILS.buildApiUrl(serverDomain, { path: '/add', query: videoData });
if (typeof GM_xmlhttpRequest !== 'undefined') {
GM_xmlhttpRequest({
method: 'GET',
url: apiUrl,
timeout: 10000,
onload: (response) => {
if (response.status >= 200 && response.status < 300) {
UTILS.showToast("数据已发送到服务器", "success");
} else {
UTILS.showToast(`服务器响应错误: ${response.status}`, "error");
console.error("Server response error:", response);
}
},
onerror: (error) => {
UTILS.showToast("发送数据时网络错误", "error");
console.error("Send data network error:", error);
},
ontimeout: () => {
UTILS.showToast("发送数据超时", "error");
}
});
} else {
fetch(apiUrl, { mode: 'no-cors', signal: AbortSignal.timeout(10000) })
.then(response => {
UTILS.showToast("数据已尝试发送 (no-cors)", "success");
})
.catch(error => {
if (error.name === 'AbortError') {
UTILS.showToast("发送数据超时", "error");
} else {
UTILS.showToast("发送数据时出错 (fetch)", "error");
}
console.error("Send data error (fetch):", error);
});
}
return videoData;
}
};
function extractCode(text) {
if (!text) return null;
const match = text.match(/([A-Za-z]{2,5}-?\d{2,5})/);
return match ? match[1].toUpperCase() : text.replace(/\s*\(.*?\)/g, '').trim().toUpperCase();
}
function waitForElement(selector, callback, timeout = CONFIG.elementCheckTimeout) {
const startTime = Date.now();
const intervalId = setInterval(() => {
const elements = document.querySelectorAll(selector);
if (elements.length > 0) {
clearInterval(intervalId);
callback(elements[0]);
} else if (Date.now() - startTime > timeout) {
clearInterval(intervalId);
console.warn(`EMH: Element "${selector}" not found within ${timeout}ms.`);
callback(null);
}
}, CONFIG.elementCheckInterval);
}
// 给可拖动按钮增加高级搜索功能
function enhanceDraggableButton() {
const draggableBtn = document.getElementById('emh-draggable-custom-subtitle-btn');
if (draggableBtn) {
draggableBtn.innerHTML = '<span>🔍 高级搜索</span>';
draggableBtn.title = '拖动我 | 点击打开高级字幕搜索';
// 保留原有的拖动功能,但改变点击行为
const originalClickHandler = draggableBtn.onclick;
draggableBtn.onclick = function(e) {
// 检查是否进行了拖动
if (this.hasDragged) {
if (originalClickHandler) {
originalClickHandler.call(this, e);
}
return;
}
// 没有拖动,则打开高级搜索
e.preventDefault();
e.stopPropagation();
let defaultSearchTerm = '';
if (typeof EMH_currentVideoCode !== 'undefined' && EMH_currentVideoCode) {
defaultSearchTerm = EMH_currentVideoCode;
}
UTILS.createSearchModal(defaultSearchTerm);
};
} else {
// 如果找不到原有按钮,创建新的
UTILS.createFloatingSearchButton();
}
}
function main() {
let handlerFound = false;
for (const [name, handler] of Object.entries(SITE_HANDLERS)) {
if (handler.isMatch()) {
handlerFound = true;
if (handler.targetSelector) {
waitForElement(handler.targetSelector, (targetElement) => {
if (targetElement || name === 'javgg') {
try {
setTimeout(() => { handler.process(targetElement); }, 50);
} catch (e) { console.error(`EMH: Error processing handler ${name} with target:`, e, targetElement); }
}
});
} else {
try {
setTimeout(() => handler.process(null), 150);
} catch (e) { console.error(`EMH: Error processing handler ${name} immediately:`, e); }
}
break;
}
}
if (!handlerFound) {
console.log("EMH: No matching handler found for this site.");
}
setupEventListeners();
// 延迟执行增强可拖动按钮功能,确保原按钮已创建
setTimeout(() => {
enhanceDraggableButton();
}, 1500);
}
function setupEventListeners() {
$(document).off('.emh');
$(document).on('click.emh', '#emh-copyLink', function () {
UTILS.copyToClipboard($(this).data('videoUrl'));
});
$(document).on('click.emh', '#emh-sendData', function () {
VIDEO_MANAGER.sendVideoData(this);
});
$(document).on('click.emh', '#emh-getSubtitles, .emh-subtitle-button-small', function (e) {
e.preventDefault();
const videoCode = $(this).data('videoCode'); // This is for auto-detected codes
if (videoCode) {
SUBTITLE_MANAGER.fetchSubtitles(videoCode);
} else {
UTILS.showToast("无法从此按钮获取番号", "warning");
}
});
$(document).on('click.emh', '#emh-dataElement', function () {
const code = $(this).data('videoCode');
if (code) {
window.open(`https://1cili.com/search?q=${code}`, "_blank");
}
});
$(document).on('click.emh', '#emh-floating-search-btn', function () {
const defaultSearchTerm = EMH_currentVideoCode || '';
UTILS.createSearchModal(defaultSearchTerm);
});
$(document).on('click.emh', '.emh-trending-tag', function () {
const searchInput = document.getElementById('emh-subtitle-search-input');
if (searchInput) {
searchInput.value = $(this).text();
searchInput.focus();
}
});
// 字幕下载按钮点击事件
$(document).on('click.emh', '.emh-download-subtitle-btn', function(e) {
e.preventDefault();
const url = $(this).data('url');
const filename = $(this).data('filename');
if (url && filename) {
SUBTITLE_MANAGER.downloadSubtitle(url, filename);
} else {
UTILS.showToast("下载信息不完整", "error");
}
});
// 番号管理按钮点击事件
$(document).on('click.emh', '#emh-code-manager-btn', function() {
if (window.CodeManagerPanel) {
window.CodeManagerPanel.togglePanel();
} else {
UTILS.showToast("番号管理面板未能加载", "error");
}
});
}
function addCustomStyles() {
const style = document.createElement('style');
style.textContent = `
/* ==== CSS from Script ==== */
.navbar{z-index:12345679!important}.sub-header,#footer,.search-recent-keywords,.app-desktop-banner,div[data-controller=movie-tab] .tabs,h3.main-title,div.video-meta-panel>div>div:nth-child(2)>nav>div.review-buttons>div:nth-child(2),div.video-detail>div:nth-child(4)>div>div.tabs.no-bottom>ul>li:nth-child(3),div.video-detail>div:nth-child(4)>div>div.tabs.no-bottom>ul>li:nth-child(2),div.video-detail>div:nth-child(4)>div>div.tabs.no-bottom>ul>li:nth-child(1),.top-meta,.float-buttons{display:none!important}div.tabs.no-bottom,.tabs ul{border-bottom:none!important}.movie-list .item{position:relative!important}.fr-btn{float:right;margin-left:4px!important}.menu-box{position:fixed;right:10px;top:50%;transform:translateY(-50%);display:flex;flex-direction:column;z-index:1000;gap:6px}.menu-btn{display:inline-block!important;min-width:80px;padding:7px 12px;border-radius:4px;color:#fff!important;text-decoration:none;font-weight:700;font-size:12px;text-align:center;cursor:pointer;transition:all .3s ease;box-shadow:0 2px 5px #0000001a;text-shadow:0 1px 1px rgba(0,0,0,.2);border:none;line-height:1.3;margin:0}.menu-btn:hover{transform:translateY(-1px);box-shadow:0 3px 6px #00000026;opacity:.9}.menu-btn:active{transform:translateY(0);box-shadow:0 1px 2px #0000001a}.my-btn-primary,.my-btn-success,.my-btn-danger,.btn-warning,.btn-info,.btn-dark,.btn-outline,.btn-disabled{display:inline-flex;align-items:center;justify-content:center;padding:6px 14px;margin-left:10px;border-radius:6px;text-decoration:none;font-size:13px;font-weight:500;transition:all .2s ease;cursor:pointer;border:1px solid rgba(0,0,0,.08);white-space:nowrap}.btn:hover{transform:translateY(-1px);box-shadow:0 2px 8px #0000000d}.my-btn-primary{background:#e0f2fe;color:#0369a1;border-color:#bae6fd}.my-btn-primary:hover{background:#bae6fd}.my-btn-success{background:#dcfce7;color:#166534;border-color:#bbf7d0}.my-btn-success:hover{background:#bbf7d0}.my-btn-danger{background:#fee2e2;color:#b91c1c;border-color:#fecaca}.my-btn-danger:hover{background:#fecaca}.btn-warning{background:#ffedd5;color:#9a3412;border-color:#fed7aa}.btn-warning:hover{background:#fed7aa}.btn-info{background:#ccfbf1;color:#0d9488;border-color:#99f6e4}.btn-info:hover{background:#99f6e4}.btn-dark{background:#e2e8f0;color:#334155;border-color:#cbd5e1}.btn-dark:hover{background:#cbd5e1}.btn-outline{background:transparent;color:#64748b;border-color:#cbd5e1}.btn-outline:hover{background:#f8fafc}.btn-disabled{background:#f1f5f9!important;color:#94a3b8!important;border-color:#e2e8f0!important;cursor:not-allowed!important}.btn-disabled:hover{transform:none!important;box-shadow:none!important;background:#f1f5f9!important;color:#94a3b8!important;}.data-table{width:100%;border-collapse:separate;border-spacing:0;font-family:Helvetica Neue,Arial,sans-serif;background:#fff;overflow:hidden;box-shadow:0 4px 20px #00000008;margin:0 auto}.data-table thead tr{background:#f8fafc}.data-table th{padding:16px 20px;text-align:center!important;color:#64748b;font-weight:500;font-size:14px;text-transform:uppercase;letter-spacing:.5px;border-bottom:1px solid #e2e8f0}.data-table td{padding:14px 20px;color:#334155;font-size:15px;border-bottom:1px solid #f1f5f9;text-align:center!important;vertical-align:middle}.data-table tbody tr:last-child td{border-bottom:none}.data-table tbody tr{transition:all .2s ease}.data-table tbody tr:hover{background:#f8fafc}.data-table .text-left{text-align:left}.data-table .text-right{text-align:right}.data-table.show-border,.data-table.show-border th,.data-table.show-border td{border:1px solid #e2e8f0}
.loading-container{position:fixed;top:0;left:0;width:100%;height:100%;display:flex;justify-content:center;align-items:center;background-color:rgba(0,0,0,.1);z-index:99999999}.loading-animation{position:relative;width:60px;height:12px;background:linear-gradient(90deg,#4facfe 0,#00f2fe 100%);border-radius:6px;animation:loading-animate 1.8s ease-in-out infinite;box-shadow:0 4px 12px rgba(0,0,0,.1)}.loading-animation:after,.loading-animation:before{position:absolute;display:block;content:"";animation:loading-animate 1.8s ease-in-out infinite;height:12px;border-radius:6px;box-shadow:0 4px 12px rgba(0,0,0,.1)}.loading-animation:before{top:-20px;left:10px;width:40px;background:linear-gradient(90deg,#ff758c 0,#ff7eb3 100%)}.loading-animation:after{bottom:-20px;width:35px;background:linear-gradient(90deg,#ff9a9e 0,#fad0c4 100%)}@keyframes loading-animate{0%{transform:translateX(40px)}50%{transform:translateX(-30px)}100%{transform:translateX(40px)}}
/* ==== Additional Styles needed by EMH ==== */
.emh-modal{display:flex;align-items:center;justify-content:center;position:fixed;z-index:9998;left:0;top:0;width:100%;height:100%;overflow:auto;background-color:rgba(0,0,0,0.7);opacity:0;transition:opacity 0.3s ease;backdrop-filter:blur(3px);}.emh-modal.show{opacity:1;}.emh-modal-content{background-color:#fff;margin:auto;padding:0;border-radius:12px;width:90%;max-width:650px;box-shadow:0 8px 24px rgba(0,0,0,0.4);transform:scale(0.9);transition:transform 0.3s ease;overflow:hidden;}.emh-modal.show .emh-modal-content{transform:scale(1);}.emh-modal-header{display:flex;justify-content:space-between;align-items:center;padding:16px 24px;background-color:#f8f9fa;border-bottom:1px solid #dadce0;}.emh-modal-header h3{margin:0;color:#202124;font-size:18px;font-weight:500;}.emh-modal-close{color:#5f6368;font-size:28px;font-weight:bold;cursor:pointer;line-height:1;padding:0 5px;transition:color 0.2s ease;}.emh-modal-close:hover{color:#202124;}.emh-modal-body{padding:20px 24px;max-height:65vh;overflow-y:auto;background-color:#fff;}.emh-no-subtitle-message{text-align:center;color:#5f6368;font-size:16px;padding:36px 0;}.emh-subtitle-list{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:14px;}.emh-subtitle-item{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;padding:16px;background-color:#f8f9fa;border-radius:8px;border:1px solid #e8eaed;gap:12px;transition:all 0.2s ease;}.emh-subtitle-item:hover{transform:translateY(-2px);box-shadow:0 4px 8px rgba(0,0,0,0.1);background-color:#f1f5f9;}.emh-subtitle-info{flex:1 1 auto;min-width:200px;}.emh-subtitle-info h4{margin:0 0 8px 0;color:#202124;font-size:16px;font-weight:500;overflow:hidden;text-overflow:ellipsis;}.emh-subtitle-info p{margin:3px 0;color:#5f6368;font-size:14px;line-height:1.5;}.emh-subtitle-item .my-btn-primary{flex-shrink:0; margin-left: auto !important;}
.emh-ui-container{margin:12px 0 8px 0;text-align:center;display:flex;flex-wrap:wrap;justify-content:center;align-items:center;gap:10px;border-top:1px solid #dadce0;padding:14px 8px 6px 8px;background-color:rgba(248,249,250,0.5);border-radius:8px;}
.emh-action-buttons{display:flex;flex-wrap:wrap;justify-content:center;gap:8px;}
.emh-controls-container{margin-top:10px;padding:8px;display:flex;gap:10px;align-items:center;flex-wrap:wrap;background-color:rgba(248,249,250,0.5);border-radius:8px;}
.emh-javgg-controls{margin-top:6px;display:inline-flex;flex-wrap:wrap;gap:6px;align-items:center;margin-left:10px;vertical-align:middle;padding:4px 6px;background-color:rgba(248,249,250,0.5);border-radius:6px;}
#custom-toast-container{position:fixed;top:70px;right:20px;z-index:10000;display:flex;flex-direction:column;gap:10px;}
.custom-toast{padding:14px 20px;border-radius:8px;color:#fff;box-shadow:0 4px 12px rgba(0,0,0,0.25);transition:opacity 0.3s ease,transform 0.3s ease;opacity:0;transform:translateX(100%);font-size:14px;font-weight:500;display:flex;align-items:center;}
.custom-toast.show{opacity:1;transform:translateX(0);}
.custom-toast::before{margin-right:8px;font-weight:bold;}.custom-toast-success{background:linear-gradient(to right, #68D391, #9AE6B4);}.custom-toast-success::before{content:"✓";}
.custom-toast-error{background:linear-gradient(to right, #FC8181, #FEB2B2);}.custom-toast-error::before{content:"✕";}
.custom-toast-info{background:linear-gradient(to right, #A78BFA, #C4B5FD);}.custom-toast-info::before{content:"ℹ";}
.custom-toast-warning{background:linear-gradient(to right, #ff9a9e, #fad0c4); color:#202124;}.custom-toast-warning::before{content:"⚠";}
.emh-button {} .emh-subtitle-button-small {min-width: auto;} .emh-external-link, .emh-external-link-small {} .emh-data-element {cursor: pointer;} .emh-data-element::after {content: " 🔍";opacity: 0.7;margin-left: 4px;} .emh-action-buttons, .emh-controls-container, .emh-javgg-controls, .emh-ui-container {display: flex;flex-wrap: wrap;align-items: center;gap: 8px;} .emh-ui-container {justify-content: center;margin-top: 10px;padding-top: 10px;border-top: 1px solid #e2e8f0;} .emh-javgg-controls {margin-left: 5px;}
/* Styles for the Draggable Custom Subtitle Button */
.emh-draggable-btn {
z-index: 10001 !important;
cursor: grab;
padding: 8px 12px !important;
box-shadow: 0 4px 10px rgba(0,0,0,0.2);
min-width: auto !important;
margin: 0 !important; /* Reset margin from .btn if any */
}
.emh-draggable-btn:active {
cursor: grabbing !important;
}
/* Status Indicator for Video Codes */
.emh-code-status-indicator {
width: 16px;
height: 16px;
border-radius: 50%;
cursor: pointer;
margin-right: 8px;
transition: all 0.2s ease;
position: relative;
border: 1px solid rgba(0,0,0,0.1);
}
.emh-code-status-indicator:hover {
transform: scale(1.2);
box-shadow: 0 0 5px rgba(0,0,0,0.2);
}
.emh-code-status-indicator[data-status="favorite"] {
background-color: #ff4757; /* Red */
}
.emh-code-status-indicator[data-status="watched"] {
background-color: #2ed573; /* Green */
}
.emh-code-status-indicator[data-status="unmarked"] {
background-color: #909090; /* Gray */
}
/* 搜索模态框样式 */
.emh-search-modal-content {
max-width: 540px;
}
.emh-search-form {
margin-bottom: 20px;
}
.emh-search-input-group {
display: flex;
gap: 8px;
width: 100%;
}
.emh-input-wrapper {
position: relative;
flex-grow: 1;
}
.emh-search-input {
width: 100%;
padding: 12px 32px 12px 16px;
border-radius: 8px;
border: 1px solid #dadce0;
font-size: 15px;
outline: none;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.emh-search-input:focus {
border-color: #a78bfa;
box-shadow: 0 0 0 3px rgba(167, 139, 250, 0.2);
}
.emh-search-clear-btn {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: #5f6368;
font-size: 20px;
line-height: 1;
padding: 4px;
cursor: pointer;
border-radius: 50%;
visibility: hidden;
}
.emh-search-clear-btn:hover {
background-color: rgba(0,0,0,0.05);
}
.emh-search-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 0 20px;
border: none;
border-radius: 8px;
background: linear-gradient(45deg, #a78bfa, #c4b5fd);
color: white;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.emh-search-btn:hover {
box-shadow: 0 4px 12px rgba(167, 139, 250, 0.3);
transform: translateY(-1px);
}
.emh-search-btn:active {
transform: translateY(0);
}
/* 搜索历史样式 */
.emh-search-history-section,
.emh-trending-section,
.emh-settings-section {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #f1f5f9;
}
.emh-search-history-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.emh-search-history-header h4,
.emh-trending-section h4,
.emh-settings-section h4 {
margin: 0;
color: #202124;
font-size: 15px;
font-weight: 500;
}
.emh-clear-history-btn {
background: none;
border: none;
color: #5f6368;
font-size: 13px;
cursor: pointer;
padding: 4px 8px;
border-radius: 4px;
}
.emh-clear-history-btn:hover {
background-color: rgba(0,0,0,0.05);
color: #202124;
}
.emh-search-history-list {
display: flex;
flex-direction: column;
gap: 8px;
max-height: 150px;
overflow-y: auto;
}
.emh-history-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
background-color: #f8f9fa;
border-radius: 8px;
transition: background-color 0.2s ease;
}
.emh-history-item:hover {
background-color: #f1f5f9;
}
.emh-history-text {
color: #202124;
font-size: 14px;
flex-grow: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.emh-history-use-btn {
background: none;
border: none;
color: #5f6368;
display: flex;
align-items: center;
justify-content: center;
padding: 4px;
border-radius: 50%;
cursor: pointer;
opacity: 0.7;
transition: all 0.2s ease;
}
.emh-history-use-btn:hover {
background-color: rgba(0,0,0,0.05);
color: #a78bfa;
opacity: 1;
}
.emh-empty-history {
text-align: center;
color: #5f6368;
font-size: 14px;
padding: 12px 0;
}
/* 热门推荐样式 */
.emh-trending-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 12px;
}
.emh-trending-tag {
background: linear-gradient(45deg, #e9d5ff, #f3e8ff);
color: #7e22ce;
border-radius: 16px;
padding: 6px 14px;
font-size: 13px;
cursor: pointer;
transition: all 0.2s ease;
}
.emh-trending-tag:hover {
background: linear-gradient(45deg, #d8b4fe, #e9d5ff);
transform: translateY(-1px);
box-shadow: 0 2px 6px rgba(167, 139, 250, 0.2);
}
/* 悬浮搜索按钮 */
.emh-floating-btn {
position: fixed;
bottom: 120px;
right: 20px;
width: 50px;
height: 50px;
border-radius: 50%;
background: linear-gradient(45deg, #a78bfa, #c4b5fd);
color: white;
border: none;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 4px 10px rgba(0,0,0,0.2);
z-index: 10001;
transition: all 0.2s ease;
}
.emh-floating-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(0,0,0,0.3);
}
/* 自定义滚动条 */
.emh-search-history-list::-webkit-scrollbar {
width: 8px;
}
.emh-search-history-list::-webkit-scrollbar-track {
background: #f1f5f9;
border-radius: 8px;
}
.emh-search-history-list::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 8px;
}
.emh-search-history-list::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
/* 设置项样式 */
.emh-setting-item {
margin: 12px 0;
}
.emh-setting-disabled {
opacity: 0.5;
pointer-events: none;
}
.emh-setting-label {
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
padding: 8px 12px;
background-color: #f8f9fa;
border-radius: 8px;
transition: background-color 0.2s ease;
}
.emh-setting-label:hover {
background-color: #f1f5f9;
}
.emh-setting-label span {
color: #202124;
font-size: 14px;
}
/* 开关样式 */
.emh-toggle-checkbox {
height: 0;
width: 0;
visibility: hidden;
position: absolute;
}
.emh-toggle-switch {
position: relative;
display: inline-block;
width: 40px;
height: 20px;
background: #e2e8f0;
border-radius: 20px;
transition: 0.3s;
}
.emh-toggle-switch:after {
content: '';
position: absolute;
top: 2px;
left: 2px;
width: 16px;
height: 16px;
background: #fff;
border-radius: 16px;
transition: 0.3s;
}
.emh-toggle-checkbox:checked + .emh-toggle-switch {
background: #a78bfa;
}
.emh-toggle-checkbox:checked + .emh-toggle-switch:after {
left: calc(100% - 2px);
transform: translateX(-100%);
}
/* 响应式调整 */
@media (max-width: 576px) {
.emh-search-input-group {
flex-direction: column;
}
.emh-search-btn {
height: 44px;
}
}
/* 美化字幕模态框和列表 */
#emh-subtitle-modal {
backdrop-filter: blur(5px);
}
#emh-subtitle-modal .emh-modal-content {
border: none;
box-shadow: 0 10px 30px rgba(0,0,0,0.25);
}
#emh-subtitle-modal .emh-modal-header {
background: linear-gradient(135deg, #f8f9fa, #e9ecef);
padding: 18px 24px;
}
#emh-subtitle-modal .emh-modal-header h3 {
font-size: 20px;
background: linear-gradient(90deg, #4b6cb7, #182848);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-weight: 600;
}
#emh-subtitle-modal .emh-modal-close {
font-size: 26px;
transition: all 0.2s ease;
border-radius: 50%;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
}
#emh-subtitle-modal .emh-modal-close:hover {
background-color: rgba(0,0,0,0.05);
transform: rotate(90deg);
}
#emh-subtitle-modal .emh-modal-body {
background: linear-gradient(135deg, #ffffff, #f8f9fa);
padding: 22px 24px;
}
/* 美化字幕列表样式 */
#emh-subtitle-modal .emh-subtitle-list {
gap: 16px;
}
#emh-subtitle-modal .emh-subtitle-item {
background: #ffffff;
box-shadow: 0 2px 6px rgba(0,0,0,0.05);
border-radius: 12px;
border: 1px solid rgba(0,0,0,0.08);
padding: 18px;
transition: all 0.25s cubic-bezier(0.25, 0.8, 0.25, 1);
}
#emh-subtitle-modal .emh-subtitle-item:hover {
transform: translateY(-3px);
box-shadow: 0 8px 16px rgba(0,0,0,0.1);
border-color: rgba(75, 108, 183, 0.3);
}
#emh-subtitle-modal .emh-subtitle-info h4 {
font-size: 17px;
font-weight: 600;
margin-bottom: 10px;
color: #2d3748;
}
#emh-subtitle-modal .emh-subtitle-info p {
color: #718096;
font-size: 14.5px;
line-height: 1.5;
}
/* 美化字幕按钮样式 */
#emh-subtitle-modal .emh-subtitle-actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
justify-content: flex-end;
margin-left: auto;
}
#emh-subtitle-modal .emh-subtitle-actions .btn {
padding: 8px 16px;
border-radius: 8px;
font-weight: 500;
transition: all 0.2s ease;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
#emh-subtitle-modal .emh-subtitle-actions .my-btn-primary {
background: linear-gradient(90deg, #4b6cb7, #182848);
border: none;
color: white;
}
#emh-subtitle-modal .emh-subtitle-actions .my-btn-primary:hover {
box-shadow: 0 4px 8px rgba(75, 108, 183, 0.3);
transform: translateY(-2px);
}
#emh-subtitle-modal .emh-subtitle-actions .btn-outline {
border: 1px solid #cbd5e0;
color: #4a5568;
background: white;
}
#emh-subtitle-modal .emh-subtitle-actions .btn-outline:hover {
background: #f7fafc;
border-color: #a0aec0;
}
/* 无字幕时的提示 */
#emh-subtitle-modal .emh-no-subtitle-message {
text-align: center;
color: #718096;
font-size: 17px;
padding: 40px 0;
font-weight: 500;
background: #f7fafc;
border-radius: 8px;
border: 1px dashed #cbd5e0;
}
/* 响应式调整 - 字幕模态框 */
@media (max-width: 576px) {
#emh-subtitle-modal .emh-subtitle-item {
flex-direction: column;
align-items: stretch;
}
#emh-subtitle-modal .emh-subtitle-actions {
margin-top: 16px;
justify-content: center;
}
#emh-subtitle-modal .emh-modal-content {
width: 95%;
max-width: 95%;
}
#emh-subtitle-modal .emh-modal-header h3 {
font-size: 18px;
}
}
.emh-code-manager-toggle {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 10000;
padding: 8px 16px;
border-radius: 8px;
background: #3b82f6;
color: white;
border: none;
cursor: pointer;
font-size: 14px;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
transition: all 0.2s ease;
}
.emh-code-manager-toggle:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
background: #2563eb;
}
.emh-code-manager-panel {
position: fixed;
top: 0;
right: -500px;
width: 450px;
height: 100vh;
background: white;
box-shadow: -5px 0 15px rgba(0,0,0,0.2);
z-index: 22345679 !important;
transition: right 0.3s ease;
display: flex;
flex-direction: column;
}
.emh-code-manager-panel.visible {
right: 0;
}
`;
document.head.appendChild(style);
}
// 创建番号状态标记按钮
function createCodeStatusIndicator(container, code) {
if (!code || !container) return null;
// 初始化 CODE_LIBRARY
if (!CODE_LIBRARY.initialized) {
CODE_LIBRARY.init();
}
// 获取当前番号状态
const currentStatus = CODE_LIBRARY.getStatus(code);
// 创建状态指示器
const statusIndicator = document.createElement('div');
statusIndicator.className = 'emh-code-status-indicator';
statusIndicator.dataset.code = code;
statusIndicator.dataset.status = currentStatus;
// 设置状态图标和颜色
const statusColors = CONFIG.codeManager.statusColors;
statusIndicator.style.backgroundColor = statusColors[currentStatus] || statusColors.unmarked;
// 状态提示文本
let statusText = '未标记';
if (currentStatus === 'favorite') statusText = '已关注';
if (currentStatus === 'watched') statusText = '已看过';
// 根据状态设置不同的提示文本
if (currentStatus === 'watched') {
statusIndicator.title = `状态: ${statusText} (请在番号库中修改状态)`;
statusIndicator.style.cursor = 'default'; // 已看状态下不可点击
} else {
statusIndicator.title = `状态: ${statusText} (点击${currentStatus === 'favorite' ? '取消' : ''}关注)`;
statusIndicator.style.cursor = 'pointer'; // 可点击状态
}
// 点击事件 - 只能切换关注状态
statusIndicator.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
// 获取最新的当前状态
const currentStatus = CODE_LIBRARY.getStatus(code);
// 如果是已看状态,不允许修改
if (currentStatus === 'watched') {
UTILS.showToast('已看状态请在番号库中修改', 'warning');
return;
}
// 在未标记和关注之间切换
const newStatus = currentStatus === 'favorite' ? 'unmarked' : 'favorite';
// 更新标记
CODE_LIBRARY.markItem(code, newStatus);
// 更新UI
updateCodeStatusIndicators();
// 显示提示
const statusText = newStatus === 'favorite' ? '已关注' : '已取消关注';
UTILS.showToast(`番号 ${code} ${statusText}`, 'success');
});
// 添加到容器
container.appendChild(statusIndicator);
return statusIndicator;
}
// 更新所有番号状态指示器
function updateCodeStatusIndicators() {
// 更新所有页面上的状态指示器
document.querySelectorAll('.emh-code-status-indicator').forEach(indicator => {
const code = indicator.dataset.code;
if (!code) return;
const currentStatus = CODE_LIBRARY.getStatus(code);
indicator.dataset.status = currentStatus;
// 更新颜色
const statusColors = CONFIG.codeManager.statusColors;
indicator.style.backgroundColor = statusColors[currentStatus] || statusColors.unmarked;
// 更新提示和鼠标样式
let statusText = '未标记';
if (currentStatus === 'favorite') statusText = '已关注';
if (currentStatus === 'watched') statusText = '已看过';
if (currentStatus === 'watched') {
indicator.title = `状态: ${statusText} (请在番号库中修改状态)`;
indicator.style.cursor = 'default';
} else {
indicator.title = `状态: ${statusText} (点击${currentStatus === 'favorite' ? '取消' : ''}关注)`;
indicator.style.cursor = 'pointer';
}
});
}
// Code Manager Panel Implementation
const CodeManagerPanel = {
initialized: false,
panelElement: null,
currentFilter: 'all',
searchQuery: '',
selectedItems: [],
multiSelectMode: false,
createToggleButton: function() {
const existingButton = document.getElementById('emh-code-manager-toggle');
if (existingButton) {
existingButton.remove();
}
const btn = document.createElement('button');
btn.id = 'emh-code-manager-toggle';
btn.className = 'emh-code-manager-toggle';
btn.innerHTML = '<span>📋 番号库</span>';
btn.title = '管理番号库';
btn.addEventListener('click', () => {
this.togglePanel();
});
document.body.appendChild(btn);
},
init: function() {
if (!this.initialized) {
this.createStyles();
this.createPanelElement();
this.createToggleButton(); // 确保创建番号库按钮
this.attachEventListeners();
this.initialized = true;
console.log('Code Manager Panel initialized');
}
},
// Toggle panel visibility
togglePanel: function() {
if (this.isVisible) {
this.hidePanel();
} else {
this.showPanel();
}
},
// Show the panel
showPanel: function() {
if (!this.panelElement) { // 确保面板 DOM 元素已创建
this.createPanelElement();
}
if (!CODE_LIBRARY.initialized) {
CODE_LIBRARY.init();
}
this.isVisible = true;
this.panelElement.classList.add('visible'); // 使面板滑入视图
this.refreshPanelContent(); // 加载或刷新面板内容
// ---- 新增:针对 javtxt 类网站调整面板位置 ----
if (SITE_HANDLERS.javtxt && typeof SITE_HANDLERS.javtxt.isMatch === 'function' && SITE_HANDLERS.javtxt.isMatch()) {
// !!! 重要: 下面的 'nav.site-navbar' 是一个示例选择器 !!!
// !!! 你需要通过浏览器开发者工具检查 javtxt 网站,找到实际的导航栏元素的选择器 !!!
// !!! 目标是找到那个 z-index 为 12345679 !important 的导航栏元素 !!!
const navbarSelector = 'nav.navbar'; // <--- 【请修改为 javtxt 导航栏的实际 CSS 选择器】
// 例如可能是: 'header#main-header', '.fixed-top-bar', 'div[class*="navbar-fixed"]' 等
let navbarElement = document.querySelector(navbarSelector);
// 如果特定选择器找不到,尝试更通用的、基于已知高 z-index 的启发式查找
if (!navbarElement) {
const potentialNavs = document.querySelectorAll('nav, header'); // 常见的导航栏标签
for (const el of potentialNavs) {
const style = window.getComputedStyle(el);
// 检查是否固定定位在顶部且具有非常高的 z-index
if ((style.position === 'fixed' || style.position === 'sticky') &&
parseInt(style.zIndex) >= 10000000 && // 查找具有类似超高 z-index 的元素
el.getBoundingClientRect().top < 10 && // 确保它在视口顶部
el.offsetHeight > 20) { // 确保它有一定高度
navbarElement = el;
console.log("EMH: Detected potential javtxt navbar via heuristics:", navbarElement);
break;
}
}
}
if (navbarElement) {
const navbarHeight = navbarElement.offsetHeight;
if (navbarHeight > 0) {
this.panelElement.style.top = `${navbarHeight}px`;
this.panelElement.style.height = `calc(100vh - ${navbarHeight}px)`;
console.log(`EMH: Adjusted panel for javtxt navbar. Top: ${navbarHeight}px`);
// (可选) 如果面板的 z-index 之前设置得非常高以覆盖导航栏,现在可以考虑降低它
// this.panelElement.style.zIndex = '10050'; // 例如,一个仍然较高但低于导航栏的值
} else {
// 导航栏找到但高度为0,或不可见,则使用默认全屏
this.panelElement.style.top = '0px';
this.panelElement.style.height = '100vh';
}
} else {
// 未找到导航栏,使用默认全屏
this.panelElement.style.top = '0px';
this.panelElement.style.height = '100vh';
console.warn("EMH: javtxt navbar element not found with selector or heuristics. Panel might be obscured if navbar exists.");
}
} else {
// 非 javtxt 类网站,使用默认全屏
this.panelElement.style.top = '0px';
this.panelElement.style.height = '100vh';
}
// ---- 调整结束 ----
},
// Hide the panel
hidePanel: function() {
this.isVisible = false;
if (this.panelElement) {
this.panelElement.classList.remove('visible');
}
if (this.multiSelectMode) {
this.toggleMultiSelectMode();
}
},
// Create the panel element
createPanelElement: function() {
if (this.panelElement) return;
const panel = document.createElement('div');
panel.id = 'emh-code-manager-panel';
panel.className = 'emh-code-manager-panel';
panel.innerHTML = `
<div class="emh-panel-header">
<h2>番号管理</h2>
<div class="emh-panel-controls">
<button class="emh-panel-close">×</button>
</div>
</div>
<div class="emh-panel-tabs">
<button data-filter="all" class="active">全部</button>
<button data-filter="favorite">关注列表</button>
<button data-filter="watched">已看记录</button>
<button data-filter="trash">回收站</button>
</div>
<div class="emh-panel-search">
<input type="text" placeholder="搜索番号或备注..." />
<button class="emh-search-btn">🔍</button>
</div>
<div class="emh-panel-content">
<!-- Content will be filled dynamically -->
</div>
<div class="emh-panel-actions">
<button id="emh-add-code" class="btn my-btn-primary">添加</button>
<button id="emh-multi-select" class="btn btn-outline">多选</button>
<button id="emh-export" class="btn btn-info">导出</button>
<button id="emh-import" class="btn btn-info">导入</button>
<button id="emh-clear-trash" class="btn my-btn-danger" style="display: none;">清空回收站</button>
</div>
<div class="emh-panel-multi-actions" style="display: none;">
<span class="emh-selected-count">已选择 0 项</span>
<button id="emh-mark-favorite" class="btn my-btn-danger">标为关注</button>
<button id="emh-mark-watched" class="btn my-btn-success">标为已看</button>
<button id="emh-delete-selected" class="btn btn-outline">删除</button>
<button id="emh-cancel-multi" class="btn btn-outline">取消</button>
</div>
<div class="emh-panel-modal" style="display: none;">
<div class="emh-panel-modal-content">
<h3></h3>
<div class="emh-panel-modal-buttons">
<button class="btn my-btn-danger emh-panel-modal-confirm">确定</button>
<button class="btn btn-outline emh-panel-modal-cancel">取消</button>
</div>
</div>
</div>
`;
document.body.appendChild(panel);
this.panelElement = panel;
panel.querySelector('.emh-panel-close').addEventListener('click', () => {
this.hidePanel();
});
},
// Add CSS styles for the panel
createStyles: function() {
const styleElement = document.createElement('style');
styleElement.textContent = `
.emh-code-manager-toggle {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 10000;
padding: 8px 16px;
border-radius: 8px;
background: #3b82f6;
color: white;
border: none;
cursor: pointer;
font-size: 14px;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
transition: all 0.2s ease;
}
.emh-code-manager-toggle:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
background: #2563eb;
}
.emh-code-manager-panel {
position: fixed;
top: 0;
right: -500px;
width: 450px;
height: 100vh;
background: white;
box-shadow: -5px 0 15px rgba(0,0,0,0.2);
z-index: 10010;
transition: right 0.3s ease;
display: flex;
flex-direction: column;
}
.emh-code-manager-panel.visible {
right: 0;
}
.emh-panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
border-bottom: 1px solid #e2e8f0;
}
.emh-panel-header h2 {
margin: 0;
font-size: 20px;
font-weight: 500;
color: #334155;
}
.emh-panel-close {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #64748b;
}
.emh-panel-tabs {
display: flex;
border-bottom: 1px solid #e2e8f0;
padding: 0 15px;
}
.emh-panel-tabs button {
background: none;
border: none;
padding: 12px 15px;
font-size: 14px;
cursor: pointer;
color: #64748b;
position: relative;
}
.emh-panel-tabs button.active {
color: #3b82f6;
font-weight: 500;
}
.emh-panel-tabs button.active::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background: #3b82f6;
}
.emh-panel-search {
padding: 15px;
display: flex;
border-bottom: 1px solid #e2e8f0;
}
.emh-panel-search input {
flex: 1;
padding: 8px 12px;
border: 1px solid #cbd5e1;
border-radius: 4px 0 0 4px;
outline: none;
}
.emh-search-btn {
background: #f1f5f9;
border: 1px solid #cbd5e1;
border-left: none;
border-radius: 0 4px 4px 0;
padding: 0 12px;
cursor: pointer;
}
.emh-panel-content {
flex: 1;
overflow-y: auto;
padding: 15px;
background: #f8fafc;
}
.emh-panel-actions,
.emh-panel-multi-actions {
padding: 15px;
display: flex;
gap: 10px;
border-top: 1px solid #e2e8f0;
background: white;
}
.emh-item {
display: flex;
align-items: center;
padding: 12px;
border: 1px solid #e2e8f0;
border-radius: 6px;
margin-bottom: 8px;
background: white;
}
.emh-item.favorite {
border-color: #f56565;
background: #fff5f5;
}
.emh-item.watched {
border-color: #48bb78;
background: #f0fff4;
}
.emh-item.selected {
border-color: #4299e1;
background: #ebf8ff;
}
.emh-item-code {
font-weight: 500;
color: #2d3748;
margin-right: 12px;
}
.emh-item-remarks {
flex: 1;
color: #718096;
font-size: 14px;
}
.emh-item-actions {
display: flex;
gap: 8px;
}
.emh-item-actions button {
background: none;
border: none;
cursor: pointer;
padding: 4px;
opacity: 0.7;
transition: opacity 0.2s;
}
.emh-item-actions button:hover {
opacity: 1;
}
.emh-empty-state {
text-align: center;
color: #718096;
padding: 40px 0;
}
@media (max-width: 576px) {
.emh-code-manager-panel {
width: 100%;
right: -100%;
}
}
.emh-panel-modal {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: none;
justify-content: center;
align-items: center;
z-index: 10011;
}
.emh-panel-modal-content {
background: white;
padding: 20px;
border-radius: 8px;
width: 80%;
max-width: 300px;
text-align: center;
}
.emh-panel-modal-content h3 {
margin: 0 0 20px 0;
color: #334155;
}
.emh-panel-modal-buttons {
display: flex;
justify-content: center;
gap: 10px;
margin-top: 20px;
}
`;
document.head.appendChild(styleElement);
},
// Attach event listeners
attachEventListeners: function() {
// Tab switching
this.panelElement.querySelectorAll('.emh-panel-tabs button').forEach(tab => {
tab.addEventListener('click', (e) => {
this.currentFilter = e.target.dataset.filter;
this.refreshPanelContent();
// Update active tab
this.panelElement.querySelectorAll('.emh-panel-tabs button').forEach(t => {
t.classList.remove('active');
});
e.target.classList.add('active');
});
});
// Search functionality
const searchInput = this.panelElement.querySelector('.emh-panel-search input');
const searchBtn = this.panelElement.querySelector('.emh-search-btn');
searchInput.addEventListener('input', (e) => {
this.searchQuery = e.target.value;
this.refreshPanelContent();
});
searchBtn.addEventListener('click', () => {
this.refreshPanelContent();
});
// Multi-select mode
const multiSelectBtn = this.panelElement.querySelector('#emh-multi-select');
multiSelectBtn.addEventListener('click', () => {
this.toggleMultiSelectMode();
});
// Multi-select actions
this.panelElement.querySelector('#emh-mark-favorite').addEventListener('click', () => {
this.batchMarkItems('favorite');
});
this.panelElement.querySelector('#emh-mark-watched').addEventListener('click', () => {
this.batchMarkItems('watched');
});
this.panelElement.querySelector('#emh-delete-selected').addEventListener('click', () => {
this.batchDeleteItems();
});
this.panelElement.querySelector('#emh-cancel-multi').addEventListener('click', () => {
this.toggleMultiSelectMode();
});
// Add code button
this.panelElement.querySelector('#emh-add-code').addEventListener('click', () => {
const code = prompt('请输入要添加的番号:');
if (code) {
CODE_LIBRARY.add(code);
this.refreshPanelContent();
UTILS.showToast(`番号 ${code} 已添加`, 'success');
}
});
// Export button
this.panelElement.querySelector('#emh-export').addEventListener('click', () => {
const data = CODE_LIBRARY.exportData();
const blob = new Blob([JSON.stringify(data, null, 2)], {type: 'application/json'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'emh_code_library.json';
a.click();
URL.revokeObjectURL(url);
UTILS.showToast('数据导出成功', 'success');
});
// Import button
this.panelElement.querySelector('#emh-import').addEventListener('click', () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = JSON.parse(e.target.result);
CODE_LIBRARY.importData(data);
this.refreshPanelContent();
UTILS.showToast('数据导入成功', 'success');
} catch (err) {
console.error('Import failed:', err);
UTILS.showToast('数据导入失败', 'error');
}
};
reader.readAsText(file);
}
};
input.click();
});
// 修改删除按钮的事件处理
const self = this;
this.panelElement.addEventListener('click', function(e) {
const deleteBtn = e.target.closest('.emh-delete');
if (deleteBtn) {
e.stopPropagation();
const item = deleteBtn.closest('.emh-item');
const code = item.dataset.code;
if (code) {
self.showConfirmDialog(`确定要删除番号 ${code} 吗?`, function() {
CODE_LIBRARY.delete(code);
self.refreshPanelContent();
UTILS.showToast(`番号 ${code} 已删除`, 'success');
});
}
}
});
// 添加清空回收站按钮事件
const clearTrashBtn = this.panelElement.querySelector('#emh-clear-trash');
if (clearTrashBtn) {
clearTrashBtn.addEventListener('click', () => {
this.clearTrash();
});
}
},
// Toggle multi-select mode
toggleMultiSelectMode: function() {
this.multiSelectMode = !this.multiSelectMode;
this.selectedItems = [];
const actionsBar = this.panelElement.querySelector('.emh-panel-actions');
const multiActionsBar = this.panelElement.querySelector('.emh-panel-multi-actions');
if (this.multiSelectMode) {
actionsBar.style.display = 'none';
multiActionsBar.style.display = 'flex';
} else {
actionsBar.style.display = 'flex';
multiActionsBar.style.display = 'none';
}
this.refreshPanelContent();
},
// Refresh panel content based on current filter and search term
refreshPanelContent: function() {
const contentArea = this.panelElement.querySelector('.emh-panel-content');
let items = [];
// Get items based on current filter
switch(this.currentFilter) {
case 'favorite':
items = CODE_LIBRARY.getFavorites();
break;
case 'watched':
items = CODE_LIBRARY.getWatched();
break;
case 'trash':
items = CODE_LIBRARY.getTrash();
// 在回收站视图中显示清空按钮
this.panelElement.querySelector('#emh-clear-trash').style.display = 'inline-flex';
// 隐藏不相关的按钮
this.panelElement.querySelector('#emh-add-code').style.display = 'none';
this.panelElement.querySelector('#emh-multi-select').style.display = 'none';
break;
default:
items = CODE_LIBRARY.getAll();
// 在非回收站视图中隐藏清空按钮
this.panelElement.querySelector('#emh-clear-trash').style.display = 'none';
// 显示常规按钮
this.panelElement.querySelector('#emh-add-code').style.display = 'inline-flex';
this.panelElement.querySelector('#emh-multi-select').style.display = 'inline-flex';
}
// Apply search filter if needed
if (this.searchQuery) {
items = items.filter(item =>
item.code.toLowerCase().includes(this.searchQuery.toLowerCase()) ||
(item.remarks && item.remarks.toLowerCase().includes(this.searchQuery.toLowerCase()))
);
}
// Generate HTML for items
const itemsHtml = items.map(item => this.generateItemHtml(item)).join('');
contentArea.innerHTML = itemsHtml || '<div class="emh-empty-state">没有找到相关记录</div>';
// Update selected count if in multi-select mode
if (this.multiSelectMode) {
this.panelElement.querySelector('.emh-selected-count').textContent =
`已选择 ${this.selectedItems.length} 项`;
}
// Add click handlers for items
contentArea.querySelectorAll('.emh-item').forEach(item => {
const code = item.dataset.code;
if (this.multiSelectMode) {
// Multi-select mode click handler
item.addEventListener('click', () => {
const checkbox = item.querySelector('input[type="checkbox"]');
checkbox.checked = !checkbox.checked;
if (checkbox.checked) {
this.selectedItems.push(code);
item.classList.add('selected');
} else {
this.selectedItems = this.selectedItems.filter(c => c !== code);
item.classList.remove('selected');
}
this.panelElement.querySelector('.emh-selected-count').textContent =
`已选择 ${this.selectedItems.length} 项`;
});
} else {
// Normal mode - individual action handlers
const actions = item.querySelector('.emh-item-actions');
if (actions) {
actions.querySelector('.emh-mark-favorite').addEventListener('click', (e) => {
e.stopPropagation();
CODE_LIBRARY.markItem(code, 'favorite');
this.refreshPanelContent();
UTILS.showToast(`番号 ${code} 已标记为关注`, 'success');
});
actions.querySelector('.emh-mark-watched').addEventListener('click', (e) => {
e.stopPropagation();
CODE_LIBRARY.markItem(code, 'watched');
this.refreshPanelContent();
UTILS.showToast(`番号 ${code} 已标记为已看`, 'success');
});
actions.querySelector('.emh-delete').addEventListener('click', (e) => {
e.stopPropagation();
this.showConfirmDialog(`确定要删除番号 ${code} 吗?`, () => {
CODE_LIBRARY.delete(code);
this.refreshPanelContent();
UTILS.showToast(`番号 ${code} 已删除`, 'success');
});
});
}
}
});
},
// Generate HTML for a single item
generateItemHtml: function(item) {
const isSelected = this.selectedItems.includes(item.code);
const statusClass = item.status === 'favorite' ? 'favorite' :
item.status === 'watched' ? 'watched' : '';
return `
<div class="emh-item ${statusClass} ${isSelected ? 'selected' : ''}"
data-code="${item.code}">
${this.multiSelectMode ? `
<input type="checkbox" ${isSelected ? 'checked' : ''} />
` : ''}
<div class="emh-item-code">${item.code}</div>
<div class="emh-item-remarks">${item.remarks || ''}</div>
<div class="emh-item-actions">
${!this.multiSelectMode ? `
<button class="emh-mark-favorite">❤️</button>
<button class="emh-mark-watched">✓</button>
<button class="emh-delete">🗑️</button>
` : ''}
</div>
</div>
`;
},
// Batch operations
batchMarkItems: function(status) {
this.selectedItems.forEach(code => {
CODE_LIBRARY.markItem(code, status);
});
this.toggleMultiSelectMode();
this.refreshPanelContent();
UTILS.showToast(`已批量标记 ${this.selectedItems.length} 个番号`, 'success');
},
batchDeleteItems: function() {
const self = this;
this.showConfirmDialog(`确定要删除选中的 ${this.selectedItems.length} 项吗?`, function() {
self.selectedItems.forEach(code => {
CODE_LIBRARY.delete(code);
});
self.toggleMultiSelectMode();
self.refreshPanelContent();
UTILS.showToast(`已删除 ${self.selectedItems.length} 个番号`, 'success');
});
},
// Show confirmation dialog
showConfirmDialog: function(message, onConfirm) {
const modal = this.panelElement.querySelector('.emh-panel-modal');
const modalTitle = modal.querySelector('h3');
const confirmBtn = modal.querySelector('.emh-panel-modal-confirm');
const cancelBtn = modal.querySelector('.emh-panel-modal-cancel');
modalTitle.textContent = message;
modal.style.display = 'flex';
const handleConfirm = () => {
modal.style.display = 'none';
onConfirm();
cleanup();
};
const handleCancel = () => {
modal.style.display = 'none';
cleanup();
};
const cleanup = () => {
confirmBtn.removeEventListener('click', handleConfirm);
cancelBtn.removeEventListener('click', handleCancel);
};
confirmBtn.addEventListener('click', handleConfirm);
cancelBtn.addEventListener('click', handleCancel);
},
// 添加清空回收站方法
clearTrash: function() {
if (!CODE_LIBRARY.trash.items.length) {
UTILS.showToast('回收站已经是空的', 'info');
return;
}
this.showConfirmDialog('确定要清空回收站吗?此操作不可撤销!', () => {
CODE_LIBRARY.trash.items = [];
CODE_LIBRARY.save();
this.refreshPanelContent();
UTILS.showToast('回收站已清空', 'success');
});
},
};
function initialize() {
addCustomStyles();
// 加载用户设置
try {
const savedSubtitleOptions = localStorage.getItem('emh_subtitle_filename_options');
if (savedSubtitleOptions) {
const parsedOptions = JSON.parse(savedSubtitleOptions);
// 合并保存的设置到CONFIG
if (parsedOptions) {
CONFIG.subtitleFilenameOptions = {
...CONFIG.subtitleFilenameOptions,
...parsedOptions
};
}
}
} catch (err) {
console.error('加载字幕设置失败:', err);
}
// 初始化番号库
CODE_LIBRARY.init();
UTILS.createDraggableSubtitleButton(); // Create the draggable button
// 初始化番号管理面板
if (typeof CodeManagerPanel !== 'undefined') {
window.CodeManagerPanel = CodeManagerPanel;
CodeManagerPanel.init();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', main);
} else {
main();
}
console.log("EMH Initialized with Enhanced Subtitle Search and Code Manager");
// 添加自定义事件监听器
window.addEventListener('emh_library_updated', function(e) {
if (e.detail.type === 'library_update') {
// 更新所有状态指示器
updateCodeStatusIndicators();
// 如果面板是打开的,刷新面板内容
if (CodeManagerPanel.isVisible) {
CodeManagerPanel.refreshPanelContent();
}
}
});
// 添加 GM 存储变化监听
if (typeof GM_addValueChangeListener !== 'undefined') {
GM_addValueChangeListener('emh_sync_timestamp', function(name, old_value, new_value, remote) {
if (remote) {
CODE_LIBRARY.init();
updateCodeStatusIndicators();
if (CodeManagerPanel.isVisible) {
CodeManagerPanel.refreshPanelContent();
}
}
});
}
// 定期检查更新(作为备用同步机制)
setInterval(function() {
if (typeof GM_getValue !== 'undefined') {
const lastUpdate = GM_getValue('emh_sync_timestamp');
if (lastUpdate && lastUpdate !== CodeManagerPanel.lastSyncTimestamp) {
CodeManagerPanel.lastSyncTimestamp = lastUpdate;
CODE_LIBRARY.init();
updateCodeStatusIndicators();
if (CodeManagerPanel.isVisible) {
CodeManagerPanel.refreshPanelContent();
}
}
}
}, 2000); // 每2秒检查一次
}
initialize();
})();