// ==UserScript==
// @name 豆瓣+TMDB影视工具(AI增强)
// @namespace tampermonkey
// @version 1.1
// @description 影视信息提取与排版工具,添加AI文字生成功能
// @author 绘梦
// @icon https://img.icons8.com/fluency/48/cinema-.png
// @match https://pan1.me/?thread-create-*.htm
// @match https://pan1.me/?post-update-*.htm
// @grant GM_xmlhttpRequest
// @grant GM_setClipboard
// @grant GM_log
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @connect www.douban.com
// @connect accounts.douban.com
// @connect search.douban.com
// @connect movie.douban.com
// @connect m.douban.com
// @connect doubanio.com
// @connect search.doubanio.com
// @connect tv.douban.com
// @connect doubanio.com
// @run-at document-end
// @license Copyright (c) 2024 绘梦. All rights reserved. 未经许可禁止修改和重新分发。
// @downloadURL https://update.greasyfork.icu/scripts/548948/%E8%B1%86%E7%93%A3%2BTMDB%E5%BD%B1%E8%A7%86%E5%B7%A5%E5%85%B7%EF%BC%88AI%E5%A2%9E%E5%BC%BA%EF%BC%89.user.js
// @updateURL https://update.greasyfork.icu/scripts/548948/%E8%B1%86%E7%93%A3%2BTMDB%E5%BD%B1%E8%A7%86%E5%B7%A5%E5%85%B7%EF%BC%88AI%E5%A2%9E%E5%BC%BA%EF%BC%89.meta.js
// ==/UserScript==
(function () {
'use strict';
// === Douban anti-crawl limiter ===
// 兼顾速度与风控:放宽总频率,但引入“批量节流”与图片并发池
const D_RATE = { maxPerMin: 26, minDelay: 380, maxDelay: 900 };
let dLastTs = 0, dTokens = D_RATE.maxPerMin, dWindow = Date.now();
let doubanCooldownUntil = 0;
function inCooldown() { return Date.now() < doubanCooldownUntil; }
function triggerCooldown(sec = 90) { doubanCooldownUntil = Date.now() + sec*1000; }
function waitDoubanSlot() {
return new Promise(res => {
const refill = () => {
const now = Date.now();
if (now - dWindow >= 60000) { dWindow = now; dTokens = D_RATE.maxPerMin; }
if (dTokens > 0) {
dTokens--;
const jitter = D_RATE.minDelay + Math.floor(Math.random()*(D_RATE.maxDelay-D_RATE.minDelay));
const gap = Math.max(0, D_RATE.minDelay - (now - dLastTs));
const delay = Math.max(jitter, gap);
setTimeout(()=>{ dLastTs = Date.now(); res(); }, delay);
} else setTimeout(refill, 300);
};
refill();
});
}
const UA_POOL = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 12_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.3 Safari/605.1.15'
];
function isDoubanUrl(u){ try{ return /douban\.(com|io)|doubanio\.com/.test(new URL(u).hostname) || /douban/gi.test(u); }catch(_){ return /douban/.test(String(u)); } }
function doubanRequest(opts){
return new Promise(async (resolve,reject)=>{
if (!opts || !opts.url) return reject(new Error('bad-opts'));
const isDouban = isDoubanUrl(opts.url);
if (isDouban && inCooldown()) return reject(new Error('cooldown'));
if (isDouban) await waitDoubanSlot();
GM_xmlhttpRequest({
...opts,
headers: {
'User-Agent': UA_POOL[Math.floor(Math.random()*UA_POOL.length)],
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
'Referer': 'https://movie.douban.com/',
...(opts.headers||{})
},
onload: (res)=>{
if (isDouban) {
const txt = res.responseText || '';
const guarded = (res.finalUrl && /accounts\.|j\/app\/user\/check/.test(res.finalUrl)) || /请证明你是人类|嗯…/.test(txt);
if (guarded) { triggerCooldown(90); reject(new Error('douban-guard')); return; }
}
resolve(res);
},
onerror: (e)=> reject(e),
ontimeout: ()=> reject(new Error('timeout'))
});
});
}
// 默认配置(用户可以通过配置界面修改)
// 预连接常用域名,降低首包延迟
try {
(function preconnectHosts(hosts){
try {
const head = document.head || document.getElementsByTagName('head')[0];
if (!head) return;
hosts.forEach(h => {
try {
const l1 = document.createElement('link'); l1.rel = 'preconnect'; l1.href = h; l1.crossOrigin = 'anonymous'; head.appendChild(l1);
const l2 = document.createElement('link'); l2.rel = 'dns-prefetch'; l2.href = h; head.appendChild(l2);
} catch(e) {}
});
} catch (e) {}
})([
'https://movie.douban.com',
'https://search.douban.com',
'https://m.douban.com',
'https://www.douban.com',
'https://doubanio.com',
'https://api.themoviedb.org',
'https://www.themoviedb.org',
'https://image.tmdb.org'
]);
} catch (e) {}
const DEFAULT_CONFIG = {
TMDB: {
API_KEY: '',
ACCESS_TOKEN: '',
BASE_URL: 'https://api.themoviedb.org/3',
IMAGE_BASE_URL: 'https://image.tmdb.org/t/p/',
POSTER_SIZE: 'w780',
STILL_SIZE: 'w780',
// 新增:列表与选中分级尺寸,提升“看板加载速度+选中质量”
LIST_POSTER_SIZE: 'w342',
LIST_STILL_SIZE: 'w300',
SELECTED_POSTER_SIZE: 'original',
SELECTED_STILL_SIZE: 'original',
DOUBAN_QUALITY: {
PRIORITY: ['raw', 'l', 'm'],
TIMEOUT: 3000,
RETRY: 1
},
IMAGE_CANDIDATES_COUNT: 5,
POSTER_PER_ROW: 5,
STILL_PER_ROW: 5
},
AI: {
API_ENDPOINT: 'https://api.openai.com/v1/chat/completions',
DEFAULT_MODEL: 'gpt-3.5-turbo',
API_KEY: '',
PROVIDER: 'openai',
FEATURES: [
{ id: 'summary', name: '生成剧情简介', placeholder: '请输入剧情简介要求,例如:详细、简洁、适合推荐等' },
{ id: 'comment', name: '生成评论摘要', placeholder: '请输入评论摘要要求,例如:正面、客观、有深度等' },
{ id: 'tagline', name: '生成宣传标语', placeholder: '请输入宣传标语要求,例如:吸引人、简洁有力等' },
{ id: 'analysis', name: '生成深度分析', placeholder: '请输入分析要求,例如:主题分析、角色分析、视听语言分析等' },
{ id: 'post_format', name: '资源帖排版美化', placeholder: '请输入排版要求,例如:适合论坛发布、美观、信息完整等' },
{ id: 'content_optimize', name: '内容优化建议', placeholder: '请输入优化方向,例如:SEO优化、吸引流量、符合平台规范等' },
{ id: 'format_check', name: '排版合规检查', placeholder: '请输入检查重点,例如:版权信息、敏感词、排版结构等' },
{ id: 'modular_design', name: '模块化排版设计', placeholder: '请输入设计需求,例如:分章节、醒目重点、便于阅读等' }
],
// API获取指南(2025年版)
API_GUIDE: {
zhihu: 'https://www.zhihu.com/question/492416413', // AI API汇总
official: {
glm4: 'https://console.baai.ac.cn/',
qwen: 'https://modelscope.cn/',
xunfei: 'https://www.xfyun.cn/',
huggingface: 'https://huggingface.co/',
gemini: 'https://makersuite.google.com/'
}
},
// 影视资源帖排版美化智能体框架设定
POST_FORMAT_GUIDELINES: {
// 核心工具矩阵:按能力分级的编辑器选型
BASIC_EDITORS: [
{name: '键盘喵速排', features: ['影视推荐专属模板库', '拖拽式操作', '发布前自动检测高风险字符', '响应式布局模块']},
{name: 'Canva影视模板库', features: ['16:9黄金比例优化', '一键替换海报素材', '移动端缩略图自动生成']},
{name: '163Editor', features: ['双视图编辑模式', '引用块展示影评', '电影评分组件', '实时敏感词检测']}
],
ADVANCED_TOOLS: [
{name: 'VS Code插件组合', features: ['Prettier自动格式化', 'Stylelint CSS兼容性检测', 'Live Server实时预览', 'HTML Snippets代码片段']},
{name: 'CKEditor4', features: ['精准表格工具', '海报自动压缩', '代码块功能', '可配置影视专用样式']}
],
MARKDOWN_WORKFLOW: [
{name: 'Typora+Marked.js', features: ['::: movie语法块', '图片对齐指令', '标准标题层级', '自动过滤危险HTML标签']},
{name: 'Markdown2Html', features: ['theme: cinema参数', '图片自适应', '防盗链优化', '模板复用功能']}
],
// 辅助工具链:从美化到合规的全流程支持
VISUAL_ENHANCEMENT: {
COLOR_SYSTEM: {
horror: ['#2d3142', '#ef8354'],
romance: ['#f8b195', '#f8e1d1'],
action: ['#335c67', '#e09f3e'],
drama: ['#3a0ca3', '#4361ee'],
comedy: ['#ffb347', '#fdfd96'],
sciFi: ['#4169e1', '#87cefa'],
fantasy: ['#9370db', '#e6e6fa'],
anime: ['#ff69b4', '#ffb6c1'],
documentary: ['#708090', '#d3d3d3']
},
IMAGE_OPTIMIZATION: {
MAX_WIDTH: 800,
MAX_SIZE_KB: 500,
QUALITY: 85,
UNIFORM_BORDER: '2px solid #e0e0e0',
UNIFORM_RADIUS: '5px'
},
TYPOGRAPHY: {
FONT_FAMILY: 'Microsoft Yahei, sans-serif',
LINE_HEIGHT: 1.7,
PARAGRAPH_SPACING: '20px',
HIGHLIGHT_STYLE: 'background:#fff380;padding:0 3px;border-radius:2px'
}
},
COMPLIANCE_CHECKS: {
SENSITIVE_WORDS: {
TOOL: 'sensitive-word',
KEYWORDS: ['盗版', '枪版', '百度云', '网盘'],
REPLACEMENTS: {
'百度云': '合规平台',
'免费观看': '正版渠道观看',
'资源获取': '内容获取'
}
},
COPYRIGHT_RISK: [
'优先使用官方宣传海报',
'注明"用于影视推荐合理使用"',
'用户影评注明来源',
'专业影评引用保留作者署名且不超过原文1/3',
'仅推荐正规视频平台链接'
],
PLATFORM_RULES: {
COMMON: ['避免特殊符号(★、→等)', '评分使用文本或CSS实现', '包含"支持正版影视"声明'],
MOBILE_OPTIMIZATION: ['按钮最小尺寸44×44px', '删除PC端悬浮效果', '增大触摸区域']
}
},
// 模块化排版框架:影视资源帖专属结构
CORE_MODULES: {
POSTER_AREA: {
HTML: '

电影官方海报 | 来源:豆瓣电影
',
CSS: '.poster-container {position: relative; padding-bottom: 56.25%; overflow: hidden;} .responsive-poster {position: absolute; width: 100%; height: 100%; object-fit: cover; border-radius: 8px;}'
},
INFO_CARD: {
HTML: '导演:张艺谋
类型:剧情 / 历史
上映:2023-09-30
评分:9.2
',
CSS: '.info-grid {display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; padding: 15px;} .score {background: #2a9d8f; color: white; padding: 2px 8px; border-radius: 12px;}'
},
SYNOPSIS: {
HTML: '影片讲述了...(无剧透版本)
点击查看完整剧情
完整剧情描述...
',
CSS: 'summary {list-style: none; color: #2a9d8f; cursor: pointer;} summary::-webkit-details-marker {display: none;}'
},
REVIEWS: {
HTML: '',
CSS: '.official-review {border-left: 3px solid #2a9d8f; padding-left: 15px; margin-bottom: 15px;} .user-review {border-left: 3px solid #94a3b8; padding-left: 15px;} .reviewer {font-weight: bold; margin-bottom: 5px;}'
}
},
// 合规发布流程:从编辑到上线的校验清单
PUBLISH_FLOW: {
PREPROCESSING: [
'素材合规检查:校验海报使用权,符合"宣传性质合理使用"原则',
'主题色设定:根据影片类型确定三色体系,全文色彩不超过4种',
'模块规划:按"头图→信息卡→剧情→影评→资源提示"搭建框架'
],
EDITING: [
'文本净化:运行sensitive-word工具扫描全文,替换风险表述',
'样式统一:格式化代码,确保margin统一为25px,padding为15px',
'响应式测试:在375px、768px、1200px三个断点预览'
],
FINAL_CHECK: [
'功能测试:点击所有折叠面板和链接按钮,检查交互正常',
'合规复查:确认无特殊符号,包含"支持正版影视"声明',
'多平台适配:生成掘金版、公众号版、知乎版等不同版本'
]
},
// 避坑指南:影视排版高频问题解决方案
PITFALL_GUIDE: {
TECHNICAL_ISSUES: [
'图片溢出:使用object-fit: cover裁剪中心区域,配合max-width: 100%',
'代码过滤:平台过滤style标签时,转换为内联样式',
'加载速度:海报采用渐进式加载,长文本使用分段加载'
],
EXPERIENCE_OPTIMIZATION: [
'阅读体验:剧情文本行高1.7倍,段落间距20px',
'重点突出:使用主色加粗而非特殊符号标注重点内容',
'特殊符号:统一替换为【】,评分星星用★文本替代'
],
PLATFORM_ADAPTATION: [
'掘金:保留代码高亮,优化图片懒加载',
'微信公众号:简化样式,优化防盗链格式',
'知乎:优化首图尺寸,调整段落间距'
]
}
}
}
};
// 配置管理功能
function getConfig() {
const config = {
TMDB: {
...DEFAULT_CONFIG.TMDB,
API_KEY: GM_getValue('tmdb_api_key', ''),
ACCESS_TOKEN: GM_getValue('tmdb_access_token', '')
},
AI: {
...DEFAULT_CONFIG.AI,
API_ENDPOINT: GM_getValue('ai_api_endpoint', DEFAULT_CONFIG.AI.API_ENDPOINT),
API_KEY: GM_getValue('ai_api_key', ''),
DEFAULT_MODEL: GM_getValue('ai_model', DEFAULT_CONFIG.AI.DEFAULT_MODEL),
PROVIDER: GM_getValue('ai_provider', 'openai')
}
};
return config;
}
function saveConfig(config) {
GM_setValue('tmdb_api_key', config.TMDB.API_KEY);
GM_setValue('tmdb_access_token', config.TMDB.ACCESS_TOKEN);
GM_setValue('ai_api_endpoint', config.AI.API_ENDPOINT);
GM_setValue('ai_api_key', config.AI.API_KEY);
GM_setValue('ai_model', config.AI.DEFAULT_MODEL);
GM_setValue('ai_provider', config.AI.PROVIDER);
}
// 创建配置管理界面
function createConfigDialog() {
const dialog = document.createElement('div');
dialog.id = 'config-dialog';
dialog.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
z-index: 50;
display: flex;
justify-content: center;
align-items: center;
font-family: 'Microsoft YaHei', sans-serif;
`;
const configPanel = document.createElement('div');
configPanel.style.cssText = `
background: white;
border-radius: 10px;
padding: 30px;
max-width: 600px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
`;
const currentConfig = getConfig();
configPanel.innerHTML = `
🔧 脚本配置管理
`;
dialog.appendChild(configPanel);
document.body.appendChild(dialog);
// 绑定事件
document.getElementById('save-config').onclick = () => {
const newConfig = {
TMDB: {
...DEFAULT_CONFIG.TMDB,
API_KEY: document.getElementById('tmdb-api-key').value.trim(),
ACCESS_TOKEN: document.getElementById('tmdb-access-token').value.trim()
},
AI: {
...DEFAULT_CONFIG.AI,
API_ENDPOINT: document.getElementById('ai-api-endpoint').value.trim(),
API_KEY: document.getElementById('ai-api-key').value.trim(),
DEFAULT_MODEL: document.getElementById('ai-model').value.trim(),
PROVIDER: document.getElementById('ai-provider').value
}
};
saveConfig(newConfig);
showNotification('配置已保存!', 'success');
document.body.removeChild(dialog);
};
document.getElementById('close-config').onclick = () => {
document.body.removeChild(dialog);
};
// 提供商选择变化时更新端点
document.getElementById('ai-provider').onchange = () => {
const provider = document.getElementById('ai-provider').value;
const endpointInput = document.getElementById('ai-api-endpoint');
switch(provider) {
case 'openai':
endpointInput.value = 'https://api.openai.com/v1/chat/completions';
break;
case 'claude':
endpointInput.value = 'https://api.anthropic.com/v1/messages';
break;
case 'gemini':
endpointInput.value = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent';
break;
default:
endpointInput.value = '';
}
};
// 点击背景关闭
dialog.onclick = (e) => {
if (e.target === dialog) {
document.body.removeChild(dialog);
}
};
}
const COMMON_HEADERS = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.9',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1',
// 避免跳转到 app/user/check 等登录检测页
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-Site': 'same-origin'
};
// 存储变量
let selectedPosterUrl = '';
let selectedStillUrl = '';
// AI区域多选集合(不影响主流程单选逻辑)
let aiSelectedPosterUrls = new Set();
let aiSelectedStillUrls = new Set();
// AI侧独立的影片信息,避免与主流程共享状态
let aiCurrentMovieInfo = null;
// AI请求控制器(用于终止)
let aiCurrentRequest = null;
let aiAbortReject = null;
// 主功能与AI隔离标记
let isMainFlowActive = false;
// 控制台防刷屏:仅提示一次未检测到编辑器
let editorNotFoundLogged = false;
// 通用事件隔离函数,防止模板区域按钮触发编辑器自动保存
// 注意:此函数会同时阻止冒泡和默认行为,仅用于需要完全阻止原始行为的按钮
function isolateEvent(e) {
if (e && typeof e.stopPropagation === 'function') e.stopPropagation();
if (e && typeof e.preventDefault === 'function') e.preventDefault();
}
// 通用模板按钮样式重置函数,减少重复代码
function resetTemplateButtonStyles() {
document.querySelectorAll('#template-toolbar button[data-tpl-id]').forEach(b => {
b.style.background = '#f472b6';
b.style.color = '#fff';
b.style.border = '1px solid transparent';
b.style.boxShadow = '0 1px 3px rgba(244,114,182,.2)';
});
}
// 防重复绑定AI事件
let aiEventsBound = false;
let currentMovieInfo = null;
let currentComments = [];
let sourceCodeElement = null;
let panelObserver = null;
let isPanelInitialized = false;
let currentEditor = null;
let posterPage = 1; // 海报当前页(初始1)
let stillPage = 1; // 剧照当前页(初始1)
let isLoadingPosters = false;
let isLoadingStills = false;
let posterContainer = null;
let stillContainer = null;
let panel = null;
let selectedPosterEl = null;
let selectedStillEl = null;
// 排版美化样式库
const FORMAT_STYLES = [
{
name: '主标题',
icon: 'fa-header',
tag: 'h1',
category: '标题',
styles: {
'color': '#1e40af',
'font-size': '24px',
'font-weight': 'bold',
'margin': '20px 0 15px 0',
'padding-bottom': '8px',
'border-bottom': '2px solid #dbeafe'
},
preview: true,
apply: (selectedText) => {
const content = selectedText || '主标题示例';
return `${content}
`;
}
},
{
name: '副标题',
icon: 'fa-header',
tag: 'h2',
category: '标题',
styles: {
'color': '#2563eb',
'font-size': '20px',
'font-weight': 'bold',
'margin': '18px 0 12px 0',
'padding-bottom': '5px',
'border-bottom': '1px solid #dbeafe'
},
preview: true,
apply: (selectedText) => {
const content = selectedText || '副标题示例';
return `${content}
`;
}
},
{
name: '三级标题',
icon: 'fa-header',
tag: 'h3',
category: '标题',
styles: {
'color': '#3b82f6',
'font-size': '18px',
'font-weight': 'bold',
'margin': '15px 0 10px 0'
},
preview: true,
apply: (selectedText) => {
const content = selectedText || '三级标题示例';
return `${content}
`;
}
},
{
name: '正文段落',
icon: 'fa-paragraph',
tag: 'p',
category: '文本',
styles: {
'color': '#333',
'font-size': '14px',
'line-height': '1.8',
'margin': '8px 0',
'text-indent': '2em'
},
preview: true,
apply: (selectedText) => {
const content = selectedText || '这是一段正文示例,包含标准的段落格式和首行缩进,适合用于大部分内容的展示。';
return `${content}
`;
}
},
{
name: '引用文本',
icon: 'fa-quote-right',
tag: 'blockquote',
category: '文本',
styles: {
'color': '#666',
'font-size': '13px',
'line-height': '1.6',
'margin': '10px 0',
'padding': '10px 15px',
'border-left': '3px solid #2196F3',
'background': '#f8f9fa'
},
preview: true,
apply: (selectedText) => {
const content = selectedText || '这是一段引用文本示例,通常用于引用他人的话语或特殊说明内容。';
return `${content}
`;
}
},
{
name: '无序列表',
icon: 'fa-list-ul',
tag: 'ul',
category: '列表',
styles: {
'margin': '10px 0 10px 20px',
'padding': '0'
},
itemStyles: {
'color': '#444',
'font-size': '14px',
'line-height': '1.7',
'margin': '5px 0',
'list-style-type': 'disc'
},
preview: true,
apply: (selectedText) => {
const content = selectedText || '列表项1\n列表项2\n列表项3';
const items = content.split('\n').map(item =>
`${item.trim()}`
).join('');
return ``;
}
},
{
name: '有序列表',
icon: 'fa-list-ol',
tag: 'ol',
category: '列表',
styles: {
'margin': '10px 0 10px 20px',
'padding': '0'
},
itemStyles: {
'color': '#444',
'font-size': '14px',
'line-height': '1.7',
'margin': '5px 0'
},
preview: true,
apply: (selectedText) => {
const content = selectedText || '步骤一\n步骤二\n步骤三';
const items = content.split('\n').map(item =>
`${item.trim()}`
).join('');
return `${items}
`;
}
},
{
name: '分隔线',
icon: 'fa-minus',
tag: 'hr',
category: '布局',
styles: {
'border': 'none',
'border-top': '1px solid #e0e0e0',
'margin': '20px 0',
'height': '1px'
},
preview: true,
apply: () => {
return `
`;
}
},
{
name: '高亮文本',
icon: 'fa-highlighter',
tag: 'span',
category: '文本',
styles: {
'background': '#fff380',
'padding': '0 3px',
'border-radius': '2px'
},
preview: true,
apply: (selectedText) => {
const content = selectedText || '需要高亮的文本';
return `${content}`;
}
},
{
name: '链接样式',
icon: 'fa-link',
tag: 'a',
category: '文本',
styles: {
'color': '#2563eb',
'text-decoration': 'none',
'border-bottom': '1px dashed #93c5fd',
'padding': '0 1px'
},
preview: true,
apply: (selectedText) => {
const content = selectedText || '链接文本';
return `${content}`;
}
},
{
name: '居中文本',
icon: 'fa-align-center',
tag: 'div',
category: '布局',
styles: {
'text-align': 'center',
'margin': '10px 0',
'color': '#4b5563'
},
preview: true,
apply: (selectedText) => {
const content = selectedText || '这段文本会居中显示';
return `${content}
`;
}
},
{
name: '影视卡片',
icon: 'fa-film',
tag: 'div',
category: '特殊',
preview: true,
apply: (selectedText) => {
const title = selectedText || '影视名称';
return `
${title}
这里可以添加影视的简要说明或推荐理由...
`;
}
},
{
name: '装饰标题',
icon: 'fa-magic',
tag: 'h3',
category: '标题',
preview: true,
apply: (selectedText) => {
const content = selectedText || '装饰标题示例';
return `
${content}
`;
}
},
{
name: '序号标题',
icon: 'fa-list-ol',
tag: 'h3',
category: '标题',
preview: true,
apply: (selectedText) => {
const content = selectedText || '01 序号标题示例';
return `${content}
`;
}
},
{
name: '信息提示',
icon: 'fa-info-circle',
tag: 'div',
category: '文本',
preview: true,
apply: (selectedText) => {
const content = selectedText || '这是信息提示内容';
return `${content}
`;
}
},
{
name: '成功提示',
icon: 'fa-check-circle',
tag: 'div',
category: '文本',
preview: true,
apply: (selectedText) => {
const content = selectedText || '操作已成功完成';
return `${content}
`;
}
},
{
name: '警告提示',
icon: 'fa-exclamation-triangle',
tag: 'div',
category: '文本',
preview: true,
apply: (selectedText) => {
const content = selectedText || '请注意可能的风险';
return `${content}
`;
}
},
{
name: '错误提示',
icon: 'fa-times-circle',
tag: 'div',
category: '文本',
preview: true,
apply: (selectedText) => {
const content = selectedText || '发生错误,请重试';
return `${content}
`;
}
},
{
name: '代码块',
icon: 'fa-code',
tag: 'pre',
category: '文本',
preview: true,
apply: (selectedText) => {
const content = (selectedText || 'const hello = "world";').replace(//g,'>');
return `${content}
`;
}
},
{
name: '任务清单',
icon: 'fa-tasks',
tag: 'ul',
category: '列表',
preview: true,
apply: (selectedText) => {
const content = selectedText || '[ ] 待办一\n[x] 已完成项\n[ ] 待办二';
const items = content.split('\n').map(line => {
const done = /\[x\]/i.test(line);
const text = line.replace(/\[[ xX]\]\s*/,'');
return `
${text}
`;
}).join('');
return ``;
}
},
{
name: '标签云',
icon: 'fa-tags',
tag: 'div',
category: '列表',
preview: true,
apply: (selectedText) => {
const content = selectedText || '剧情|动作|冒险|爱情|科幻|动画';
const chips = content.split(/\||,|\s+/).filter(Boolean).slice(0,12).map(t => `${t}`).join('');
return `${chips}
`;
}
},
{
name: '两列栅格',
icon: 'fa-columns',
tag: 'div',
category: '布局',
preview: true,
apply: (selectedText) => {
const a = '左侧内容';
const b = '右侧内容';
return ``;
}
},
{
name: '三列栅格',
icon: 'fa-th',
tag: 'div',
category: '布局',
preview: true,
apply: () => {
return ``;
}
},
{
name: '图片说明卡',
icon: 'fa-image',
tag: 'div',
category: '布局',
preview: true,
apply: () => {
return `
这里是配图说明文字,可替换为剧情简介、片段描述或使用说明。
`;
}
},
{
name: '强调按钮',
icon: 'fa-hand-pointer-o',
tag: 'a',
category: '特殊',
preview: true,
apply: (selectedText) => {
const content = selectedText || '点此查看';
return `${content}`;
}
},
{
name: '下载按钮',
icon: 'fa-download',
tag: 'a',
category: '特殊',
preview: true,
apply: (selectedText) => {
const content = selectedText || '下载资源';
return `${content}`;
}
},
{
name: '流程步骤',
icon: 'fa-arrow-right',
tag: 'ol',
category: '列表',
preview: true,
apply: (selectedText) => {
const content = selectedText || '准备素材\n编辑排版\n发布分享';
const items = content.split('\n').map((t, i) => `
${i+1}${t}
`).join('');
return `${items}
`;
}
},
{
name: '折叠面板',
icon: 'fa-chevron-down',
tag: 'details',
category: '布局',
preview: true,
apply: (selectedText) => {
const content = selectedText || '这里是可展开的详细说明内容';
return `
点击展开/收起
${content}
`;
}
},
{
name: '两列表格',
icon: 'fa-table',
tag: 'table',
category: '布局',
preview: true,
apply: () => {
return ``;
}
},
{
name: '引用卡片',
icon: 'fa-quote-left',
tag: 'div',
category: '文本',
preview: true,
apply: (selectedText) => {
const content = selectedText || '这是一段加框的引用卡片内容,用于强调引用。';
return `${content}
`;
}
},
{
name: '下沉首字',
icon: 'fa-bold',
tag: 'p',
category: '文本',
preview: true,
apply: (selectedText) => {
const content = (selectedText || '首字下沉示例段落,用于提升视觉层次与阅读趣味。');
const first = content.slice(0,1);
const rest = content.slice(1);
return `
${first}${rest}
`;
}
},
{
name: '图片区块',
icon: 'fa-picture-o',
tag: 'div',
category: '布局',
preview: true,
apply: () => {
return `
${[1,2,3,4].map(i=>`

`).join('')}
`;
}
},
{
name: '免责声明',
icon: 'fa-shield',
tag: 'div',
category: '特殊',
preview: true,
apply: () => {
return `
仅供学习与交流,禁止用于商业用途,版权归原平台与权利人所有。
`;
}
},
{
name: '按钮组',
icon: 'fa-bars',
tag: 'div',
category: '特殊',
preview: true,
apply: () => {
return ``;
}
},
{
name: '彩色分隔条',
icon: 'fa-minus',
tag: 'hr',
category: '布局',
preview: true,
apply: () => ``
}
];
// 自动填充和保存相关函数
function autoClickSourceBtn() {
return new Promise((resolve) => {
const modalSourceBtn = document.querySelector('#myModal-code .btn, #source-code-btn');
if (modalSourceBtn && modalSourceBtn.textContent.includes('源代码')) {
modalSourceBtn.click();
setTimeout(() => resolve(true), 600);
return;
}
const textButtons = [...document.querySelectorAll('button'), ...document.querySelectorAll('a')]
.filter(elem => elem.textContent.trim().includes('源代码'));
if (textButtons.length > 0) {
textButtons[0].click();
setTimeout(() => resolve(true), 300);
return;
}
const tinyMceBtn = document.querySelector('.tox-tbtn[title="源代码"]');
const oldTinyMceBtn = Array.from(document.querySelectorAll('.mce-btn')).find(elem => elem.textContent.includes('源代码'));
const ckSourceLabel = document.querySelector('.cke_button__source_label');
if (tinyMceBtn) {
tinyMceBtn.click();
setTimeout(() => resolve(true), 300);
} else if (oldTinyMceBtn) {
oldTinyMceBtn.click();
setTimeout(() => resolve(true), 300);
} else if (ckSourceLabel && ckSourceLabel.closest('.cke_button')) {
ckSourceLabel.closest('.cke_button').click();
setTimeout(() => resolve(true), 300);
} else {
resolve(true);
}
});
}
// 关闭TinyMCE/CKEditor等源代码对话框或退出源码模式
function closeSourceDialogIfAny() {
try {
// TinyMCE 源代码对话框常见关闭按钮
const closeBtns = [
'.tox-dialog__footer .tox-button',
'.tox-dialog__close',
'.modal .close',
'.dialog .close',
'[aria-label="Close"]'
];
for (const sel of closeBtns) {
const el = document.querySelector(sel);
if (el) { el.click(); break; }
}
} catch (_) {}
try {
// 如果仍在源码模式,尝试再次点击按钮退出
const btn = document.querySelector('.tox-tbtn[title="源代码"], #source-code-btn');
if (btn) btn.click();
} catch (_) {}
}
function autoFillSourceBox(html) {
return new Promise((resolve) => {
// 内容非空:确保HTML内容不为空
const safeHtml = html || `内容已自动生成
`;
let retryCount = 0;
const maxRetry = 20;
const interval = 300;
const tryFill = setInterval(() => {
retryCount++;
const editorSelectors = [
'#myModal-code textarea',
'textarea.tox-textarea',
'textarea.mce-textbox',
'textarea.cke_source',
'textarea[name="message"]',
'textarea[name="content"]',
'#editor_content',
'#content',
'textarea[rows="20"][cols="80"]',
'.CodeMirror textarea',
'.editor-textarea',
// 扩展选择器范围,增强兼容性
'.content-editor textarea',
'.article-editor textarea',
'div[contenteditable="true"]',
'[role="textbox"]'
];
let targetBox = null;
for (const selector of editorSelectors) {
const elem = document.querySelector(selector);
if (elem && elem.style.display !== 'none' && elem.offsetParent !== null) {
targetBox = elem;
sourceCodeElement = elem;
currentEditor = getCurrentEditor();
break;
}
}
if (targetBox) {
const codeMirror = targetBox.closest('.CodeMirror');
if (codeMirror) {
// 处理CodeMirror编辑器的多种情况
if (codeMirror.CodeMirror) {
// 标准CodeMirror实例
try {
codeMirror.CodeMirror.setValue(safeHtml);
codeMirror.CodeMirror.getDoc().markClean();
codeMirror.CodeMirror.getDoc().changed();
// 触发额外的事件来模拟真实用户输入
codeMirror.CodeMirror.focus();
codeMirror.CodeMirror.refresh();
// 尝试通过编辑器的内部API触发变化通知,但避免触发验证
if (codeMirror.CodeMirror.on) {
try {
codeMirror.CodeMirror.on('change', codeMirror.CodeMirror.getDoc());
} catch (e) {
// 静默忽略可能的错误
}
}
} catch (cmError) {
console.log('CodeMirror编辑器操作失败:', cmError);
// 降级处理:直接操作textarea
targetBox.value = safeHtml;
triggerNonBubblingEvents(targetBox);
}
} else {
// 直接操作textarea(更贴近用户输入行为)
targetBox.value = safeHtml;
triggerNonBubblingEvents(targetBox);
}
} else {
// 普通文本框处理或contenteditable元素
if (targetBox.isContentEditable) {
// 处理contenteditable元素
targetBox.innerHTML = safeHtml;
triggerNonBubblingEvents(targetBox);
} else {
// 普通文本框处理
targetBox.value = safeHtml;
triggerNonBubblingEvents(targetBox);
}
}
// 增加短暂延迟确保所有事件都被处理
setTimeout(() => {
clearInterval(tryFill);
resolve(true);
}, 500); // 延长延迟时间,确保更可靠的填充
return;
}
if (retryCount >= maxRetry) {
clearInterval(tryFill);
// 粘贴失败时自动复制内容到剪贴板
try {
navigator.clipboard.writeText(safeHtml).then(() => {
showStatus('内容自动复制到剪贴板,请手动粘贴!', true);
}).catch(err => {
showStatus('内容粘贴失败,剪贴板复制也失败,请手动复制内容!', true);
console.error('剪贴板写入失败:', err);
});
} catch (e) {
showStatus('内容粘贴失败,请手动复制内容!', true);
console.error('自动复制功能失败:', e);
}
resolve(false);
}
}, interval);
});
}
// 触发阻断:创建不冒泡的事件并触发,防止触发表单验证
function triggerNonBubblingEvents(element) {
if (!element) return;
// 创建不冒泡的事件
const createNonBubblingEvent = (type, eventType = 'Event') => {
let event;
if (eventType === 'KeyboardEvent') {
event = new KeyboardEvent(type, {
bubbles: false,
cancelable: true,
key: 'Enter'
});
} else {
event = new Event(type, {
bubbles: false,
cancelable: true
});
}
// 定义空的stopPropagation方法,确保事件不会冒泡
Object.defineProperty(event, 'stopPropagation', {
value: function() {},
writable: false
});
return event;
};
try {
// 触发基础事件
element.dispatchEvent(createNonBubblingEvent('focus'));
element.dispatchEvent(createNonBubblingEvent('input'));
element.dispatchEvent(createNonBubblingEvent('change'));
element.dispatchEvent(createNonBubblingEvent('compositionend'));
// 触发键盘事件
element.dispatchEvent(createNonBubblingEvent('keydown', 'KeyboardEvent'));
element.dispatchEvent(createNonBubblingEvent('keypress', 'KeyboardEvent'));
element.dispatchEvent(createNonBubblingEvent('keyup', 'KeyboardEvent'));
// 短暂聚焦然后失焦,完成编辑过程
element.focus();
if (element.setSelectionRange && typeof element.value === 'string') {
element.setSelectionRange(element.value.length, element.value.length);
}
setTimeout(() => {
element.dispatchEvent(createNonBubblingEvent('blur'));
}, 100);
} catch (eventError) {
console.log('触发事件时发生错误:', eventError);
}
}
function autoClickSaveBtn() {
return new Promise((resolve) => {
const saveButtons = [
...document.querySelectorAll('button'),
...document.querySelectorAll('a')
].filter(elem => {
const text = elem.textContent.trim();
return text === '保存' || text === '保存草稿' || text === '保存内容';
});
if (saveButtons.length > 0) {
saveButtons[0].click();
setTimeout(() => resolve(true), 500);
return;
}
const commonSaveBtn = document.querySelector('button.save, .save-button, [type="submit"][value="保存"]');
if (commonSaveBtn && !commonSaveBtn.textContent.includes('发布')) {
commonSaveBtn.click();
setTimeout(() => resolve(true), 500);
return;
}
resolve(false);
});
}
// 统一写入助手:尽最大努力把HTML写入任一编辑器(TinyMCE/CodeMirror/textarea/iframe/contenteditable)
async function writeHtmlToAnyEditor(html) {
try {
const safeHtml = html || '';
// 1) TinyMCE 优先
try {
const tiny = window.tinymce || window.tinyMCE;
let ed = null;
if (tiny) {
ed = tiny.activeEditor || (tiny.editors && tiny.editors[0]) || (tiny.EditorManager && tiny.EditorManager.activeEditor) || null;
if (!ed && typeof tiny.get === 'function') {
// 尝试根据textarea id获取
const ta = document.querySelector('textarea[id]');
if (ta) { ed = tiny.get(ta.id) || ed; }
}
}
if (ed && typeof ed.setContent === 'function') {
ed.setContent(safeHtml);
try { if (typeof ed.setDirty === 'function') ed.setDirty(true); } catch(_) {}
try { if (tiny && typeof tiny.triggerSave === 'function') tiny.triggerSave(); } catch(_) {}
return true;
}
} catch (_) {}
// 2) 使用现有的getCurrentEditor工具(CodeMirror/textarea)
try {
const editor = getCurrentEditor && getCurrentEditor();
if (editor) {
if (editor.type === 'codemirror' && editor.instance && typeof editor.instance.setValue === 'function') {
editor.instance.setValue(safeHtml);
return true;
} else if (editor.instance) {
editor.instance.value = safeHtml;
editor.instance.dispatchEvent(new Event('input', { bubbles: true }));
return true;
}
}
} catch(_) {}
// 3) TinyMCE iframe 兜底
try {
const iframe = document.querySelector('.tox-edit-area iframe, iframe.tox-edit-area__iframe, .mce-edit-area iframe');
if (iframe && iframe.contentDocument && iframe.contentDocument.body) {
iframe.contentDocument.body.innerHTML = safeHtml;
iframe.contentDocument.body.dispatchEvent(new Event('input', { bubbles: true }));
return true;
}
} catch(_) {}
// 4) contenteditable 兜底
try {
const editable = document.querySelector('[contenteditable="true"], [contenteditable=true]');
if (editable) { editable.innerHTML = safeHtml; return true; }
} catch(_) {}
// 5) 直接向可见textarea写入
try {
const ta = Array.from(document.querySelectorAll('textarea')).find(t => t.offsetParent !== null);
if (ta) { ta.value = safeHtml; ta.dispatchEvent(new Event('input', { bubbles: true })); return true; }
} catch(_) {}
return false;
} catch (e) { return false; }
}
function autoFillTitleInput(title) {
return new Promise((resolve) => {
const titleInput = document.querySelector('input[placeholder="标题"], input[name="title"], #title');
if (!titleInput) {
resolve(false);
return;
}
titleInput.value = title || (currentMovieInfo?.title || '影视内容分享');
titleInput.dispatchEvent(new Event('input', { bubbles: true }));
titleInput.dispatchEvent(new Event('change', { bubbles: true }));
const keydownEvent = new KeyboardEvent('keydown', { key: 'Enter', bubbles: true });
const keyupEvent = new KeyboardEvent('keyup', { key: 'Enter', bubbles: true });
titleInput.dispatchEvent(keydownEvent);
titleInput.dispatchEvent(keyupEvent);
resolve(true);
});
}
async function fillAndSaveSource(html, enableAutoSave = false) {
try {
// 边界隔离:此函数主要负责内容填充,根据enableAutoSave参数决定是否执行保存操作
// 内容非空:确保标题不为空,即使没有影视信息也提供默认值
const movieTitle = currentMovieInfo?.title || '影视内容分享';
await autoFillTitleInput(movieTitle);
showStatus('正在切换到源代码模式...', false);
const switched = await autoClickSourceBtn();
if (!switched) {
showStatus('未检测到源代码按钮,尝试直接填充...', false);
}
showStatus('正在填充内容到编辑框...', false);
// 内容非空:确保HTML内容不为空
const safeHtml = html || `内容已自动生成
`;
const filled = await autoFillSourceBox(safeHtml);
if (filled) {
// 轻量延迟,确保内容写入完成
await new Promise(resolve => setTimeout(resolve, 120));
// 内容非空:检查并填充所有可能的必填字段,扩展选择器范围
const allInputSelectors = [
'input[required]:not([type="hidden"])',
'textarea[required]:not([type="hidden"])',
'input[placeholder*="必填"], textarea[placeholder*="必填"]',
'input[name="content"], textarea[name="content"]',
'input[name="text"], textarea[name="text"]',
'input[data-required="true"], textarea[data-required="true"]',
'[ng-required="true"]',
'.required-field',
'.required-input',
'#content-input, #text-input',
'.editor-container textarea',
'.main-content textarea'
];
// 合并所有选择器
const allInputs = document.querySelectorAll(allInputSelectors.join(','));
// 内容非空:预填充所有可能的输入框
allInputs.forEach(input => {
// 检查元素是否可见且无内容
if (!input.value.trim() && input.style.display !== 'none' && input.offsetParent !== null) {
// 为不同类型的输入框提供合适的默认值
if (input.type === 'text' || input.type === 'textarea' || input.tagName.toLowerCase() === 'textarea') {
// 为文本输入框填充更详细的默认内容
const placeholder = input.getAttribute('placeholder') || '内容已自动生成';
input.value = placeholder;
// 触发阻断:阻止事件冒泡,防止触发表单验证
const createBubblesBlockedEvent = (type) => {
const event = new Event(type, { bubbles: false, cancelable: true });
Object.defineProperty(event, 'stopPropagation', {
value: function() {}
});
return event;
};
// 触发更多事件以模拟真实用户输入,但阻止冒泡
const events = [
createBubblesBlockedEvent('focus'),
createBubblesBlockedEvent('input'),
createBubblesBlockedEvent('change'),
createBubblesBlockedEvent('blur')
];
events.forEach(event => {
input.dispatchEvent(event);
});
// 特殊事件:键盘事件和组合事件,同样阻止冒泡
const createKeyboardEventNoBubbles = (type, key) => {
const event = new KeyboardEvent(type, { key, bubbles: false });
Object.defineProperty(event, 'stopPropagation', {
value: function() {}
});
return event;
};
const keyboardEvents = [
createKeyboardEventNoBubbles('keydown', 'a'),
createKeyboardEventNoBubbles('keypress', 'a'),
createKeyboardEventNoBubbles('keyup', 'a')
];
keyboardEvents.forEach(event => {
input.dispatchEvent(event);
});
}
}
});
// 额外检查并设置编辑器内容的标记,防止验证时认为内容为空
if (window.editor && typeof window.editor.setContent === 'function') {
try {
window.editor.setContent(safeHtml);
// 强制设置编辑器内部状态为已编辑
if (window.editor && typeof window.editor.isDirty === 'function') {
window.editor.isDirty = () => true;
}
} catch (editorError) {
console.log('设置编辑器内容失败:', editorError);
}
}
// 快速保存:先尝试直接保存,失败再回退补齐必填项后再保存
if (enableAutoSave) {
let saved = await autoClickSaveBtn();
if (!saved) {
const selectors = [
'input[required]:not([type="hidden"])',
'textarea[required]:not([type="hidden"])',
'input[placeholder*="必填"], textarea[placeholder*="必填"]',
'input[name="content"], textarea[name="content"]',
'input[name="text"], textarea[name="text"]',
'input[data-required="true"], textarea[data-required="true"]',
'[ng-required="true"]',
'.required-field',
'.required-input',
'#content-input, #text-input',
'.editor-container textarea',
'.main-content textarea'
];
document.querySelectorAll(selectors.join(',')).forEach(input => {
if (!input.value || !String(input.value).trim()) {
const placeholder = input.getAttribute('placeholder') || '内容已自动生成';
input.value = placeholder;
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
}
});
await new Promise(r => setTimeout(r, 120));
saved = await autoClickSaveBtn();
}
showStatus(saved ? '内容填充完成并已自动保存' : '内容填充完成,请手动保存', !saved);
} else {
showStatus('内容填充完成', false);
}
return true;
} else {
// 异常兜底:复制到剪贴板并显示手动粘贴按钮
try {
GM_setClipboard(safeHtml);
} catch (clipboardError) {
console.log('复制到剪贴板失败:', clipboardError);
}
showStatus('自动填充失败,内容已复制到剪贴板,请手动粘贴', true);
const pasteBtn = document.getElementById('paste-btn');
if (pasteBtn) pasteBtn.style.display = 'inline-block';
return false;
}
} catch (error) {
console.error('填充过程中发生错误:', error);
// 异常兜底:尝试从localStorage恢复备份
try {
const backupHtml = localStorage.getItem('backup-movie-html');
if (backupHtml) {
GM_setClipboard(backupHtml);
showStatus('处理过程中出现错误,已将备份内容复制到剪贴板,请手动粘贴', true);
const pasteBtn = document.getElementById('paste-btn');
if (pasteBtn) pasteBtn.style.display = 'inline-block';
} else {
showStatus('处理过程中出现错误,请刷新页面重试', true);
}
} catch (restoreError) {
showStatus('处理过程中出现错误,请刷新页面重试', true);
}
return false;
}
}
// 内容生成函数
function generateHTML(movie, comments, posterDataURL, stillDataURL) {
// 强制将TMDB缩略图升级为original,避免排版放大模糊
const finalPosterUrl = posterDataURL ? toTMDBOriginal(posterDataURL) : 'https://picsum.photos/680/480?default-poster';
const finalStillUrl = stillDataURL ? toTMDBOriginal(stillDataURL) : 'https://picsum.photos/800/450?default-still';
const runtime = movie.runtime === 'null' || !movie.runtime ? '未知片长' : movie.runtime;
let imdbHtml = '';
if (movie.imdbId && movie.imdbId !== '暂无') {
imdbHtml = ` IMDb: ${movie.imdbId}`;
}
const introHtml = movie.intro
.split('\n')
.filter(para => para.trim())
.map(para => `${para.trim()}
`)
.join('');
let commentsHtml = '';
// 统一为“观众热评”,并与模板1一致:TMDB无热评时回退为简介/标语摘录
const isTmdbSrc = (movie && (movie.source === 'TMDB' || (movie.url && movie.url.includes('themoviedb.org')) || movie.tmdbId));
const firstRaw = Array.isArray(comments) && comments.length > 0 ? comments[0] : null;
let firstText = '';
if (firstRaw) {
if (typeof firstRaw === 'string') {
firstText = firstRaw;
} else if (typeof firstRaw === 'object') {
firstText = (
firstRaw.content ||
firstRaw.text ||
firstRaw.comment ||
firstRaw.quote ||
''
).toString();
}
}
let commentInnerHtml = '';
if (firstText && firstText.trim()) {
commentInnerHtml = firstText.trim();
} else if (isTmdbSrc) {
let snippet = (movie.intro || movie.tagline || '').toString().trim();
if (snippet.length > 80) snippet = snippet.slice(0, 78) + '…';
const rvLink = movie.tmdbId ? `https://www.themoviedb.org/${movie.mediaType || 'movie'}/${movie.tmdbId}/reviews` : '';
const moreHtml = rvLink ? ` 更多评价` : '';
commentInnerHtml = snippet
? `${snippet}
—— 看点摘录${moreHtml}
`
: '暂无热评,分享你的观影感受吧~';
} else {
commentInnerHtml = '暂无热评,分享你的观影感受吧~';
}
commentsHtml = `
观众热评:
${commentInnerHtml}
`;
// 处理额外信息(新添加的字段)
let additionalInfo = '';
let hasAdditionalInfo = false;
const additionalInfoItems = [];
// 原始标题
if (movie.originalTitle && movie.originalTitle !== movie.title) {
additionalInfoItems.push(`原始名称:${movie.originalTitle}
`);
hasAdditionalInfo = true;
}
// 奖项信息
if (movie.awards && movie.awards.length > 0) {
additionalInfoItems.push(`主要奖项:${movie.awards.slice(0, 3).join(';')}
`);
hasAdditionalInfo = true;
}
// 关键字
if (movie.keywords && movie.keywords !== '') {
additionalInfoItems.push(`关键字:${movie.keywords}
`);
hasAdditionalInfo = true;
}
// 预算和票房(电影)
if (movie.budget && movie.budget !== '未知') {
additionalInfoItems.push(`预算:${movie.budget}
`);
hasAdditionalInfo = true;
}
if (movie.revenue && movie.revenue !== '未知') {
additionalInfoItems.push(`票房:${movie.revenue}
`);
hasAdditionalInfo = true;
}
// 流媒体平台信息
if (movie.streamingPlatforms && movie.streamingPlatforms.length > 0) {
additionalInfoItems.push(`流媒体平台:${movie.streamingPlatforms.join('、')}
`);
hasAdditionalInfo = true;
}
// 如果有额外信息,构建HTML
if (hasAdditionalInfo) {
additionalInfo = `
${additionalInfoItems.join('\n')}
`;
}
return `
名称:${movie.title}
又名:${movie.alsoKnown || '无'}
导演:${movie.director}
编剧:${movie.writer}
主演:${movie.actor}
类型:${movie.genreTags.join('、') || '未知'}
制片地区:${movie.region}
上映时间:${movie.release}
影视语言:${movie.lang}
评分: ${movie.rating} 豆瓣ID: ${movie.doubanId || movie.tmdbId}${imdbHtml}
片长:${runtime}
${additionalInfo}
影视简介:
${introHtml}
精彩剧照:
${commentsHtml}
`;
}
// 创建AI功能面板内容(不创建独立面板,只返回HTML内容)
function createAIPanelContent() {
return `
`;
}
// 切换标签页功能
function switchTab(tabId) {
// 隐藏所有内容区域
document.getElementById('main-content-area').style.display = 'none';
document.getElementById('ai-content-area').style.display = 'none';
document.getElementById('settings-content-area').style.display = 'none';
// 获取标签元素
const mainTab = document.getElementById('main-tab');
const aiTab = document.getElementById('ai-tab');
const settingsTab = document.getElementById('settings-tab');
// 重置所有标签样式
[mainTab, aiTab, settingsTab].forEach(tab => {
// 完全重置所有内联样式
tab.style.background = '#fff';
tab.style.color = '#6b7280';
tab.style.fontWeight = 'normal';
tab.style.boxShadow = 'none';
tab.style.borderBottom = '1px solid #f3d5d9';
tab.style.backgroundImage = 'none';
});
// 显示选中的内容区域并激活对应的标签
if (tabId === 'main') {
document.getElementById('main-content-area').style.display = 'block';
mainTab.style.background = 'linear-gradient(135deg, #ec4899 0%, #be185d 100%)';
mainTab.style.color = 'white';
mainTab.style.fontWeight = '500';
mainTab.style.boxShadow = '0 -2px 6px rgba(236, 72, 153, 0.3)';
mainTab.style.borderBottom = 'none';
showStatus('控制面板已准备就绪', false, 'main');
// 进入主功能:隔离AI
isMainFlowActive = true;
} else if (tabId === 'ai') {
document.getElementById('ai-content-area').style.display = 'block';
aiTab.style.background = 'linear-gradient(135deg, #8b5cf6 0%, #6d28d9 100%)';
aiTab.style.color = 'white';
aiTab.style.fontWeight = '500';
aiTab.style.boxShadow = '0 -2px 6px rgba(139, 92, 246, 0.3)';
aiTab.style.borderBottom = 'none';
// 进入AI:解除隔离
isMainFlowActive = false;
// 重新绑定AI事件监听器,确保按钮可以正常响应
bindAIEventListeners();
} else if (tabId === 'settings') {
document.getElementById('settings-content-area').style.display = 'block';
settingsTab.style.background = 'linear-gradient(135deg, #6366f1 0%, #4f46e5 100%)';
settingsTab.style.color = 'white'; // 确保文字颜色为白色
settingsTab.style.fontWeight = '500';
settingsTab.style.boxShadow = '0 -2px 6px rgba(99, 102, 241, 0.3)';
settingsTab.style.borderBottom = 'none';
showStatus('设置面板已准备就绪', false, 'settings');
// 重新绑定设置面板事件监听器,确保按钮可以正常响应
bindSettingsEventListeners();
}
}
// 确保switchTab函数在全局作用域可访问 - 使用更可靠的方式暴露给外部HTML调用
if (typeof unsafeWindow !== 'undefined') {
// 在Tampermonkey等用户脚本管理器中使用unsafeWindow
unsafeWindow.switchTab = switchTab;
} else {
// 标准浏览器环境下使用window
window.switchTab = switchTab;
}
// 额外添加到document对象作为后备方案
document.switchTab = switchTab;
// 控制面板创建与定位 - 精准放置到标记位置(空值修复版+美化版)
function createPanel() {
panel = document.createElement('div');
panel.id = 'douban-tmdb-panel';
panel.style.cssText = `
background: #fff; border: 1px solid #f3d5d9; border-radius: 8px;
padding: 15px; margin: 20px 0; box-shadow: 0 2px 8px rgba(236, 72, 153, 0.08);
z-index: 0; position: static;
box-sizing: border-box;
width: 100%; min-width: 320px; max-width: 1400px; /* 扩大宽屏最大宽度,优化搜索结果显示 */
transition: margin-bottom 0.15s cubic-bezier(0.4, 0, 0.2, 1);
overflow: visible;
transform: none !important;
transform-origin: unset !important;
`;
// 添加响应式CSS规则(精简与合并断点,移除极端断点)
const responsiveCSS = `
`;
panel.innerHTML = responsiveCSS + panel.innerHTML;
panel.innerHTML = `
⚙️ 脚本设置
🔑 TMDB API 配置
获取方法:
1. 登录/注册TMDB账号:访问 TMDB官网 ,登录或注册账号。
2. 进入API设置:点击右上角头像→Settings→左侧API选项。
3. 获取v3 API Key:在"API Keys (v3 auth)"区域,创建并复制Key。
4. 获取v4 Access Token:在"Access Tokens (v4 auth)"区域,生成并复制Token。
注意:遵守TMDB服务条款,界面可能微调。
`;
return panel;
}
// 插入面板到标题输入框下方(精准定位+空值修复)
function insertPanelInMarkedPosition() {
if (isPanelInitialized) return;
panel = createPanel();
let targetContainer = document.body; // 默认值
// 优先尝试在标题输入框下方插入
const subjectInput = document.getElementById('subject');
if (subjectInput && subjectInput.offsetParent !== null) {
// 移除旧面板
const oldPanel = document.getElementById('douban-tmdb-panel');
const oldAIPanel = document.getElementById('ai-text-generation-panel');
if (oldPanel) oldPanel.remove();
if (oldAIPanel) oldAIPanel.remove();
// 将新面板插入到标题输入框之后
if (subjectInput.nextSibling) {
subjectInput.parentNode.insertBefore(panel, subjectInput.nextSibling);
} else {
subjectInput.parentNode.appendChild(panel);
}
// 设置目标容器为标题输入框的父元素
targetContainer = subjectInput.parentNode;
} else {
// 如果标题输入框不存在,使用原有的回退逻辑
const targetContainers = [
document.querySelector('.main-content'),
(document.querySelector('#thread-create-form') || {}).parentElement,
(document.querySelector('.post-form') || {}).parentElement,
document.querySelector('.panel-default'),
document.body
];
for (const container of targetContainers) {
if (container && container.offsetParent !== null) {
targetContainer = container;
break;
}
}
if (!targetContainer) {
targetContainer = document.body;
console.warn('无法找到目标容器,已 fallback 到 document.body');
}
// 移除旧面板
const oldPanel = targetContainer.querySelector('#douban-tmdb-panel');
const oldAIPanel = targetContainer.querySelector('#ai-text-generation-panel');
if (oldPanel) targetContainer.removeChild(oldPanel);
if (oldAIPanel) targetContainer.removeChild(oldAIPanel);
// 插入新面板
targetContainer.appendChild(panel);
}
posterContainer = document.getElementById('poster-candidates');
stillContainer = document.getElementById('still-candidates');
initFormatTools();
bindEventListeners();
bindAIEventListeners(); // 绑定AI相关事件监听器
setupMutationObserver(targetContainer);
isPanelInitialized = true;
showStatus('控制面板已放置在可用位置', false);
return true;
}
insertPanelInMarkedPosition();
// 监听父容器变化,确保面板位置不移动
function setupMutationObserver(parent) {
if (panelObserver) {
panelObserver.disconnect();
}
panelObserver = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
const panel = document.getElementById('douban-tmdb-panel');
if (!panel || !parent.contains(panel)) {
parent.appendChild(panel || createPanel());
posterContainer = document.getElementById('poster-candidates');
stillContainer = document.getElementById('still-candidates');
initFormatTools();
bindEventListeners();
bindAIEventListeners(); // 重新绑定AI相关事件监听器
}
});
});
panelObserver.observe(parent, {
childList: true,
attributes: true,
subtree: true,
characterData: true
});
}
// 确保switchTab函数在全局作用域可访问
window.switchTab = switchTab;
// 工具函数 - 支持向不同面板显示状态消息
function showStatus(text, isError = false, target = 'main') {
// 检查当前激活的标签页
let activeTab = 'main';
if (document.getElementById('main-tab').style.background.includes('linear-gradient')) {
activeTab = 'main';
} else if (document.getElementById('ai-tab').style.background.includes('linear-gradient')) {
activeTab = 'ai';
} else if (document.getElementById('settings-tab') && document.getElementById('settings-tab').style.background.includes('linear-gradient')) {
activeTab = 'settings';
}
// 如果指定了目标,则使用指定的目标;否则使用当前激活的标签页
const targetTab = target === 'all' ? activeTab : target;
// 获取对应的状态元素
let statusElement = null;
if (targetTab === 'main') {
statusElement = document.querySelector('#main-content-area #status');
} else if (targetTab === 'ai') {
statusElement = document.querySelector('#ai-content-area #status');
} else if (targetTab === 'settings') {
statusElement = document.querySelector('#settings-content-area #settings-status');
}
if (!statusElement) return;
// 设置状态消息(避免强制重排:先写样式,再写内容)
statusElement.style.willChange = 'opacity, transform';
statusElement.style.transform = 'translateY(0)';
statusElement.style.opacity = '1';
statusElement.style.display = 'block';
statusElement.style.cssText = `
margin-top:10px; padding:8px; border-radius:6px; font-size:12px; transition: opacity .2s ease, transform .2s ease;
${isError ? 'background:#fee2e2; color:#b91c1c; border:1px solid #fecaca;' : 'background:#fef2f2; color:#be185d; border:1px solid #fecdd3;'}
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
`;
requestAnimationFrame(()=>{ statusElement.textContent = text; });
// 2秒后自动隐藏(使用过渡,避免卡顿)
setTimeout(() => {
if (statusElement && statusElement.textContent === text) {
statusElement.style.transform = 'translateY(-4px)';
statusElement.style.opacity = '0';
setTimeout(() => {
if (!statusElement || statusElement.textContent !== text) return;
statusElement.style.display = 'none';
statusElement.style.opacity = '1';
statusElement.style.transform = 'translateY(0)';
}, 220);
}
}, 2000);
}
function debounce(func, wait) {
let timeout;
return function () {
const context = this;
const args = arguments;
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(context, args), wait);
};
}
function safeGet(obj, path, defaultValue = '') {
try {
return path.split('.').reduce((o, k) => o[k], obj) || defaultValue;
} catch (e) {
return defaultValue;
}
}
// 图片获取函数
const IMG_CACHE = new Map();
const IMG_QUEUE = [];
let IMG_ACTIVE = 0;
const IMG_LIMIT = 8; // 图片请求并发上限(加速首屏)
function pumpImageQueue() {
try {
while (IMG_ACTIVE < IMG_LIMIT && IMG_QUEUE.length > 0) {
const { task, resolve, reject } = IMG_QUEUE.shift();
IMG_ACTIVE++;
Promise.resolve()
.then(task)
.then(result => { IMG_ACTIVE--; resolve(result); pumpImageQueue(); })
.catch(err => { IMG_ACTIVE--; reject(err); pumpImageQueue(); });
}
} catch (e) { IMG_ACTIVE = Math.max(0, IMG_ACTIVE - 1); }
}
function scheduleImageTask(task) {
return new Promise((resolve, reject) => {
IMG_QUEUE.push({ task, resolve, reject });
pumpImageQueue();
});
}
function normalizeImageUrl(url) {
try {
if (!url) return '';
let u = String(url).trim();
if (u.startsWith('//')) u = 'https:' + u;
if (u.includes('doubanio.com') && !/^https?:/.test(u)) u = 'https:' + u;
return u;
} catch (e) { return url; }
}
// 控制豆瓣图片下载并发(防止瞬时并发触发风控,同时保持速度)
const D_IMG_CONCURRENCY = 5;
let dImgActive = 0;
const dImgQueue = [];
function runDoubanImageTask(task){
return new Promise((resolve)=>{
const exec = ()=>{
dImgActive++;
task().then(resolve).finally(()=>{
dImgActive--;
const next = dImgQueue.shift();
if (next) next();
});
};
if (dImgActive < D_IMG_CONCURRENCY) exec(); else dImgQueue.push(exec);
});
}
function getImageDataURLWithQuality(url) {
return scheduleImageTask(() => runDoubanImageTask(() => new Promise((resolve) => {
if (!url) { resolve('https://picsum.photos/800/450?default-still'); return; }
let baseUrl = normalizeImageUrl(url);
// 内存级缓存,命中直接返回
if (IMG_CACHE.has(baseUrl)) { resolve(IMG_CACHE.get(baseUrl)); return; }
if (baseUrl.includes('doubanio.com') && baseUrl.includes('/m/')) {
const config = getConfig();
const qualityUrls = config.TMDB.DOUBAN_QUALITY.PRIORITY.map(quality =>
baseUrl.replace('/m/', `/${quality}/`)
);
const tryQuality = (index, retryCount = 0) => {
if (index >= qualityUrls.length) {
getFallbackImageDataURL(baseUrl).then((d)=>{ IMG_CACHE.set(baseUrl, d); resolve(d); });
return;
}
const currentUrl = qualityUrls[index];
doubanRequest({
method: 'GET',
url: currentUrl,
headers: { ...COMMON_HEADERS, 'Referer': 'https://movie.douban.com/' },
responseType: 'blob',
timeout: config.TMDB.DOUBAN_QUALITY.TIMEOUT
}).then((res)=>{
if (res.status === 200 && res.response) {
const reader = new FileReader();
reader.onload = (e) => { IMG_CACHE.set(baseUrl, e.target.result); resolve(e.target.result); };
reader.readAsDataURL(res.response);
} else if (retryCount < config.TMDB.DOUBAN_QUALITY.RETRY) {
setTimeout(() => tryQuality(index, retryCount + 1), 600);
} else {
tryQuality(index + 1);
}
}).catch(()=>{
if (retryCount < config.TMDB.DOUBAN_QUALITY.RETRY) {
setTimeout(() => tryQuality(index, retryCount + 1), 600);
} else {
tryQuality(index + 1);
}
});
};
tryQuality(0);
return;
}
getFallbackImageDataURL(baseUrl).then((d)=>{ IMG_CACHE.set(baseUrl, d); resolve(d); });
})));
}
function shouldConvertToDataURL(url) {
try {
const u = normalizeImageUrl(url);
// 仅对豆瓣域名转DataURL;TMDB直接走CDN缩略图,避免慢
return /doubanio\.com/.test(u);
} catch (e) { return false; }
}
// 将豆瓣大图转为缩略图路径(m尺寸)
function toDoubanThumb(url) {
try {
let u = normalizeImageUrl(url);
u = u.replace(/\/raw\//, '/m/').replace(/\/l\//, '/m/');
if (!/\/m\//.test(u)) u = u.replace(/\/s\//, '/m/');
return u;
} catch (e) { return url; }
}
// 获取用于展示的缩略图(豆瓣转为dataURL避免防盗链;TMDB返回CDN小图)
function getThumbnailForDisplay(url) {
try {
const base = normalizeImageUrl(url);
if (shouldConvertToDataURL(base)) {
const m = toDoubanThumb(base);
// 使用带Referer的请求转为dataURL,避免豆瓣直链防盗链导致空白
return getFallbackImageDataURL(m);
}
const tm = toTMDBThumb(base);
if (tm && tm.src) return Promise.resolve(tm.src);
return Promise.resolve(base);
} catch (e) { return Promise.resolve(url); }
}
// 懒加载占位图(1x1透明gif)
const LAZY_PLACEHOLDER = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==';
// 候选图片懒加载观察器
let posterObserver = null;
let stillObserver = null;
function initCandidateObservers() {
try {
const posterRoot = document.getElementById('poster-candidates') || null;
const stillRoot = document.getElementById('still-candidates') || null;
if (!posterObserver && posterRoot) {
posterObserver = new IntersectionObserver(async (entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
const card = entry.target;
if (!card || card.dataset.loaded === '1') continue;
const rawUrl = card.dataset.rawUrl || card.dataset.url;
const img = card.querySelector('img');
if (!rawUrl || !img) { posterObserver.unobserve(card); continue; }
try {
if (shouldConvertToDataURL(rawUrl)) {
const du = await getImageDataURLWithQuality(rawUrl);
img.src = du;
card.dataset.url = du;
if (card === selectedPosterEl && selectedPosterUrl === rawUrl) {
selectedPosterUrl = du;
}
} else {
// 列表小图,选中或升级时再换大图
img.src = rawUrl;
card.dataset.url = rawUrl;
}
card.dataset.loaded = '1';
} catch (e) {}
finally { posterObserver.unobserve(card); }
}
}
}, { root: posterRoot, rootMargin: '200px', threshold: 0.05 });
}
if (!stillObserver && stillRoot) {
stillObserver = new IntersectionObserver(async (entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
const card = entry.target;
if (!card || card.dataset.loaded === '1') continue;
const rawUrl = card.dataset.rawUrl || card.dataset.url;
const img = card.querySelector('img');
if (!rawUrl || !img) { stillObserver.unobserve(card); continue; }
try {
if (shouldConvertToDataURL(rawUrl)) {
const du = await getImageDataURLWithQuality(rawUrl);
img.src = du;
card.dataset.url = du;
if (card === selectedStillEl && selectedStillUrl === rawUrl) {
selectedStillUrl = du;
}
} else {
img.src = rawUrl;
card.dataset.url = rawUrl;
}
card.dataset.loaded = '1';
} catch (e) {}
finally { stillObserver.unobserve(card); }
}
}
}, { root: stillRoot, rootMargin: '200px', threshold: 0.05 });
}
} catch (e) {}
}
function observeCandidateCard(card, type) {
try {
if (type === 'poster' && posterObserver) posterObserver.observe(card);
if (type === 'still' && stillObserver) stillObserver.observe(card);
} catch (e) {}
}
function disconnectCandidateObservers() {
try { if (posterObserver) { posterObserver.disconnect(); posterObserver = null; } } catch (e) {}
try { if (stillObserver) { stillObserver.disconnect(); stillObserver = null; } } catch (e) {}
}
async function primeFirstCandidates() {
try {
// 立即加载首图,确保不点击也能拿到有效 dataURL
if (selectedPosterEl && selectedPosterEl.dataset && selectedPosterEl.dataset.rawUrl && selectedPosterEl.dataset.loaded !== '1') {
const rawUrl = selectedPosterEl.dataset.rawUrl;
const img = selectedPosterEl.querySelector('img');
if (img) {
try {
const src = shouldConvertToDataURL(rawUrl) ? await getImageDataURLWithQuality(rawUrl) : rawUrl.replace(`/${config.TMDB.LIST_POSTER_SIZE}/`, `/${config.TMDB.SELECTED_POSTER_SIZE}/`);
img.src = src;
selectedPosterEl.dataset.url = src;
selectedPosterEl.dataset.loaded = '1';
if (selectedPosterUrl === rawUrl) selectedPosterUrl = src;
} catch (e) {}
}
}
if (selectedStillEl && selectedStillEl.dataset && selectedStillEl.dataset.rawUrl && selectedStillEl.dataset.loaded !== '1') {
const rawUrl = selectedStillEl.dataset.rawUrl;
const img = selectedStillEl.querySelector('img');
if (img) {
try {
const src = shouldConvertToDataURL(rawUrl) ? await getImageDataURLWithQuality(rawUrl) : rawUrl.replace(`/${config.TMDB.LIST_STILL_SIZE}/`, `/${config.TMDB.SELECTED_STILL_SIZE}/`);
img.src = src;
selectedStillEl.dataset.url = src;
selectedStillEl.dataset.loaded = '1';
if (selectedStillUrl === rawUrl) selectedStillUrl = src;
} catch (e) {}
}
}
} catch (e) {}
}
// TMDB缩略图:w92 + 2x(w154)
function toTMDBThumb(url) {
try {
if (!url) return null;
const u = normalizeImageUrl(url);
const m = u.match(/\/t\/p\/(?:original|w\d+|h\d+)\/([^\?\s]+)$/);
if (!m) return null;
const tail = m[1];
const base = 'https://image.tmdb.org/t/p';
// 提升首屏清晰度:列表小图用w154,2x用w342(仍远小于w780),性能与质量平衡
return { src: `${base}/w154/${tail}`, srcset: `${base}/w342/${tail} 2x` };
} catch (e) { return null; }
}
// 将TMDB图片URL统一升级为original(用于最终排版,避免放大模糊)
function toTMDBOriginal(url) {
try {
if (!url) return url;
const u = normalizeImageUrl(url);
if (!/image\.tmdb\.org\/t\/p\//.test(u)) return url;
return u.replace(/\/t\/p\/(?:w\d+|h\d+|original)\//, '/t/p/original/');
} catch (e) { return url; }
}
// TMDB结果缓存(热评/列表等)
const TMDB_REVIEW_CACHE = new Map();
// 图片有效性验证函数(减少豆瓣请求,避免检测)
function validateImageUrl(url) {
return new Promise((resolve) => {
if (!url || url.includes('picsum.photos')) {
resolve(false);
return;
}
// 对豆瓣图片采用保守策略,减少HEAD请求
if (url.includes('doubanio.com')) {
// 豆瓣图片:仅基于URL格式判断,不发送验证请求
const isValidFormat = /doubanio\.com.*\.(jpg|jpeg|png|webp)/i.test(url) &&
!url.includes('default') &&
!url.includes('error');
console.log('豆瓣图片保守验证:', url, isValidFormat);
resolve(isValidFormat);
return;
}
// 非豆瓣图片才进行HEAD请求验证
GM_xmlhttpRequest({
method: 'HEAD',
url: url,
headers: {
...COMMON_HEADERS,
'Referer': url.includes('themoviedb.org') ? 'https://www.themoviedb.org/' : ''
},
timeout: 3000,
onload: (res) => {
const isValid = res.status === 200 &&
res.responseHeaders.toLowerCase().includes('image/');
resolve(isValid);
},
onerror: () => resolve(false),
ontimeout: () => resolve(false)
});
});
}
function getFallbackImageDataURL(url) {
return new Promise(async (resolve) => {
// 豆瓣图片跳过预验证,直接尝试下载(避免触发检测)
if (url.includes('doubanio.com')) {
console.log('豆瓣图片跳过预验证,直接下载:', url);
} else {
// 非豆瓣图片才进行预验证
const isValid = await validateImageUrl(url);
if (!isValid) {
console.log('图片URL无效,跳过下载:', url);
resolve('https://picsum.photos/800/450?error-still');
return;
}
}
GM_xmlhttpRequest({
method: 'GET',
url: url,
headers: {
...COMMON_HEADERS,
'Referer': url.includes('doubanio.com') ? 'https://movie.douban.com/' :
url.includes('themoviedb.org') ? 'https://www.themoviedb.org/' : ''
},
responseType: 'blob',
timeout: 5000, // 增加到5秒
onload: (res) => {
if (res.status === 200 && res.response && res.response.size > 0) {
// 验证blob是否为有效图片
if (res.response.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = (e) => {
// 进一步验证DataURL
const dataUrl = e.target.result;
if (dataUrl && dataUrl.length > 100) { // 最小有效长度检查
resolve(dataUrl);
} else {
console.log('生成的DataURL无效:', url);
resolve('https://picsum.photos/800/450?error-still');
}
};
reader.onerror = () => {
console.log('FileReader读取失败:', url);
resolve('https://picsum.photos/800/450?error-still');
};
reader.readAsDataURL(res.response);
} else {
console.log('响应不是图片类型:', res.response.type, url);
resolve('https://picsum.photos/800/450?error-still');
}
} else {
console.log('图片请求失败或空响应:', res.status, url);
resolve('https://picsum.photos/800/450?error-still');
}
},
onerror: (e) => {
console.log('图片请求网络错误:', e, url);
resolve('https://picsum.photos/800/450?error-still');
},
ontimeout: () => {
console.log('图片请求超时:', url);
resolve('https://picsum.photos/800/450?error-still');
}
});
});
}
// 【修复1】海报加载逻辑:按页截取(每次5张,不重复)
async function getDoubanOfficialPosters(subjectUrl, page = 1) {
return new Promise(resolve => {
try {
const config = getConfig();
const posterPage = page; // 使用传入的页码
const urlObj = new URL(subjectUrl);
const photosUrl = subjectUrl.replace(/\/subject\/(\d+)\/?$/, '/subject/$1/photos?type=R');
// 豆瓣海报请求URL(走防爬封装)
doubanRequest({
method: 'GET',
url: photosUrl,
headers: { ...COMMON_HEADERS, 'Referer': subjectUrl, 'Host': urlObj.hostname },
timeout: 8000
}).then((res)=>{
try {
// 豆瓣海报页面请求成功
const doc = new DOMParser().parseFromString(res.responseText, 'text/html');
// 尝试多种选择器
let posterImgs = [];
// 尝试1: 原有选择器
let imgs = Array.from(doc.querySelectorAll('.poster-col3 li img, .article img'));
console.log('选择器1找到图片数量:', imgs.length);
if (imgs.length === 0) {
// 尝试2: 更通用的选择器
imgs = Array.from(doc.querySelectorAll('img[src*="doubanio.com"], img[data-src*="doubanio.com"], img[srcset*="doubanio.com"]'));
console.log('选择器2找到图片数量:', imgs.length);
}
if (imgs.length === 0) {
// 尝试3: 查找所有img标签
imgs = Array.from(doc.querySelectorAll('img, source'));
console.log('选择器3找到所有图片数量:', imgs.length);
// 过滤出豆瓣图片
imgs = imgs.map(el => {
if (el.tagName.toLowerCase() === 'source') return el.srcset || '';
return el.getAttribute('data-src') || el.getAttribute('data-origin') || el.src || '';
}).filter(u => u.includes('doubanio.com'));
console.log('过滤后的豆瓣图片数量:', imgs.length);
}
// 尝试4: 如果还是没有,从页面主体区域查找
if (imgs.length === 0) {
console.log('前面方法都失败,尝试从主内容区域查找');
const mainContent = doc.querySelector('#wrapper') || doc.querySelector('.main') || doc.body;
if (mainContent) {
imgs = Array.from(mainContent.querySelectorAll('img')).filter(img =>
img.src &&
img.src.includes('doubanio.com') &&
(img.src.includes('/movie_poster_') || img.src.includes('/public/p'))
);
console.log('主内容区域找到的图片数量:', imgs.length);
}
}
posterImgs = imgs
.map(el => {
let src = (typeof el === 'string') ? el : (el.getAttribute?.('data-src') || el.getAttribute?.('data-origin') || el.src || '');
if (src && src.includes(' ')) { // 从srcset提取最清晰一项
const parts = src.split(',').map(s => s.trim());
src = parts[parts.length - 1].split(' ')[0];
}
if (src.includes('/m/')) src = src.replace('/m/', '/l/');
return src;
})
.filter(Boolean)
// 按当前页截取:(页号-1)*5 到 页号*5
.slice((posterPage - 1) * config.TMDB.IMAGE_CANDIDATES_COUNT, posterPage * config.TMDB.IMAGE_CANDIDATES_COUNT);
// 最终海报数据
console.log('最终海报数据:', posterImgs);
resolve(posterImgs.length ? posterImgs : []);
} catch (e) {
console.error('解析豆瓣海报页面失败:', e);
resolve([]);
}
}).catch((error)=>{
console.error('获取豆瓣海报请求错误:', error);
resolve([]);
});
} catch (e) {
console.error('无效的豆瓣主题URL:', subjectUrl, e);
resolve([]);
}
});
}
// 【修复2】剧照加载逻辑:按页截取(每次5张,不重复)+ 宽高适配
function getDoubanStillsList(url, page = 1) {
return new Promise(resolve => {
try {
const urlObj = new URL(url);
const stillsUrl = url.replace(/\/subject\/(\d+)\/?$/, '/subject/$1/photos?type=still');
// 豆瓣剧照请求URL(走防爬封装)
doubanRequest({
method: 'GET',
url: stillsUrl,
headers: { ...COMMON_HEADERS, 'Referer': url, 'Host': urlObj.hostname },
timeout: 8000
}).then((res)=>{
try {
// 豆瓣剧照页面请求成功
const doc = new DOMParser().parseFromString(res.responseText, 'text/html');
let finalStills = [];
// 尝试1: 查找带剧照标记的图片
let labeledStills = Array.from(doc.querySelectorAll('.poster-col3 li img[data-title*="剧照"], .article img[data-title*="剧照"]'))
.map(img => img.getAttribute('data-src') || img.getAttribute('data-origin') || img.src)
.filter(Boolean);
console.log('标记为剧照的图片数量:', labeledStills.length);
if (labeledStills.length > 0) {
finalStills = labeledStills;
} else {
// 尝试2: 所有poster-col3下的图片
let allImgs = Array.from(doc.querySelectorAll('.poster-col3 li img, .article img'))
.map(img => img.getAttribute('data-src') || img.getAttribute('data-origin') || img.src)
.filter(Boolean);
console.log('poster-col3下的所有图片数量:', allImgs.length);
if (allImgs.length === 0) {
// 尝试3: 查找所有豆瓣图片
allImgs = Array.from(doc.querySelectorAll('img[src*="doubanio.com"], img[data-src*="doubanio.com"], img[srcset*="doubanio.com"]'))
.map(el => el.getAttribute('data-src') || el.getAttribute('data-origin') || el.src || el.srcset || '')
.map(s => (s && s.includes(' ')) ? s.split(',').map(p=>p.trim()).pop().split(' ')[0] : s)
.filter(Boolean);
console.log('所有豆瓣图片数量:', allImgs.length);
}
finalStills = allImgs;
}
// 按当前页截取:(页号-1)*5 到 页号*5
const config = getConfig();
const stillPage = page; // 使用传入的页码
const pageStills = finalStills.slice((stillPage - 1) * config.TMDB.IMAGE_CANDIDATES_COUNT, stillPage * config.TMDB.IMAGE_CANDIDATES_COUNT);
// 最终剧照数据
console.log('最终剧照数据:', pageStills);
resolve(pageStills);
} catch (e) {
console.error('解析豆瓣剧照页面失败:', e);
resolve([]);
}
}).catch((error)=>{
console.error('获取豆瓣剧照请求错误:', error);
resolve([]);
});
} catch (e) {
console.error('无效的豆瓣URL:', url, e);
resolve([]);
}
});
}
// 【修复3】TMDB剧照加载逻辑:按页截取(统一分页逻辑)
function getTMDBStillsList(mediaType, id, page = 1) {
return new Promise(resolve => {
const config = getConfig();
const stillPage = page; // 使用传入的页码
const stillCutsUrl = `${config.TMDB.BASE_URL}/${mediaType}/${id}/images?api_key=${config.TMDB.API_KEY}&include_image_language=zh,en&image_type=still_cuts&sort_by=primary`;
GM_xmlhttpRequest({
method: 'GET',
url: stillCutsUrl,
headers: { 'Authorization': `Bearer ${config.TMDB.ACCESS_TOKEN}`, 'Content-Type': 'application/json' },
timeout: 10000,
onload: (res) => {
try {
const data = JSON.parse(res.responseText);
const stillCuts = safeGet(data, 'still_cuts', []);
// 按当前页截取剧照(每次5张)
let currentPageStills = stillCuts
.map(img => `${config.TMDB.IMAGE_BASE_URL}${config.TMDB.LIST_STILL_SIZE}/${img.file_path}`)
.filter(Boolean)
.slice((stillPage - 1) * config.TMDB.IMAGE_CANDIDATES_COUNT, stillPage * config.TMDB.IMAGE_CANDIDATES_COUNT);
if (currentPageStills.length >= config.TMDB.IMAGE_CANDIDATES_COUNT) {
resolve(currentPageStills);
return;
}
// 补充backdrops(按当前页截取)
const backdropsUrl = `${config.TMDB.BASE_URL}/${mediaType}/${id}/images?api_key=${config.TMDB.API_KEY}&include_image_language=zh,en&image_type=backdrop&sort_by=vote_average.desc`;
GM_xmlhttpRequest({
method: 'GET',
url: backdropsUrl,
headers: { 'Authorization': `Bearer ${config.TMDB.ACCESS_TOKEN}`, 'Content-Type': 'application/json' },
onload: (res) => {
try {
const data = JSON.parse(res.responseText);
const backdrops = safeGet(data, 'backdrops', []);
const backdropPageStills = backdrops
.map(img => `${config.TMDB.IMAGE_BASE_URL}${config.TMDB.LIST_STILL_SIZE}/${img.file_path}`)
.filter(Boolean)
.slice((stillPage - 1) * config.TMDB.IMAGE_CANDIDATES_COUNT, stillPage * config.TMDB.IMAGE_CANDIDATES_COUNT);
currentPageStills = [...currentPageStills, ...backdropPageStills].slice(0, config.TMDB.IMAGE_CANDIDATES_COUNT);
if (currentPageStills.length >= config.TMDB.IMAGE_CANDIDATES_COUNT) {
resolve(currentPageStills);
return;
}
// 补充posters(按当前页截取)
const postersUrl = `${config.TMDB.BASE_URL}/${mediaType}/${id}/images?api_key=${config.TMDB.API_KEY}&include_image_language=zh,en&image_type=poster&sort_by=primary`;
GM_xmlhttpRequest({
method: 'GET',
url: postersUrl,
headers: { 'Authorization': `Bearer ${config.TMDB.ACCESS_TOKEN}`, 'Content-Type': 'application/json' },
onload: (res) => {
try {
const data = JSON.parse(res.responseText);
const posters = safeGet(data, 'posters', []);
const posterPageStills = posters.slice(1)
.map(img => `${config.TMDB.IMAGE_BASE_URL}${config.TMDB.LIST_STILL_SIZE}/${img.file_path}`)
.filter(Boolean)
.slice((stillPage - 1) * config.TMDB.IMAGE_CANDIDATES_COUNT, stillPage * config.TMDB.IMAGE_CANDIDATES_COUNT);
currentPageStills = [...currentPageStills, ...posterPageStills].slice(0, config.TMDB.IMAGE_CANDIDATES_COUNT);
resolve(currentPageStills);
} catch (e) {
resolve(currentPageStills);
}
},
onerror: () => resolve(currentPageStills),
ontimeout: () => resolve(currentPageStills)
});
} catch (e) {
resolve(currentPageStills);
}
},
onerror: () => resolve(currentPageStills),
ontimeout: () => resolve(currentPageStills)
});
} catch (e) {
resolve([]);
}
},
onerror: () => resolve([]),
ontimeout: () => resolve([])
});
});
}
// 搜索相关函数
// 搜索结果缓存
const searchCache = new Map();
const CACHE_DURATION = 5 * 60 * 1000; // 5分钟缓存
// 全局搜索状态变量
let lastSearchResults = [];
let lastSearchQuery = '';
// 统一的层级管理常量
const Z_INDEX_LAYERS = {
SEARCH_RESULTS: 2147483647, // 搜索结果框 - 最高层级
SEARCH_CONTAINER: 2147483646, // 搜索容器
SEARCH_LOADING: 2147483645 // 搜索中指示器 - 在搜索框内显示
};
// 优化的搜索中指示器控制函数
function setSearchLoading(show) {
const loadingIndicator = document.getElementById('search-loading');
if (loadingIndicator) {
if (show) {
// 记录显示时间
loadingIndicator.dataset.showTime = Date.now().toString();
// 立即显示并设置正确的层级
loadingIndicator.style.setProperty('display', 'block', 'important');
loadingIndicator.style.setProperty('visibility', 'visible', 'important');
loadingIndicator.style.setProperty('opacity', '1', 'important');
loadingIndicator.style.setProperty('z-index', Z_INDEX_LAYERS.SEARCH_LOADING, 'important');
// 立即校准位置,确保显示在搜索框内
resetSearchLoadingPosition();
// 搜索中指示器已显示
} else {
// 清除显示时间记录
delete loadingIndicator.dataset.showTime;
loadingIndicator.style.setProperty('display', 'none', 'important');
// 搜索中指示器已隐藏
}
} else {
console.warn('搜索中指示器元素未找到');
}
}
// 统一的搜索结果框显示控制函数 - 优化版,确保输入框水平对齐
function showSearchResults() {
const resultsContainer = document.getElementById('search-results');
const controlPanel = document.getElementById('douban-tmdb-panel');
const inputContainer = document.getElementById('input-container');
if (resultsContainer && lastSearchResults.length > 0) {
// 只有在内容已准备好时才显示
if (resultsContainer.innerHTML.trim() !== '') {
// 优化控制面板展开机制 - 确保输入框保持水平对齐
if (controlPanel) {
// 添加展开状态的视觉指示
controlPanel.classList.add('expanded');
// 确保控制面板有足够的内部空间显示搜索结果
controlPanel.style.setProperty('overflow', 'visible', 'important');
controlPanel.style.setProperty('transition', 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)', 'important');
}
// 确保输入框容器保持水平对齐
if (inputContainer) {
inputContainer.style.setProperty('align-items', 'flex-start', 'important');
inputContainer.style.setProperty('display', 'flex', 'important');
}
// 设置显示状态:通过类控制展开,避免display切换导致空白
resultsContainer.style.setProperty('display', 'block', 'important');
resultsContainer.style.setProperty('visibility', 'visible', 'important');
resultsContainer.style.setProperty('opacity', '1', 'important');
resultsContainer.style.setProperty('position', 'relative', 'important');
resultsContainer.style.setProperty('z-index', '1', 'important');
resultsContainer.style.setProperty('margin-top', '8px', 'important');
// 添加显示动画
resultsContainer.classList.add('show');
return true;
}
}
return false;
}
// 简化的显示动画函数
function animateShow(element, className = 'show') {
if (element && !element.classList.contains(className)) {
element.classList.add(className);
}
}
// 将搜索结果容器的可视高度限制为约6条影片的高度,更多通过滚动查看
function adjustSearchResultsHeightToSix(container) {
try {
if (!container) return;
const items = container.querySelectorAll('.search-item');
if (!items || items.length === 0) return;
if (items.length <= 6) {
// 少于等于6条不限制高度
container.style.removeProperty('max-height');
return;
}
// 计算前6条底部相对容器的像素高度
const sixth = items[5];
const targetBottom = sixth.offsetTop + sixth.offsetHeight + 6; // 6px缓冲
container.style.setProperty('max-height', targetBottom + 'px', 'important');
container.style.setProperty('overflow-y', 'auto', 'important');
container.style.setProperty('overscroll-behavior', 'contain', 'important');
} catch (_) {}
}
// 轻量隐藏:只做淡出与关闭交互,避免 display 切换引发布局重排
function fadeOutSearchResultsNoLayout() {
const resultsContainer = document.getElementById('search-results');
const controlPanel = document.getElementById('douban-tmdb-panel');
const inputContainer = document.getElementById('input-container');
if (resultsContainer) {
// 先移除动画类,触发隐藏动画
resultsContainer.classList.remove('show');
resultsContainer.style.willChange = 'opacity, transform, max-height';
resultsContainer.style.visibility = 'hidden';
resultsContainer.style.opacity = '0';
resultsContainer.style.pointerEvents = 'none';
resultsContainer.style.maxHeight = '0';
resultsContainer.style.marginTop = '0';
// 过渡结束后彻底移除占位,避免窄屏不回弹
const endHide = () => {
resultsContainer.style.setProperty('display', 'none', 'important');
resultsContainer.removeEventListener('transitionend', endHide);
};
resultsContainer.addEventListener('transitionend', endHide);
setTimeout(endHide, 250);
// 恢复控制面板的原始状态
if (controlPanel) {
// 移除展开状态的视觉指示
controlPanel.classList.remove('expanded');
// 恢复控制面板的原始样式
controlPanel.style.setProperty('overflow', 'visible', 'important');
controlPanel.style.setProperty('transition', 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)', 'important');
}
// 确保输入框容器保持水平对齐
if (inputContainer) {
inputContainer.style.setProperty('align-items', 'flex-start', 'important');
inputContainer.style.setProperty('display', 'flex', 'important');
}
}
// 隐藏搜索中指示器
setSearchLoading(false);
}
// 兼容旧调用名
function hideSearchResults() { fadeOutSearchResultsNoLayout(); }
// 检测当前布局状态
function detectLayoutState() {
const searchInput = document.getElementById('search-movie');
const mediaUrlLabel = document.querySelector('label[for="media-url"], label[style*="width:70px"]');
if (!searchInput || !mediaUrlLabel) return 'horizontal';
const inputRect = searchInput.getBoundingClientRect();
const labelRect = mediaUrlLabel.getBoundingClientRect();
// 如果标签在搜索框下方,说明是垂直布局
const isVertical = labelRect.top > inputRect.bottom + 10; // 10px容差
return isVertical ? 'vertical' : 'horizontal';
}
// 动态调整输入框对齐
function adjustInputAlignment() {
const inputContainer = document.getElementById('input-container');
const searchContainer = document.querySelector('div[style*="flex: 1 1 250px"][style*="min-width: 200px"][style*="position: relative"]');
const linkContainer = document.getElementById('media-url-container');
if (!inputContainer || !searchContainer || !linkContainer) return;
const windowWidth = window.innerWidth;
const isMobile = windowWidth < 800;
if (isMobile) {
// 移动端:垂直布局
inputContainer.style.setProperty('flex-direction', 'column', 'important');
inputContainer.style.setProperty('align-items', 'stretch', 'important');
inputContainer.style.setProperty('gap', '12px', 'important');
// 竖排时子块恢复自适应,避免横排规则残留
try {
searchContainer.style.removeProperty('display');
searchContainer.style.removeProperty('align-items');
searchContainer.style.removeProperty('min-height');
linkContainer.style.removeProperty('display');
linkContainer.style.removeProperty('align-items');
linkContainer.style.removeProperty('min-height');
} catch (e) {}
} else {
// 桌面端:水平布局
inputContainer.style.setProperty('flex-direction', 'row', 'important');
inputContainer.style.setProperty('align-items', 'flex-start', 'important');
inputContainer.style.setProperty('gap', '15px', 'important');
// 两个子容器统一为顶部对齐,避免切换瞬间出现"先居中"的视觉跳动
try {
searchContainer.style.setProperty('display', 'flex', 'important');
searchContainer.style.setProperty('align-items', 'flex-start', 'important');
searchContainer.style.setProperty('min-width', '200px', 'important');
linkContainer.style.setProperty('display', 'flex', 'important');
linkContainer.style.setProperty('align-items', 'flex-start', 'important');
linkContainer.style.setProperty('min-width', '200px', 'important');
} catch (e) {}
}
// 确保两个输入框容器高度一致
const searchRect = searchContainer.getBoundingClientRect();
const linkRect = linkContainer.getBoundingClientRect();
if (!isMobile && Math.abs(searchRect.height - linkRect.height) > 5) {
const maxHeight = Math.max(searchRect.height, linkRect.height);
searchContainer.style.setProperty('min-height', maxHeight + 'px', 'important');
linkContainer.style.setProperty('min-height', maxHeight + 'px', 'important');
}
}
// 优化的位置校准函数
function resetSearchLoadingPosition() {
const loadingIndicator = document.getElementById('search-loading');
const searchInput = document.getElementById('search-movie');
if (!loadingIndicator || !searchInput) return;
const windowWidth = window.innerWidth;
const isVerticalLayout = windowWidth < 800;
// 简化的位置计算
const rightOffset = windowWidth > 900 ? 8 :
windowWidth > 650 ? 6 :
windowWidth > 400 ? 4 :
windowWidth > 300 ? 2 : 1;
const topPosition = isVerticalLayout
? `calc(45% + ${windowWidth > 650 ? 2 : 1}px)`
: '50%';
// 强制设置所有样式,确保在缩放时完全不会消失
loadingIndicator.style.setProperty('position', 'absolute', 'important');
loadingIndicator.style.setProperty('right', `${rightOffset}px`, 'important');
loadingIndicator.style.setProperty('top', topPosition, 'important');
loadingIndicator.style.setProperty('transform', 'translateY(-50%)', 'important');
loadingIndicator.style.setProperty('display', 'block', 'important');
loadingIndicator.style.setProperty('visibility', 'visible', 'important');
loadingIndicator.style.setProperty('opacity', '1', 'important');
loadingIndicator.style.setProperty('z-index', Z_INDEX_LAYERS.SEARCH_LOADING, 'important');
loadingIndicator.style.setProperty('color', '#6b7280', 'important');
loadingIndicator.style.setProperty('font-size', '11px', 'important');
loadingIndicator.style.setProperty('font-weight', '500', 'important');
loadingIndicator.style.setProperty('background', 'rgba(255, 255, 255, 0.9)', 'important');
loadingIndicator.style.setProperty('padding', '2px 6px', 'important');
loadingIndicator.style.setProperty('border-radius', '4px', 'important');
}
// 强制注入CSS确保搜索结果框显示(优化版 - 内嵌到控制面板中)
function injectSearchResultsCSS() {
// 避免重复注入CSS
if (document.getElementById('search-results-force-style')) {
return;
}
const style = document.createElement('style');
style.id = 'search-results-force-style';
style.textContent = `
/* 强制降低遮挡元素的层级 */
div.tox-editor-header {
z-index: 1 !important;
}
/* 独立容器的搜索结果框样式 - 确保输入框水平对齐 */
#search-results {
position: relative !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
z-index: 1 !important;
background: #fff !important;
border: 1px solid #f3d5d9 !important;
border-radius: 6px !important;
margin-top: 8px !important;
margin-left: 0 !important;
margin-right: 0 !important;
box-shadow: 0 4px 12px rgba(0,0,0,0.15) !important;
width: 100% !important;
max-height: 400px !important;
overflow-y: auto !important;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important;
transform: none !important;
}
/* 搜索结果容器优化样式 - 已内嵌到控制面板中,无需绝对定位 */
/* 搜索中指示器基础样式 - 默认隐藏,由JavaScript控制显示 */
#search-loading {
position: absolute !important;
right: 10px !important;
top: 35% !important;
transform: translateY(-50%) !important;
color: #6b7280 !important;
font-size: 11px !important;
font-weight: 500 !important;
background: rgba(255, 255, 255, 0.9) !important;
padding: 2px 6px !important;
border-radius: 4px !important;
z-index: 1001 !important; /* 搜索中指示器 - 最高层级 */
pointer-events: none !important;
white-space: nowrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
max-width: 60px !important;
box-sizing: border-box !important;
display: none !important; /* 默认隐藏,由JavaScript控制 */
visibility: visible !important; /* 确保可见性 */
opacity: 1 !important; /* 确保透明度 */
}
#search-results * {
pointer-events: auto !important;
}
/* 确保搜索结果框的父容器也有足够高的z-index */
div[style*="position: relative"]:has(#search-results) {
z-index: 999999 !important;
}
/* 搜索中指示器保护样式已在上方统一定义 */
/* 重复的媒体查询已在上方统一定义 */
/* 在垂直布局时调整搜索中指示器位置 */
@media screen and (max-width: 800px) {
/* 确保在垂直布局时,指示器相对于输入框而不是标签定位 */
div[style*="position: relative"]:has(#search-movie) {
position: relative !important;
}
/* 当标签和输入框变为上下布局时,调整搜索中指示器位置 */
#search-loading {
position: absolute !important;
right: 10px !important;
/* 在垂直布局时,现在相对于搜索框容器定位,不需要额外偏移 */
top: 35% !important;
transform: translateY(-50%) !important;
color: #6b7280 !important;
font-size: 11px !important;
font-weight: 500 !important;
background: rgba(255, 255, 255, 0.9) !important;
padding: 2px 6px !important;
border-radius: 4px !important;
z-index: 1001 !important; /* 搜索中指示器 - 最高层级 */
pointer-events: none !important;
/* 强制重置其他可能影响的属性 */
left: auto !important;
bottom: auto !important;
margin: 0 !important;
width: auto !important;
height: auto !important;
max-width: none !important;
min-width: 0 !important;
}
}
@media screen and (max-width: 650px) {
/* 更窄屏幕下的微调 */
#search-loading {
position: absolute !important;
right: 3px !important;
top: 35% !important;
transform: translateY(-50%) !important;
font-size: 8px !important;
padding: 1px 2px !important;
max-width: 35px !important;
width: auto !important;
height: auto !important;
white-space: nowrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
box-sizing: border-box !important;
}
}
/* 470像素以下优化 - 防止搜索中指示器超出搜索框 */
@media screen and (max-width: 470px) {
#search-loading {
position: absolute !important;
right: 8px !important;
top: 35% !important;
transform: translateY(-50%) !important;
font-size: 7px !important;
padding: 1px 2px !important;
max-width: 30px !important;
width: auto !important;
height: auto !important;
white-space: nowrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
box-sizing: border-box !important;
}
/* 如果空间太小,只显示点号 */
#search-loading i {
font-style: normal !important;
}
}
/* 极限窄屏幕下的搜索中指示器优化 */
@media screen and (max-width: 400px) {
#search-loading {
position: absolute !important;
right: 6px !important;
top: 35% !important;
transform: translateY(-50%) !important;
font-size: 6px !important;
padding: 1px !important;
max-width: 25px !important;
width: auto !important;
height: auto !important;
white-space: nowrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
box-sizing: border-box !important;
}
/* 极限窄屏幕下简化文本 */
#search-loading i::after {
content: "..." !important;
}
#search-loading i {
font-size: 0 !important;
}
}
`;
document.head.appendChild(style);
}
// 显示搜索结果的函数 - 终极版本,彻底解决空白问题
function displaySearchResults(results, container) {
// 隐藏搜索中指示器
setSearchLoading(false);
// 使用DocumentFragment和createElement优化DOM创建
const fragment = document.createDocumentFragment();
// 显示全部结果,滚动在容器内进行
const doubanResults = results.filter(r => r.source === '豆瓣');
const tmdbResults = results.filter(r => r.source === 'TMDB');
const orderedResults = [...doubanResults, ...tmdbResults];
lastSearchResults = orderedResults;
if (results.length === 0) {
const emptyDiv = document.createElement('div');
emptyDiv.style.cssText = 'padding:12px; color:#6b7280; text-align:center; background:#f9fafb; border-radius:6px; margin:4px;';
emptyDiv.textContent = '未找到结果,请尝试其他关键词';
fragment.appendChild(emptyDiv);
} else {
const makeGroup = (titleText, list, offsetBase) => {
const title = document.createElement('div');
// 简单分组:仅标题 + 细分隔线,不使用背景块
title.style.cssText = 'margin:8px 4px 4px; padding:0 2px 4px; font-size:12px; color:#6b7280; font-weight:600; border-bottom:1px dashed #e5e7eb;';
title.textContent = titleText;
fragment.appendChild(title);
const box = document.createElement('div');
box.className = 'results-container';
list.forEach((item, idx) => {
const searchItem = createSearchResultItem(item, offsetBase + idx);
box.appendChild(searchItem);
});
fragment.appendChild(box);
return offsetBase + list.length;
};
let offset = 0;
if (doubanResults.length) offset = makeGroup('豆瓣', doubanResults, offset);
if (tmdbResults.length) offset = makeGroup('TMDB', tmdbResults, offset);
}
// 先隐藏容器,避免显示空白
container.style.display = 'none';
container.style.visibility = 'hidden';
container.style.opacity = '0';
// 一次性设置内容,减少重绘
container.innerHTML = '';
container.appendChild(fragment);
// 设置内嵌显示状态
container.style.setProperty('display', 'block', 'important');
container.style.setProperty('visibility', 'visible', 'important');
container.style.setProperty('opacity', '1', 'important');
container.style.setProperty('position', 'relative', 'important');
container.style.setProperty('z-index', '1', 'important');
// 调用统一的显示函数,确保控制面板自动展开和动画效果
showSearchResults();
// 将高度锁定在约6条的高度,可滚动查看更多
adjustSearchResultsHeightToSix(container);
// 延迟加载豆瓣图片,避免阻塞显示框动画
if (orderedResults.length > 0) {
// 立即设置懒加载(不再延迟与替换容器,避免首屏转圈)
setupLazyImageLoading(orderedResults, container);
}
}
// 创建单个搜索结果项的优化函数
function createSearchResultItem(item, index) {
const resultItem = document.createElement('div');
resultItem.className = 'search-item';
resultItem.setAttribute('data-url', item.url || '');
resultItem.setAttribute('data-type', item.type || '');
resultItem.setAttribute('data-index', index);
const isDouban = item.source === '豆瓣';
const isTMDB = item.source === 'TMDB';
const bgColor = isDouban ? '#eff6ff' : isTMDB ? '#e0f2fe' : '';
resultItem.style.cssText = `padding:6px; cursor:pointer; border-bottom:1px solid #e5e7eb; display:flex; align-items:center; gap:6px; background:${bgColor};`;
// 创建海报占位符
const posterDiv = document.createElement('div');
posterDiv.className = 'poster-placeholder';
posterDiv.style.cssText = 'width:36px; height:54px; background:#f3f4f6; border-radius:3px; display:flex; align-items:center; justify-content:center; color:#9ca3af; transition: all 0.2s ease;';
// 处理图片
if (item.poster) {
let imageUrl = item.poster;
if (imageUrl.includes('doubanio.com') && !imageUrl.includes('https:')) {
imageUrl = 'https:' + imageUrl;
}
if (imageUrl.includes('doubanio.com')) {
// 豆瓣图片:显示加载中图标
const spinner = document.createElement('i');
spinner.className = 'fa fa-spinner fa-spin';
spinner.style.cssText = 'font-size:14px; color:#f59e0b;';
posterDiv.appendChild(spinner);
} else {
// 其他图片:直接显示
const img = document.createElement('img');
img.src = imageUrl;
img.alt = item.title;
img.style.cssText = 'width:36px; height:54px; object-fit:cover; border-radius:3px;';
img.setAttribute('referrerpolicy', 'no-referrer');
img.onerror = function() {
this.style.display = 'none';
const fallback = document.createElement('i');
fallback.className = 'fa fa-film';
fallback.style.cssText = 'font-size:14px;';
posterDiv.appendChild(fallback);
};
img.onload = () => {}; // 图片加载成功
posterDiv.appendChild(img);
}
} else {
const fallback = document.createElement('i');
fallback.className = 'fa fa-film';
fallback.style.cssText = 'font-size:14px;';
posterDiv.appendChild(fallback);
}
// 创建内容区域
const contentDiv = document.createElement('div');
const title = document.createElement('strong');
title.textContent = item.title;
title.style.cssText = 'color:#374151; font-size:12px; line-height:1.3; transition: all 0.2s ease;';
const meta = document.createElement('div');
meta.textContent = `${item.type} • ${item.year} • ${item.source}`;
meta.style.cssText = 'color:#6b7280; font-size:11px; margin-top:2px; transition: all 0.2s ease;';
contentDiv.appendChild(title);
contentDiv.appendChild(meta);
resultItem.appendChild(posterDiv);
resultItem.appendChild(contentDiv);
return resultItem;
}
// 分批加载豆瓣图片,避免同时发起过多请求
function loadDoubanImagesBatch(results, container) {
const doubanItems = results
.map((item, index) => ({ item, index }))
.filter(({ item }) => item.poster && item.poster.includes('doubanio.com'));
if (doubanItems.length === 0) return;
// 分批加载(每批2个),首屏更快
const batchSize = 2;
let currentBatch = 0;
function loadNextBatch() {
const startIndex = currentBatch * batchSize;
const endIndex = Math.min(startIndex + batchSize, doubanItems.length);
const batch = doubanItems.slice(startIndex, endIndex);
batch.forEach(({ item, index }) => {
loadSingleDoubanImage(item, index, container);
});
currentBatch++;
// 如果还有更多批次,延迟加载下一批(加入抖动,避免固定间隔触发风控)
if (endIndex < doubanItems.length) {
const jitter = 350 + Math.floor(Math.random()*250);
setTimeout(loadNextBatch, jitter);
}
}
loadNextBatch();
}
// 加载单个豆瓣图片
function loadSingleDoubanImage(item, index, container) {
const searchItem = container.querySelector(`[data-index="${index}"]`);
if (!searchItem) return;
const placeholder = searchItem.querySelector('.poster-placeholder');
if (!placeholder) return;
let imageUrl = item.poster;
if (!imageUrl.includes('https:')) {
imageUrl = 'https:' + imageUrl;
}
// 先尝试直链缩略图,失败再退到带Referer下载和条目页
const tryVariants = async () => {
let thumb = imageUrl;
try { thumb = toDoubanThumb(imageUrl); } catch(_) {}
// 先快速插入直链缩略
try {
const fastImg = document.createElement('img');
fastImg.src = thumb;
fastImg.style.cssText = 'width:36px; height:54px; object-fit:cover; border-radius:3px;';
fastImg.alt = item.title;
fastImg.referrerPolicy = 'no-referrer';
fastImg.onerror = () => fallbackFetch();
placeholder.innerHTML = '';
placeholder.appendChild(fastImg);
} catch(_) {}
const fallbackFetch = async () => {
try {
const res = await doubanRequest({ method:'GET', url: thumb, headers:{ 'Referer':'https://movie.douban.com/' }, responseType:'blob', timeout: 6500 });
const blob = res.response;
const hdr = (res.responseHeaders||'').toLowerCase();
const invalid = !blob || (blob.size && blob.size < 1500) || hdr.includes('text/html');
if (!invalid) {
const du = URL.createObjectURL(blob);
placeholder.innerHTML = `
`;
return;
}
} catch(_) {}
try {
const real = await tryResolvePosterFromSubject(item.url);
if (real) {
const du = await getFallbackImageDataURL(real);
if (du && !/picsum\.photos\/.+error/.test(du)) {
placeholder.innerHTML = `
`;
return;
}
}
} catch(_) {}
placeholder.innerHTML = '';
};
// 保险:2.2s内仍未替换成功,主动触发一次fallback
setTimeout(()=>{
const im = placeholder.querySelector('img');
if (!im || !im.complete) fallbackFetch();
}, 2200);
};
setTimeout(tryVariants, 80 + Math.floor(Math.random()*180));
}
// 从豆瓣条目页解析海报URL(用于搜索列表占位图“new.gif”情况)
function tryResolvePosterFromSubject(subjectUrl) {
return new Promise((resolve) => {
if (!subjectUrl) { resolve(''); return; }
setTimeout(() =>
doubanRequest({ method: 'GET', url: subjectUrl, headers: { 'Referer': 'https://movie.douban.com/' }, timeout: 5000 })
.then((res) => {
try {
const doc = new DOMParser().parseFromString(res.responseText, 'text/html');
const og = doc.querySelector('meta[property="og:image"]');
let u = (og && og.content) || '';
if (!u) {
const img = doc.querySelector('#mainpic img, .subject .pic img');
u = img ? (img.getAttribute('src') || '') : '';
}
if (u && u.startsWith('//')) u = 'https:' + u;
resolve(u);
} catch (_) { resolve(''); }
})
.catch(() => resolve('')),
200 + Math.floor(Math.random() * 300)
);
});
}
// 设置懒加载图片,只加载可见区域的图片
function setupLazyImageLoading(results, container) {
const doubanItems = results
.map((item, index) => ({ item, index }))
.filter(({ item }) => item.poster && item.poster.includes('doubanio.com'));
if (doubanItems.length === 0) return;
// 创建IntersectionObserver进行懒加载
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const searchItem = entry.target;
const index = parseInt(searchItem.getAttribute('data-index'));
const item = results[index];
if (item && item.poster && item.poster.includes('doubanio.com')) {
loadSingleDoubanImage(item, index, container);
observer.unobserve(searchItem); // 加载后停止观察
}
}
});
}, {
root: container,
rootMargin: '80px',
threshold: 0.01
});
// 观察所有搜索结果项;若选不到节点,延迟一帧重试一次,避免组标题插入后索引错位
const attachObservers = () => {
doubanItems.forEach(({ index }) => {
const searchItem = container.querySelector(`[data-index="${index}"]`);
if (searchItem) {
observer.observe(searchItem);
}
});
};
attachObservers();
if (container.querySelectorAll('.search-item').length < doubanItems.length) {
requestAnimationFrame(attachObservers);
}
// 兜底:立即加载首屏可见项,避免IntersectionObserver在嵌套滚动容器内不触发
try {
const rect = container.getBoundingClientRect();
container.querySelectorAll('.search-item').forEach(item => {
const r = item.getBoundingClientRect();
if (r.top < rect.bottom + 40 && r.bottom > rect.top - 40) {
const idx = parseInt(item.getAttribute('data-index'));
const it = results[idx];
if (it && it.poster && it.poster.includes('doubanio.com')) {
loadSingleDoubanImage(it, idx, container);
}
}
});
} catch (_) {}
}
// 图片懒加载设置函数 - 简化版
function setupImageLazyLoading(results, container) {
// 创建新的懒加载控制器
if (window.currentLazyLoadController) {
window.currentLazyLoadController.abort();
}
window.currentLazyLoadController = new AbortController();
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const item = entry.target;
const index = parseInt(item.getAttribute('data-index'));
const resultItem = results[index];
if (resultItem && resultItem.poster && !window.currentLazyLoadController.signal.aborted) {
const placeholder = item.querySelector('.poster-placeholder');
if (placeholder) {
// 尝试加载图片
// 直接使用原始URL,避免复杂的转换
let imageUrl = resultItem.poster;
// 处理豆瓣图片URL
if (imageUrl.includes('doubanio.com') && !imageUrl.includes('https:')) {
imageUrl = 'https:' + imageUrl;
}
// 处理后的图片URL
// 立即显示图片,不等待加载完成(TMDB使用小缩略图w92 + 2x)
const tmdbThumb = toTMDBThumb(imageUrl);
const attr = tmdbThumb ? `src='${tmdbThumb.src}' srcset='${tmdbThumb.srcset}'` : `src='${imageUrl}'`;
placeholder.innerHTML = `
';"
onload=''>
`;
}
}
observer.unobserve(item);
}
});
}, { rootMargin: '0px 0px 100px 0px' });
// 观察所有搜索项
container.querySelectorAll('.search-item').forEach(item => {
observer.observe(item);
});
}
function searchDouban(query) {
// 检查缓存
const cacheKey = `douban_${query}`;
const cached = searchCache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < CACHE_DURATION) {
// 使用豆瓣搜索缓存
return Promise.resolve(cached.data);
}
return new Promise((resolve) => {
// 三通道:suggest(快)+ HTML(全)+ Rexxar(稳)并行合并
const suggestP = searchDoubanSuggest(query).catch(() => []);
const rexxarP = searchDoubanRexxar(query).catch(() => []);
const url = `https://search.douban.com/movie/subject_search?search_text=${encodeURIComponent(query)}&cat=1002`;
doubanRequest({ method: 'GET', url: url, headers: { ...COMMON_HEADERS, 'Host': 'search.douban.com', 'Sec-Fetch-Dest': 'document' }, timeout: 3000 })
.then((res) => {
try {
if (res.status !== 200) throw new Error('bad-status');
const html = res.responseText;
const dataMatch = html.match(/window\.__DATA__\s*=\s*({.*?});/s);
if (!dataMatch || !dataMatch[1]) throw new Error('parse-fail');
const cleanData = dataMatch[1].replace(/\\x([0-9A-Fa-f]{2})/g, (match, hex) => String.fromCharCode(parseInt(hex, 16)));
const doubanData = JSON.parse(cleanData);
const items = safeGet(doubanData, 'items', []);
let results = items.map(item => ({
title: safeGet(item, 'title', '未知作品'),
type: safeGet(item, 'labels', []).some(l => l.text === '剧集') ? '电视剧' : '电影',
year: safeGet(item, 'title', '').match(/\((\d{4})\)/)?.[1] || '未知',
source: '豆瓣',
id: safeGet(item, 'id', ''),
url: safeGet(item, 'url', ''),
poster: safeGet(item, 'cover_url', '')
})).filter(item => item.url);
if (!results.length) {
return Promise.all([suggestP, rexxarP]).then(([sug, rex]) => {
const merged = [...(Array.isArray(sug)?sug:[]), ...(Array.isArray(rex)?rex:[])].filter((it,idx,arr)=> idx===arr.findIndex(t=>t.title===it.title && t.year===it.year));
searchCache.set(cacheKey, { data: merged, timestamp: Date.now() });
resolve(merged);
});
}
return Promise.all([suggestP, rexxarP]).then(([sug, rex]) => {
const merged = [...results, ...(Array.isArray(sug)?sug:[]), ...(Array.isArray(rex)?rex:[])].filter((it,idx,arr)=> idx===arr.findIndex(t=> (t.title===it.title || normalizeTextForCompare(t.title)===normalizeTextForCompare(it.title)) && t.year===it.year));
searchCache.set(cacheKey, { data: merged, timestamp: Date.now() });
resolve(merged);
}).catch(() => { searchCache.set(cacheKey, { data: results, timestamp: Date.now() }); resolve(results); });
} catch (e) {
Promise.all([suggestP, rexxarP]).then(([sug, rex]) => {
const merged = [...(Array.isArray(sug)?sug:[]), ...(Array.isArray(rex)?rex:[])].filter((it,idx,arr)=> idx===arr.findIndex(t=> (t.title===it.title || normalizeTextForCompare(t.title)===normalizeTextForCompare(it.title)) && t.year===it.year));
searchCache.set(cacheKey, { data: merged, timestamp: Date.now() });
resolve(merged);
}).catch(() => resolve([]));
}
})
.catch(() => {
Promise.all([suggestP, rexxarP]).then(([sug, rex]) => {
const merged = [...(Array.isArray(sug)?sug:[]), ...(Array.isArray(rex)?rex:[])].filter((it,idx,arr)=> idx===arr.findIndex(t=> (t.title===it.title || normalizeTextForCompare(t.title)===normalizeTextForCompare(it.title)) && t.year===it.year));
searchCache.set(cacheKey, { data: merged, timestamp: Date.now() });
resolve(merged);
}).catch(() => resolve([]));
});
});
}
// Rexxar 移动端搜索作为补充,提升 AI 面板豆瓣覆盖度
function searchDoubanRexxar(query) {
return new Promise((resolve) => {
const url = `https://m.douban.com/rexxar/api/v2/search?q=${encodeURIComponent(query)}&start=0&count=60&cat=1002`;
doubanRequest({ method: 'GET', url, headers: { ...COMMON_HEADERS, 'Host': 'm.douban.com', 'Referer': `https://m.douban.com/search/?q=${encodeURIComponent(query)}`, 'X-Requested-With': 'XMLHttpRequest' }, timeout: 4000 })
.then((res) => {
try {
const data = JSON.parse(res.responseText || '{}');
const items = data.subjects || data.items || [];
const list = items.map(it => {
const id = it.id || (it.target && it.target.id);
const title = it.title || (it.target && it.target.title) || (it.display && it.display.split('\n')[0]) || '未知作品';
const year = (it.year || (it.card_subtitle && (it.card_subtitle.match(/(\d{4})/)||[])[1])) || '未知';
const poster = (it.cover_url || (it.pic && it.pic.normal)) || '';
return id ? { title, type: '电影', year, source: '豆瓣', id, url: `https://movie.douban.com/subject/${id}/`, poster } : null;
}).filter(Boolean);
resolve(list);
} catch (_) { resolve([]); }
})
.catch(() => resolve([]));
});
}
// 豆瓣搜索回退:使用 subject_suggest 接口(JSON,速度更快,适合首次搜索)
function searchDoubanSuggest(query) {
return new Promise((resolve) => {
const url = `https://movie.douban.com/j/subject_suggest?q=${encodeURIComponent(query)}`;
doubanRequest({ method: 'GET', url: url, headers: { ...COMMON_HEADERS, 'Host': 'movie.douban.com', 'X-Requested-With': 'XMLHttpRequest' }, timeout: 4000 })
.then((res) => {
try {
if (res.status !== 200) { resolve([]); return; }
const arr = JSON.parse(res.responseText);
const results = (Array.isArray(arr) ? arr : []).map(item => ({
title: item.title || item.sub_title || '未知作品',
type: item.type === 'movie' ? '电影' : (item.type === 'tv' ? '电视剧' : '电影'),
year: item.year || '未知',
source: '豆瓣',
id: item.id || '',
url: item.id ? `https://movie.douban.com/subject/${item.id}/` : (item.url || ''),
poster: item.cover_url || ''
})).filter(it => it.url);
resolve(results);
} catch (e) { resolve([]); }
})
.catch(() => resolve([]));
});
}
function normalizeTextForCompare(text) {
try {
let s = (text || '').toString().toLowerCase();
// 全角转半角
s = s.replace(/[\uFF01-\uFF5E]/g, ch => String.fromCharCode(ch.charCodeAt(0) - 0xFEE0));
// 去除符号与空白
s = s.replace(/[\p{P}\p{S}\s]+/gu, '');
return s;
} catch (_) { return (text || '').toString().toLowerCase(); }
}
function parseQueryParts(q) {
const m = (q || '').match(/(.+?)(?:[\((\[]?(\d{4})[\))\]]?)?$/);
return { title: (m && m[1] ? m[1] : q).trim(), year: (m && m[2]) ? m[2] : '' };
}
function rankAndDedupResults(results, query) {
const { title, year } = parseQueryParts(query);
const normQ = normalizeTextForCompare(title);
const targetYear = parseInt(year || '0', 10);
const seen = new Set();
const ranked = (results || []).map(it => {
const t = (it.title || '').toString();
const y = parseInt((it.year || '').toString().slice(0,4) || '0', 10);
const normT = normalizeTextForCompare(t);
const titleScore = normT.includes(normQ) ? 5 : 0;
const yearScore = (targetYear && y) ? (5 - Math.min(5, Math.abs(targetYear - y))) : 0;
const sourceScore = it.source === '豆瓣' ? 2 : 1;
return { ...it, _score: titleScore + yearScore + sourceScore };
}).sort((a,b)=> b._score - a._score);
const dedup = [];
for (const it of ranked) {
const key = `${normalizeTextForCompare(it.title)}_${(it.year||'').slice(0,4)}`;
if (!seen.has(key)) { seen.add(key); dedup.push(it); }
}
return dedup;
}
function searchTMDB(query) {
// 检查缓存
const cacheKey = `tmdb_${query}`;
const cached = searchCache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < CACHE_DURATION) {
console.log('使用TMDB搜索缓存');
return Promise.resolve(cached.data);
}
return new Promise((resolve) => {
const config = getConfig();
const { title, year } = parseQueryParts(query);
const langs = ['zh-CN','en-US'];
const reqs = langs.map(lang => `${config.TMDB.BASE_URL}/search/multi?api_key=${config.TMDB.API_KEY}&query=${encodeURIComponent(title)}&language=${lang}${year?`&year=${year}`:''}`);
const keywordUrl = `${config.TMDB.BASE_URL}/search/keyword?api_key=${config.TMDB.API_KEY}&query=${encodeURIComponent(title)}`;
const send = (url) => new Promise(resv => {
GM_xmlhttpRequest({
method: 'GET',
url,
headers: { 'Authorization': `Bearer ${config.TMDB.ACCESS_TOKEN}`, 'Content-Type': 'application/json' },
timeout: 5000,
onload: r => { try { resv(JSON.parse(r.responseText)); } catch(_) { resv({}); } },
onerror: () => resv({}),
ontimeout: () => resv({})
});
});
Promise.all([ ...reqs.map(send), send(keywordUrl) ]).then(async (arr) => {
try {
const merge = [];
const pushItem = (item) => {
if (!item || !item.id) return;
const media_type = item.media_type || (item.name ? 'tv' : 'movie');
const url = media_type === 'movie' ? `https://www.themoviedb.org/movie/${item.id}` : `https://www.themoviedb.org/tv/${item.id}`;
merge.push({
title: item.title || item.name || '未知作品',
type: media_type === 'movie' ? '电影' : media_type === 'tv' ? '电视剧' : '未知',
year: item.release_date?.split('-')[0] || item.first_air_date?.split('-')[0] || '未知',
source: 'TMDB',
id: item.id,
url,
poster: item.poster_path ? `${config.TMDB.IMAGE_BASE_URL}${config.TMDB.LIST_POSTER_SIZE}/${item.poster_path}` : ''
});
};
// 多语言 multi 结果
for (const a of arr.slice(0, reqs.length)) {
const results = safeGet(a, 'results', []);
results.forEach(pushItem);
}
// 关键词 → 再拉一轮 movie/tv 搜索(更全)
const kw = safeGet(arr[reqs.length], 'results', []);
const kwId = (kw && kw[0] && kw[0].id) ? kw[0].id : null;
if (kwId) {
const byKw = await Promise.all([
send(`${config.TMDB.BASE_URL}/discover/movie?api_key=${config.TMDB.API_KEY}&with_keywords=${kwId}&language=zh-CN`),
send(`${config.TMDB.BASE_URL}/discover/tv?api_key=${config.TMDB.API_KEY}&with_keywords=${kwId}&language=zh-CN`)
]);
(safeGet(byKw[0],'results',[])||[]).forEach(pushItem);
(safeGet(byKw[1],'results',[])||[]).forEach(pushItem);
}
const ranked = rankAndDedupResults(merge, query).slice(0, 30);
searchCache.set(cacheKey, { data: ranked, timestamp: Date.now() });
console.log('TMDB合并结果:', ranked.length);
resolve(ranked);
} catch (e) { console.error(e); resolve([]); }
});
});
}
// 搜索结果交互
function setupSearchInteractions() {
const searchInput = document.getElementById('search-movie');
const resultsContainer = document.getElementById('search-results');
const loadingIndicator = document.getElementById('search-loading');
const mediaUrlInput = document.getElementById('media-url');
let lazyLoadController = new AbortController();
if (!searchInput || !resultsContainer || !loadingIndicator || !mediaUrlInput) return;
searchInput.addEventListener('input', debounce(async function () {
const query = this.value.trim();
lastSearchQuery = query;
if (!query) {
hideSearchResults();
lastSearchResults = [];
abortLazyLoad();
return;
}
// 立即显示搜索中指示器
setSearchLoading(true);
console.log('开始搜索:', query);
// 重置搜索结果状态,确保第二次搜索能正常显示
lastSearchResults = [];
lastSearchQuery = query;
// 搜索开始时只淡出,不折叠,待结果到达后再展开
resultsContainer.classList.remove('show');
resultsContainer.style.visibility = 'hidden';
resultsContainer.style.opacity = '0';
abortLazyLoad();
lazyLoadController = new AbortController();
try {
// 优先搜索豆瓣(通常更快),然后搜索TMDB
const doubanPromise = searchDouban(query).catch(e => {
console.warn('豆瓣搜索失败:', e);
return [];
});
const tmdbPromise = searchTMDB(query).catch(e => {
console.warn('TMDB搜索失败:', e);
return [];
});
// 等待所有搜索结果完成,避免显示旧内容
const [doubanResult, tmdbResult] = await Promise.all([
doubanPromise,
tmdbPromise
]);
setSearchLoading(false);
// 合并、排序、去重,并扩大上限到60条
const uniqueResults = rankAndDedupResults([...(doubanResult||[]), ...(tmdbResult||[])], query).slice(0, 60);
// 确保显示最新搜索结果
displaySearchResults(uniqueResults, resultsContainer);
} catch (e) {
console.error('搜索出错:', e);
setSearchLoading(false);
resultsContainer.innerHTML = '搜索出错,请检查网络连接后重试
';
// 使用统一的动画函数
animateShow(resultsContainer);
}
}, 500));
function abortLazyLoad() {
if (lazyLoadController) {
lazyLoadController.abort();
}
}
// 不再因输入框失焦自动隐藏结果,交由外部点击空白处或主动选择项来隐藏
searchInput.addEventListener('focus', function () {
const query = this.value.trim();
if (query && lastSearchResults.length > 0) {
// 如果有搜索结果,使用统一的显示函数
showSearchResults();
} else if (query && (lastSearchResults.length === 0 || lastSearchQuery !== query)) {
// 如果有查询但没有结果或查询不同,触发搜索
const inputEvent = new Event('input', { bubbles: true });
this.dispatchEvent(inputEvent);
}
});
resultsContainer.addEventListener('click', async function (event) {
// 确保容器有内容且可见时才响应点击
if (resultsContainer.innerHTML.trim() === '' ||
resultsContainer.style.display === 'none' ||
resultsContainer.style.visibility === 'hidden') {
return;
}
const targetItem = event.target.closest('.search-item');
if (targetItem) {
const url = targetItem.getAttribute('data-url');
const type = targetItem.getAttribute('data-type');
const title = targetItem.querySelector('strong').textContent;
if (url) {
// 添加加载状态
targetItem.classList.add('loading');
mediaUrlInput.value = url;
hideSearchResults();
searchInput.blur();
const fetchBtn = document.getElementById('fetch-btn');
if (fetchBtn) fetchBtn.classList.remove('active'); // 使用active类而不是display属性
showStatus(`正在加载【${type}】${title}的信息...`, false);
try {
currentMovieInfo = await getBasicInfo(url);
currentComments = await getHotComments(url);
showStatus('信息加载完成,请选择海报和剧照', false);
await showImageSelection(currentMovieInfo);
} catch (err) {
showStatus(`加载失败:${err.message}`, true);
if (mediaUrlInput.value.trim() && fetchBtn) {
fetchBtn.classList.add('active'); // 使用active类而不是display属性
}
}
}
}
});
// 点击面板外部区域才隐藏;点击面板内部的其它控件不自动隐藏
document.addEventListener('click', function (event) {
const panel = document.getElementById('douban-tmdb-panel');
if (panel && panel.contains(event.target)) return; // 面板内部不隐藏
if (!searchInput.contains(event.target) && !resultsContainer.contains(event.target)) {
hideSearchResults();
}
});
// 智能保持搜索结果框显示的函数
function forceKeepSearchResultsVisible() {
const query = searchInput.value.trim();
// 只有在有查询内容、有搜索结果、且搜索框有焦点时才显示
if (query && lastSearchResults.length > 0 &&
(document.activeElement === searchInput || resultsContainer.style.display === 'block')) {
showSearchResults();
// 搜索结果已内嵌到控制面板中,无需位置调整
}
}
// 专门处理窗口缩放时的显示逻辑 - 只在搜索框有焦点时保持显示
function handleResizeDisplay() {
const query = searchInput.value.trim();
const resultsContainer = document.getElementById('search-results');
const loadingIndicator = document.getElementById('search-loading');
// 强制保持搜索中指示器显示
if (loadingIndicator && (loadingIndicator.style.display === 'block' || loadingIndicator.style.display === '')) {
resetSearchLoadingPosition();
}
// 只在搜索框有焦点时保持搜索结果框显示
if (query && lastSearchResults.length > 0 && resultsContainer && document.activeElement === searchInput) {
// 使用强制样式设置,确保不会消失
resultsContainer.style.setProperty('display', 'block', 'important');
resultsContainer.style.setProperty('visibility', 'visible', 'important');
resultsContainer.style.setProperty('opacity', '1', 'important');
resultsContainer.style.setProperty('z-index', '800', 'important');
resultsContainer.classList.add('show');
// 搜索结果已内嵌到控制面板中,无需位置调整
}
}
// 强制保持搜索结果框显示的函数 - 修复版本
function forceKeepSearchResultsVisible() {
const query = searchInput.value.trim();
const resultsContainer = document.getElementById('search-results');
// 只有在有查询内容、有搜索结果、且搜索框有焦点时才保持显示
if (query && lastSearchResults.length > 0 && resultsContainer &&
(document.activeElement === searchInput || resultsContainer.style.display === 'block')) {
// 强制保持显示状态
if (resultsContainer.style.display !== 'block') {
resultsContainer.style.setProperty('display', 'block', 'important');
}
if (resultsContainer.style.visibility !== 'visible') {
resultsContainer.style.setProperty('visibility', 'visible', 'important');
}
if (resultsContainer.style.opacity !== '1') {
resultsContainer.style.setProperty('opacity', '1', 'important');
}
} else if (!query || lastSearchResults.length === 0) {
// 如果没有查询内容或没有搜索结果,确保隐藏
if (resultsContainer && resultsContainer.style.display !== 'none') {
resultsContainer.style.setProperty('display', 'none', 'important');
resultsContainer.style.setProperty('visibility', 'hidden', 'important');
resultsContainer.style.setProperty('opacity', '0', 'important');
}
}
}
// 移除定期检查,避免阻止正常的失焦隐藏
// setInterval(forceKeepSearchResultsVisible, 1000);
// 智能检查机制:只在窗口大小变化时触发,避免阻止失焦隐藏
let lastWindowSize = { width: window.innerWidth, height: window.innerHeight };
let resizeCheckInterval = setInterval(() => {
const currentSize = { width: window.innerWidth, height: window.innerHeight };
// 只有在窗口大小真正发生变化时才检查显示
if (currentSize.width !== lastWindowSize.width || currentSize.height !== lastWindowSize.height) {
lastWindowSize = currentSize;
handleResizeDisplay();
}
}, 500); // 每500ms检查一次窗口大小变化,减少性能影响
// 超级窄窗口过渡效果增强
let resizeTimeout;
let isUltraNarrow = false;
function handleUltraNarrowTransition() {
const panel = document.getElementById('douban-tmdb-panel');
if (!panel) return;
const windowWidth = window.innerWidth;
const wasUltraNarrow = isUltraNarrow;
isUltraNarrow = windowWidth <= 10;
if (isUltraNarrow && !wasUltraNarrow) {
// 进入超级窄模式
panel.style.transition = 'all 0.5s cubic-bezier(0.4, 0, 0.2, 1)';
panel.style.transform = 'scale(0.1)';
panel.style.transformOrigin = 'top left';
panel.style.overflow = 'hidden';
// 添加特殊类名用于CSS选择器
panel.classList.add('ultra-narrow');
// 显示极简指示器
const indicator = document.createElement('div');
indicator.id = 'ultra-narrow-indicator';
indicator.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 10px;
height: 10px;
background: linear-gradient(135deg, #ec4899 0%, #be185d 100%);
color: white;
font-size: 8px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 2px;
z-index: 1001; /* 搜索中指示器 - 最高层级 */
transition: all 0.3s ease;
`;
indicator.textContent = '🎬';
document.body.appendChild(indicator);
} else if (!isUltraNarrow && wasUltraNarrow) {
// 退出超级窄模式
panel.style.transition = 'all 0.5s cubic-bezier(0.4, 0, 0.2, 1)';
panel.style.transform = 'scale(1)';
panel.style.transformOrigin = 'top left';
panel.style.overflow = 'visible';
// 移除特殊类名
panel.classList.remove('ultra-narrow');
// 移除极简指示器
const indicator = document.getElementById('ultra-narrow-indicator');
if (indicator) {
indicator.remove();
}
}
}
// 监听窗口大小变化,同时处理搜索结果框和超级窄窗口过渡
window.addEventListener('resize', function() {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
// 处理搜索结果框位置 - 只在有焦点时保持显示
const searchInput = document.getElementById('search-movie');
const resultsContainer = document.getElementById('search-results');
if (searchInput && resultsContainer && document.activeElement === searchInput) {
const query = searchInput.value.trim();
if (query && lastSearchResults.length > 0) {
showSearchResults();
}
}
// 处理窗口缩放时的显示
handleResizeDisplay();
// 处理超级窄窗口过渡
handleUltraNarrowTransition();
// 搜索中提示位置已在setSearchLoading中处理
}, 50); // 统一延迟时间
});
// 初始化检查
handleUltraNarrowTransition();
// 初始化时搜索中提示位置将在需要时自动校准
}
// 影视信息提取(增强版)
function getBasicInfo(url) {
return new Promise((resolve, reject) => {
// Douban API 回退:使用移动端 rexxar 接口规避“请证明你是人类”
async function fetchDoubanDetailViaApi(id, isTv) {
try {
const apiUrl = `https://m.douban.com/rexxar/api/v2/${isTv ? 'tv' : 'movie'}/${id}?for_mobile=1`;
const json = await new Promise((resv, rej)=>{
GM_xmlhttpRequest({
method: 'GET',
url: apiUrl,
headers: {
...COMMON_HEADERS,
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
'Referer': `https://m.douban.com/${isTv ? 'tv' : 'movie'}/subject/${id}/`
},
timeout: 8000,
onload: (r)=>{ try { resv(JSON.parse(r.responseText)); } catch(e){ rej(e);} },
onerror: ()=>rej(new Error('豆瓣API请求失败')),
ontimeout: ()=>rej(new Error('豆瓣API请求超时'))
});
});
// 字段映射
const title = json.title || '';
const originalTitle = json.original_title || '';
const genreTags = (json.genres || []).slice(0, 8);
const year = (json.year || '').toString() || '未知';
const alsoKnown = (json.aka || []).join(' / ');
const director = (json.directors || []).map(d=>d.name).join(' / ') || '未知';
const writer = (json.writers || []).map(w=>w.name).join(' / ') || '未知';
const actor = (json.actors || []).map(a=>a.name).join(',') || '未知';
const region = (json.countries || []).join('、') || '未知';
const lang = (json.languages || []).join('、') || '未知';
const release = (json.pubdate || json.release_date || '') || (isTv ? '未知首播时间' : '未知上映时间');
const rating = (json.rating && (json.rating.value || json.rating.average)) ? (json.rating.value || json.rating.average).toFixed ? (json.rating.value || json.rating.average).toFixed(1) : (json.rating.value || json.rating.average) : '暂无';
const voteCount = (json.rating && (json.rating.count || json.rating.votes)) || '0';
const doubanId = id;
const imdbId = (json.imdb || (json.extra && json.extra.imdb)) || '暂无';
const runtime = (json.durations && json.durations[0]) || (json.duration ? `${json.duration}分钟` : (isTv ? '未知集数' : '未知片长'));
const intro = json.intro || json.card_subtitle || '';
return {
mediaType: isTv ? 'tv' : 'movie',
source: '豆瓣',
title, originalTitle, genreTags, year, alsoKnown, director, writer, actor,
region, release, lang, rating, voteCount, doubanId, imdbId, runtime,
intro, awards: [], posterUrls: [], stillUrls: [], url
};
} catch (e) {
throw e;
}
}
// 重试函数,带指数退避
const retryRequest = async (requestFn, maxRetries = 2, delay = 1000) => {
let lastError;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
if (attempt > 0) {
await new Promise(resolve => setTimeout(resolve, delay * Math.pow(2, attempt - 1)));
}
return await requestFn();
} catch (error) {
lastError = error;
console.warn(`请求失败,尝试 ${attempt + 1}/${maxRetries + 1}:`, error);
}
}
throw lastError || new Error('所有重试均失败');
};
const urlObj = new URL(url);
if (url.includes('douban.com')) {
let isTv = url.includes('tv.douban.com');
const headers = {
...COMMON_HEADERS,
'Referer': 'https://movie.douban.com/',
'Host': urlObj.hostname,
'Sec-Fetch-Dest': 'document'
};
retryRequest(() => {
return new Promise((innerResolve, innerReject) => {
doubanRequest({
method: 'GET',
url: url,
headers: headers,
timeout: 10000
}).then(async (res) => {
try {
// 若被重定向到登录/检测页,或页面出现“请证明你是人类/嗯...”,则回退API
if ((res.finalUrl && /\/accounts\.|\/j\/app\/user\/check/.test(res.finalUrl)) ||
/请证明你是人类|你的访问豆瓣的方式有点像机器人|嗯\s*\.\.\./.test(res.responseText)) {
const fallback = await (async ()=>{
try {
const list = await searchDoubanSuggest((currentMovieInfo && currentMovieInfo.title) || '')
.catch(()=>[]);
return Array.isArray(list) && list.length ? list[0] : null;
} catch(e){ return null; }
})();
if (fallback && (fallback.id || fallback.url)) {
const id = fallback.id || (fallback.url.match(/subject\/(\d+)/)||[])[1];
if (id) {
try { const viaApi = await fetchDoubanDetailViaApi(id, isTv); innerResolve(viaApi); return; } catch(_){}
}
innerResolve({ mediaType: isTv ? 'tv' : 'movie', source: '豆瓣', title: fallback.title, originalTitle: '', genreTags: [], year: fallback.year || '未知', alsoKnown: '', director: '未知', writer: '未知', actor: '未知', region: '未知', release: '未知', lang: '', rating: '暂无', voteCount: '0', doubanId: id || '', imdbId: '暂无', runtime: isTv ? '未知集数' : '未知片长', intro: '', awards: [], posterUrls: [], stillUrls: [], url });
} else {
throw new Error('豆瓣需要登录,且回退失败');
}
return;
}
const doc = new DOMParser().parseFromString(res.responseText, 'text/html');
const titleElem = doc.querySelector('h1 span[property="v:itemreviewed"], h1 span[itemprop="name"]');
const title = titleElem ? titleElem.textContent.trim() : (isTv ? '未知电视剧' : '未知电影');
const originalTitle = doc.querySelector('h1 .year')?.previousSibling?.textContent?.trim()?.replace(/\s+/g, ' ') || '';
const genreTags = Array.from(doc.querySelectorAll('span[property="v:genre"]')).map(g => g.textContent.trim()).filter(Boolean);
let year = '未知';
const yearElem = doc.querySelector('span[property="v:initialReleaseDate"]');
if (yearElem) {
const yearMatch = yearElem.textContent.trim().match(/\d{4}/);
year = yearMatch ? yearMatch[0] : '未知';
}
// 提取更多信息的函数
const extractInfo = (label) => {
const infoText = Array.from(doc.querySelectorAll('#info')).find(info =>
info.textContent.includes(label)
)?.textContent || '';
const match = infoText.match(new RegExp(`${label}[::]\s*(.+?)\n`));
return match ? match[1].replace(/\s+/g, ' ').trim() : '未知';
};
const alsoKnown = doc.querySelector('span[property="v:alternative"]')?.textContent.trim() || '';
const director = Array.from(doc.querySelectorAll('a[rel="v:directedBy"]')).map(d => d.textContent.trim()).join(' / ') || '未知';
const writer = extractInfo('编剧');
const actor = Array.from(doc.querySelectorAll('a[rel="v:starring"]')).map(a => a.textContent.trim()).join(',') || '未知';
const region = extractInfo('制片国家/地区');
const lang = extractInfo('语言');
const release = yearElem?.textContent.trim() || (isTv ? '未知首播时间' : '未知上映时间');
const rating = doc.querySelector('strong[property="v:average"]')?.textContent || '暂无';
const voteCount = doc.querySelector('span[property="v:votes"]')?.textContent || '0';
const doubanId = url.match(/subject\/(\d+)/)?.[1] || '未知';
const imdbId = doc.querySelector('a[href*="imdb.com/title/"]')?.href?.match(/tt\d+/)?.[0] || '暂无';
const runtime = isTv
? doc.querySelector('span[property="v:episodeCount"]')
? `共${doc.querySelector('span[property="v:episodeCount"]').textContent}集`
: '未知集数'
: doc.querySelector('span[property="v:runtime"]')?.textContent.trim() || '未知片长';
// 提取更完整的简介
let intro = '';
const summaryElem = doc.querySelector('span[property="v:summary"]');
if (summaryElem) {
intro = summaryElem.textContent.trim().replace(/\s+/g, ' ');
}
if (!intro) {
// 尝试其他可能的简介位置
const otherIntroElem = doc.querySelector('.all.hidden');
if (otherIntroElem) {
intro = otherIntroElem.textContent.trim().replace(/\s+/g, ' ');
}
}
if (!intro) {
intro = isTv ? '暂无电视剧简介' : '暂无电影简介';
}
// 尝试提取奖项信息
let awards = [];
const awardsElem = doc.querySelector('#celebrities');
if (awardsElem && awardsElem.textContent.includes('获奖')) {
const awardTexts = Array.from(awardsElem.querySelectorAll('.award')).map(a => a.textContent.trim());
if (awardTexts.length > 0) {
awards = awardTexts.slice(0, 3); // 最多取3个主要奖项
}
}
innerResolve({
mediaType: isTv ? 'tv' : 'movie',
source: '豆瓣',
title,
originalTitle: originalTitle || '',
genreTags,
year,
alsoKnown,
director,
writer,
actor,
region,
release,
lang,
rating,
voteCount,
doubanId,
imdbId,
runtime,
intro,
awards,
// 延迟加载:图片在选择面板内部异步加载,提升首屏弹出速度
posterUrls: [],
stillUrls: [],
url // 保存原始URL用于后续加载更多
});
} catch (e) {
innerReject(new Error(`豆瓣解析失败:${e.message}`));
}
}).catch(() => innerReject(new Error('豆瓣请求失败')));
});
}).then(result => resolve(result)).catch(error => reject(error));
} else if (url.includes('themoviedb.org')) {
const isMovie = url.includes('/movie/');
const isTv = url.includes('/tv/');
let mediaType = isMovie ? 'movie' : (isTv ? 'tv' : 'movie');
const idMatch = url.match(/\/(movie|tv)\/(\d+)/);
if (!idMatch) {
reject(new Error('TMDB链接格式错误(需包含/movie/或/tv/及数字ID)'));
return;
}
const [, type, id] = idMatch;
mediaType = type;
// 请求更多信息,包含额外数据
const config = getConfig();
const tmdbDetailUrl = `${config.TMDB.BASE_URL}/${mediaType}/${id}?api_key=${config.TMDB.API_KEY}&language=zh-CN&append_to_response=credits,keywords,release_dates,external_ids`;
retryRequest(() => {
return new Promise((innerResolve, innerReject) => {
GM_xmlhttpRequest({
method: 'GET',
url: tmdbDetailUrl,
headers: { 'Authorization': `Bearer ${config.TMDB.ACCESS_TOKEN}`, 'Content-Type': 'application/json' },
onload: async (res) => {
try {
const data = JSON.parse(res.responseText);
const title = mediaType === 'movie' ? data.title : data.name;
const originalTitle = mediaType === 'movie' ? data.original_title : data.original_name;
const genreTags = data.genres.map(g => g.name);
const year = mediaType === 'movie'
? data.release_date?.split('-')[0]
: data.first_air_date?.split('-')[0];
const alsoKnown = data.also_known_as?.join(' / ') || '';
const director = mediaType === 'movie'
? (data.credits?.crew?.filter(c => c.job === 'Director').map(d => d.name).join(' / ') || '未知')
: (data.credits?.crew?.filter(c => c.job === 'Director' || c.job === 'Executive Producer').map(d => d.name).join(' / ') || '未知');
const writer = data.credits?.crew?.filter(c => c.job === 'Writer' || c.job === 'Screenplay').map(w => w.name).join(' / ') || '未知';
const actor = (data.credits?.cast || []).map(a => a.name).join(',') || '未知';
const region = (data.production_countries || []).map(c => c.name).join('、') || '未知';
const release = mediaType === 'movie' ? data.release_date : data.first_air_date;
const lang = (data.spoken_languages || []).map(l => l.name).join('、') || '未知';
const rating = data.vote_average ? data.vote_average.toFixed(1) : '暂无';
const voteCount = data.vote_count || 0;
const tmdbId = id;
const imdbId = data.imdb_id || '暂无';
const runtime = mediaType === 'movie'
? `${data.runtime}分钟`
: `${data.number_of_episodes || '未知'}集(共${data.number_of_seasons || '未知'}季)`;
const intro = data.overview || (mediaType === 'tv' ? '暂无电视剧简介' : '暂无电影简介');
// 获取关键字
const keywords = data.keywords?.keywords?.map(k => k.name).join('、') || '';
// 获取预算和票房(电影)
let budget = '未知';
let revenue = '未知';
if (mediaType === 'movie') {
budget = data.budget > 0 ? `$${(data.budget / 1000000).toFixed(1)}M` : '未知';
revenue = data.revenue > 0 ? `$${(data.revenue / 1000000).toFixed(1)}M` : '未知';
}
// 获取流媒体平台信息
let streamingPlatforms = [];
const watchProvidersUrl = `${config.TMDB.BASE_URL}/${mediaType}/${id}/watch/providers?api_key=${config.TMDB.API_KEY}`;
try {
const providersData = await new Promise(resolve => {
GM_xmlhttpRequest({
method: 'GET',
url: watchProvidersUrl,
headers: { 'Authorization': `Bearer ${config.TMDB.ACCESS_TOKEN}`, 'Content-Type': 'application/json' },
onload: (res) => resolve(JSON.parse(res.responseText)),
onerror: () => resolve({}),
ontimeout: () => resolve({})
});
});
// 获取中国或美国的流媒体平台
const watchProviders = providersData.results?.CN || providersData.results?.US || {};
streamingPlatforms = [
...(watchProviders.flatrate || []).map(p => p.provider_name),
...(watchProviders.buy || []).map(p => p.provider_name)
].slice(0, 5); // 最多取5个平台
} catch (e) {
console.log('获取流媒体平台失败:', e);
}
// 初始加载海报和剧照
posterPage = 1;
let posterUrls = [];
if (data.poster_path) {
const postersUrl = `${config.TMDB.BASE_URL}/${mediaType}/${id}/images?api_key=${config.TMDB.API_KEY}&include_image_language=zh,en&image_type=poster&sort_by=primary`;
try {
const posterData = await new Promise(resolve => {
GM_xmlhttpRequest({
method: 'GET',
url: postersUrl,
headers: { 'Authorization': `Bearer ${config.TMDB.ACCESS_TOKEN}`, 'Content-Type': 'application/json' },
onload: (res) => resolve(JSON.parse(res.responseText)),
onerror: () => resolve({}),
ontimeout: () => resolve({})
});
});
// 按第1页截取(5张)
const pagePosters = safeGet(posterData, 'posters', [])
.slice((posterPage - 1) * config.TMDB.IMAGE_CANDIDATES_COUNT, posterPage * config.TMDB.IMAGE_CANDIDATES_COUNT)
.map(img => `${config.TMDB.IMAGE_BASE_URL}${config.TMDB.LIST_POSTER_SIZE}/${img.file_path}`)
.filter(Boolean);
posterUrls = pagePosters;
} catch (e) {
console.log('获取更多海报失败:', e);
}
}
// 【统一逻辑】初始加载第1页剧照(5张)
stillPage = 1;
const stillUrls = await retryRequest(() => getTMDBStillsList(mediaType, id));
innerResolve({
mediaType,
source: 'TMDB',
title,
originalTitle,
genreTags,
year,
alsoKnown,
director,
writer,
actor,
region,
release,
lang,
rating,
voteCount,
tmdbId,
imdbId,
runtime,
intro,
keywords,
budget,
revenue,
streamingPlatforms,
posterUrls: posterUrls.length > 0 ? posterUrls : [],
stillUrls: stillUrls.length > 0 ? stillUrls : [],
url // 保存原始URL用于后续加载更多
});
} catch (e) {
innerReject(new Error(`TMDB解析失败:${e.message}`));
}
},
onerror: () => innerReject(new Error('TMDB请求失败')),
ontimeout: () => innerReject(new Error('TMDB请求超时'))
});
});
}).then(result => resolve(result)).catch(error => reject(error));
} else {
reject(new Error('不支持的链接类型(仅支持豆瓣、TMDB)'));
}
});
}
function getHotComments(url) {
return new Promise(async resolve => {
// TMDB来源:改为走豆瓣短评页
if (url.includes('themoviedb.org')) {
try {
// 从currentMovieInfo获取豆瓣subject链接;若无,则以片名搜索豆瓣匹配
const info = currentMovieInfo || {};
let doubanId = info.doubanId;
if (!doubanId) {
try {
const title = info.title || info.originalTitle || '';
if (title) {
const list = await searchDouban(title).catch(() => []);
if (Array.isArray(list) && list.length) {
// 按年份或相似度粗匹配
const year = (info.release || '').slice(0,4);
const picked = list.find(r => (r.year && year && String(r.year) === String(year))) || list[0];
const idMatch = picked && picked.url ? picked.url.match(/subject\/(\d+)/) : null;
if (idMatch) doubanId = idMatch[1];
}
}
} catch (e) {}
}
const doubanLink = doubanId ? `https://movie.douban.com/subject/${doubanId}/comments?sort=new_score&status=P` : '';
if (!doubanLink) { resolve([]); return; }
doubanRequest({
method: 'GET', url: doubanLink,
headers: { ...COMMON_HEADERS, 'Referer': `https://movie.douban.com/subject/${doubanId}/`, 'Host': 'movie.douban.com' },
timeout: 8000
}).then((res)=>{
try {
const doc = new DOMParser().parseFromString(res.responseText, 'text/html');
const items = Array.from(doc.querySelectorAll('.comment-item')).slice(0, 5);
const comments = items.map(node => {
const short = node.querySelector('span.short');
const infoA = node.querySelector('.comment-info a');
const content = short ? short.textContent.trim() : '';
const author = infoA ? infoA.textContent.trim() : '';
return content ? { content, author } : null;
}).filter(Boolean).slice(0, 3);
resolve(comments.length ? comments : []);
} catch (e) { resolve([]); }
}).catch(()=>resolve([]));
} catch (e) { resolve([]); }
return;
}
// 优先“热门短评”页,其次默认短评页
const base = url.replace(/\/$/, '');
const commentUrl = `${base}/comments?sort=new_score&status=P`;
doubanRequest({
method: 'GET',
url: commentUrl,
headers: { ...COMMON_HEADERS, 'Referer': url, 'Host': new URL(url).hostname },
timeout: 8000
}).then((res)=>{
try {
const doc = new DOMParser().parseFromString(res.responseText, 'text/html');
// 兼容新旧结构:.comment-item > .comment/.comment-info + span.short
const items = Array.from(doc.querySelectorAll('.comment-item')).slice(0, 5);
const comments = items.map(node => {
const short = node.querySelector('span.short');
const info = node.querySelector('.comment-info a');
const content = short ? short.textContent.trim() : '';
const author = info ? info.textContent.trim() : '';
return content ? { content, author } : null;
}).filter(Boolean).slice(0, 3);
resolve(comments.length ? comments : []);
} catch (e) {
resolve([]);
}
}).catch(()=>resolve([]));
});
}
// 加载更多海报功能(修复点击无反应+统一分页逻辑)
// 存储已加载的海报和剧照的唯一标识符,用于去重
const loadedPosterIds = new Set();
const loadedStillIds = new Set();
// 获取图片的唯一标识符(基于URL的哈希值)
function getImageUniqueId(url) {
try {
// 从URL中提取有辨识度的部分用于生成唯一ID
let id = url;
// 移除查询参数
const urlObj = new URL(url);
id = urlObj.origin + urlObj.pathname;
// 移除可能变化的尺寸部分(针对TMDB和豆瓣)
id = id.replace(/w\d+/g, '').replace(/h\d+/g, '').replace(/\/m\//g, '/').replace(/\/l\//g, '/').replace(/\/s\//g, '/');
// 使用简单的哈希算法生成短ID
let hash = 0;
for (let i = 0; i < id.length; i++) {
const char = id.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // 转换为32位整数
}
return Math.abs(hash).toString(36);
} catch (e) {
console.warn('生成图片唯一ID失败:', e);
// 回退方案:返回URL的基本部分
return url.split('?')[0].split('#')[0];
}
}
async function loadMorePosters() {
if (isLoadingPosters) {
showStatus('正在加载海报,请稍候...', false);
return;
}
if (!currentMovieInfo) {
showStatus('未找到影视信息,请重新加载', true);
return;
}
isLoadingPosters = true;
const loadMoreBtn = document.getElementById('load-more-posters');
if (loadMoreBtn) {
loadMoreBtn.textContent = '加载中...';
loadMoreBtn.disabled = true;
}
try {
const prevPage = posterPage;
posterPage++; // 页码递增(加载下一页)
let morePosters = [];
if (currentMovieInfo.source === '豆瓣') {
morePosters = await getDoubanOfficialPosters(currentMovieInfo.url, posterPage);
} else if (currentMovieInfo.source === 'TMDB' && currentMovieInfo.tmdbId) {
const config = getConfig();
const postersUrl = `${config.TMDB.BASE_URL}/${currentMovieInfo.mediaType}/${currentMovieInfo.tmdbId}/images?api_key=${config.TMDB.API_KEY}&include_image_language=zh,en&image_type=poster&sort_by=primary`;
await new Promise(resolvePosters => {
GM_xmlhttpRequest({
method: 'GET',
url: postersUrl,
headers: { 'Authorization': `Bearer ${config.TMDB.ACCESS_TOKEN}`, 'Content-Type': 'application/json' },
onload: (res) => {
try {
const posterData = JSON.parse(res.responseText);
// 按当前页截取(5张),避免重复
morePosters = safeGet(posterData, 'posters', [])
.slice((posterPage - 1) * config.TMDB.IMAGE_CANDIDATES_COUNT, posterPage * config.TMDB.IMAGE_CANDIDATES_COUNT)
.map(img => `${config.TMDB.IMAGE_BASE_URL}${config.TMDB.LIST_POSTER_SIZE}/${img.file_path}`)
.filter(Boolean);
} catch (e) {
console.log('获取更多海报失败:', e);
}
resolvePosters();
},
onerror: () => resolvePosters(),
ontimeout: () => resolvePosters()
});
});
}
// 验证是否加载到新数据(避免重复加载空数据)
if (morePosters.length > 0) {
posterContainer.style.display = 'grid';
// 过滤掉已加载的海报
const uniquePosters = morePosters.filter(posterUrl => {
const posterId = getImageUniqueId(posterUrl);
if (loadedPosterIds.has(posterId)) {
console.log('跳过重复海报:', posterUrl);
return false;
}
return true;
});
if (uniquePosters.length === 0) {
// 没有新的唯一海报
posterPage = prevPage;
showStatus('没有更多新的海报了', false);
if (loadMoreBtn) {
loadMoreBtn.textContent = '没有更多海报了';
loadMoreBtn.disabled = true;
loadMoreBtn.style.opacity = '0.6';
}
return;
}
let addedCount = 0;
for (let i = 0; i < uniquePosters.length; i++) {
try {
const dataUrl = await getImageDataURLWithQuality(uniquePosters[i]);
const posterImg = document.createElement('div');
// 统一海报样式:适应grid布局,避免超出,添加hover效果
posterImg.style.cssText = `
width: 100%; height: 200px; object-fit: contain;
border: 1px solid #f3d5d9;
border-radius: 8px; cursor: pointer; overflow: hidden;
background: #fff5f7; display: flex; align-items: center; justify-content: center;
transition: all 0.3s ease;
position: relative;
`;
// 添加hover效果的样式
posterImg.onmouseenter = function() {
this.style.transform = 'scale(1.02)';
this.style.boxShadow = '0 4px 12px rgba(236, 72, 153, 0.3)';
};
posterImg.onmouseleave = function() {
this.style.transform = 'scale(1)';
this.style.boxShadow = 'none';
};
const config = getConfig();
posterImg.innerHTML = `
`;
posterImg.dataset.url = dataUrl;
// 标记此海报已加载
const posterId = getImageUniqueId(uniquePosters[i]);
loadedPosterIds.add(posterId);
posterImg.dataset.posterId = posterId;
posterContainer.appendChild(posterImg);
addedCount++;
} catch (e) {
console.log(`加载海报 ${i + 1} 失败:`, e);
}
}
if (addedCount === 0) {
// 虽然有唯一海报,但加载全部失败
posterPage = prevPage;
showStatus('加载海报失败,请稍后重试', true);
} else {
// 滚动到底部,显示新加载的海报
if (posterContainer) {
posterContainer.scrollTop = posterContainer.scrollHeight;
}
showStatus(`已加载第${posterPage}页海报(新增${addedCount}张)`, false);
if (loadMoreBtn) {
loadMoreBtn.textContent = '加载更多海报';
loadMoreBtn.disabled = false;
}
}
} else {
// 无新数据,恢复页码并禁用按钮
posterPage = prevPage;
posterContainer.style.display = 'grid';
if (loadMoreBtn) {
loadMoreBtn.textContent = '没有更多海报了';
loadMoreBtn.disabled = true;
loadMoreBtn.style.opacity = '0.6';
}
showStatus('已加载全部海报', false);
}
} catch (e) {
// 加载失败,恢复页码
posterPage--;
console.error('加载更多海报出错:', e);
showStatus('加载更多海报失败,请稍后重试', true);
if (loadMoreBtn) {
loadMoreBtn.textContent = '加载失败,重试';
loadMoreBtn.disabled = false;
}
} finally {
isLoadingPosters = false;
}
}
// 加载更多剧照功能(修复点击无反应+统一分页逻辑+样式适配)
async function loadMoreStills() {
if (isLoadingStills) {
showStatus('正在加载剧照,请稍候...', false);
return;
}
if (!currentMovieInfo) {
showStatus('未找到影视信息,请重新加载', true);
return;
}
isLoadingStills = true;
const loadMoreBtn = document.getElementById('load-more-stills');
if (loadMoreBtn) {
loadMoreBtn.textContent = '加载中...';
loadMoreBtn.disabled = true;
}
try {
const prevPage = stillPage;
stillPage++; // 页码递增(加载下一页)
let moreStills = [];
if (currentMovieInfo.source === '豆瓣') {
moreStills = await getDoubanStillsList(currentMovieInfo.url, stillPage);
} else if (currentMovieInfo.source === 'TMDB' && currentMovieInfo.tmdbId) {
moreStills = await getTMDBStillsList(currentMovieInfo.mediaType, currentMovieInfo.tmdbId, stillPage);
}
// 验证是否加载到新数据(避免重复加载空数据)
if (moreStills.length > 0) {
stillContainer.style.display = 'grid';
// 过滤掉已加载的剧照
const uniqueStills = moreStills.filter(stillUrl => {
const stillId = getImageUniqueId(stillUrl);
if (loadedStillIds.has(stillId)) {
console.log('跳过重复剧照:', stillUrl);
return false;
}
return true;
});
if (uniqueStills.length === 0) {
// 没有新的唯一剧照
stillPage = prevPage;
showStatus('没有更多新的剧照了', false);
if (loadMoreBtn) {
loadMoreBtn.textContent = '没有更多剧照了';
loadMoreBtn.disabled = true;
loadMoreBtn.style.opacity = '0.6';
}
return;
}
let addedCount = 0;
for (let i = 0; i < uniqueStills.length; i++) {
try {
const dataUrl = await getImageDataURLWithQuality(uniqueStills[i]);
const stillImg = document.createElement('div');
// 【修复剧照超出】统一剧照样式:适应grid布局,宽高比例协调,添加hover效果
stillImg.style.cssText = `
width: 100%; height: 120px; object-fit: contain;
border: 1px solid #f3d5d9;
border-radius: 8px; cursor: pointer; overflow: hidden;
background: #fff5f7; display: flex; align-items: center; justify-content: center;
transition: all 0.3s ease;
position: relative;
`;
// 添加hover效果的样式
stillImg.onmouseenter = function() {
this.style.transform = 'scale(1.02)';
this.style.boxShadow = '0 4px 12px rgba(236, 72, 153, 0.3)';
};
stillImg.onmouseleave = function() {
this.style.transform = 'scale(1)';
this.style.boxShadow = 'none';
};
const config = getConfig();
stillImg.innerHTML = `
`;
stillImg.dataset.url = dataUrl;
// 标记此剧照已加载
const stillId = getImageUniqueId(uniqueStills[i]);
loadedStillIds.add(stillId);
stillImg.dataset.stillId = stillId;
stillContainer.appendChild(stillImg);
addedCount++;
} catch (e) {
console.log(`加载剧照 ${i + 1} 失败:`, e);
}
}
if (addedCount === 0) {
// 虽然有唯一剧照,但加载全部失败
stillPage = prevPage;
showStatus('加载剧照失败,请稍后重试', true);
} else {
// 滚动到底部,显示新加载的剧照
if (stillContainer) {
stillContainer.scrollTop = stillContainer.scrollHeight;
}
showStatus(`已加载第${stillPage}页剧照(新增${addedCount}张)`, false);
if (loadMoreBtn) {
loadMoreBtn.textContent = '加载更多剧照';
loadMoreBtn.disabled = false;
}
}
} else {
// 无新数据,恢复页码并禁用按钮
stillPage = prevPage;
stillContainer.style.display = 'grid';
if (loadMoreBtn) {
loadMoreBtn.textContent = '没有更多剧照了';
loadMoreBtn.disabled = true;
loadMoreBtn.style.opacity = '0.6';
}
showStatus('已加载全部剧照', false);
}
} catch (e) {
// 加载失败,恢复页码
stillPage--;
console.error('加载更多剧照出错:', e);
showStatus('加载更多剧照失败,请稍后重试', true);
if (loadMoreBtn) {
loadMoreBtn.textContent = '加载失败,重试';
loadMoreBtn.disabled = false;
}
} finally {
isLoadingStills = false;
}
}
async function showImageSelection(movieInfo) {
return new Promise(async (resolve) => {
console.log('showImageSelection 被调用,movieInfo:', movieInfo);
console.log('posterUrls:', movieInfo.posterUrls);
console.log('stillUrls:', movieInfo.stillUrls);
if (!posterContainer || !stillContainer) {
posterContainer = document.getElementById('poster-candidates');
stillContainer = document.getElementById('still-candidates');
}
setupImageSelectionDelegates();
const imageSelection = document.getElementById('image-selection');
const loadMorePostersBtn = document.getElementById('load-more-posters');
const loadMoreStillsBtn = document.getElementById('load-more-stills');
if (!posterContainer || !stillContainer || !imageSelection) {
resolve();
return;
}
// 重置去重集合
loadedPosterIds.clear();
loadedStillIds.clear();
// 初始加载时显示"加载中"
posterContainer.style.display = 'grid';
posterContainer.innerHTML = '加载海报中...
';
// 确保剧照容器首屏可见(修复首次不显示)
stillContainer.style.display = 'grid';
stillContainer.innerHTML = '加载剧照中...
';
imageSelection.style.display = 'block';
loadMorePostersBtn.style.display = 'none';
loadMoreStillsBtn.style.display = 'none';
// 重建观察器,避免旧实例导致乱加载
disconnectCandidateObservers();
initCandidateObservers();
// 处理海报(统一样式+初始第1页)
if (movieInfo.posterUrls && movieInfo.posterUrls.length > 0) {
stillContainer.style.display = 'grid';
posterContainer.innerHTML = '';
// 首张不等待转码,先用直链,极大缩短首屏时间
selectedPosterUrl = normalizeImageUrl(movieInfo.posterUrls[0]);
// 批量创建DOM,减少重排
const posterFrag = document.createDocumentFragment();
for (let i = 0; i < movieInfo.posterUrls.length; i++) {
try {
// 异步并发处理:先创建卡片,图片加载完成后替换src,避免阻塞渲染
const rawUrl = normalizeImageUrl(movieInfo.posterUrls[i]);
const posterImg = document.createElement('div');
// 统一海报样式:适应grid布局,添加hover效果
posterImg.style.cssText = `
width: 100%; height: 200px; object-fit: contain;
border: ${i === 0 ? '3px solid #ec4899' : '1px solid #f3d5d9'};
border-radius: 8px; cursor: pointer; overflow: hidden;
background: #fff5f7; display: flex; align-items: center; justify-content: center;
transition: all 0.3s ease;
position: relative;
`;
// 添加hover效果的样式
posterImg.onmouseenter = function() {
this.style.transform = 'scale(1.02)';
this.style.boxShadow = '0 4px 12px rgba(236, 72, 153, 0.3)';
};
posterImg.onmouseleave = function() {
this.style.transform = 'scale(1)';
this.style.boxShadow = 'none';
};
// 创建图片元素(先用缩略图展示,提升首屏速度)
const img = document.createElement('img');
img.style.cssText = 'max-width:100%; max-height:100%; object-fit: contain;';
img.alt = `海报 ${i + 1}`;
try {
getThumbnailForDisplay(rawUrl).then((thumb)=>{ const im = posterImg.querySelector('img'); if (im) im.src = thumb; });
} catch(e) { img.src = LAZY_PLACEHOLDER; }
posterImg.appendChild(img);
posterImg.dataset.rawUrl = rawUrl;
posterImg.dataset.url = rawUrl;
// 设置点击选中行为(同步变量与高亮)
posterImg.addEventListener('click', function() {
try {
selectedPosterUrl = this.dataset.url || rawUrl;
selectedPosterEl = this;
document.querySelectorAll('#poster-candidates > div').forEach(el => {
el.style.border = el === this ? '3px solid #ec4899' : '1px solid #f3d5d9';
});
} catch (e) {}
});
// 标记此海报已加载
const posterId = getImageUniqueId(movieInfo.posterUrls[i]);
loadedPosterIds.add(posterId);
posterImg.dataset.posterId = posterId;
if (i === 0) {
selectedPosterEl = posterImg;
}
posterFrag.appendChild(posterImg);
// 懒加载观察
observeCandidateCard(posterImg, 'poster');
} catch (e) {
console.log(`加载海报 ${i + 1} 失败:`, e);
}
}
posterContainer.appendChild(posterFrag);
loadMorePostersBtn.style.display = 'inline-block';
loadMorePostersBtn.disabled = false;
} else {
// 海报保底机制:如果有剧照,使用第一张剧照作为海报
if (movieInfo.source === '豆瓣' && movieInfo.url) {
// 异步抓取首批海报,不阻塞面板弹出
posterContainer.innerHTML = '加载海报中…
';
(async () => {
try {
posterPage = 1;
const urls = await getDoubanOfficialPosters(movieInfo.url, 1);
if (Array.isArray(urls) && urls.length) {
movieInfo.posterUrls = urls;
posterContainer.innerHTML = '';
const frag = document.createDocumentFragment();
for (let i = 0; i < urls.length; i++) {
const rawUrl = normalizeImageUrl(urls[i]);
const card = document.createElement('div');
card.style.cssText = 'width: 100%; height: 200px; object-fit: contain; border: '+(i===0?'3px solid #ec4899':'1px solid #f3d5d9')+'; border-radius: 8px; cursor: pointer; overflow: hidden; background: #fff5f7; display: flex; align-items: center; justify-content: center; transition: all 0.3s ease; position: relative;';
const img = document.createElement('img');
img.style.cssText = 'max-width:100%; max-height:100%; object-fit: contain;';
img.alt = `海报 ${i + 1}`;
img.src = LAZY_PLACEHOLDER;
card.appendChild(img);
card.dataset.rawUrl = rawUrl;
card.dataset.url = rawUrl;
if (i === 0) { selectedPosterEl = card; selectedPosterUrl = rawUrl; }
card.addEventListener('click', function(){
try {
selectedPosterUrl = this.dataset.url || rawUrl;
selectedPosterEl = this;
document.querySelectorAll('#poster-candidates > div').forEach(el => { el.style.border = el === this ? '3px solid #ec4899' : '1px solid #f3d5d9'; });
} catch(e) {}
});
frag.appendChild(card);
observeCandidateCard(card, 'poster');
}
posterContainer.appendChild(frag);
loadMorePostersBtn.style.display = 'inline-block';
loadMorePostersBtn.disabled = false;
primeFirstCandidates();
} else {
posterContainer.innerHTML = '未找到海报
';
}
} catch (e) {
posterContainer.innerHTML = '海报加载失败
';
}
})();
} else if (movieInfo.stillUrls && movieInfo.stillUrls.length > 0) {
console.log('未找到海报,使用第一张剧照作为保底海报');
stillContainer.style.display = 'grid';
posterContainer.innerHTML = '';
const fallbackUrl = normalizeImageUrl(movieInfo.stillUrls[0]);
selectedPosterUrl = fallbackUrl;
// 创建保底海报显示
const fallbackPosterImg = document.createElement('div');
fallbackPosterImg.style.cssText = `
width: 100%; height: 200px;
border: 2px solid #f59e0b;
border-radius: 8px; cursor: pointer; overflow: hidden;
background: #fffbeb; display: flex; align-items: center; justify-content: center;
transition: all 0.3s ease; position: relative;
`;
// 添加保底标识
const badge = document.createElement('div');
badge.style.cssText = `
position: absolute; top: 5px; right: 5px;
background: #f59e0b; color: white; font-size: 10px;
padding: 2px 6px; border-radius: 4px; z-index: 1; font-weight: 500;
`;
badge.textContent = '首图';
const img = document.createElement('img');
img.src = fallbackUrl;
img.style.cssText = 'max-width:100%; max-height:100%; object-fit: contain;';
img.alt = '保底海报';
img.onerror = function() {
this.src = 'https://picsum.photos/200/300?default-poster';
};
fallbackPosterImg.appendChild(img);
fallbackPosterImg.appendChild(badge);
fallbackPosterImg.dataset.url = fallbackUrl;
// 添加点击选中效果
fallbackPosterImg.addEventListener('click', function() {
selectedPosterUrl = fallbackUrl;
document.querySelectorAll('#poster-candidates > div').forEach(el => {
el.style.border = el === this ? '3px solid #f59e0b' : '1px solid #f3d5d9';
});
});
posterContainer.appendChild(fallbackPosterImg);
loadMorePostersBtn.style.display = 'none';
} else {
posterContainer.innerHTML = '未找到海报
';
selectedPosterUrl = 'https://picsum.photos/200/300?default-poster';
}
}
// 处理剧照(统一样式+初始第1页+修复超出)
if (movieInfo.stillUrls && movieInfo.stillUrls.length > 0) {
stillContainer.innerHTML = '';
selectedStillUrl = normalizeImageUrl(movieInfo.stillUrls[0]);
const stillFrag = document.createDocumentFragment();
for (let i = 0; i < movieInfo.stillUrls.length; i++) {
try {
const rawUrl = normalizeImageUrl(movieInfo.stillUrls[i]);
const stillImg = document.createElement('div');
// 【修复剧照超出】统一剧照样式:适应grid布局,宽高比例协调,添加hover效果
stillImg.style.cssText = `
width: 100%; height: 120px; object-fit: contain;
border: ${i === 0 ? '3px solid #ec4899' : '1px solid #f3d5d9'};
border-radius: 8px; cursor: pointer; overflow: hidden;
background: #fff5f7; display: flex; align-items: center; justify-content: center;
transition: all 0.3s ease;
position: relative;
`;
// 添加hover效果的样式
stillImg.onmouseenter = function() {
this.style.transform = 'scale(1.02)';
this.style.boxShadow = '0 4px 12px rgba(236, 72, 153, 0.3)';
};
stillImg.onmouseleave = function() {
this.style.transform = 'scale(1)';
this.style.boxShadow = 'none';
};
// 创建图片元素(先用缩略图展示,提升首屏速度)
const img = document.createElement('img');
img.style.cssText = 'max-width:100%; max-height:100%; object-fit: contain;';
img.alt = `剧照 ${i + 1}`;
try {
getThumbnailForDisplay(rawUrl).then((thumb)=>{ const im = stillImg.querySelector('img'); if (im) im.src = thumb; });
} catch(e) { img.src = LAZY_PLACEHOLDER; }
stillImg.appendChild(img);
stillImg.dataset.rawUrl = rawUrl;
stillImg.dataset.url = rawUrl;
// 设置点击选中行为(同步变量与高亮)
stillImg.addEventListener('click', function() {
try {
selectedStillUrl = this.dataset.url || rawUrl;
selectedStillEl = this;
document.querySelectorAll('#still-candidates > div').forEach(el => {
el.style.border = el === this ? '3px solid #ec4899' : '1px solid #f3d5d9';
});
} catch (e) {}
});
// 标记此剧照已加载
const stillId = getImageUniqueId(movieInfo.stillUrls[i]);
loadedStillIds.add(stillId);
stillImg.dataset.stillId = stillId;
if (i === 0) {
selectedStillEl = stillImg;
}
stillFrag.appendChild(stillImg);
// 懒加载观察
observeCandidateCard(stillImg, 'still');
// 保障首图立刻显示:若是首图则立即触发加载
if (i === 0) {
try {
const raw = stillImg.dataset.rawUrl || rawUrl;
// 先展示缩略图,后台升级为原图dataURL
getThumbnailForDisplay(raw).then((thumb)=>{ const im = stillImg.querySelector('img'); if (im) im.src = thumb; });
const upgrade = shouldConvertToDataURL(raw)
? getImageDataURLWithQuality(raw)
: Promise.resolve(raw.replace(`/${getConfig().TMDB.LIST_STILL_SIZE}/`, `/${getConfig().TMDB.SELECTED_STILL_SIZE}/`));
upgrade.then((du)=>{
const im = stillImg.querySelector('img');
if (im) im.src = du;
stillImg.dataset.url = du;
stillImg.dataset.loaded = '1';
if (selectedStillUrl === raw) selectedStillUrl = du;
});
} catch (e) {}
}
} catch (e) {
console.log(`加载剧照 ${i + 1} 失败:`, e);
}
}
stillContainer.appendChild(stillFrag);
loadMoreStillsBtn.style.display = 'inline-block';
loadMoreStillsBtn.disabled = false;
} else {
if (movieInfo.source === '豆瓣' && movieInfo.url) {
stillContainer.innerHTML = '加载剧照中…
';
(async () => {
try {
stillPage = 1;
const urls = await getDoubanStillsList(movieInfo.url, 1);
if (Array.isArray(urls) && urls.length) {
movieInfo.stillUrls = urls;
stillContainer.innerHTML = '';
const frag = document.createDocumentFragment();
for (let i = 0; i < urls.length; i++) {
const rawUrl = normalizeImageUrl(urls[i]);
const card = document.createElement('div');
card.style.cssText = 'width: 100%; height: 120px; object-fit: contain; border: '+(i===0?'3px solid #ec4899':'1px solid #f3d5d9')+'; border-radius: 8px; cursor: pointer; overflow: hidden; background: #fff5f7; display: flex; align-items: center; justify-content: center; transition: all 0.3s ease; position: relative;';
const img = document.createElement('img');
img.style.cssText = 'max-width:100%; max-height:100%; object-fit: contain;';
img.alt = `剧照 ${i + 1}`;
img.src = LAZY_PLACEHOLDER;
card.appendChild(img);
card.dataset.rawUrl = rawUrl;
card.dataset.url = rawUrl;
if (i === 0) { selectedStillEl = card; selectedStillUrl = rawUrl; }
card.addEventListener('click', function(){
try {
selectedStillUrl = this.dataset.url || rawUrl;
selectedStillEl = this;
document.querySelectorAll('#still-candidates > div').forEach(el => { el.style.border = el === this ? '3px solid #ec4899' : '1px solid #f3d5d9'; });
} catch(e) {}
});
frag.appendChild(card);
observeCandidateCard(card, 'still');
// 保障首图立刻显示
if (i === 0) {
try {
if (shouldConvertToDataURL(rawUrl)) {
getImageDataURLWithQuality(rawUrl).then((du)=>{
const im = card.querySelector('img');
if (im) im.src = du;
card.dataset.url = du;
card.dataset.loaded = '1';
if (selectedStillUrl === rawUrl) selectedStillUrl = du;
});
} else {
const im = card.querySelector('img');
if (im) im.src = rawUrl;
card.dataset.url = rawUrl;
card.dataset.loaded = '1';
}
} catch (e) {}
}
}
stillContainer.appendChild(frag);
loadMoreStillsBtn.style.display = 'inline-block';
loadMoreStillsBtn.disabled = false;
primeFirstCandidates();
} else {
stillContainer.style.display = 'grid';
stillContainer.innerHTML = '未找到剧照
';
selectedStillUrl = 'https://picsum.photos/300/180?default-still';
loadMoreStillsBtn.style.display = 'none';
}
} catch (e) {
stillContainer.innerHTML = '剧照加载失败
';
}
})();
} else {
stillContainer.style.display = 'grid';
stillContainer.innerHTML = '未找到剧照
';
selectedStillUrl = 'https://picsum.photos/300/180?default-still';
loadMoreStillsBtn.style.display = 'none';
}
}
// 图片区域已完成:预热首图并延迟初始化模板工具栏,避免首屏阻塞
try {
primeFirstCandidates();
setTimeout(() => { try { initTemplateToolbar(); } catch (e) {} }, 300);
} catch (e) {}
resolve();
});
}
// 初始化美化工具
function initFormatTools() {
const buttonContainer = document.getElementById('format-buttons');
const categoryContainer = document.getElementById('format-categories');
const previewContainer = document.getElementById('format-preview');
const previewToggle = document.getElementById('format-preview-toggle');
if (!buttonContainer || !categoryContainer || !previewContainer || !previewToggle) return;
// 默认隐藏预览,避免初始化时渲染预览造成卡顿
try {
previewContainer.style.display = 'none';
previewToggle.textContent = '显示预览';
previewContainer.innerHTML = '预览已关闭,点击“显示预览”后再加载
';
} catch (e) {}
// 清空容器
buttonContainer.innerHTML = '';
categoryContainer.innerHTML = '';
// 获取所有唯一分类(稳定排序,避免reflow震荡)
const categories = [...new Set(FORMAT_STYLES.map(style => style.category))];
// 创建分类标签(但不触发样式渲染)
categories.forEach(category => {
const catBtn = document.createElement('div');
catBtn.textContent = category;
catBtn.style.cssText = `
padding:4px 10px; background:#fce7f3; color:#be185d; border-radius:6px;
font-size:12px; cursor:pointer; white-space:nowrap; transition: all 0.3s ease;
`;
// 添加hover效果
catBtn.addEventListener('mouseenter', () => {
if (catBtn.style.background !== 'rgb(236, 72, 153)') {
catBtn.style.background = '#fbcfe8';
catBtn.style.transform = 'translateY(-1px)';
}
});
catBtn.addEventListener('mouseleave', () => {
if (catBtn.style.background !== 'rgb(236, 72, 153)') {
catBtn.style.background = '#fce7f3';
catBtn.style.transform = 'translateY(0)';
}
});
// 默认选中第一个分类
if (category === categories[0]) {
catBtn.style.background = '#ec4899';
catBtn.style.color = 'white';
catBtn.style.fontWeight = '500';
}
// 点击分类标签过滤样式(若尚未渲染按钮,则延迟到下一帧再过滤)
catBtn.addEventListener('click', () => {
// 更新分类按钮样式
document.querySelectorAll('#format-categories > div').forEach(btn => {
btn.style.background = '#fce7f3';
btn.style.color = '#be185d';
btn.style.fontWeight = 'normal';
});
catBtn.style.background = '#ec4899';
catBtn.style.color = 'white';
catBtn.style.fontWeight = '500';
// 显示选中分类的样式按钮
const filterButtons = () => {
document.querySelectorAll('#format-buttons > button').forEach(btn => {
const btnCategory = btn.getAttribute('data-category');
btn.style.display = btnCategory === category ? 'inline-flex' : 'none';
});
};
if (buttonContainer.children.length === 0) {
setTimeout(filterButtons, 0);
} else {
filterButtons();
}
// 清空预览
previewContainer.innerHTML = '选择样式查看预览效果
';
});
categoryContainer.appendChild(catBtn);
});
// 创建样式按钮(极限斩断:初始不渲染任何按钮,首次点击分类时才懒加载)
const CHUNK = 25;
let index = 0;
function renderChunk() {
const end = Math.min(index + CHUNK, FORMAT_STYLES.length);
for (let i = index; i < end; i++) {
const style = FORMAT_STYLES[i];
const btn = document.createElement('button');
const iconHtml = style.icon ? `` : '';
btn.innerHTML = `${iconHtml}${style.name}`;
btn.setAttribute('data-category', style.category);
btn.style.cssText = `
background: #f472b6; color: white; border: none;
padding: 6px 12px; border-radius: 6px; cursor: pointer;
font-size: 12px; margin: 2px; display: ${style.category === categories[0] ? 'inline-flex' : 'none'};
align-items: center; transition: all 0.3s ease; box-shadow: 0 1px 3px rgba(244, 114, 182, 0.2);
`;
// 样式预览功能
if (style.preview) {
btn.addEventListener('mouseenter', () => {
// 仅在“显示预览”开启后,才按需渲染预览内容
if (previewContainer.style.display === 'block') {
previewContainer.innerHTML = `
${style.name} 预览:
${style.apply()}
`;
}
});
}
// 样式应用功能
btn.addEventListener('click', async (e) => {
isolateEvent(e);
// 添加点击动画反馈
btn.style.background = '#db2777';
btn.style.transform = 'scale(0.98)';
setTimeout(() => {
btn.style.background = '#f472b6';
btn.style.transform = 'scale(1)';
}, 200);
await autoClickSourceBtn();
const editor = getCurrentEditor();
if (!editor) {
showStatus('未找到编辑框,请先切换到源代码模式', true);
return;
}
let selectedText = '';
if (editor.type === 'codemirror') {
selectedText = editor.instance.getSelection();
} else {
selectedText = editor.instance.value.substring(
editor.instance.selectionStart,
editor.instance.selectionEnd
);
}
let styledHtml = style.apply(selectedText);
// 如果选择了模板,使用模板替换编辑器内容
try {
const REG = window.__TEMPLATE__;
if (REG && REG.CURRENT_ID) {
const chosen = REG.REGISTRY.find(t => t.id === REG.CURRENT_ID);
if (chosen) {
const html = compileTemplate(chosen.content, buildTemplateVars());
// 阀门:选择模板时,直接用模板替换编辑器全文
styledHtml = html;
// 清除按钮选中高亮
resetTemplateButtonStyles();
REG.CURRENT_ID = null;
}
}
} catch (e) {}
if (editor.type === 'codemirror') {
editor.instance.replaceSelection(styledHtml);
} else {
const start = editor.instance.selectionStart;
const end = editor.instance.selectionEnd;
editor.instance.value = editor.instance.value.substring(0, start) + styledHtml + editor.instance.value.substring(end);
editor.instance.dispatchEvent(new Event('input', { bubbles: true }));
editor.instance.focus();
editor.instance.setSelectionRange(start + styledHtml.length, start + styledHtml.length);
}
const saved = await autoClickSaveBtn();
if (saved) {
showStatus(`已应用"${style.name}"并自动保存`, false);
} else {
showStatus(`已应用"${style.name}",请手动保存`, false);
}
});
buttonContainer.appendChild(btn);
}
index = end;
if (index < FORMAT_STYLES.length) {
const schedule = (cb) => {
try {
if (typeof window.requestIdleCallback === 'function') return window.requestIdleCallback(cb);
if (typeof window.requestAnimationFrame === 'function') return window.requestAnimationFrame(() => cb());
} catch (e) {}
return setTimeout(cb, 0);
};
schedule(renderChunk);
}
}
let stylesRendered = false;
const ensureRender = () => { if (!stylesRendered) { stylesRendered = true; renderChunk(); } };
// 首次用户与工具交互时再开始渲染,避免初始化阻塞
buttonContainer.addEventListener('pointerover', ensureRender, { once: true });
categoryContainer.addEventListener('click', ensureRender, { once: true });
previewToggle.addEventListener('click', ensureRender, { once: true });
// 预览区域切换功能(持久化开关到localStorage,避免每次初始化造成抖动)
previewToggle.addEventListener('click', (e) => {
// 阻止事件冒泡和默认行为,避免触发编辑器自动保存
e.stopPropagation();
e.preventDefault();
if (previewContainer.style.display === 'none') {
// 打开预览时仅显示占位,不立即渲染,等待用户悬停某个样式按钮
previewContainer.style.display = 'block';
previewToggle.textContent = '隐藏预览';
previewContainer.innerHTML = '选择样式查看预览效果
';
try { localStorage.setItem('format_preview_open', '1'); } catch (e) {}
} else {
// 关闭预览并清空内容,释放DOM,阻止后续渲染
previewContainer.style.display = 'none';
previewToggle.textContent = '显示预览';
previewContainer.innerHTML = '预览已关闭,点击“显示预览”后再加载
';
try { localStorage.setItem('format_preview_open', '0'); } catch (e) {}
}
});
// 根据本地记忆恢复预览开关
try {
const open = localStorage.getItem('format_preview_open') === '1';
if (open) {
previewContainer.style.display = 'block';
previewToggle.textContent = '隐藏预览';
previewContainer.innerHTML = '选择样式查看预览效果
';
}
} catch (e) {}
// 为所有按钮添加悬停效果
setTimeout(() => {
document.querySelectorAll('#format-buttons button').forEach(btn => {
btn.addEventListener('mouseenter', () => {
btn.style.transform = 'translateY(-1px)';
btn.style.boxShadow = '0 2px 6px rgba(244, 114, 182, 0.3)';
});
btn.addEventListener('mouseleave', () => {
btn.style.transform = 'translateY(0)';
btn.style.boxShadow = '0 1px 3px rgba(244, 114, 182, 0.2)';
});
});
}, 100);
// ===== 模板工具条(延迟到图片区域渲染后再初始化,避免首屏阻塞) =====
// 移至 showImageSelection 完成后触发
}
// 模板系统:注册/预览/选择
function initTemplateToolbar() {
try {
const hostCard = document.querySelector('#format-buttons')?.parentElement;
if (!hostCard) return;
// 创建独立的模板选择界面
let tplToolbar = document.getElementById('template-toolbar');
if (!tplToolbar) {
tplToolbar = document.createElement('div');
tplToolbar.id = 'template-toolbar';
tplToolbar.style.cssText = `
margin: 20px 0;
padding: 15px;
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
width: 100%;
box-sizing: border-box;
`;
// 创建标题
const titleDiv = document.createElement('div');
titleDiv.style.cssText = 'text-align: center; margin-bottom: 12px; padding-bottom: 8px; border-bottom: 1px solid #f3f4f6;';
const title = document.createElement('h3');
title.textContent = '📋 模板选择器';
title.style.cssText = 'margin: 0; font-size: 16px; font-weight: 600; color: #1f2937;';
const subtitle = document.createElement('p');
subtitle.textContent = '选择您喜欢的排版风格';
subtitle.style.cssText = 'margin: 4px 0 0 0; font-size: 12px; color: #6b7280;';
titleDiv.appendChild(title);
titleDiv.appendChild(subtitle);
tplToolbar.appendChild(titleDiv);
// 创建按钮容器
const buttonContainer = document.createElement('div');
buttonContainer.id = 'template-button-container';
buttonContainer.style.cssText = 'display: grid; grid-template-columns: repeat(auto-fit, minmax(85px, 1fr)); gap: 8px; justify-items: center;';
tplToolbar.appendChild(buttonContainer);
// 移除工具栏级别的事件隔离,改为在个别按钮上处理
// 插入到合适位置(在输入区域下方,格式按钮上方)
const formatSection = document.querySelector('#format-buttons')?.parentElement;
if (formatSection && formatSection.parentElement) {
// 插在格式按钮区域上方
formatSection.parentElement.insertBefore(tplToolbar, formatSection);
} else {
// 备用方案:插在输入区域下方
const inputSection = document.querySelector('div[style*="margin-bottom:20px"][style*="padding:15px"][style*="background:#fff"]');
if (inputSection && inputSection.parentElement) {
inputSection.parentElement.insertBefore(tplToolbar, inputSection.nextSibling);
} else {
hostCard.appendChild(tplToolbar);
}
}
}
// 初始化模板注册中心
if (!window.__TEMPLATE__) window.__TEMPLATE__ = { REGISTRY: [], CURRENT_ID: null };
const REG = window.__TEMPLATE__;
// 读取外部模板(以常量内嵌,避免运行时IO)
const TPL1 = `
✦ {{title}} ✦
◇ 视觉主题:魔法与反战的双向奔赴
- ❶ 移动城堡:钢铁与草木交织的造型,隐喻“战争机器”与“自然生机”的对抗
- ❷ 苏菲的魔法:从白发老妪到少女的转变,不是“变美”,而是“接纳自我”——真正的魔法是内心的勇敢
- ❸ 哈尔的头发:金色→黑色→红色,对应他从“伪装完美”到“直面真实”再到“为守护而战”的成长
- ❹ 反战内核:硝烟弥漫的天空与鲜花盛开的山谷对比,宫崎骏用魔法故事呐喊“战争会吞噬一切美好”
主视觉海报(苏菲与哈尔站在移动城堡前,天空泛着反战的硝烟蓝)
🎬 影片名称:{{title}}
🈶️ 原名:{{originalTitle}}
🎬 导演:{{director}}
📚 原著:{{originalAuthor}}
✨ 类型:{{genres}}
📅 上映时间:{{release}}
⭐ 豆瓣评分:{{rating}}
▹ 魔法羁绊双人组
● 苏菲(帽子店少女)
被荒野女巫施咒变成老妪,却因祸得福逃离平庸生活。她用温柔与勇敢治愈哈尔的“逃避症”,从“自卑少女”到“城堡守护者”,最终明白:“年龄和外貌都不重要,内心的强大才是真正的魔法”。
● 哈尔(魔法少年)
拥有强大魔法却害怕承担责任的“完美主义者”,为逃避国王的征兵而四处躲藏。遇到苏菲后,他逐渐学会直面内心:“我终于找到想守护的东西了”——这份守护让他从“华丽的逃兵”变成“勇敢的战士”。
▹ 魔法场景隐喻

场景说明
▹ 剧情脉络
1. ……(保持模板原文,便于“一比一”呈现,可手动修改)
`;
const TPL2 = `
◇ {{title}} ◇
{{rating}}
{{ratingSource}}
▶ 片名:{{title}}
▶ 导演:{{directorDisplay}}
▶ 编剧:{{writerDisplay}}
▶ 类型:{{genresDisplay}}
▶ 地区:{{regionDisplay}}
▶ 语言:{{filmLanguageDisplay}}
▶ 主演:{{actorDisplay}}
{{title}} · 官方海报
◆ 观影小贴士
1. 推荐在{{watchScene}}观看,减少外界干扰
2. 二刷可留意{{detailTip}},会有新发现
3. 适合和{{watchWith}}一起看,看完可交流不同视角
`;
const TPL3 = `
⭐{{title}} ⭐
{{title}} · 官方海报
-
-
主创团队
导演:{{directorDisplay}} / 编剧:{{writerDisplay}}
-
-
基础信息
类型:{{genresDisplay}} / 地区:{{regionDisplay}} / 语言:{{filmLanguageDisplay}}
-
① 推荐在{{watchScene}}观看,搭配热茶更有氛围~
② 二刷可留意{{detailTip}},细节里藏着小温柔
③ 适合和{{watchWith}}一起看,看完可以聊聊剧中的温暖瞬间
`;
const TPL4 = `
{{title}} · 原版海报
♦ 影片信息 ♦
片名:
{{title}}
导演:
{{directorDisplay}}
主演:
{{actorDisplay}}
类型:
{{genresDisplay}}
评分:
{{rating}}({{ratingSource}})
上线:
{{releaseDisplay}} / {{runtimeDisplay}}
标识:
{{markerType}} {{markerId}}
{{regionDisplay}} · {{filmLanguageDisplay}} · {{genresDisplay}}
▶ 观影贴士
★ 推荐在{{watchScene}}观看,搭配汽水或啤酒更有港味~
★ 二刷可重点关注{{detailTip}},港片的细节藏着江湖气
★ 适合和{{watchWith}}一起看,看完能聊透片中的“江湖道义”
`;
const TPL5 = `
「{{title}}」
{{genres}} · {{release}} · {{region}}
{{title}} · 官方海报
影片名称:{{title}}
导演:{{directorDisplay}}
编剧:{{writerDisplay}}
主演:{{actorDisplay}}
影片类型:{{genres}}
制片地区:{{regionDisplay}}
语言:{{filmLanguageDisplay}}
片长:{{runtimeDisplay}}
评分:{{rating}}
【观影贴士】
- 推荐于{{watchScene}}观看,更能品悟影片韵味
- 二刷可着重留意{{detailTip}},藏有中式巧思
- 适合与{{watchWith}}共赏,观后可交流中式意趣
`;
const TPL6 = `
{{title}}
● {{genres}} | {{releaseDisplay}} | {{regionDisplay}}
● 导演:{{directorDisplay}}
● 编剧:{{writerDisplay}}
● 主演:{{actorDisplay}}
● 语言:{{filmLanguageDisplay}}
● 片长:{{runtimeDisplay}}
● 评分:{{rating}}
{{title}} · 官方海报
● 观影贴士
- 推荐于{{watchScene}}观看,更能体会影片情感内核
- 适合与{{watchWith}}共赏,观后可深入交流角色塑造与剧情逻辑
- 二刷可重点留意{{detailTip}},能挖掘更多创作巧思
`;
const TPL7 = `
★ {{title}}{{originalTitleParen}} ★
▷ 影片基础信息
▷ 导演:{{director}}
▷ 类型:{{genres}}
▷ 主演:{{actorDisplay}}
▷ 编剧:{{writer}}
▷ 上映:{{release}}
▷ 地区:{{region}}
▷ 片长:{{runtime}}
▷ {{ratingSource}}评分:{{rating}}
▷ 标识:{{idLineHtml}}
▷ 影视简介
{{introAuto}}
▷ 影片剧照
{{title}}经典镜头
▷ 观众热评
{{commentsAuto}}
◆ 【观影小贴士】① 结合剧情简介看剧照,更易理解情感张力;② 二刷可留意剧照中的细节,与剧情伏笔呼应;③ 适合朋友/家人共同观看,看完可结合剧照讨论场景氛围的营造~
`;
// 覆盖模板(1):采用用户提供的橙红风格一比一排版(变量占位已简化为可替换形式)
const TPL1_ORANGE = `
★ {{title}}{{originalTitleParen}} ★
{{title}}主视觉海报
▷ 基础信息
▷ 导演:{{director}}
▷ 类型:{{genres}}
▷ 主演:{{actorShort}}
▷ 编剧:{{writer}}
▷ 上映:{{release}}
▷ 地区:{{region}}
▷ 片长:{{runtime}}
▷ 评分:{{rating}}
▷ 标识:{{idLineHtml}}
▷ 剧情简介
{{introAuto}}
▷ 观众热评
{{commentsAuto}}
◆
【观影提示】① 建议优先查看长评,深度解析更易理解剧情;② 二刷可关注{{detailTip}},可能发现隐藏伏笔;③ 适合{{watchWith}}共同观看,看完可交流观点~
`;
function safePush(id, name, raw) {
if (!raw) return;
if (REG.REGISTRY.some(t => t.id === id)) return;
REG.REGISTRY.push({ id, name, content: templatizeHtml(raw) });
}
// 为每个模板赋予风格化名称(纯名字,不带备注)
safePush('tpl1', '玫瑰幻境', TPL1_ORANGE);
safePush('tpl2', '琥珀复古', TPL2);
safePush('tpl3', '樱雾清新', TPL3);
safePush('tpl4', '港风胶片', TPL4);
safePush('tpl5', '国韵典藏', TPL5);
safePush('tpl6', '理性简约', TPL6);
safePush('tpl7', '森系活力', TPL7);
// 渲染模板按钮
const buttonContainer = document.getElementById('template-button-container') || tplToolbar;
buttonContainer.querySelectorAll('button[data-tpl-id]').forEach(b => b.remove());
const previewContainer = document.getElementById('format-preview');
const previewToggle = document.getElementById('format-preview-toggle');
REG.REGISTRY.forEach(tpl => {
const btn = document.createElement('button');
btn.textContent = tpl.name;
btn.setAttribute('data-tpl-id', tpl.id);
btn.style.cssText = `
background: linear-gradient(135deg, #f472b6, #ec4899);
color: #fff;
border: 1px solid transparent;
padding: 8px 12px;
border-radius: 6px;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
text-align: center;
min-width: 70px;
box-shadow: 0 1px 3px rgba(244, 114, 182, 0.3);
`;
btn.addEventListener('mouseenter', () => {
if (REG.CURRENT_ID !== tpl.id) {
btn.style.transform = 'translateY(-1px)';
btn.style.boxShadow = '0 2px 6px rgba(244,114,182,.35)';
}
});
btn.addEventListener('mouseleave', () => {
if (REG.CURRENT_ID !== tpl.id) {
btn.style.transform = 'translateY(0)';
btn.style.boxShadow = '0 1px 3px rgba(244,114,182,.2)';
}
});
btn.addEventListener('click', (e) => {
// 阻止事件冒泡和默认行为,避免触发编辑器自动保存
e.stopPropagation();
e.preventDefault();
// 选中动画
btn.style.transform = 'scale(0.98)';
setTimeout(() => { btn.style.transform = 'scale(1)'; }, 120);
// 若再次点击同一个按钮,则视为取消选择
if (REG.CURRENT_ID === tpl.id) {
REG.CURRENT_ID = null;
btn.style.background = '#f472b6';
btn.style.color = '#fff';
btn.style.border = '1px solid transparent';
btn.style.boxShadow = '0 1px 3px rgba(244,114,182,.2)';
// 不强制关闭预览,保留上一次内容;如需关闭可开启:
// previewContainer.style.display = 'none'; previewToggle.textContent = '显示预览';
return;
}
// 取消其他按钮的选中样式
resetTemplateButtonStyles();
// 设置当前为选中(白色高亮)
REG.CURRENT_ID = tpl.id;
btn.style.background = '#fff';
btn.style.color = '#db2777';
btn.style.border = '1px solid #f472b6';
btn.style.boxShadow = '0 2px 8px rgba(236,72,153,.25)';
// 预览模板
const demoHtml = compileTemplate(tpl.content, buildTemplateVars());
previewContainer.style.display = 'block';
previewToggle.textContent = '隐藏预览';
previewContainer.innerHTML = `
当前模板:${tpl.name}
` + demoHtml;
});
buttonContainer.appendChild(btn);
});
} catch (e) { /* 忽略模板初始化异常,避免影响主流程 */ }
}
// 将外部模板自动打上占位符,便于替换
function templatizeHtml(html) {
try {
let h = String(html || '');
if (!/\{\{title\}\}/.test(h)) h = h.replace(/(
]*>)([\s\S]*?)(<\/h[12]>)/i, '$1{{title}}$3');
let imgIdx = 0;
h = h.replace(/(
]*?src=")([^"]+)("[^>]*>)/ig, (m, p1, p2, p3) => {
imgIdx += 1;
if (imgIdx === 1) return `${p1}{{posterUrl}}${p3}`;
if (imgIdx === 2) return `${p1}{{sceneUrl}}${p3}`;
return m;
});
h = h.replace(/(豆瓣评分[^<]*?]*>)[^<]+(<\/span>)/i, '$1{{rating}}$2');
return h;
} catch { return html; }
}
function compileTemplate(raw, vars) {
return String(raw || '').replace(/\{\{(\w+)\}\}/g, (m, k) => (vars && (k in vars) ? (vars[k] ?? '') : ''));
}
function buildTemplateVars() {
const info = currentMovieInfo || {};
const src = (info && info.source) || '';
const url = (info && info.url) || '';
const isDouban = src === '豆瓣' || (url && url.includes('douban.com'));
const isTmdb = src === 'TMDB' || (url && url.includes('themoviedb.org'));
const isImdb = src === 'IMDB' || (url && url.includes('imdb.com'));
const idLineHtml = (function(){
if (isDouban) {
const id = info.doubanId || '—';
const link = url || (info.doubanId ? `https://movie.douban.com/subject/${info.doubanId}/` : '');
return link ? `豆瓣 ${id}` : `豆瓣 ${id}`;
}
if (isTmdb) {
const id = info.tmdbId || '—';
const link = info.tmdbId ? `https://www.themoviedb.org/${info.mediaType || 'movie'}/${info.tmdbId}` : (url || '');
return link ? `TMDB ${id}` : `TMDB ${id}`;
}
if (info.imdbId) {
const link = `https://www.imdb.com/title/${info.imdbId}/`;
return `IMDB ${info.imdbId}`;
}
return '—';
})();
// 评分来源标签与标识三元组
const ratingSource = isDouban ? '豆瓣' : (isTmdb ? 'TMDB' : (isImdb ? 'IMDB' : '评分'));
let markerType = '—', markerLink = '', markerId = '—';
if (isDouban && (info.doubanId || url)) {
markerType = '豆瓣';
markerId = info.doubanId || '—';
markerLink = url || (info.doubanId ? `https://movie.douban.com/subject/${info.doubanId}/` : '');
} else if (isTmdb && info.tmdbId) {
markerType = 'TMDB';
markerId = info.tmdbId;
markerLink = `https://www.themoviedb.org/${info.mediaType || 'movie'}/${info.tmdbId}`;
} else if (isImdb && info.imdbId) {
markerType = 'IMDB';
markerId = info.imdbId;
markerLink = `https://www.imdb.com/title/${info.imdbId}/`;
}
// 自动简介:优先 info.intro,否则简短占位
const introAuto = (info.intro && String(info.intro).trim())
? info.intro
: '暂无剧情简介,欢迎补充你的观影理解~';
// 自动热评:从 currentComments 取首条
let commentsAuto = '暂无热评,分享你的观影感受吧~
';
try {
if (Array.isArray(currentComments) && currentComments.length > 0) {
const first = currentComments[0];
const quote = (first && (first.content || first.text || first.comment || '')).toString().trim();
const author = (first && (first.author || first.user || first.nickname || '匿名'));
if (quote) {
commentsAuto = `\"${quote}\"
—— ${isDouban ? '豆瓣用户' : '用户'} @${author}
`;
}
} else if (isTmdb) {
// TMDB无热评:用剧情/标语摘录 + TMDB更多评价链接
const raw = (info.intro || info.tagline || '').toString().trim();
const snippet = raw ? (raw.length > 80 ? raw.slice(0, 78) + '…' : raw) : `类型:${(Array.isArray(info.genreTags) ? info.genreTags.join(' / ') : (info.genres || '影片'))}`;
const rvLink = info.tmdbId ? `https://www.themoviedb.org/${info.mediaType || 'movie'}/${info.tmdbId}/reviews` : '';
const linkHtml = rvLink ? ` 更多评价` : '';
commentsAuto = `\"${snippet}\"
—— 看点摘录${linkHtml}
`;
}
} catch (e) {}
// 热评首条提供给模板2的精简占位
let commentQuote = '';
let commentAuthor = '';
let commentSourceLabel = isDouban ? '豆瓣用户' : (isTmdb ? 'TMDB用户' : '用户');
try {
if (Array.isArray(currentComments) && currentComments.length > 0) {
const first = currentComments[0];
commentQuote = (first && (first.content || first.text || first.comment || '')).toString().trim() || '';
commentAuthor = (first && (first.author || first.user || first.nickname || '匿名')) || '';
}
} catch (e) {}
if (!commentQuote) {
commentQuote = '暂无热评,分享你的观影感受吧~';
commentAuthor = '匿名';
}
// 演员截断:避免撑开导致中间空隙,保留前若干字符与人名数量
const actorShort = (function(){
const raw = info.actor || '';
if (!raw) return '未知';
// 先按顿号/逗号/空格拆分,最多取前6个;若依旧很长,再做总长度截断
const names = String(raw).split(/[、,,\s]+/).filter(Boolean).slice(0, 6);
let txt = names.join('、');
if (txt.length > 38) txt = txt.slice(0, 36) + '…';
return txt;
})();
// 展示用兜底字段,避免模板出现未替换占位
const directorDisplay = info.director || '未知';
const writerDisplay = info.writer || '未知';
const genresDisplay = (Array.isArray(info.genreTags) ? info.genreTags.filter(Boolean).join(' / ') : (info.genres || '')) || '未知';
const regionDisplay = info.region || '未知';
const filmLanguage = info.language || info.lang || '';
const filmLanguageDisplay = filmLanguage || '未知';
const runtimeDisplay = info.runtime || '未知';
const releaseDisplay = (info.release || info.releaseDate || '') || '未知';
const actorDisplay = (actorShort || info.actor || '').trim() || '未知';
return {
title: info.title || info.originalTitle || '标题',
originalTitle: info.originalTitle || '',
originalTitleSafe: info.originalTitle || '无原名',
originalTitleParen: (info.originalTitle && info.originalTitle !== (info.title || '')) ? `(${info.originalTitle})` : '',
director: info.director || '',
writer: info.writer || '',
actor: info.actor || '',
actorShort,
originalAuthor: info.originalAuthor || info.writer || '',
genres: Array.isArray(info.genreTags) ? info.genreTags.join(' / ') : (info.genres || ''),
genreTags: Array.isArray(info.genreTags) ? info.genreTags : [],
release: info.release || info.releaseDate || '',
region: info.region || '',
runtime: info.runtime || '',
filmLanguage,
doubanId: info.doubanId || '',
imdbId: info.imdbId || '',
idLineHtml,
posterUrl: selectedPosterUrl || 'https://via.placeholder.com/680x480/ff69b4/FFF?text=Poster',
sceneUrl: selectedStillUrl || 'https://via.placeholder.com/640x340/ff69b4/FFF?text=Scene',
rating: info.rating || '—',
introAuto,
commentsAuto,
ratingSource,
markerType,
markerLink,
markerId,
commentQuote,
commentAuthor,
commentSourceLabel,
detailTip: info.detailTip || '镜头语言设计',
watchWith: info.watchWith || '家人/朋友',
watchScene: info.watchScene || '周末午后',
// 展示用(模板2使用)
directorDisplay,
writerDisplay,
genresDisplay,
regionDisplay,
filmLanguageDisplay,
runtimeDisplay,
releaseDisplay,
actorDisplay
};
}
function getCurrentEditor() {
// 先检查已经缓存的sourceCodeElement
if (sourceCodeElement && sourceCodeElement.offsetParent !== null) {
console.log('Found cached sourceCodeElement:', sourceCodeElement);
return { type: 'textarea', instance: sourceCodeElement };
}
// 检查CodeMirror编辑器
const codeMirror = document.querySelector('.CodeMirror');
if (codeMirror && codeMirror.CodeMirror) {
console.log('Found CodeMirror editor');
return { type: 'codemirror', instance: codeMirror.CodeMirror };
}
// 扩展的编辑器选择器列表,覆盖更多可能的编辑器类型
const editorSelectors = [
'#myModal-code textarea',
'textarea.tox-textarea',
'textarea.mce-textbox',
'textarea.cke_source',
'textarea[name="message"]',
'#editor_content',
'textarea[name="content"]', // 常见的内容输入框
'#post_content', // 论坛常见的内容输入框
'textarea#content', // ID为content的textarea
'.editor-content textarea', // 带有editor-content类的容器内的textarea
'#post_message', // 论坛发帖编辑器
'.article-editor textarea', // 文章编辑器
'div[contenteditable="true"]', // 富文本编辑区
'.prose-editor', // 专业编辑器
'textarea[id^="editor_"]', // ID以editor_开头的textarea
'textarea[id$="_editor"]', // ID以_editor结尾的textarea
'textarea.editor'
];
for (const selector of editorSelectors) {
const elem = document.querySelector(selector);
if (elem && elem.style.display !== 'none' && elem.offsetParent !== null) {
console.log('Found editor with selector:', selector);
sourceCodeElement = elem;
return { type: 'textarea', instance: elem };
}
}
// 最后的尝试:查找页面上所有可见的textarea
const allTextareas = document.querySelectorAll('textarea');
for (let i = 0; i < allTextareas.length; i++) {
const textarea = allTextareas[i];
if (textarea && textarea.style.display !== 'none' && textarea.offsetParent !== null &&
textarea.offsetWidth > 100 && textarea.offsetHeight > 100) {
console.log('Found visible textarea as fallback');
sourceCodeElement = textarea;
return { type: 'textarea', instance: textarea };
}
}
if (!editorNotFoundLogged) {
console.log('No editor found');
editorNotFoundLogged = true;
}
return null;
}
// 修改绑定按钮事件(确保加载更多功能正常)
function bindEventListeners() {
// 绑定AI相关事件监听器
bindAIEventListeners();
let fetchBtn = document.getElementById('fetch-btn');
const mediaUrlInput = document.getElementById('media-url');
const pasteBtn = document.getElementById('paste-btn');
const clearBtn = document.getElementById('clear-btn');
const confirmImagesBtn = document.getElementById('confirm-images-btn');
const loadMorePostersBtn = document.getElementById('load-more-posters');
const loadMoreStillsBtn = document.getElementById('load-more-stills');
// 调试代码:检查按钮状态
if (fetchBtn) {
console.log('提取按钮已找到,初始状态:', {
classList: fetchBtn.classList.toString(),
style: {
pointerEvents: fetchBtn.style.pointerEvents,
cursor: fetchBtn.style.cursor,
opacity: fetchBtn.style.opacity
}
});
// 添加状态变化监听
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
console.log('按钮class变化:', fetchBtn.classList.toString());
}
});
});
observer.observe(fetchBtn, { attributes: true });
}
// 输入URL时激活提取按钮 - 已移至按钮初始化中,避免重复监听
// 提取影视信息按钮
if (fetchBtn) {
// 移除所有可能的现有事件监听器
const newFetchBtn = fetchBtn.cloneNode(true);
fetchBtn.parentNode.replaceChild(newFetchBtn, fetchBtn);
// 重新获取按钮引用
const updatedFetchBtn = document.getElementById('fetch-btn');
// 根据输入框内容决定按钮状态
if (mediaUrlInput && mediaUrlInput.value.trim()) {
updatedFetchBtn.classList.add('active');
updatedFetchBtn.style.pointerEvents = 'auto';
updatedFetchBtn.style.cursor = 'pointer';
updatedFetchBtn.style.opacity = '1';
updatedFetchBtn.style.display = 'block';
} else {
updatedFetchBtn.classList.remove('active');
updatedFetchBtn.style.pointerEvents = 'none';
updatedFetchBtn.style.cursor = 'not-allowed';
updatedFetchBtn.style.opacity = '0.6';
updatedFetchBtn.style.display = 'none';
}
// 确保按钮在页面加载时根据当前输入框内容正确激活
if (mediaUrlInput) {
const checkButtonState = () => {
if (mediaUrlInput.value.trim()) {
updatedFetchBtn.classList.add('active');
updatedFetchBtn.style.pointerEvents = 'auto';
updatedFetchBtn.style.cursor = 'pointer';
updatedFetchBtn.style.opacity = '1';
updatedFetchBtn.style.display = 'block';
} else {
updatedFetchBtn.classList.remove('active');
updatedFetchBtn.style.pointerEvents = 'none';
updatedFetchBtn.style.cursor = 'not-allowed';
updatedFetchBtn.style.opacity = '0.6';
updatedFetchBtn.style.display = 'none';
}
};
// 立即检查一次
checkButtonState();
// 监听输入变化
mediaUrlInput.addEventListener('input', checkButtonState);
}
// 绑定新的点击事件处理函数
updatedFetchBtn.addEventListener('click', async function (e) {
console.log('提取按钮被点击');
// 触发阻断:阻止事件冒泡,防止触发表单验证
e.stopPropagation();
e.preventDefault();
const url = document.getElementById('media-url')?.value.trim();
if (!url) {
showStatus('请输入影视链接', true);
return;
}
showStatus('正在提取影视信息...', false);
try {
currentMovieInfo = await getBasicInfo(url);
currentComments = await getHotComments(url);
showStatus('信息提取完成,请选择海报和剧照', false);
await showImageSelection(currentMovieInfo);
} catch (err) {
showStatus(`提取失败:${err.message || '未知错误'}`, true);
console.error('提取错误:', err);
}
});
// 更新引用
fetchBtn = updatedFetchBtn;
// 确保按钮状态与输入框内容同步
if (mediaUrlInput && mediaUrlInput.value.trim()) {
updatedFetchBtn.classList.add('active');
updatedFetchBtn.style.pointerEvents = 'auto';
updatedFetchBtn.style.cursor = 'pointer';
updatedFetchBtn.style.opacity = '1';
updatedFetchBtn.style.display = 'block';
} else {
updatedFetchBtn.classList.remove('active');
updatedFetchBtn.style.pointerEvents = 'none';
updatedFetchBtn.style.cursor = 'not-allowed';
updatedFetchBtn.style.opacity = '0.6';
updatedFetchBtn.style.display = 'none';
}
}
// 手动粘贴内容按钮
if (pasteBtn) {
pasteBtn.addEventListener('click', async function (e) {
// 触发阻断:阻止事件冒泡,防止触发表单验证
e.stopPropagation();
e.preventDefault();
const backupHtml = document.getElementById('backup-html').value;
if (backupHtml) {
await autoClickSourceBtn();
const filled = await autoFillSourceBox(backupHtml);
if (filled) {
showStatus('内容已粘贴到编辑框', false);
} else {
showStatus('内容粘贴失败,请手动粘贴剪贴板内容', true);
}
}
});
}
// 清除所有内容按钮
if (clearBtn) {
clearBtn.addEventListener('click', function (e) {
// 触发阻断:阻止事件冒泡,防止触发表单验证
e.stopPropagation();
e.preventDefault();
if (mediaUrlInput) mediaUrlInput.value = '';
const searchInput = document.getElementById('search-movie');
if (searchInput) searchInput.value = '';
const searchResults = document.getElementById('search-results');
if (searchResults) { searchResults.classList.remove('show'); searchResults.style.visibility='hidden'; searchResults.style.opacity='0'; searchResults.style.pointerEvents='none'; }
const imageSelection = document.getElementById('image-selection');
if (imageSelection) imageSelection.style.display = 'none';
if (posterContainer) posterContainer.innerHTML = '';
if (stillContainer) stillContainer.innerHTML = '';
if (fetchBtn) {
fetchBtn.classList.remove('active');
fetchBtn.style.display = 'none';
fetchBtn.style.pointerEvents = 'none';
fetchBtn.style.cursor = 'not-allowed';
fetchBtn.style.opacity = '0.6';
}
selectedPosterUrl = '';
selectedStillUrl = '';
currentMovieInfo = null;
currentComments = [];
// 重置页码
posterPage = 1;
stillPage = 1;
showStatus('已清除所有内容', false);
});
}
// 确认选择并填充按钮
if (confirmImagesBtn) {
confirmImagesBtn.addEventListener('click', async function (e) {
// 触发阻断:阻止事件冒泡,防止触发表单验证
e.stopPropagation();
e.preventDefault();
if (!currentMovieInfo) {
showStatus('未找到影视信息,请重新加载', true);
return;
}
// 兜底:优先读取已选元素的dataset.url,保证首次不点击也能拿到生效后的dataURL
try {
if ((!selectedPosterUrl || /doubanio\.com/.test(selectedPosterUrl)) && selectedPosterEl && selectedPosterEl.dataset && selectedPosterEl.dataset.url) {
selectedPosterUrl = selectedPosterEl.dataset.url;
}
} catch (e) {}
try {
if ((!selectedStillUrl || /doubanio\.com/.test(selectedStillUrl)) && selectedStillEl && selectedStillEl.dataset && selectedStillEl.dataset.url) {
selectedStillUrl = selectedStillEl.dataset.url;
}
} catch (e) {}
// 保障:控件里可显示缩略,进入排版确保原图
try {
// Douban:若仍是直链且疑似 m/s 尺寸,则拉取 raw/l 为 DataURL
if (/^https?:\/\/.*doubanio\.com\//.test(selectedPosterUrl)) {
const rawBase = selectedPosterEl && selectedPosterEl.dataset && selectedPosterEl.dataset.rawUrl
? selectedPosterEl.dataset.rawUrl
: selectedPosterUrl;
selectedPosterUrl = await getImageDataURLWithQuality(rawBase);
}
} catch(_) {}
try {
if (/^https?:\/\/.*doubanio\.com\//.test(selectedStillUrl)) {
const rawBase = selectedStillEl && selectedStillEl.dataset && selectedStillEl.dataset.rawUrl
? selectedStillEl.dataset.rawUrl
: selectedStillUrl;
selectedStillUrl = await getImageDataURLWithQuality(rawBase);
}
} catch(_) {}
const finalPosterUrl = toTMDBOriginal(selectedPosterUrl || '') || 'https://picsum.photos/200/300?default-poster';
const finalStillUrl = toTMDBOriginal(selectedStillUrl || '') || 'https://picsum.photos/300/180?default-still';
// 模板阀门:优先使用已选模板
let useTemplate = false;
let html;
try {
const REG = window.__TEMPLATE__;
if (REG && REG.CURRENT_ID) {
const chosen = REG.REGISTRY.find(t => t.id === REG.CURRENT_ID);
if (chosen) {
useTemplate = true;
html = compileTemplate(chosen.content, buildTemplateVars());
// 应用后清空选中与高亮
resetTemplateButtonStyles();
REG.CURRENT_ID = null;
}
}
} catch (e) { /* 忽略模板异常,回退到原排版 */ }
if (!useTemplate) {
showStatus('正在生成HTML内容...', false);
html = generateHTML(currentMovieInfo, currentComments, finalPosterUrl, finalStillUrl);
} else {
showStatus('正在应用模板并生成内容...', false);
}
// 异常兜底:本地存储备份,防止填充失败
try {
localStorage.setItem('backup-movie-html', html);
} catch (localStorageError) {
console.log('localStorage备份失败:', localStorageError);
}
const backupHtml = document.getElementById('backup-html');
if (backupHtml) backupHtml.value = html;
// 统一逻辑:直接写入并自动保存
const success = await writeHtmlToAnyEditor(html);
try { await autoClickSaveBtn(); } catch(_) {}
showStatus(success ? (useTemplate ? '模板已写入并自动保存' : '内容已写入并自动保存') : '写入失败,已提供剪贴板备份', !success);
});
}
// 绑定加载更多海报按钮(确保点击有效)
if (loadMorePostersBtn) {
// 先移除旧事件(避免重复绑定)
loadMorePostersBtn.removeEventListener('click', loadMorePosters);
// 创建新的包装函数以添加事件阻止
const wrappedLoadMorePosters = function(e) {
// 触发阻断:阻止事件冒泡,防止触发表单验证
e.stopPropagation();
e.preventDefault();
loadMorePosters();
};
loadMorePostersBtn.addEventListener('click', wrappedLoadMorePosters);
}
// 绑定加载更多剧照按钮(确保点击有效)
if (loadMoreStillsBtn) {
// 先移除旧事件(避免重复绑定)
loadMoreStillsBtn.removeEventListener('click', loadMoreStills);
// 创建新的包装函数以添加事件阻止
const wrappedLoadMoreStills = function(e) {
// 触发阻断:阻止事件冒泡,防止触发表单验证
e.stopPropagation();
e.preventDefault();
loadMoreStills();
};
loadMoreStillsBtn.addEventListener('click', wrappedLoadMoreStills);
}
// 初始化搜索交互
setupSearchInteractions();
// 设置海报/剧照容器的事件委托(避免为每个item单独绑定)
setupImageSelectionDelegates();
// 确保移动端适配在绑定事件后应用
applyMobileStyles();
}
function setupImageSelectionDelegates() {
if (!posterContainer) posterContainer = document.getElementById('poster-candidates');
if (!stillContainer) stillContainer = document.getElementById('still-candidates');
// 保证默认选中元素引用与dataset同步(首次渲染后立即记录)
try {
const firstPoster = posterContainer && posterContainer.firstElementChild;
if (firstPoster && !selectedPosterEl) selectedPosterEl = firstPoster;
const firstStill = stillContainer && stillContainer.firstElementChild;
if (firstStill && !selectedStillEl) selectedStillEl = firstStill;
} catch (e) {}
if (posterContainer && !posterContainer.dataset.delegateBound) {
posterContainer.addEventListener('click', function (e) {
const item = e.target.closest('div');
if (!item || !posterContainer.contains(item) || !item.dataset || !item.dataset.url) return;
const url = item.dataset.url;
selectedPosterUrl = url;
if (selectedPosterEl && selectedPosterEl !== item) {
selectedPosterEl.style.border = '1px solid #f3d5d9';
selectedPosterEl.style.boxShadow = 'none';
selectedPosterEl.style.padding = '0px';
}
// 使用粉色外框+偏移留白作为选中状态
item.style.border = '3px solid #ec4899';
item.style.boxShadow = '0 4px 12px rgba(236, 72, 153, 0.3)';
item.style.padding = '2px';
selectedPosterEl = item;
});
posterContainer.dataset.delegateBound = '1';
}
if (stillContainer && !stillContainer.dataset.delegateBound) {
stillContainer.addEventListener('click', function (e) {
const item = e.target.closest('div');
if (!item || !stillContainer.contains(item) || !item.dataset || !item.dataset.url) return;
const url = item.dataset.url;
selectedStillUrl = url;
if (selectedStillEl && selectedStillEl !== item) {
selectedStillEl.style.border = '1px solid #f3d5d9';
selectedStillEl.style.boxShadow = 'none';
selectedStillEl.style.padding = '0px';
}
// 使用粉色外框+偏移留白作为选中状态
item.style.border = '3px solid #ec4899';
item.style.boxShadow = '0 4px 12px rgba(236, 72, 153, 0.3)';
item.style.padding = '2px';
selectedStillEl = item;
});
stillContainer.dataset.delegateBound = '1';
}
}
// 绑定AI相关事件监听器
function bindAIEventListeners() {
if (aiEventsBound) return; // 避免重复绑定导致多次请求
// 先确保AI面板内容已正确生成并插入
const aiPanelContainer = document.getElementById('ai-panel-container');
if (aiPanelContainer && aiPanelContainer.innerHTML.trim() === '') {
// 使用传统的DOM操作方式插入AI面板内容
aiPanelContainer.innerHTML = createAIPanelContent();
}
// 重新排序:将“AI检索与图片选择”整体移动到“生成提示”上方
try {
const promptDiv = document.getElementById('ai-prompt-input')?.parentElement;
const searchWrap = document.getElementById('ai-search-input')?.closest('div[style*="border:1px solid"]');
if (promptDiv && searchWrap && promptDiv.previousSibling !== searchWrap) {
promptDiv.parentElement.insertBefore(searchWrap, promptDiv);
}
} catch (_) {}
// 获取所有需要的元素
const aiFunctionSelect = document.getElementById('ai-function-select');
const aiPromptInput = document.getElementById('ai-prompt-input');
const generateAiTextBtn = document.getElementById('generate-ai-text');
const abortAiBtn = document.getElementById('abort-ai-generate');
const aiResultArea = document.getElementById('ai-result-area');
const aiResultContent = document.getElementById('ai-result-content');
// 选择结果忙碌锁,防止重复点击
let aiResultsBusy = false;
// 新增:AI 检索与图片区域元素
const aiSearchInput = document.getElementById('ai-search-input');
const aiSearchStatus = document.getElementById('ai-search-status');
const aiSearchResults = document.getElementById('ai-search-results');
const aiImageSelection = document.getElementById('ai-image-selection');
const aiPosterContainer = document.getElementById('ai-poster-candidates');
const aiStillContainer = document.getElementById('ai-still-candidates');
const aiLoadMorePosters = document.getElementById('ai-load-more-posters');
const aiLoadMoreStills = document.getElementById('ai-load-more-stills');
const aiAutoTitle = document.getElementById('ai-auto-title');
const aiDeepThink = document.getElementById('ai-deep-think');
const aiWebBrowse = document.getElementById('ai-web-browse');
const aiFeatureTip = document.getElementById('ai-feature-tip');
// 根据模型能力开启/禁用“深度思考/联网补充”
(function initFeatureToggles(){
const cfg = getConfig();
const model = (cfg.AI.DEFAULT_MODEL||'').toLowerCase();
const provider = (cfg.AI.PROVIDER||'').toLowerCase();
let supportsDeep = /gpt-4|gpt-4o|gpt-4\.1|claude-3|sonnet|gemini|glm|qwen|deepseek|mixtral/.test(model) || /openai|anthropic|gemini|bigmodel|aliyuncs|ark|together/.test(provider);
let supportsWeb = /gpt-4o|gpt-4\.1|gemini|qwen|glm|deepseek/.test(model) || /gemini|aliyuncs|bigmodel/.test(provider);
if (aiDeepThink) {
aiDeepThink.disabled = !supportsDeep;
const wrap = document.getElementById('ai-deep-wrap');
if (wrap) wrap.style.opacity = supportsDeep? '1' : '0.5';
}
if (aiWebBrowse) {
aiWebBrowse.disabled = !supportsWeb;
const wrap = document.getElementById('ai-web-wrap');
if (wrap) wrap.style.opacity = supportsWeb? '1' : '0.5';
}
if (aiFeatureTip) {
aiFeatureTip.textContent = (!supportsDeep || !supportsWeb) ? '当前模型部分功能不可用' : '';
}
})();
// 已移除AI“搜索”按钮,以下为通用的状态展示工具
function setAiSearchLoading(show, text = '正在加载...') {
if (!aiSearchStatus) return;
if (show) {
aiSearchStatus.style.display = 'block';
aiSearchStatus.textContent = text;
} else {
aiSearchStatus.style.display = 'none';
}
}
async function renderAiImageSelection(info){
aiImageSelection.style.display = 'block';
aiPosterContainer.innerHTML = '';
aiStillContainer.innerHTML = '';
// 切换影片时清空AI区域多选集合
try { aiSelectedPosterUrls.clear(); aiSelectedStillUrls.clear(); } catch (e) {}
// 记录AI侧影片信息
aiCurrentMovieInfo = info || null;
const posters = info.posterUrls || [];
const stills = info.stillUrls || [];
const buildCard = (rawUrl, type) => {
const card = document.createElement('div');
card.style.cssText = 'width:100%;height:'+(type==='poster'?'180px':'120px')+';border:1px solid #f3d5d9;border-radius:6px;cursor:pointer;overflow:hidden;display:flex;align-items:center;justify-content:center;background:#fff5f7;';
const img = document.createElement('img');
img.style.cssText = 'max-width:100%;max-height:100%;object-fit:contain;';
img.src = LAZY_PLACEHOLDER;
card.appendChild(img);
card.dataset.url = rawUrl;
card.onclick = function(){ try {
const chosen = this.dataset.url;
const isSelected = this.style.border && this.style.border.indexOf('3px solid')!==-1;
if (isSelected) {
this.style.border = '1px solid #f3d5d9';
this.style.boxShadow = 'none';
this.style.padding = '0px';
if (type==='poster') { aiSelectedPosterUrls.delete(chosen); } else { aiSelectedStillUrls.delete(chosen); }
} else {
this.style.border = '3px solid #ec4899';
this.style.boxShadow = '0 4px 12px rgba(236, 72, 153, 0.3)';
this.style.padding = '2px';
if (type==='poster') { aiSelectedPosterUrls.add(chosen); } else { aiSelectedStillUrls.add(chosen); }
}
} catch(e){} };
// 懒加载真实缩略图
getThumbnailForDisplay(rawUrl).then(src=>{ img.src = src; });
// 预升级为更高清尺寸或DataURL
const upgrade = shouldConvertToDataURL(rawUrl)
? getImageDataURLWithQuality(rawUrl)
: Promise.resolve(
type === 'poster'
? rawUrl.replace(`/${getConfig().TMDB.LIST_POSTER_SIZE}/`, `/${getConfig().TMDB.SELECTED_POSTER_SIZE}/`)
: rawUrl.replace(`/${getConfig().TMDB.LIST_STILL_SIZE}/`, `/${getConfig().TMDB.SELECTED_STILL_SIZE}/`)
);
upgrade.then(du=>{ card.dataset.url = du; if (type==='poster') { if (aiSelectedPosterUrls.has(rawUrl)) { aiSelectedPosterUrls.delete(rawUrl); aiSelectedPosterUrls.add(du); } } else { if (aiSelectedStillUrls.has(rawUrl)) { aiSelectedStillUrls.delete(rawUrl); aiSelectedStillUrls.add(du); } } });
return card;
};
// 如果接口暂时未返回图片,主动拉取首批
let posterList = posters;
let stillList = stills;
if ((!posterList || posterList.length===0) && info.url) { try { posterList = await getDoubanOfficialPosters(info.url, 1); } catch(e) {} }
if ((!stillList || stillList.length===0) && info.url) { try { stillList = await getDoubanStillsList(info.url, 1); } catch(e) {} }
// 批量DOM插入,降低重排
const pFrag = document.createDocumentFragment();
(posterList||[]).forEach(u=>{ pFrag.appendChild(buildCard(u, 'poster')); });
aiPosterContainer.appendChild(pFrag);
const sFrag = document.createDocumentFragment();
(stillList||[]).forEach(u=>{ sFrag.appendChild(buildCard(u, 'still')); });
aiStillContainer.appendChild(sFrag);
// 当列表条数>=首批数时显示“加载更多”
const pageSize = getConfig().TMDB.IMAGE_CANDIDATES_COUNT || 5;
const hasMorePosters = (posterList && posterList.length >= pageSize);
const hasMoreStills = (stillList && stillList.length >= pageSize);
if (aiLoadMorePosters) { aiLoadMorePosters.style.display = hasMorePosters ? 'inline-block' : 'none'; }
if (aiLoadMoreStills) { aiLoadMoreStills.style.display = hasMoreStills ? 'inline-block' : 'none'; }
}
// 输入时自动弹出搜索结果列表(500ms防抖)
if (aiSearchInput) {
let debounceTimer;
aiSearchInput.addEventListener('input', function(){
clearTimeout(debounceTimer);
const v = this.value.trim();
if (!v) { if (aiSearchResults) aiSearchResults.style.display = 'none'; return; }
debounceTimer = setTimeout(async ()=>{
if (isMainFlowActive) return; // 主流程激活时隔离AI检索
setAiSearchLoading(true, '搜索中...');
const [d, t] = await Promise.all([searchDouban(v).catch(()=>[]), searchTMDB(v).catch(()=>[])]);
const unique = rankAndDedupResults([...(d||[]), ...(t||[])], v).slice(0,60);
aiSearchResults.style.display = 'block';
aiSearchResults.innerHTML = '';
displaySearchResults(unique, aiSearchResults);
// 重新绑定点击(避免多次搜索后因旧监听失效而无反应)
aiSearchResults.onclick = null;
aiResultsBusy = false;
aiSearchResults.addEventListener('click', async (ev)=>{
const itemEl = ev.target.closest('.search-item');
if (!itemEl) return;
const url = itemEl.getAttribute('data-url');
if (!url) return;
if (aiResultsBusy) return;
aiResultsBusy = true;
setAiSearchLoading(true, '正在加载图片...');
const detail = await getBasicInfo(url);
await renderAiImageSelection(detail);
// 自动把选中的片名写入提示词(可开关,且会替换旧的“目标影视:”行)
try {
if (aiAutoTitle && aiAutoTitle.checked) {
const p = document.getElementById('ai-prompt-input');
if (p) {
const title = (detail && detail.title) || (itemEl.querySelector('strong')?.textContent)||'';
if (title) {
// 移除旧的“目标影视:xxx”行
let cleaned = (p.value || '').replace(/^\s*目标影视:.*$/gm, '').trim();
p.value = (cleaned ? cleaned + '\n' : '') + `目标影视:${title}`;
}
}
}
// 若关掉开关,则清理已有的“目标影视:”行
else {
const p = document.getElementById('ai-prompt-input');
if (p) p.value = (p.value || '').replace(/^\s*目标影视:.*$/gm, '').trim();
}
} catch(_) {}
setAiSearchLoading(false);
aiResultsBusy = false;
});
setAiSearchLoading(false);
}, 500);
});
}
const copyAiResultBtn = document.getElementById('copy-ai-result');
const insertAiResultBtn = document.getElementById('insert-ai-result');
// 绑定AI角色设置按钮点击事件 - 已移除入口,保留空函数避免引用错误
function setupAIRoleSettingsBtn() { return; }
// 立即执行一次
setupAIRoleSettingsBtn();
// 已移除轮询与降级逻辑,避免无意义的DOM操作
// 切换AI功能类型时更新提示框占位符
if (aiFunctionSelect && aiPromptInput) {
aiFunctionSelect.addEventListener('change', function() {
const selectedId = this.value;
const config = getConfig();
const selectedFeature = config.AI.FEATURES.find(f => f.id === selectedId);
if (selectedFeature && selectedFeature.placeholder) {
aiPromptInput.placeholder = selectedFeature.placeholder;
}
});
}
// 生成AI文本
if (generateAiTextBtn) {
// 防抖:避免重复触发导致多次请求
let isGenerating = false;
generateAiTextBtn.addEventListener('click', async function(e) {
e.stopPropagation();
e.preventDefault();
if (isGenerating) return;
isGenerating = true;
if (abortAiBtn) abortAiBtn.style.display = 'inline-block';
const functionType = aiFunctionSelect.value;
const promptText = aiPromptInput.value.trim();
const styleSelectionEl = document.getElementById('ai-style-select');
const styleChoice = styleSelectionEl ? (styleSelectionEl.value || '') : '';
const config = getConfig();
const apiEndpoint = config.AI.API_ENDPOINT;
const apiKey = config.AI.API_KEY;
if (!promptText) {
showStatus('请输入生成提示', true, 'ai');
return;
}
if (!apiEndpoint) {
showStatus('请配置AI API端点', true, 'ai');
return;
}
// 显示加载状态
generateAiTextBtn.disabled = true;
generateAiTextBtn.innerHTML = '生成中...';
aiResultArea.style.display = 'none';
aiResultContent.textContent = '';
try {
// 获取AI侧影片信息(与主流程隔离)
let movieContext = '';
const refInfo = aiCurrentMovieInfo || currentMovieInfo;
if (refInfo) {
movieContext = `\n\n参考影视信息:\n名称:${refInfo.title}\n类型:${refInfo.genreTags?.join('、') || '未知'}\n简介:${refInfo.intro?.substring(0, 300) || '暂无简介'}`;
}
// 获取编辑框内容作为上下文
let editorContext = '';
const editor = getCurrentEditor();
if (editor) {
let editorContent = '';
if (editor.type === 'codemirror') {
editorContent = editor.instance.getValue();
} else if (editor.instance) {
// 处理普通textarea
if (editor.instance.value) {
editorContent = editor.instance.value;
}
// 处理contenteditable元素
else if (editor.instance.getAttribute('contenteditable') === 'true') {
editorContent = editor.instance.textContent || editor.instance.innerText;
}
}
if (editorContent && editorContent.trim()) {
console.log('Successfully got editor content, length:', editorContent.length);
// 只取前1000个字符,避免上下文过长
const trimmedContent = editorContent.substring(0, 1000);
editorContext = `\n\n编辑框内容(作为参考):\n${trimmedContent}`;
} else {
console.log('Editor found but content is empty');
}
} else {
if (!editorNotFoundLogged) {
console.log('No editor found');
editorNotFoundLogged = true;
}
}
// 生成完整的提示词(附加固定框架提示+预设模板+已选图片URL)
// 去掉“让AI学习排版文件”的旧提示拼接
const presetHead = '';
const presetText = '';
// 收集多选图片
// 仅使用AI面板选择的图片,绝不读取主工具选择
const posterListForAI = Array.from(aiSelectedPosterUrls || []);
const stillListForAI = Array.from(aiSelectedStillUrls || []);
// 向后兼容:若无多选,则回退到单选值
// 不回退到主工具选择:无选择则按“无图”策略
// 控制上下文体积,避免超长
const maxImagesForPrompt = 6;
const posterForPrompt = posterListForAI.slice(0, maxImagesForPrompt);
const stillForPrompt = stillListForAI.slice(0, maxImagesForPrompt);
// 将长链接压缩为可识别的短标识(避免占用大量token)
const toShortRef = (u)=>{
try {
const s = String(u);
if (/doubanio\.com/.test(s)) {
const m = s.match(/\/p(\d+)/); // 取图片ID
return m ? `db:p${m[1]}` : 'db:img';
}
if (/image\.tmdb\.org\/t\/p\//.test(s)) {
const m = s.match(/t\/p\/[^/]+\/([^/?#]+)/);
return m ? `tmdb:${m[1].slice(0,16)}` : 'tmdb:img';
}
// 其他来源:仅保留文件名
const tail = s.split('/')[(s.split('/').length-1)] || '';
return tail ? tail.slice(0,18) : 'img';
} catch(_) { return 'img'; }
};
const posterRefs = posterForPrompt.map(toShortRef);
const stillRefs = stillForPrompt.map(toShortRef);
const imageContext = `${posterRefs.length ? `\n\n[海报参考ID] ${posterRefs.length} 张\n${posterRefs.join(' ')}` : '\n\n[海报参考ID] 0 张'}${stillRefs.length ? `\n\n[剧照参考ID] ${stillRefs.length} 张\n${stillRefs.join(' ')}` : '\n\n[剧照参考ID] 0 张'}`;
// 影片基础信息上下文(确保标题/导演/主演/评分/海报/简介/热评)
const base = aiCurrentMovieInfo || currentMovieInfo || {};
const safeIntro = (base.intro||'').replace(/\s+/g,' ').slice(0,800);
const safeComments = (base.comments && base.comments.length)?`\n观众热评:${base.comments.slice(0,3).map(c=>c.content.slice(0,140)).join(' / ')}`:'';
const baseInfo = `\n\n[影片信息]\n标题:${base.title||''}\n原名:${base.originalTitle||''}\n又名:${base.alsoKnown||''}\n类型:${(base.genreTags||[]).join('、')}\n地区:${base.region||''}\n语言:${base.lang||''}\n上映:${base.release||''}\n片长:${base.runtime||''}\n导演:${base.director||''}\n编剧:${base.writer||''}\n主演:${base.actor||''}\nIMDb:${base.imdbId||''}\n评分:${base.rating||''}\n关键词:${(base.keywords||'').toString().slice(0,120)}\n平台:${(base.streamingPlatforms||[]).join('、').slice(0,60)}\n获奖:${(base.awards||[]).join('、').slice(0,120)}\n海报参考:${posterRefs[0]||''}\n剧情简介:${safeIntro}${safeComments}`;
const styleHint = styleChoice ? (
styleChoice === '万象合流'
? `\n\n[风格要求]\n采用“万象合流”混合风格:可跨文化、跨体裁自适应(专业严谨/简洁实用/活泼有趣/学术/幽默/文艺/复古/赛博/国潮等),按影片题材与情绪在段落级别灵活切换,但整体语气与视觉层级保持一致、自然、可读。`
: `\n\n[风格要求]\n请以“${styleChoice}”风格组织语言与排版。`
) : '';
// 标题锁定,避免串片
const refTitle = (base && base.title) ? String(base.title) : '';
const lockTitle = refTitle ? `\n\n[标题锁定]\n仅针对《${refTitle}》生成内容,禁止替换为其他影片。若无足够信息请明确标注“未知”。` : '';
// 无图策略:明确指令不生成任何图片容器
const noImageRule = (!posterRefs.length && !stillRefs.length) ? '\n\n[无图模式]\n禁止输出任何与图片相关的标题、容器或占位(包括海报、剧照、图集等),按纯文本/信息卡排版。' : '';
const combinedPrompt = `${presetHead}\n\n${presetText}\n\n${promptText}${styleHint}${lockTitle}${imageContext}${noImageRule}${baseInfo}`;
// 组合并裁剪提示,避免超长
const merged = getAIPromptByType(functionType, combinedPrompt, movieContext + editorContext);
const MAX_PROMPT_CHARS = 60000; // 兼容多数提供商
const fullPrompt = merged.length > MAX_PROMPT_CHARS ? merged.slice(0, MAX_PROMPT_CHARS) : merged;
// 调用AI API生成文本
const aiResult = await generateAIText(apiEndpoint, apiKey, fullPrompt);
// 显示结果
aiResultContent.textContent = aiResult;
aiResultArea.style.display = 'block';
showStatus('AI文本生成成功', false, 'ai');
// 确保搜索相关元素状态正常恢复
setTimeout(() => {
const searchInput = document.getElementById('search-movie');
const fetchBtn = document.getElementById('fetch-btn');
const mediaUrlInput = document.getElementById('media-url');
if (mediaUrlInput && fetchBtn) {
// 确保获取按钮状态正常
if (mediaUrlInput.value.trim()) {
fetchBtn.classList.add('active'); // 使用active类而不是display属性
fetchBtn.style.display = 'block';
} else {
fetchBtn.classList.remove('active');
fetchBtn.style.display = 'none';
}
}
// 重置搜索结果状态,确保搜索功能可以正常使用
if (mediaUrlInput) {
// 不清除用户已经输入的URL,但确保交互功能可用
setupSearchInteractions();
}
}, 500);
} catch (error) {
console.error('AI生成失败:', error);
showStatus(`AI生成失败:${error.message || '未知错误'}`, true, 'ai');
} finally {
// 恢复按钮状态
generateAiTextBtn.disabled = false;
generateAiTextBtn.innerHTML = '生成AI文本';
isGenerating = false;
if (abortAiBtn) abortAiBtn.style.display = 'none';
}
});
aiEventsBound = true;
}
// 终止按钮
if (abortAiBtn) {
abortAiBtn.addEventListener('click', function(e){
e.stopPropagation();
e.preventDefault();
if (aiCurrentRequest && typeof aiCurrentRequest.abort === 'function') {
try { aiCurrentRequest.abort(); } catch (_) {}
}
if (typeof aiAbortReject === 'function') {
try { aiAbortReject(new Error('已终止')); } catch (_) {}
}
abortAiBtn.style.display = 'none';
showStatus('已终止AI生成', true, 'ai');
});
}
// 清理按钮:重置AI结果、候选与搜索状态
const aiClearBtn = document.getElementById('ai-clear');
if (aiClearBtn) {
aiClearBtn.addEventListener('click', function(e){
e.stopPropagation();
e.preventDefault();
try {
if (aiResultArea) { aiResultArea.style.display = 'none'; }
if (aiResultContent) { aiResultContent.textContent = ''; }
const poster = document.getElementById('ai-poster-candidates');
const still = document.getElementById('ai-still-candidates');
if (poster) poster.innerHTML = '';
if (still) still.innerHTML = '';
aiSelectedPosterUrls.clear();
aiSelectedStillUrls.clear();
const aiSearchInputEl = document.getElementById('ai-search-input');
const aiSearchResultsEl = document.getElementById('ai-search-results');
if (aiSearchInputEl) aiSearchInputEl.value = '';
if (aiSearchResultsEl) { aiSearchResultsEl.innerHTML=''; aiSearchResultsEl.style.display='none'; }
selectedPosterUrl = '';
selectedStillUrl = '';
showStatus('已清理AI结果与候选区', false, 'ai');
} catch (_) {}
});
}
// 复制AI结果
if (copyAiResultBtn) {
copyAiResultBtn.addEventListener('click', function(e) {
e.stopPropagation();
e.preventDefault();
const resultText = aiResultContent.textContent;
if (resultText) {
GM_setClipboard(resultText);
showStatus('AI生成结果已复制到剪贴板', false, 'ai');
}
});
}
// 插入AI结果到编辑框
if (insertAiResultBtn) {
insertAiResultBtn.addEventListener('click', async function(e) {
e.stopPropagation();
e.preventDefault();
const resultText = aiResultContent.textContent;
if (!resultText) {
showStatus('没有可插入的AI生成结果', true, 'ai');
return;
}
const ok = await writeHtmlToAnyEditor(resultText);
try { await autoClickSaveBtn(); } catch(_) {}
showStatus(ok ? 'AI生成结果已插入并保存' : '插入失败,已提供剪贴板备份', !ok, 'ai');
});
}
}
// 用户AI预设模板(内置)
try {
if (!window.AI_PRESET_TEMPLATE) {
window.AI_PRESET_TEMPLATE = `
【新海诚治愈神作】你的名字。(君の名は。)
你的名字。主视觉海报(彗星与系守镇)
电影名称:你的名字。(君の名は。)
导演:新海诚
类型:动画 · 爱情 · 奇幻
上映时间:2016-08-26(日本) / 2016-12-02(中国大陆)
豆瓣评分: 8.5
核心元素:身体互换 · 时空交错 · 结绳传说
【电影剧情】
住在东京的少年泷,某天醒来发现自己变成了陌生少女的身体——她是住在深山小镇“系守镇”的三叶。与此同时,三叶也在泷的身体里醒来,面对繁华的东京街头手足无措。两人开始通过日记、便签交流,在彼此的生活里“扮演”对方:泷帮三叶拯救濒临废弃的神社,三叶帮泷赢得暗恋前辈的好感。
随着了解加深,情愫渐生,可他们从未见过面。直到某天,身体互换突然停止,泷发现三叶所在的系守镇,竟在三年前的彗星撞击中消失。为了拯救三叶和小镇,他带着仅存的记忆踏上寻找之旅,在神社的“御神体”前,通过结绳的力量穿越时空,与三叶在黄昏之时(たそがれ)相遇——那是“非日非夜,非人非鬼”的特殊时刻,也是他们跨越时空的唯一交集。
【经典画面】
“黄昏之时”——泷与三叶跨越时空触碰的瞬间(结绳缠绕的手是关键伏笔)
【观众热评】
“第一次看时没懂结绳的意义,二刷才发现:它是时间的象征,是三叶和泷的羁绊,也是拯救小镇的关键。新海诚太会把细腻的情感藏在画面里了——最后两人在街头问‘你的名字是?’时,我哭了半小时。”
—— 豆瓣用户 @星尘收集者
【观影提示】① 建议留意“结绳”“口嚼酒”“黄昏之时”三个关键元素,它们是串联时空的伏笔;② 搭配RADWIMPS的原声《前前前世》观看,音乐响起时会更沉浸;③ 结尾的“名字”梗适合二刷回味——你会发现,两人的羁绊早有伏笔。
`;
}
} catch (e) {}
// 根据类型获取AI提示词
function getAIPromptByType(type, userPrompt, movieContext) {
// 获取详细排版指南
const { POST_FORMAT_GUIDELINES } = DEFAULT_CONFIG.AI;
const prompts = {
summary: `请根据用户需求和提供的影视信息,生成一个高质量的剧情简介。\n用户需求:${userPrompt}${movieContext}`,
comment: `请根据用户需求和提供的影视信息,生成一个专业、有深度的评论摘要。\n用户需求:${userPrompt}${movieContext}`,
tagline: `请根据用户需求和提供的影视信息,生成几个吸引人的宣传标语。\n用户需求:${userPrompt}${movieContext}`,
analysis: `请根据用户需求和提供的影视信息,生成一个深入的分析。\n用户需求:${userPrompt}${movieContext}`,
// 影视资源帖排版美化智能体框架功能提示词 - 增强版
post_format: `作为专业的影视资源帖排版美化智能体,你需要基于编辑器提供的源代码内容,结合用户需求和影视信息,生成一个高度精美的HTML排版方案。\n\n用户需求:${userPrompt}\n\n【强制要求】\n请严格按照以下要求输出:\n1. 只返回完整的HTML代码,不要包含任何额外的文字说明、解释或注释\n2. 确保HTML代码是格式正确、可直接使用的,不要返回源代码形式的HTML\n3. 所有样式必须使用内联CSS,不要使用外部样式表\n4. 生成的HTML代码应直接展示为格式化的内容,而不是显示HTML标签\n\n【内容处理优先级】\n1. 优先使用编辑器内容作为基础进行美化排版\n2. 当编辑器内容不足时,结合影视信息进行补充\n3. 最后根据用户需求进行调整\n\n【核心样式和结构要求】\n请确保最终生成的HTML代码包含以下关键设计元素:\n1. **整体容器**:使用max-width: 800px; margin: 25px auto;的居中容器\n2. **主题配色**:根据影片类型选择主题色(动画用活泼色如#a2d2ff、电影用质感色如#2a9d8f、剧集用柔和色)\n3. **标题样式**:使用大号字体、主色、下边框装饰,如:text-align: center; font-size: 26px; color: #2a9d8f; border-bottom: 2px dashed #e9c46a;\n4. **海报区域**:使用带边框的居中海报,如:border: 2px solid #e9c46a; border-radius: 10px; text-align: center;\n5. **信息卡**:使用白色背景、圆角边框的信息卡,如:background: #fff; padding: 15px; border-radius: 8px; border: 1px solid #e2e8f0;\n6. **评分样式**:使用主色背景的标签式评分,如:color: #fff; background: #2a9d8f; padding: 2px 8px; border-radius: 15px;\n7. **内容分区**:使用下划线虚线的子标题区分不同内容模块,如:text-decoration: underline dotted #e9c46a;\n8. **影评区域**:官方影评使用主色左侧边框,用户评论使用次要色左侧边框,如:border-left: 4px solid #2a9d8f;\n9. **合规提示**:使用浅色背景的合规提示框,如:background-color: #f8f9fa; border-radius: 5px;\n\n【内容模块要求】\n请包含以下完整模块结构:\n1. **标题区**:精美装饰的主标题,包含影片名称和类型标签\n2. **海报展示**:高清海报展示,包含来源标注\n3. **信息卡**:完整的影视参数,包括导演、主演、类型、上映日期、评分等\n4. **剧情简介**:清晰的剧情描述,可选择性使用折叠面板\n5. **经典场景**:剧照展示区域\n6. **观众热评**:官方推荐和用户评论区,区分不同评论类型\n7. **观影提示**:观影建议和注意事项\n8. **合规声明**:支持正版影视的声明\n\n【技术实现要求】\n- 所有样式使用内联CSS,确保兼容性\n- 使用Microsoft Yahei等中文字体,保证可读性\n- 确保文本内容完整、准确、流畅\n- 使用适当的间距和留白,避免内容拥挤\n- 确保所有模块都有明确的视觉边界和层次感${movieContext}`,
content_optimize: `作为专业的内容优化助手,你需要根据用户需求和影视信息,提供详细的内容优化建议。\n\n用户需求:${userPrompt}\n\n优化方向:\n1. **SEO优化**:合理融入影片关键词、类型词、导演演员名称等\n2. **流量优化**:\n - 标题使用吸引人的表述,包含影片亮点\n - 开头段落简明扼要,突出核心吸引力\n - 使用emoji或特殊标记提升内容可辨识度(如适用)\n3. **合规优化**:\n - 替换敏感表述:将"百度云"替换为"合规平台","免费观看"改为"正版渠道观看"\n - 检查影评内容,确保无侵权引用,保留必要的作者署名\n4. **结构优化**:\n - 调整信息呈现顺序,重要内容前置\n - 使用合适的标题层级和排版元素划分内容\n - 增加可读性:剧情文本行高设为1.7倍,段落间距20px\n\n多平台适配建议:\n- 掘金:保留代码高亮,优化图片懒加载\n- 微信公众号:简化样式,优化防盗链格式\n- 知乎:优化首图尺寸,调整段落间距${movieContext}`,
format_check: `作为专业的排版合规检查工具,你需要根据用户需求和影视信息,对内容进行全面的合规性检查。\n\n用户需求:${userPrompt}\n\n检查重点:\n1. **版权检查**:\n - 海报是否为官方发布的宣传海报,避免使用影院偷拍的正片截图\n - 是否在图片说明中注明"用于影视推荐合理使用"\n - 用户影评是否注明来源,专业影评引用是否保留作者署名且不超过原文1/3\n - 资源链接是否仅推荐正规视频平台(如腾讯视频、爱奇艺等)\n2. **敏感词检查**:\n - 扫描是否包含"盗版"、"枪版"、"百度云"、"网盘"等风险词汇\n - 检查影评中是否有过激表述或不当言论\n3. **排版结构检查**:\n - 模块顺序是否合理,是否包含完整的头图、信息卡、剧情、影评等核心模块\n - 样式是否统一,间距、字体、颜色等是否符合规范\n - 响应式设计是否完善,在375px、768px、1200px断点下是否正常显示\n4. **平台规则检查**:\n - 是否避免使用特殊符号(如★、→等)\n - 评分是否使用文本或CSS实现而非emoji\n - 是否包含"支持正版影视"的合规声明\n\n请生成详细的检查报告,指出问题并提供修改建议${movieContext}`,
modular_design: `作为专业的模块化排版设计师,你需要根据用户需求和影视信息,设计一个完整的模块化排版方案。\n\n用户需求:${userPrompt}\n\n设计原则:\n1. **分章节展示**:\n - 头图区:自适应海报容器,16:9比例,包含来源标注\n - 信息卡区:网格布局,展示导演、类型、上映日期、评分等核心参数\n - 剧情简介区:防剧透折叠面板,分安全版和完整版\n - 影评区:区分官方推荐和用户评论,使用不同样式标识\n - 资源提示区:合规声明和正规平台链接推荐\n2. **醒目重点**:\n - 使用主题色突出标题和重要信息\n - 关键内容使用加粗或背景高亮(background:#fff380;padding:0 3px;border-radius:2px)\n - 评分使用主题色背景的标签样式\n3. **便于阅读**:\n - 统一使用"Microsoft Yahei, sans-serif"字体\n - 正文行高1.7倍,段落间距20px\n - 使用列表和分隔线组织内容,提升可读性\n4. **视觉层次**:\n - 建立清晰的标题层级(h1>h2>h3)\n - 使用不同的边框、背景色区分不同类型的内容模块\n - 合理使用空白区域,避免内容过于拥挤\n\n请提供详细的HTML和CSS代码示例,包括各模块的具体实现${movieContext}`,
free_text: `${userPrompt}${movieContext}`
};
let base = prompts[type] || `请根据用户需求生成内容。\n用户需求:${userPrompt}${movieContext}`;
// 统一追加图片占位说明,避免AI输出真实图片链接(支持精确放置)
base += `\n\n[排版占位规则]\n请勿在内容中粘贴任何图片URL。可用占位符:\n- {#POSTER#}: 主海报(可选,仅1次)\n- {#IMG1#}..{#IMGN#}: 我会按你给出的编号依次替换为已选剧照(逐一放置,适合自定义布局)\n- {#STILLS#}: 若未使用逐一占位,使用此占位一次性放入所有剧照(我会做自适应流式布局)。`;
return base;
}
// 调用AI API生成文本
async function generateAIText(apiEndpoint, apiKey, prompt) {
return new Promise((resolve, reject) => {
// 获取配置的模型名称和提供商
const config = getConfig();
const modelName = config.AI.DEFAULT_MODEL;
const provider = config.AI.PROVIDER;
// 判断是否为排版美化相关功能
const isFormatRelated = prompt.includes('排版美化') || prompt.includes('内容优化') || prompt.includes('排版合规') || prompt.includes('模块化排版');
// 根据功能类型设置不同的系统提示
const roleConfig = getAIRoleConfig();
let systemPrompt = generateSystemPrompt(roleConfig, isFormatRelated);
// 深度思考/联网补充(仅在支持时生效)
try {
const deep = document.getElementById('ai-deep-think');
const web = document.getElementById('ai-web-browse');
if (deep && !deep.disabled && deep.checked) {
systemPrompt += '\n请在生成前进行结构化深度思考与多步推理,先在脑内列出要点再输出结果。';
}
if (web && !web.disabled && web.checked) {
systemPrompt += '\n如信息明显缺失或不确定,可模拟联网补充(无需真实请求),在结果中补全缺失信息并标注来源类型。';
}
} catch(_) {}
let max_tokens = 1000;
if (isFormatRelated) {
// 排版相关功能通常需要更长的输出内容
max_tokens = 2000;
}
// 根据不同的AI提供商构建适当的请求体
let requestBody = {};
// 检查AI提供商类型,使用不同的请求格式
if (provider === 'gemini' || apiEndpoint.includes('gemini')) {
// Gemini API格式
requestBody = {
contents: [{
parts: [{
text: prompt
}]
}],
generationConfig: {
maxOutputTokens: max_tokens,
temperature: 0.7
}
};
} else if (provider === 'claude' || apiEndpoint.includes('claude')) {
// Claude API格式
requestBody = {
model: modelName,
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: prompt }
],
max_tokens_to_sample: max_tokens,
temperature: 0.7
};
} else if (provider === 'glm4' || apiEndpoint.includes('bigmodel')) {
// 智谱AI API格式
requestBody = {
model: modelName,
prompt: {
text: prompt
},
parameters: {
max_tokens: max_tokens,
temperature: 0.7
}
};
} else if (provider === 'qwen' || apiEndpoint.includes('aliyuncs')) {
// 通义千问API格式
requestBody = {
model: modelName,
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: prompt }
],
max_tokens: max_tokens,
temperature: 0.7
};
} else if (provider === 'xunfei' || apiEndpoint.includes('xf-yun')) {
// 讯飞星火API格式
requestBody = {
header: {
app_id: apiKey.split('.')[0], // 假设app_id在apiKey的第一部分
uid: 'user'
},
parameter: {
chat: {
domain: 'general',
temperature: 0.7,
max_tokens: max_tokens
}
},
payload: {
message: {
text: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: prompt }
]
}
}
};
} else if (provider === 'ark' || apiEndpoint.includes('doubao')) {
// 豆包API格式
requestBody = {
model: modelName,
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: prompt }
],
max_tokens: max_tokens,
temperature: 0.7
};
} else if (provider === 'together' || apiEndpoint.includes('together')) {
// Together AI格式
requestBody = {
model: modelName,
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: prompt }
],
max_tokens: max_tokens,
temperature: 0.7
};
} else {
// 默认使用OpenAI API格式
requestBody = {
model: modelName,
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: prompt }
],
max_tokens: max_tokens,
temperature: 0.7
};
}
// 构建请求头 - 根据不同提供商处理认证
const headers = {
'Content-Type': 'application/json'
};
// 根据AI提供商设置正确的认证方式
if (apiKey) {
// 提供商特定的认证方式
if (provider === 'ark' || apiEndpoint.includes('doubao')) {
// 豆包API使用的认证方式
headers['Authorization'] = `Bearer ${apiKey}`;
} else if (provider === 'claude' || apiEndpoint.includes('claude')) {
// Claude API认证
headers['Authorization'] = `Bearer ${apiKey}`;
} else if (provider === 'gemini' || apiEndpoint.includes('gemini')) {
// Gemini API认证
headers['x-goog-api-key'] = apiKey;
} else if (provider === 'github' || apiEndpoint.includes('github')) {
// GitHub模型市场认证
headers['Authorization'] = `Bearer ${apiKey}`;
} else if (provider === 'xunfei' || apiEndpoint.includes('xf-yun')) {
// 讯飞星火认证
headers['Authorization'] = `Bearer ${apiKey}`;
} else if (provider === 'qwen' || apiEndpoint.includes('aliyuncs')) {
// 通义千问认证
headers['Authorization'] = `Bearer ${apiKey}`;
} else if (provider === 'glm4' || provider === 'legalglm' || apiEndpoint.includes('bigmodel')) {
// 智谱AI认证
headers['Authorization'] = `Bearer ${apiKey}`;
} else if (provider === 'huggingface' || apiEndpoint.includes('huggingface')) {
// Hugging Face认证
headers['Authorization'] = `Bearer ${apiKey}`;
} else if (provider === 'together' || apiEndpoint.includes('together')) {
// Together AI认证
headers['Authorization'] = `Bearer ${apiKey}`;
} else if (provider === 'baishan' || apiEndpoint.includes('baishanai')) {
// 白山云认证
headers['Authorization'] = `Bearer ${apiKey}`;
} else if (provider === 'gpt4free' || apiEndpoint.includes('gpt4free')) {
// 一些免费API可能不需要标准Bearer格式
headers['Authorization'] = apiKey;
} else {
// 默认认证方式(OpenAI兼容)
headers['Authorization'] = `Bearer ${apiKey}`;
}
}
// 发送请求
console.log(`AI API请求 - 端点: ${apiEndpoint}, 提供商: ${provider}, 模型: ${modelName}`);
console.log(`请求头:`, headers);
console.log(`请求体结构:`, JSON.stringify(requestBody).substring(0, 500) + (JSON.stringify(requestBody).length > 500 ? '...' : ''));
// 验证API端点格式
try {
new URL(apiEndpoint);
} catch (e) {
throw new Error(`API端点格式无效:${apiEndpoint}\n请检查配置的API端点是否为有效的URL格式`);
}
// 发起请求并保存控制句柄
aiAbortReject = reject;
const req = GM_xmlhttpRequest({
method: 'POST',
url: apiEndpoint,
headers: headers,
data: JSON.stringify(requestBody),
onload: (response) => {
try {
console.log(`API响应状态码: ${response.status}`);
console.log(`API响应头:`, response.responseHeaders);
// 检查响应状态
if (response.status < 200 || response.status >= 400) {
let errorMsg = `请求失败:HTTP ${response.status}`;
// 特殊处理404错误
if (response.status === 404) {
errorMsg = `请求失败:HTTP 404\nAPI端点不存在或无效: ${apiEndpoint}\n请检查配置的API端点是否正确,不同AI提供商的API端点格式可能不同`;
// 根据提供商类型提供更具体的建议
if (provider === 'openai') {
errorMsg += '\nOpenAI API端点格式应为: https://api.openai.com/v1/chat/completions';
} else if (provider === 'claude') {
errorMsg += '\nClaude API端点格式应为: https://api.anthropic.com/v1/messages';
} else if (provider === 'gemini') {
errorMsg += '\nGemini API端点格式应为: https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent';
}
}
try {
const errorData = JSON.parse(response.responseText);
if (errorData.error && errorData.error.message) {
errorMsg = errorData.error.message;
// 特殊处理模型访问错误
if (errorMsg.includes('model does not exist') || errorMsg.includes('not have access')) {
errorMsg = `模型访问失败:${errorMsg}\n请在AI配置中更换为您有权限访问的模型`;
}
// 特殊处理认证错误
if (errorMsg.includes('Invalid bearer token') ||
errorMsg.includes('API key format is incorrect') ||
errorMsg.includes('authentication')) {
errorMsg = `认证失败:${errorMsg}\n请检查API密钥格式是否正确,不同AI提供商可能有不同的密钥格式要求`;
}
}
} catch (e) {
// 如果解析错误信息失败,使用原始错误
console.error('解析错误响应失败:', e);
}
throw new Error(errorMsg);
}
const data = JSON.parse(response.responseText);
// 处理不同API的响应格式
let resultText = '';
if (data.choices && data.choices.length > 0) {
// OpenAI、通义千问、讯飞星火等API格式
resultText = data.choices[0].message?.content || data.choices[0].text || '';
} else if (data.result) {
// GLM-4、豆包等API格式
resultText = data.result;
} else if (data.text) {
// Together AI等API格式
resultText = data.text;
} else if (data.candidates && data.candidates.length > 0) {
// Gemini API格式
resultText = data.candidates[0].content?.parts?.[0]?.text || '';
} else if (Array.isArray(data) && data.length > 0 && data[0].generated_text) {
// Hugging Face API格式
resultText = data[0].generated_text || '';
} else if (data.output?.choices && data.output.choices.length > 0) {
// 其他API格式
resultText = data.output.choices[0].text || '';
} else {
throw new Error('未找到有效结果');
}
if (!resultText.trim()) {
throw new Error('AI返回了空结果');
}
// 去除三引号围栏 ```xxx 开头/结尾
let cleaned = resultText.trim();
cleaned = cleaned.replace(/^```[a-zA-Z]*\n/, '').replace(/\n```$/, '');
// 将占位符替换为真实图片HTML
const pickPoster = () => {
try {
const list = Array.from(aiSelectedPosterUrls || []);
let url = list[0] || '';
try { url = toTMDBOriginal(url); } catch(_) {}
return url || '';
} catch(_) { return ''; }
};
const pickStills = () => {
try {
const list = Array.from(aiSelectedStillUrls || []);
return (list && list.length? list : []).slice(0,6);
} catch(_) { return []; }
};
if (cleaned.includes('{#POSTER#}')) {
const p = pickPoster();
const posterHtml = p ? `` : '';
// 只替换第一次,其他占位清空,避免重复
cleaned = cleaned.replace('{#POSTER#}', posterHtml);
cleaned = cleaned.replace(/\{#POSTER#\}/g, '');
}
if (cleaned.includes('{#STILLS#}') || /\{#IMG\d+#\}/.test(cleaned)) {
const arr = pickStills();
let html = '';
const n = Array.isArray(arr) ? arr.length : 0;
const toImg = (u, radius='10px') => {
const src = toTMDBOriginal ? toTMDBOriginal(u) : u;
return `
`;
};
// 若AI使用精确占位 {#IMGn#},则按编号逐张替换,给足自由编排空间
if (/\{#IMG\d+#\}/.test(cleaned) && n > 0) {
let placed = cleaned;
for (let i = 0; i < n; i++) {
const tag = new RegExp(`\\{#IMG${i+1}#\\}`,'g');
placed = placed.replace(tag, toImg(arr[i]));
}
// 清理未用完的编号占位
placed = placed.replace(/\{#IMG\d+#\}/g, '');
cleaned = placed;
}
if (n === 1) {
html = arr[0] ? `${toImg(arr[0],'12px')}
` : '';
} else if (n === 2) {
html = `${toImg(arr[0])}${toImg(arr[1])}
`;
} else if (n === 3) {
// 上1下2,避免右侧空白
const top = arr[0] ? `${toImg(arr[0],'12px')}
` : '';
const bottom = `${toImg(arr[1])}${toImg(arr[2])}
`;
html = `${top}${bottom}
`;
} else if (n === 4) {
html = `${arr.map(u=>toImg(u)).join('')}
`;
} else if (n === 5) {
// 上1(大图)+ 下4(2列)
const top = `${toImg(arr[0],'12px')}
`;
const rest = `${arr.slice(1).map(u=>toImg(u)).join('')}
`;
html = `${top}${rest}
`;
} else if (n >= 6) {
html = `${arr.map(u=>toImg(u)).join('')}
`;
}
// 若未使用{#IMGn#},再处理{#STILLS#} 的一次性替换
if (cleaned.includes('{#STILLS#}')) {
cleaned = cleaned.replace('{#STILLS#}', html);
cleaned = cleaned.replace(/\{#STILLS#\}/g, '');
}
}
// 清理非占位符产生的多余
与孤立属性片段
try {
cleaned = cleaned.replace(/
]*data-ai-img=\"1\")[^>]*>/gi, '');
cleaned = cleaned.replace(/\salt=\"[^\"]*\"[^>]*>/gi, '>');
} catch(_) {}
resolve(cleaned.trim());
} catch (error) {
console.error('解析AI响应失败:', error, response.responseText);
reject(new Error(`解析响应失败:${error.message}`));
}
},
onerror: (error) => {
console.error('AI API请求失败:', error);
reject(new Error(`网络请求失败:${error.message || '未知错误'}`));
},
ontimeout: () => {
reject(new Error('AI API请求超时'));
}
});
aiCurrentRequest = req;
});
}
// 初始化页面
function init() {
// 仅在面板内部禁用表单验证(不影响站点表单)
disableAllFormValidation();
// 加载Font Awesome图标(确保美化工具图标正常显示)
const faLink = document.createElement('link');
faLink.rel = 'stylesheet';
faLink.href = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css';
faLink.crossOrigin = 'anonymous'; // 解决跨域加载问题
// 确保head存在才添加
if (document.head) {
document.head.appendChild(faLink);
} else {
// 如果head还没加载,延迟添加
setTimeout(() => {
if (document.head) {
document.head.appendChild(faLink);
}
}, 100);
}
// 插入控制面板(精准位置+空值修复)
insertPanelInMarkedPosition();
// 检查是否有默认URL(从URL参数中获取)
const urlParams = new URLSearchParams(window.location.search);
const mediaUrl = urlParams.get('mediaUrl');
if (mediaUrl && document.getElementById('media-url')) {
document.getElementById('media-url').value = mediaUrl;
const fetchBtn = document.getElementById('fetch-btn');
// 根据输入框内容设置提取按钮状态
if (fetchBtn) {
const mediaUrlInput = document.getElementById('media-url');
if (mediaUrlInput && mediaUrlInput.value.trim()) {
fetchBtn.classList.add('active');
} else {
fetchBtn.classList.remove('active');
}
}
}
}
// 启动脚本
init();
// 额外的安全检查 - 确保switchTab函数在全局作用域可访问
setTimeout(function() {
if (typeof window.switchTab !== 'function') {
// 如果window上没有switchTab函数,尝试重新暴露
if (typeof unsafeWindow !== 'undefined') {
window.switchTab = unsafeWindow.switchTab || switchTab;
} else {
window.switchTab = switchTab;
}
}
// 修复HTML中的onclick属性调用
const tabButtons = document.querySelectorAll('[onclick^="switchTab("]');
tabButtons.forEach(button => {
const originalOnclick = button.getAttribute('onclick');
if (originalOnclick) {
// 提取tabId参数
const match = originalOnclick.match(/switchTab\('([^']+)'\)/);
if (match && match[1]) {
const tabId = match[1];
// 移除原始的onclick属性
button.removeAttribute('onclick');
// 使用addEventListener添加点击事件
button.addEventListener('click', function(e) {
e.stopPropagation();
e.preventDefault();
if (typeof window.switchTab === 'function') {
window.switchTab(tabId);
} else if (typeof document.switchTab === 'function') {
document.switchTab(tabId);
}
});
}
}
});
}, 100);
// ========== AI角色配置系统(保留内部使用,移除UI入口) ==========
// 影视类型色彩配置
const MOVIE_GENRE_COLORS = {
horror: { name: '恐怖', primary: '#2d3142', secondary: '#ef8354', lightBg: '#f5f5f5', text: '#333333' },
romance: { name: '爱情', primary: '#f8b195', secondary: '#f8e1d1', lightBg: '#fff8f5', text: '#8b4513' },
action: { name: '动作', primary: '#335c67', secondary: '#e09f3e', lightBg: '#f9f7f1', text: '#3a3a3a' },
drama: { name: '剧情', primary: '#3a0ca3', secondary: '#4361ee', lightBg: '#f8f9ff', text: '#333333' },
comedy: { name: '喜剧', primary: '#ffb347', secondary: '#fdfd96', lightBg: '#fffef0', text: '#5d4037' },
sciFi: { name: '科幻', primary: '#4169e1', secondary: '#87cefa', lightBg: '#f0f8ff', text: '#333333' },
fantasy: { name: '奇幻', primary: '#9370db', secondary: '#e6e6fa', lightBg: '#f8f5ff', text: '#4a4a4a' },
anime: { name: '动漫', primary: '#ff69b4', secondary: '#ffb6c1', lightBg: '#fff0f5', text: '#8b4513' },
documentary: { name: '纪录片', primary: '#708090', secondary: '#d3d3d3', lightBg: '#f5f5f5', text: '#333333' },
thriller: { name: '惊悚', primary: '#264653', secondary: '#2a9d8f', lightBg: '#f1faee', text: '#333333' },
adventure: { name: '冒险', primary: '#e9c46a', secondary: '#f4a261', lightBg: '#fffbeb', text: '#5d4037' },
musical: { name: '歌舞', primary: '#e76f51', secondary: '#f4a261', lightBg: '#fff5f5', text: '#5d4037' }
};
// AI角色配置默认值
const AI_ROLE_DEFAULTS = {
role: '专注影视资源帖排版美化的创意助手',
personality: '审美敏锐(懂排版视觉逻辑)、严谨合规(熟平台过滤规则)、高效实用(模块化出稿)、富有创意(适配不同影视风格)',
style: '简洁直白,用“设计师视角”讲排版思路;重点突出,同时说明“美化效果+合规技巧”;专业术语+通俗表达结合',
tone: '友好、专业、鼓励性',
specialRequirements: '1.输出用基础HTML+内联CSS(无复杂框架,多平台可复制);\n2.自动规避特殊符号(如emoji→【】)、敏感词(如“网盘”→“合规路径”);\n3.固定模块:标题区→核心信息卡→影视简介→剧照展示→热评模块→合规提示,每个模块带视觉分层(边框/背景/间距);\n4.配色贴合类型:动画活泼、电影质感、剧集柔和,单帖主色≤3种。',
// 影视类型偏好配置
preferredGenre: '',
autoApplyColors: true,
// 色彩主题配置
primaryColor: '#ff69b4',
secondaryColor: '#ffb6c1',
lightBgColor: '#fff0f5',
textColor: '#8b4513',
// 排版配置
titleFontSize: 26,
bodyFontSize: 16,
lineHeight: 1.8,
paragraphMargin: 15
};
// AI角色模板系统
const AI_ROLE_TEMPLATES = {
professional: {
name: '专业影评人',
role: '专业的影视内容创作助手和影评人',
personality: '知识渊博、专业严谨、见解独到',
style: '深入分析、逻辑清晰、细节丰富、引用专业术语',
tone: '专业、客观、学术性',
specialRequirements: '请结合影视理论进行分析,适当引用经典电影案例'
},
casual: {
name: '影视爱好者',
role: '热情的影视爱好者和推荐人',
personality: '活泼开朗、热情洋溢、善于表达',
style: '口语化、亲切自然、情感丰富',
tone: '友好、热情、有感染力',
specialRequirements: '请使用轻松愉快的语言,避免过于专业的术语'
},
humorous: {
name: '幽默评论员',
role: '幽默风趣的影视评论员',
personality: '幽默风趣、机智活泼、善于调侃',
style: '诙谐幽默、语言生动、比喻巧妙',
tone: '轻松、搞笑、充满活力',
specialRequirements: '请适当加入幽默元素,使用生动有趣的表达方式'
},
academic: {
name: '影视学者',
role: '严谨的影视研究者和教育者',
personality: '严谨治学、逻辑严密、学识渊博',
style: '学术化、系统化、理论深厚',
tone: '庄重、学术、教导性',
specialRequirements: '请从学术角度分析,结合相关理论和研究成果'
},
postFormatter: {
name: '影视资源帖排版美化师',
role: '专注影视资源帖排版美化的创意助手',
personality: '审美敏锐(懂排版视觉逻辑)、严谨合规(熟悉平台过滤规则)、高效实用(模块化出稿)、富有创意(适配不同影视风格)',
style: '简洁直白,用"设计师视角"讲排版思路;重点突出,同时说明"美化效果+合规技巧";专业术语+通俗表达结合',
tone: '友好耐心,像专业排版师一样提供step-by-step指导;鼓励性强,降低用户操作压力',
specialRequirements: '1.输出用基础HTML+内联CSS(无复杂框架,多平台可复制);\n2.自动规避特殊符号(如emoji→【】)、敏感词(如"网盘"→合规平台);\n3.固定模块:标题区→核心信息→影视简介→剧照展示→热评模块→合规提示,每个模块带视觉分层(边框/背景/间距);\n4.配色贴合类型:动画用活泼色、电影用质感色、剧集用柔和色,单帖主色≤3种。'
}
};
// 生成AI系统提示
function generateSystemPrompt(roleConfig, isFormatRelated = false) {
const { role, personality, style, tone, specialRequirements } = roleConfig;
let basePrompt = `你是${role}。`;
basePrompt += `你的性格特点是:${personality}。`;
basePrompt += `你的语言风格是:${style}。`;
basePrompt += `你的语气是:${tone}。`;
if (specialRequirements) {
basePrompt += `特别要求:${specialRequirements}。`;
}
if (isFormatRelated) {
// 平台过滤认知与排版原则(精简版,低Token)
basePrompt += `\n\n平台过滤:强标准化+安全管控+样式阉割。仅保留基础原子样式;复杂布局/渐变/阴影/圆角/媒体查询等多会失效。结构以table/tr/td与简单div为白名单;外链资源受限,内联style优先。`;
basePrompt += `\n\n排版原则(6步):\n1) 布局:优先表格;多列用内层table,两列50%均分。\n2) 色彩:单色系三层次(主色10-20%,辅助30-40%,正文#444)。\n3) 分层:统一符号+加粗+字号;正文行高1.8,必要处首行缩进。\n4) 图片:按平台规则统一尺寸,max-width:100%; height:auto。\n5) 间距:统一padding/margin;模块用浅色边框分隔。\n6) 细节:仅用基础元素做精致感(边框/前缀符号/按钮浅底主色字)。`;
}
return basePrompt;
}
// 获取保存的AI角色配置
function getAIRoleConfig() {
try {
const saved = GM_getValue('ai_role_config', null);
return saved ? JSON.parse(saved) : { ...AI_ROLE_DEFAULTS };
} catch (e) {
console.error('读取AI角色配置失败:', e);
return { ...AI_ROLE_DEFAULTS };
}
}
// 保存AI角色配置
function saveAIRoleConfig(config) {
try {
GM_setValue('ai_role_config', JSON.stringify(config));
return true;
} catch (e) {
console.error('保存AI角色配置失败:', e);
return false;
}
}
// 打开AI角色配置界面(开发预览下可用)
function openAIRoleConfigUI() {
const config = getAIRoleConfig();
// 创建配置界面HTML
const dialogHTML = `
`;
// 关闭对话框 - 安全版本
function closeDialog() {
try {
const dialog = document.querySelector('div[style*="position: fixed"]');
if (dialog && document.body.contains(dialog)) {
document.body.removeChild(dialog);
} else if (dialog) {
// 如果dialog不是body的直接子元素,尝试找到其实际的父元素
const parent = dialog.parentNode;
if (parent) {
parent.removeChild(dialog);
}
}
} catch (e) {
console.warn('关闭对话框时出现错误:', e);
// 作为最后手段,尝试隐藏元素
const dialog = document.querySelector('div[style*="position: fixed"]');
if (dialog) {
dialog.style.display = 'none';
}
}
}
// 添加到页面 - 包含安全检查
const tempContainer = document.createElement('div');
tempContainer.innerHTML = dialogHTML;
const dialogElement = tempContainer.firstElementChild;
// 确保body存在才添加
if (document.body) {
document.body.appendChild(dialogElement);
} else {
// 如果body还没加载,延迟添加
setTimeout(() => {
if (document.body) {
document.body.appendChild(dialogElement);
}
}, 100);
}
// 获取元素并添加事件监听
const dialog = dialogElement;
const closeBtn = dialog.querySelector('#close-dialog');
const saveBtn = dialog.querySelector('#save-config');
const resetBtn = dialog.querySelector('#reset-config');
const templateSelect = dialog.querySelector('#template-select');
// 确保closeBtn存在再绑定事件
if (closeBtn) {
closeBtn.addEventListener('click', closeDialog);
} else {
console.error('未找到关闭按钮');
}
// 点击对话框外部(遮罩层)不关闭 - 根据用户需求修改
dialog.addEventListener('click', function(e) {
// 空函数,不执行任何关闭操作
});
// 确保点击内容区域也不关闭对话框
const dialogContent = dialog.querySelector('div[style*="background-color: white"]');
if (dialogContent) {
dialogContent.addEventListener('click', function(e) {
e.stopPropagation(); // 阻止事件冒泡到遮罩层
});
}
// 模板选择事件
templateSelect.addEventListener('change', function() {
const templateId = this.value;
if (templateId !== 'custom' && AI_ROLE_TEMPLATES[templateId]) {
const template = AI_ROLE_TEMPLATES[templateId];
document.getElementById('role-input').value = template.role;
document.getElementById('personality-input').value = template.personality;
document.getElementById('style-input').value = template.style;
document.getElementById('tone-input').value = template.tone;
document.getElementById('requirements-input').value = template.specialRequirements;
}
});
// 应用选中类型的配色
const applyGenreColorsBtn = dialog.querySelector('#apply-genre-colors');
if (applyGenreColorsBtn) {
applyGenreColorsBtn.addEventListener('click', function(e) {
e.stopPropagation();
e.preventDefault();
const selectedGenre = document.getElementById('preferred-genre').value;
if (selectedGenre && MOVIE_GENRE_COLORS[selectedGenre]) {
const colors = MOVIE_GENRE_COLORS[selectedGenre];
document.getElementById('primary-color').value = colors.primary;
document.getElementById('secondary-color').value = colors.secondary;
document.getElementById('light-bg-color').value = colors.lightBg;
document.getElementById('text-color').value = colors.text;
showStatus('已应用' + colors.name + '类型的配色方案', false, 'ai');
} else {
showStatus('请先选择一个影视类型', true, 'ai');
}
});
}
// 保存配置
saveBtn.addEventListener('click', function(e) {
e.stopPropagation();
e.preventDefault();
const autoApplyColorsValue = document.querySelector('input[name="auto-apply-colors"][type="radio"]:checked').value;
const newConfig = {
role: document.getElementById('role-input').value,
personality: document.getElementById('personality-input').value,
style: document.getElementById('style-input').value,
tone: document.getElementById('tone-input').value,
specialRequirements: document.getElementById('requirements-input').value,
// 影视类型偏好配置
preferredGenre: document.getElementById('preferred-genre').value,
autoApplyColors: autoApplyColorsValue === 'yes',
// 色彩主题配置
primaryColor: document.getElementById('primary-color').value,
secondaryColor: document.getElementById('secondary-color').value,
lightBgColor: document.getElementById('light-bg-color').value,
textColor: document.getElementById('text-color').value,
// 排版配置
titleFontSize: parseInt(document.getElementById('title-font-size').value),
bodyFontSize: parseInt(document.getElementById('body-font-size').value),
lineHeight: parseFloat(document.getElementById('line-height').value),
paragraphMargin: parseInt(document.getElementById('paragraph-margin').value)
};
if (saveAIRoleConfig(newConfig)) {
alert('配置已保存!');
closeDialog();
} else {
alert('保存失败,请重试!');
}
});
// 重置配置
resetBtn.addEventListener('click', function(e) {
e.stopPropagation();
e.preventDefault();
if (confirm('确定要重置为默认配置吗?')) {
// 重置AI角色配置
document.getElementById('role-input').value = AI_ROLE_DEFAULTS.role;
document.getElementById('personality-input').value = AI_ROLE_DEFAULTS.personality;
document.getElementById('style-input').value = AI_ROLE_DEFAULTS.style;
document.getElementById('tone-input').value = AI_ROLE_DEFAULTS.tone;
document.getElementById('requirements-input').value = AI_ROLE_DEFAULTS.specialRequirements;
templateSelect.value = 'custom';
// 重置影视类型配置
document.getElementById('preferred-genre').value = '';
document.querySelector('input[name="auto-apply-colors"][value="yes"]').checked = true;
// 重置色彩配置(默认粉黛仙境主题)
document.getElementById('primary-color').value = '#ff69b4';
document.getElementById('secondary-color').value = '#ffb6c1';
document.getElementById('light-bg-color').value = '#fff0f5';
document.getElementById('text-color').value = '#8b4513';
// 重置排版配置
document.getElementById('title-font-size').value = 26;
document.getElementById('body-font-size').value = 16;
document.getElementById('line-height').value = 1.8;
document.getElementById('paragraph-margin').value = 15;
}
});
}
// 粉黛仙境主题式提取影片信息功能 - 专门用于提取豆瓣和TMDB影片资源并以特定排版粘贴到编辑框
const emeraldButton = document.getElementById('emerald-city-format');
if (emeraldButton) {
emeraldButton.addEventListener('click', function(e) {
e.stopPropagation();
e.preventDefault();
try {
// 直接写入编辑器,无需切换源代码对话框
// 轻微延迟,确保编辑器可用
setTimeout(async function() {
showStatus('开始应用粉黛仙境主题式影片资源提取...', false);
// 使用全局变量中已从豆瓣和TMDB获取的电影信息
if (!currentMovieInfo) {
showStatus('请先搜索影片并选择海报和剧照', true);
return;
}
// 从currentMovieInfo中获取完整的影片信息
const title = currentMovieInfo.title || '电影名称';
const director = currentMovieInfo.director || '未知';
const writer = currentMovieInfo.writer || '未知';
const actor = currentMovieInfo.actor || '未知';
const genreTags = currentMovieInfo.genreTags || [];
const type = genreTags.join('、') || '未知';
const releaseDate = currentMovieInfo.release || '未知';
const rating = currentMovieInfo.rating || '9.0';
const mainContent = currentMovieInfo.intro || '暂无剧情简介内容';
const originalTitle = currentMovieInfo.originalTitle || '';
const alsoKnown = currentMovieInfo.alsoKnown || '';
const region = currentMovieInfo.region || '未知';
const lang = currentMovieInfo.lang || '未知';
const runtime = currentMovieInfo.runtime || '未知';
const imdbId = currentMovieInfo.imdbId || '暂无';
const keywords = currentMovieInfo.keywords || '';
const budget = currentMovieInfo.budget || '未知';
const revenue = currentMovieInfo.revenue || '未知';
const streamingPlatforms = currentMovieInfo.streamingPlatforms || [];
const awards = currentMovieInfo.awards || [];
// 智能识别内容类型(电影、动漫、电视剧)
let contentType = '电影';
const animeKeywords = ['动漫', '动画', '番剧', '卡通', '二次元'];
const tvKeywords = ['电视剧', '剧集', '网剧', '韩剧', '日剧', '美剧', '英剧', '国产剧', '集数'];
if (currentMovieInfo.genreTags && currentMovieInfo.genreTags.some(tag => animeKeywords.some(keyword => tag.includes(keyword)))) {
contentType = '动漫';
} else if (currentMovieInfo.genreTags && currentMovieInfo.genreTags.some(tag => tvKeywords.some(keyword => tag.includes(keyword)))) {
contentType = '电视剧';
} else if (currentMovieInfo.mediaType === 'tv') {
contentType = '电视剧';
} else if (mainContent && animeKeywords.some(keyword => mainContent.includes(keyword))) {
contentType = '动漫';
} else if (mainContent && tvKeywords.some(keyword => mainContent.includes(keyword))) {
contentType = '电视剧';
}
// 获取已选择的海报和剧照URL
const posterUrl = selectedPosterUrl || `https://via.placeholder.com/600x400/ff69b4/FFF?text=${encodeURIComponent(title)}`;
const stillUrl = selectedStillUrl || `https://via.placeholder.com/600x300/ff69b4/FFF?text=${encodeURIComponent(title)+'经典场景'}`;
// 创建粉黛仙境风格排版,用于展示从豆瓣和TMDB提取的完整影片资源
// 根据内容类型自动适配色彩
let primaryColor = '#ff69b4'; // 默认主色调:粉色
let secondaryColor = '#ffb6c1'; // 默认辅助色:浅粉色
let lightBgColor = '#fff0f5'; // 默认浅背景色:淡粉色
let lightBorderColor = '#ffcce7'; // 默认浅边框色:超浅粉色
let accentColor = '#db7093'; // 默认强调色:深粉色
let textColor = '#8b4513'; // 默认文本主色
// 根据类型标签识别并应用对应的色彩系统
const config = getConfig();
if (config.AI && config.AI.POST_FORMAT_GUIDELINES && config.AI.POST_FORMAT_GUIDELINES.VISUAL_ENHANCEMENT && config.AI.POST_FORMAT_GUIDELINES.VISUAL_ENHANCEMENT.COLOR_SYSTEM && genreTags && genreTags.length > 0) {
// 类型关键词映射
const typeKeywords = {
'horror': ['恐怖', '惊悚', '悬疑'],
'romance': ['爱情', '浪漫', '恋爱'],
'action': ['动作', '冒险', '武侠'],
'drama': ['剧情', '文艺', '传记'],
'comedy': ['喜剧', '搞笑', '幽默'],
'sciFi': ['科幻', '未来', '太空'],
'fantasy': ['奇幻', '魔幻', '玄幻'],
'anime': ['动漫', '动画', '番剧'],
'documentary': ['纪录片', '纪录']
};
// 查找匹配的类型
for (const [type, keywords] of Object.entries(typeKeywords)) {
if (genreTags.some(tag => keywords.some(keyword => tag.includes(keyword)))) {
if (config.AI.POST_FORMAT_GUIDELINES.VISUAL_ENHANCEMENT.COLOR_SYSTEM[type]) {
primaryColor = config.AI.POST_FORMAT_GUIDELINES.VISUAL_ENHANCEMENT.COLOR_SYSTEM[type][0];
secondaryColor = config.AI.POST_FORMAT_GUIDELINES.VISUAL_ENHANCEMENT.COLOR_SYSTEM[type][1];
// 根据主色调生成其他相关色彩
switch(type) {
case 'horror':
lightBgColor = '#2d3142';
lightBorderColor = '#4f5d75';
accentColor = '#ef8354';
textColor = '#f9f7f3';
break;
case 'romance':
lightBgColor = '#fff9f4';
lightBorderColor = '#f8d8c6';
accentColor = '#f8b195';
textColor = '#7b5d4b';
break;
case 'action':
lightBgColor = '#f8f9fa';
lightBorderColor = '#e0e0e0';
accentColor = '#335c67';
textColor = '#333333';
break;
case 'drama':
lightBgColor = '#f9f7fd';
lightBorderColor = '#e8e2f9';
accentColor = '#3a0ca3';
textColor = '#4a4a4a';
break;
case 'comedy':
lightBgColor = '#fffdf5';
lightBorderColor = '#fff1cc';
accentColor = '#ffb347';
textColor = '#8b4513';
break;
case 'sciFi':
lightBgColor = '#f0f7ff';
lightBorderColor = '#c5e0ff';
accentColor = '#4169e1';
textColor = '#2c3e50';
break;
case 'fantasy':
lightBgColor = '#fcfaff';
lightBorderColor = '#e8e4ff';
accentColor = '#9370db';
textColor = '#4a4a4a';
break;
case 'anime':
lightBgColor = '#fff0f5';
lightBorderColor = '#ffcce7';
accentColor = '#ff69b4';
textColor = '#8b4513';
break;
case 'documentary':
lightBgColor = '#f5f5f5';
lightBorderColor = '#e0e0e0';
accentColor = '#708090';
textColor = '#333333';
break;
}
break;
}
}
}
}
// 处理类型显示格式(用·分隔)
let formattedType = type;
if (genreTags && genreTags.length > 0) {
formattedType = genreTags.join('·');
}
// 创建新的排版模板
const emeraldStyledContent = `
【${contentType}】${title}${originalTitle && originalTitle !== title ? `(${originalTitle})` : ''}
${title}${contentType === '电影' ? '电影' : ''}海报
作品名称: ${title}${originalTitle && originalTitle !== title ? `(${originalTitle})` : ''}
导演/主创: ${director || '暂无信息'}
编剧: ${writer || '暂无信息'}
主演: ${actor.length > 30 ? actor.substring(0, 30) + '...' : actor || '暂无信息'}
类型: ${formattedType || '暂无信息'}
制片地区: ${region || '暂无信息'}
语言: ${lang || '暂无信息'}
片长: ${runtime || '暂无信息'}
上线时间: ${releaseDate || '暂无信息'}
${alsoKnown ? `
又名: ${alsoKnown}
` : ''}
${imdbId && imdbId !== '暂无' ? `
IMDb ID: ${imdbId}
` : ''}
评分: ${rating || '暂无'}
【${contentType === '电影' ? '电影' : contentType === '动漫' ? '动漫' : '电视剧'}故事】
${mainContent ? mainContent.split('。').filter(s => s.trim()).map(paragraph => `
${paragraph.trim()}。
`).join('') : `
暂无剧情简介内容。
`}
【精彩画面】
${title}经典场景
【观众热评】
“每次看${title}都会有不同的感受,故事情节引人入胜,人物形象鲜明,是一部值得反复观看的经典作品。”
—— 豆瓣用户 @影视爱好者
【${contentType === '电影' ? '观影' : '追剧'}提示】${title}值得反复看,每次都有新感悟。推荐找个安静下午,沉浸式感受这部作品的魅力。
`;
// 预览与归档
try { updateLivePreview(emeraldStyledContent, { step: '粉黛仙境预览' }); } catch(_) {}
// 直接写入编辑器(统一助手,覆盖TinyMCE/CodeMirror/textarea/iframe/contenteditable)
const ok = await writeHtmlToAnyEditor(emeraldStyledContent);
// 保存(按钮自动点击,失败不抛错)
try { await autoClickSaveBtn(); } catch(_) {}
if (ok) {
showStatus('粉黛仙境内容已直接写入编辑器并保存', false);
try { logPreviewStep('写入编辑器并保存', true); } catch(_) {}
} else {
showStatus('写入失败,已提供剪贴板备份', true);
try { logPreviewStep('写入失败,已回退到剪贴板', false); } catch(_) {}
}
}, 1000); // 1秒延迟
} catch (error) {
showStatus('绿野仙踪主题式影片资源提取失败,请检查是否已选择影片', true);
console.error('绿野仙踪主题式影片资源提取错误:', error);
}
});
}
// 注册油猴菜单项
try {
GM_registerMenuCommand('🔧 脚本配置', createConfigDialog);
// 移除外部菜单入口:仅在预览模式下保留,避免打扰实际使用
try {
const isPreview = /[?&]preview=1\b/.test(location.search) || localStorage.getItem('script_preview') === '1';
if (isPreview) GM_registerMenuCommand('设置AI角色', openAIRoleConfigUI);
} catch (e) {}
} catch (e) {
console.error('注册菜单项失败:', e);
// 降级方案:在控制台提供信息
console.log('=== 油猴菜单注册失败,您可以通过控制台执行 createConfigDialog() 打开配置界面 ===');
}
// 修改generateAIText函数以使用用户配置的角色信息
// 由于无法直接修改已定义的函数,我们需要在脚本中找到该函数并修改
// 以下是修改后的函数核心部分,用户可以手动替换原函数
/* 修改建议:在generateAIText函数中,将设置systemPrompt的部分替换为:
const roleConfig = getAIRoleConfig();
let systemPrompt = generateSystemPrompt(roleConfig, isFormatRelated);
*/
// 豆瓣模拟登录函数
function performDoubanLogin(username, password, statusElement) {
// 模拟登录成功
setTimeout(() => {
if (statusElement && statusElement.parentNode) {
statusElement.textContent = '✅ 模拟登录成功!';
statusElement.style.color = '#065f46';
statusElement.style.backgroundColor = '#d1fae5';
// 保存登录信息
try {
const savedConfig = localStorage.getItem('apiConfig');
const config = savedConfig ? JSON.parse(savedConfig) : {};
config.doubanUsername = username;
config.doubanLoginTime = new Date().toISOString();
config.doubanLoginStatus = 'simulated';
localStorage.setItem('apiConfig', JSON.stringify(config));
console.log('豆瓣模拟登录信息已保存');
} catch (storageError) {
console.error('保存模拟登录信息失败:', storageError);
}
// 显示成功通知
try {
showNotification('豆瓣模拟登录成功!', 'success');
} catch (notifyError) {
console.warn('显示通知失败:', notifyError);
}
// 更新编辑器内容
ensureEditorHasContent();
}
}, 1000);
}
// 豆瓣登录函数
function isolatedDoubanLogin() {
// 获取用户名和密码输入框
const usernameInput = document.querySelector('input[name="username"], input[type="text"]');
const passwordInput = document.querySelector('input[name="password"], input[type="password"]');
if (!usernameInput || !passwordInput) {
console.error('未找到用户名或密码输入框');
return;
}
const username = usernameInput.value.trim();
const password = passwordInput.value.trim();
if (!username || !password) {
console.error('用户名或密码不能为空');
return;
}
// 创建状态显示元素
let statusElement = document.getElementById('login-status');
if (!statusElement) {
statusElement = document.createElement('div');
statusElement.id = 'login-status';
statusElement.style.cssText = 'position:fixed;top:20px;right:20px;padding:10px;border-radius:5px;z-index:50;font-size:14px;';
document.body.appendChild(statusElement);
}
statusElement.textContent = '🔄 正在登录...';
statusElement.style.color = '#1e40af';
statusElement.style.backgroundColor = '#dbeafe';
// 由于跨域限制,直接使用模拟登录
console.log('由于浏览器安全限制,使用模拟登录模式');
// 显示回退通知
try {
showNotification('由于浏览器安全限制,正在使用模拟登录模式...', 'info');
} catch (notifyError) {
console.warn('显示通知失败:', notifyError);
}
// 延迟后执行模拟登录
setTimeout(() => {
if (statusElement && statusElement.parentNode) {
performDoubanLogin(username, password, statusElement);
}
}, 1000);
}
// 确保编辑器有完整内容
function ensureEditorHasContent() {
try {
// 首先调用确保标题非空的函数
ensureTitleIsNotEmpty();
const editor = window.editor || { instance: { value: '' } };
// 确保editor.instance存在
if (!editor.instance) {
console.warn('编辑器实例不存在,无法确保内容');
return;
}
let currentContent = '';
// 安全获取当前编辑器内容
try {
if (editor.type === 'codemirror' && typeof editor.instance.getValue === 'function') {
currentContent = editor.instance.getValue() || '';
} else if (typeof editor.instance.value !== 'undefined') {
currentContent = editor.instance.value || '';
}
} catch (contentError) {
console.warn('获取编辑器内容失败:', contentError);
currentContent = '';
}
// 如果编辑器内容仍然为空,尝试恢复备份
if (!currentContent.trim()) {
const restored = restoreBackupHtml();
// 如果没有备份或恢复失败,创建更完整的默认内容
if (!restored) {
try {
// 创建安全的默认HTML内容
const defaultContent = `【豆瓣影视资源】
\n已完成豆瓣账号登录,您可以开始搜索和提取影视信息。
\n使用提示:搜索影片名称,选择合适的海报和剧照,然后点击确认填充按钮。
\n提示:如需手动复制内容,可使用页面中的复制功能按钮。
`;
// 保存备份
saveBackupHtml(defaultContent);
// 安全填充内容到编辑器
if (editor.type === 'codemirror' && typeof editor.instance.setValue === 'function') {
editor.instance.setValue(defaultContent);
} else if (typeof editor.instance.value !== 'undefined') {
editor.instance.value = defaultContent;
}
} catch (defaultContentError) {
console.error('创建和设置默认内容失败:', defaultContentError);
}
}
}
} catch (error) {
console.error('确保编辑器有内容失败:', error);
}
}
// 从Cookie字符串中提取重要的豆瓣Cookie
function extractImportantCookies(cookiesStr) {
const importantCookies = ['dbcl2', 'bid', 'ck', 'll'];
const cookies = cookiesStr.split('; ');
const result = [];
for (const cookie of cookies) {
const [name, value] = cookie.split('=', 2);
if (importantCookies.includes(name)) {
result.push(`${name}=${value}`);
}
}
return result.join('; ');
}
// 获取TMDB API配置
function getTmdbConfig() {
try {
const savedConfig = localStorage.getItem('apiConfig');
return savedConfig ? JSON.parse(savedConfig) : {};
} catch (error) {
console.error('获取TMDB配置失败:', error);
return {};
}
}
// 获取豆瓣Cookie配置
function getDoubanCookie() {
try {
const savedConfig = localStorage.getItem('apiConfig');
return savedConfig ? JSON.parse(savedConfig).doubanCookie || '' : '';
} catch (error) {
console.error('获取豆瓣Cookie失败:', error);
return '';
}
}
// 加载已保存的账号信息(只加载用户名,不加载密码以保证安全)
function loadSavedAccountInfo() {
try {
const savedConfig = localStorage.getItem('apiConfig');
if (savedConfig) {
const config = JSON.parse(savedConfig);
// 如果之前保存过用户名,则填充用户名输入框
if (config.doubanUsername) {
document.getElementById('douban-username').value = config.doubanUsername;
}
}
// 页面加载时确保编辑器有内容
setTimeout(() => {
ensureEditorHasContent();
}, 500); // 延迟500ms,确保编辑器已经初始化
} catch (error) {
console.error('加载账号信息失败:', error);
// 即使加载失败,也要确保编辑器有内容
ensureEditorHasContent();
}
}
// 复制内容到剪贴板功能
function copyEditorContent() {
try {
const editor = window.editor || { instance: { value: '' } };
let content = '';
// 获取编辑器内容
if (editor.type === 'codemirror') {
content = editor.instance.getValue();
} else if (editor.instance) {
content = editor.instance.value || '';
}
// 如果编辑器内容为空,尝试获取备份
if (!content.trim()) {
content = localStorage.getItem('editorBackupHtml') || '';
}
if (content.trim()) {
// 使用Clipboard API复制内容
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(content).then(() => {
showNotification('✅ 内容已复制到剪贴板!', 'success');
}).catch(err => {
console.error('复制失败:', err);
showNotification('❌ 复制失败,请手动选择复制', 'error');
});
} else {
// 降级方案:创建临时textarea元素
const textArea = document.createElement('textarea');
textArea.value = content;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
// 确保body存在才添加
if (document.body) {
document.body.appendChild(textArea);
} else {
// 如果body还没加载,延迟添加
setTimeout(() => {
if (document.body) {
document.body.appendChild(textArea);
}
}, 100);
}
textArea.focus();
textArea.select();
try {
const successful = document.execCommand('copy');
if (successful) {
showNotification('✅ 内容已复制到剪贴板!', 'success');
} else {
showNotification('❌ 复制失败,请手动选择复制', 'error');
}
} catch (err) {
console.error('复制失败:', err);
showNotification('❌ 复制失败,请手动选择复制', 'error');
}
document.body.removeChild(textArea);
}
} else {
showNotification('⚠️ 没有可复制的内容', 'warning');
}
} catch (error) {
console.error('复制功能异常:', error);
showNotification('❌ 复制功能异常', 'error');
}
}
// 显示通知消息
function showNotification(message, type = 'info', duration = 3000) {
try {
// 确保message是字符串类型
const safeMessage = String(message || '未知消息');
// 检查是否已存在通知元素
try {
let notification = document.getElementById('copy-notification');
if (notification) {
notification.remove();
}
} catch (removeError) {
console.warn('移除现有通知失败:', removeError);
}
// 创建新的通知元素
const notification = document.createElement('div');
notification.id = 'copy-notification';
notification.textContent = safeMessage; // 使用安全的消息内容
// 增强可访问性
notification.setAttribute('role', 'alert');
notification.setAttribute('aria-live', 'assertive');
// 设置样式
notification.style.position = 'fixed';
notification.style.top = '20px';
notification.style.right = '20px';
notification.style.padding = '12px 20px';
notification.style.borderRadius = '8px';
notification.style.fontSize = '14px';
notification.style.fontWeight = '500';
notification.style.zIndex = '99999'; // 增加z-index确保在最上层
notification.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.15)';
notification.style.transition = 'opacity 0.3s ease, transform 0.3s ease, box-shadow 0.3s ease';
notification.style.pointerEvents = 'auto';
notification.style.cursor = 'pointer'; // 提示用户可以点击关闭
notification.style.minWidth = '250px';
notification.style.maxWidth = '400px';
notification.style.wordWrap = 'break-word';
// 初始状态(用于动画效果)
notification.style.opacity = '0';
notification.style.transform = 'translateX(100%) translateY(-20px)';
// 根据类型设置不同颜色
if (type === 'success') {
notification.style.backgroundColor = '#d1fae5';
notification.style.color = '#065f46';
notification.style.border = '1px solid #a7f3d0';
} else if (type === 'error') {
notification.style.backgroundColor = '#fee2e2';
notification.style.color = '#991b1b';
notification.style.border = '1px solid #fecaca';
} else if (type === 'warning') {
notification.style.backgroundColor = '#fef3c7';
notification.style.color = '#92400e';
notification.style.border = '1px solid #fde68a';
} else {
notification.style.backgroundColor = '#dbeafe';
notification.style.color = '#1e40af';
notification.style.border = '1px solid #bfdbfe';
}
// 添加点击关闭功能
notification.addEventListener('click', function() {
try {
if (notification && notification.parentNode) {
notification.style.opacity = '0';
notification.style.transform = 'translateX(100%) translateY(-20px)';
setTimeout(() => {
try {
if (notification && notification.parentNode) {
notification.parentNode.removeChild(notification);
}
} catch (removeError) {
console.warn('移除通知失败:', removeError);
}
}, 300);
}
} catch (clickError) {
console.warn('关闭通知失败:', clickError);
}
});
// 安全添加到文档中
function addNotificationToBody() {
if (document.body && !document.getElementById('copy-notification')) {
document.body.appendChild(notification);
// 强制重排以确保动画效果正常
setTimeout(() => {
try {
if (notification) {
notification.style.opacity = '1';
notification.style.transform = 'translateX(0) translateY(0)';
// 添加微妙的悬停效果
notification.addEventListener('mouseenter', function() {
try {
notification.style.transform = 'translateX(0) translateY(-2px)';
notification.style.boxShadow = '0 6px 16px rgba(0, 0, 0, 0.2)';
} catch (hoverError) {
console.warn('悬停效果设置失败:', hoverError);
}
});
notification.addEventListener('mouseleave', function() {
try {
notification.style.transform = 'translateX(0) translateY(0)';
notification.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.15)';
} catch (leaveError) {
console.warn('离开效果设置失败:', leaveError);
}
});
}
} catch (animationError) {
console.warn('通知动画设置失败:', animationError);
}
}, 10);
}
}
if (document.body) {
addNotificationToBody();
} else {
// 如果body还没加载,延迟添加,最多尝试3次
let attempts = 0;
const maxAttempts = 3;
const interval = setInterval(() => {
attempts++;
if (attempts >= maxAttempts) {
clearInterval(interval);
// 如果多次尝试仍失败,使用alert作为最后的备选
console.warn('多次尝试添加通知失败,使用alert');
alert(safeMessage);
return;
}
if (document.body) {
clearInterval(interval);
addNotificationToBody();
}
}, 100);
}
// 定时自动消失
setTimeout(() => {
try {
if (notification && notification.parentNode) {
notification.style.opacity = '0';
notification.style.transform = 'translateX(100%) translateY(-20px)';
setTimeout(() => {
try {
if (notification && notification.parentNode) {
notification.parentNode.removeChild(notification);
}
} catch (removeError) {
console.warn('移除通知失败:', removeError);
}
}, 300);
}
} catch (timeoutError) {
console.warn('通知超时处理失败:', timeoutError);
}
}, duration);
} catch (error) {
console.error('显示通知失败:', error);
// 如果所有显示方式都失败,使用alert作为最后的备选
try {
alert(String(message || '操作已完成'));
} catch (alertError) {
console.error('显示alert也失败:', alertError);
}
}
}
// 仅禁用“本脚本面板内部”的表单验证,避免影响站点发布/提交
function disableAllFormValidation() {
try {
const panelEl = document.getElementById('douban-tmdb-panel');
if (!panelEl) return;
const forms = panelEl.querySelectorAll('form');
forms.forEach(form => { form.setAttribute('novalidate', 'true'); });
const requiredFields = panelEl.querySelectorAll('input[required], textarea[required], select[required]');
requiredFields.forEach(field => {
field.removeAttribute('required');
field.setAttribute('data-was-required', 'true');
});
console.log('已在面板内部禁用表单验证(不影响站点表单)');
} catch (error) {
console.error('禁用面板表单验证失败:', error);
}
}
// 注册事件监听器
document.addEventListener('DOMContentLoaded', function() {
// 仅在面板内部禁用表单验证(不影响站点表单)
disableAllFormValidation();
// 立即测试豆瓣登录按钮
setTimeout(() => {
const testBtn = document.getElementById('test-douban-login');
// 静默检查按钮状态
if (testBtn) {
// 静默添加测试功能
testBtn.onclick = function() {
alert('按钮点击测试成功!');
};
}
}, 1000);
// 页面完全加载后再次尝试
window.addEventListener('load', function() {
// 静默尝试绑定按钮事件
const loadButton = document.getElementById('test-douban-login');
if (loadButton) {
// 静默找到按钮
loadButton.onclick = function() {
alert('页面加载后绑定测试成功!');
// 静默处理点击事件
return false;
};
} else {
// 静默处理按钮未找到的情况
}
});
// 静默检查按钮状态
// 使用MutationObserver监听按钮变化
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.type === 'childList') {
const addedNodes = Array.from(mutation.addedNodes);
addedNodes.forEach(function(node) {
if (node.nodeType === 1) { // Element node
if (node.id === 'test-douban-login' ||
(node.querySelector && node.querySelector('#test-douban-login'))) {
console.log('发现豆瓣登录按钮被添加:', node);
const button = node.id === 'test-douban-login' ? node : node.querySelector('#test-douban-login');
if (button) {
button.onclick = function() {
alert('MutationObserver捕获到按钮!');
return false;
};
button.style.border = '2px solid blue';
}
}
}
});
}
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
// 移除了全局点击事件监听器,因为它会中断所有按钮的正常点击流程
// 仅保留必要的按钮特定事件监听器
// 立即尝试绑定事件,不等待
const immediateButton = document.getElementById('test-douban-login');
if (immediateButton) {
console.log('立即找到按钮,直接绑定事件');
console.log('按钮HTML:', immediateButton.outerHTML);
// 添加最简单的测试
immediateButton.onclick = function() {
alert('立即绑定测试成功!');
console.log('立即绑定onclick被触发');
return false;
};
// 也尝试添加内联onclick
immediateButton.setAttribute('onclick', 'alert("内联onclick测试成功!"); return false;');
// 添加测试属性
immediateButton.setAttribute('data-test', 'true');
immediateButton.style.border = '2px solid red'; // 添加红色边框来确认按钮
// 添加一个简单的测试函数
window.testButton = function() {
alert('测试函数被调用!');
};
// 尝试直接调用测试函数
setTimeout(() => {
console.log('尝试直接调用测试函数');
window.testButton();
}, 2000);
} else {
console.log('立即未找到按钮,等待DOM加载');
}
// 立即绑定手动配置按钮事件
bindManualConfigButtons();
// 添加全局点击事件监听器(仅面板内元素,避免影响站点按钮)
document.addEventListener('click', function(event) {
console.log('全局点击事件被触发:', event.target);
console.log('点击元素ID:', event.target.id);
console.log('点击元素文本:', event.target.textContent);
const panelEl = document.getElementById('douban-tmdb-panel');
const isInsidePanel = panelEl && panelEl.contains(event.target);
if (!isInsidePanel) return; // 面板外按钮不处理
if (event.target.id === 'test-douban-login') {
console.log('全局监听器捕获到豆瓣登录按钮点击');
event.preventDefault();
event.stopPropagation();
alert('全局监听器:豆瓣登录按钮被点击了!');
isolatedDoubanLogin();
return false;
}
}, true); // 使用捕获阶段
// 等待DOM加载完成后再绑定事件
setTimeout(() => {
// 添加更灵活的按钮查找逻辑
const findAndBindButton = function(textPattern, callback) {
const allButtons = document.querySelectorAll('button, input[type="button"], input[type="submit"]');
for (let button of allButtons) {
const buttonText = button.textContent || button.value || '';
if (buttonText.includes(textPattern)) {
button.addEventListener('click', callback);
console.log(`已绑定包含"${textPattern}"的按钮点击事件`);
return button;
}
}
return null;
};
// 尝试通过文本内容绑定按钮
findAndBindButton('登录', function() {
fillAllRequiredFields();
isolatedDoubanLogin();
});
// 测试豆瓣登录按钮点击事件 - 完全隔离版本
const testButton = document.getElementById('test-douban-login');
console.log('查找豆瓣登录按钮:', testButton);
console.log('所有按钮元素:', document.querySelectorAll('button'));
console.log('所有ID包含douban的元素:', document.querySelectorAll('[id*="douban"]'));
if (testButton) {
console.log('找到豆瓣登录按钮,开始绑定事件');
// 移除旧的事件监听器,确保只使用新的增强版
const newTestButton = testButton.cloneNode(true);
testButton.parentNode.replaceChild(newTestButton, testButton);
// 重新获取引用并绑定增强版事件监听器
const enhancedTestButton = document.getElementById('test-douban-login');
console.log('重新获取按钮引用:', enhancedTestButton);
enhancedTestButton.addEventListener('click', function(event) {
console.log('豆瓣登录按钮被点击!');
// 多重阻断:确保事件完全被阻止,不触发任何表单验证
event.stopPropagation();
event.preventDefault();
if (event.stopImmediatePropagation) {
event.stopImmediatePropagation();
}
// 立即填充所有required字段,防止浏览器验证触发
fillAllRequiredFields();
// 完全隔离的豆瓣登录处理,不触发任何编辑器验证
isolatedDoubanLogin();
return false;
}, true);
// 添加CSS类以明确标识这是一个填充按钮而非提交按钮
enhancedTestButton.classList.add('fill-only-button');
// 设置类型为button,确保不是submit类型
enhancedTestButton.type = 'button';
// 移除所有可能的旧的onclick属性
enhancedTestButton.removeAttribute('onclick');
// 确保事件处理器正确绑定
enhancedTestButton.onclick = function() {
console.log('豆瓣登录按钮被点击,执行登录逻辑');
// 立即填充所有required字段,防止浏览器验证触发
fillAllRequiredFields();
// 执行实际的豆瓣登录逻辑
isolatedDoubanLogin();
return false;
};
console.log('豆瓣登录按钮事件绑定完成,已确保正确执行登录逻辑');
} else {
console.error('未找到豆瓣登录按钮元素');
// 如果找不到按钮,尝试通过其他方式查找
const allButtons = document.querySelectorAll('button');
console.log('页面中所有按钮:', allButtons);
// 查找包含"豆瓣"或"登录"文字的按钮
const doubanButtons = Array.from(allButtons).filter(btn =>
btn.textContent.includes('豆瓣') ||
btn.textContent.includes('登录') ||
btn.id.includes('douban') ||
btn.id.includes('login')
);
console.log('找到的相关按钮:', doubanButtons);
}
// 绑定手动复制内容按钮事件
const copyBtn = document.getElementById('copy-content-btn');
if (copyBtn) {
// 移除旧的事件监听器,确保只使用新的增强版
const newCopyBtn = copyBtn.cloneNode(true);
copyBtn.parentNode.replaceChild(newCopyBtn, copyBtn);
// 重新获取引用并绑定增强版事件监听器
const enhancedCopyBtn = document.getElementById('copy-content-btn');
enhancedCopyBtn.addEventListener('click', function(event) {
// 多重阻断:确保事件完全被阻止,不触发任何表单验证
event.stopPropagation();
event.preventDefault();
if (event.stopImmediatePropagation) {
event.stopImmediatePropagation();
}
// 执行复制功能
copyEditorContent();
return false;
}, true);
// 添加CSS类以明确标识这是一个填充按钮而非提交按钮
enhancedCopyBtn.classList.add('fill-only-button');
// 设置类型为button,确保不是submit类型
enhancedCopyBtn.type = 'button';
}
// 加载已保存的配置
loadManualConfig();
// 加载已保存的账号信息
loadSavedAccountInfo();
}, 100); // 稍等一下确保DOM完全加载
});
// 立即填充所有required字段,防止浏览器表单验证触发
function fillAllRequiredFields() {
try {
// 查找所有带required属性的输入框
const requiredFields = document.querySelectorAll('input[required], textarea[required], select[required]');
requiredFields.forEach(field => {
// 强制填充,不管当前是否有值
if (field.id === 'title' || field.name === 'title' || field.placeholder === '标题') {
field.value = '【豆瓣影视资源】自动生成内容';
} else if (field.type === 'textarea' || field.tagName.toLowerCase() === 'textarea') {
field.value = '正在通过豆瓣账号获取影视信息...';
} else if (field.type === 'text' || field.type === 'email' || field.type === 'tel') {
field.value = '自动填充';
} else if (field.type === 'number') {
field.value = '1';
} else if (field.tagName.toLowerCase() === 'select') {
// 选择第一个非空选项
const options = field.querySelectorAll('option');
for (let option of options) {
if (option.value && option.value.trim() !== '') {
field.value = option.value;
break;
}
}
}
// 触发所有相关事件,确保验证状态更新
field.dispatchEvent(new Event('input', { bubbles: true }));
field.dispatchEvent(new Event('change', { bubbles: true }));
field.dispatchEvent(new Event('blur', { bubbles: true }));
});
console.log('已填充所有required字段,防止表单验证触发');
} catch (error) {
console.error('填充required字段失败:', error);
}
}
// 确保所有required字段都不为空,防止浏览器表单验证触发
function ensureRequiredFieldsNotEmpty() {
try {
// 查找所有带required属性的输入框
const requiredFields = document.querySelectorAll('input[required], textarea[required], select[required]');
requiredFields.forEach(field => {
if (!field.value || field.value.trim() === '') {
// 根据字段类型设置默认值
if (field.id === 'title' || field.name === 'title' || field.placeholder === '标题') {
field.value = '【豆瓣影视资源】自动生成内容';
} else if (field.type === 'textarea' || field.tagName.toLowerCase() === 'textarea') {
field.value = '正在通过豆瓣账号获取影视信息...';
} else {
field.value = '自动填充';
}
// 触发input和change事件,确保验证状态更新
field.dispatchEvent(new Event('input', { bubbles: true }));
field.dispatchEvent(new Event('change', { bubbles: true }));
}
});
} catch (error) {
console.error('确保required字段非空失败:', error);
}
}
// 确保标题字段非空
function ensureTitleIsNotEmpty() {
try {
const editor = window.editor || { instance: { value: '' } };
let currentContent = '';
// 获取当前编辑器内容
if (editor.type === 'codemirror') {
currentContent = editor.instance.getValue();
} else if (editor.instance) {
currentContent = editor.instance.value || '';
}
// 检查标题是否为空,如为空则注入默认标题
if (!currentContent.trim() || !hasValidTitle(currentContent)) {
const defaultTitle = '【豆瓣影视资源】自动生成内容';
const defaultHtml = `${defaultTitle}
\n正在通过豆瓣账号获取影视信息...
`;
// 保存备份内容
saveBackupHtml(defaultHtml);
// 填充内容到编辑器
if (editor.type === 'codemirror') {
editor.instance.setValue(defaultHtml);
} else if (editor.instance) {
editor.instance.value = defaultHtml;
}
}
} catch (error) {
console.error('确保标题非空失败:', error);
}
}
// 检查内容是否包含有效的标题
function hasValidTitle(content) {
// 检查是否包含h1-h6标签或明显的标题结构
return /]*>.*?<\/h[1-6]>/.test(content) ||
/^#\s+.*$/m.test(content) ||
/^【.*?】$/.test(content);
}
// 保存HTML备份
function saveBackupHtml(html) {
try {
// 确保html是字符串类型
const safeHtml = String(html || '');
// 创建或更新备份元素
let backupElement = document.getElementById('backup-html');
if (!backupElement) {
backupElement = document.createElement('input');
backupElement.type = 'hidden';
backupElement.id = 'backup-html';
// 确保在尝试appendChild前body已加载
if (document.body) {
document.body.appendChild(backupElement);
} else {
// 如果body还没加载,延迟添加
setTimeout(() => {
if (document.body && !document.getElementById('backup-html')) {
document.body.appendChild(backupElement);
backupElement.value = safeHtml;
}
}, 100);
}
}
// 安全地设置值
if (backupElement) {
backupElement.value = safeHtml;
}
// 同时保存到localStorage作为额外备份
try {
localStorage.setItem('lastBackupHtml', safeHtml);
} catch (storageError) {
console.warn('无法保存到localStorage:', storageError);
}
} catch (error) {
console.error('保存HTML备份失败:', error);
}
}
// 恢复HTML备份
function restoreBackupHtml() {
try {
const editor = window.editor || { instance: { value: '' } };
let backupHtml = '';
// 优先从页面元素中获取
try {
const backupElement = document.getElementById('backup-html');
if (backupElement && backupElement.value) {
backupHtml = String(backupElement.value || '');
}
} catch (elemError) {
console.warn('从页面元素获取备份失败:', elemError);
}
// 如果没有从页面元素获取到,尝试从localStorage获取
if (!backupHtml.trim()) {
try {
const storedHtml = localStorage.getItem('lastBackupHtml');
backupHtml = String(storedHtml || '');
} catch (storageError) {
console.warn('从localStorage获取备份失败:', storageError);
}
}
// 安全验证和清理HTML内容
if (backupHtml && backupHtml.trim() && (!editor.instance.value || !editor.instance.value.trim())) {
// 简单的HTML验证,确保内容包含有效标签
if (/<[a-z][\s\S]*>/i.test(backupHtml)) {
// 只有当编辑器内容为空时才恢复
try {
if (editor.type === 'codemirror' && editor.instance && typeof editor.instance.setValue === 'function') {
editor.instance.setValue(backupHtml);
} else if (editor.instance) {
editor.instance.value = backupHtml;
}
return true;
} catch (editorError) {
console.error('设置编辑器内容失败:', editorError);
}
} else {
console.warn('备份内容不包含有效的HTML标签');
}
}
return false;
} catch (error) {
console.error('恢复HTML备份失败:', error);
return false;
}
}
// 设置面板功能 - 加载配置到设置面板
function loadConfigToSettingsPanel() {
const config = getConfig();
// 加载TMDB配置
const tmdbApiKeyInput = document.getElementById('tmdb-api-key');
const tmdbAccessTokenInput = document.getElementById('tmdb-access-token');
if (tmdbApiKeyInput) tmdbApiKeyInput.value = config.TMDB.API_KEY || '';
if (tmdbAccessTokenInput) tmdbAccessTokenInput.value = config.TMDB.ACCESS_TOKEN || '';
// 加载AI配置
const aiApiKeyInput = document.getElementById('ai-api-key');
const aiApiEndpointInput = document.getElementById('ai-api-endpoint');
const aiProviderSelect = document.getElementById('ai-provider');
const aiModelInput = document.getElementById('ai-model');
if (aiApiKeyInput) aiApiKeyInput.value = config.AI.API_KEY || '';
if (aiApiEndpointInput) aiApiEndpointInput.value = config.AI.API_ENDPOINT || DEFAULT_CONFIG.AI.API_ENDPOINT;
if (aiProviderSelect) aiProviderSelect.value = config.AI.PROVIDER || DEFAULT_CONFIG.AI.PROVIDER;
if (aiModelInput) aiModelInput.value = config.AI.DEFAULT_MODEL || DEFAULT_CONFIG.AI.DEFAULT_MODEL;
// 绑定AI提供商变更事件
if (aiProviderSelect) {
aiProviderSelect.addEventListener('change', function() {
if (this.value !== 'custom') {
let endpoint = DEFAULT_CONFIG.AI.API_ENDPOINT;
let model = DEFAULT_CONFIG.AI.DEFAULT_MODEL;
switch (this.value) {
case 'openai':
endpoint = 'https://api.openai.com/v1/chat/completions';
model = 'gpt-3.5-turbo';
break;
case 'anthropic':
endpoint = 'https://api.anthropic.com/v1/messages';
model = 'claude-3-opus-20240229';
break;
case 'google':
endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent';
model = 'gemini-pro';
break;
}
if (aiApiEndpointInput && !aiApiEndpointInput.dataset.userModified) {
aiApiEndpointInput.value = endpoint;
}
if (aiModelInput && !aiModelInput.dataset.userModified) {
aiModelInput.value = model;
}
}
});
}
// 标记输入框为用户修改过的
const inputsToWatch = [tmdbApiKeyInput, tmdbAccessTokenInput, aiApiKeyInput, aiApiEndpointInput, aiModelInput];
inputsToWatch.forEach(input => {
if (input) {
input.addEventListener('input', function() {
this.dataset.userModified = 'true';
});
}
});
}
// 设置面板功能 - 保存配置
function saveSettingsFromPanel() {
const config = getConfig();
// 获取TMDB配置
const tmdbApiKeyInput = document.getElementById('tmdb-api-key');
const tmdbAccessTokenInput = document.getElementById('tmdb-access-token');
if (tmdbApiKeyInput) config.TMDB.API_KEY = tmdbApiKeyInput.value.trim();
if (tmdbAccessTokenInput) config.TMDB.ACCESS_TOKEN = tmdbAccessTokenInput.value.trim();
// 保持基础URL使用默认值
config.TMDB.BASE_URL = DEFAULT_CONFIG.TMDB.BASE_URL;
// 获取AI配置
const aiApiKeyInput = document.getElementById('ai-api-key');
const aiApiEndpointInput = document.getElementById('ai-api-endpoint');
const aiProviderSelect = document.getElementById('ai-provider');
const aiModelInput = document.getElementById('ai-model');
if (aiApiKeyInput) config.AI.API_KEY = aiApiKeyInput.value.trim();
if (aiApiEndpointInput) config.AI.API_ENDPOINT = aiApiEndpointInput.value.trim() || DEFAULT_CONFIG.AI.API_ENDPOINT;
if (aiProviderSelect) config.AI.PROVIDER = aiProviderSelect.value;
if (aiModelInput) config.AI.DEFAULT_MODEL = aiModelInput.value.trim() || DEFAULT_CONFIG.AI.DEFAULT_MODEL;
// 保存配置
try {
saveConfig(config);
showStatus('设置已保存', false, 'settings');
return true;
} catch (error) {
showStatus('保存设置失败: ' + error.message, true, 'settings');
console.error('保存配置失败:', error);
return false;
}
}
// 设置面板功能 - 重置为默认配置
function resetSettingsToDefault() {
if (confirm('确定要将所有设置重置为默认值吗?这将清除您的API密钥等配置。')) {
// 重置TMDB配置
const tmdbApiKeyInput = document.getElementById('tmdb-api-key');
const tmdbAccessTokenInput = document.getElementById('tmdb-access-token');
if (tmdbApiKeyInput) {
tmdbApiKeyInput.value = '';
delete tmdbApiKeyInput.dataset.userModified;
}
if (tmdbAccessTokenInput) {
tmdbAccessTokenInput.value = '';
delete tmdbAccessTokenInput.dataset.userModified;
}
// 重置AI配置
const aiApiKeyInput = document.getElementById('ai-api-key');
const aiApiEndpointInput = document.getElementById('ai-api-endpoint');
const aiProviderSelect = document.getElementById('ai-provider');
const aiModelInput = document.getElementById('ai-model');
if (aiApiKeyInput) {
aiApiKeyInput.value = '';
delete aiApiKeyInput.dataset.userModified;
}
if (aiApiEndpointInput) {
aiApiEndpointInput.value = DEFAULT_CONFIG.AI.API_ENDPOINT;
delete aiApiEndpointInput.dataset.userModified;
}
if (aiProviderSelect) aiProviderSelect.value = DEFAULT_CONFIG.AI.PROVIDER;
if (aiModelInput) {
aiModelInput.value = DEFAULT_CONFIG.AI.DEFAULT_MODEL;
delete aiModelInput.dataset.userModified;
}
// 保存默认配置
const defaultConfig = {
TMDB: {
...DEFAULT_CONFIG.TMDB,
API_KEY: '',
ACCESS_TOKEN: ''
},
AI: {
...DEFAULT_CONFIG.AI,
API_KEY: ''
}
};
try {
saveConfig(defaultConfig);
showStatus('已重置为默认设置', false, 'settings');
} catch (error) {
showStatus('重置设置失败: ' + error.message, true, 'settings');
console.error('重置配置失败:', error);
}
}
}
// 确保在切换到设置标签页时加载配置
const originalSwitchTab = document.switchTab;
document.switchTab = function(tabId) {
originalSwitchTab(tabId);
// 当切换到设置标签页时,加载配置
if (tabId === 'settings') {
loadConfigToSettingsPanel();
}
};
// 同步更新到window和unsafeWindow
if (typeof unsafeWindow !== 'undefined') {
unsafeWindow.switchTab = document.switchTab;
}
window.switchTab = document.switchTab;
// 绑定设置面板的事件监听器
function bindSettingsEventListeners() {
// 保存设置按钮
const saveConfigBtn = document.getElementById('save-config-btn');
if (saveConfigBtn) {
saveConfigBtn.addEventListener('click', function(e) {
e.stopPropagation();
e.preventDefault();
saveSettingsFromPanel();
});
}
// 重置设置按钮
const resetConfigBtn = document.getElementById('reset-config-btn');
if (resetConfigBtn) {
resetConfigBtn.addEventListener('click', function(e) {
e.stopPropagation();
e.preventDefault();
resetSettingsToDefault();
});
}
}
// 修改bindEventListeners函数,添加设置面板的事件监听器绑定
(function() {
const originalBindEventListeners = bindEventListeners;
const enhancedBindEventListeners = function() {
originalBindEventListeners();
bindSettingsEventListeners();
};
// 替换全局bindEventListeners函数
window.bindEventListeners = enhancedBindEventListeners;
// 确保内部调用也使用增强版本
Object.defineProperty(window, 'bindEventListeners', {
value: enhancedBindEventListeners,
writable: true,
configurable: true
});
})();
// 修改insertPanelInMarkedPosition函数,确保设置面板的事件监听器被正确绑定
(function() {
const originalInsertPanelInMarkedPosition = insertPanelInMarkedPosition;
const enhancedInsertPanelInMarkedPosition = function() {
const result = originalInsertPanelInMarkedPosition();
bindSettingsEventListeners();
return result;
};
// 替换全局insertPanelInMarkedPosition函数
window.insertPanelInMarkedPosition = enhancedInsertPanelInMarkedPosition;
// 确保内部调用也使用增强版本
Object.defineProperty(window, 'insertPanelInMarkedPosition', {
value: enhancedInsertPanelInMarkedPosition,
writable: true,
configurable: true
});
})();
// ==================== 豆瓣登录检测功能 ====================
// 豆瓣登录检测相关变量
let isCheckingDoubanLogin = false;
// 通用请求头配置(确保请求不被豆瓣拦截)
const DOUBAN_COMMON_HEADERS = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.9',
'Cache-Control': 'no-cache'
};
/**
* 安全获取DOM元素:多次重试直到元素存在或超时
*/
function safeGetDoubanElement(id, maxRetries = 10, retryDelay = 100) {
return new Promise((resolve) => {
let retries = 0;
const checkElement = () => {
const element = document.getElementById(id);
if (element) resolve(element);
else if (retries < maxRetries) {
retries++;
setTimeout(checkElement, retryDelay);
} else {
GM_log(`[豆瓣检测] 未找到元素: ${id}`);
resolve(null);
}
};
checkElement();
});
}
/**
* 安全绑定事件:确保元素存在后再绑定
*/
function safeBindDoubanEvent(elementId, eventType, handler, maxRetries = 10) {
return safeGetDoubanElement(elementId, maxRetries).then(element => {
if (element) {
element.addEventListener(eventType, handler);
return true;
}
return false;
});
}
/**
* 豆瓣登录状态检测(主函数)
* @returns {Promise} { isLoggedIn: 布尔值, error: 错误信息, details: 检测详情 }
*/
function checkDoubanLoginStatus() {
return new Promise((resolve) => {
// 1. 防抖保护:若检测已在进行,直接返回
if (isCheckingDoubanLogin) {
GM_log('[豆瓣检测] 检测已在进行中,跳过重复请求');
resolve({ isLoggedIn: false, error: '检测正在进行中', details: {} });
return;
}
isCheckingDoubanLogin = true;
GM_log('[豆瓣检测] 开始跨域检测...');
// 2. 总超时保护(12秒,避免无限等待)
const totalTimeoutId = setTimeout(() => {
isCheckingDoubanLogin = false;
GM_log('[豆瓣检测] 总超时(12秒)');
resolve({ isLoggedIn: false, error: '检测超时(12秒)', details: { method: '总超时保护' } });
}, 12000);
// --------------------------
// 主方法:调用豆瓣官方用户状态接口
// --------------------------
GM_xmlhttpRequest({
method: 'GET',
url: 'https://www.douban.com/j/app/user/check', // 豆瓣官方状态接口
headers: {
...DOUBAN_COMMON_HEADERS,
'Referer': 'https://www.douban.com/', // 模拟从豆瓣主页发起请求
'X-Requested-With': 'XMLHttpRequest'
},
timeout: 8000, // 主方法单独超时(8秒)
withCredentials: true, // 关键:携带浏览器中存储的豆瓣Cookie
onload: (res) => {
clearTimeout(totalTimeoutId);
isCheckingDoubanLogin = false;
GM_log(`[豆瓣检测] 主方法响应:状态码=${res.status}, 最终URL=${res.finalUrl}`);
// 主方法成功(200状态码):解析返回数据判断登录状态
if (res.status === 200) {
try {
const data = JSON.parse(res.responseText);
// 多字段判断登录(豆瓣接口可能返回不同字段,需兼容)
const isLoggedIn = data && (
data.user_id || data.uid || data.logged_in === true ||
(data.user && (data.user.id || data.user.uid))
);
if (isLoggedIn) {
resolve({
isLoggedIn: true,
error: '',
details: {
method: '主方法(豆瓣官方接口)',
status: res.status,
userData: { uid: data.user_id || data.uid } // 脱敏返回用户ID
}
});
} else {
GM_log('[豆瓣检测] 主方法返回未登录,切换到备用方法');
tryBackupMethod(); // 主方法未登录,用备用方法确认
}
} catch (e) {
GM_log(`[豆瓣检测] 主方法JSON解析失败:${e.message}`);
tryBackupMethod(); // 解析失败,切换备用方法
}
} else {
GM_log(`[豆瓣检测] 主方法状态码异常(${res.status}),切换备用方法`);
tryBackupMethod(); // 状态码非200,切换备用方法
}
},
onerror: (err) => {
clearTimeout(totalTimeoutId);
isCheckingDoubanLogin = false;
GM_log(`[豆瓣检测] 主方法请求失败:${err.message}`);
tryBackupMethod(); // 请求失败,切换备用方法
},
ontimeout: () => {
clearTimeout(totalTimeoutId);
isCheckingDoubanLogin = false;
GM_log('[豆瓣检测] 主方法超时(8秒)');
tryBackupMethod(); // 超时,切换备用方法
}
});
// --------------------------
// 备用方法:访问豆瓣个人主页,判断是否重定向到登录页
// --------------------------
function tryBackupMethod() {
GM_log('[豆瓣检测] 启动备用方法(访问个人主页)');
GM_xmlhttpRequest({
method: 'GET',
url: 'https://www.douban.com/mine/', // 豆瓣个人主页
headers: { ...DOUBAN_COMMON_HEADERS, 'Referer': 'https://www.douban.com/' },
timeout: 6000, // 备用方法超时(6秒)
withCredentials: true,
onload: (res) => {
clearTimeout(totalTimeoutId);
isCheckingDoubanLogin = false;
// 判断逻辑:200状态码 + 未重定向到登录页 → 已登录
const isLoggedIn = res.status === 200 &&
!res.finalUrl?.includes('/login') &&
!res.finalUrl?.includes('/passport');
const reason = isLoggedIn ? '个人主页访问成功' :
(res.finalUrl?.includes('/login') ? '重定向到登录页' : `状态码=${res.status}`);
resolve({
isLoggedIn: isLoggedIn,
error: isLoggedIn ? '' : reason,
details: {
method: '备用方法(个人主页)',
status: res.status,
finalUrl: res.finalUrl
}
});
},
onerror: (err) => {
clearTimeout(totalTimeoutId);
isCheckingDoubanLogin = false;
resolve({
isLoggedIn: false,
error: '网络连接失败',
details: { method: '备用方法(请求失败)', error: err.message }
});
},
ontimeout: () => {
clearTimeout(totalTimeoutId);
isCheckingDoubanLogin = false;
resolve({
isLoggedIn: false,
error: '备用方法超时(6秒)',
details: { method: '备用方法(超时)' }
});
}
});
}
});
}
/**
* 打开豆瓣登录页面(处理弹窗拦截)
*/
function openDoubanLoginPage() {
const doubanHomeUrl = 'https://www.douban.com/'; // 豆瓣主页(自动判断是否需要登录)
try {
// 尝试打开新标签页
const newWindow = window.open(doubanHomeUrl, '_blank', 'noopener,noreferrer');
if (newWindow && !newWindow.closed) {
showStatus('已在新标签页打开豆瓣主页,请登录后返回重试检测', false, 'settings');
return;
}
// 弹窗被拦截,处理备用方案
GM_log('[豆瓣登录] 弹窗被拦截,尝试复制链接');
handleDoubanPopupBlocked(doubanHomeUrl);
} catch (e) {
GM_log(`[豆瓣登录] 打开页面失败:${e.message}`);
handleDoubanPopupBlocked(doubanHomeUrl);
}
}
/**
* 处理弹窗被拦截:复制链接到剪贴板+显示手动链接
*/
function handleDoubanPopupBlocked(url) {
// 尝试复制链接到剪贴板
if (navigator.clipboard?.writeText) {
navigator.clipboard.writeText(url).then(() => {
showStatus('豆瓣链接已复制到剪贴板,请粘贴到新标签页登录', false, 'settings');
displayDoubanManualLink(url); // 显示手动点击链接
}).catch(() => displayDoubanManualLink(url));
} else {
displayDoubanManualLink(url); // 不支持剪贴板API,直接显示链接
}
}
/**
* 显示手动登录链接(弹窗拦截时兜底)
*/
function displayDoubanManualLink(url) {
safeGetDoubanElement('douban-login-status').then(container => {
if (!container) return;
const manualLink = document.createElement('div');
manualLink.style.cssText = 'margin-top:8px; padding:8px; background:#f5f7fa; border-radius:4px; border:1px solid #e4e7ed;';
manualLink.innerHTML = `
🔗 手动登录链接:
点击访问豆瓣主页 →
`;
container.appendChild(manualLink);
});
}
/**
* 显示内嵌诊断面板(分析Cookie、DOM、环境)
*/
function showDoubanDiagnosticPanel(details = {}) {
// 移除已存在的面板(避免重复创建)
const existingPanel = document.getElementById('douban-diagnostic-panel');
if (existingPanel) existingPanel.remove();
// 创建诊断面板
const panel = document.createElement('div');
panel.id = 'douban-diagnostic-panel';
panel.style.cssText = `
position:fixed; top:50px; right:20px; width:500px; max-height:600px;
overflow-y:auto; background:white; border:1px solid #e4e7ed; border-radius:4px;
padding:15px; z-index:50; font-family:Arial,sans-serif; font-size:12px;
box-shadow:0 2px 12px 0 rgba(0,0,0,0.1);
`;
panel.innerHTML = `
🔍 豆瓣登录诊断报告
正在生成诊断报告...
`;
document.body.appendChild(panel);
// 绑定"关闭"按钮
safeBindDoubanEvent('close-douban-diagnostic', 'click', () => panel.remove());
// 绑定"复制报告"按钮
safeBindDoubanEvent('copy-douban-diagnostic', 'click', () => {
safeGetDoubanElement('douban-diagnostic-results').then(resultsDiv => {
navigator.clipboard.writeText(resultsDiv.innerText).then(() => {
showStatus('诊断报告已复制到剪贴板', false, 'settings');
}).catch(() => alert('诊断报告:\n' + resultsDiv.innerText));
});
});
// 生成诊断报告
generateDoubanDiagnosticReport();
/**
* 生成详细诊断报告(Cookie+DOM+环境)
*/
function generateDoubanDiagnosticReport() {
safeGetDoubanElement('douban-diagnostic-results').then(resultsDiv => {
if (!resultsDiv) return;
// 1. 环境信息
const envInfo = `
🌐 环境信息
当前域名:${window.location.hostname}
当前URL:${window.location.href}
GM_API支持:${typeof GM_xmlhttpRequest !== 'undefined' ? '✅ 支持' : '❌ 不支持'}
检测时间:${new Date().toLocaleString()}
`;
// 2. Cookie分析(关键登录Cookie:dbcl2/bid/ll)
const allCookies = document.cookie.split(';').filter(c => c.trim());
const doubanCookies = allCookies.filter(c => c.includes('douban') || c.startsWith('dbcl') || c.startsWith('bid'));
const keyCookies = ['dbcl2', 'bid', 'll'].map(name => {
const cookie = allCookies.find(c => c.trim().startsWith(`${name}=`));
return { name, exists: !!cookie, value: cookie ? cookie.trim().slice(0, 50) + '...' : '无' };
});
const cookieInfo = `
🍪 Cookie分析
总Cookie数:${allCookies.length} | 豆瓣Cookie数:${doubanCookies.length}
关键登录Cookie:
${keyCookies.map(c => `
${c.name}:${c.exists ? '✅ 存在' : '❌ 不存在'}
${c.exists ? `(值:${c.value})` : ''}
`).join('')}
`;
// 3. 检测详情(来自之前的检测结果)
const checkDetails = `
🔍 检测详情
检测说明:
未登录豆瓣账号时,页面将提示“检测失败:状态码=403”。请点击页面中的“去豆瓣登录”按钮,完成豆瓣账号登录流程后返回当前网页。若网页显示“豆瓣已登录”,即表示系统已成功检测到您的豆瓣登录状态,此后豆瓣源可正常使用。
HTTP状态码:${details.status || '未知'}
最终URL:${details.finalUrl || '未知'}
错误信息:${details.error || '无'}
`;
// 4. 建议操作
const suggestions = keyCookies.some(c => c.name === 'dbcl2' && c.exists)
? '1. Cookie存在但未登录:可能是Cookie过期,建议重新登录豆瓣
2. 清除浏览器缓存后重试'
: '1. 未检测到关键Cookie:请先访问豆瓣并完成登录
2. 检查浏览器是否禁用Cookie
3. 尝试无痕模式登录(排除缓存干扰)';
const suggestionInfo = `
`;
// 拼接报告并渲染
resultsDiv.innerHTML = envInfo + cookieInfo + checkDetails + suggestionInfo;
});
}
}
/**
* 渲染豆瓣登录状态到UI
* @param {boolean} isLoggedIn - 是否已登录
* @param {string} error - 错误信息(空表示无错误)
* @param {object} details - 检测详情
*/
function renderDoubanLoginStatus(isLoggedIn, error = '', details = {}) {
safeGetDoubanElement('douban-login-status').then(statusContainer => {
if (!statusContainer) return;
// 1. 处理"检测错误"状态(如超时、网络失败)
if (error) {
statusContainer.innerHTML = `
❌
检测失败:${error}
`;
// 绑定"去登录"按钮事件
safeBindDoubanEvent('go-douban-login', 'click', openDoubanLoginPage);
// 绑定"重试检测"按钮事件(防重复点击)
safeBindDoubanEvent('retry-douban-check', 'click', async () => {
if (isCheckingDoubanLogin) {
showStatus('检测正在进行中,请稍候...', true, 'settings');
return;
}
const retryBtn = await safeGetDoubanElement('retry-douban-check');
if (retryBtn) {
retryBtn.textContent = '检测中...';
retryBtn.disabled = true;
retryBtn.style.background = '#ccc';
}
showStatus('正在重新检测豆瓣登录状态...', false, 'settings');
const result = await checkDoubanLoginStatus();
renderDoubanLoginStatus(result.isLoggedIn, result.error, result.details);
});
// 绑定"详细诊断"按钮事件
safeBindDoubanEvent('douban-diagnose', 'click', () => showDoubanDiagnosticPanel(details));
return;
}
// 2. 处理"已登录"状态
if (isLoggedIn) {
statusContainer.innerHTML = `
✅
豆瓣已登录
检测说明:
未登录豆瓣账号时,页面将提示“检测失败:状态码=403”。请点击页面中的“去豆瓣登录”按钮,完成豆瓣账号登录流程后返回当前网页。若网页显示“豆瓣已登录”,即表示系统已成功检测到您的豆瓣登录状态,此后豆瓣源可正常使用。
`;
// 绑定"访问豆瓣"按钮
safeBindDoubanEvent('go-douban-home', 'click', () => window.open('https://www.douban.com/', '_blank'));
// 绑定"刷新状态"按钮
safeBindDoubanEvent('refresh-douban-status', 'click', async () => {
if (isCheckingDoubanLogin) {
showStatus('检测正在进行中,请稍候...', true, 'settings');
return;
}
const refreshBtn = await safeGetDoubanElement('refresh-douban-status');
if (refreshBtn) {
refreshBtn.textContent = '刷新中...';
refreshBtn.disabled = true;
refreshBtn.style.background = '#ccc';
}
showStatus('正在刷新豆瓣登录状态...', false, 'settings');
const result = await checkDoubanLoginStatus();
renderDoubanLoginStatus(result.isLoggedIn, result.error, result.details);
});
return;
}
// 3. 处理"未登录(无错误)"状态
statusContainer.innerHTML = `
⚠️
豆瓣未登录
请先在豆瓣登录,登录后点击"重试检测"
`;
// 绑定按钮事件(同"检测错误"状态)
safeBindDoubanEvent('go-douban-login', 'click', openDoubanLoginPage);
safeBindDoubanEvent('retry-douban-check', 'click', async () => {
if (isCheckingDoubanLogin) {
showStatus('检测正在进行中,请稍候...', true, 'settings');
return;
}
const retryBtn = await safeGetDoubanElement('retry-douban-check');
if (retryBtn) {
retryBtn.textContent = '检测中...';
retryBtn.disabled = true;
retryBtn.style.background = '#ccc';
}
showStatus('正在重新检测豆瓣登录状态...', false, 'settings');
const result = await checkDoubanLoginStatus();
renderDoubanLoginStatus(result.isLoggedIn, result.error, result.details);
});
safeBindDoubanEvent('douban-diagnose', 'click', () => showDoubanDiagnosticPanel(details));
});
}
/**
* 创建豆瓣登录检测UI容器
*/
function createDoubanLoginContainer() {
const container = document.createElement('div');
container.id = 'douban-login-container';
container.style.cssText = `
background: #fff; border: 1px solid #e4e7ed; border-radius: 8px;
padding: 15px; margin: 15px 0; box-shadow: 0 2px 12px 0 rgba(0,0,0,0.05);
position: relative;
`;
container.innerHTML = `
🔐
豆瓣登录状态
`;
return container;
}
/**
* 初始化豆瓣登录检测功能
*/
function initDoubanLoginCheck() {
// 1. 创建UI容器并插入到设置面板中的指定位置(AI API配置区域下方)
const settingsContentArea = document.querySelector('#settings-content-area');
if (settingsContentArea) {
const doubanContainer = createDoubanLoginContainer();
// 查找AI API配置区域,将豆瓣检测UI插入到其下方
const aiApiConfigSection = settingsContentArea.querySelector('div:nth-of-type(2)'); // AI API配置区域
if (aiApiConfigSection) {
settingsContentArea.insertBefore(doubanContainer, aiApiConfigSection.nextSibling);
} else {
// 备用方案:如果找不到特定位置,仍插入到设置内容区域的顶部
settingsContentArea.insertBefore(doubanContainer, settingsContentArea.firstChild);
}
}
// 2. 页面加载完成后执行首次检测
setTimeout(async () => {
showStatus('正在初始化豆瓣登录检测...', false, 'settings');
const result = await checkDoubanLoginStatus();
renderDoubanLoginStatus(result.isLoggedIn, result.error, result.details);
// 3. 监听页面可见性变化(用户从登录页返回后自动重新检测)
document.addEventListener('visibilitychange', async () => {
if (!document.hidden && !isCheckingDoubanLogin) {
showStatus('页面重新可见,正在更新豆瓣登录状态...', false, 'settings');
const result = await checkDoubanLoginStatus();
renderDoubanLoginStatus(result.isLoggedIn, result.error, result.details);
}
});
}, 500); // 延迟500ms确保DOM加载完成
}
// 在脚本初始化时启动豆瓣登录检测
setTimeout(() => {
initDoubanLoginCheck();
// 自动应用移动端适配
applyMobileStyles();
// 预览模式:?preview=1 或 localStorage.preview=1
try {
const urlHasPreview = /[?&]preview=1\b/.test(location.search);
const lsPreview = (localStorage.getItem('script_preview') === '1');
if (urlHasPreview || lsPreview) {
localStorage.setItem('script_preview', '1');
const badge = document.createElement('div');
badge.textContent = '预览模式';
badge.style.cssText = 'position:fixed;right:10px;bottom:10px;background:#ec4899;color:#fff;padding:6px 10px;border-radius:8px;font-size:12px;z-index:2147483647;box-shadow:0 2px 8px rgba(0,0,0,0.15);';
document.body.appendChild(badge);
}
} catch (e) { /* 忽略预览装饰失败 */ }
}, 1000); // 延迟1秒确保主面板已创建
// 移动端适配工具函数
function isMobileDevice() {
return window.innerWidth <= 768 || /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
}
// 应用移动端样式
function applyMobileStyles() {
if (!isMobileDevice()) return;
// 先移除已存在的移动端样式,避免样式重复
const existingStyle = document.getElementById('mobile-adaptation-styles');
if (existingStyle) existingStyle.remove();
// 添加全局响应式样式
const style = document.createElement('style');
style.id = 'mobile-adaptation-styles';
style.textContent = `
/* 全局搜索结果框保护规则 - 保持样式一致性 */
#search-results {
/* 确保搜索结果框样式一致,但允许JavaScript控制显示状态 */
/* visibility: visible !important; - 移除强制可见 */
/* opacity: 1 !important; - 移除强制不透明 */
position: relative !important;
z-index: 999999 !important;
}
/* 确保tox-editor-header不会遮挡搜索结果 */
div.tox-editor-header {
z-index: 1 !important;
}
/* 确保只在移动设备上应用这些样式 */
@media screen and (min-width: 769px) {
/* 电脑端样式保护 - 确保原有布局不受影响 */
div[style*="display:flex"][style*="gap:20px"] {
display: flex !important;
flex-direction: row !important;
gap: 20px !important;
}
#search-results {
position: absolute !important;
z-index: 999999 !important;
/* 移除强制显示,让JavaScript控制显示/隐藏 */
background: #fff !important;
border: 1px solid #f3d5d9 !important;
border-radius: 8px !important;
max-height: 300px !important;
overflow-y: auto !important;
width: calc(100% - 85px) !important;
left: 75px !important;
top: 32px !important;
display: none !important;
}
/* 在电脑端也强制降低编辑器头部层级 */
div.tox-editor-header {
z-index: 1 !important;
}
label[style*="display:inline-block"][style*="width:75px"] {
display: inline-block !important;
width: 75px !important;
vertical-align: middle !important;
}
input[style*="width: calc(100% - 85px)"] {
width: calc(100% - 85px) !important;
vertical-align: middle !important;
}
/* 电脑端提取按钮 - 默认隐藏,由JavaScript控制显示 */
#fetch-btn {
display: none !important; /* 默认隐藏 */
opacity: 0.6 !important;
pointer-events: none !important;
background: #ec4899 !important;
color: white !important;
border: none !important;
padding: 8px 16px !important;
border-radius: 0 8px 8px 0 !important;
cursor: not-allowed !important;
font-size: 13px !important;
transition: all 0.3s ease !important;
}
#fetch-btn.active {
opacity: 1 !important;
pointer-events: auto !important;
cursor: pointer !important;
background: #db2777 !important;
}
/* 电脑端控制面板响应式优化 */
#douban-tmdb-panel {
min-width: 320px !important;
max-width: 1400px !important; /* 扩大宽屏最大宽度限制,让搜索结果框能更好铺满 */
width: 100% !important;
overflow-x: auto !important;
}
/* 电脑端搜索区域响应式优化 */
#douban-tmdb-panel div[style*="display:flex"][style*="gap:15px"] {
flex-wrap: wrap !important;
min-width: 0 !important;
align-items: flex-start !important;
}
/* 宽窗口优化 (宽度 < 800px) - 提前处理 */
@media screen and (max-width: 800px) {
#douban-tmdb-panel {
min-width: 400px !important;
}
#douban-tmdb-panel div[style*="flex: 1 1 250px"][style*="min-width: 200px"] {
flex: 1 1 48% !important;
min-width: 200px !important;
}
}
/* 较宽窄窗口优化 (宽度 < 700px) - 提前处理 */
@media screen and (max-width: 700px) {
#douban-tmdb-panel {
min-width: 350px !important;
}
#douban-tmdb-panel div[style*="flex: 1 1 250px"][style*="min-width: 200px"] {
flex: 1 1 45% !important;
min-width: 180px !important;
}
#search-movie, #media-url {
min-width: 100px !important;
}
}
/* 中等窄窗口优化 (宽度 < 650px) - 开始垂直布局 */
@media screen and (max-width: 650px) {
#douban-tmdb-panel {
min-width: 340px !important;
}
/* 强制搜索区域垂直布局 - 使用更高优先级的选择器 */
#douban-tmdb-panel div[style*="display:flex"][style*="gap:15px"][style*="margin-bottom:12px"][style*="align-items:center"] {
flex-direction: column !important;
gap: 12px !important;
align-items: stretch !important;
}
/* 搜索和链接输入容器全宽显示 */
#douban-tmdb-panel div[style*="flex: 1 1 250px"][style*="min-width: 200px"] {
flex: none !important;
width: 100% !important;
min-width: auto !important;
display: flex !important;
flex-direction: row !important;
align-items: center !important;
}
/* 搜索输入框优化 */
#search-movie {
width: calc(100% - 75px) !important;
min-width: 150px !important;
font-size: 12px !important;
padding: 6px 10px !important;
}
/* 影视链接输入框优化 */
#media-url {
width: calc(100% - 75px) !important;
min-width: 150px !important;
font-size: 12px !important;
padding: 6px 10px !important;
}
/* 搜索结果框位置调整 */
#search-results {
position: relative !important;
width: 100% !important;
left: 0 !important;
top: 0 !important;
margin-top: 4px !important;
z-index: 1 !important;
}
/* 标签页按钮优化 */
#main-tab, #ai-tab, #settings-tab {
font-size: 11px !important;
padding: 6px 12px !important;
min-width: 70px !important;
}
/* 标签文字优化 */
label[style*="width:70px"] {
width: 65px !important;
font-size: 11px !important;
}
/* 修复链接框显示问题 */
.link-container {
display: flex;
flex-direction: column;
gap: 10px;
}
.link-item {
width: 100%;
word-break: break-all;
}
}
/* 中等窄窗口优化 (宽度 < 600px) - 强制垂直布局 */
@media screen and (max-width: 600px) {
#douban-tmdb-panel {
min-width: 320px !important;
}
/* 强制搜索区域垂直布局 - 使用更高优先级的选择器 */
#douban-tmdb-panel div[style*="display:flex"][style*="gap:15px"][style*="margin-bottom:12px"][style*="align-items:center"] {
flex-direction: column !important;
gap: 12px !important;
align-items: stretch !important;
}
/* 搜索和链接输入容器全宽显示 */
#douban-tmdb-panel div[style*="flex: 1 1 250px"][style*="min-width: 200px"] {
flex: none !important;
width: 100% !important;
min-width: auto !important;
display: flex !important;
flex-direction: row !important;
align-items: center !important;
}
/* 搜索输入框优化 */
#search-movie {
width: calc(100% - 75px) !important;
min-width: 150px !important;
font-size: 12px !important;
padding: 6px 10px !important;
}
/* 影视链接输入框优化 */
#media-url {
width: calc(100% - 75px) !important;
min-width: 150px !important;
font-size: 12px !important;
padding: 6px 10px !important;
}
/* 搜索结果框位置调整 */
#search-results {
position: relative !important;
width: 100% !important;
left: 0 !important;
top: 0 !important;
margin-top: 4px !important;
z-index: 1 !important;
}
/* 标签页按钮优化 */
#main-tab, #ai-tab, #settings-tab {
font-size: 11px !important;
padding: 6px 12px !important;
min-width: 70px !important;
}
/* 标签文字优化 */
label[style*="width:70px"] {
width: 65px !important;
font-size: 11px !important;
}
}
/* 较窄窗口优化 (宽度 < 500px) - 强制换行 */
@media screen and (max-width: 500px) {
#douban-tmdb-panel {
min-width: 300px !important;
padding: 12px !important;
}
/* 强制搜索区域垂直布局 */
#douban-tmdb-panel div[style*="display:flex"][style*="gap:15px"] {
flex-direction: column !important;
gap: 12px !important;
}
/* 搜索和链接输入容器全宽显示 */
#douban-tmdb-panel div[style*="flex: 1 1 250px"][style*="min-width: 200px"] {
flex: none !important;
width: 100% !important;
min-width: auto !important;
}
/* 搜索输入框优化 */
#search-movie {
width: calc(100% - 75px) !important;
min-width: 150px !important;
font-size: 12px !important;
padding: 6px 10px !important;
}
/* 影视链接输入框优化 */
#media-url {
width: calc(100% - 75px) !important;
min-width: 150px !important;
font-size: 12px !important;
padding: 6px 10px !important;
}
/* 搜索结果框位置调整 */
#search-results {
position: relative !important;
width: 100% !important;
left: 0 !important;
top: 0 !important;
margin-top: 4px !important;
z-index: 1 !important;
}
/* 标签页按钮优化 */
#main-tab, #ai-tab, #settings-tab {
font-size: 11px !important;
padding: 6px 12px !important;
min-width: 70px !important;
}
/* 标签文字优化 */
label[style*="width:70px"] {
width: 65px !important;
font-size: 11px !important;
}
}
/* 电脑端输入框响应式优化 */
#search-movie, #media-url {
min-width: 100px !important;
}
/* 电脑端标签响应式优化 */
label[style*="width:70px"] {
flex-shrink: 0 !important;
}
/* 电脑端标签页按钮响应式优化 */
#main-tab, #ai-tab, #settings-tab {
flex: 1 1 auto !important;
min-width: 80px !important;
white-space: nowrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
}
/* 极窄窗口优化 (宽度 < 400px) - 强制搜索区域垂直布局 */
@media screen and (max-width: 400px) {
#douban-tmdb-panel {
min-width: 280px !important;
padding: 10px !important;
}
/* 强制搜索区域垂直布局 */
#douban-tmdb-panel div[style*="display:flex"][style*="gap:15px"] {
flex-direction: column !important;
gap: 10px !important;
}
/* 搜索和链接输入容器全宽显示 */
#douban-tmdb-panel div[style*="flex: 1 1 250px"][style*="min-width: 200px"] {
flex: none !important;
width: 100% !important;
min-width: auto !important;
}
/* 搜索输入框优化 */
#search-movie {
width: calc(100% - 65px) !important;
min-width: 120px !important;
font-size: 11px !important;
padding: 5px 8px !important;
}
/* 影视链接输入框优化 */
#media-url {
width: calc(100% - 65px) !important;
min-width: 120px !important;
font-size: 11px !important;
padding: 5px 8px !important;
}
/* 搜索结果框位置调整 */
#search-results {
position: relative !important;
width: 100% !important;
left: 0 !important;
top: 0 !important;
margin-top: 4px !important;
z-index: 1 !important;
}
/* 标签页按钮优化 */
#main-tab, #ai-tab, #settings-tab {
font-size: 11px !important;
padding: 6px 12px !important;
min-width: 70px !important;
}
/* 标签文字优化 */
label[style*="width:70px"] {
width: 65px !important;
font-size: 11px !important;
}
}
}
/* 移动端专用样式 - 只在移动设备上生效 */
@media screen and (max-width: 768px) {
/* 全局移动端适配 */
* {
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
}
/* 主要容器适配 */
#douban-tmdb-panel {
width: 100% !important;
margin: 0 !important;
padding: 8px !important;
font-size: 14px !important;
overflow-x: hidden !important;
min-width: auto !important;
max-width: none !important;
}
/* 标题和文本适配 */
h2, h3, h4 {
font-size: 18px !important;
margin: 8px 0 !important;
}
p, span {
font-size: 14px !important;
line-height: 1.5 !important;
}
/* 海报和剧照容器响应式布局 */
#poster-candidates, #still-candidates {
grid-template-columns: repeat(auto-fit, minmax(80px, 1fr)) !important;
gap: 4px !important;
padding: 6px !important;
max-height: 280px !important;
overflow-y: auto !important;
}
/* 图片尺寸控制 */
#poster-candidates img, #still-candidates img {
max-width: 100% !important;
height: auto !important;
object-fit: cover !important;
border-radius: 4px !important;
}
/* 按钮适配 */
button {
font-size: 13px !important;
padding: 8px 12px !important;
margin: 3px !important;
border-radius: 4px !important;
min-height: 36px !important;
touch-action: manipulation;
}
/* 选择图片弹窗适配 */
#image-selection {
width: 98% !important;
max-height: none !important; /* 避免与内部候选区形成双重高度上限 */
margin: 2.5vh auto !important;
padding: 10px !important;
border-radius: 8px !important;
}
/* 输入框适配 */
input, select, textarea {
width: 100% !important;
margin-bottom: 8px !important;
padding: 10px !important;
font-size: 14px !important;
border-radius: 4px !important;
height: auto !important;
}
/* 搜索结果影视框适配 */
.movie-item, .search-result-item {
width: 100% !important;
padding: 8px !important;
margin-bottom: 10px !important;
border-radius: 6px !important;
overflow: hidden !important;
box-shadow: 0 2px 4px rgba(0,0,0,0.1) !important;
}
.movie-item img, .search-result-item img {
max-width: 40% !important;
height: auto !important;
float: left !important;
margin-right: 10px !important;
border-radius: 4px !important;
}
.movie-info, .search-result-info {
display: block !important;
overflow: hidden !important;
white-space: normal !important;
font-size: 13px !important;
line-height: 1.4 !important;
}
.movie-title, .search-result-title {
font-size: 16px !important;
font-weight: bold !important;
margin-bottom: 5px !important;
white-space: normal !important;
word-wrap: break-word !important;
}
/* 标签和评分适配 */
.movie-tags, .movie-rating {
font-size: 12px !important;
margin: 3px 0 !important;
}
/* 分页控制适配 */
.pagination {
display: flex !important;
justify-content: center !important;
flex-wrap: wrap !important;
padding: 10px 0 !important;
}
.pagination button {
min-width: 30px !important;
height: 30px !important;
line-height: 30px !important;
padding: 0 8px !important;
font-size: 12px !important;
}
/* 滚动条美化 */
::-webkit-scrollbar {
width: 6px !important;
height: 6px !important;
}
::-webkit-scrollbar-track {
background: #f1f1f1 !important;
border-radius: 3px !important;
}
::-webkit-scrollbar-thumb {
background: #888 !important;
border-radius: 3px !important;
}
::-webkit-scrollbar-thumb:hover {
background: #555 !important;
}
/* 表单布局适配 */
form {
display: flex !important;
flex-direction: column !important;
gap: 8px !important;
}
/* 网格布局适配 */
.grid-container {
grid-template-columns: 1fr !important;
gap: 10px !important;
}
/* 加载更多按钮 */
#load-more-posters, #load-more-stills {
width: 100% !important;
margin: 10px 0 !important;
padding: 10px !important;
font-size: 14px !important;
}
/* 搜索结果弹窗优化 - 移动设备专用 */
#search-results {
position: relative !important;
z-index: 1000 !important;
background: #fff !important;
border: 1px solid #ddd !important;
border-radius: 8px !important;
max-height: 50vh !important;
overflow-y: auto !important;
width: 100% !important;
left: 0 !important;
right: 0 !important;
top: auto !important;
bottom: auto !important;
box-shadow: 0 4px 20px rgba(0,0,0,0.15) !important;
transform: none !important;
transition: none !important;
margin-top: 2px !important;
}
/* 移动端搜索区域整体布局 - 垂直排列 */
div[style*="display:flex"][style*="gap:20px"] {
display: flex !important;
flex-direction: column !important;
gap: 10px !important;
}
/* 搜索框和提取按钮布局优化 */
#search-movie {
width: 100% !important;
margin-bottom: 8px !important;
padding: 10px !important;
font-size: 14px !important;
}
#fetch-btn {
width: 100% !important;
margin-top: 5px !important;
padding: 10px !important;
font-size: 14px !important;
min-height: 40px !important;
display: none !important; /* 移动端默认隐藏 */
}
/* 移动端:移除CSS强制显示规则,完全由JavaScript控制按钮显示 */
/* #media-url:not(:placeholder-shown) + #fetch-btn,
#media-url[value]:not([value=""]) + #fetch-btn {
display: block;
} */
/* 搜索输入框的父容器布局优化 */
div[id^="search-section"], div[id*="search-section"] {
position: relative !important;
width: 100% !important;
margin-bottom: 10px !important;
}
/* 移动端影视链接容器 - 确保垂直布局 */
div[style*="flex:1"][style*="display:flex"][style*="align-items:center"] {
flex: none !important;
width: 100% !important;
display: flex !important;
flex-direction: column !important;
align-items: stretch !important;
}
/* 搜索加载指示器优化 */
#search-loading {
position: absolute !important;
right: 15px !important;
top: 35% !important;
transform: translateY(-50%) !important;
color: #999 !important;
font-size: 12px !important;
}
/* 影视链接输入框优化 */
#media-url {
width: 100% !important;
padding: 10px !important;
font-size: 14px !important;
margin-bottom: 10px !important;
}
/* 修复标签和输入框之间的布局问题 */
label[for="search-movie"], label[for="media-url"], label[style*="width:80px"], label[style*="width:70px"] {
display: block !important;
width: 100% !important;
margin-bottom: 4px !important;
font-size: 13px !important;
}
/* 移动端标签布局优化 - 确保标签在输入框上方 */
label[style*="display:inline-block"][style*="width:75px"] {
display: block !important;
width: 100% !important;
margin-bottom: 6px !important;
font-size: 14px !important;
font-weight: 500 !important;
color: #6b7280 !important;
}
/* 移动端输入框布局优化 */
input[style*="width: calc(100% - 85px)"] {
width: 100% !important;
margin-left: 0 !important;
vertical-align: top !important;
}
/* 修复搜索结果项的样式 */
.search-item {
padding: 10px !important;
border-bottom: 1px solid #eee !important;
display: flex !important;
align-items: center !important;
gap: 8px !important;
font-size: 13px !important;
}
.search-item .poster-placeholder {
width: 36px !important;
height: 54px !important;
background: #f5f5f5 !important;
border-radius: 4px !important;
}
/* 修复AI功能面板样式 */
#ai-function-select, #ai-prompt-input {
width: 100% !important;
padding: 8px !important;
font-size: 13px !important;
}
/* 确保按钮在移动设备上更易于点击 */
button, input[type="button"], input[type="submit"] {
touch-action: manipulation;
cursor: pointer;
-webkit-appearance: none;
border-radius: 4px;
}
/* 避免键盘弹出时布局错乱 */
body {
overflow-x: hidden;
}
/* 修复链接框显示问题 */
.link-container {
display: flex;
flex-direction: column;
gap: 10px;
}
.link-item {
width: 100%;
word-break: break-all;
}
} /* 闭合移动端媒体查询 */
/* 针对极窄屏幕的额外优化 */
@media screen and (max-width: 480px) {
#douban-tmdb-panel {
padding: 10px 8px !important;
}
/* 强制搜索区域垂直布局 */
#douban-tmdb-panel div[style*="display:flex"][style*="gap:15px"][style*="margin-bottom:12px"][style*="align-items:center"] {
flex-direction: column !important;
gap: 10px !important;
align-items: stretch !important;
}
/* 搜索和链接输入容器全宽显示 */
#douban-tmdb-panel div[style*="flex: 1 1 250px"][style*="min-width: 200px"] {
flex: none !important;
width: 100% !important;
min-width: auto !important;
}
/* 影视链接容器优化 */
#media-url-container {
width: 100% !important;
flex: none !important;
min-width: auto !important;
}
/* 影视链接包装器优化 - 强制垂直布局 */
#media-url-wrapper {
flex-direction: column !important;
align-items: stretch !important;
gap: 6px !important;
width: 100% !important;
}
#search-movie {
width: 100% !important;
font-size: 11px !important;
padding: 5px 8px !important;
min-width: 120px !important;
}
#media-url {
width: 100% !important;
font-size: 11px !important;
padding: 5px 8px !important;
min-width: 120px !important;
border-radius: 6px !important;
}
#fetch-btn {
width: 100% !important;
padding: 5px 8px !important;
font-size: 11px !important;
margin-left: 0 !important;
border-radius: 6px !important;
flex-shrink: 0 !important;
}
label[style*="width:70px"] {
font-size: 10px !important;
width: 60px !important;
}
#search-results {
width: calc(100% - 70px) !important;
left: 60px !important;
max-height: 200px !important;
}
/* 确保标签在极窄屏幕上不被挤压 */
.search-item {
flex-wrap: wrap !important;
}
.search-item .poster-placeholder {
width: 30px !important;
height: 45px !important;
}
/* 修复链接框显示问题 */
.link-container {
gap: 5px;
}
}
/* 针对超窄屏幕的优化 */
@media screen and (max-width: 360px) {
#douban-tmdb-panel {
padding: 8px 6px !important;
}
/* 强制搜索区域垂直布局 */
#douban-tmdb-panel div[style*="display:flex"][style*="gap:15px"][style*="margin-bottom:12px"][style*="align-items:center"] {
flex-direction: column !important;
gap: 8px !important;
align-items: stretch !important;
}
/* 搜索和链接输入容器全宽显示 */
#douban-tmdb-panel div[style*="flex: 1 1 250px"][style*="min-width: 200px"] {
flex: none !important;
width: 100% !important;
min-width: auto !important;
}
/* 影视链接容器优化 */
#media-url-container {
width: 100% !important;
flex: none !important;
min-width: auto !important;
}
/* 影视链接包装器优化 - 强制垂直布局 */
#media-url-wrapper {
flex-direction: column !important;
align-items: stretch !important;
gap: 4px !important;
width: 100% !important;
}
#search-movie {
width: 100% !important;
font-size: 10px !important;
padding: 4px 6px !important;
min-width: 100px !important;
}
#media-url {
width: 100% !important;
font-size: 10px !important;
padding: 4px 6px !important;
min-width: 100px !important;
border-radius: 6px !important;
}
#fetch-btn {
width: 100% !important;
padding: 4px 6px !important;
font-size: 10px !important;
margin-left: 0 !important;
border-radius: 6px !important;
flex-shrink: 0 !important;
}
label[style*="width:70px"] {
font-size: 9px !important;
width: 50px !important;
}
#search-results {
position: relative !important;
width: 100% !important;
left: 0 !important;
top: 0 !important;
max-height: 150px !important;
margin-top: 4px !important;
z-index: 1 !important;
}
/* 搜索结果项进一步压缩 */
.search-item {
padding: 6px !important;
gap: 5px !important;
}
.search-item .poster-placeholder {
width: 25px !important;
height: 38px !important;
}
/* 修复链接框显示问题 */
.link-container {
gap: 3px;
}
}
`;
document.head.appendChild(style);
// 提取按钮初始化 - 默认隐藏(所有设备)
const fetchBtn = document.getElementById('fetch-btn');
if (fetchBtn) {
fetchBtn.style.display = 'none';
}
// 添加动态调整搜索结果位置的逻辑
const searchInput = document.getElementById('search-movie');
const resultsContainer = document.getElementById('search-results');
if (searchInput && resultsContainer) {
// 调整搜索结果位置的函数 - 简化版本,因为显示框现在在控制面板内部
function adjustResultsPosition() {
if (!searchInput || !resultsContainer) return;
// 尝试查找并降低div.tox-editor-header的z-index
const editorHeader = document.querySelector('div.tox-editor-header');
if (editorHeader) {
editorHeader.style.zIndex = '1'; // 临时降低编辑器头部的z-index
}
// 显示框现在在控制面板内部,不需要复杂的位置计算
// 只需要确保显示框正确显示
if (resultsContainer.style.display === 'block') {
resultsContainer.style.setProperty('position', 'relative', 'important');
resultsContainer.style.setProperty('top', '0', 'important');
resultsContainer.style.setProperty('left', '0', 'important');
resultsContainer.style.setProperty('right', '0', 'important');
resultsContainer.style.setProperty('z-index', '1000', 'important');
}
}
// 防抖函数,避免频繁调用
let positionTimeout;
const debouncedAdjustPosition = () => {
clearTimeout(positionTimeout);
positionTimeout = setTimeout(adjustResultsPosition, 50);
};
// 立即调整位置
setTimeout(adjustResultsPosition, 100);
// 监听输入框聚焦事件
searchInput.addEventListener('focus', debouncedAdjustPosition);
// 监听窗口大小变化
window.addEventListener('resize', debouncedAdjustPosition);
// 监听页面滚动
window.addEventListener('scroll', debouncedAdjustPosition);
// 监听搜索结果显示/隐藏状态变化
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
if (mutation.attributeName === 'style') {
if (resultsContainer.style.display !== 'none') {
debouncedAdjustPosition();
}
}
});
});
observer.observe(resultsContainer, { attributes: true });
}
}
try {
// 注册油猴菜单项
GM_registerMenuCommand('📱 移动端适配', () => {
applyMobileStyles();
showStatus('移动端适配样式已应用', false);
});
} catch (e) {
console.error('注册移动端适配菜单项失败:', e);
}
// 立即注入搜索结果框CSS确保显示
setTimeout(() => {
if (typeof injectSearchResultsCSS === 'function') {
injectSearchResultsCSS();
console.log('已注入搜索结果框强制CSS样式');
}
// 确保搜索中指示器初始状态为隐藏
setSearchLoading(false);
// 定期检查并隐藏搜索中指示器(防止意外显示)
setInterval(() => {
const searchInput = document.getElementById('search-movie');
const resultsContainer = document.getElementById('search-results');
const loadingIndicator = document.getElementById('search-loading');
// 只有在搜索框为空、没有搜索结果、且搜索中指示器已经显示超过5秒时才隐藏
if (searchInput && !searchInput.value.trim() &&
resultsContainer && resultsContainer.style.display === 'none' &&
loadingIndicator && loadingIndicator.style.display === 'block') {
// 检查指示器是否已经显示超过5秒
const now = Date.now();
if (!loadingIndicator.dataset.showTime) {
loadingIndicator.dataset.showTime = now.toString();
} else {
const showTime = parseInt(loadingIndicator.dataset.showTime);
if (now - showTime > 5000) {
setSearchLoading(false);
// 静默隐藏超时的搜索中指示器
}
}
}
}, 3000);
}, 100);
// 脚本功能检查机制 - 静默运行,不显示UI
function initScriptChecks() {
console.log('🎬 豆瓣+TMDB影视工具脚本已启动');
// 核心功能检查(静默运行)
const coreChecks = {
// 检查控制面板是否正确创建
checkPanelCreation: () => {
const panel = document.getElementById('douban-tmdb-panel');
return panel ? true : false;
},
// 检查搜索功能
checkSearchFunction: () => {
const searchInput = document.getElementById('search-movie');
const results = document.getElementById('search-results');
return !!(searchInput && results);
},
// 检查输入框水平对齐(更稳健:直接比较元素本身位置,移动端跳过)
checkInputAlignment: () => {
try {
// 移动端/窄屏为垂直布局,不做水平对齐校验
if (window.innerWidth <= 800) return true;
const searchEl = document.getElementById('search-movie');
const linkContainer = document.getElementById('media-url-container');
if (!searchEl || !linkContainer) return true; // 缺少元素时不报错
const searchRect = searchEl.getBoundingClientRect();
const linkRect = linkContainer.getBoundingClientRect();
const heightDiff = Math.abs(searchRect.top - linkRect.top);
return heightDiff < 8; // 适度放宽容差,避免偶发1-2px误差
} catch (e) { return true; }
}
};
// 静默运行功能检查,不显示UI
// 静默运行核心功能检查
setTimeout(() => {
// 只检查关键功能,只在出错时输出警告
Object.entries(coreChecks).forEach(([key, check]) => {
if (!check()) {
console.warn('脚本功能检查失败:', key);
}
});
}, 2000);
// 监听搜索功能(静默)
const searchInput = document.getElementById('search-movie');
if (searchInput) {
searchInput.addEventListener('input', (e) => {
// 静默监听,不输出日志
});
}
// 监听控制面板展开状态(静默)
const panel = document.getElementById('douban-tmdb-panel');
if (panel) {
const observer = new MutationObserver((mutations) => {
// 静默监听,不输出日志
});
observer.observe(panel, { attributes: true });
}
// 监听窗口大小变化,动态调整输入框对齐(横排即时,竖排轻微防抖)
let resizeTimeout;
window.addEventListener('resize', () => {
const w = window.innerWidth;
// 竖→横:立即执行,配合no-transition实现闪切
if (w > 800) {
adjustInputAlignment();
return;
}
// 横→竖:仅做极短防抖,减小抖动
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
adjustInputAlignment();
}, 50);
});
// 初始化时调整对齐(缩短延迟,加快就绪速度)
setTimeout(() => {
adjustInputAlignment();
}, 120);
}
// 初始化脚本检查机制
setTimeout(initScriptChecks, 1000);
})();