// ==UserScript== // @name 豆瓣站外搜索下载 // @namespace http://tampermonkey.net/ // @version 0.74 // @description 在豆瓣电影标题下添加多个站外搜索按钮(含源隐藏/恢复、手动排序、yellowrabbit可读页、KDocs自动搜索) // @author JIEMO // @match *://movie.douban.com/subject/* // @match *://www.kdocs.cn/* // @match *://appdocs.wpscdn.cn/* // @match *://lemonun.top/* // @match *://www.6v520.tv/* // @icon https://www.google.com/s2/favicons?domain=douban.com // @grant none // @license GPL-3.0-or-later; https://www.gnu.org/licenses/gpl-3.0.txt // @downloadURL https://update.greasyfork.icu/scripts/533898/%E8%B1%86%E7%93%A3%E7%AB%99%E5%A4%96%E6%90%9C%E7%B4%A2%E4%B8%8B%E8%BD%BD.user.js // @updateURL https://update.greasyfork.icu/scripts/533898/%E8%B1%86%E7%93%A3%E7%AB%99%E5%A4%96%E6%90%9C%E7%B4%A2%E4%B8%8B%E8%BD%BD.meta.js // ==/UserScript== (function () { 'use strict'; const CONTAINER_CLASS = 'douban-jump-btn-container'; const BTN_CLASS = 'douban-jump-btn'; const STYLE_CLASS = 'douban-btn-style'; const HIDDEN_SOURCES_KEY = 'douban_hidden_sources_v1'; const SOURCE_ORDER_KEY = 'douban_source_order_v1'; const SOURCE_LABEL_ALIASES_KEY = 'douban_source_label_aliases_v1'; const SOURCE_MANAGER_MODAL_ID = 'douban-source-manager-modal'; const KDOCS_OVERLAY_ID = 'douban-kdocs-overlay'; const KDOCS_RUNNING_FLAG = '__douban_kdocs_search_running__'; const KDOCS_MATCH_ATTR = 'data-douban-kdocs-match-id'; const KDOCS_MATCH_HIGHLIGHT_CLASS = 'douban-kdocs-match-highlight'; const KDOCS_SOURCES = [ { id: 'orange-theater', label: '橙子剧场', url: 'https://www.kdocs.cn/l/cpCsvQoAunbY?R=L1MvMzU2', type: 'sheet', }, ]; const SOURCES = [ { label: 'gying', buildUrl: (title) => `https://www.教父.com/s/1---1/${encodeURIComponent(title)}`, }, { label: '不太灵', buildUrl: (title) => `https://web5.mukaku.com/search?sb=${encodeURIComponent(title)}`, }, { label: '看片咖', buildUrl: (title) => `https://tv.kanpian.club/s/${encodeURIComponent(title)}.html`, }, { label: 'yppan', buildUrl: (title) => `https://www.yppan.com/?s=${encodeURIComponent(title)}&cat=5`, }, { label: '豆荚盘', buildUrl: (title) => `https://www.jpmom.com/?s=${encodeURIComponent(title)}`, }, { label: '秒搜', buildUrl: (title) => `https://miaosou.fun/info?searchKey=${encodeURIComponent(title)}`, }, { label: '1LOU', buildUrl: (title) => `https://www.1lou.me/search-${encodeAsUnderscoreUtf8Hex(title)}-1.htm`, }, { label: 'btbtla', buildUrl: (title) => `https://www.btbtla.com/search/${title}`, }, { label: '夸克猫', buildUrl: (title) => `https://www.kuakemao.com/?s=${encodeURIComponent(title)}`, }, { label: '磁力柠檬', buildUrl: (title) => buildExternalAutoSearchUrl('https://lemonun.top/', 'lemonSearch', title), onClick: ({ title }) => openLemonSearch(title), }, { label: 'BD影视', buildUrl: (title) => `https://www.bdjuhe.com/q/index----?k=${encodeURIComponent(title)}`, }, { label: '爱恋动漫', buildUrl: (title) => `https://www.kisssub.org/search.php?keyword=${encodeURIComponent(title)}`, }, { label: '七味', buildUrl: (title) => `https://www.qmp4.com/vs/-------------.html?wd=${encodeURIComponent(title)}`, }, { label: 'SeedHub', buildUrl: (title) => `https://www.seedhub.cc/s/${encodeURIComponent(title)}`, }, { label: '影巢', buildUrl: (title) => `https://hdhive.com/search?query=${encodeURIComponent(title)}&type=multi&page=1`, }, { label: '盘尊社区', buildUrl: (title) => `https://www.panzun.cc/?q=${encodeURIComponent(title)}`, }, { label: '海绵小站', buildUrl: (title) => `https://www.hmxz.org/search.htm?keyword=${encodeURIComponent(title)}`, }, { label: '6v电影', buildUrl: (title) => buildExternalAutoSearchUrl('https://www.6v520.tv/sousuo.html', 'sixvSearch', title), onClick: ({ title }) => open6vMovieSearch(title), }, ...KDOCS_SOURCES.map(function (source) { return { label: source.label, buildUrl: (title) => buildKdocsSearchUrl(source, title), onClick: ({ title }) => openKdocsViewer(source, title), }; }), { label: 'yellowrabbit', buildUrl: (title) => `https://api.yellowrabbit.online/api/movie/search?key=${encodeURIComponent(title)}`, onClick: ({ title, url }) => openYellowrabbitViewer(title, url), }, ]; let renderTimer = 0; let sourceManagerLastFocused = null; function normalizeTitle(raw) { return String(raw || '') .replace(/\s+\(\d{4}\)\s*$/, '') .replace(/\s+/g, ' ') .trim(); } function getMovieTitle() { const h1Element = document.querySelector('h1'); if (!h1Element) return ''; const itemReviewed = h1Element.querySelector('span[property="v:itemreviewed"]'); const yearNode = h1Element.querySelector('.year'); const itemReviewedText = normalizeTitle(itemReviewed ? itemReviewed.textContent : ''); if (itemReviewedText) return itemReviewedText; const fullTitle = normalizeTitle(document.title.replace('(豆瓣)', '')); const yearText = normalizeTitle(yearNode ? yearNode.textContent : '').replace(/[()]/g, ''); if (yearText) { return normalizeTitle(fullTitle.replace(new RegExp(`\\b${yearText}\\b`), '')); } return fullTitle; } function getSearchKeyword(title) { const normalized = normalizeTitle(title); if (!normalized) return ''; return normalized.split(' ')[0].trim(); } function getDefaultSourceLabels() { return SOURCES.map(function (source) { return source.label; }); } function loadSourceOrder() { const defaultLabels = getDefaultSourceLabels(); const allLabels = new Set(defaultLabels); try { const raw = localStorage.getItem(SOURCE_ORDER_KEY); if (!raw) return defaultLabels.slice(); const parsed = JSON.parse(raw); if (!Array.isArray(parsed)) return defaultLabels.slice(); const seen = new Set(); const ordered = parsed.filter(function (label) { if (typeof label !== 'string' || !allLabels.has(label) || seen.has(label)) { return false; } seen.add(label); return true; }); const orderedSet = new Set(ordered); for (const label of defaultLabels) { if (!orderedSet.has(label)) { ordered.push(label); } } return ordered; } catch (_error) { return defaultLabels.slice(); } } function saveSourceOrder(labels) { localStorage.setItem(SOURCE_ORDER_KEY, JSON.stringify(labels)); } function clearSourceOrder() { localStorage.removeItem(SOURCE_ORDER_KEY); } function normalizeSourceDisplayLabel(value, fallbackLabel) { const normalized = String(value == null ? '' : value).trim(); return normalized || fallbackLabel; } function loadSourceLabelAliases() { const allLabels = new Set(getDefaultSourceLabels()); try { const raw = localStorage.getItem(SOURCE_LABEL_ALIASES_KEY); if (!raw) return {}; const parsed = JSON.parse(raw); if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return {}; return Object.keys(parsed).reduce(function (aliases, label) { const value = parsed[label]; if (!allLabels.has(label) || typeof value !== 'string') { return aliases; } const displayLabel = normalizeSourceDisplayLabel(value, label); if (displayLabel !== label) { aliases[label] = displayLabel; } return aliases; }, {}); } catch (_error) { return {}; } } function saveSourceLabelAliases(labelAliases) { localStorage.setItem(SOURCE_LABEL_ALIASES_KEY, JSON.stringify(labelAliases)); } function clearSourceLabelAliases() { localStorage.removeItem(SOURCE_LABEL_ALIASES_KEY); } function getSourceDisplayLabel(label, labelAliases) { if (!labelAliases || typeof labelAliases[label] !== 'string') { return label; } return normalizeSourceDisplayLabel(labelAliases[label], label); } function getOrderedSources() { const sourceMap = new Map(SOURCES.map(function (source) { return [source.label, source]; })); return loadSourceOrder().map(function (label) { return sourceMap.get(label); }).filter(Boolean); } function getAllSourceLabels() { return getOrderedSources().map(function (source) { return source.label; }); } function loadHiddenSources() { const allLabels = new Set(getDefaultSourceLabels()); try { const raw = localStorage.getItem(HIDDEN_SOURCES_KEY); if (!raw) return new Set(); const parsed = JSON.parse(raw); if (!Array.isArray(parsed)) return new Set(); return new Set(parsed.filter(function (label) { return typeof label === 'string' && allLabels.has(label); })); } catch (_error) { return new Set(); } } function saveHiddenSources(hiddenSet) { localStorage.setItem(HIDDEN_SOURCES_KEY, JSON.stringify(Array.from(hiddenSet))); } function hideSource(label) { const hidden = loadHiddenSources(); hidden.add(label); saveHiddenSources(hidden); } function toggleSourceHidden(label) { const hidden = loadHiddenSources(); if (hidden.has(label)) { hidden.delete(label); saveHiddenSources(hidden); return false; } hidden.add(label); saveHiddenSources(hidden); return true; } function clearHiddenSources() { localStorage.removeItem(HIDDEN_SOURCES_KEY); } function openSourceOrderManager() { openSourceManager(); } function openSourceManager() { if (!document.body) return; sourceManagerLastFocused = document.activeElement instanceof HTMLElement ? document.activeElement : null; closeSourceManager(); const defaultLabels = getDefaultSourceLabels(); const state = { labels: getAllSourceLabels(), hidden: loadHiddenSources(), labelAliases: loadSourceLabelAliases(), draggedLabel: '', }; const mask = document.createElement('div'); mask.id = SOURCE_MANAGER_MODAL_ID; mask.className = 'douban-source-manager-mask'; const panel = document.createElement('section'); panel.className = 'douban-source-manager-panel'; panel.tabIndex = -1; panel.setAttribute('role', 'dialog'); panel.setAttribute('aria-modal', 'true'); panel.setAttribute('aria-label', '源设置'); const header = document.createElement('div'); header.className = 'douban-source-manager-header'; const titleWrap = document.createElement('div'); titleWrap.className = 'douban-source-manager-title-wrap'; const title = document.createElement('h2'); title.className = 'douban-source-manager-title'; title.textContent = '源设置'; const subtitle = document.createElement('div'); subtitle.className = 'douban-source-manager-subtitle'; subtitle.textContent = '点击卡片按钮切换显示,拖动卡片调整顺序,也可直接修改显示名称。'; titleWrap.appendChild(title); titleWrap.appendChild(subtitle); const closeBtn = document.createElement('button'); closeBtn.type = 'button'; closeBtn.className = 'douban-source-manager-close'; closeBtn.setAttribute('aria-label', '关闭源设置'); closeBtn.textContent = '×'; header.appendChild(titleWrap); header.appendChild(closeBtn); const toolbar = document.createElement('div'); toolbar.className = 'douban-source-manager-toolbar'; const resetOrderBtn = document.createElement('button'); resetOrderBtn.type = 'button'; resetOrderBtn.className = 'douban-source-manager-action'; resetOrderBtn.textContent = '恢复默认排序'; const resetVisibleBtn = document.createElement('button'); resetVisibleBtn.type = 'button'; resetVisibleBtn.className = 'douban-source-manager-action'; resetVisibleBtn.textContent = '恢复全部显示'; toolbar.appendChild(resetOrderBtn); toolbar.appendChild(resetVisibleBtn); const summary = document.createElement('div'); summary.className = 'douban-source-manager-summary'; const list = document.createElement('ul'); list.className = 'douban-source-manager-list'; const footer = document.createElement('div'); footer.className = 'douban-source-manager-footer'; const cancelBtn = document.createElement('button'); cancelBtn.type = 'button'; cancelBtn.className = 'douban-source-manager-btn-secondary'; cancelBtn.textContent = '取消'; const saveBtn = document.createElement('button'); saveBtn.type = 'button'; saveBtn.className = 'douban-source-manager-btn-primary'; saveBtn.textContent = '保存'; footer.appendChild(cancelBtn); footer.appendChild(saveBtn); panel.appendChild(header); panel.appendChild(toolbar); panel.appendChild(summary); panel.appendChild(list); panel.appendChild(footer); mask.appendChild(panel); function renderSummary() { const hiddenCount = state.hidden.size; const visibleCount = state.labels.length - hiddenCount; summary.textContent = '共 ' + state.labels.length + ' 个源,当前显示 ' + visibleCount + ' 个,隐藏 ' + hiddenCount + ' 个。'; } function moveLabelToIndex(draggedLabel, targetIndex) { if (!draggedLabel) return; const nextLabels = state.labels.slice(); const sourceIndex = nextLabels.indexOf(draggedLabel); if (sourceIndex === -1) return; nextLabels.splice(sourceIndex, 1); const safeIndex = Math.max(0, Math.min(targetIndex, nextLabels.length)); nextLabels.splice(safeIndex, 0, draggedLabel); state.labels = nextLabels; } function getReorderEntries() { return Array.from(list.querySelectorAll('.douban-source-manager-item')).filter(function (item) { return item.getAttribute('data-label') !== state.draggedLabel; }).map(function (item, index) { return { item: item, index: index, rect: item.getBoundingClientRect(), }; }); } function clearDropIndicators() { Array.from(list.querySelectorAll('.is-drop-target, .is-drop-after')).forEach(function (node) { node.classList.remove('is-drop-target', 'is-drop-after'); }); } function updateDropIndicators(insertIndex) { clearDropIndicators(); const entries = getReorderEntries(); if (entries.length === 0) return; if (insertIndex >= entries.length) { entries[entries.length - 1].item.classList.add('is-drop-after'); return; } entries[insertIndex].item.classList.add('is-drop-target'); } function getInsertIndexFromPointer(clientX, clientY) { const entries = getReorderEntries(); if (entries.length === 0) return 0; const rows = []; entries.forEach(function (entry) { const lastRow = rows[rows.length - 1]; const sameRow = lastRow && Math.abs(lastRow.top - entry.rect.top) < Math.max(12, entry.rect.height / 2); if (sameRow) { lastRow.entries.push(entry); lastRow.bottom = Math.max(lastRow.bottom, entry.rect.bottom); return; } rows.push({ top: entry.rect.top, bottom: entry.rect.bottom, entries: [entry], }); }); for (let rowIndex = 0; rowIndex < rows.length; rowIndex += 1) { const row = rows[rowIndex]; const rowMiddle = row.top + (row.bottom - row.top) / 2; if (clientY <= rowMiddle) { for (let entryIndex = 0; entryIndex < row.entries.length; entryIndex += 1) { const entry = row.entries[entryIndex]; const entryMiddle = entry.rect.left + entry.rect.width / 2; if (clientX <= entryMiddle) { return entry.index; } } return row.entries[row.entries.length - 1].index + 1; } } return entries.length; } function handleListDragOver(event) { event.preventDefault(); if (event.dataTransfer) { event.dataTransfer.dropEffect = 'move'; } if (!state.draggedLabel) return; updateDropIndicators(getInsertIndexFromPointer(event.clientX, event.clientY)); } function handleListDrop(event) { event.preventDefault(); const draggedLabel = state.draggedLabel || (event.dataTransfer ? event.dataTransfer.getData('text/plain') : ''); if (!draggedLabel) return; moveLabelToIndex(draggedLabel, getInsertIndexFromPointer(event.clientX, event.clientY)); clearDropIndicators(); renderList(); } function renderList() { list.innerHTML = ''; renderSummary(); state.labels.forEach(function (label, index) { const item = document.createElement('li'); const isHidden = state.hidden.has(label); const displayLabel = getSourceDisplayLabel(label, state.labelAliases); item.className = 'douban-source-manager-item' + (isHidden ? ' is-hidden' : ''); item.draggable = true; item.setAttribute('data-label', label); const indexNode = document.createElement('span'); indexNode.className = 'douban-source-manager-index'; indexNode.textContent = String(index + 1).padStart(2, '0'); const main = document.createElement('div'); main.className = 'douban-source-manager-main'; const nameGroup = document.createElement('div'); nameGroup.className = 'douban-source-manager-name-group'; const itemTop = document.createElement('div'); itemTop.className = 'douban-source-manager-item-top'; const labelNode = document.createElement('div'); labelNode.className = 'douban-source-manager-label'; labelNode.textContent = displayLabel; const metaNode = document.createElement('div'); metaNode.className = 'douban-source-manager-meta'; metaNode.textContent = '原始名称:' + label; const renameInput = document.createElement('input'); renameInput.type = 'text'; renameInput.className = 'douban-source-manager-rename'; renameInput.value = displayLabel; renameInput.placeholder = label; renameInput.draggable = false; renameInput.setAttribute('aria-label', '修改源“' + label + '”的显示名称'); renameInput.addEventListener('mousedown', function (event) { event.stopPropagation(); }); renameInput.addEventListener('dragstart', function (event) { event.preventDefault(); event.stopPropagation(); }); renameInput.addEventListener('input', function () { const nextDisplayLabel = normalizeSourceDisplayLabel(renameInput.value, label); labelNode.textContent = nextDisplayLabel; if (nextDisplayLabel === label) { delete state.labelAliases[label]; } else { state.labelAliases[label] = nextDisplayLabel; } }); renameInput.addEventListener('blur', function () { renameInput.value = getSourceDisplayLabel(label, state.labelAliases); }); const toggleBtn = document.createElement('button'); toggleBtn.type = 'button'; toggleBtn.className = 'douban-source-manager-toggle' + (isHidden ? ' is-hidden' : ''); toggleBtn.textContent = isHidden ? '显示' : '隐藏'; toggleBtn.addEventListener('click', function () { if (state.hidden.has(label)) { state.hidden.delete(label); } else { state.hidden.add(label); } renderList(); }); item.addEventListener('dragstart', function (event) { state.draggedLabel = label; if (event.dataTransfer) { event.dataTransfer.effectAllowed = 'move'; event.dataTransfer.setData('text/plain', label); } item.classList.add('is-dragging'); }); item.addEventListener('dragover', function (event) { handleListDragOver(event); }); item.addEventListener('dragleave', function () { item.classList.remove('is-drop-target', 'is-drop-after'); }); item.addEventListener('drop', function (event) { event.stopPropagation(); handleListDrop(event); }); item.addEventListener('dragend', function () { state.draggedLabel = ''; Array.from(list.querySelectorAll('.is-drop-target, .is-drop-after, .is-dragging')).forEach(function (node) { node.classList.remove('is-drop-target', 'is-drop-after', 'is-dragging'); }); }); itemTop.appendChild(indexNode); itemTop.appendChild(toggleBtn); main.appendChild(itemTop); nameGroup.appendChild(labelNode); nameGroup.appendChild(metaNode); nameGroup.appendChild(renameInput); main.appendChild(nameGroup); item.appendChild(main); list.appendChild(item); }); } resetOrderBtn.addEventListener('click', function () { state.labels = defaultLabels.slice(); renderList(); }); resetVisibleBtn.addEventListener('click', function () { state.hidden.clear(); renderList(); }); list.addEventListener('dragover', function (event) { handleListDragOver(event); }); list.addEventListener('drop', function (event) { handleListDrop(event); }); closeBtn.addEventListener('click', closeSourceManager); cancelBtn.addEventListener('click', closeSourceManager); saveBtn.addEventListener('click', function () { if (state.hidden.size > 0) { saveHiddenSources(state.hidden); } else { clearHiddenSources(); } if (JSON.stringify(state.labels) === JSON.stringify(defaultLabels)) { clearSourceOrder(); } else { saveSourceOrder(state.labels.slice()); } if (Object.keys(state.labelAliases).length > 0) { saveSourceLabelAliases(state.labelAliases); } else { clearSourceLabelAliases(); } closeSourceManager(); renderButtons(true); }); mask.addEventListener('click', function (event) { if (event.target === mask) { closeSourceManager(); } }); document.body.appendChild(mask); document.addEventListener('keydown', handleSourceManagerKeydown, true); renderList(); closeBtn.focus(); } function closeSourceManager() { const existing = document.getElementById(SOURCE_MANAGER_MODAL_ID); if (existing) { existing.remove(); } document.removeEventListener('keydown', handleSourceManagerKeydown, true); if (sourceManagerLastFocused && document.contains(sourceManagerLastFocused)) { sourceManagerLastFocused.focus(); } sourceManagerLastFocused = null; } function handleSourceManagerKeydown(event) { const modal = document.getElementById(SOURCE_MANAGER_MODAL_ID); if (!modal) return; if (event.key === 'Escape') { event.preventDefault(); closeSourceManager(); return; } if (event.key !== 'Tab') return; const focusables = Array.from(modal.querySelectorAll('button, input, [href], [tabindex]:not([tabindex="-1"])')).filter(function (node) { return node instanceof HTMLElement && !node.hasAttribute('disabled') && node.tabIndex !== -1; }); if (focusables.length === 0) return; const first = focusables[0]; const last = focusables[focusables.length - 1]; const active = document.activeElement; if (event.shiftKey && active === first) { event.preventDefault(); last.focus(); return; } if (!event.shiftKey && active === last) { event.preventDefault(); first.focus(); } } function encodeAsUnderscoreUtf8Hex(text) { const bytes = new TextEncoder().encode(String(text || '')); let output = ''; for (const value of bytes) { output += `_${value.toString(16).toUpperCase().padStart(2, '0')}`; } return output; } function getKdocsSourceById(sourceId) { return KDOCS_SOURCES.find(function (source) { return source.id === sourceId; }) || null; } function buildKdocsSearchUrl(source, keyword) { const config = { doubanMode: 'kdocsSearch', sourceId: source.id, sourceType: source.type, keyword: keyword, sheetLabel: source.label, }; const url = new URL(source.url); url.searchParams.set('doubanMode', config.doubanMode); url.searchParams.set('doubanSourceId', config.sourceId); url.searchParams.set('doubanSourceType', config.sourceType); url.searchParams.set('doubanKeyword', config.keyword); url.searchParams.set('doubanSheetLabel', config.sheetLabel); url.hash = new URLSearchParams(config).toString(); return url.toString(); } function openKdocsViewer(source, keyword) { const url = buildKdocsSearchUrl(source, keyword); window.open(url, '_blank'); } function parseKdocsSearchConfig() { const hash = String(window.location.hash || ''); const hashParams = hash.startsWith('#') ? new URLSearchParams(hash.slice(1)) : null; const searchParams = new URLSearchParams(window.location.search || ''); const mode = (hashParams && hashParams.get('doubanMode')) || searchParams.get('doubanMode') || ''; if (mode !== 'kdocsSearch') return null; const sourceId = (hashParams && hashParams.get('sourceId')) || searchParams.get('doubanSourceId') || ''; const sourceTypeFromQuery = (hashParams && hashParams.get('sourceType')) || searchParams.get('doubanSourceType') || ''; const matchedSource = getKdocsSourceById(sourceId); const keyword = (hashParams && hashParams.get('keyword')) || searchParams.get('doubanKeyword') || ''; const sheetLabel = (hashParams && hashParams.get('sheetLabel')) || searchParams.get('doubanSheetLabel') || (matchedSource && matchedSource.label) || document.title || 'KDocs'; const sourceType = sourceTypeFromQuery || (matchedSource && matchedSource.type) || 'sheet'; if (!keyword) return null; return { keyword, sheetLabel, sourceId, sourceType }; } function ensureKdocsOverlayStyle() { if (document.getElementById(KDOCS_OVERLAY_ID + '-style')) return; const style = document.createElement('style'); style.id = KDOCS_OVERLAY_ID + '-style'; style.textContent = ` #${KDOCS_OVERLAY_ID} { position: fixed; top: 16px; right: 16px; width: min(440px, calc(100vw - 24px)); max-height: calc(100vh - 32px); overflow: auto; z-index: 2147483647; background: rgba(255, 255, 255, 0.98); border: 1px solid #cfe0f6; border-radius: 14px; box-shadow: 0 14px 38px rgba(15, 23, 42, 0.18); color: #0f172a; font: 14px/1.55 -apple-system, BlinkMacSystemFont, Segoe UI, PingFang SC, Hiragino Sans GB, Microsoft YaHei, sans-serif; } #${KDOCS_OVERLAY_ID} * { box-sizing: border-box; } #${KDOCS_OVERLAY_ID} .douban-kdocs-head { padding: 12px 14px 8px; border-bottom: 1px solid #e3ecf8; background: linear-gradient(180deg, #f7fbff 0%, #ffffff 100%); } #${KDOCS_OVERLAY_ID} .douban-kdocs-title { margin: 0; font-size: 17px; font-weight: 700; } #${KDOCS_OVERLAY_ID} .douban-kdocs-sub, #${KDOCS_OVERLAY_ID} .douban-kdocs-count, #${KDOCS_OVERLAY_ID} .douban-kdocs-empty { margin-top: 4px; color: #4b5563; word-break: break-word; } #${KDOCS_OVERLAY_ID} .douban-kdocs-status { margin-top: 6px; font-weight: 600; color: #1565c0; } #${KDOCS_OVERLAY_ID} .douban-kdocs-status.is-error { color: #b91c1c; } #${KDOCS_OVERLAY_ID} .douban-kdocs-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; } #${KDOCS_OVERLAY_ID} .douban-kdocs-btn { appearance: none; border: 1px solid #c8d7ef; background: #eef5ff; color: #1565c0; border-radius: 999px; padding: 6px 12px; font: inherit; font-weight: 600; cursor: pointer; } #${KDOCS_OVERLAY_ID} .douban-kdocs-body { padding: 10px 14px 14px; display: grid; gap: 8px; } #${KDOCS_OVERLAY_ID} .douban-kdocs-row { border: 1px solid #dbe6f4; background: #f8fbff; border-radius: 10px; padding: 8px 10px; word-break: break-word; } #${KDOCS_OVERLAY_ID} .douban-kdocs-row[data-anchor-id] { cursor: pointer; transition: background 0.2s ease, border-color 0.2s ease; } #${KDOCS_OVERLAY_ID} .douban-kdocs-row[data-anchor-id]:hover { background: #eef5ff; border-color: #b7ccef; } [${KDOCS_MATCH_ATTR}].${KDOCS_MATCH_HIGHLIGHT_CLASS} { outline: 3px solid rgba(21, 101, 192, 0.35); background: rgba(255, 243, 205, 0.75) !important; border-radius: 8px; transition: outline 0.2s ease, background 0.2s ease; } @media (max-width: 720px) { #${KDOCS_OVERLAY_ID} { top: auto; right: 8px; bottom: 8px; left: 8px; width: auto; max-height: 55vh; } } `; document.head.appendChild(style); } function escapeHtml(input) { return String(input || '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function kdocsNormalizeOverlayItems(result) { if (Array.isArray(result.items) && result.items.length > 0) { return result.items.map(function (item) { if (item && typeof item === 'object') { return { text: String(item.text || ''), anchorId: item.anchorId ? String(item.anchorId) : '', }; } return { text: String(item || ''), anchorId: '' }; }).filter(function (item) { return Boolean(item.text); }); } return (Array.isArray(result.rows) ? result.rows : []).map(function (row) { return { text: String(row || ''), anchorId: '' }; }).filter(function (item) { return Boolean(item.text); }); } function kdocsFindMatchNode(anchorId) { if (!anchorId) return null; return Array.from(document.querySelectorAll('[' + KDOCS_MATCH_ATTR + ']')).find(function (node) { return node.getAttribute(KDOCS_MATCH_ATTR) === anchorId; }) || null; } function kdocsScrollToMatch(anchorId) { const node = kdocsFindMatchNode(anchorId); if (!node) return false; node.scrollIntoView({ behavior: 'smooth', block: 'center' }); node.classList.add(KDOCS_MATCH_HIGHLIGHT_CLASS); window.setTimeout(function () { node.classList.remove(KDOCS_MATCH_HIGHLIGHT_CLASS); }, 2200); return true; } function renderKdocsSearchOverlay(config, result) { ensureKdocsOverlayStyle(); let root = document.getElementById(KDOCS_OVERLAY_ID); if (!root) { root = document.createElement('section'); root.id = KDOCS_OVERLAY_ID; document.body.appendChild(root); } const items = kdocsNormalizeOverlayItems(result); const countText = result.countText || (items.length > 0 ? ('结果数: ' + items.length) : ''); const statusClass = result.errorText ? 'douban-kdocs-status is-error' : 'douban-kdocs-status'; const statusText = result.statusText || (result.errorText ? ('搜索失败: ' + result.errorText) : '搜索完成'); const rowsHtml = items.length > 0 ? items.map(function (item, index) { const anchorAttr = item.anchorId ? ' data-anchor-id="' + escapeHtml(item.anchorId) + '"' : ''; return '
' + escapeHtml(item.text) + '
'; }).join('') : '
未找到匹配行
'; root.innerHTML = '' + '
' + '

