您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
影视信息提取与排版工具,优化图片重复与剧照展示(海报滚轮分页加载优化)
// ==UserScript== // @name 豆瓣+TMDB影视工具(图片去重与全展示优化版) // @namespace tampermonkey // @version 24.85 // @description 影视信息提取与排版工具,优化图片重复与剧照展示(海报滚轮分页加载优化) // @author 豆包 // @match https://pan1.me/?thread-create-*.htm // @grant GM_xmlhttpRequest // @grant GM_setClipboard // @grant GM_log // @run-at document-end // @license MIT // 新增:声明MIT许可证 // ==/UserScript== (function () { 'use strict'; // 核心配置 const TMDB_CONFIG = { API_KEY: '860ffcf671f5c664bbadeb5a972f9650', ACCESS_TOKEN: 'eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI4NjBmZmNmNjcxZjVjNjY0YmJhZGViNWE5NzJmOTY1MCIsIm5iZiI6MTc1NjE3NDMxMS40NDIsInN1YiI6IjY4YWQxN2U3YjYzZDI2MWNlNDQ0OTZlMiIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.XXHfRXpDp7S42rK5PdptQ6xGwtG5ohnXYTfdILwVxXg', BASE_URL: 'https://api.themoviedb.org/3', IMAGE_BASE_URL: 'https://image.tmdb.org/t/p/', POSTER_SIZE: 'w1280', STILL_SIZE: 'w1280', DOUBAN_QUALITY: { PRIORITY: ['raw', 'l', 'm'], TIMEOUT: 3000, RETRY: 1 }, IMAGE_CANDIDATES_COUNT: 5, // 每组加载数量(海报、剧照通用) POSTER_PER_ROW: 5, STILL_PER_ROW: 5 }; 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' }; // 存储变量 let selectedPosterUrl = ''; let selectedStillUrl = ''; let currentMovieInfo = null; let currentComments = []; let sourceCodeElement = null; let panelObserver = null; let isPanelInitialized = false; let currentEditor = null; let posterPage = 0; let isLoadingPosters = false; let isLoadingStills = false; let posterContainer = null; let stillContainer = null; let panel = null; let loadedPosterIds = new Set(); // 海报去重:记录已加载的海报唯一ID let stillPage = 0; // 剧照分页标记 let loadedStillIds = new Set(); // 剧照去重:记录已加载的剧照唯一ID // 排版美化样式库 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 `<h1 style="color:#1e40af;font-size:24px;font-weight:bold;margin:20px 0 15px 0;padding-bottom:8px;border-bottom:2px solid #dbeafe;">${content}</h1>`; } }, { 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 `<h2 style="color:#2563eb;font-size:20px;font-weight:bold;margin:18px 0 12px 0;padding-bottom:5px;border-bottom:1px solid #dbeafe;">${content}</h2>`; } }, { 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 `<h3 style="color:#3b82f6;font-size:18px;font-weight:bold;margin:15px 0 10px 0;">${content}</h3>`; } }, { 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 `<p style="color:#333;font-size:14px;line-height:1.8;margin:8px 0;text-indent:2em;">${content}</p>`; } }, { 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 `<blockquote style="color:#666;font-size:13px;line-height:1.6;margin:10px 0;padding:10px 15px;border-left:3px solid #2196F3;background:#f8f9fa;">${content}</blockquote>`; } }, { 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 => `<li style="color:#444;font-size:14px;line-height:1.7;margin:5px 0;list-style-type:disc;">${item.trim()}</li>` ).join(''); return `<ul style="margin:10px 0 10px 20px;padding:0;">${items}</ul>`; } }, { 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 => `<li style="color:#444;font-size:14px;line-height:1.7;margin:5px 0;">${item.trim()}</li>` ).join(''); return `<ol style="margin:10px 0 10px 20px;padding:0;">${items}</ol>`; } }, { name: '分隔线', icon: 'fa-minus', tag: 'hr', category: '布局', styles: { 'border': 'none', 'border-top': '1px solid #e0e0e0', 'margin': '20px 0', 'height': '1px' }, preview: true, apply: () => { return `<hr style="border:none;border-top:1px solid #e0e0e0;margin:20px 0;height:1px;">`; } }, { name: '高亮文本', icon: 'fa-highlighter', tag: 'span', category: '文本', styles: { 'background': '#fff380', 'padding': '0 3px', 'border-radius': '2px' }, preview: true, apply: (selectedText) => { const content = selectedText || '需要高亮的文本'; return `<span style="background:#fff380;padding:0 3px;border-radius:2px;">${content}</span>`; } }, { 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 `<a href="#" style="color:#2563eb;text-decoration:none;border-bottom:1px dashed #93c5fd;padding:0 1px;">${content}</a>`; } }, { 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 `<div style="text-align:center;margin:10px 0;color:#4b5563;">${content}</div>`; } }, { name: '影视卡片', icon: 'fa-film', tag: 'div', category: '特殊', preview: true, apply: (selectedText) => { const title = selectedText || '影视名称'; return ` <div style="border:1px solid #e5e7eb;border-radius:6px;padding:15px;margin:15px 0;box-shadow:0 1px 3px rgba(0,0,0,0.05);"> <h3 style="margin-top:0;color:#1e40af;">${title}</h3> <p style="margin-bottom:0;color:#4b5563;font-size:14px;">这里可以添加影视的简要说明或推荐理由...</p> </div> `; } } ]; // 去重工具函数:过滤重复的图片URL function removeDuplicateUrls(urls) { return [...new Set(urls)].filter(url => url.trim() !== ''); } // 自动填充和保存相关函数 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); } }); } function autoFillSourceBox(html) { return new Promise((resolve) => { 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' ]; 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.CodeMirror) { codeMirror.CodeMirror.setValue(html); codeMirror.CodeMirror.getDoc().markClean(); codeMirror.CodeMirror.getDoc().changed(); } else { targetBox.value = html; const inputEvent = new Event('input', { bubbles: true, cancelable: true }); const changeEvent = new Event('change', { bubbles: true, cancelable: true }); const blurEvent = new Event('blur', { bubbles: true, cancelable: true }); targetBox.dispatchEvent(inputEvent); targetBox.dispatchEvent(changeEvent); targetBox.dispatchEvent(blurEvent); targetBox.focus(); targetBox.setSelectionRange(html.length, html.length); } clearInterval(tryFill); resolve(true); return; } if (retryCount >= maxRetry) { clearInterval(tryFill); resolve(false); } }, interval); }); } 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); }); } 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) { // 强制填充标题,解决标题缺失提示 await autoFillTitleInput(currentMovieInfo?.title); showStatus('正在切换到源代码模式...', false); const switched = await autoClickSourceBtn(); if (!switched) { showStatus('未检测到源代码按钮,尝试直接填充...', false); } showStatus('正在填充内容到编辑框...', false); const filled = await autoFillSourceBox(html); if (filled) { showStatus('内容填充完成,正在自动保存编辑内容(非发布)...', false); const saved = await autoClickSaveBtn(); if (saved) { showStatus('内容已填充并自动保存(非发布)', false); } else { showStatus('内容已填充,未找到“保存”按钮,请手动点击保存', false); } return true; } else { GM_setClipboard(html); showStatus('自动填充失败,内容已复制到剪贴板,请手动粘贴', true); const pasteBtn = document.getElementById('paste-btn'); if (pasteBtn) pasteBtn.style.display = 'inline-block'; return false; } } // 内容生成函数 function generateHTML(movie, comments, posterDataURL, stillDataURL) { const finalStillUrl = 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 = `<span> </span><strong style="box-sizing: border-box; font-weight: bolder;">IMDb:</strong><span style="box-sizing: border-box; color: rgb(0, 2, 255);"><a style="box-sizing: border-box; color: rgb(0, 2, 255); text-decoration: none; background-color: transparent; transition: 0.2s;" href="https://www.imdb.com/title/${movie.imdbId}/" target="_blank" rel="noopener"><span> </span>${movie.imdbId}</a></span>`; } const introHtml = movie.intro .split('\n') .filter(para => para.trim()) .map(para => `<div style="box-sizing: border-box; color: rgb(33, 37, 41); font-family: 'Helvetica Neue', Helvetica, 'Microsoft Yahei', 'Hiragino Sans GB', 'WenQuanYi Micro Hei', 微软雅黑, 华文细黑, STHeiti, sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">${para.trim()}</div>`) .join(''); let commentsHtml = ''; if (comments && comments.length > 0) { commentsHtml = ` <h3 style="box-sizing: border-box; margin-top: 0px; margin-bottom: 0.5rem; font-family: 'Helvetica Neue', Helvetica, 'Microsoft Yahei', 'Hiragino Sans GB', 'WenQuanYi Micro Hei', 微软雅黑, 华文细黑, STHeiti, sans-serif; font-weight: 500; line-height: 1.2; color: rgb(33, 37, 41); font-size: 1.75rem; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">影视热评:</h3> <div style="box-sizing: border-box; color: rgb(33, 37, 41); font-family: 'Helvetica Neue', Helvetica, 'Microsoft Yahei', 'Hiragino Sans GB', 'WenQuanYi Micro Hei', 微软雅黑, 华文细黑, STHeiti, sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">${comments[0]}</div> `; } return ` <div class="card border" style="box-sizing: border-box; position: relative; display: flex; flex-direction: column; min-width: 0px; overflow-wrap: break-word; background: none 0% 0% / auto repeat scroll padding-box border-box rgb(255, 255, 255); border: none; border-radius: 0.75rem; margin-bottom: 1rem; box-shadow: rgba(46, 45, 116, 0.05) 0px 0.25rem 1.875rem; color: rgb(33, 37, 41); font-family: 'Helvetica Neue', Helvetica, 'Microsoft Yahei', 'Hiragino Sans GB', 'WenQuanYi Micro Hei', 微软雅黑, 华文细黑, STHeiti, sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"> <div class="movie-info" style="box-sizing: border-box; flex: 1 1 400px; padding: 20px;"><img class="lazy img-responsive" style="box-sizing: border-box; vertical-align: middle; border: 1px solid transparent; max-width: 100%; -webkit-user-drag: none; margin-bottom: 0.5rem; height: auto; width: 816.818px; cursor: pointer;" src="${posterDataURL}" width="1080" height="1522" data-original="${posterDataURL}"><br style="box-sizing: border-box;"> <div class="movie-info-content" style="box-sizing: border-box;"> <p style="box-sizing: border-box; margin: 0.2rem 0px; line-height: 1.7;"><strong style="box-sizing: border-box; font-weight: bolder;">名称:</strong>${movie.title}</p> <p style="box-sizing: border-box; margin: 0.2rem 0px; line-height: 1.7;"><strong style="box-sizing: border-box; font-weight: bolder;">又名:</strong>${movie.alsoKnown || '无'}</p> <p style="box-sizing: border-box; margin: 0.2rem 0px; line-height: 1.7;"><strong style="box-sizing: border-box; font-weight: bolder;">导演:</strong>${movie.director}</p> <p style="box-sizing: border-box; margin: 0.2rem 0px; line-height: 1.7;"><strong style="box-sizing: border-box; font-weight: bolder;">编剧:</strong>${movie.writer}</p> <p style="box-sizing: border-box; margin: 0.2rem 0px; line-height: 1.7;"><strong style="box-sizing: border-box; font-weight: bolder;">主演:</strong>${movie.actor}</p> <p style="box-sizing: border-box; margin: 0.2rem 0px; line-height: 1.7;"><strong style="box-sizing: border-box; font-weight: bolder;">类型:</strong>${movie.genreTags.join('、') || '未知'}</p> <p style="box-sizing: border-box; margin: 0.2rem 0px; line-height: 1.7;"><strong style="box-sizing: border-box; font-weight: bolder;">制片地区:</strong>${movie.region}</p> <p style="box-sizing: border-box; margin: 0.2rem 0px; line-height: 1.7;"><strong style="box-sizing: border-box; font-weight: bolder;">上映时间:</strong>${movie.release}</p> <p style="box-sizing: border-box; margin: 0.2rem 0px; line-height: 1.7;"><strong style="box-sizing: border-box; font-weight: bolder;">影视语言:</strong>${movie.lang}</p> <p style="box-sizing: border-box; margin: 0.2rem 0px; line-height: 1.7;"><strong style="box-sizing: border-box; font-weight: bolder;">评分:</strong><span style="box-sizing: border-box; color: rgb(0, 132, 255); font-weight: 600;"><span> </span>${movie.rating}</span><span> </span><strong style="box-sizing: border-box; font-weight: bolder;">豆瓣ID:</strong><span style="box-sizing: border-box; color: rgb(0, 2, 255);"><a style="box-sizing: border-box; color: rgb(0, 2, 255); text-decoration: none; background-color: transparent; transition: 0.2s;" href="${movie.mediaType === 'tv' ? 'https://tv.douban.com/subject/' : 'https://movie.douban.com/subject/'}${movie.doubanId || movie.tmdbId}/" target="_blank" rel="noopener"><span> </span>${movie.doubanId || movie.tmdbId}</a></span>${imdbHtml}</p> <p style="box-sizing: border-box; margin: 0.2rem 0px; line-height: 1.7;"><strong style="box-sizing: border-box; font-weight: bolder;">片长:</strong>${runtime}</p> </div> </div> </div> <h3 style="box-sizing: border-box; margin-top: 0px; margin-bottom: 0.5rem; font-family: 'Helvetica Neue', Helvetica, 'Microsoft Yahei', 'Hiragino Sans GB', 'WenQuanYi Micro Hei', 微软雅黑, 华文细黑, STHeiti, sans-serif; font-weight: 500; line-height: 1.2; color: rgb(33, 37, 41); font-size: 1.75rem; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">影视简介:</h3> ${introHtml} <div style="box-sizing: border-box; color: rgb(33, 37, 41); font-family: 'Helvetica Neue', Helvetica, 'Microsoft Yahei', 'Hiragino Sans GB', 'WenQuanYi Micro Hei', 微软雅黑, 华文细黑, STHeiti, sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"> </div> <div style="box-sizing: border-box; color: rgb(33, 37, 41); font-family: 'Helvetica Neue', Helvetica, 'Microsoft Yahei', 'Hiragino Sans GB', 'WenQuanYi Micro Hei', 微软雅黑, 华文细黑, STHeiti, sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"> <h3 style="box-sizing: border-box; margin-top: 0px; margin-bottom: 0.5rem; font-family: 'Helvetica Neue', Helvetica, 'Microsoft Yahei', 'Hiragino Sans GB', 'WenQuanYi Micro Hei', 微软雅黑, 华文细黑, STHeiti, sans-serif; font-weight: 500; line-height: 1.2; color: rgb(33, 37, 41); font-size: 1.75rem; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">精彩剧照:</h3> <img src="${finalStillUrl}" style="box-sizing: border-box; vertical-align: middle; border-style: none; max-width: 100%; height: auto; border-radius: 4px; margin-bottom: 1rem;" alt="${movie.title} 剧照"> </div> ${commentsHtml} `; } // 控制面板创建与定位 - 精准放置到标记位置 function createPanel() { panel = document.createElement('div'); panel.id = 'douban-tmdb-panel'; panel.style.cssText = ` background: #fff; border: 1px solid #e5e7eb; border-radius: 6px; padding: 10px; margin: 8px 0; box-shadow: 0 1px 2px rgba(0,0,0,0.05); z-index: 999; position: relative; box-sizing: border-box; width: 100%; max-width: 1000px; `; panel.innerHTML = ` <div style="margin:0 0 10px 0; font-size:16px; font-weight:600; color:#374151; border-bottom:1px solid #e5e7eb; padding-bottom:6px;">豆瓣+TMDB影视工具</div> <!-- 排版美化工具区域 --> <div style="margin-bottom:12px; padding:8px; background:#f9fafb; border-radius:4px;"> <h4 style="margin:0 0 8px 0; color:#4b5563; font-size:14px; display:flex; justify-content:space-between; align-items:center;"> 排版美化工具 <span id="format-preview-toggle" style="font-size:12px; color:#3b82f6; cursor:pointer; text-decoration:underline;">显示预览</span> </h4> <!-- 样式分类标签 --> <div id="format-categories" style="display:flex; gap:8px; margin-bottom:8px; overflow-x:auto; padding-bottom:4px; border-bottom:1px solid #e2e8f0;"> <!-- 分类标签通过JS生成 --> </div> <!-- 样式按钮区域 --> <div style="display:flex; flex-wrap:wrap; gap:6px;" id="format-buttons"> <!-- 美化按钮通过JS生成 --> </div> <!-- 样式预览区域 --> <div id="format-preview" style="margin-top:10px; padding:10px; background:white; border:1px solid #e5e7eb; border-radius:4px; display:none; max-height:200px; overflow-y:auto; font-family:'Microsoft YaHei', sans-serif;"> <div style="text-align:center; color:#6b7280; font-size:13px;">选择样式查看预览效果</div> </div> </div> <div style="margin-bottom:10px; position:relative;"> <label style="display:inline-block; width:70px; font-weight:500; color:#4b5563;">搜索影片:</label> <input type="text" id="search-movie" placeholder="直接输入名称(如:奥本海默)" style="width: calc(100% - 80px); padding:5px; border:1px solid #d1d5db; border-radius:4px; font-size:13px;"> <div id="search-loading" style="position:absolute; right:10px; top:6px; color:#9ca3af; display:none;"> <i>搜索中...</i> </div> <div id="search-results" style="position:absolute; z-index:1000; background:#fff; border:1px solid #d1d5db; border-radius:4px; max-height:300px; overflow-y:auto; width: calc(100% - 80px); left:70px; top: 32px; display:none;"></div> </div> <div style="margin-bottom:8px;"> <label style="display:inline-block; width:70px; font-weight:500; color:#4b5563;">影视链接:</label> <input type="text" id="media-url" placeholder="豆瓣或TMDB链接" style="width: calc(100% - 80px); padding:5px; border:1px solid #d1d5db; border-radius:4px; font-size:13px;"> </div> <div style="margin-bottom:8px; display:flex; align-items:center;"> <button id="fetch-btn" style="background:#3b82f6; color:white; border:none; padding:6px 14px; border-radius:4px; cursor:pointer; font-size:13px; display:none;">提取并填充</button> <button id="paste-btn" style="background:#8b5cf6; color:white; border:none; padding:6px 14px; border-radius:4px; cursor:pointer; font-size:13px; display:none; margin-left:8px;">手动复制内容</button> <textarea id="backup-html" style="display:none;"></textarea> </div> <!-- 选择海报区域(Grid 布局 + 滚动条) --> <div id="image-selection" style="margin-top:12px; display:none;"> <h4 style="color:#3b82f6; margin-bottom:8px; font-size:14px;">选择海报(点击图片选择):</h4> <div id="poster-candidates" style="display: grid; grid-template-columns: repeat(5, 1fr); gap:8px; padding:8px; margin-bottom:10px; border:1px solid #e5e7eb; border-radius:4px; min-height:200px; max-height:400px; overflow-y:auto;"></div> <button id="load-more-posters" style="display: none; background:#60a5fa; color:white; border:none; padding:4px 10px; border-radius:3px; cursor:pointer; font-size:12px; margin-bottom:15px;">加载更多海报</button> <h4 style="color:#3b82f6; margin-bottom:8px; font-size:14px;">选择剧照(点击图片选择):</h4> <div id="still-candidates" style="display: none; grid-template-columns: repeat(5, 1fr); gap:8px; padding:8px; margin-bottom:10px; border:1px solid #e5e7eb; border-radius:4px; min-height:200px; max-height:400px; overflow-y:auto;"></div> <button id="load-more-stills" style="display: none; background:#60a5fa; color:white; border:none; padding:4px 10px; border-radius:3px; cursor:pointer; font-size:12px; margin-bottom:15px;">加载更多剧照</button> <!-- 清除按钮与确认按钮同组 --> <div style="display:flex; align-items:center; margin-top:10px;"> <button id="clear-btn" style="background:#ef4444; color:white; border:none; padding:6px 14px; border-radius:4px; cursor:pointer; font-size:13px; margin-right:8px;">清除</button> <button id="confirm-images-btn" style="background:#10b981; color:white; border:none; padding:6px 16px; border-radius:4px; cursor:pointer; font-size:13px;">确认选择并填充(自动保存编辑内容)</button> </div> </div> <div id="status" style="margin-top:8px; padding:6px; border-radius:4px; font-size:12px; display:none;"></div> `; return panel; } // 插入面板到图片标记的固定位置(精准定位) function insertPanelInMarkedPosition() { if (isPanelInitialized) return; // 创建面板 panel = createPanel(); // 扩展目标容器选择器 + 空值安全处理 const targetContainers = [ // 优先匹配内容区域主容器 document.querySelector('.main-content'), // 对可能为 null 的选择器进行空值包装 (document.querySelector('#thread-create-form') || {}).parentElement, (document.querySelector('.post-form') || {}).parentElement, document.querySelector('.panel-default'), document.body // 最终 fallback 到 body ]; let targetContainer = null; // 遍历找到第一个有效容器 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'); if (oldPanel) targetContainer.removeChild(oldPanel); // 插入新面板到容器顶部 if (targetContainer.firstChild) { targetContainer.insertBefore(panel, targetContainer.firstChild); } else { targetContainer.appendChild(panel); } // 初始化组件 posterContainer = document.getElementById('poster-candidates'); stillContainer = document.getElementById('still-candidates'); initFormatTools(); bindEventListeners(); 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)) { if (parent.firstChild) { parent.insertBefore(panel || createPanel(), parent.firstChild); } else { parent.appendChild(panel || createPanel()); } // 重新初始化以保持功能完整 posterContainer = document.getElementById('poster-candidates'); stillContainer = document.getElementById('still-candidates'); initFormatTools(); bindEventListeners(); } }); }); panelObserver.observe(parent, { childList: true, attributes: true, subtree: true, characterData: true }); } // 工具函数 function showStatus(text, isError = false) { const status = document.getElementById('status'); if (!status) return; status.textContent = text; status.style.display = 'block'; status.style.cssText = ` margin-top:8px; padding:6px; border-radius:4px; font-size:12px; ${isError ? 'background:#fee2e2; color:#b91c1c; border:1px solid #fecaca;' : 'background:#ecfdf5; color:#065f46; border:1px solid #d1fae5;'} `; } 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; } } // 图片获取函数 function getImageDataURLWithQuality(url) { return new Promise((resolve) => { if (!url) { resolve('https://picsum.photos/800/450?default-still'); return; } let baseUrl = url; if (baseUrl.includes('doubanio.com') && !baseUrl.includes('https:')) { baseUrl = 'https:' + baseUrl; } if (baseUrl.includes('doubanio.com') && baseUrl.includes('/m/')) { const qualityUrls = TMDB_CONFIG.DOUBAN_QUALITY.PRIORITY.map(quality => baseUrl.replace('/m/', `/${quality}/`) ); const tryQuality = (index, retryCount = 0) => { if (index >= qualityUrls.length) { getFallbackImageDataURL(baseUrl).then(resolve); return; } const currentUrl = qualityUrls[index]; GM_xmlhttpRequest({ method: 'GET', url: currentUrl, headers: { ...COMMON_HEADERS, 'Referer': 'https://movie.douban.com/' }, responseType: 'blob', timeout: TMDB_CONFIG.DOUBAN_QUALITY.TIMEOUT, onload: (res) => { if (res.status === 200 && res.response) { const reader = new FileReader(); reader.onload = (e) => resolve(e.target.result); reader.readAsDataURL(res.response); } else if (retryCount < TMDB_CONFIG.DOUBAN_QUALITY.RETRY) { setTimeout(() => tryQuality(index, retryCount + 1), 500); } else { tryQuality(index + 1); } }, onerror: () => { if (retryCount < TMDB_CONFIG.DOUBAN_QUALITY.RETRY) { setTimeout(() => tryQuality(index, retryCount + 1), 500); } else { tryQuality(index + 1); } }, ontimeout: () => { if (retryCount < TMDB_CONFIG.DOUBAN_QUALITY.RETRY) { setTimeout(() => tryQuality(index, retryCount + 1), 500); } else { tryQuality(index + 1); } } }); }; tryQuality(0); return; } getFallbackImageDataURL(baseUrl).then(resolve); }); } function getFallbackImageDataURL(url) { return new Promise(resolve => { 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, onload: (res) => { if (res.status === 200 && res.response) { const reader = new FileReader(); reader.onload = (e) => resolve(e.target.result); reader.readAsDataURL(res.response); } else { resolve('https://picsum.photos/800/450?error-still'); } }, onerror: () => resolve('https://picsum.photos/800/450?error-still'), ontimeout: () => resolve('https://picsum.photos/800/450?error-still') }); }); } // 确保URL有效性,保障加载更多功能(去重处理) async function getDoubanOfficialPosters(subjectUrl) { return new Promise(resolve => { try { const urlObj = new URL(subjectUrl); // 访问豆瓣“全部图片”页面(type=R),获取更多海报 const photosUrl = subjectUrl.replace(/\/subject\/(\d+)\/?$/, '/subject/$1/photos?type=R'); GM_xmlhttpRequest({ method: 'GET', url: photosUrl, headers: { ...COMMON_HEADERS, 'Referer': subjectUrl, 'Host': urlObj.hostname }, timeout: 8000, onload: (res) => { try { const doc = new DOMParser().parseFromString(res.responseText, 'text/html'); // 提取所有海报图片(含多页),并替换为高质量版本(m→l) const posterImgs = Array.from(doc.querySelectorAll('.poster-col3 li img')) .map(img => { let src = img.src; if (src.includes('/m/')) src = src.replace('/m/', '/l/'); // 低质量→高质量 return { url: src, id: src }; // 返回带id的对象(用url作为唯一标识) }) .filter(Boolean); // 去重处理(通过id) const uniquePosters = []; const idSet = new Set(); posterImgs.forEach(img => { if (!idSet.has(img.id)) { idSet.add(img.id); uniquePosters.push(img); } }); resolve(uniquePosters.map(img => img.url)); // 仍返回url数组,保持原有逻辑兼容 } catch (e) { console.error('解析豆瓣海报页面失败:', e); resolve([]); } }, onerror: () => { console.error('获取豆瓣海报请求错误'); resolve([]); }, ontimeout: () => { console.error('获取豆瓣海报请求超时'); resolve([]); } }); } catch (e) { console.error('无效的豆瓣主题URL:', subjectUrl, e); resolve([]); } }); } // 确保URL有效性,保障剧照展示(去重处理) function getDoubanStillsList(url) { return new Promise(resolve => { try { const urlObj = new URL(url); const stillsUrl = url.replace(/\/subject\/(\d+)\/?$/, '/subject/$1/photos?type=still'); GM_xmlhttpRequest({ method: 'GET', url: stillsUrl, headers: { ...COMMON_HEADERS, 'Referer': url, 'Host': urlObj.hostname }, timeout: 8000, onload: (res) => { try { const doc = new DOMParser().parseFromString(res.responseText, 'text/html'); const labeledStills = Array.from(doc.querySelectorAll('.poster-col3 li img[data-title*="剧照"]')).map(img => img.src).filter(Boolean); const finalStills = labeledStills.length ? labeledStills : Array.from(doc.querySelectorAll('.poster-col3 li img')).map(img => img.src).filter(Boolean); // 去重处理 const uniqueStills = removeDuplicateUrls(finalStills); resolve(uniqueStills); } catch (e) { console.error('解析豆瓣剧照页面失败:', e); resolve([]); } }, onerror: () => { console.error('获取豆瓣剧照请求错误'); resolve([]); }, ontimeout: () => { console.error('获取豆瓣剧照请求超时'); resolve([]); } }); } catch (e) { console.error('无效的豆瓣URL:', url, e); resolve([]); } }); } function getTMDBStillsList(mediaType, id) { return new Promise(resolve => { const stillCutsUrl = `${TMDB_CONFIG.BASE_URL}/${mediaType}/${id}/images?api_key=${TMDB_CONFIG.API_KEY}&include_image_language=zh,en&image_type=still_cuts&sort_by=primary`; GM_xmlhttpRequest({ method: 'GET', url: stillCutsUrl, headers: { 'Authorization': `Bearer ${TMDB_CONFIG.ACCESS_TOKEN}`, 'Content-Type': 'application/json' }, timeout: 10000, onload: (res) => { try { const data = JSON.parse(res.responseText); const stillCuts = safeGet(data, 'still_cuts', []); let stillUrls = stillCuts.map(img => `${TMDB_CONFIG.IMAGE_BASE_URL}${TMDB_CONFIG.STILL_SIZE}/${img.file_path}`).filter(Boolean); if (stillUrls.length >= TMDB_CONFIG.IMAGE_CANDIDATES_COUNT) { // 去重处理 stillUrls = removeDuplicateUrls(stillUrls); resolve(stillUrls); return; } const backdropsUrl = `${TMDB_CONFIG.BASE_URL}/${mediaType}/${id}/images?api_key=${TMDB_CONFIG.API_KEY}&include_image_language=zh,en&image_type=backdrop&sort_by=vote_average.desc`; GM_xmlhttpRequest({ method: 'GET', url: backdropsUrl, headers: { 'Authorization': `Bearer ${TMDB_CONFIG.ACCESS_TOKEN}`, 'Content-Type': 'application/json' }, onload: (res) => { try { const data = JSON.parse(res.responseText); const backdrops = safeGet(data, 'backdrops', []); const backdropUrls = backdrops.map(img => `${TMDB_CONFIG.IMAGE_BASE_URL}${TMDB_CONFIG.STILL_SIZE}/${img.file_path}`).filter(Boolean); stillUrls = [...stillUrls, ...backdropUrls].slice(0, TMDB_CONFIG.IMAGE_CANDIDATES_COUNT); // 去重处理 stillUrls = removeDuplicateUrls(stillUrls); if (stillUrls.length >= TMDB_CONFIG.IMAGE_CANDIDATES_COUNT) { resolve(stillUrls); return; } const postersUrl = `${TMDB_CONFIG.BASE_URL}/${mediaType}/${id}/images?api_key=${TMDB_CONFIG.API_KEY}&include_image_language=zh,en&image_type=poster&sort_by=primary`; GM_xmlhttpRequest({ method: 'GET', url: postersUrl, headers: { 'Authorization': `Bearer ${TMDB_CONFIG.ACCESS_TOKEN}`, 'Content-Type': 'application/json' }, onload: (res) => { try { const data = JSON.parse(res.responseText); const posters = safeGet(data, 'posters', []); const posterUrls = posters.slice(1).map(img => `${TMDB_CONFIG.IMAGE_BASE_URL}${TMDB_CONFIG.STILL_SIZE}/${img.file_path}`).filter(Boolean); stillUrls = [...stillUrls, ...posterUrls].slice(0, TMDB_CONFIG.IMAGE_CANDIDATES_COUNT); // 去重处理 stillUrls = removeDuplicateUrls(stillUrls); resolve(stillUrls); } catch (e) { resolve(stillUrls); } }, onerror: () => resolve(stillUrls), ontimeout: () => resolve(stillUrls) }); } catch (e) { resolve(stillUrls); } }, onerror: () => resolve(stillUrls), ontimeout: () => resolve(stillUrls) }); } catch (e) { resolve([]); } }, onerror: () => resolve([]), ontimeout: () => resolve([]) }); }); } // 搜索相关函数 function searchDouban(query) { return new Promise((resolve) => { const url = `https://search.douban.com/movie/subject_search?search_text=${encodeURIComponent(query)}&cat=1002`; GM_xmlhttpRequest({ method: 'GET', url: url, headers: { ...COMMON_HEADERS, 'Referer': 'https://movie.douban.com/', 'Host': 'search.douban.com' }, timeout: 8000, onload: (res) => { try { const html = res.responseText; const dataMatch = html.match(/window\.__DATA__\s*=\s*({.*?});/s); if (!dataMatch || !dataMatch[1]) { resolve([]); return; } 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', []); const 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); resolve(results); } catch (e) { resolve([]); } }, onerror: () => resolve([]), ontimeout: () => resolve([]) }); }); } function searchTMDB(query) { return new Promise((resolve) => { const searchUrl = `${TMDB_CONFIG.BASE_URL}/search/multi?api_key=${TMDB_CONFIG.API_KEY}&query=${encodeURIComponent(query)}&language=zh-CN`; GM_xmlhttpRequest({ method: 'GET', url: searchUrl, headers: { 'Authorization': `Bearer ${TMDB_CONFIG.ACCESS_TOKEN}`, 'Content-Type': 'application/json' }, timeout: 8000, onload: (res) => { try { const data = JSON.parse(res.responseText); const results = safeGet(data, 'results', []).map(item => ({ title: item.title || item.name || '未知作品', type: item.media_type === 'movie' ? '电影' : item.media_type === 'tv' ? '电视剧' : '未知', year: item.release_date?.split('-')[0] || item.first_air_date?.split('-')[0] || '未知', source: 'TMDB', id: item.id || '', url: item.media_type === 'movie' ? `https://www.themoviedb.org/movie/${item.id}` : item.media_type === 'tv' ? `https://www.themoviedb.org/tv/${item.id}` : '', poster: item.poster_path ? `${TMDB_CONFIG.IMAGE_BASE_URL}${TMDB_CONFIG.POSTER_SIZE}/${item.poster_path}` : '' })).filter(item => item.url); resolve(results); } catch (e) { resolve([]); } }, onerror: () => resolve([]), ontimeout: () => 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(); let lastSearchQuery = ''; let lastSearchResults = []; if (!searchInput || !resultsContainer || !loadingIndicator || !mediaUrlInput) return; searchInput.addEventListener('input', debounce(async function () { const query = this.value.trim(); lastSearchQuery = query; if (!query) { resultsContainer.style.display = 'none'; loadingIndicator.style.display = 'none'; lastSearchResults = []; abortLazyLoad(); return; } loadingIndicator.style.display = 'block'; resultsContainer.style.display = 'none'; abortLazyLoad(); lazyLoadController = new AbortController(); try { const searchPromise = Promise.all([searchDouban(query), searchTMDB(query)]); const [doubanResults, tmdbResults] = await Promise.race([ searchPromise, new Promise(resolve => setTimeout(() => resolve([[], []]), 8000)) ]); loadingIndicator.style.display = 'none'; const allResults = [...doubanResults, ...tmdbResults]; const uniqueResults = allResults.filter((item, index, self) => index === self.findIndex(t => t.title === item.title && t.year === item.year) ); lastSearchResults = uniqueResults; if (uniqueResults.length === 0) { resultsContainer.innerHTML = '<div style="padding:6px; color:#6b7280;">未找到结果,请尝试其他关键词</div>'; resultsContainer.style.display = 'block'; return; } let resultHtml = '<div class="results-container">'; uniqueResults.forEach((item, index) => { const isDouban = item.source === '豆瓣'; const isTMDB = item.source === 'TMDB'; resultHtml += ` <div class="search-item" data-url="${item.url}" data-type="${item.type}" data-index="${index}" style="padding:6px; cursor:pointer; border-bottom:1px solid #e5e7eb; display:flex; align-items:center; gap:6px; ${isDouban ? 'background:#eff6ff;' : isTMDB ? 'background:#e0f2fe;' : ''}"> <div class="poster-placeholder" style="width:36px; height:54px; background:#f3f4f6; border-radius:3px; display:flex; align-items:center; justify-content:center; color:#9ca3af;"> <i class="fa fa-film" style="font-size:14px;"></i> </div> <div> <div><strong>${item.title}</strong> ${isDouban ? '<span style="background:#3b82f6; color:white; font-size:9px; padding:1px 2px; border-radius:2px;">豆瓣</span>' : isTMDB ? '<span style="background:#0ea5e9; color:white; font-size:9px; padding:1px 2px; border-radius:2px;">TMDB</span>' : ''}</div> <div style="font-size:11px; color:#6b7280;">${item.type} · ${item.year} · ${item.source}</div> </div> </div> `; }); resultHtml += '</div>'; resultsContainer.innerHTML = resultHtml; resultsContainer.style.display = 'block'; const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const item = entry.target; const index = parseInt(item.getAttribute('data-index')); const resultItem = uniqueResults[index]; if (resultItem.poster && !lazyLoadController.signal.aborted) { getImageDataURLWithQuality(resultItem.poster).then(dataUrl => { if (lazyLoadController.signal.aborted) return; const placeholder = item.querySelector('.poster-placeholder'); if (placeholder) { placeholder.innerHTML = `<img src="${dataUrl}" style="width:36px; height:54px; object-fit:cover; border-radius:3px;" alt="${resultItem.title}">`; } }); } observer.unobserve(item); } }); }, { rootMargin: '0px 0px 150px 0px' }); document.querySelectorAll('.search-item').forEach(item => { observer.observe(item); }); } catch (e) { loadingIndicator.style.display = 'none'; resultsContainer.innerHTML = '<div style="padding:6px; color:#6b7280;">搜索出错,请重试</div>'; resultsContainer.style.display = 'block'; } }, 500)); function abortLazyLoad() { if (lazyLoadController) { lazyLoadController.abort(); } } searchInput.addEventListener('blur', () => { const hideTimeout = setTimeout(() => { resultsContainer.style.display = 'none'; }, 200); searchInput.dataset.hideTimeout = hideTimeout; }); searchInput.addEventListener('focus', function () { const existingTimeout = this.dataset.hideTimeout; if (existingTimeout) { clearTimeout(existingTimeout); delete this.dataset.hideTimeout; } const query = this.value.trim(); if (query) { resultsContainer.style.display = 'block'; if (lastSearchResults.length === 0 || lastSearchQuery !== query) { const inputEvent = new Event('input', { bubbles: true }); this.dispatchEvent(inputEvent); } } }); resultsContainer.addEventListener('click', async function (event) { 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) { mediaUrlInput.value = url; resultsContainer.style.display = 'none'; searchInput.blur(); const fetchBtn = document.getElementById('fetch-btn'); if (fetchBtn) fetchBtn.style.display = 'none'; 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.style.display = 'inline-block'; } } } } }); document.addEventListener('click', function (event) { if (!searchInput.contains(event.target) && !resultsContainer.contains(event.target)) { resultsContainer.style.display = 'none'; } }); } // 影视信息提取 function getBasicInfo(url) { return new Promise((resolve, reject) => { 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 }; GM_xmlhttpRequest({ method: 'GET', url: url, headers: headers, timeout: 10000, onload: async (res) => { try { 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 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 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 = Array.from(doc.querySelectorAll('.attrs')[1]?.querySelectorAll('a') || []).map(w => w.textContent.trim()).join(' / ') || '未知'; const actor = Array.from(doc.querySelectorAll('a[rel="v:starring"]')).map(a => a.textContent.trim()).slice(0, 6).join(',') || '未知'; let region = '未知'; const infoLis = doc.querySelectorAll('#info li'); Array.from(infoLis).some(li => { const text = li.textContent.trim(); if (text.startsWith('制片国家/地区') || text.startsWith('地区')) { region = text.split(/[::]/)[1]?.trim() || '未知'; return true; } return false; }); let lang = '未知'; Array.from(infoLis).some(li => { const text = li.textContent.trim(); if (text.startsWith('语言')) { lang = text.split(/[::]/)[1]?.trim() || '未知'; return true; } return false; }); const release = yearElem?.textContent.trim() || (isTv ? '未知首播时间' : '未知上映时间'); const rating = doc.querySelector('strong[property="v:average"]')?.textContent || '暂无'; 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() || '未知片长'; const intro = doc.querySelector('span[property="v:summary"]')?.textContent.trim().replace(/\s+/g, ' ') || (isTv ? '暂无电视剧简介' : '暂无电影简介'); const posterUrls = await getDoubanOfficialPosters(url); const stillUrls = await getDoubanStillsList(url); resolve({ mediaType: isTv ? 'tv' : 'movie', source: '豆瓣', title, genreTags, year, alsoKnown, director, writer, actor, region, release, lang, rating, doubanId, imdbId, runtime, intro, posterUrls, stillUrls, url // 保存原始URL用于后续加载更多 }); } catch (e) { reject(new Error(`豆瓣解析失败:${e.message}`)); } }, onerror: () => reject(new Error('豆瓣请求失败')), ontimeout: () => reject(new 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 tmdbDetailUrl = `${TMDB_CONFIG.BASE_URL}/${mediaType}/${id}?api_key=${TMDB_CONFIG.API_KEY}&language=zh-CN&append_to_response=credits`; GM_xmlhttpRequest({ method: 'GET', url: tmdbDetailUrl, headers: { 'Authorization': `Bearer ${TMDB_CONFIG.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 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(' / ') || '未知') : '未知'; const writer = '未知'; const actor = (data.credits?.cast || []).slice(0, 6).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 || '暂无'; 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' ? '暂无电视剧简介' : '暂无电影简介'); let posterUrls = []; if (data.poster_path) { posterUrls.push(`${TMDB_CONFIG.IMAGE_BASE_URL}${TMDB_CONFIG.POSTER_SIZE}/${data.poster_path}`); const postersUrl = `${TMDB_CONFIG.BASE_URL}/${mediaType}/${id}/images?api_key=${TMDB_CONFIG.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 ${TMDB_CONFIG.ACCESS_TOKEN}`, 'Content-Type': 'application/json' }, onload: (res) => { try { const posterData = JSON.parse(res.responseText); const additionalPosters = safeGet(posterData, 'posters', []) .slice(1, TMDB_CONFIG.IMAGE_CANDIDATES_COUNT) .map(img => `${TMDB_CONFIG.IMAGE_BASE_URL}${TMDB_CONFIG.POSTER_SIZE}/${img.file_path}`) .filter(Boolean); posterUrls = [...posterUrls, ...additionalPosters].slice(0, TMDB_CONFIG.IMAGE_CANDIDATES_COUNT); // 去重处理 posterUrls = removeDuplicateUrls(posterUrls); } catch (e) { console.log('获取更多海报失败:', e); } resolvePosters(); }, onerror: () => resolvePosters(), ontimeout: () => resolvePosters() }); }); } const stillUrls = await getTMDBStillsList(mediaType, id); resolve({ mediaType, source: 'TMDB', title, genreTags, year, alsoKnown, director, writer, actor, region, release, lang, rating, tmdbId, imdbId, runtime, intro, posterUrls, stillUrls, url // 保存原始URL用于后续加载更多 }); } catch (e) { reject(new Error(`TMDB解析失败:${e.message}`)); } }, onerror: () => reject(new Error('TMDB请求失败')), ontimeout: () => reject(new Error('TMDB请求超时')) }); } else { reject(new Error('不支持的链接类型(仅支持豆瓣、TMDB)')); } }); } function getHotComments(url) { return new Promise(resolve => { if (url.includes('themoviedb.org')) { resolve([]); return; } const commentUrl = url.replace(/\/subject\/(\d+)\/?$/, '/subject/$1/comments?status=P'); GM_xmlhttpRequest({ method: 'GET', url: commentUrl, headers: { ...COMMON_HEADERS, 'Referer': url, 'Host': new URL(url).hostname }, timeout: 8000, onload: (res) => { try { const doc = new DOMParser().parseFromString(res.responseText, 'text/html'); const comments = Array.from(doc.querySelectorAll('.comment-item .short')) .slice(0, 3) .map(elem => elem.textContent.trim()) .filter(Boolean); resolve(comments.length ? comments : []); } catch (e) { resolve([]); } }, onerror: () => resolve([]), ontimeout: () => resolve([]) }); }); } // 加载更多海报功能(每组5张,滚轮触发,去重优化) async function loadMorePosters() { if (isLoadingPosters) { showStatus('正在加载海报,请稍候...', false); return; } if (!currentMovieInfo) { showStatus('未找到影视信息,请重新加载', true); return; } isLoadingPosters = true; const loadMorePostersBtn = document.getElementById('load-more-posters'); if (loadMorePostersBtn) { loadMorePostersBtn.textContent = '加载中...'; loadMorePostersBtn.disabled = true; } try { // 首次加载所有海报(带唯一标识)并去重 if (!window.allPostersWithIds) { window.allPostersWithIds = []; if (currentMovieInfo.source === '豆瓣') { const rawPosters = await getDoubanOfficialPosters(currentMovieInfo.url); window.allPostersWithIds = rawPosters.map(url => ({ url, id: url // 用URL作为唯一标识 })); } else if (currentMovieInfo.source === 'TMDB' && currentMovieInfo.tmdbId) { const postersUrl = `${TMDB_CONFIG.BASE_URL}/${currentMovieInfo.mediaType}/${currentMovieInfo.tmdbId}/images?api_key=${TMDB_CONFIG.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 ${TMDB_CONFIG.ACCESS_TOKEN}`, 'Content-Type': 'application/json' }, onload: (res) => { try { const posterData = JSON.parse(res.responseText); window.allPostersWithIds = safeGet(posterData, 'posters', []).map(img => ({ url: `${TMDB_CONFIG.IMAGE_BASE_URL}${TMDB_CONFIG.POSTER_SIZE}/${img.file_path}`, id: img.file_path // 用TMDB的file_path作为唯一标识 })).filter(img => img.url); // 去重处理(通过id) const uniquePosters = []; const idSet = new Set(); window.allPostersWithIds.forEach(img => { if (!idSet.has(img.id)) { idSet.add(img.id); uniquePosters.push(img); } }); window.allPostersWithIds = uniquePosters; } catch (e) { console.log('获取TMDB海报列表失败:', e); window.allPostersWithIds = []; } resolvePosters(); }, onerror: () => { window.allPostersWithIds = []; resolvePosters(); }, ontimeout: () => { window.allPostersWithIds = []; resolvePosters(); } }); }); } else { window.allPostersWithIds = []; } } const allPosters = window.allPostersWithIds || []; // 每组5张,计算当前组的起始和结束索引 const startIndex = posterPage * TMDB_CONFIG.IMAGE_CANDIDATES_COUNT; const endIndex = (posterPage + 1) * TMDB_CONFIG.IMAGE_CANDIDATES_COUNT; const postersToLoad = allPosters.slice(startIndex, endIndex); if (postersToLoad.length > 0) { if (posterContainer) { posterContainer.style.display = 'grid'; posterContainer.style.gridTemplateColumns = 'repeat(5, 1fr)'; // 每行5张,排版整齐 posterContainer.style.gap = '8px'; // 图片间距,避免遮挡 for (let i = 0; i < postersToLoad.length; i++) { const poster = postersToLoad[i]; if (loadedPosterIds.has(poster.id)) { continue; // 跳过已加载的海报(去重) } loadedPosterIds.add(poster.id); // 标记为已加载 try { const dataUrl = await getImageDataURLWithQuality(poster.url); const posterImg = document.createElement('div'); posterImg.style.cssText = ` width: 150px; height: 190px; flex-shrink: 0; border: 1px solid #e5e7eb; border-radius: 4px; cursor: pointer; overflow: hidden; background: #f9fafb; display: flex; align-items: center; justify-content: center; `; posterImg.innerHTML = `<img src="${dataUrl}" style="width:100%; height:100%; object-fit: contain;" alt="海报 ${startIndex + i + 1}">`; posterImg.addEventListener('click', function () { selectedPosterUrl = dataUrl; document.querySelectorAll('#poster-candidates > div').forEach(el => el.style.border = '1px solid #e5e7eb'); this.style.border = '3px solid #3b82f6'; }); posterContainer.appendChild(posterImg); } catch (e) { console.log(`加载海报 ${startIndex + i + 1} 失败:`, e); } } // 滚动到容器底部,显示新增海报 posterContainer.scrollTop = posterContainer.scrollHeight; // 页码递增,准备下一组加载 posterPage++; showStatus(`已加载第${posterPage}组海报`, false); } } else { showStatus('已加载全部海报', false); } } catch (e) { console.error('加载更多海报出错:', e); showStatus('加载更多海报失败,请稍后重试', true); } finally { if (loadMorePostersBtn) { loadMorePostersBtn.textContent = '加载更多海报'; loadMorePostersBtn.disabled = false; } isLoadingPosters = false; } } // 加载更多剧照功能(每组5张,滚轮触发,去重优化) async function loadMoreStills() { if (isLoadingStills) { showStatus('正在加载剧照,请稍候...', false); return; } if (!currentMovieInfo) { showStatus('未找到影视信息,请重新加载', true); return; } isLoadingStills = true; const loadMoreStillsBtn = document.getElementById('load-more-stills'); if (loadMoreStillsBtn) { loadMoreStillsBtn.textContent = '加载中...'; loadMoreStillsBtn.disabled = true; } try { // 首次加载所有剧照(带唯一标识)并去重 if (!window.allStillsWithIds) { window.allStillsWithIds = []; if (currentMovieInfo.source === '豆瓣') { const rawStills = await getDoubanStillsList(currentMovieInfo.url); window.allStillsWithIds = rawStills.map(url => ({ url, id: url // 用URL作为唯一标识 })); } else if (currentMovieInfo.source === 'TMDB' && currentMovieInfo.tmdbId) { const allStillsUrls = await getTMDBStillsList(currentMovieInfo.mediaType, currentMovieInfo.tmdbId); window.allStillsWithIds = allStillsUrls.map(url => ({ url, id: url // 用URL作为唯一标识 })); } else { window.allStillsWithIds = []; } // 去重处理(通过id) const uniqueStills = []; const idSet = new Set(); window.allStillsWithIds.forEach(img => { if (!idSet.has(img.id)) { idSet.add(img.id); uniqueStills.push(img); } }); window.allStillsWithIds = uniqueStills; } const allStills = window.allStillsWithIds || []; // 每组5张,计算当前组的起始和结束索引 const startIndex = stillPage * TMDB_CONFIG.IMAGE_CANDIDATES_COUNT; const endIndex = (stillPage + 1) * TMDB_CONFIG.IMAGE_CANDIDATES_COUNT; const stillsToLoad = allStills.slice(startIndex, endIndex); if (stillsToLoad.length > 0) { if (stillContainer) { stillContainer.style.display = 'grid'; stillContainer.style.gridTemplateColumns = 'repeat(5, 1fr)'; // 每行5张,排版整齐 stillContainer.style.gap = '8px'; // 图片间距,避免遮挡 for (let i = 0; i < stillsToLoad.length; i++) { const still = stillsToLoad[i]; if (loadedStillIds.has(still.id)) { continue; // 跳过已加载的剧照(去重) } loadedStillIds.add(still.id); // 标记为已加载 try { const dataUrl = await getImageDataURLWithQuality(still.url); const stillImg = document.createElement('div'); stillImg.style.cssText = ` width: 200px; height: 110px; flex-shrink: 0; border: 1px solid #e5e7eb; border-radius: 4px; cursor: pointer; overflow: hidden; background: #f9fafb; display: flex; align-items: center; justify-content: center; `; stillImg.innerHTML = `<img src="${dataUrl}" style="width:100%; height:100%; object-fit: contain;" alt="剧照 ${startIndex + i + 1}">`; stillImg.addEventListener('click', function () { selectedStillUrl = dataUrl; document.querySelectorAll('#still-candidates > div').forEach(el => el.style.border = '1px solid #e5e7eb'); this.style.border = '3px solid #3b82f6'; }); stillContainer.appendChild(stillImg); } catch (e) { console.log(`加载剧照 ${startIndex + i + 1} 失败:`, e); } } // 滚动到容器底部,显示新增剧照 stillContainer.scrollTop = stillContainer.scrollHeight; // 页码递增,准备下一组加载 stillPage++; showStatus(`已加载第${stillPage}组剧照`, false); } } else { showStatus('已加载全部剧照', false); if (loadMoreStillsBtn) { loadMoreStillsBtn.textContent = '没有更多剧照了'; loadMoreStillsBtn.disabled = true; loadMoreStillsBtn.style.opacity = '0.6'; } } } catch (e) { console.error('加载更多剧照出错:', e); showStatus('加载更多剧照失败,请稍后重试', true); if (loadMoreStillsBtn) { loadMoreStillsBtn.textContent = '加载失败,重试'; loadMoreStillsBtn.disabled = false; } } finally { if (loadMoreStillsBtn) { loadMoreStillsBtn.textContent = '加载更多剧照'; loadMoreStillsBtn.disabled = false; } isLoadingStills = false; } } async function showImageSelection(movieInfo) { return new Promise(async (resolve) => { if (!posterContainer || !stillContainer) { posterContainer = document.getElementById('poster-candidates'); stillContainer = document.getElementById('still-candidates'); } const imageSelection = document.getElementById('image-selection'); const loadMorePostersBtn = document.getElementById('load-more-posters'); const loadMoreStillsBtn = document.getElementById('load-more-stills'); loadedPosterIds.clear(); // 清空已加载海报标记,重新开始 posterPage = 0; // 重置海报分页 loadedStillIds.clear(); // 清空已加载剧照标记,重新开始 stillPage = 0; // 重置剧照分页 if (!posterContainer || !stillContainer || !imageSelection) { resolve(); return; } posterContainer.style.display = 'grid'; posterContainer.innerHTML = '<div style="color:#6b7280;">加载海报中...</div>'; stillContainer.innerHTML = '<div style="color:#6b7280;">加载剧照中...</div>'; imageSelection.style.display = 'block'; loadMorePostersBtn.style.display = 'none'; loadMoreStillsBtn.style.display = 'none'; if (movieInfo.posterUrls && movieInfo.posterUrls.length > 0) { posterContainer.style.display = 'grid'; posterContainer.innerHTML = ''; selectedPosterUrl = await getImageDataURLWithQuality(movieInfo.posterUrls[0]); // 初始加载第一组(5张)海报,标记已加载ID const initialPosters = movieInfo.posterUrls.slice(0, TMDB_CONFIG.IMAGE_CANDIDATES_COUNT).map(url => ({ url, id: url })); for (let i = 0; i < initialPosters.length; i++) { const poster = initialPosters[i]; loadedPosterIds.add(poster.id); // 标记初始海报为已加载 try { const dataUrl = await getImageDataURLWithQuality(poster.url); const posterImg = document.createElement('div'); posterImg.style.cssText = ` width: 150px; height: 190px; flex-shrink: 0; border: ${i === 0 ? '3px solid #3b82f6' : '1px solid #e5e7eb'}; border-radius: 4px; cursor: pointer; overflow: hidden; background: #f9fafb; display: flex; align-items: center; justify-content: center; `; posterImg.innerHTML = `<img src="${dataUrl}" onError="this.src='https://picsum.photos/150/190?default-poster'" style="width:100%; height:100%; object-fit: contain;" alt="海报 ${i + 1}">`; posterImg.addEventListener('click', function () { selectedPosterUrl = dataUrl; document.querySelectorAll('#poster-candidates > div').forEach(el => el.style.border = '1px solid #e5e7eb'); this.style.border = '3px solid #3b82f6'; }); posterContainer.appendChild(posterImg); } catch (e) { console.log(`加载初始海报 ${i + 1} 失败:`, e); } } // 如果有更多海报,显示“加载更多”按钮 if (movieInfo.posterUrls.length > TMDB_CONFIG.IMAGE_CANDIDATES_COUNT) { loadMorePostersBtn.style.display = 'inline-block'; loadMorePostersBtn.disabled = false; } } else { posterContainer.innerHTML = '<div style="color:#6b7280;">未找到海报</div>'; selectedPosterUrl = 'https://picsum.photos/800/450?default-poster'; } // 剧照初始加载逻辑:只准备加载更多按钮,不自动加载 const hasStills = (movieInfo.stillUrls && movieInfo.stillUrls.length > 0) || (currentMovieInfo.source === '豆瓣' && currentMovieInfo.url) || (currentMovieInfo.source === 'TMDB' && currentMovieInfo.tmdbId); if (hasStills) { stillContainer.innerHTML = '<div style="color:#6b7280;">点击“加载更多剧照”查看剧照</div>'; loadMoreStillsBtn.style.display = 'inline-block'; loadMoreStillsBtn.disabled = false; } else { stillContainer.style.display = 'grid'; stillContainer.innerHTML = '<div style="color:#6b7280;">未找到剧照</div>'; selectedStillUrl = 'https://picsum.photos/800/450?default-still'; loadMoreStillsBtn.style.display = 'none'; } 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; // 清空容器 buttonContainer.innerHTML = ''; categoryContainer.innerHTML = ''; // 获取所有唯一分类 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:3px 8px; background:#e2e8f0; color:#4b5563; border-radius:4px; font-size:12px; cursor:pointer; white-space:nowrap; `; // 默认选中第一个分类 if (category === categories[0]) { catBtn.style.background = '#3b82f6'; catBtn.style.color = 'white'; } // 点击分类标签过滤样式 catBtn.addEventListener('click', () => { // 更新分类按钮样式 document.querySelectorAll('#format-categories > div').forEach(btn => { btn.style.background = '#e2e8f0'; btn.style.color = '#4b5563'; }); catBtn.style.background = '#3b82f6'; catBtn.style.color = 'white'; // 显示选中分类的样式按钮 document.querySelectorAll('#format-buttons > button').forEach(btn => { const btnCategory = btn.getAttribute('data-category'); btn.style.display = btnCategory === category ? 'inline-flex' : 'none'; }); // 清空预览 previewContainer.innerHTML = '<div style="text-align:center; color:#6b7280; font-size:13px;">选择样式查看预览效果</div>'; }); categoryContainer.appendChild(catBtn); }); // 创建样式按钮 FORMAT_STYLES.forEach((style, index) => { const btn = document.createElement('button'); const iconHtml = style.icon ? `<i class="fa ${style.icon}" style="margin-right:4px;"></i>` : ''; btn.innerHTML = `${iconHtml}${style.name}`; btn.setAttribute('data-category', style.category); btn.style.cssText = ` background: #22c55e; color: white; border: none; padding: 5px 10px; border-radius: 3px; cursor: pointer; font-size: 12px; margin: 1px; display: ${index === 0 ? 'inline-flex' : 'none'}; align-items: center; display: ${style.category === categories[0] ? 'inline-flex' : 'none'}; `; // 样式预览功能 if (style.preview) { btn.addEventListener('mouseenter', () => { if (previewContainer.style.display === 'block') { previewContainer.innerHTML = ` <div style="margin-bottom:5px; font-size:13px; color:#4b5563; font-weight:500;"> ${style.name} 预览: </div> <div class="style-preview-content"> ${style.apply()} </div> `; } }); } // 样式应用功能 btn.addEventListener('click', async (e) => { e.stopPropagation(); e.preventDefault(); // 添加点击动画反馈 btn.style.background = '#166534'; setTimeout(() => { btn.style.background = '#22c55e'; }, 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 ); } const styledHtml = style.apply(selectedText); 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); }); // 预览区域切换功能 previewToggle.addEventListener('click', () => { if (previewContainer.style.display === 'none') { previewContainer.style.display = 'block'; previewToggle.textContent = '隐藏预览'; } else { previewContainer.style.display = 'none'; previewToggle.textContent = '显示预览'; } }); } function getCurrentEditor() { if (sourceCodeElement && sourceCodeElement.offsetParent !== null) { return { type: 'textarea', instance: sourceCodeElement }; } const codeMirror = document.querySelector('.CodeMirror'); if (codeMirror && codeMirror.CodeMirror) { 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' ]; for (const selector of editorSelectors) { const elem = document.querySelector(selector); if (elem && elem.style.display !== 'none') { sourceCodeElement = elem; return { type: 'textarea', instance: elem }; } } return null; } // 绑定按钮事件 function bindEventListeners() { const 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 (mediaUrlInput) { mediaUrlInput.addEventListener('input', function () { if (fetchBtn) { fetchBtn.style.display = this.value.trim() ? 'inline-block' : 'none'; } }); } if (fetchBtn) { fetchBtn.addEventListener('click', async function () { const url = mediaUrlInput.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); } }); } if (pasteBtn) { pasteBtn.addEventListener('click', async function () { 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 () { if (mediaUrlInput) mediaUrlInput.value = ''; if (document.getElementById('search-movie')) document.getElementById('search-movie').value = ''; if (document.getElementById('search-results')) document.getElementById('search-results').style.display = 'none'; if (document.getElementById('image-selection')) document.getElementById('image-selection').style.display = 'none'; if (posterContainer) posterContainer.innerHTML = ''; if (stillContainer) stillContainer.innerHTML = ''; if (fetchBtn) fetchBtn.style.display = 'none'; selectedPosterUrl = ''; selectedStillUrl = ''; currentMovieInfo = null; currentComments = []; posterPage = 0; loadedPosterIds.clear(); // 清空去重标记 stillPage = 0; loadedStillIds.clear(); // 清空剧照去重标记 showStatus('已清除所有内容', false); }); } if (confirmImagesBtn) { confirmImagesBtn.addEventListener('click', async function () { if (!currentMovieInfo) { showStatus('未找到影视信息,请重新加载', true); return; } const finalPosterUrl = selectedPosterUrl || 'https://picsum.photos/800/450?default-poster'; const finalStillUrl = selectedStillUrl || 'https://picsum.photos/800/450?default-still'; showStatus('正在生成HTML内容...', false); const html = generateHTML(currentMovieInfo, currentComments, finalPosterUrl, finalStillUrl); const backupHtml = document.getElementById('backup-html'); if (backupHtml) backupHtml.value = html; const success = await fillAndSaveSource(html); if (success) { showStatus('内容已填充并自动保存(非发布)', false); } }); } // 绑定加载更多海报按钮 if (loadMorePostersBtn) { loadMorePostersBtn.addEventListener('click', loadMorePosters); } // 绑定加载更多剧照按钮 if (loadMoreStillsBtn) { loadMoreStillsBtn.addEventListener('click', loadMoreStills); } // 绑定海报容器滚轮事件:点击“加载更多海报”后,滚轮可触发加载下一组 if (posterContainer) { posterContainer.addEventListener('wheel', function (e) { // 只有点击过“加载更多海报”(posterPage > 0)才开启滚轮加载 if (posterPage > 0 && !isLoadingPosters) { // 向下滚动时加载下一组 if (e.deltaY > 0) { const loadMoreBtn = document.getElementById('load-more-posters'); if (loadMoreBtn) { loadMoreBtn.click(); } } } }); } // 绑定剧照容器滚轮事件:点击“加载更多剧照”后,滚轮可触发加载下一组 if (stillContainer) { stillContainer.addEventListener('wheel', function (e) { // 只有点击过“加载更多剧照”(stillPage > 0)才开启滚轮加载 if (stillPage > 0 && !isLoadingStills) { // 向下滚动时加载下一组 if (e.deltaY > 0) { const loadMoreBtn = document.getElementById('load-more-stills'); if (loadMoreBtn) { loadMoreBtn.click(); } } } }); } setupSearchInteractions(); } // 初始化页面 function init() { // 加载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'; document.head.appendChild(faLink); // 插入控制面板 insertPanelInMarkedPosition(); // 检查是否有默认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) fetchBtn.style.display = 'inline-block'; } } // 启动脚本 init(); })();