您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
种子标题智能编辑工具:支持方块化拖拽重组、在线保存、多种显示模式、错误方块检测、种子列表页快捷编辑,让标题修复变得简单高效。
// ==UserScript== // @name 青蛙种子标题快修 // @namespace http://tampermonkey.net/ // @version 1.1.1 // @description 种子标题智能编辑工具:支持方块化拖拽重组、在线保存、多种显示模式、错误方块检测、种子列表页快捷编辑,让标题修复变得简单高效。 // @author You // @match *://www.qingwapt.com/details.php* // @match *://new.qingwa.pro/details.php* // @match *://www.qingwapt.org/details.php* // @match *://www.qingwapt.com/torrents.php* // @match *://new.qingwa.pro/torrents.php* // @match *://www.qingwapt.org/torrents.php* // @grant none // @license MIT // ==/UserScript== (function () { 'use strict'; // 显示模式:0=标题下方(紧凑), 1=右侧悬浮, 2=收起图标 let displayMode = 0; let canEdit = false; // 是否可以编辑(能否访问edit页面) let torrentId = null; // 种子ID let isBlockMode = false; // 是否处于方块编辑模式 let isDeleteMode = false; // 是否处于删除模式 let isErrorDetectionEnabled = true; // 是否启用错误检测(默认开启) let hasErrors = false; // 是否有错误 let isListPage = false; // 是否是种子列表页面 let easterEggEnabled = true; // 设置为 false 可禁用彩蛋 // 种子列表页面相关状态 let activeListEditor = null; // 当前活跃的列表编辑器ID // 两层方块结构 let rawBlocks = []; // 原始方块:最基础的单元 let finalBlocks = []; // 最终方块:用于显示的方块,每个都包含原始方块数组 let dragState = { isDragging: false, dragElement: null, dragIndex: -1, placeholder: null, currentContainer: null, originalNextSibling: null }; // 定义元素类型和颜色 const ELEMENT_TYPES = { SERIES_NAME: { color: '#d1ecf1', textColor: '#0c5460', name: '剧名' }, SEASON_EPISODE: { color: '#d4edda', textColor: '#155724', name: '季集' }, YEAR: { color: '#fff3cd', textColor: '#856404', name: '年份' }, RESOLUTION: { color: '#f8d7da', textColor: '#721c24', name: '分辨率' }, REGION_CODE: { color: '#e2e3e5', textColor: '#383d41', name: '地区码' }, SOURCE_TYPE: { color: '#cce5ff', textColor: '#004085', name: '片源' }, SPEC: { color: '#ffeaa7', textColor: '#b7651d', name: '规格' }, HDR_TYPE: { color: '#fdcae1', textColor: '#6f1734', name: 'HDR' }, VIDEO_CODEC: { color: '#c3e6cb', textColor: '#155724', name: '视频编码' }, PROFILE: { color: '#a8e6cf', textColor: '#2d5016', name: 'Profile' }, AUDIO_CODEC: { color: '#bee5eb', textColor: '#0c5460', name: '音频编码' }, CHANNEL_INFO: { color: '#d4f6ff', textColor: '#0c5460', name: '声道数' }, OBJECT_AUDIO: { color: '#f3e5f5', textColor: '#6a1b9a', name: '对象信息' }, STREAMING_SERVICE: { color: '#e1d5e7', textColor: '#6f1734', name: '流媒体' }, DISC_BRAND: { color: '#ffe6cc', textColor: '#cc5500', name: '碟片品牌' }, TV_STATION: { color: '#e6f3ff', textColor: '#0066cc', name: '电视台' }, RELEASE_GROUP: { color: '#e1ecf4', textColor: '#0c5460', name: '制作组' }, TRACK_INFO: { color: '#ffe4b5', textColor: '#8b5c2a', name: '音轨数' }, OTHER: { color: '#f8f9fa', textColor: '#6c757d', name: '其他' } }; // 检测方块错误 function detectBlockErrors(blocks) { hasErrors = false; // 重置错误状态 let allFalse = blocks.map(block => ({ ...block, renderError: false })); let result = allFalse; const firstSourceOrSpecIndex = result.findIndex(block => block.type === 'SOURCE_TYPE' || block.type === 'SPEC'); // 检查每个方块类型是否属于不应单独出现的类型 result.forEach((block, idx) => { const errorTypes = [ 'TV_STATION', 'DISC_BRAND', 'PROFILE', 'CHANNEL_INFO', 'OBJECT_AUDIO' ]; if (errorTypes.includes(block.type)) { block.renderError = true; return; } if (block.type == 'RESOLUTION' && firstSourceOrSpecIndex < idx) { block.renderError = true; // 分辨率必须在片源或规格之前 return; } if (block.type == 'SEASON_EPISODE' && blocks.at(idx - 1)?.type === 'YEAR') { block.renderError = true; return; } if (block.type === 'YEAR' && blocks.at(idx + 1)?.type === 'SEASON_EPISODE') { block.renderError = true; return; } if (block.type === 'HDR_TYPE' && blocks.at(idx + 1)?.type !== 'VIDEO_CODEC') { block.renderError = true; return; } if (block.type === 'VIDEO_CODEC' && blocks.at(idx + 1)?.type !== 'AUDIO_CODEC') { block.renderError = true; return; } if (block.type === 'AUDIO_CODEC' && blocks.at(idx - 1)?.type !== 'VIDEO_CODEC') { block.renderError = true; return; } if (block.type === 'TRACK_INFO' && blocks.at(idx - 1)?.type !== 'AUDIO_CODEC') { block.renderError = true; return; } }); hasErrors = result.some(block => block.renderError); if (!isErrorDetectionEnabled || isDeleteMode) return allFalse; // 如果禁用错误检测或处于删除模式,返回所有方块不显示错误 return result; } // 检查当前是否有错误方块 function hasErrorBlocks() { return hasErrors; } // 切换错误检测模式 function toggleErrorDetection() { isErrorDetectionEnabled = !isErrorDetectionEnabled; // 更新错误检测按钮状态 updateErrorDetectionButtons(); // 重新渲染方块以显示/隐藏错误提示 renderTitleBlocks(finalBlocks); } // 更新错误检测按钮的显示状态 function updateErrorDetectionButtons() { const errorBtn = document.getElementById('toggleErrorDetection'); const errorBtnFloat = document.getElementById('toggleErrorDetectionFloat'); const hasErrors = hasErrorBlocks(); // 紧凑模式按钮 if (errorBtn) { if (!hasErrors) { // 无错误时:绿色背景,禁用 errorBtn.textContent = '✓'; errorBtn.title = '未检测到错误'; errorBtn.style.background = '#28a745'; errorBtn.style.color = 'white'; errorBtn.disabled = true; errorBtn.style.opacity = '0.6'; errorBtn.style.cursor = 'not-allowed'; } else if (isErrorDetectionEnabled) { errorBtn.textContent = '🙈'; errorBtn.title = '隐藏错误提示'; errorBtn.style.background = '#fd7e14'; errorBtn.style.color = 'white'; errorBtn.disabled = false; errorBtn.style.opacity = '1'; errorBtn.style.cursor = 'pointer'; } else { errorBtn.textContent = '⚠️'; errorBtn.title = '显示错误提示'; errorBtn.style.background = '#6c757d'; errorBtn.style.color = 'white'; errorBtn.disabled = false; errorBtn.style.opacity = '1'; errorBtn.style.cursor = 'pointer'; } } // 悬浮模式按钮 if (errorBtnFloat) { if (!hasErrors) { errorBtnFloat.textContent = '✓ 无错误'; errorBtnFloat.title = '未检测到错误'; errorBtnFloat.style.background = '#28a745'; errorBtnFloat.style.color = 'white'; errorBtnFloat.disabled = true; errorBtnFloat.style.opacity = '0.6'; errorBtnFloat.style.cursor = 'not-allowed'; } else if (isErrorDetectionEnabled) { errorBtnFloat.textContent = '🙈 隐藏错误'; errorBtnFloat.title = '隐藏错误提示'; errorBtnFloat.style.background = '#fd7e14'; errorBtnFloat.style.color = 'white'; errorBtnFloat.disabled = false; errorBtnFloat.style.opacity = '1'; errorBtnFloat.style.cursor = 'pointer'; } else { errorBtnFloat.textContent = '⚠️ 显示错误'; errorBtnFloat.title = '显示错误提示'; errorBtnFloat.style.background = '#6c757d'; errorBtnFloat.style.color = 'white'; errorBtnFloat.disabled = false; errorBtnFloat.style.opacity = '1'; errorBtnFloat.style.cursor = 'pointer'; } } } // 创建占位符元素(改进版) function createPlaceholder(width, height) { const placeholder = document.createElement('div'); placeholder.className = 'drag-placeholder'; placeholder.style.cssText = ` width: ${width}px; height: ${height}px; background: #e3f2fd; border: 2px dashed #2196f3; border-radius: 6px; margin: 2px; transition: all 0.2s ease; `; return placeholder; } // 改进的拖拽开始处理 function handleDragStart(e) { const target = e.target.closest('.title-block'); if (!target) return; dragState.isDragging = true; dragState.dragElement = target; dragState.dragIndex = parseInt(target.dataset.index); dragState.currentContainer = target.parentElement; dragState.originalNextSibling = target.nextSibling; // 创建占位符 const rect = target.getBoundingClientRect(); dragState.placeholder = createPlaceholder(rect.width, rect.height); // 延迟一点添加样式,避免影响拖拽图像 setTimeout(() => { target.classList.add('dragging'); target.style.opacity = '0.4'; // 插入占位符到原位置 target.parentElement.insertBefore(dragState.placeholder, target); // 暂时隐藏原元素(但不移除,以保持拖拽) target.style.display = 'none'; }, 0); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setDragImage(target, e.offsetX, e.offsetY); } // 改进的拖拽结束处理 function handleDragEnd(e) { if (!dragState.isDragging) return; const target = e.target.closest('.title-block'); if (!target) return; // 在移除占位符之前,先记录它的位置 let newPosition = -1; if (dragState.placeholder && dragState.placeholder.parentElement) { // 获取占位符在容器中的位置 const allChildren = Array.from(dragState.currentContainer.children); newPosition = allChildren.indexOf(dragState.placeholder); // 移除占位符 dragState.placeholder.remove(); } // 恢复元素显示 target.style.display = ''; target.style.opacity = ''; target.classList.remove('dragging'); // 如果位置改变了,更新方块顺序 if (newPosition !== -1 && newPosition !== dragState.dragIndex) { // 重新排序 finalBlocks const draggedBlock = finalBlocks[dragState.dragIndex]; finalBlocks.splice(dragState.dragIndex, 1); // 计算新索引(考虑移除元素后的位置调整) let newIndex = newPosition; if (dragState.dragIndex < newPosition) { newIndex--; } // 确保索引在有效范围内 newIndex = Math.max(0, Math.min(finalBlocks.length, newIndex)); finalBlocks.splice(newIndex, 0, draggedBlock); // 重组并更新 recombineBlocks(); renderTitleBlocks(finalBlocks); updateTitleFromBlocks(); updateErrorDetectionButtons(); } // 重置状态 dragState.isDragging = false; dragState.dragElement = null; dragState.dragIndex = -1; dragState.placeholder = null; dragState.currentContainer = null; } // 改进的拖拽悬停处理 function handleDragOver(e) { e.preventDefault(); if (!dragState.isDragging || !dragState.placeholder) return; const container = e.currentTarget; const afterElement = getDragAfterElement(container, e.clientX); if (afterElement == null) { // 放在最后 container.appendChild(dragState.placeholder); } else if (afterElement !== dragState.placeholder) { // 插入到特定位置 container.insertBefore(dragState.placeholder, afterElement); } } // 获取应该插入的位置 function getDragAfterElement(container, x) { const draggableElements = [...container.querySelectorAll('.title-block:not(.dragging)')]; return draggableElements.reduce((closest, child) => { const box = child.getBoundingClientRect(); const offset = x - box.left - box.width / 2; if (offset < 0 && offset > closest.offset) { return { offset: offset, element: child }; } else { return closest; } }, { offset: Number.NEGATIVE_INFINITY }).element; } // 简化的拖拽放下处理 function handleDrop(e) { e.preventDefault(); // 实际的重排序已经在 handleDragEnd 中处理 } // 将标题分割成原始方块(最基础单元) function splitTitleIntoRawBlocks(title) { if (!title) return []; // 只用正则识别跨空格的完整词组 const crossSpacePatterns = [ // 音频编码跨空格组合 /\b(DTS-HD\s+MA)\b/gi, /\b(DTS-HD\s+HRA)\b/gi, // HDR格式跨空格组合 /\b((DV|DoVi)\s+HDR10\+)(?=\s|$)/gi, /\b((DV|DoVi)\s+HDR)\b/gi, /\b(HDR\s+vivid)\b/gi, // 片源类型跨空格组合 /\b(UHD\s+Blu-ray)\b/gi, /\b(UHD\s+BluRay)\b/gi, /\b(3D\s+Blu-ray)\b/gi, /\b(3D\s+BluRay)\b/gi, /\b(HD\s+DVD)\b/gi, // 碟片品牌跨空格组合 /\b(Masters\s+of\s+Cinema)\b/gi, /\b(Warner\s+Archive\s+Collection)\b/gi, /\b(Criterion\s+Collection)\b/gi, // 电视台跨空格组合 /\b(HOY\s+TV)\b/gi, /\b(PHOENIX\s+HK)\b/gi, ]; let segments = []; let matches = []; // 收集所有跨空格词组的匹配 crossSpacePatterns.forEach(pattern => { let match; pattern.lastIndex = 0; while ((match = pattern.exec(title)) !== null) { matches.push({ start: match.index, end: match.index + match[0].length, text: match[0].trim() }); } }); // 按位置排序并去重叠 matches.sort((a, b) => a.start - b.start); let cleanMatches = []; let lastEnd = 0; matches.forEach(match => { if (match.start >= lastEnd) { cleanMatches.push(match); lastEnd = match.end; } }); // 提取文本片段 let currentPos = 0; cleanMatches.forEach(match => { // 添加匹配前的普通文本 if (match.start > currentPos) { const beforeText = title.substring(currentPos, match.start).trim(); if (beforeText) { beforeText.split(/\s+/).forEach(word => { if (word.trim()) segments.push(word.trim()); }); } } // 添加跨空格的完整词组 segments.push(match.text); currentPos = match.end; }); // 添加最后剩余的文本 if (currentPos < title.length) { const remainingText = title.substring(currentPos).trim(); if (remainingText) { remainingText.split(/\s+/).forEach(word => { if (word.trim()) segments.push(word.trim()); }); } } // 如果没有跨空格词组,就简单按空格分割 if (segments.length === 0) { segments = title.split(/\s+/).filter(word => word.trim()); } // 在原始方块生成阶段进行制作组分离处理(只对最后一个segment) const separatedSegments = []; segments.forEach((segment, index) => { if (index === segments.length - 1) { // 只对最后一个segment进行制作组分离 const separated = separateReleaseGroupInSegment(segment); separatedSegments.push(...separated); } else { separatedSegments.push(segment); } }); // 生成原始方块(简单结构) return separatedSegments.map((segment, index) => ({ id: `raw_${Date.now()}_${index}`, text: segment, type: identifyElementType(segment) })); } // 在单个片段中分离制作组(处理类似 "10bit-BeiTai" 的情况) function separateReleaseGroupInSegment(segment) { const lastDashIndex = segment.lastIndexOf('-'); // 如果包含-且不是开头,且后面还有内容,且不是年份范围或DTS-HD等固定格式 if (lastDashIndex > 0 && lastDashIndex < segment.length - 1 && !isYearRange(segment) && !segment.includes('DTS-HD') && !segment.includes('Blu-ray')) { const beforeDash = segment.substring(0, lastDashIndex); const afterDash = segment.substring(lastDashIndex); // 包含- const result = []; if (beforeDash.trim()) { result.push(beforeDash.trim()); } if (afterDash.trim()) { result.push(afterDash.trim()); } return result; } else { return [segment]; } } // 基于原始方块生成最终方块(用于显示) function combineRawBlocks(rawBlocksArray) { // 先进行组合处理 let processedBlocks = combineAdjacentElements([...rawBlocksArray]); processedBlocks = combineSeriesName(processedBlocks); return processedBlocks; } // 检测标题中是否包含HDTV或UHDTV规格 function hasHDTVorUHDTV(blocks) { return blocks.some(block => block.type === 'SPEC' && (block.text === 'HDTV' || block.text === 'UHDTV') ); } // 组合季数或年份前的剧名 function combineSeriesName(blocks) { if (blocks.length <= 1) return blocks; const result = []; let seriesNameParts = []; let foundBreakPoint = false; let skipFirstBlock = false; // 检查是否需要跳过第一个方块(电视台 + HDTV/UHDTV 的情况) if (hasHDTVorUHDTV(blocks) && blocks.length > 0 && blocks[0].type === 'TV_STATION') { skipFirstBlock = true; result.push(ensureFinalBlock(blocks[0])); // 直接添加电视台方块到结果 } // 从第二个方块开始处理(如果需要跳过第一个)或从第一个开始 const startIndex = skipFirstBlock ? 1 : 0; for (let i = startIndex; i < blocks.length; i++) { const block = blocks[i]; const currentFinal = ensureFinalBlock(block); // 如果遇到季数或年份,停止收集剧名 if (!foundBreakPoint && (block.type === 'SEASON_EPISODE' || block.type === 'YEAR')) { foundBreakPoint = true; // 如果有收集到的剧名部分,组合它们 if (seriesNameParts.length > 0) { result.push({ id: `final_series_${Date.now()}_${i}`, text: seriesNameParts.map(p => p.text).join(' '), type: 'SERIES_NAME', rawBlocks: seriesNameParts.flatMap(p => p.rawBlocks) }); } // 清空收集器并添加当前元素 seriesNameParts = []; result.push(currentFinal); } else if (!foundBreakPoint && // 只排除绝对确定的技术参数,其他都可能是剧名 !['RESOLUTION', 'SOURCE_TYPE', 'SPEC', 'HDR_TYPE', 'VIDEO_CODEC', 'PROFILE', 'AUDIO_CODEC', 'OBJECT_AUDIO'].includes(block.type)) { // 收集剧名候选(包括可能被误识别的 STREAMING_SERVICE, REGION_CODE, RELEASE_GROUP, OTHER 等) seriesNameParts.push(currentFinal); } else { // 如果已经遇到断点,或者是绝对的技术类型,直接添加 if (seriesNameParts.length > 0) { result.push({ id: `final_series_${Date.now()}_${i}`, text: seriesNameParts.map(p => p.text).join(' '), type: 'SERIES_NAME', rawBlocks: seriesNameParts.flatMap(p => p.rawBlocks) }); seriesNameParts = []; } result.push(currentFinal); foundBreakPoint = true; } } // 处理末尾剩余的剧名部分 if (seriesNameParts.length > 1) { result.push({ id: `final_series_${Date.now()}_end`, text: seriesNameParts.map(p => p.text).join(' '), type: 'SERIES_NAME', rawBlocks: seriesNameParts.flatMap(p => p.rawBlocks) }); } else if (seriesNameParts.length === 1) { result.push(seriesNameParts[0]); } return result; } // 判断是否是年份范围 function isYearRange(text) { return /^(\d{4}-\d{4}|\d{8}-\d{8})$/.test(text); } // 组合相邻的相关元素 function combineAdjacentElements(blocks) { const result = []; let i = 0; while (i < blocks.length) { const current = blocks[i]; const next = blocks[i + 1]; const next2 = blocks[i + 2]; // 确保当前方块是最终方块格式 const currentFinal = ensureFinalBlock(current); // 三元组合优先检查 // 音频编码 + 声道数 + 对象音频 if (next && next2 && current.type === 'AUDIO_CODEC' && next.type === 'CHANNEL_INFO' && next2.type === 'OBJECT_AUDIO') { const nextFinal = ensureFinalBlock(next); const next2Final = ensureFinalBlock(next2); result.push({ id: `final_audio_${Date.now()}_${i}`, text: `${current.text} ${next.text} ${next2.text}`, type: 'AUDIO_CODEC', rawBlocks: [...currentFinal.rawBlocks, ...nextFinal.rawBlocks, ...next2Final.rawBlocks] }); i += 3; continue; } // 二元组合 // (音频编码 + 声道数) + 对象音频 if (next && current.type === 'AUDIO_CODEC' && next.type === 'OBJECT_AUDIO' && /(\d\.\d)$/.match(current.text)) { const nextFinal = ensureFinalBlock(next); const currentFinal = ensureFinalBlock(current); result.push({ id: `final_audio_${Date.now()}_${i}`, text: `${current.text} ${next.text}`, type: 'AUDIO_CODEC', rawBlocks: [...currentFinal.rawBlocks, ...nextFinal.rawBlocks] }); i += 2; continue; } // 流媒体厂商 + WEB-DL if (next && current.type === 'STREAMING_SERVICE' && next.type === 'SPEC' && next.text === 'WEB-DL') { const nextFinal = ensureFinalBlock(next); result.push({ id: `final_streaming_${Date.now()}_${i}`, text: `${current.text} ${next.text}`, type: 'SOURCE_TYPE', // 组合后归类为片源 rawBlocks: [...currentFinal.rawBlocks, ...nextFinal.rawBlocks] }); i += 2; continue; } // 碟片品牌 + 片源(新增) if (next && current.type === 'DISC_BRAND' && next.type === 'SOURCE_TYPE') { const nextFinal = ensureFinalBlock(next); result.push({ id: `final_disc_source_${Date.now()}_${i}`, text: `${current.text} ${next.text}`, type: 'SOURCE_TYPE', // 组合后归类为片源 rawBlocks: [...currentFinal.rawBlocks, ...nextFinal.rawBlocks] }); i += 2; continue; } // 电视台 + 规格(HDTV/UHDTV)(新增) if (next && current.type === 'TV_STATION' && next.type === 'SPEC' && (next.text === 'HDTV' || next.text === 'UHDTV')) { const nextFinal = ensureFinalBlock(next); result.push({ id: `final_tv_spec_${Date.now()}_${i}`, text: `${current.text} ${next.text}`, type: 'SOURCE_TYPE', // 组合后归类为片源 rawBlocks: [...currentFinal.rawBlocks, ...nextFinal.rawBlocks] }); i += 2; continue; } // Profile + 视频编码 if (next && current.type === 'PROFILE' && next.type === 'VIDEO_CODEC') { const nextFinal = ensureFinalBlock(next); result.push({ id: `final_video_${Date.now()}_${i}`, text: `${current.text} ${next.text}`, type: 'VIDEO_CODEC', // 组合后归类为视频编码 rawBlocks: [...currentFinal.rawBlocks, ...nextFinal.rawBlocks] }); i += 2; continue; } // 地区码 + 片源 if (next && current.type === 'REGION_CODE' && next.type === 'SOURCE_TYPE') { const nextFinal = ensureFinalBlock(next); result.push({ id: `final_source_${Date.now()}_${i}`, text: `${current.text} ${next.text}`, type: 'SOURCE_TYPE', // 组合后归类为片源 rawBlocks: [...currentFinal.rawBlocks, ...nextFinal.rawBlocks] }); i += 2; continue; } // 音频编码 + 声道数 if (next && current.type === 'AUDIO_CODEC' && next.type === 'CHANNEL_INFO') { const nextFinal = ensureFinalBlock(next); result.push({ id: `final_audio_${Date.now()}_${i}`, text: `${current.text} ${next.text}`, type: 'AUDIO_CODEC', rawBlocks: [...currentFinal.rawBlocks, ...nextFinal.rawBlocks] }); i += 2; continue; } result.push(currentFinal); i++; } return result; } // 确保方块是最终方块格式 function ensureFinalBlock(block) { if (block.rawBlocks) { // 已经是最终方块 return block; } else { // 是原始方块,转换为最终方块 return { id: `final_${block.id}`, text: block.text, type: block.type, rawBlocks: [block] }; } } function identifyElementType(text) { if (!text) return 'OTHER'; if (text.startsWith('-')) return 'RELEASE_GROUP'; if (isSeasonEpisode(text)) return 'SEASON_EPISODE'; if (isYear(text)) return 'YEAR'; if (isResolution(text)) return 'RESOLUTION'; if (isProfile(text)) return 'PROFILE'; if (isChannelInfo(text)) return 'CHANNEL_INFO'; if (isTrackInfo(text)) return 'TRACK_INFO'; if (isRegionCode(text)) return 'REGION_CODE'; if (isSourceType(text)) return 'SOURCE_TYPE'; if (isSpec(text)) return 'SPEC'; if (isHDRType(text)) return 'HDR_TYPE'; if (isVideoCodec(text)) return 'VIDEO_CODEC'; if (isObjectAudio(text)) return 'OBJECT_AUDIO'; if (isAudioCodec(text)) return 'AUDIO_CODEC'; if (isDiscBrand(text)) return 'DISC_BRAND'; if (isTVStation(text)) return 'TV_STATION'; if (isStreamingService(text)) return 'STREAMING_SERVICE'; return 'OTHER'; } // 各种类型检测函数 function isSeasonEpisode(text) { return /^S\d+(E\d+)?(-S\d+)?(-E\d+)?$/i.test(text) || /^S\d+E\d+E\d+$/i.test(text); } function isYear(text) { return /^(\d{4}|\d{8}|\d{4}-\d{4}|\d{8}-\d{8})$/.test(text); } function isResolution(text) { const resolutions = ['4320p', '2160p', '1080p', '1080i', '720p', '576p', '576i', '480p', '480i', 'SD', 'NTSC', 'PAL']; return resolutions.includes(text); } function isRegionCode(text) { const regions = ['ITA', 'USA', 'JPN', 'HKG', 'TWN', 'GBR', 'FRA', 'GER', 'KOR', 'CHN', 'AUS', 'NLD', 'CZE', 'CEE']; return regions.includes(text); } function isSourceType(text) { const sources = ['Blu-ray', '3D Blu-ray', 'UHD Blu-ray', 'Modded Blu-ray', 'Custom BluRay', 'NTSC DVD5', 'NTSC DVD9', 'PAL DVD5', 'PAL DVD9', 'HD DVD', 'BluRay', '3D BluRay', 'UHD BluRay', 'DVDRip', 'HDDVDRip']; return sources.some(source => text.includes(source)); } function isSpec(text) { const specs = ['Remux', 'REMUX', 'WEB-DL', 'WEBRip', 'HDTV', 'UHDTV', 'HOU', 'HSBS']; return specs.includes(text); } function isHDRType(text) { const hdrTypes = ['HDR', 'HDR10+', 'HDR10', 'DV', 'HLG', 'PQ10', 'DV HDR', 'DV HDR10+', 'DoVi HDR', 'DoVi HDR10+', 'HDR vivid']; return hdrTypes.includes(text); } function isVideoCodec(text) { const codecs = ['AVC', 'HEVC', 'MPEG-2', 'VC-1', 'H.264', 'H.265', 'VP9', 'AVS+', 'AVS3', 'AV1', 'H264', 'H265', 'MPEG2', 'x264', 'x265']; return codecs.includes(text); } function isProfile(text) { return /^(Hi10P|Hi422P|Hi444PP)$/i.test(text); } function isAudioCodec(text) { // 首先检查是否以声道格式结尾(如 X.X) const channelSuffix = /(\d\.\d)$/; const channelMatch = text.match(channelSuffix); if (channelMatch) { // 如果以声道格式结尾,检查前面部分是否是音频编码 const codecPart = text.substring(0, text.length - channelMatch[1].length); const singleWordCodecs = [ 'DTS-X', 'DTS:X', 'TrueHD', 'LPCM', 'FLAC', 'DDP', 'AAC', 'MP2', 'MP3', 'OPUS', 'DTS', 'DD' ]; return singleWordCodecs.includes(codecPart); } // 检查标准的音频编码(包括跨空格的格式,这些必须保持空格) const standardCodecs = [ 'DTS-HD MA', 'DTS-HD HRA', 'DTS:X', 'DTS-X', 'TrueHD', 'LPCM', 'FLAC', 'DDP', 'AAC', 'MP2', 'MP3', 'OPUS', 'DTS', 'DD' ]; return standardCodecs.includes(text); } function isChannelInfo(text) { return /^\d\.\d$/.test(text); // 如 7.1, 5.1 } function isTrackInfo(text) { return /^\d+Audios?$/.test(text); } function isObjectAudio(text) { // 对象音频格式(目前主要是Atmos) return text.toLowerCase() === 'atmos'; } function isStreamingService(text) { // 主流流媒体厂商简称(选择比较安全、不易误识别的) const streamingServices = [ // 主流国际平台 'NF', 'AMZN', 'DSNP', 'HMAX', 'HBO', 'HULU', 'ATVP', 'PMTP', 'PCOK', 'DSCP', 'SHO', 'STZ', // 数字商店 'iT', 'PLAY', 'VMEO', 'YHOO', // 主流电视网络 'ESPN', 'NBC', 'CBS', 'FOX', 'AMBC', 'MTV', 'CW', // 专业频道 'FOOD', 'HGTV', 'TLC', 'DISC', 'ANPL', 'HIST', 'NATG', 'SYFY', 'NICK', 'FREE', 'LIFE', // 国际平台 'iP', 'ALL4', 'ITV', 'CBC', 'SBS', 'SVT', 'ZDF', 'ARD', 'NRK', 'RTE', // 其他知名平台 'TUBI', 'ROKU', 'PLUZ', 'STAN', 'BNGE', 'CRAV', 'EPIX', 'CMAX', 'SHDR', // 体育平台 'KAYO', 'UFC', 'NBA', 'NFL', 'NFLN', 'SNET', // 动画/娱乐平台 'CR', 'FUNI', 'HIDI', 'VRV', 'BOOM', 'TFOU', 'ANLB', // 新闻平台 'CNBC', 'MNBC', 'CSPN', 'AJAZ', // 亚洲平台 'TVING', 'VIKI', 'GYAO', 'Baha', 'Hami', 'HTSR', 'PUHU', // 其他 'COOK', 'TRVL', 'DIY', 'FYI', 'VH1', 'TBS', 'OXGN', 'VLCT' ]; // 精确匹配,保持大小写敏感(因为有些简称如iP需要保持特定大小写) return streamingServices.includes(text); } // 新增:碟片品牌识别函数 function isDiscBrand(text) { const discBrands = [ 'Criterion', 'CC', 'Criterion Collection', 'Masters of Cinema', 'MoC', 'Warner Archive Collection', 'WAC', 'Arrow', 'WCL', 'BFI' ]; return discBrands.includes(text); } // 新增:电视台识别函数 function isTVStation(text) { if (/^CCTV-?\d+[K]?$/i.test(text)) { return true; } // 其他电视台 const tvStations = [ 'DragonTV', 'ZJTV', 'HNTV', 'HNSTV', 'GDTV', 'JSTV', 'BRTV', 'Jade', 'Pearl', 'MATV', 'HOY TV', 'PHOENIX HK', 'TVB', 'ViuTV', 'RTHK31', 'CWJDTV' ]; return tvStations.includes(text); } // 从标题更新方块 function updateBlocksFromTitle(title) { rawBlocks = splitTitleIntoRawBlocks(title); finalBlocks = combineRawBlocks(rawBlocks); renderTitleBlocks(finalBlocks); // 更新错误检测按钮状态 updateErrorDetectionButtons(); } // 切换删除模式 function toggleDeleteMode() { isDeleteMode = !isDeleteMode; // 更新删除按钮状态 updateDeleteButtons(); // 重新渲染方块以显示/隐藏删除按钮 renderTitleBlocks(finalBlocks); // 如果退出删除模式,确保没有方块处于晃动状态 if (!isDeleteMode) { document.querySelectorAll('.title-block').forEach(block => { block.classList.remove('delete-mode'); }); } } // 更新删除按钮的显示状态 function updateDeleteButtons() { const deleteBtn = document.getElementById('toggleDeleteMode'); const deleteBtnFloat = document.getElementById('toggleDeleteModeFloat'); if (deleteBtn) { if (isDeleteMode) { deleteBtn.textContent = '✅'; deleteBtn.title = '退出删除模式'; deleteBtn.style.background = '#dc3545'; deleteBtn.style.color = 'white'; } else { deleteBtn.textContent = '🗑️'; deleteBtn.title = '进入删除模式'; deleteBtn.style.background = '#6c757d'; deleteBtn.style.color = 'white'; } } if (deleteBtnFloat) { if (isDeleteMode) { deleteBtnFloat.textContent = '✅ 退出删除'; deleteBtnFloat.style.background = '#dc3545'; deleteBtnFloat.style.color = 'white'; } else { deleteBtnFloat.textContent = '🗑️ 删除模式'; deleteBtnFloat.style.background = '#6c757d'; deleteBtnFloat.style.color = 'white'; } } } // 删除指定的方块 function deleteBlock(blockId) { // 找到要删除的方块索引 const blockIndex = finalBlocks.findIndex(block => block.id === blockId); if (blockIndex !== -1) { // 从finalBlocks中移除 finalBlocks.splice(blockIndex, 1); recombineBlocks(); renderTitleBlocks(finalBlocks); updateTitleFromBlocks(); updateErrorDetectionButtons(); // 如果删除后没有方块了,自动退出删除模式 if (finalBlocks.length === 0) { isDeleteMode = false; updateDeleteButtons(); } } } function updateTitleFromBlocks() { let title = buildTitleFromBlocks(finalBlocks); // 更新输入框 const compactInput = document.getElementById('titleInput'); const floatInput = document.getElementById('titleInputFloat'); if (compactInput) compactInput.value = title; if (floatInput) floatInput.value = title; return title; } // 渲染标题方块 function renderTitleBlocks(blocks) { // 渲染到紧凑模式 const container = document.getElementById('titleBlocks'); if (container) { renderBlocksToContainer(container, blocks); } // 渲染到悬浮模式 const floatContainer = document.getElementById('titleBlocksFloat'); if (floatContainer) { renderBlocksToContainer(floatContainer, blocks); } } // 在指定容器中渲染方块(使用改进的拖拽功能) function renderBlocksToContainer(container, blocks) { container.innerHTML = ''; // 为整个容器添加 dragover 和 drop 事件 container.addEventListener('dragover', handleDragOver); container.addEventListener('drop', handleDrop); // 应用错误检测 const blocksWithErrors = detectBlockErrors(blocks); blocksWithErrors.forEach((block, index) => { const blockEl = document.createElement('div'); blockEl.className = 'title-block'; blockEl.dataset.index = index; blockEl.dataset.blockId = block.id; blockEl.dataset.type = block.type; blockEl.dataset.typeName = ELEMENT_TYPES[block.type]?.name || '其他'; // 添加中文名称 blockEl.textContent = block.text; blockEl.draggable = !isDeleteMode; // 删除模式下禁用拖拽 // 根据方块类型设置样式 const typeInfo = ELEMENT_TYPES[block.type] || ELEMENT_TYPES.OTHER; let baseStyle = ` display: inline-flex; align-items: center; padding: 6px 10px; margin: 2px; background: ${typeInfo.color}; color: ${typeInfo.textColor}; border: 1px solid ${typeInfo.textColor}40; border-radius: 6px; font-size: 13px; cursor: ${isDeleteMode ? 'default' : 'grab'}; user-select: none; transition: all 0.2s ease; min-width: 20px; text-align: center; font-family: "Segoe UI", Arial, sans-serif; font-weight: 500; position: relative; `; // 如果有错误且错误检测开启,添加错误样式 if (block.renderError && isErrorDetectionEnabled && !isDeleteMode) { baseStyle += ` background-image: repeating-linear-gradient( 45deg, transparent, transparent 4px, rgba(220, 53, 69, 0.3) 4px, rgba(220, 53, 69, 0.3) 8px ); border: 2px solid #dc3545; box-shadow: 0 0 0 1px rgba(220, 53, 69, 0.2); `; } blockEl.style.cssText = baseStyle; // 添加类型标签(hover时显示) blockEl.title = `${typeInfo.name}: ${block.text}\n${isDeleteMode ? '点击右上角X删除' : '拖拽可移动位置'}`; // 删除模式下添加晃动效果 if (isDeleteMode) { blockEl.classList.add('delete-mode'); } // 删除模式下添加删除按钮 if (isDeleteMode) { const deleteBtn = document.createElement('div'); deleteBtn.className = 'block-delete-btn'; deleteBtn.innerHTML = '×'; deleteBtn.style.cssText = ` position: absolute; top: -8px; right: -8px; width: 18px; height: 18px; background: #dc3545; color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: bold; cursor: pointer; z-index: 1002; box-shadow: 0 2px 4px rgba(0,0,0,0.2); transition: all 0.2s ease; `; // 删除按钮悬停效果 deleteBtn.addEventListener('mouseenter', () => { deleteBtn.style.background = '#a71e2a'; deleteBtn.style.transform = 'scale(1.1)'; }); deleteBtn.addEventListener('mouseleave', () => { deleteBtn.style.background = '#dc3545'; deleteBtn.style.transform = 'scale(1)'; }); // 删除按钮点击事件 deleteBtn.addEventListener('click', (e) => { e.stopPropagation(); deleteBlock(block.id); }); blockEl.appendChild(deleteBtn); } else { // 非删除模式下的悬停效果 blockEl.addEventListener('mouseenter', () => { if (!dragState.isDragging) { blockEl.style.transform = 'translateY(-2px) scale(1.05)'; blockEl.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)'; blockEl.style.zIndex = '1000'; } }); blockEl.addEventListener('mouseleave', () => { if (!dragState.isDragging) { blockEl.style.transform = 'translateY(0) scale(1)'; blockEl.style.boxShadow = block.renderError && isErrorDetectionEnabled ? '0 0 0 1px rgba(220, 53, 69, 0.2)' : 'none'; blockEl.style.zIndex = 'auto'; } }); // 拖拽事件(只在非删除模式下) blockEl.addEventListener('dragstart', handleDragStart); blockEl.addEventListener('dragend', handleDragEnd); } container.appendChild(blockEl); }); // 更新动画样式 if (!document.getElementById('blockAnimationStyle')) { const style = document.createElement('style'); style.id = 'blockAnimationStyle'; style.textContent = ` .title-block { transition: all 0.2s ease; } .title-block.dragging { opacity: 0.4 !important; cursor: grabbing !important; } .title-block.delete-mode { animation: blockShake 0.5s ease-in-out infinite; } @keyframes blockShake { 0%, 100% { transform: rotate(0deg); } 25% { transform: rotate(-1deg); } 75% { transform: rotate(1deg); } } .drag-placeholder { transition: all 0.2s ease; animation: placeholderPulse 1s ease-in-out infinite; } @keyframes placeholderPulse { 0% { opacity: 0.6; } 50% { opacity: 1; } 100% { opacity: 0.6; } } #titleBlocks, #titleBlocksFloat { transition: all 0.2s ease; } .title-block::before { content: attr(data-type-name); position: absolute; top: -20px; left: 50%; transform: translateX(-50%); background: rgba(0,0,0,0.8); color: white; padding: 2px 6px; border-radius: 3px; font-size: 10px; white-space: nowrap; opacity: 0; pointer-events: none; transition: opacity 0.2s; z-index: 1001; } .title-block:hover::before { opacity: 1; } /* 删除模式下隐藏类型标签 */ .title-block.delete-mode::before { display: none !important; } .title-block.delete-mode:hover::before { display: none !important; } .block-delete-btn { transition: all 0.2s ease; } `; document.head.appendChild(style); } } // 切换方块编辑模式(紧凑模式) function toggleBlockMode() { const titleInput = document.getElementById('titleInput'); const titleBlocksContainer = document.getElementById('titleBlocksContainer'); const toggleBtn = document.getElementById('toggleBlockMode'); if (!titleInput || !titleBlocksContainer || !toggleBtn) return; isBlockMode = !isBlockMode; if (isBlockMode) { // 开启方块模式 - 显示方块区域 const currentTitle = titleInput.value; updateBlocksFromTitle(currentTitle); titleBlocksContainer.style.display = 'block'; toggleBtn.textContent = '📝'; toggleBtn.title = '关闭方块模式'; toggleBtn.style.background = '#28a745'; toggleBtn.style.color = 'white'; // 输入框变为只读提示 titleInput.style.background = '#f8f9fa'; titleInput.style.color = '#6c757d'; titleInput.style.fontStyle = 'italic'; } else { // 关闭方块模式 - 隐藏方块区域 titleBlocksContainer.style.display = 'none'; toggleBtn.textContent = '🧩'; toggleBtn.title = '开启方块模式'; toggleBtn.style.background = '#ffc107'; toggleBtn.style.color = '#212529'; // 恢复输入框正常状态 titleInput.style.background = '#ffffff'; titleInput.style.color = '#495057'; titleInput.style.fontStyle = 'normal'; } // 同步悬浮模式的方块状态 syncFloatBlockMode(); } // 切换方块编辑模式(悬浮模式) function toggleBlockModeFloat() { const titleInputFloat = document.getElementById('titleInputFloat'); const titleBlocksContainerFloat = document.getElementById('titleBlocksContainerFloat'); const toggleBtnFloat = document.getElementById('toggleBlockModeFloat'); if (!titleInputFloat || !titleBlocksContainerFloat || !toggleBtnFloat) return; isBlockMode = !isBlockMode; if (isBlockMode) { // 开启方块模式 const currentTitle = titleInputFloat.value; updateBlocksFromTitle(currentTitle); titleBlocksContainerFloat.style.display = 'block'; toggleBtnFloat.textContent = '📝 文本模式'; toggleBtnFloat.style.background = '#28a745'; toggleBtnFloat.style.color = 'white'; // 输入框变为只读提示 titleInputFloat.style.background = '#f8f9fa'; titleInputFloat.style.color = '#6c757d'; titleInputFloat.style.fontStyle = 'italic'; } else { // 关闭方块模式 titleBlocksContainerFloat.style.display = 'none'; toggleBtnFloat.textContent = '🧩 方块模式'; toggleBtnFloat.style.background = '#ffc107'; toggleBtnFloat.style.color = '#212529'; // 恢复输入框正常状态 titleInputFloat.style.background = '#ffffff'; titleInputFloat.style.color = '#495057'; titleInputFloat.style.fontStyle = 'normal'; } // 同步紧凑模式的方块状态 syncCompactBlockMode(); } // 同步悬浮模式的方块状态 function syncFloatBlockMode() { const titleBlocksContainerFloat = document.getElementById('titleBlocksContainerFloat'); const toggleBtnFloat = document.getElementById('toggleBlockModeFloat'); if (titleBlocksContainerFloat && toggleBtnFloat) { if (isBlockMode) { titleBlocksContainerFloat.style.display = 'block'; toggleBtnFloat.textContent = '📝 文本模式'; toggleBtnFloat.style.background = '#28a745'; toggleBtnFloat.style.color = 'white'; } else { titleBlocksContainerFloat.style.display = 'none'; toggleBtnFloat.textContent = '🧩 方块模式'; toggleBtnFloat.style.background = '#ffc107'; toggleBtnFloat.style.color = '#212529'; } } } // 同步紧凑模式的方块状态 function syncCompactBlockMode() { const titleBlocksContainer = document.getElementById('titleBlocksContainer'); const toggleBtn = document.getElementById('toggleBlockMode'); if (titleBlocksContainer && toggleBtn) { if (isBlockMode) { titleBlocksContainer.style.display = 'block'; toggleBtn.textContent = '📝'; toggleBtn.style.background = '#28a745'; toggleBtn.style.color = 'white'; } else { titleBlocksContainer.style.display = 'none'; toggleBtn.textContent = '🧩'; toggleBtn.style.background = '#ffc107'; toggleBtn.style.color = '#212529'; } } } // 通用的从方块数组生成标题文本的函数 function buildTitleFromBlocks(blocks) { let title = ''; for (let i = 0; i < blocks.length; i++) { const block = blocks[i]; if (i === 0) { title += block.text; } else { if (block.text.startsWith('-') && i == blocks.length - 1) { title += block.text; } else { title += ' ' + block.text; } } } return title; } // 从方块重新组合标题(使用通用函数) function rebuildTitleFromBlocks() { return buildTitleFromBlocks(finalBlocks); } // 重组:拆解最终方块为原始方块,然后重新组合 function recombineBlocks() { rawBlocks = []; finalBlocks.forEach(finalBlock => { rawBlocks.push(...finalBlock.rawBlocks); }); finalBlocks = combineRawBlocks(rawBlocks); } // 从当前URL解析种子ID function getTorrentId() { const url = window.location.href; const match = url.match(/[?&]id=(\d+)/); return match ? match[1] : null; } // 获取edit页面URL function getEditUrl() { const id = getTorrentId(); if (!id) return null; const baseUrl = window.location.origin + window.location.pathname.replace('details.php', 'edit.php'); return `${baseUrl}?id=${id}`; } // 从edit页面获取标题 async function fetchTitleFromEdit() { const editUrl = getEditUrl(); if (!editUrl) { console.warn('无法构造edit页面URL'); return null; } try { const response = await fetch(editUrl, { method: 'GET', credentials: 'same-origin', // 包含cookies headers: { 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', } }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const html = await response.text(); const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); // 尝试多种可能的标题字段选择器 const selectors = [ 'input[name="name"]', 'input[name="title"]', 'textarea[name="name"]', 'textarea[name="title"]', '#name', '#title' ]; for (let selector of selectors) { const element = doc.querySelector(selector); if (element && element.value) { return element.value.trim(); } } throw new Error('未找到标题字段'); } catch (error) { console.error('获取edit页面标题失败:', error); return null; } } // 从详情页重新抓取标题并更新当前页面显示(保持HTML结构) async function refreshPageTitle() { try { const currentUrl = window.location.href; const response = await fetch(currentUrl, { method: 'GET', credentials: 'same-origin', headers: { 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', } }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const html = await response.text(); const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); // 查找新页面中的标题元素 const newTitleElement = doc.querySelector('h1') || doc.querySelector('.title') || doc.querySelector('#title') || doc.querySelector('h2'); if (newTitleElement) { // 更新当前页面的标题元素 - 使用 innerHTML 保持HTML结构 const currentTitleElement = getTitleElement(); if (currentTitleElement) { // 使用 innerHTML 而不是 textContent,这样可以保持HTML结构和样式 currentTitleElement.innerHTML = newTitleElement.innerHTML; // 更新页面标题(浏览器标签) - 这里用纯文本 document.title = newTitleElement.textContent.trim(); } // 更新输入框内容 - 这里用纯文本 const newTitleText = newTitleElement.textContent.trim(); const compactInput = document.getElementById('titleInput'); const floatInput = document.getElementById('titleInputFloat'); if (compactInput) compactInput.value = newTitleText; if (floatInput) floatInput.value = newTitleText; // 如果当前是方块模式,重新生成方块 if (isBlockMode) { updateBlocksFromTitle(newTitleText); } return newTitleText; } else { throw new Error('未能从新页面中找到标题'); } } catch (error) { console.error('刷新页面标题失败:', error); throw error; } } async function saveTitleToEdit(newTitle) { const editUrl = getEditUrl(); if (!editUrl) { throw new Error('无法构造edit页面URL'); } try { // 首先获取edit页面以获取表单数据和CSRF token等 const getResponse = await fetch(editUrl, { method: 'GET', credentials: 'same-origin', }); if (!getResponse.ok) { throw new Error(`无法访问edit页面: ${getResponse.status}`); } const html = await getResponse.text(); const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); // 查找表单 const form = doc.querySelector('form[name="edittorrent"]'); if (!form) { throw new Error('未找到编辑表单'); } // 获取表单的action属性,应该是takeedit.php const formAction = form.getAttribute('action'); const submitUrl = new URL(formAction, window.location.origin + window.location.pathname.replace('details.php', '')).href; // 构造表单数据 const formData = new FormData(); // 复制所有现有的表单字段 const inputs = form.querySelectorAll('input, textarea, select'); inputs.forEach(input => { if (input.name && input.type !== 'file') { if (input.name === 'name') { // 使用新标题 formData.append(input.name, newTitle); } else if (input.type === 'checkbox' || input.type === 'radio') { if (input.checked) { formData.append(input.name, input.value); } } else if (input.type === 'submit' || input.type === 'reset' || input.type === 'button') { // 跳过按钮类型的input return; } else { formData.append(input.name, input.value || ''); } } }); // 特别处理复选框数组(如tags[]) const checkboxArrays = {}; form.querySelectorAll('input[type="checkbox"]:checked').forEach(checkbox => { if (checkbox.name.endsWith('[]')) { if (!checkboxArrays[checkbox.name]) { checkboxArrays[checkbox.name] = []; } checkboxArrays[checkbox.name].push(checkbox.value); } }); // 将复选框数组添加到FormData中 Object.keys(checkboxArrays).forEach(name => { // 先删除之前可能添加的同名字段 formData.delete(name); checkboxArrays[name].forEach(value => { formData.append(name, value); }); }); console.log('提交到URL:', submitUrl); console.log('表单数据:', Array.from(formData.entries())); // 提交表单到正确的action URL (takeedit.php) const postResponse = await fetch(submitUrl, { method: 'POST', credentials: 'same-origin', body: formData }); if (!postResponse.ok) { throw new Error(`保存失败: ${postResponse.status}`); } return true; } catch (error) { console.error('保存标题到edit页面失败:', error); throw error; } } // 创建编辑框容器 function createEditBox() { const editBox = document.createElement('div'); editBox.id = 'titleEditBox'; editBox.innerHTML = ` <!-- 紧凑模式(标题下方) --> <div id="compactMode" style=" margin: 15px 0; padding: 10px; background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 6px; font-family: Arial, sans-serif; max-width: 800px; "> <!-- 标题方块区域 --> <div id="titleBlocksContainer" style=" margin-bottom: 10px; display: none; "> <div style=" display: flex; align-items: center; gap: 10px; "> <div id="titleBlocks" style=" flex: 1; min-height: 40px; border: 1px solid #ced4da; border-radius: 4px; padding: 8px; background: #fff; display: flex; flex-wrap: wrap; gap: 4px; align-items: center; align-content: center; "></div> <!-- 方块操作按钮区域(右侧) --> <div style=" display: flex; flex-direction: column; gap: 4px; "> <button id="toggleErrorDetection" style=" background: #dc3545; color: white; border: none; padding: 4px 8px; border-radius: 4px; cursor: pointer; font-size: 12px; transition: background 0.2s; font-weight: bold; white-space: nowrap; line-height: 1.2; " title="隐藏错误提示">⚠️</button> <button id="toggleDeleteMode" style=" background: #6c757d; color: white; border: none; padding: 4px 8px; border-radius: 4px; cursor: pointer; font-size: 12px; transition: background 0.2s; font-weight: bold; white-space: nowrap; line-height: 1.2; " title="进入删除模式">🗑️</button> </div> </div> </div> <!-- 输入框和按钮行 --> <div style=" display: flex; align-items: center; gap: 10px; "> <input type="text" id="titleInput" placeholder="编辑标题..." style=" flex: 1; border: 1px solid #ced4da; border-radius: 4px; padding: 8px 12px; font-size: 14px; outline: none; transition: border-color 0.2s; " /> <button id="saveTitle" style=" background: #28a745; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; font-size: 14px; white-space: nowrap; transition: background 0.2s; ">保存</button> <button id="loadTitle" style=" background: #17a2b8; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; font-size: 14px; white-space: nowrap; transition: background 0.2s; ">载入</button> <button id="toggleBlockMode" style=" background: #ffc107; color: #212529; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; font-size: 14px; white-space: nowrap; transition: background 0.2s; font-weight: bold; " title="开启/关闭方块模式">🧩</button> <button id="switchMode" style=" background: #6c757d; color: white; border: none; padding: 8px 10px; border-radius: 4px; cursor: pointer; font-size: 12px; transition: background 0.2s; " title="切换到右侧悬浮模式">→</button> </div> </div> <!-- 悬浮模式(右侧) --> <div id="floatingMode" style=" position: fixed !important; top: 120px !important; right: 20px !important; width: 350px; background: #ffffff; border: 2px solid #28a745; border-radius: 8px; padding: 15px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 99999 !important; font-family: Arial, sans-serif; display: none; "> <div style=" display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; border-bottom: 1px solid #eee; padding-bottom: 8px; "> <h3 style="margin: 0; color: #333; font-size: 16px;">标题编辑器</h3> <button id="switchModeFloat" style=" background: #6c757d; color: white; border: none; padding: 4px 8px; border-radius: 4px; cursor: pointer; font-size: 12px; transition: background 0.2s; " title="收起为图标">●</button> </div> <!-- 悬浮模式的方块区域 --> <div id="titleBlocksContainerFloat" style=" margin-bottom: 10px; display: none; "> <div id="titleBlocksFloat" style=" min-height: 45px; border: 1px solid #ced4da; border-radius: 4px; padding: 8px; background: #fff; display: flex; flex-wrap: wrap; gap: 4px; align-items: flex-start; align-content: flex-start; margin-bottom: 8px; "></div> <!-- 方块操作按钮区域 --> <div style=" text-align: right; margin-bottom: 8px; display: flex; gap: 8px; justify-content: flex-end; "> <button id="toggleErrorDetectionFloat" style=" background: #fd7e14; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 12px; transition: background 0.2s; font-weight: bold; " title="隐藏错误提示">🙈 隐藏错误</button> <button id="toggleDeleteModeFloat" style=" background: #6c757d; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 12px; transition: background 0.2s; font-weight: bold; " title="进入删除模式">🗑️ 删除模式</button> </div> </div> <!-- 文本框 --> <textarea id="titleInputFloat" placeholder="在此输入或编辑标题..." style=" width: 100%; height: 80px; border: 1px solid #ddd; border-radius: 4px; padding: 8px; font-size: 14px; resize: vertical; box-sizing: border-box; margin-bottom: 10px; outline: none; "></textarea> <!-- 按钮(垂直布局) --> <div style="display: flex; flex-direction: column; gap: 8px;"> <div style="display: flex; gap: 10px;"> <button id="saveTitleFloat" style=" flex: 1; background: #28a745; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; font-size: 14px; transition: background 0.3s; ">保存标题</button> <button id="loadTitleFloat" style=" flex: 1; background: #17a2b8; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; font-size: 14px; transition: background 0.3s; ">载入标题</button> </div> <button id="toggleBlockModeFloat" style=" background: #ffc107; color: #212529; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; font-size: 14px; transition: background 0.2s; font-weight: bold; " title="开启/关闭方块模式">🧩 方块模式</button> </div> <div id="statusMessage" style=" margin-top: 10px; padding: 5px; border-radius: 3px; font-size: 12px; text-align: center; display: none; "></div> </div> <!-- 图标模式 --> <div id="iconMode" style=" position: fixed !important; top: 120px !important; right: 20px !important; width: 50px; height: 50px; background: #28a745; border-radius: 50%; display: none; align-items: center; justify-content: center; cursor: pointer; box-shadow: 0 2px 8px rgba(0,0,0,0.2); z-index: 99999 !important; transition: all 0.3s; " title="展开标题编辑器"> <span style="color: white; font-size: 20px; font-weight: bold;">✏️</span> </div> `; return editBox; } // 显示状态消息(仅悬浮模式使用) function showStatus(message, type = 'success') { const statusDiv = document.getElementById('statusMessage'); if (!statusDiv) return; statusDiv.style.display = 'block'; statusDiv.textContent = message; if (type === 'success') { statusDiv.style.background = '#d4edda'; statusDiv.style.color = '#155724'; statusDiv.style.border = '1px solid #c3e6cb'; } else if (type === 'error') { statusDiv.style.background = '#f8d7da'; statusDiv.style.color = '#721c24'; statusDiv.style.border = '1px solid #f5c6cb'; } setTimeout(() => { statusDiv.style.display = 'none'; }, 3000); } // 获取页面标题元素 function getTitleElement() { const selectors = [ 'h1', '.title', '#title', 'h2', '.torrent-title', '.detail-title' ]; for (let selector of selectors) { const element = document.querySelector(selector); if (element && element.textContent.trim()) { return element; } } return null; } // 载入当前页面标题 async function loadCurrentTitle() { // 重置状态 setEditStatus(true, '正在载入...'); try { const titleValue = await fetchTitleFromEdit(); if (titleValue) { // 成功获取标题 const compactInput = document.getElementById('titleInput'); const floatInput = document.getElementById('titleInputFloat'); if (compactInput) compactInput.value = titleValue; if (floatInput) floatInput.value = titleValue; // 如果当前是方块模式,重新生成方块 if (isBlockMode) { updateBlocksFromTitle(titleValue); } setEditStatus(true, '载入成功'); canEdit = true; if (displayMode === 1) { showStatus('已从edit页面载入标题'); } } else { // 无法获取标题 setEditStatus(false, '无法从edit页面读取标题'); canEdit = false; if (displayMode === 1) { showStatus('无法访问edit页面', 'error'); } } } catch (error) { setEditStatus(false, `载入失败: ${error.message}`); canEdit = false; if (displayMode === 1) { showStatus('载入失败', 'error'); } } } // 保存标题 - 智能更新版本:保存后智能更新页面标题,保持HTML结构 async function saveTitle() { if (!canEdit) { if (displayMode === 1) { showStatus('无法保存:edit页面不可访问', 'error'); } return; } let newTitle = ''; // 根据当前模式获取标题 if (displayMode === 0) { if (isBlockMode) { newTitle = rebuildTitleFromBlocks(); } else { newTitle = document.getElementById('titleInput').value.trim(); } } else if (displayMode === 1) { if (isBlockMode) { newTitle = rebuildTitleFromBlocks(); } else { newTitle = document.getElementById('titleInputFloat').value.trim(); } } if (!newTitle) { if (displayMode === 1) { showStatus('标题不能为空', 'error'); } return; } // 保存用户编辑的内容,用于保存后恢复 const userEditedTitle = newTitle; // 获取页面标题元素,准备添加视觉效果 const titleElement = getTitleElement(); let originalStyle = null; // 显示保存中状态 setEditStatus(true, '正在保存...'); if (displayMode === 1) { showStatus('正在保存...'); } try { // 添加半透明效果表示标题正在更新 if (titleElement) { originalStyle = { opacity: titleElement.style.opacity, transition: titleElement.style.transition }; titleElement.style.transition = 'all 0.3s ease'; titleElement.style.opacity = '0.5'; } // 保存到edit页面 await saveTitleToEdit(newTitle); // 保存成功,智能更新页面标题(保持HTML结构) setEditStatus(true, '正在更新显示...'); if (displayMode === 1) { showStatus('保存成功,正在更新页面标题...'); } // 从详情页重新抓取标题并更新当前页面显示(保持HTML结构) const refreshedTitle = await refreshPageTitle(); // 恢复正常显示 if (titleElement && originalStyle) { // 恢复透明度 titleElement.style.opacity = '1'; // 添加短暂的绿色光晕表示更新成功 titleElement.style.boxShadow = '0 0 8px rgba(40, 167, 69, 0.5)'; setTimeout(() => { // 恢复原始样式 titleElement.style.opacity = originalStyle.opacity; titleElement.style.transition = originalStyle.transition; titleElement.style.boxShadow = ''; }, 1000); } // 保持编辑器中的内容为用户编辑的内容(不变) const compactInput = document.getElementById('titleInput'); const floatInput = document.getElementById('titleInputFloat'); if (compactInput) compactInput.value = userEditedTitle; if (floatInput) floatInput.value = userEditedTitle; // 如果当前是方块模式,确保方块显示用户编辑的内容 if (isBlockMode) { updateBlocksFromTitle(userEditedTitle); } setEditStatus(true, '保存成功'); if (displayMode === 1) { showStatus('标题已保存并更新页面显示!'); } console.log('标题保存成功,页面标题已更新为:', refreshedTitle); console.log('编辑器保持显示用户内容:', userEditedTitle); } catch (error) { // 发生错误时,恢复标题样式 if (titleElement && originalStyle) { titleElement.style.opacity = originalStyle.opacity; titleElement.style.transition = originalStyle.transition; // 添加红色光晕表示保存失败 titleElement.style.boxShadow = '0 0 8px rgba(220, 53, 69, 0.5)'; setTimeout(() => { titleElement.style.boxShadow = ''; }, 2000); } setEditStatus(false, `保存失败: ${error.message}`); if (displayMode === 1) { showStatus(`保存失败: ${error.message}`, 'error'); } } } // 设置编辑状态(可用/不可用) function setEditStatus(canEditFlag, statusText) { canEdit = canEditFlag; // 更新按钮状态 const saveBtn = document.getElementById('saveTitle'); const saveBtnFloat = document.getElementById('saveTitleFloat'); const loadBtn = document.getElementById('loadTitle'); const loadBtnFloat = document.getElementById('loadTitleFloat'); const blockModeBtn = document.getElementById('toggleBlockMode'); const blockModeBtnFloat = document.getElementById('toggleBlockModeFloat'); // 更新输入框状态 const compactInput = document.getElementById('titleInput'); const floatInput = document.getElementById('titleInputFloat'); const titleBlocksContainer = document.getElementById('titleBlocksContainer'); const titleBlocksContainerFloat = document.getElementById('titleBlocksContainerFloat'); if (canEditFlag) { // 可编辑状态 - 绿色 if (saveBtn) { saveBtn.disabled = false; saveBtn.style.background = '#28a745'; saveBtn.style.opacity = '1'; saveBtn.style.cursor = 'pointer'; } if (saveBtnFloat) { saveBtnFloat.disabled = false; saveBtnFloat.style.background = '#28a745'; saveBtnFloat.style.opacity = '1'; saveBtnFloat.style.cursor = 'pointer'; } if (compactInput) { compactInput.style.borderColor = '#28a745'; if (!isBlockMode) { compactInput.style.background = '#ffffff'; } compactInput.disabled = false; } if (floatInput) { floatInput.style.borderColor = '#28a745'; if (!isBlockMode) { floatInput.style.background = '#ffffff'; } floatInput.disabled = false; } if (titleBlocksContainer) { titleBlocksContainer.querySelector('#titleBlocks').style.borderColor = '#28a745'; titleBlocksContainer.querySelector('#titleBlocks').style.background = '#ffffff'; } if (titleBlocksContainerFloat) { titleBlocksContainerFloat.querySelector('#titleBlocksFloat').style.borderColor = '#28a745'; titleBlocksContainerFloat.querySelector('#titleBlocksFloat').style.background = '#ffffff'; } if (blockModeBtn) { blockModeBtn.disabled = false; blockModeBtn.style.opacity = '1'; blockModeBtn.style.cursor = 'pointer'; } if (blockModeBtnFloat) { blockModeBtnFloat.disabled = false; blockModeBtnFloat.style.opacity = '1'; blockModeBtnFloat.style.cursor = 'pointer'; } } else { // 不可编辑状态 - 红色 if (saveBtn) { saveBtn.disabled = true; saveBtn.style.background = '#dc3545'; saveBtn.style.opacity = '0.6'; saveBtn.style.cursor = 'not-allowed'; } if (saveBtnFloat) { saveBtnFloat.disabled = true; saveBtnFloat.style.background = '#dc3545'; saveBtnFloat.style.opacity = '0.6'; saveBtnFloat.style.cursor = 'not-allowed'; } if (compactInput) { compactInput.style.borderColor = '#dc3545'; compactInput.style.background = '#f8f9fa'; compactInput.placeholder = statusText || '无法编辑'; } if (floatInput) { floatInput.style.borderColor = '#dc3545'; floatInput.style.background = '#f8f9fa'; floatInput.placeholder = statusText || '无法编辑'; } if (titleBlocksContainer) { titleBlocksContainer.querySelector('#titleBlocks').style.borderColor = '#dc3545'; titleBlocksContainer.querySelector('#titleBlocks').style.background = '#f8f9fa'; } if (titleBlocksContainerFloat) { titleBlocksContainerFloat.querySelector('#titleBlocksFloat').style.borderColor = '#dc3545'; titleBlocksContainerFloat.querySelector('#titleBlocksFloat').style.background = '#f8f9fa'; } if (blockModeBtn) { blockModeBtn.disabled = true; blockModeBtn.style.opacity = '0.6'; blockModeBtn.style.cursor = 'not-allowed'; } if (blockModeBtnFloat) { blockModeBtnFloat.disabled = true; blockModeBtnFloat.style.opacity = '0.6'; blockModeBtnFloat.style.cursor = 'not-allowed'; } } // 载入按钮始终可用 if (loadBtn) { loadBtn.disabled = false; loadBtn.style.opacity = '1'; } if (loadBtnFloat) { loadBtnFloat.disabled = false; loadBtnFloat.style.opacity = '1'; } } function switchMode() { const compactMode = document.getElementById('compactMode'); const floatingMode = document.getElementById('floatingMode'); const iconMode = document.getElementById('iconMode'); const switchBtn = document.getElementById('switchMode'); const switchBtnFloat = document.getElementById('switchModeFloat'); const editBox = document.getElementById('titleEditBox'); displayMode = (displayMode + 1) % 3; // 隐藏所有模式 compactMode.style.display = 'none'; floatingMode.style.display = 'none'; iconMode.style.display = 'none'; switch (displayMode) { case 0: // 紧凑模式(标题下方) // 确保编辑框在标题后面 const titleElement = getTitleElement(); if (titleElement && editBox.parentNode !== titleElement.parentNode) { titleElement.parentNode.insertBefore(editBox, titleElement.nextSibling); } compactMode.style.display = 'block'; // 重置CSS样式 - 这是修复的关键 editBox.style.position = 'static'; editBox.style.top = 'auto'; editBox.style.right = 'auto'; editBox.style.zIndex = 'auto'; switchBtn.innerHTML = '→'; switchBtn.title = '切换到右侧悬浮模式'; break; case 1: // 悬浮模式(右侧) // 修复:确保编辑框在body中且保持悬浮位置 if (editBox.parentNode !== document.body) { document.body.appendChild(editBox); } floatingMode.style.display = 'block'; // 修复:显式设置悬浮位置 editBox.style.position = 'fixed'; editBox.style.top = '120px'; editBox.style.right = '20px'; editBox.style.zIndex = '99999'; switchBtnFloat.innerHTML = '●'; switchBtnFloat.title = '收起为图标'; // 同步标题内容 syncTitleInputs(); break; case 2: // 图标模式 // 修复:确保编辑框在body中且保持悬浮位置 if (editBox.parentNode !== document.body) { document.body.appendChild(editBox); } iconMode.style.display = 'flex'; // 修复:显式设置悬浮位置 editBox.style.position = 'fixed'; editBox.style.top = '120px'; editBox.style.right = '20px'; editBox.style.zIndex = '99999'; break; } } // 同步两个输入框的内容 function syncTitleInputs() { const compactInput = document.getElementById('titleInput'); const floatInput = document.getElementById('titleInputFloat'); if (compactInput && floatInput) { if (displayMode === 0) { let titleValue; if (isBlockMode) { titleValue = rebuildTitleFromBlocks(); } else { titleValue = compactInput.value; } floatInput.value = titleValue; } else if (displayMode === 1) { let titleValue; if (isBlockMode) { titleValue = rebuildTitleFromBlocks(); } else { titleValue = floatInput.value; } compactInput.value = titleValue; // 如果紧凑模式当前是方块模式,也需要更新方块 if (isBlockMode) { updateBlocksFromTitle(titleValue); } } } } // 检查是否是种子列表页面 function isOnTorrentListPage() { return window.location.pathname.includes('torrents.php'); } // 从URL解析当前页面的种子ID function getTorrentIdFromUrl(url) { const match = url.match(/\bdetails.php\?id=(\d+)/); return match ? match[1] : null; } // 初始化种子列表页面功能 function initTorrentListPage() { console.log('种子标题快修:初始化种子列表页面功能'); // 更精确地寻找种子标题链接 - 先找详情页链接,再过滤 const torrentLinks = document.querySelectorAll('table a[href*="details.php?id="], .torrent-list a[href*="details.php?id="]'); torrentLinks.forEach((link, index) => { const href = link.getAttribute('href'); const torrentId = getTorrentIdFromUrl(href); // 只对真正的种子标题链接添加编辑按钮 if (torrentId && isValidTorrentTitleLink(link)) { console.log(`找到种子标题链接 ${index + 1}: ${link.textContent.trim()}`); addQuickEditButton(link, torrentId, index); } }); } // 判断是否是有效的种子标题链接 function isValidTorrentTitleLink(link) { const href = link.getAttribute('href'); return href && href.match(/\bdetails.php\?id=\d+/) && !href.includes('dllist') && !href.includes('leechers') && !href.includes('seeders') && !link.querySelector('img'); } // 为种子标题添加快捷编辑按钮 function addQuickEditButton(titleLink, torrentId, index) { // 创建编辑按钮 const editBtn = document.createElement('span'); editBtn.className = 'torrent-quick-edit-btn'; editBtn.innerHTML = '✏️'; editBtn.title = '快捷编辑标题'; editBtn.style.cssText = ` display: inline-block; margin-left: 6px; padding: 2px 4px; background: #ffc107; color: #212529; border-radius: 3px; cursor: pointer; font-size: 12px; vertical-align: middle; transition: all 0.2s ease; user-select: none; `; // 悬停效果 editBtn.addEventListener('mouseenter', () => { editBtn.style.background = '#e0a800'; editBtn.style.transform = 'scale(1.1)'; }); editBtn.addEventListener('mouseleave', () => { editBtn.style.background = '#ffc107'; editBtn.style.transform = 'scale(1)'; }); // 点击事件 editBtn.addEventListener('click', (e) => { e.stopPropagation(); e.preventDefault(); toggleQuickEditor(torrentId, titleLink, editBtn); }); // 将按钮添加到标题后面 titleLink.parentNode.insertBefore(editBtn, titleLink.nextSibling); } // 切换快捷编辑器显示状态 function toggleQuickEditor(torrentId, titleLink, editBtn) { const editorId = `quick-editor-${torrentId}`; // 如果有其他活跃的编辑器,先关闭它们 if (activeListEditor && activeListEditor !== editorId) { closeQuickEditor(activeListEditor); } const existingEditor = document.getElementById(editorId); if (existingEditor) { // 编辑器已存在,关闭它 closeQuickEditor(editorId); } else { // 创建新的编辑器 createQuickEditor(torrentId, titleLink, editBtn, editorId); activeListEditor = editorId; } } // 关闭快捷编辑器 function closeQuickEditor(editorId) { const editor = document.getElementById(editorId); if (editor) { editor.remove(); } if (activeListEditor === editorId) { activeListEditor = null; } } // 创建快捷编辑器 function createQuickEditor(torrentId, titleLink, editBtn, editorId) { const titleText = titleLink.textContent.trim(); // 获取当前行的位置用于计算初始位置 const currentRow = titleLink.closest('tr') || titleLink.closest('.torrent-item') || titleLink.parentElement; const rowRect = currentRow.getBoundingClientRect(); // 计算固定位置(相对于页面,不跟随滚动) const scrollTop = window.pageYOffset || document.documentElement.scrollTop; const fixedTop = rowRect.bottom + scrollTop + 5; // 转换为绝对位置 // 创建编辑器容器 const editor = document.createElement('div'); editor.id = editorId; editor.className = 'quick-title-editor'; editor.style.cssText = ` position: absolute; top: ${fixedTop}px; left: 200px; width: calc(100vw - 400px); max-width: 800px; background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 6px; padding: 10px; box-shadow: 0 8px 25px rgba(0,0,0,0.2); z-index: 999999; font-family: Arial, sans-serif; `; // 确保编辑器不会超出屏幕右侧 if (window.innerWidth < 1000) { editor.style.left = '100px'; editor.style.width = 'calc(100vw - 200px)'; } editor.innerHTML = ` <!-- 方块区域容器 --> <div class="quick-blocks-container" style=" margin-bottom: 10px; display: block; "> <div style=" display: flex; align-items: center; gap: 10px; "> <div class="quick-title-blocks" style=" flex: 1; min-height: 40px; border: 1px solid #ced4da; border-radius: 4px; padding: 8px; background: #fff; display: flex; flex-wrap: wrap; gap: 4px; align-items: center; align-content: center; "></div> <!-- 方块操作按钮区域(右侧) --> <div style=" display: flex; flex-direction: column; gap: 4px; "> <button class="quick-toggle-error" style=" background: #dc3545; color: white; border: none; padding: 4px 8px; border-radius: 4px; cursor: pointer; font-size: 12px; transition: background 0.2s; font-weight: bold; white-space: nowrap; line-height: 1.2; " title="显示错误提示">⚠️</button> <button class="quick-toggle-delete" style=" background: #6c757d; color: white; border: none; padding: 4px 8px; border-radius: 4px; cursor: pointer; font-size: 12px; transition: background 0.2s; font-weight: bold; white-space: nowrap; line-height: 1.2; " title="进入删除模式">🗑️</button> </div> </div> </div> <!-- 输入框和按钮行 --> <div style=" display: flex; align-items: center; gap: 10px; "> <input type="text" class="quick-title-input" placeholder="编辑标题..." value="${titleText}" style=" flex: 1; border: 1px solid #ced4da; border-radius: 4px; padding: 8px 12px; font-size: 14px; outline: none; transition: border-color 0.2s; " /> <button class="quick-save-btn" style=" background: #28a745; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; font-size: 14px; white-space: nowrap; transition: background 0.2s; ">保存</button> <button class="quick-load-btn" style=" background: #17a2b8; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; font-size: 14px; white-space: nowrap; transition: background 0.2s; ">载入</button> <button class="quick-toggle-blocks" style=" background: #ffc107; color: #212529; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; font-size: 14px; white-space: nowrap; transition: background 0.2s; font-weight: bold; background: #28a745; color: white; " title="关闭方块模式">📝</button> </div> <div class="quick-status-message" style=" margin-top: 10px; padding: 5px; border-radius: 3px; font-size: 12px; text-align: center; display: none; "></div> `; document.body.appendChild(editor); // 绑定事件 setupQuickEditorEvents(editor, torrentId, titleLink); // 初始化状态和自动加载方块 setTimeout(() => { const toggleErrorBtn = editor.querySelector('.quick-toggle-error'); const titleInput = editor.querySelector('.quick-title-input'); const titleBlocks = editor.querySelector('.quick-title-blocks'); if (toggleErrorBtn) { // 检查是否有错误,设置合适的初始状态 const editorStateTemp = { isBlockMode: true, isDeleteMode: false, isErrorDetectionEnabled: true, rawBlocks: splitTitleIntoRawBlocks(titleInput.value), finalBlocks: [] }; editorStateTemp.finalBlocks = combineRawBlocks(editorStateTemp.rawBlocks); const hasErrors = editorStateTemp.finalBlocks.some(block => detectBlockErrors(editorStateTemp.finalBlocks).find(b => b.id === block.id)?.renderError ); if (!hasErrors) { // 无错误时:绿色背景,禁用 toggleErrorBtn.textContent = '✓'; toggleErrorBtn.title = '未检测到错误'; toggleErrorBtn.style.background = '#28a745'; toggleErrorBtn.style.color = 'white'; toggleErrorBtn.disabled = true; toggleErrorBtn.style.opacity = '0.6'; toggleErrorBtn.style.cursor = 'not-allowed'; } else { // 有错误时:显示错误检测状态 toggleErrorBtn.textContent = '⚠️'; toggleErrorBtn.title = '显示错误提示'; toggleErrorBtn.style.background = '#dc3545'; toggleErrorBtn.style.color = 'white'; toggleErrorBtn.disabled = false; toggleErrorBtn.style.opacity = '1'; toggleErrorBtn.style.cursor = 'pointer'; } } // 初始化时自动生成方块(因为默认开启方块模式) if (titleInput && titleBlocks) { const editorStateTemp = { isBlockMode: true, isDeleteMode: false, isErrorDetectionEnabled: true, rawBlocks: [], finalBlocks: [] }; updateQuickBlocks(titleInput.value, editorStateTemp, titleBlocks); } }, 0); } // 设置快捷编辑器事件 function setupQuickEditorEvents(editor, torrentId, titleLink) { const saveBtn = editor.querySelector('.quick-save-btn'); const loadBtn = editor.querySelector('.quick-load-btn'); const toggleBlocksBtn = editor.querySelector('.quick-toggle-blocks'); const titleInput = editor.querySelector('.quick-title-input'); const blocksContainer = editor.querySelector('.quick-blocks-container'); const titleBlocks = editor.querySelector('.quick-title-blocks'); const toggleErrorBtn = editor.querySelector('.quick-toggle-error'); const toggleDeleteBtn = editor.querySelector('.quick-toggle-delete'); const statusDiv = editor.querySelector('.quick-status-message'); let editorState = { isBlockMode: true, isDeleteMode: false, isErrorDetectionEnabled: true, rawBlocks: [], finalBlocks: [] }; // 保存按钮 saveBtn.addEventListener('click', async () => { await quickSaveTitle(torrentId, titleInput.value.trim(), statusDiv); }); // 载入按钮 loadBtn.addEventListener('click', async () => { await quickLoadTitle(torrentId, titleInput, statusDiv); }); // 方块模式切换 toggleBlocksBtn.addEventListener('click', () => { editorState.isBlockMode = !editorState.isBlockMode; if (editorState.isBlockMode) { blocksContainer.style.display = 'block'; toggleBlocksBtn.textContent = '📝'; toggleBlocksBtn.title = '关闭方块模式'; toggleBlocksBtn.style.background = '#28a745'; toggleBlocksBtn.style.color = 'white'; // 更新方块 updateQuickBlocks(titleInput.value, editorState, titleBlocks); } else { blocksContainer.style.display = 'none'; toggleBlocksBtn.textContent = '🧩'; toggleBlocksBtn.title = '开启方块模式'; toggleBlocksBtn.style.background = '#ffc107'; toggleBlocksBtn.style.color = '#212529'; } }); // 文本输入实时更新方块 titleInput.addEventListener('input', () => { if (editorState.isBlockMode) { updateQuickBlocks(titleInput.value, editorState, titleBlocks); } }); // 错误检测切换 toggleErrorBtn.addEventListener('click', () => { // 重新检测当前是否有错误 const blocksWithErrors = detectBlockErrors(editorState.finalBlocks); const hasErrors = blocksWithErrors.some(block => block.renderError); if (!hasErrors) { // 如果没有错误,不允许切换 return; } // 有错误时才允许切换 editorState.isErrorDetectionEnabled = !editorState.isErrorDetectionEnabled; if (editorState.isErrorDetectionEnabled) { toggleErrorBtn.textContent = '🙈'; toggleErrorBtn.title = '隐藏错误提示'; toggleErrorBtn.style.background = '#fd7e14'; toggleErrorBtn.style.color = 'white'; toggleErrorBtn.disabled = false; toggleErrorBtn.style.opacity = '1'; toggleErrorBtn.style.cursor = 'pointer'; } else { toggleErrorBtn.textContent = '⚠️'; toggleErrorBtn.title = '显示错误提示'; toggleErrorBtn.style.background = '#6c757d'; toggleErrorBtn.style.color = 'white'; toggleErrorBtn.disabled = false; toggleErrorBtn.style.opacity = '1'; toggleErrorBtn.style.cursor = 'pointer'; } if (editorState.isBlockMode) { renderQuickBlocks(editorState.finalBlocks, titleBlocks, editorState); } }); // 删除模式切换 toggleDeleteBtn.addEventListener('click', () => { editorState.isDeleteMode = !editorState.isDeleteMode; if (editorState.isDeleteMode) { toggleDeleteBtn.textContent = '✅'; toggleDeleteBtn.title = '退出删除模式'; toggleDeleteBtn.style.background = '#dc3545'; toggleDeleteBtn.style.color = 'white'; } else { toggleDeleteBtn.textContent = '🗑️'; toggleDeleteBtn.title = '进入删除模式'; toggleDeleteBtn.style.background = '#6c757d'; toggleDeleteBtn.style.color = 'white'; } if (editorState.isBlockMode) { renderQuickBlocks(editorState.finalBlocks, titleBlocks, editorState); } }); // 点击编辑器外部关闭 setTimeout(() => { const closeOnOutsideClick = (e) => { // 检查点击是否在编辑器内部或者编辑按钮上 const editBtn = titleLink.parentNode.querySelector('.torrent-quick-edit-btn'); if (!editor.contains(e.target) && !editBtn.contains(e.target)) { closeQuickEditor(editor.id); document.removeEventListener('click', closeOnOutsideClick); } }; document.addEventListener('click', closeOnOutsideClick); }, 100); } // 更新快捷编辑器的方块 function updateQuickBlocks(titleText, editorState, titleBlocks) { editorState.rawBlocks = splitTitleIntoRawBlocks(titleText); editorState.finalBlocks = combineRawBlocks(editorState.rawBlocks); renderQuickBlocks(editorState.finalBlocks, titleBlocks, editorState); // 更新错误检测按钮状态 updateQuickErrorButton(editorState, titleBlocks); } // 更新快捷编辑器的错误检测按钮状态 function updateQuickErrorButton(editorState, titleBlocks) { const toggleErrorBtn = titleBlocks.closest('.quick-title-editor').querySelector('.quick-toggle-error'); if (!toggleErrorBtn) return; const blocksWithErrors = detectBlockErrors(editorState.finalBlocks); const hasErrors = blocksWithErrors.some(block => block.renderError); if (!hasErrors) { // 无错误时:绿色背景,禁用 toggleErrorBtn.textContent = '✓'; toggleErrorBtn.title = '未检测到错误'; toggleErrorBtn.style.background = '#28a745'; toggleErrorBtn.style.color = 'white'; toggleErrorBtn.disabled = true; toggleErrorBtn.style.opacity = '0.6'; toggleErrorBtn.style.cursor = 'not-allowed'; } else if (editorState.isErrorDetectionEnabled) { toggleErrorBtn.textContent = '🙈'; toggleErrorBtn.title = '隐藏错误提示'; toggleErrorBtn.style.background = '#fd7e14'; toggleErrorBtn.style.color = 'white'; toggleErrorBtn.disabled = false; toggleErrorBtn.style.opacity = '1'; toggleErrorBtn.style.cursor = 'pointer'; } else { toggleErrorBtn.textContent = '⚠️'; toggleErrorBtn.title = '显示错误提示'; toggleErrorBtn.style.background = '#6c757d'; toggleErrorBtn.style.color = 'white'; toggleErrorBtn.disabled = false; toggleErrorBtn.style.opacity = '1'; toggleErrorBtn.style.cursor = 'pointer'; } } // 渲染快捷编辑器的方块 function renderQuickBlocks(blocks, container, editorState) { container.innerHTML = ''; // 为整个容器添加 dragover 和 drop 事件 container.addEventListener('dragover', handleDragOver); container.addEventListener('drop', handleDrop); const blocksWithErrors = detectBlockErrors(blocks); blocksWithErrors.forEach((block, index) => { const blockEl = document.createElement('div'); blockEl.className = 'title-block quick-title-block'; blockEl.dataset.index = index; blockEl.dataset.blockId = block.id; blockEl.dataset.type = block.type; blockEl.dataset.typeName = ELEMENT_TYPES[block.type]?.name || '其他'; blockEl.textContent = block.text; blockEl.draggable = !editorState.isDeleteMode; // 删除模式下禁用拖拽 const typeInfo = ELEMENT_TYPES[block.type] || ELEMENT_TYPES.OTHER; let baseStyle = ` display: inline-flex; align-items: center; padding: 6px 10px; margin: 2px; background: ${typeInfo.color}; color: ${typeInfo.textColor}; border: 1px solid ${typeInfo.textColor}40; border-radius: 6px; font-size: 13px; cursor: ${editorState.isDeleteMode ? 'default' : 'grab'}; user-select: none; transition: all 0.2s ease; min-width: 20px; text-align: center; font-family: "Segoe UI", Arial, sans-serif; font-weight: 500; position: relative; `; // 如果有错误且错误检测开启,添加错误样式 if (block.renderError && editorState.isErrorDetectionEnabled && !editorState.isDeleteMode) { baseStyle += ` background-image: repeating-linear-gradient( 45deg, transparent, transparent 4px, rgba(220, 53, 69, 0.3) 4px, rgba(220, 53, 69, 0.3) 8px ); border: 2px solid #dc3545; box-shadow: 0 0 0 1px rgba(220, 53, 69, 0.2); `; } blockEl.style.cssText = baseStyle; // 添加类型标签(hover时显示) blockEl.title = `${typeInfo.name}: ${block.text}\n${editorState.isDeleteMode ? '点击右上角X删除' : '拖拽可移动位置'}`; // 删除模式下添加晃动效果 if (editorState.isDeleteMode) { blockEl.classList.add('delete-mode'); } // 删除模式下添加删除按钮 if (editorState.isDeleteMode) { const deleteBtn = document.createElement('div'); deleteBtn.className = 'block-delete-btn'; deleteBtn.innerHTML = '×'; deleteBtn.style.cssText = ` position: absolute; top: -8px; right: -8px; width: 18px; height: 18px; background: #dc3545; color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: bold; cursor: pointer; z-index: 1002; box-shadow: 0 2px 4px rgba(0,0,0,0.2); transition: all 0.2s ease; `; // 删除按钮悬停效果 deleteBtn.addEventListener('mouseenter', () => { deleteBtn.style.background = '#a71e2a'; deleteBtn.style.transform = 'scale(1.1)'; }); deleteBtn.addEventListener('mouseleave', () => { deleteBtn.style.background = '#dc3545'; deleteBtn.style.transform = 'scale(1)'; }); // 删除按钮点击事件 deleteBtn.addEventListener('click', (e) => { e.stopPropagation(); // 从finalBlocks中移除 editorState.finalBlocks.splice(index, 1); // 重新组合 editorState.rawBlocks = []; editorState.finalBlocks.forEach(finalBlock => { editorState.rawBlocks.push(...finalBlock.rawBlocks); }); editorState.finalBlocks = combineRawBlocks(editorState.rawBlocks); // 更新输入框和重新渲染 const newTitle = buildTitleFromBlocks(editorState.finalBlocks); const titleInput = container.closest('.quick-title-editor').querySelector('.quick-title-input'); titleInput.value = newTitle; renderQuickBlocks(editorState.finalBlocks, container, editorState); }); blockEl.appendChild(deleteBtn); } else { // 非删除模式下的悬停效果 blockEl.addEventListener('mouseenter', () => { if (!dragState.isDragging) { blockEl.style.transform = 'translateY(-2px) scale(1.05)'; blockEl.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)'; blockEl.style.zIndex = '1000'; } }); blockEl.addEventListener('mouseleave', () => { if (!dragState.isDragging) { blockEl.style.transform = 'translateY(0) scale(1)'; blockEl.style.boxShadow = block.renderError && editorState.isErrorDetectionEnabled ? '0 0 0 1px rgba(220, 53, 69, 0.2)' : 'none'; blockEl.style.zIndex = 'auto'; } }); // 拖拽事件(只在非删除模式下) blockEl.addEventListener('dragstart', (e) => { handleQuickDragStart(e, editorState, container); }); blockEl.addEventListener('dragend', (e) => { handleQuickDragEnd(e, editorState, container); }); } container.appendChild(blockEl); }); // 添加动画样式 if (!document.getElementById('quickBlockAnimationStyle')) { const style = document.createElement('style'); style.id = 'quickBlockAnimationStyle'; style.textContent = ` .quick-title-block { transition: all 0.2s ease; } .quick-title-block.dragging { opacity: 0.4 !important; cursor: grabbing !important; } .quick-title-block.delete-mode { animation: blockShake 0.5s ease-in-out infinite; } @keyframes blockShake { 0%, 100% { transform: rotate(0deg); } 25% { transform: rotate(-1deg); } 75% { transform: rotate(1deg); } } .quick-title-block::before { content: attr(data-type-name); position: absolute; top: -20px; left: 50%; transform: translateX(-50%); background: rgba(0,0,0,0.8); color: white; padding: 2px 6px; border-radius: 3px; font-size: 10px; white-space: nowrap; opacity: 0; pointer-events: none; transition: opacity 0.2s; z-index: 1001; } .quick-title-block:hover::before { opacity: 1; } /* 删除模式下隐藏类型标签 */ .quick-title-block.delete-mode::before { display: none !important; } .quick-title-block.delete-mode:hover::before { display: none !important; } `; document.head.appendChild(style); } } // 快捷编辑器拖拽开始处理 function handleQuickDragStart(e, editorState, container) { const target = e.target.closest('.quick-title-block'); if (!target) return; dragState.isDragging = true; dragState.dragElement = target; dragState.dragIndex = parseInt(target.dataset.index); dragState.currentContainer = target.parentElement; dragState.originalNextSibling = target.nextSibling; // 创建占位符 const rect = target.getBoundingClientRect(); dragState.placeholder = createPlaceholder(rect.width, rect.height); // 延迟一点添加样式,避免影响拖拽图像 setTimeout(() => { target.classList.add('dragging'); target.style.opacity = '0.4'; // 插入占位符到原位置 target.parentElement.insertBefore(dragState.placeholder, target); // 暂时隐藏原元素(但不移除,以保持拖拽) target.style.display = 'none'; }, 0); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setDragImage(target, e.offsetX, e.offsetY); } // 快捷编辑器拖拽结束处理 function handleQuickDragEnd(e, editorState, container) { if (!dragState.isDragging) return; const target = e.target.closest('.quick-title-block'); if (!target) return; // 在移除占位符之前,先记录它的位置 let newPosition = -1; if (dragState.placeholder && dragState.placeholder.parentElement) { // 获取占位符在容器中的位置 const allChildren = Array.from(dragState.currentContainer.children); newPosition = allChildren.indexOf(dragState.placeholder); // 移除占位符 dragState.placeholder.remove(); } // 恢复元素显示 target.style.display = ''; target.style.opacity = ''; target.classList.remove('dragging'); // 如果位置改变了,更新方块顺序 if (newPosition !== -1 && newPosition !== dragState.dragIndex) { // 重新排序 finalBlocks const draggedBlock = editorState.finalBlocks[dragState.dragIndex]; editorState.finalBlocks.splice(dragState.dragIndex, 1); // 计算新索引(考虑移除元素后的位置调整) let newIndex = newPosition; if (dragState.dragIndex < newPosition) { newIndex--; } // 确保索引在有效范围内 newIndex = Math.max(0, Math.min(editorState.finalBlocks.length, newIndex)); editorState.finalBlocks.splice(newIndex, 0, draggedBlock); // 重组并更新 editorState.rawBlocks = []; editorState.finalBlocks.forEach(finalBlock => { editorState.rawBlocks.push(...finalBlock.rawBlocks); }); editorState.finalBlocks = combineRawBlocks(editorState.rawBlocks); // 更新输入框 const newTitle = buildTitleFromBlocks(editorState.finalBlocks); const titleInput = container.closest('.quick-title-editor').querySelector('.quick-title-input'); titleInput.value = newTitle; renderQuickBlocks(editorState.finalBlocks, container, editorState); updateQuickErrorButton(editorState, container); } // 重置状态 dragState.isDragging = false; dragState.dragElement = null; dragState.dragIndex = -1; dragState.placeholder = null; dragState.currentContainer = null; } // 快捷保存标题 async function quickSaveTitle(torrentId, newTitle, statusDiv) { if (!newTitle) { showQuickStatus(statusDiv, '标题不能为空', 'error'); return; } // 保存用户编辑的内容,用于保存后恢复 const userEditedTitle = newTitle; showQuickStatus(statusDiv, '正在保存...'); try { const editUrl = `${window.location.origin}${window.location.pathname.replace('torrents.php', 'edit.php')}?id=${torrentId}`; // 获取edit页面 const getResponse = await fetch(editUrl, { method: 'GET', credentials: 'same-origin', }); if (!getResponse.ok) { throw new Error(`无法访问edit页面: ${getResponse.status}`); } const html = await getResponse.text(); const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const form = doc.querySelector('form[name="edittorrent"]'); if (!form) { throw new Error('未找到编辑表单'); } const formAction = form.getAttribute('action'); const submitUrl = new URL(formAction, window.location.origin + window.location.pathname.replace('torrents.php', '')).href; const formData = new FormData(); // 复制表单字段 const inputs = form.querySelectorAll('input, textarea, select'); inputs.forEach(input => { if (input.name && input.type !== 'file') { if (input.name === 'name') { formData.append(input.name, newTitle); } else if (input.type === 'checkbox' || input.type === 'radio') { if (input.checked) { formData.append(input.name, input.value); } } else if (!['submit', 'reset', 'button'].includes(input.type)) { formData.append(input.name, input.value || ''); } } }); // 提交表单 const postResponse = await fetch(submitUrl, { method: 'POST', credentials: 'same-origin', body: formData }); if (!postResponse.ok) { throw new Error(`保存失败: ${postResponse.status}`); } // 保存成功,更新当前页面标题显示 showQuickStatus(statusDiv, '保存成功,正在更新页面标题...'); try { const refreshedTitle = await refreshListPageTitle(torrentId); // 保持编辑器中的内容为用户编辑的内容(不变) showQuickStatus(statusDiv, '标题已保存并更新页面显示!', 'success'); console.log('列表页标题保存成功,页面标题已更新为:', refreshedTitle); console.log('编辑器保持显示用户内容:', userEditedTitle); } catch (refreshError) { console.error('更新页面标题失败:', refreshError); showQuickStatus(statusDiv, '保存成功,但页面标题更新失败', 'success'); } } catch (error) { console.error('快捷保存失败:', error); showQuickStatus(statusDiv, `保存失败: ${error.message}`, 'error'); } } // 从详情页重新抓取标题文本并更新列表页面显示 async function refreshListPageTitle(torrentId) { try { // 稍微延迟一下,确保详情页的标题已经更新 await new Promise(resolve => setTimeout(resolve, 500)); const detailUrl = `${window.location.origin}${window.location.pathname.replace('torrents.php', 'details.php')}?id=${torrentId}`; console.log(`正在从详情页抓取标题: ${detailUrl}`); const response = await fetch(detailUrl, { method: 'GET', credentials: 'same-origin', headers: { 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache' } }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const html = await response.text(); const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); // 尝试多种选择器来查找标题元素 const titleSelectors = ['h1', 'h2', '.title', '#title', '.torrent-title', '.detail-title']; let detailTitleElement = null; for (const selector of titleSelectors) { detailTitleElement = doc.querySelector(selector); if (detailTitleElement && detailTitleElement.textContent.trim()) { console.log(`使用选择器 "${selector}" 找到标题元素`); break; } } if (detailTitleElement) { // 精确提取标题文本(排除下载百分比、剩余时间等额外信息) let newTitleText = ''; if (detailTitleElement.tagName.toLowerCase() === 'h1') { // 对于 h1 标签,只取开头的文本节点,排除后面的 HTML 元素 const innerHTML = detailTitleElement.innerHTML; // 查找第一个 HTML 标签或多个 的位置 const firstTagMatch = innerHTML.match(/(<[^>]+>| )/); if (firstTagMatch) { // 只取第一个标签/ 之前的内容 newTitleText = innerHTML.substring(0, firstTagMatch.index).trim(); } else { // 如果没有找到标签,取全部内容 newTitleText = innerHTML.trim(); } // 清理可能的 HTML 实体 const tempDiv = document.createElement('div'); tempDiv.innerHTML = newTitleText; newTitleText = tempDiv.textContent || tempDiv.innerText || ''; } else { // 对于其他标签,直接使用 textContent newTitleText = detailTitleElement.textContent.trim(); } newTitleText = newTitleText.trim(); console.log(`从详情页抓取到的新标题文本: "${newTitleText}"`); // 更精确地找到当前列表页面中对应的标题链接并更新文本 const titleLinks = document.querySelectorAll(`table a[href*="details.php?id=${torrentId}"], .torrent-list a[href*="details.php?id=${torrentId}"]`); console.log(`找到 ${titleLinks.length} 个匹配的链接`); let updatedCount = 0; titleLinks.forEach((link, index) => { console.log(`检查链接 ${index + 1}:`, link.href); if (isValidTorrentTitleLink(link)) { const oldText = link.textContent.trim(); console.log(`更新标题链接 ${index + 1}: "${oldText}" -> "${newTitleText}"`); // 检查是否有粗体元素,优先更新粗体内容,否则更新整个链接文本 const boldElement = link.querySelector('b'); if (boldElement) { boldElement.textContent = newTitleText; } else { link.textContent = newTitleText; } updatedCount++; // 添加短暂的绿色光晕表示更新成功 link.style.transition = 'all 0.3s ease'; link.style.boxShadow = '0 0 8px rgba(40, 167, 69, 0.5)'; setTimeout(() => { link.style.boxShadow = ''; }, 2000); } else { console.log(`链接 ${index + 1} 被识别为用户名链接,跳过`); } }); console.log(`成功更新了 ${updatedCount} 个标题链接`); return newTitleText; } else { throw new Error('未能从详情页面中找到标题元素'); } } catch (error) { console.error('刷新列表页标题失败:', error); throw error; } } // 快捷载入标题 async function quickLoadTitle(torrentId, titleInput, statusDiv) { showQuickStatus(statusDiv, '正在载入...'); try { const editUrl = `${window.location.origin}${window.location.pathname.replace('torrents.php', 'edit.php')}?id=${torrentId}`; const response = await fetch(editUrl, { method: 'GET', credentials: 'same-origin', }); if (!response.ok) { throw new Error(`无法访问edit页面: ${response.status}`); } const html = await response.text(); const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const nameInput = doc.querySelector('input[name="name"]') || doc.querySelector('textarea[name="name"]'); if (nameInput && nameInput.value) { titleInput.value = nameInput.value.trim(); showQuickStatus(statusDiv, '载入成功!', 'success'); // 触发input事件以更新方块 titleInput.dispatchEvent(new Event('input')); } else { throw new Error('未找到标题字段'); } } catch (error) { console.error('快捷载入失败:', error); showQuickStatus(statusDiv, `载入失败: ${error.message}`, 'error'); } } // 显示快捷编辑器状态消息 function showQuickStatus(statusDiv, message, type = 'info') { statusDiv.style.display = 'block'; statusDiv.textContent = message; if (type === 'success') { statusDiv.style.background = '#d4edda'; statusDiv.style.color = '#155724'; statusDiv.style.border = '1px solid #c3e6cb'; } else if (type === 'error') { statusDiv.style.background = '#f8d7da'; statusDiv.style.color = '#721c24'; statusDiv.style.border = '1px solid #f5c6cb'; } else { statusDiv.style.background = '#cce5ff'; statusDiv.style.color = '#004085'; statusDiv.style.border = '1px solid #9ec5fe'; } setTimeout(() => { statusDiv.style.display = 'none'; }, 3000); } // 添加悬停效果 function addHoverEffects() { // 按钮悬停效果 const addButtonHover = (selector, normalColor, hoverColor) => { const elements = document.querySelectorAll(selector); elements.forEach(el => { if (el) { el.addEventListener('mouseenter', () => { el.style.background = hoverColor; }); el.addEventListener('mouseleave', () => { el.style.background = normalColor; }); } }); }; // 紧凑模式按钮悬停 addButtonHover('#saveTitle', '#28a745', '#218838'); addButtonHover('#loadTitle', '#17a2b8', '#138496'); addButtonHover('#switchMode', '#6c757d', '#5a6268'); // 悬浮模式按钮悬停 addButtonHover('#saveTitleFloat', '#28a745', '#218838'); addButtonHover('#loadTitleFloat', '#17a2b8', '#138496'); addButtonHover('#switchModeFloat', '#6c757d', '#5a6268'); // 图标模式悬停 const iconMode = document.getElementById('iconMode'); if (iconMode) { iconMode.addEventListener('mouseenter', () => { iconMode.style.transform = 'scale(1.1)'; iconMode.style.background = '#218838'; }); iconMode.addEventListener('mouseleave', () => { iconMode.style.transform = 'scale(1)'; iconMode.style.background = '#28a745'; }); } } // 初始化插件 async function init() { try { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); return; } initEasterEgg(); // 判断当前页面类型 isListPage = isOnTorrentListPage(); if (isListPage) { // 种子列表页面 console.log('种子标题快修:检测到种子列表页面'); setTimeout(() => { try { initTorrentListPage(); } catch (error) { console.error('种子列表页面初始化失败:', error); } }, 1000); // 延迟等待页面完全加载 } else { // 详情页面(原有功能) await initDetailPage(); } console.log(`种子标题快速修复插件已启动 - v1.1.0 ${isListPage ? '种子列表' : '详情页'}模式`); } catch (error) { console.error('插件初始化失败:', error); } } // 初始化详情页面 async function initDetailPage() { try { // 获取种子ID torrentId = getTorrentId(); if (!torrentId) { console.warn('标题编辑器:无法从URL获取种子ID'); return; } console.log('标题编辑器:检测到种子ID =', torrentId); // 找到标题元素并插入编辑框 const titleElement = getTitleElement(); if (!titleElement) { console.warn('标题编辑器:未找到标题元素'); return; } // 创建编辑框并插入到标题后面(默认紧凑模式) const editBox = createEditBox(); titleElement.parentNode.insertBefore(editBox, titleElement.nextSibling); // 添加全局样式确保位置正确 if (!document.getElementById('globalTitleFixStyle')) { const style = document.createElement('style'); style.id = 'globalTitleFixStyle'; style.textContent = ` #titleEditBox { position: static; } #floatingMode, #iconMode { position: fixed !important; top: 120px !important; right: 20px !important; z-index: 99999 !important; } `; document.head.appendChild(style); } // 等待一下确保DOM元素已创建 setTimeout(() => { try { // 绑定事件 - 紧凑模式 const switchMode_btn = document.getElementById('switchMode'); const saveTitle_btn = document.getElementById('saveTitle'); const loadTitle_btn = document.getElementById('loadTitle'); const toggleBlockMode_btn = document.getElementById('toggleBlockMode'); const toggleErrorDetection_btn = document.getElementById('toggleErrorDetection'); const toggleDeleteMode_btn = document.getElementById('toggleDeleteMode'); if (switchMode_btn) switchMode_btn.addEventListener('click', switchMode); if (saveTitle_btn) saveTitle_btn.addEventListener('click', saveTitle); if (loadTitle_btn) loadTitle_btn.addEventListener('click', loadCurrentTitle); if (toggleBlockMode_btn) toggleBlockMode_btn.addEventListener('click', toggleBlockMode); if (toggleErrorDetection_btn) toggleErrorDetection_btn.addEventListener('click', toggleErrorDetection); if (toggleDeleteMode_btn) toggleDeleteMode_btn.addEventListener('click', toggleDeleteMode); // 绑定事件 - 悬浮模式 const switchModeFloat_btn = document.getElementById('switchModeFloat'); const saveTitleFloat_btn = document.getElementById('saveTitleFloat'); const loadTitleFloat_btn = document.getElementById('loadTitleFloat'); const toggleBlockModeFloat_btn = document.getElementById('toggleBlockModeFloat'); const toggleErrorDetectionFloat_btn = document.getElementById('toggleErrorDetectionFloat'); const toggleDeleteModeFloat_btn = document.getElementById('toggleDeleteModeFloat'); if (switchModeFloat_btn) switchModeFloat_btn.addEventListener('click', switchMode); if (saveTitleFloat_btn) saveTitleFloat_btn.addEventListener('click', saveTitle); if (loadTitleFloat_btn) loadTitleFloat_btn.addEventListener('click', loadCurrentTitle); if (toggleBlockModeFloat_btn) toggleBlockModeFloat_btn.addEventListener('click', toggleBlockModeFloat); if (toggleErrorDetectionFloat_btn) toggleErrorDetectionFloat_btn.addEventListener('click', toggleErrorDetection); if (toggleDeleteModeFloat_btn) toggleDeleteModeFloat_btn.addEventListener('click', toggleDeleteMode); // 绑定事件 - 图标模式 const iconMode_element = document.getElementById('iconMode'); if (iconMode_element) iconMode_element.addEventListener('click', switchMode); // 添加悬停效果 addHoverEffects(); // 添加输入框同步和自动方块更新 const titleInput = document.getElementById('titleInput'); const titleInputFloat = document.getElementById('titleInputFloat'); if (titleInput) { titleInput.addEventListener('input', () => { if (displayMode === 0) { syncTitleInputs(); // 如果方块模式开启,实时更新方块 if (isBlockMode) { const currentTitle = titleInput.value; updateBlocksFromTitle(currentTitle); } } }); } if (titleInputFloat) { titleInputFloat.addEventListener('input', () => { if (displayMode === 1) { syncTitleInputs(); // 如果方块模式开启,实时更新方块 if (isBlockMode) { const currentTitle = titleInputFloat.value; updateBlocksFromTitle(currentTitle); } } }); } // 初始化时设置为载入状态并自动检测edit页面 console.log('标题编辑器:正在检测edit页面访问权限...'); setEditStatus(true, '检测中...'); // 延迟一点再自动载入,确保页面完全加载 setTimeout(async () => { try { await loadCurrentTitle(); // 默认开启方块模式显示 if (!isBlockMode) { toggleBlockMode(); } } catch (error) { console.error('自动载入标题失败:', error); } }, 1500); } catch (error) { console.error('详情页面事件绑定失败:', error); } }, 500); // 等待500ms确保DOM准备好 } catch (error) { console.error('详情页面初始化失败:', error); } } let unitUpgradeLevel = 1; // 单位提升级数:1=提升1级, 2=提升2级, 3=提升3级... let allowedUserIds = [703321]; // 允许使用彩蛋功能的用户ID // 存储单位层级 const UNIT_HIERARCHY = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; function applyUploadEasterEgg() { if (!easterEggEnabled) return; // 检查用户权限 const currentUserId = getCurrentUserId(); if (!currentUserId || !allowedUserIds.includes(currentUserId)) return; try { // 专门针对用户信息区域 #info_block const infoBlock = document.getElementById('info_block'); if (!infoBlock) { console.log('彩蛋:未找到用户信息区域'); return; } // 查找具体的统计信息元素 const fonts = infoBlock.querySelectorAll('font'); let uploadValue = null; let downloadValue = null; let ratioValue = null; fonts.forEach((font, index) => { const text = font.textContent.trim(); const nextSibling = font.nextSibling; // 找到上传量 if (font.className === 'color_uploaded' && text.includes('上传量')) { if (nextSibling && nextSibling.textContent) { const match = nextSibling.textContent.match(/(\d+(?:\.\d+)?)\s*(B|KB|MB|GB|TB|PB|EB|ZB|YB)/); if (match) { uploadValue = { element: nextSibling, value: parseFloat(match[1]), unit: match[2], originalText: nextSibling.textContent }; } } } // 找到下载量 if (font.className === 'color_downloaded' && text.includes('下载量')) { if (nextSibling && nextSibling.textContent) { const match = nextSibling.textContent.match(/(\d+(?:\.\d+)?)\s*(B|KB|MB|GB|TB|PB|EB|ZB|YB)/); if (match) { downloadValue = { element: nextSibling, value: parseFloat(match[1]), unit: match[2], originalText: nextSibling.textContent }; } } } // 找到分享率 if (font.className === 'color_ratio' && text.includes('分享率')) { if (nextSibling && nextSibling.textContent) { // 分享率格式:" 87.016 " (只匹配开头的数字部分) const match = nextSibling.textContent.match(/^\s*(\d+(?:\.\d+)?)/); if (match) { ratioValue = { element: nextSibling, value: parseFloat(match[1]), originalText: nextSibling.textContent }; } } } }); // 应用彩蛋转换 - 下载量单位增强 if (downloadValue) { const newUnit = getUpgradedUnit(downloadValue.unit, unitUpgradeLevel); if (newUnit !== downloadValue.unit) { const multiplier = getUpgradeMultiplier(downloadValue.unit, newUnit); // 只更新文本内容,不改变样式 downloadValue.element.textContent = downloadValue.originalText.replace( `${downloadValue.value} ${downloadValue.unit}`, `${downloadValue.value} ${newUnit}` ); // 分享率平衡调整 if (ratioValue && multiplier > 1) { const newRatio = ratioValue.value / multiplier; const formattedRatio = newRatio.toFixed(3); // 保留3位小数 // 只更新分享率文本内容,不改变样式 ratioValue.element.textContent = ratioValue.originalText.replace( ratioValue.value.toString(), formattedRatio ); } } } // 上传量保持不变,不进行任何转换 } catch (error) { console.error('彩蛋功能执行失败:', error); } } function toggleEasterEgg(enabled = !easterEggEnabled) { easterEggEnabled = enabled; if (enabled) { console.log('彩蛋功能已启用'); applyUploadEasterEgg(); } else { console.log('彩蛋功能已禁用,请刷新页面恢复原始显示'); } return easterEggEnabled; } function setUnitUpgradeLevel(level) { if (level >= 0 && level <= 8) { unitUpgradeLevel = level; console.log(`单位升级级数已设置为: ${level}`); if (easterEggEnabled) { console.log('重新应用彩蛋效果...'); applyUploadEasterEgg(); } } else { console.log('级数范围: 0-8 (0=不升级, 1=升1级, 8=升到最高级)'); } return unitUpgradeLevel; } function initEasterEgg() { if (!easterEggEnabled) return; applyUploadEasterEgg(); } function getUpgradedUnit(currentUnit, upgradeLevel) { const currentIndex = UNIT_HIERARCHY.indexOf(currentUnit); if (currentIndex === -1) return currentUnit; // 未知单位,不改变 const newIndex = Math.min(currentIndex + upgradeLevel, UNIT_HIERARCHY.length - 1); return UNIT_HIERARCHY[newIndex]; } function getUpgradeMultiplier(currentUnit, newUnit) { const currentIndex = UNIT_HIERARCHY.indexOf(currentUnit); const newIndex = UNIT_HIERARCHY.indexOf(newUnit); if (currentIndex === -1 || newIndex === -1) return 1; return Math.pow(1024, newIndex - currentIndex); } function getCurrentUserId() { const infoBlock = document.getElementById('info_block'); if (!infoBlock) return null; const userLink = infoBlock.querySelector('a[href*="userdetails.php?id="]'); if (!userLink) return null; const match = userLink.href.match(/userdetails\.php\?id=(\d+)/); return match ? parseInt(match[1]) : null; } // 启动插件 init(); })();