' + escapeHtml(config.sheetLabel || 'KDocs') + ' 自动搜索

' + '
关键词: ' + escapeHtml(config.keyword) + '
' + '
' + escapeHtml(statusText) + '
' + (countText ? '
' + escapeHtml(countText) + '
' : '') + '
' + '' + '' + '
' + '
' + '
' + rowsHtml + '
'; const retryBtn = root.querySelector('[data-action="retry"]'); const closeBtn = root.querySelector('[data-action="close"]'); if (retryBtn) { retryBtn.onclick = function () { runKdocsAutoSearch(config); }; } if (closeBtn) { closeBtn.onclick = function () { root.remove(); }; } Array.from(root.querySelectorAll('.douban-kdocs-row[data-anchor-id]')).forEach(function (rowNode) { rowNode.onclick = function () { const anchorId = rowNode.getAttribute('data-anchor-id') || ''; kdocsScrollToMatch(anchorId); }; }); const firstAnchorItem = items.find(function (item) { return Boolean(item.anchorId); }); if (firstAnchorItem && !result.errorText) { window.setTimeout(function () { kdocsScrollToMatch(firstAnchorItem.anchorId); }, 120); } } function kdocsSleep(ms) { return new Promise(function (resolve) { window.setTimeout(resolve, ms); }); } function kdocsTextOf(node) { return String((node && (node.innerText || node.textContent)) || '').trim(); } function kdocsFindByText(selector, text, exact) { return Array.from(document.querySelectorAll(selector)).find(function (node) { const value = kdocsTextOf(node); return exact ? value === text : value.indexOf(text) !== -1; }) || null; } function kdocsClick(node) { if (!node) return false; node.click(); return true; } function kdocsSetInputValue(input, value) { if (!input) return false; const prototype = input.tagName === 'TEXTAREA' ? window.HTMLTextAreaElement.prototype : window.HTMLInputElement.prototype; const descriptor = Object.getOwnPropertyDescriptor(prototype, 'value'); if (descriptor && typeof descriptor.set === 'function') { descriptor.set.call(input, value); } else { input.value = value; } input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true })); return true; } function kdocsCloseLoginModal() { const closeBtn = document.querySelector('.component-login-modal-pop .kdv-button'); if (closeBtn) { closeBtn.click(); } } function kdocsNormalizeResultText(text) { return String(text || '').replace(/\s+/g, ' ').trim(); } function kdocsCollectTexts(selectors) { const values = []; for (const selector of selectors) { const nodes = document.querySelectorAll(selector); for (const node of nodes) { const text = kdocsNormalizeResultText(kdocsTextOf(node)); if (!text) continue; values.push(text); } if (values.length > 0) { break; } } return values; } function kdocsSplitTextLines(text) { return String(text || '') .split(/\r?\n+/) .map(function (line) { return kdocsNormalizeResultText(line); }) .filter(Boolean); } function kdocsIncludesKeyword(text, keyword) { return String(text || '').toLowerCase().indexOf(String(keyword || '').toLowerCase()) !== -1; } async function kdocsResolveValue(value) { if (value && typeof value.then === 'function') { return await value; } return value; } function kdocsIsNoiseLine(text, keyword) { if (!text) return true; if (text === keyword) return true; if (text.indexOf('重新搜索') !== -1) return true; if (text.indexOf('关闭结果') !== -1) return true; if (text.indexOf('正在自动搜索') !== -1) return true; if (text.indexOf('自动搜索超时') !== -1) return true; if (text.indexOf('查找全部') !== -1) return true; return false; } function kdocsFilterKeywordLines(lines, keyword) { return lines.filter(function (line) { if (!kdocsIncludesKeyword(line, keyword)) return false; if (line.length > 500) return false; return !kdocsIsNoiseLine(line, keyword); }); } function kdocsFilterKeywordItems(items, keyword) { return items.filter(function (item) { const text = item && typeof item === 'object' ? String(item.text || '') : String(item || ''); if (!kdocsIncludesKeyword(text, keyword)) return false; if (text.length > 500) return false; return !kdocsIsNoiseLine(text, keyword); }).map(function (item) { if (item && typeof item === 'object') return item; return { text: String(item || ''), anchorId: '' }; }); } function kdocsCreateAnchorId() { return 'kdocs-match-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8); } function kdocsEnsureMatchAnchor(node) { if (!node || !node.setAttribute) return ''; let anchorId = node.getAttribute(KDOCS_MATCH_ATTR) || ''; if (!anchorId) { anchorId = kdocsCreateAnchorId(); node.setAttribute(KDOCS_MATCH_ATTR, anchorId); } return anchorId; } function kdocsUniqueItems(items) { const seen = new Set(); return items.filter(function (item) { const key = (item.anchorId || '') + '::' + item.text; if (seen.has(key)) return false; seen.add(key); return true; }); } function kdocsIsVisibleElement(node) { if (!node || node.nodeType !== 1) return false; const style = window.getComputedStyle(node); if (!style) return true; if (style.display === 'none' || style.visibility === 'hidden') return false; return true; } function kdocsIsClickableElement(node) { if (!node || node.nodeType !== 1) return false; const tagName = String(node.tagName || '').toLowerCase(); if (['button', 'a', 'summary'].indexOf(tagName) !== -1) return true; if (node.hasAttribute('role')) { const role = String(node.getAttribute('role') || '').toLowerCase(); if (['tab', 'button', 'link', 'menuitem', 'option', 'switch', 'radio'].indexOf(role) !== -1) return true; } if (node.hasAttribute('tabindex')) return true; if (node.hasAttribute('aria-selected')) return true; if (node.hasAttribute('aria-pressed')) return true; if (node.hasAttribute('aria-expanded')) return true; if (node.hasAttribute('aria-checked')) return true; if (typeof node.onclick === 'function') return true; const style = window.getComputedStyle(node); return Boolean(style && style.cursor === 'pointer'); } function kdocsNormalizeSectionLabel(text, node) { const normalized = kdocsNormalizeResultText(text); if (!normalized || normalized.length > 40) return ''; const hasTabSemantics = Boolean( node && ( node.getAttribute('role') === 'tab' || node.hasAttribute('aria-selected') || node.hasAttribute('aria-controls') || node.closest('[role="tablist"]') || node.closest('[class*="tab"]') || node.closest('[class*="segmented"]') || node.closest('[class*="segment"]') ) ); if (/^[0-9]+$/.test(normalized) && !hasTabSemantics) return ''; if (normalized.length < 2 && !hasTabSemantics) return ''; return normalized; } function kdocsGetDocSectionSwitches() { const selector = [ '[role="tablist"] [role="tab"]', '[role="tablist"] [aria-selected]', '[role="tab"]', '[aria-selected]', '[aria-controls]', '[aria-owns]', '[class*="tab"]', '[class*="segmented"] [role="button"]', '[class*="segment"] [role="button"]', '[class*="toggle"] [role="button"]', '[class*="catalog"] [class*="item"]', '[class*="toc"] [class*="item"]', '[class*="nav"] [class*="item"]', '[class*="menu"] [class*="item"]', '[class*="outline"] [class*="item"]', '[class*="sidebar"] [class*="item"]', '[class*="switch"] [class*="item"]', '[data-tab]', '[data-panel]', '[data-column]', '[data-col]', '[data-index]', 'nav [role="tab"]', 'nav button', 'nav a', '[class*="tabs"] *', ].join(', '); const seen = new Set(); const results = []; Array.from(document.querySelectorAll(selector)).forEach(function (node) { if (!kdocsIsVisibleElement(node)) return; if (node.closest('#' + KDOCS_OVERLAY_ID)) return; const clickable = kdocsIsClickableElement(node) ? node : node.closest('button, a, [role="tab"], [role="button"], [tabindex]'); if (!clickable || !kdocsIsVisibleElement(clickable)) return; const label = kdocsNormalizeSectionLabel(kdocsTextOf(clickable), clickable); if (!label) return; const key = (clickable.getAttribute('aria-controls') || clickable.getAttribute('data-tab') || clickable.getAttribute('data-column') || label) + '::' + String(clickable.className || ''); if (seen.has(key)) return; seen.add(key); results.push({ node: clickable, label: label }); }); return results.slice(0, 20); } function kdocsFindGenericDocMatchItems(config) { if (!document.body) return []; const results = []; const nodes = document.body.querySelectorAll('*'); for (const node of nodes) { if (!kdocsIsVisibleElement(node)) continue; if (node.closest('#' + KDOCS_OVERLAY_ID)) continue; const tagName = String(node.tagName || '').toLowerCase(); if (['script', 'style', 'noscript', 'svg', 'path'].indexOf(tagName) !== -1) continue; const text = kdocsNormalizeResultText(kdocsTextOf(node)); if (!text || text.length < config.keyword.length || text.length > 500) continue; if (!kdocsIncludesKeyword(text, config.keyword)) continue; if (kdocsIsNoiseLine(text, config.keyword)) continue; const childContains = Array.from(node.children || []).some(function (child) { if (!kdocsIsVisibleElement(child)) return false; const childText = kdocsNormalizeResultText(kdocsTextOf(child)); return childText && childText.length <= 500 && kdocsIncludesKeyword(childText, config.keyword); }); if (childContains) continue; results.push({ text: text, anchorId: kdocsEnsureMatchAnchor(node) }); if (results.length >= 20) break; } return results; } async function kdocsScanDocSectionsForMatches(config) { const sections = kdocsGetDocSectionSwitches(); for (const section of sections) { try { section.node.click(); } catch (_error) { continue; } await kdocsSleep(260); const directResult = kdocsExtractDocDirectResult(config, { allowPlainTextFallback: false }); if (Array.isArray(directResult.items) && directResult.items.some(function (item) { return Boolean(item.anchorId); })) { directResult.countText = section.label ? ('栏目匹配: ' + directResult.items.length + '(' + section.label + ')') : ('栏目匹配: ' + directResult.items.length); return directResult; } } return { countText: '', rows: [], items: [], errorText: '', }; } function kdocsBuildDocRowsResult(rows, label) { const items = kdocsUniqueItems(rows.map(function (row) { if (row && typeof row === 'object') { return { text: String(row.text || ''), anchorId: row.anchorId ? String(row.anchorId) : '', }; } return { text: String(row || ''), anchorId: '' }; }).filter(function (item) { return Boolean(item.text); })); return { countText: items.length > 0 ? (label + ': ' + items.length) : '', rows: items.map(function (item) { return item.text; }).slice(0, 20), items: items.slice(0, 20), errorText: '', }; } function kdocsGetDocApiCandidates() { const candidates = [ window.wpsInstance && window.wpsInstance.Application, window.instance && window.instance.Application, window.jssdk && window.jssdk.Application, window.webOfficeInstance && window.webOfficeInstance.Application, window.Application, window.WpsApplication, window.wpsApplication, ].filter(Boolean); return Array.from(new Set(candidates)); } async function kdocsExtractDocApiResult(config) { const apps = kdocsGetDocApiCandidates(); for (const app of apps) { try { const doc = await kdocsResolveValue(app.ActiveDocument); if (!doc) continue; const rows = []; const paragraphs = await kdocsResolveValue(doc.Paragraphs); const paragraphCount = paragraphs ? Number(await kdocsResolveValue(paragraphs.Count)) || 0 : 0; if (paragraphCount > 0 && typeof paragraphs.Item === 'function') { const limit = Math.min(paragraphCount, 400); for (let index = 1; index <= limit; index += 1) { const paragraph = await kdocsResolveValue(paragraphs.Item(index)); if (!paragraph) continue; const range = await kdocsResolveValue(paragraph.Range); const text = range ? kdocsNormalizeResultText(await kdocsResolveValue(range.Text)) : ''; if (!text) continue; rows.push({ text: text, anchorId: '' }); } } let filteredRows = kdocsFilterKeywordItems(rows, config.keyword); if (filteredRows.length === 0 && typeof doc.Range === 'function') { const fullRange = await kdocsResolveValue(doc.Range(0, 200000)); const fullText = fullRange ? await kdocsResolveValue(fullRange.Text) : ''; filteredRows = kdocsFilterKeywordItems(kdocsSplitTextLines(fullText), config.keyword); } if (filteredRows.length > 0) { return kdocsBuildDocRowsResult(filteredRows, 'API匹配'); } } catch (_error) { continue; } } return { countText: '', rows: [], errorText: '', }; } function kdocsExtractDocDirectResult(config, options) { const settings = options || {}; const directSelectors = [ 'p', 'li', 'h1', 'h2', 'h3', 'h4', 'blockquote', 'pre', 'td', 'th', '[class*="paragraph"]', '[class*="para"]', '[class*="doc"] [class*="text"]', '[class*="editor"] [class*="text"]', '[class*="reader"] [class*="text"]', '[class*="page"] [class*="text"]', '[data-content]', '[data-contents]', ]; const rows = []; for (const selector of directSelectors) { const nodes = document.querySelectorAll(selector); for (const node of nodes) { if (node.closest('#' + KDOCS_OVERLAY_ID)) continue; const text = kdocsNormalizeResultText(kdocsTextOf(node)); if (!text || text.length < config.keyword.length) continue; if (text.length > 500) continue; if (!kdocsIncludesKeyword(text, config.keyword)) continue; if (kdocsIsNoiseLine(text, config.keyword)) continue; rows.push({ text: text, anchorId: kdocsEnsureMatchAnchor(node) }); } if (rows.length >= 10) { break; } } if (rows.length === 0) { const genericMatches = kdocsFindGenericDocMatchItems(config); if (genericMatches.length > 0) { rows.push.apply(rows, genericMatches); } } if (rows.length === 0 && settings.allowPlainTextFallback !== false) { const bodyText = document.body ? String(document.body.innerText || '') : ''; const lines = kdocsFilterKeywordLines(kdocsSplitTextLines(bodyText), config.keyword); rows.push.apply(rows, lines.slice(0, 20).map(function (line) { return { text: line, anchorId: '' }; })); } return kdocsBuildDocRowsResult(rows, '正文匹配'); } function kdocsExtractSearchResult(config) { const countText = kdocsTextOf(document.querySelector('.match-result-text')); const selectors = config.sourceType === 'doc' ? [ '.db-global-find-result .db-global-find-select-list .select-item-value', '.db-global-find-result .select-item-value', '.db-global-find-result [class*="select-item-value"]', '.db-global-find-result [class*="snippet"]', '.db-global-find-result [class*="content"]', '.db-global-find-result [class*="item"]', ] : [ '.db-global-find-result .db-global-find-select-list:not(.db-global-find-small-select-list) .select-item-value', '.db-global-find-result .db-global-find-select-list .select-item-value', '.db-global-find-result .select-item-value', '.db-global-find-result [class*="select-item-value"]', ]; const rows = kdocsCollectTexts(selectors).filter(function (text) { return text && text !== countText && text !== config.keyword; }); let uniqueRows = Array.from(new Set(rows)); const errorText = Array.from(document.querySelectorAll('.db-global-find-modal-panel *')) .map(function (node) { return kdocsTextOf(node); }) .find(function (text) { return text.indexOf('未找到') !== -1; }) || ''; if (config.sourceType === 'doc' && uniqueRows.length === 0) { const directResult = kdocsExtractDocDirectResult(config); if (directResult.rows.length > 0) { return directResult; } } return { countText: countText, rows: uniqueRows.slice(0, 50), errorText: errorText, }; } async function runKdocsAutoSearch(config) { if (window[KDOCS_RUNNING_FLAG]) return; window[KDOCS_RUNNING_FLAG] = true; renderKdocsSearchOverlay(config, { statusText: '正在自动搜索,请稍候...' }); try { for (let attempt = 0; attempt < 60; attempt += 1) { kdocsCloseLoginModal(); if (config.sourceType === 'doc') { const directAnchoredResult = kdocsExtractDocDirectResult(config, { allowPlainTextFallback: false }); if (directAnchoredResult.rows.length > 0) { renderKdocsSearchOverlay(config, directAnchoredResult); return; } const sectionResult = await kdocsScanDocSectionsForMatches(config); if (sectionResult.rows.length > 0) { renderKdocsSearchOverlay(config, sectionResult); return; } const apiResult = await kdocsExtractDocApiResult(config); if (apiResult.rows.length > 0) { renderKdocsSearchOverlay(config, apiResult); return; } const directResult = kdocsExtractDocDirectResult(config); if (directResult.rows.length > 0) { renderKdocsSearchOverlay(config, directResult); return; } } const findButton = kdocsFindByText('button, [role="button"], span', '查找', true); if (findButton && !document.querySelector('.db-global-find-modal-panel')) { kdocsClick(findButton); await kdocsSleep(300); } const input = document.querySelector( '.db-global-find-keyword-setting input, ' + '.db-global-find-keyword-setting textarea, ' + 'input[placeholder*="查找"], ' + 'textarea[placeholder*="查找"], ' + 'input[type="search"]' ); if (!input) { await kdocsSleep(300); continue; } kdocsSetInputValue(input, config.keyword); await kdocsSleep(150); const allSheetsLabel = config.sourceType === 'sheet' ? kdocsFindByText('label, span', '全部数据表', true) : null; if (allSheetsLabel) { kdocsClick(allSheetsLabel); await kdocsSleep(150); } const findAllButton = kdocsFindByText('button, [role="button"], span', '查找全部', true); if (findAllButton) { kdocsClick(findAllButton); await kdocsSleep(800); } for (let waitRound = 0; waitRound < 20; waitRound += 1) { const result = kdocsExtractSearchResult(config); if (result.countText || result.rows.length > 0 || result.errorText) { renderKdocsSearchOverlay(config, result); return; } await kdocsSleep(300); } } if (config.sourceType === 'doc') { const bodyLines = kdocsSplitTextLines(document.body ? String(document.body.innerText || '') : ''); if (bodyLines.length > 20) { renderKdocsSearchOverlay(config, { statusText: '正文已加载,但未找到匹配内容。', countText: '正文扫描: 0', rows: [], errorText: '', }); return; } } renderKdocsSearchOverlay(config, { statusText: '自动搜索超时,请点击 KDocs 页面的查找按钮再试一次。', countText: '', rows: [], errorText: '自动搜索超时', }); } finally { window[KDOCS_RUNNING_FLAG] = false; } } async function openYellowrabbitViewer(title, apiUrl) { const win = window.open('about:blank', '_blank'); if (!win) { window.open(apiUrl, '_blank', 'noopener'); return; } writeViewerWindow(win, buildYellowrabbitViewerHtml({ title: title, apiUrl: apiUrl, statusText: '加载中...', phase: 'loading', items: [], rawText: '', errorText: '', })); try { const response = await fetch(apiUrl, { method: 'GET', headers: { Accept: 'application/json' }, credentials: 'omit', }); if (!response.ok) { throw new Error('HTTP ' + response.status); } const text = await response.text(); let payload = null; try { payload = JSON.parse(text); } catch (_error) { throw new Error('返回不是合法 JSON'); } const items = extractYellowrabbitItems(payload); writeViewerWindow(win, buildYellowrabbitViewerHtml({ title: title, apiUrl: apiUrl, statusText: '共 ' + items.length + ' 条结果', phase: 'done', items: items, rawText: text, errorText: '', })); } catch (error) { writeViewerWindow(win, buildYellowrabbitViewerHtml({ title: title, apiUrl: apiUrl, statusText: '加载失败', phase: 'error', items: [], rawText: '', errorText: error && error.message ? error.message : '未知错误', })); } } function writeViewerWindow(win, html) { if (!win || win.closed) return; win.document.open(); win.document.write(html); win.document.close(); } function buildExternalAutoSearchUrl(baseUrl, mode, keyword) { const url = new URL(baseUrl); url.hash = new URLSearchParams({ doubanMode: mode, doubanKeyword: keyword, }).toString(); return url.toString(); } function openAutoSubmitWindow(config) { const url = String((config && config.url) || ''); if (!url) return; window.open(url, '_blank', 'noopener'); } function openLemonSearch(keyword) { openAutoSubmitWindow({ url: buildExternalAutoSearchUrl('https://lemonun.top/', 'lemonSearch', keyword), }); } function open6vMovieSearch(keyword) { openAutoSubmitWindow({ url: buildExternalAutoSearchUrl('https://www.6v520.tv/sousuo.html', 'sixvSearch', keyword), }); } function parseExternalAutoSearchConfig() { const hash = String(window.location.hash || ''); const hashParams = hash.startsWith('#') ? new URLSearchParams(hash.slice(1)) : null; const searchParams = new URLSearchParams(window.location.search || ''); const mode = (hashParams && hashParams.get('doubanMode')) || searchParams.get('doubanMode') || ''; const keyword = normalizeTitle((hashParams && hashParams.get('doubanKeyword')) || searchParams.get('doubanKeyword') || ''); if (!mode || !keyword) return null; return { mode: mode, keyword: keyword }; } function ensureHiddenFormField(form, name, value) { let input = form.querySelector('input[name="' + name + '"]'); if (!input) { input = document.createElement('input'); input.type = 'hidden'; input.name = name; form.appendChild(input); } input.value = String(value); } function scheduleExternalAutoSubmit(submitter) { let attempts = 0; function trySubmit() { if (submitter()) return; attempts += 1; if (attempts < 40) { window.setTimeout(trySubmit, 150); } } trySubmit(); } function runLemonAutoSearch(config) { scheduleExternalAutoSubmit(function () { const form = document.querySelector('#search-form'); const input = form && form.querySelector('input[name="keyword"]'); if (!form || !input) return false; kdocsSetInputValue(input, config.keyword); form.submit(); return true; }); } function runSixvAutoSearch(config) { scheduleExternalAutoSubmit(function () { const form = document.querySelector('#searchform'); const input = form && form.querySelector('input[name="keyboard"]'); if (!form || !input) return false; ensureHiddenFormField(form, 'show', 'title,smalltext'); ensureHiddenFormField(form, 'tempid', '1'); ensureHiddenFormField(form, 'x', '0'); ensureHiddenFormField(form, 'y', '0'); const tbnameField = form.querySelector('[name="tbname"]'); if (tbnameField) { tbnameField.value = 'article'; } else { ensureHiddenFormField(form, 'tbname', 'article'); } kdocsSetInputValue(input, config.keyword); form.submit(); return true; }); } function extractYellowrabbitItems(payload) { if (Array.isArray(payload)) return payload; if (payload && Array.isArray(payload.data)) return payload.data; return []; } function getHttpLinks(item) { const links = []; for (const key in item) { if (!Object.prototype.hasOwnProperty.call(item, key)) continue; const value = item[key]; if (typeof value === 'string' && /^https?:\/\//i.test(value)) { links.push({ key: key, url: value }); } } return links; } function renderYellowrabbitItems(items, phase) { if (phase === 'loading') { return '
正在请求数据,请稍候...
'; } if (!Array.isArray(items) || items.length === 0) { return '
暂无结果
'; } return items.map(function (item, index) { const idText = item && item.id != null ? String(item.id) : String(index + 1); const titleText = item && item.title ? String(item.title) : '未命名资源'; const titleDisplay = titleText.length > 85 ? titleText.slice(0, 85) + '...' : titleText; const links = getHttpLinks(item || {}); const linkHtml = links.length > 0 ? links.map(function (entry) { return '' + escapeHtml(entry.key) + ''; }).join('') : '无可用链接'; return '
' + '
' + '
' + '
#' + escapeHtml(idText) + '
' + '

' + escapeHtml(titleDisplay) + '

' + '
' + '' + '
' + '
'; }).join(''); } function buildYellowrabbitViewerHtml(options) { const title = options.title; const apiUrl = options.apiUrl; const statusText = options.statusText; const phase = options.phase; const items = options.items; const rawText = options.rawText; const errorText = options.errorText; const listHtml = renderYellowrabbitItems(items, phase); const errorHtml = errorText ? '
错误: ' + escapeHtml(errorText) + '
' : ''; const rawHtml = rawText ? escapeHtml(rawText) : ''; return ` yellowrabbit 搜索结果

yellowrabbit 搜索结果

关键词: ${escapeHtml(title)}
API: 打开原始 JSON
${escapeHtml(statusText)}
${errorHtml}
${listHtml}
原始 JSON
${rawHtml}
`; } function ensureStyle() { if (document.querySelector(`.${STYLE_CLASS}`)) return; const style = document.createElement('style'); style.className = STYLE_CLASS; style.textContent = ` .${CONTAINER_CLASS} { margin-top: 10px; display: flex; flex-wrap: wrap; gap: 8px; } .${BTN_CLASS} { padding: 3px 10px; border: 1px solid #1565C0; border-radius: 3px; text-decoration: none; color: #ffffff; font-size: 18px; font-weight: normal; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); transition: all 0.2s ease; display: inline-block; vertical-align: middle; line-height: 1.5; /* background: linear-gradient(to bottom, #2196F3, #1976D2); */ } .${BTN_CLASS}:hover { background: linear-gradient(to bottom, #64B5F6, #42A5F5); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15); transform: translateY(-1px); } .${BTN_CLASS}:active { transform: translateY(0); box-shadow: none; } .douban-source-manager-btn { color: #4d7c2f; border-style: dashed; border-color: #9fc37b; background: #f2f9e8; } .douban-source-manager-btn:hover { background: #e5f3d4; border-color: #86af61; } #${SOURCE_MANAGER_MODAL_ID}.douban-source-manager-mask { --douban-source-manager-accent: #5f9133; --douban-source-manager-accent-strong: #467121; --douban-source-manager-accent-soft: #edf6e3; --douban-source-manager-accent-soft-strong: #dcedc7; --douban-source-manager-border: #c8dbb6; --douban-source-manager-border-strong: #a7c48b; --douban-source-manager-text: #1c2c12; --douban-source-manager-muted: #5f7050; --douban-source-manager-panel-bg: linear-gradient(180deg, #f6faef 0%, #ffffff 18%, #f0f6e7 100%); --douban-source-manager-card-bg: linear-gradient(180deg, #ffffff 0%, #f4f8ea 100%); --douban-source-manager-card-hidden-bg: linear-gradient(180deg, #f4f6ef 0%, #eaf1df 100%); --douban-source-manager-shadow: 0 24px 54px rgba(43, 71, 20, 0.24); --douban-source-manager-ring: rgba(95, 145, 51, 0.22); position: fixed; inset: 0; z-index: 2147483647; display: flex; align-items: center; justify-content: center; padding: 20px; background: rgba(15, 23, 42, 0.45); backdrop-filter: blur(2px); } #${SOURCE_MANAGER_MODAL_ID} * { box-sizing: border-box; } #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-panel { width: min(860px, 100%); max-height: min(720px, calc(100vh - 40px)); display: flex; flex-direction: column; gap: 14px; padding: 18px; overflow: hidden; border: 1px solid var(--douban-source-manager-border); border-radius: 18px; background: var(--douban-source-manager-panel-bg); box-shadow: var(--douban-source-manager-shadow); color: var(--douban-source-manager-text); font: 14px/1.55 -apple-system, BlinkMacSystemFont, Segoe UI, PingFang SC, Hiragino Sans GB, Microsoft YaHei, sans-serif; } #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-header, #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-toolbar, #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-footer { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; } #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-title-wrap { min-width: 0; } #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-title { margin: 0; font-size: 22px; font-weight: 700; color: var(--douban-source-manager-accent-strong); } #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-subtitle, #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-summary { color: var(--douban-source-manager-muted); } #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-subtitle { margin-top: 4px; } #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-summary { padding: 10px 12px; border: 1px solid var(--douban-source-manager-border); border-radius: 12px; background: var(--douban-source-manager-accent-soft); } #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-close, #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-action, #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-toggle, #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-btn-primary, #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-btn-secondary { appearance: none; border: 1px solid var(--douban-source-manager-border); border-radius: 999px; background: var(--douban-source-manager-accent-soft); color: var(--douban-source-manager-accent-strong); font: inherit; font-weight: 600; cursor: pointer; transition: background 0.2s ease, border-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease; } #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-close:hover, #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-action:hover, #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-toggle:hover, #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-btn-primary:hover, #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-btn-secondary:hover { background: var(--douban-source-manager-accent-soft-strong); border-color: var(--douban-source-manager-border-strong); transform: translateY(-1px); } #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-close { width: 38px; height: 38px; padding: 0; font-size: 22px; line-height: 1; } #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-action, #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-toggle, #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-btn-primary, #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-btn-secondary { padding: 8px 14px; } #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-list { margin: 0; padding: 0; list-style: none; display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); align-content: start; gap: 12px; overflow: auto; } #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-item { position: relative; display: block; min-height: 150px; padding: 12px; border: 1px solid var(--douban-source-manager-border); border-radius: 14px; background: var(--douban-source-manager-card-bg); box-shadow: 0 10px 22px rgba(95, 145, 51, 0.1); cursor: move; transition: transform 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease, opacity 0.18s ease; } #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-item.is-hidden { background: var(--douban-source-manager-card-hidden-bg); border-style: dashed; } #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-item.is-dragging { opacity: 0.42; transform: scale(0.98); } #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-item.is-drop-target, #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-item.is-drop-after { border-color: var(--douban-source-manager-accent); box-shadow: 0 0 0 3px var(--douban-source-manager-ring), 0 14px 26px rgba(95, 145, 51, 0.18); } #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-item.is-drop-target { transform: translateY(-2px); } #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-item.is-drop-after { box-shadow: 0 0 0 3px var(--douban-source-manager-ring), inset 0 -4px 0 var(--douban-source-manager-accent), 0 14px 26px rgba(95, 145, 51, 0.18); } #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-index { min-width: 36px; padding: 4px 8px; border-radius: 999px; background: var(--douban-source-manager-accent); color: #ffffff; text-align: center; font-weight: 700; } #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-main { min-width: 0; height: 100%; display: grid; grid-template-rows: auto 1fr; gap: 8px; } #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-name-group { display: grid; gap: 6px; min-width: 0; } #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-item-top { display: flex; align-items: center; justify-content: space-between; gap: 8px; } #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-label { font-size: 15px; font-weight: 700; color: var(--douban-source-manager-text); word-break: break-word; } #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-meta { font-size: 12px; color: var(--douban-source-manager-muted); word-break: break-word; } #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-rename { width: 100%; padding: 8px 10px; border: 1px solid var(--douban-source-manager-border); border-radius: 10px; background: rgba(255, 255, 255, 0.92); color: var(--douban-source-manager-text); font: inherit; transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease; } #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-rename:focus { outline: none; border-color: var(--douban-source-manager-accent); box-shadow: 0 0 0 3px var(--douban-source-manager-ring); background: #ffffff; } #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-toggle.is-hidden, #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-btn-secondary { background: #ffffff; } #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-btn-primary { border-color: var(--douban-source-manager-accent-strong); background: linear-gradient(180deg, #78b44d 0%, #5f9133 100%); color: #ffffff; } #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-btn-primary:hover { background: linear-gradient(180deg, #89be61 0%, #4f7f2b 100%); border-color: var(--douban-source-manager-accent-strong); } @media (max-width: 720px) { #${SOURCE_MANAGER_MODAL_ID}.douban-source-manager-mask { padding: 12px; } #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-panel { padding: 14px; max-height: calc(100vh - 24px); } #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-list { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); } #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-item { min-height: 138px; } #${SOURCE_MANAGER_MODAL_ID} .douban-source-manager-toggle { padding-inline: 12px; } } `; document.head.appendChild(style); } function createButton(source, title, labelAliases) { const link = document.createElement('a'); const url = source.buildUrl(title); const displayLabel = getSourceDisplayLabel(source.label, labelAliases); link.href = url; link.target = '_blank'; link.rel = 'noopener noreferrer'; link.textContent = displayLabel; link.className = BTN_CLASS; link.title = displayLabel + '(右键隐藏该源)'; if (typeof source.onClick === 'function') { link.addEventListener('click', function (event) { event.preventDefault(); source.onClick({ title: title, url: url, event: event }); }); } link.addEventListener('contextmenu', function (event) { event.preventDefault(); const ok = window.confirm('隐藏源「' + displayLabel + '」?\n可通过“源设置”按钮恢复。'); if (!ok) return; hideSource(source.label); renderButtons(true); }); return link; } function createSourceManagerButton() { const manager = document.createElement('a'); manager.href = 'javascript:void(0)'; manager.target = '_self'; manager.textContent = '源设置'; manager.className = BTN_CLASS + ' douban-source-manager-btn'; manager.title = '管理隐藏源、排序和显示名称'; manager.addEventListener('click', function (event) { event.preventDefault(); openSourceManager(); }); return manager; } function removeOldContainer() { const old = document.querySelector(`.${CONTAINER_CLASS}`); if (old) old.remove(); } function renderButtons(force) { const h1Element = document.querySelector('h1'); if (!h1Element) return; const title = getMovieTitle(); if (!title) return; const searchKeyword = getSearchKeyword(title); if (!searchKeyword) return; if (!force && document.querySelector(`.${CONTAINER_CLASS}`)) return; ensureStyle(); removeOldContainer(); const container = document.createElement('div'); container.className = CONTAINER_CLASS; const hiddenSources = loadHiddenSources(); const labelAliases = loadSourceLabelAliases(); for (const source of getOrderedSources()) { if (hiddenSources.has(source.label)) continue; container.appendChild(createButton(source, searchKeyword, labelAliases)); } container.appendChild(createSourceManagerButton()); h1Element.insertAdjacentElement('afterend', container); } function scheduleRender() { if (renderTimer) { clearTimeout(renderTimer); } renderTimer = window.setTimeout(renderButtons, 120); } const host = window.location.hostname; const isKdocsHost = host === 'www.kdocs.cn' || host === 'appdocs.wpscdn.cn'; const isLemonHost = host === 'lemonun.top'; const isSixvHost = host === 'www.6v520.tv'; const kdocsConfig = isKdocsHost ? parseKdocsSearchConfig() : null; const externalAutoSearchConfig = (isLemonHost || isSixvHost) ? parseExternalAutoSearchConfig() : null; if (isKdocsHost) { if (kdocsConfig) { runKdocsAutoSearch(kdocsConfig); } return; } if (isLemonHost) { if (externalAutoSearchConfig && externalAutoSearchConfig.mode === 'lemonSearch') { runLemonAutoSearch(externalAutoSearchConfig); } return; } if (isSixvHost) { if (externalAutoSearchConfig && externalAutoSearchConfig.mode === 'sixvSearch') { runSixvAutoSearch(externalAutoSearchConfig); } return; } const observer = new MutationObserver(scheduleRender); observer.observe(document.body, { childList: true, subtree: true }); scheduleRender(); })();