单部屏蔽 · 番头屏蔽 · 关键字屏蔽 · 已下载过滤 · 批量导入导出 · 右下角导航
// ==UserScript==
// @name JAV Blade
// @namespace https://github.com/yourusername/jav-blade
// @version 3.2
// @author JAV Blade
// @description 单部屏蔽 · 番头屏蔽 · 关键字屏蔽 · 已下载过滤 · 批量导入导出 · 右下角导航
// @match https://javdb.com/*
// @match https://www.javbus.com/*
// @match https://javdb*.com/*
// @match https://*javbus*/*
// @grant GM_xmlhttpRequest
// @grant GM_openInTab
// @grant GM_registerMenuCommand
// @grant unsafeWindow
// @run-at document-idle
// ==/UserScript==
(function() {
'use strict';
// ============================================================
// 1. 存储工具
// ============================================================
const DY_WATCHED_KEY = 'jav_blade_watched_videos';
const DY_BLOCKED_KEY = 'jav_blade_blocked_videos';
const DY_BLOCKED_SERIES_KEY = 'jav_blade_blocked_series';
const DY_BLOCKED_KEYWORDS_KEY = 'jav_blade_blocked_keywords';
const DY_FILTER_KEY = 'jav_blade_filter_enabled';
const DY_BLOCK_SWITCH_KEY = 'jav_blade_block_switch_enabled';
function getDYWatched() {
try { return JSON.parse(localStorage.getItem(DY_WATCHED_KEY) || '[]'); } catch { return []; }
}
function getDYBlocked() {
try { return JSON.parse(localStorage.getItem(DY_BLOCKED_KEY) || '[]'); } catch { return []; }
}
function getDYBlockedSeries() {
try { return JSON.parse(localStorage.getItem(DY_BLOCKED_SERIES_KEY) || '[]'); } catch { return []; }
}
function getDYBlockedKeywords() {
try { return JSON.parse(localStorage.getItem(DY_BLOCKED_KEYWORDS_KEY) || '[]'); } catch { return []; }
}
function getDYFilter() {
return localStorage.getItem(DY_FILTER_KEY) === 'true';
}
function getDYBlockSwitch() {
return localStorage.getItem(DY_BLOCK_SWITCH_KEY) !== 'false';
}
function setDYFilter(enabled) {
localStorage.setItem(DY_FILTER_KEY, String(enabled));
}
function setDYBlockSwitch(enabled) {
localStorage.setItem(DY_BLOCK_SWITCH_KEY, String(enabled));
}
function setDYWatched(list) {
localStorage.setItem(DY_WATCHED_KEY, JSON.stringify(list));
}
function setDYBlocked(list) {
localStorage.setItem(DY_BLOCKED_KEY, JSON.stringify(list));
}
function setDYBlockedSeries(list) {
localStorage.setItem(DY_BLOCKED_SERIES_KEY, JSON.stringify(list));
}
function setDYBlockedKeywords(list) {
localStorage.setItem(DY_BLOCKED_KEYWORDS_KEY, JSON.stringify(list));
}
function toggleDYWatched(videoId) {
let list = getDYWatched();
const idx = list.indexOf(videoId);
if (idx > -1) list.splice(idx, 1);
else list.push(videoId);
setDYWatched(list);
return list;
}
function toggleDYBlocked(videoId) {
let list = getDYBlocked();
const idx = list.indexOf(videoId);
if (idx > -1) list.splice(idx, 1);
else list.push(videoId);
setDYBlocked(list);
return list;
}
function toggleDYBlockedSeries(series) {
const lower = series.toLowerCase();
let list = getDYBlockedSeries();
const idx = list.indexOf(lower);
if (idx > -1) list.splice(idx, 1);
else list.push(lower);
setDYBlockedSeries(list);
return list;
}
function toggleDYBlockedKeyword(keyword) {
const lower = keyword.toLowerCase().trim();
let list = getDYBlockedKeywords();
const idx = list.indexOf(lower);
if (idx > -1) list.splice(idx, 1);
else list.push(lower);
setDYBlockedKeywords(list);
return list;
}
function extractDYSeries(carNum) {
if (!carNum) return null;
const match = carNum.match(/^([A-Z]{2,})/i);
if (match) return match[1].toLowerCase();
const vrMatch = carNum.match(/^(VR)/i);
if (vrMatch) return vrMatch[1].toLowerCase();
return null;
}
// ============================================================
// 2. 获取影片信息
// ============================================================
function getCarNumFromItem(item) {
const strong = item.querySelector('.video-title strong');
if (strong) return strong.textContent.trim();
const link = item.querySelector('a');
if (link) {
const href = link.href || link.getAttribute('href') || '';
let match = href.match(/\/(?:v\/)?([A-Za-z]{2,}[-_]?\d+)/i);
if (match) return match[1].toUpperCase();
match = href.match(/\/(\d{6,}[-_]\d{3,})/);
if (match) return match[1];
match = href.match(/\/(\d{8,})/);
if (match) return match[1];
const parts = href.split('/');
const last = parts[parts.length - 1].split(/[?#]/)[0];
if (/[A-Z]{2,}[-_]?\d+/i.test(last)) return last.toUpperCase();
if (/\d{6,}[-_]\d{3,}/.test(last)) return last;
if (/\d{8,}/.test(last)) return last;
}
const img = item.querySelector('img');
if (img) {
const text = (img.title || img.alt || '');
let match = text.match(/[A-Z]{2,}[-_]?\d+/i);
if (match) return match[0].toUpperCase();
match = text.match(/\d{6,}[-_]\d{3,}/);
if (match) return match[0];
match = text.match(/\d{8,}/);
if (match) return match[0];
}
const text = item.textContent;
let match = text.match(/[A-Z]{2,}[-_]?\d+/i);
if (match) return match[0].toUpperCase();
match = text.match(/\d{6,}[-_]\d{3,}/);
if (match) return match[0];
match = text.match(/\d{8,}/);
if (match) return match[0];
return '';
}
function getMovieTitle(item) {
const titleEl = item.querySelector('.video-title');
if (titleEl) return titleEl.textContent.trim();
const link = item.querySelector('a');
if (link) return link.textContent.trim() || link.title || '';
const img = item.querySelector('img');
if (img) return img.title || img.alt || '';
return '';
}
function getMovieItems() {
const selectors = [
'.movie-list .item',
'.masonry .item',
'.waterfall .item',
'#waterfall .item',
'[class*="movie"] [class*="item"]',
'[class*="video"] [class*="item"]'
];
for (const selector of selectors) {
const items = document.querySelectorAll(selector);
if (items.length > 0) return items;
}
return [];
}
// ============================================================
// 3. 核心过滤逻辑
// ============================================================
function applyDYFilter() {
const filterEnabled = getDYFilter();
const blockEnabled = getDYBlockSwitch();
const watchedList = getDYWatched();
const blockedVideos = getDYBlocked();
const blockedSeries = getDYBlockedSeries();
const blockedKeywords = getDYBlockedKeywords();
const items = getMovieItems();
if (items.length === 0) return;
let total = 0, hidden = 0;
const toHide = [];
items.forEach(item => {
total++;
const carNum = getCarNumFromItem(item);
const title = getMovieTitle(item);
const series = extractDYSeries(carNum);
let shouldHide = false;
// 1. 番头屏蔽(系列屏蔽)
if (blockEnabled && series && blockedSeries.includes(series)) {
shouldHide = true;
}
// 2. 单部屏蔽
if (!shouldHide && blockEnabled && carNum && blockedVideos.includes(carNum)) {
shouldHide = true;
}
// 3. 关键字屏蔽
if (!shouldHide && blockEnabled && blockedKeywords.length > 0) {
const checkText = (carNum + ' ' + title).toLowerCase();
for (const keyword of blockedKeywords) {
if (checkText.includes(keyword)) {
shouldHide = true;
break;
}
}
}
// 4. 已下载过滤
if (!shouldHide && filterEnabled && carNum && watchedList.includes(carNum)) {
shouldHide = true;
}
if (shouldHide) {
toHide.push(item);
hidden++;
}
});
if (toHide.length > 0) {
const hideClass = 'dy-filter-hidden';
if (!document.getElementById('dy-filter-style')) {
const style = document.createElement('style');
style.id = 'dy-filter-style';
style.textContent = `
.dy-filter-hidden {
display: none !important;
visibility: hidden !important;
height: 0 !important;
min-height: 0 !important;
padding: 0 !important;
margin: 0 !important;
border: none !important;
overflow: hidden !important;
position: absolute !important;
pointer-events: none !important;
opacity: 0 !important;
}
`;
document.head.appendChild(style);
}
toHide.forEach(item => {
item.classList.add('dy-filter-hidden');
item.style.display = '';
item.style.visibility = '';
item.style.height = '';
item.style.minHeight = '';
item.style.padding = '';
item.style.margin = '';
item.style.border = '';
item.style.overflow = '';
item.style.position = '';
item.style.pointerEvents = '';
item.style.opacity = '';
});
items.forEach(item => {
if (!toHide.includes(item)) {
item.classList.remove('dy-filter-hidden');
item.style.display = '';
item.style.visibility = '';
}
});
} else {
items.forEach(item => {
item.classList.remove('dy-filter-hidden');
item.style.display = '';
item.style.visibility = '';
});
}
fixWaterfallLayout();
const statsEl = document.getElementById('dy-stats');
if (statsEl) {
const visible = total - hidden;
const blockStatus = blockEnabled ? '🟢' : '🔴';
statsEl.textContent = `📊 显示: ${visible}/${total} | ${blockStatus}屏蔽: ${blockedVideos.length}部 ${blockedSeries.length}番头 ${blockedKeywords.length}关键字 | 已下载: ${watchedList.length}`;
}
}
function fixWaterfallLayout() {
if (typeof $.fn.masonry === 'function') {
$('.masonry').masonry('layout');
}
if (typeof $.fn.isotope === 'function') {
$('.masonry').isotope('layout');
}
window.dispatchEvent(new Event('resize'));
const containers = document.querySelectorAll('.masonry, .waterfall, #waterfall, .movie-list');
containers.forEach(container => {
const currentHeight = container.style.height;
container.style.height = 'auto';
void container.offsetHeight;
if (currentHeight) {
container.style.height = currentHeight;
}
});
}
// ============================================================
// 4. 批量导入解析函数
// ============================================================
function parseTXTFile(file) {
return new Promise((resolve, reject) => {
if (!file) { reject('未选择文件'); return; }
if (!file.name.endsWith('.txt')) { reject('请上传 .txt 文件'); return; }
const reader = new FileReader();
reader.onload = function(e) {
const text = e.target.result;
const lines = text.split('\n')
.map(line => line.trim())
.filter(line => line.length > 0);
const results = lines.map(line => {
let carNum = null;
let url = line;
let match = line.match(/\/(?:v\/)?([A-Za-z]{2,}[-_]?\d+)/i);
if (match) { carNum = match[1].toUpperCase(); }
if (!carNum) {
match = line.match(/(\d{6,}[-_]\d{3,})/);
if (match) carNum = match[1];
}
if (!carNum) {
match = line.match(/(\d{8,})/);
if (match) carNum = match[1];
}
if (!carNum) {
match = line.match(/[A-Z]{2,}[-_]?\d+/i);
if (match) carNum = match[0].toUpperCase();
}
if (!carNum) {
match = line.match(/FC2[-_]?(?:PPV-)?(\d+)/i);
if (match) carNum = 'FC2-' + match[1];
}
return {
original: line,
carNum: carNum,
url: line.includes('http') ? line : null,
isKeyword: !carNum
};
});
const validItems = results.filter(item => item.carNum);
const keywordItems = results.filter(item => !item.carNum);
const invalidItems = results.filter(item => !item.carNum && item.original.length < 3);
resolve({
valid: validItems,
keywordItems: keywordItems.filter(item => item.original.length >= 2),
invalid: invalidItems,
total: results.length
});
};
reader.onerror = function() {
reject('读取文件失败');
};
reader.readAsText(file);
});
}
// ============================================================
// 5. 导入屏蔽目录
// ============================================================
function openBlockImportDialog() {
const overlay = document.createElement('div');
overlay.id = 'dy-block-import-overlay';
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.7);
z-index: 9999999;
display: flex;
justify-content: center;
align-items: center;
`;
const dialog = document.createElement('div');
dialog.style.cssText = `
background: #1a1a2e;
border-radius: 12px;
padding: 30px;
max-width: 700px;
width: 90%;
max-height: 85vh;
overflow-y: auto;
border: 1px solid #e74c3c;
color: #ecf0f1;
font-family: 'Segoe UI', Arial, sans-serif;
`;
dialog.innerHTML = `
<h2 style="color:#e74c3c;margin-bottom:10px;">🚫 导入屏蔽目录</h2>
<p style="color:#aaa;font-size:13px;margin-bottom:15px;">
上传TXT文件,每行一个链接、番号或关键字。<br>
<span style="color:#f39c12;">📌 番号/链接 → 单部屏蔽(只屏蔽当前这一部)</span><br>
<span style="color:#e67e22;">📌 番号/链接 + 标记【番头】→ 屏蔽整个系列(如 SSIS-001 → 屏蔽所有 SSIS)</span><br>
<span style="color:#3498db;">📌 关键字 → 屏蔽所有包含该关键字的影片</span>
</p>
<div style="margin-bottom:15px;background:#2c3e50;padding:15px;border-radius:8px;">
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center;">
<input type="file" id="dy-block-file-input" accept=".txt" style="color:#aaa;font-size:13px;">
<button id="dy-block-import-btn" style="padding:6px 20px;border:none;border-radius:4px;background:#e74c3c;color:#fff;cursor:pointer;font-size:13px;font-weight:bold;">🚫 导入屏蔽</button>
</div>
<div style="margin-top:8px;font-size:11px;color:#888;">
💡 提示:在番号后面加上 <span style="color:#e67e22;font-weight:bold;">【番头】</span> 标记可屏蔽整个系列,例如:<span style="color:#aaa;">SSIS-001 【番头】</span>
</div>
</div>
<div id="dy-block-preview" style="display:none;margin-bottom:15px;background:#2c3e50;padding:15px;border-radius:8px;max-height:300px;overflow-y:auto;">
<div style="display:flex;justify-content:space-between;margin-bottom:10px;">
<span id="dy-block-count" style="color:#e74c3c;font-weight:bold;">找到 0 个</span>
<span id="dy-block-result" style="color:#f39c12;"></span>
</div>
<div id="dy-block-list" style="font-size:12px;color:#aaa;word-break:break-all;"></div>
</div>
<div id="dy-block-result-box" style="display:none;margin-bottom:15px;background:#2c3e50;padding:15px;border-radius:8px;">
<div id="dy-block-result-text" style="color:#e74c3c;font-weight:bold;"></div>
<div id="dy-block-error-list" style="font-size:12px;color:#f39c12;margin-top:5px;"></div>
</div>
<div style="display:flex;gap:10px;justify-content:flex-end;margin-top:10px;">
<button id="dy-close-block-import" style="padding:8px 24px;border:none;border-radius:6px;background:#34495e;color:#fff;cursor:pointer;font-size:14px;">关闭</button>
</div>
`;
overlay.appendChild(dialog);
document.body.appendChild(overlay);
const fileInput = document.getElementById('dy-block-file-input');
const importBtn = document.getElementById('dy-block-import-btn');
const preview = document.getElementById('dy-block-preview');
const countEl = document.getElementById('dy-block-count');
const listEl = document.getElementById('dy-block-list');
const resultBox = document.getElementById('dy-block-result-box');
const resultText = document.getElementById('dy-block-result-text');
const errorList = document.getElementById('dy-block-error-list');
let parsedValid = [];
let parsedSeries = [];
let parsedKeywords = [];
let parsedInvalid = [];
fileInput.addEventListener('change', async function(e) {
const file = this.files[0];
if (!file) return;
try {
const result = await parseTXTFile(file);
parsedValid = [];
parsedSeries = [];
parsedKeywords = [];
parsedInvalid = [];
result.valid.forEach(item => {
if (item.original.includes('【番头】') || item.original.includes('[番头]')) {
parsedSeries.push(item);
} else {
parsedValid.push(item);
}
});
parsedKeywords = result.keywordItems;
parsedInvalid = result.invalid;
preview.style.display = 'block';
let countMsg = `✅ 识别到 ${parsedValid.length} 个单部屏蔽`;
if (parsedSeries.length > 0) {
countMsg += `,📦 ${parsedSeries.length} 个番头屏蔽`;
}
if (parsedKeywords.length > 0) {
countMsg += `,🔑 ${parsedKeywords.length} 个关键字`;
}
if (parsedInvalid.length > 0) {
countMsg += `,❌ ${parsedInvalid.length} 个无效`;
}
countEl.textContent = countMsg;
if (parsedValid.length > 0 || parsedSeries.length > 0 || parsedKeywords.length > 0) {
let listHtml = '';
if (parsedValid.length > 0) {
const videoList = parsedValid.map(item => item.carNum);
const uniqueVideos = [...new Set(videoList)];
listHtml += `<div style="color:#f39c12;font-weight:bold;margin-bottom:4px;">🚫 单部屏蔽 (${uniqueVideos.length}部):</div>`;
listHtml += uniqueVideos.slice(0, 15).map(v =>
`<div style="padding-left:12px;">• ${v}</div>`
).join('');
if (uniqueVideos.length > 15) {
listHtml += `<div style="padding-left:12px;color:#666;">... 还有 ${uniqueVideos.length - 15} 部</div>`;
}
}
if (parsedSeries.length > 0) {
const seriesList = parsedSeries.map(item => {
const s = extractDYSeries(item.carNum);
return s ? s.toUpperCase() : item.carNum;
});
const uniqueSeries = [...new Set(seriesList)];
listHtml += `<div style="color:#e67e22;font-weight:bold;margin-top:8px;margin-bottom:4px;">📦 番头屏蔽 (${uniqueSeries.length}个系列):</div>`;
listHtml += uniqueSeries.slice(0, 15).map(s =>
`<div style="padding-left:12px;">• ${s}*</div>`
).join('');
if (uniqueSeries.length > 15) {
listHtml += `<div style="padding-left:12px;color:#666;">... 还有 ${uniqueSeries.length - 15} 个</div>`;
}
}
if (parsedKeywords.length > 0) {
const keywordList = parsedKeywords.map(item => item.original);
const uniqueKeywords = [...new Set(keywordList)];
listHtml += `<div style="color:#3498db;font-weight:bold;margin-top:8px;margin-bottom:4px;">🔑 屏蔽关键字 (${uniqueKeywords.length}个):</div>`;
listHtml += uniqueKeywords.slice(0, 15).map(k =>
`<div style="padding-left:12px;">• "${k}"</div>`
).join('');
if (uniqueKeywords.length > 15) {
listHtml += `<div style="padding-left:12px;color:#666;">... 还有 ${uniqueKeywords.length - 15} 个</div>`;
}
}
listEl.innerHTML = listHtml;
}
if (parsedInvalid.length > 0) {
listEl.innerHTML += `<div style="color:#f39c12;margin-top:5px;">⚠️ 无效: ${parsedInvalid.slice(0,5).map(i => i.original).join(', ')}${parsedInvalid.length > 5 ? ` ... 等 ${parsedInvalid.length} 个` : ''}</div>`;
}
resultBox.style.display = 'none';
} catch (err) {
showToast('⚠️ ' + err);
this.value = '';
}
});
importBtn.addEventListener('click', function() {
if (parsedValid.length === 0 && parsedSeries.length === 0 && parsedKeywords.length === 0) {
showToast('⚠️ 没有可导入的内容,请先选择文件');
return;
}
const currentBlocked = getDYBlocked();
const currentSeries = getDYBlockedSeries();
const currentKeywords = getDYBlockedKeywords();
let videoSuccess = 0, videoFail = 0;
let seriesSuccess = 0, seriesFail = 0;
let keywordSuccess = 0, keywordFail = 0;
const failed = [];
// 单部屏蔽
const videoList = parsedValid.map(item => item.carNum);
const uniqueVideos = [...new Set(videoList)];
uniqueVideos.forEach(video => {
if (!currentBlocked.includes(video)) {
currentBlocked.push(video);
videoSuccess++;
} else {
videoFail++;
failed.push(video + ' (已存在)');
}
});
// 番头屏蔽
const seriesList = parsedSeries.map(item => {
const s = extractDYSeries(item.carNum);
return s || item.carNum;
});
const uniqueSeries = [...new Set(seriesList)];
uniqueSeries.forEach(series => {
const lower = series.toLowerCase();
if (!currentSeries.includes(lower)) {
currentSeries.push(lower);
seriesSuccess++;
} else {
seriesFail++;
failed.push(series.toUpperCase() + '* (已存在)');
}
});
// 关键字屏蔽
const keywordList = parsedKeywords.map(item => item.original.toLowerCase().trim());
const uniqueKeywords = [...new Set(keywordList)];
uniqueKeywords.forEach(keyword => {
if (!currentKeywords.includes(keyword)) {
currentKeywords.push(keyword);
keywordSuccess++;
} else {
keywordFail++;
failed.push('"' + keyword + '" (已存在)');
}
});
setDYBlocked(currentBlocked);
setDYBlockedSeries(currentSeries);
setDYBlockedKeywords(currentKeywords);
resultBox.style.display = 'block';
let resultMsg = `✅ 单部: ${videoSuccess} 部`;
if (videoFail > 0) resultMsg += `,❌ ${videoFail} 部已存在`;
resultMsg += ` | 📦 番头: ${seriesSuccess} 个`;
if (seriesFail > 0) resultMsg += `,❌ ${seriesFail} 个已存在`;
resultMsg += ` | 🔑 关键字: ${keywordSuccess} 个`;
if (keywordFail > 0) resultMsg += `,❌ ${keywordFail} 个已存在`;
resultText.textContent = resultMsg;
if (failed.length > 0 && failed.length <= 10) {
errorList.textContent = '失败: ' + failed.join('; ');
} else if (failed.length > 10) {
errorList.textContent = '失败: ' + failed.slice(0, 10).join('; ') + ` ... 等 ${failed.length} 个`;
} else {
errorList.textContent = '';
}
setDYBlockSwitch(true);
showToast(`✅ 导入完成!单部 ${videoSuccess} 部,番头 ${seriesSuccess} 个,关键字 ${keywordSuccess} 个`);
applyDYFilter();
fileInput.value = '';
parsedValid = [];
parsedSeries = [];
parsedKeywords = [];
parsedInvalid = [];
});
document.getElementById('dy-close-block-import').onclick = () => overlay.remove();
overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); };
}
// ============================================================
// 6. 导入已下载目录
// ============================================================
function openWatchedImportDialog() {
const overlay = document.createElement('div');
overlay.id = 'dy-watched-import-overlay';
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.7);
z-index: 9999999;
display: flex;
justify-content: center;
align-items: center;
`;
const dialog = document.createElement('div');
dialog.style.cssText = `
background: #1a1a2e;
border-radius: 12px;
padding: 30px;
max-width: 700px;
width: 90%;
max-height: 85vh;
overflow-y: auto;
border: 1px solid #3498db;
color: #ecf0f1;
font-family: 'Segoe UI', Arial, sans-serif;
`;
dialog.innerHTML = `
<h2 style="color:#3498db;margin-bottom:10px;">📥️ 导入已下载目录</h2>
<p style="color:#aaa;font-size:13px;margin-bottom:15px;">
上传TXT文件,每行一个链接或番号。<br>
系统会自动识别番号并标记为"已下载"
</p>
<div style="margin-bottom:15px;background:#2c3e50;padding:15px;border-radius:8px;">
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center;">
<input type="file" id="dy-watched-file-input" accept=".txt" style="color:#aaa;font-size:13px;">
<button id="dy-watched-import-btn" style="padding:6px 20px;border:none;border-radius:4px;background:#3498db;color:#fff;cursor:pointer;font-size:13px;font-weight:bold;">📥️ 导入已下载</button>
</div>
</div>
<div id="dy-watched-preview" style="display:none;margin-bottom:15px;background:#2c3e50;padding:15px;border-radius:8px;max-height:300px;overflow-y:auto;">
<div style="display:flex;justify-content:space-between;margin-bottom:10px;">
<span id="dy-watched-count" style="color:#3498db;font-weight:bold;">找到 0 个</span>
<span id="dy-watched-result" style="color:#f39c12;"></span>
</div>
<div id="dy-watched-list" style="font-size:12px;color:#aaa;word-break:break-all;"></div>
</div>
<div id="dy-watched-result-box" style="display:none;margin-bottom:15px;background:#2c3e50;padding:15px;border-radius:8px;">
<div id="dy-watched-result-text" style="color:#3498db;font-weight:bold;"></div>
<div id="dy-watched-error-list" style="font-size:12px;color:#f39c12;margin-top:5px;"></div>
</div>
<div style="display:flex;gap:10px;justify-content:flex-end;margin-top:10px;">
<button id="dy-close-watched-import" style="padding:8px 24px;border:none;border-radius:6px;background:#34495e;color:#fff;cursor:pointer;font-size:14px;">关闭</button>
</div>
`;
overlay.appendChild(dialog);
document.body.appendChild(overlay);
const fileInput = document.getElementById('dy-watched-file-input');
const importBtn = document.getElementById('dy-watched-import-btn');
const preview = document.getElementById('dy-watched-preview');
const countEl = document.getElementById('dy-watched-count');
const listEl = document.getElementById('dy-watched-list');
const resultBox = document.getElementById('dy-watched-result-box');
const resultText = document.getElementById('dy-watched-result-text');
const errorList = document.getElementById('dy-watched-error-list');
let parsedValid = [];
let parsedInvalid = [];
fileInput.addEventListener('change', async function(e) {
const file = this.files[0];
if (!file) return;
try {
const result = await parseTXTFile(file);
parsedValid = result.valid;
parsedInvalid = result.invalid;
preview.style.display = 'block';
countEl.textContent = `✅ 识别到 ${parsedValid.length} 个番号${parsedInvalid.length > 0 ? `,❌ ${parsedInvalid.length} 个无效` : ''}`;
if (parsedValid.length > 0) {
const listHtml = parsedValid.slice(0, 20).map(item =>
`<div>• ${item.carNum} ${item.url ? '(链接)' : ''}</div>`
).join('');
listEl.innerHTML = listHtml + (parsedValid.length > 20 ? `<div style="color:#666;">... 还有 ${parsedValid.length - 20} 个</div>` : '');
}
if (parsedInvalid.length > 0) {
listEl.innerHTML += `<div style="color:#f39c12;margin-top:5px;">⚠️ 无效: ${parsedInvalid.slice(0,5).map(i => i.original).join(', ')}${parsedInvalid.length > 5 ? ` ... 等 ${parsedInvalid.length} 个` : ''}</div>`;
}
resultBox.style.display = 'none';
} catch (err) {
showToast('⚠️ ' + err);
this.value = '';
}
});
importBtn.addEventListener('click', function() {
if (parsedValid.length === 0) {
showToast('⚠️ 没有可导入的番号,请先选择文件');
return;
}
const current = getDYWatched();
let successCount = 0;
let failCount = 0;
const failed = [];
parsedValid.forEach(item => {
if (!current.includes(item.carNum)) {
current.push(item.carNum);
successCount++;
} else {
failCount++;
failed.push(item.carNum + ' (已存在)');
}
});
setDYWatched(current);
resultBox.style.display = 'block';
resultText.textContent = `✅ 成功导入 ${successCount} 个已下载${failCount > 0 ? `,❌ ${failCount} 个已存在` : ''}`;
if (failed.length > 0) {
errorList.textContent = '已存在: ' + failed.slice(0, 10).join('; ') + (failed.length > 10 ? ` ... 等 ${failed.length} 个` : '');
} else {
errorList.textContent = '';
}
showToast(`✅ 已下载导入完成,成功 ${successCount} 个`);
applyDYFilter();
fileInput.value = '';
parsedValid = [];
parsedInvalid = [];
});
document.getElementById('dy-close-watched-import').onclick = () => overlay.remove();
overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); };
}
// ============================================================
// 7. 导出数据对话框
// ============================================================
function openExportDialog() {
const watched = getDYWatched();
const blocked = getDYBlocked();
const blockedSeries = getDYBlockedSeries();
const keywords = getDYBlockedKeywords();
const overlay = document.createElement('div');
overlay.id = 'dy-export-overlay';
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.7);
z-index: 9999999;
display: flex;
justify-content: center;
align-items: center;
`;
const dialog = document.createElement('div');
dialog.style.cssText = `
background: #1a1a2e;
border-radius: 12px;
padding: 30px;
max-width: 500px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
border: 1px solid #2ecc71;
color: #ecf0f1;
font-family: 'Segoe UI', Arial, sans-serif;
`;
dialog.innerHTML = `
<h2 style="color:#2ecc71;margin-bottom:15px;">📤 导出数据</h2>
<p style="color:#aaa;font-size:13px;margin-bottom:15px;">
选择要导出的数据类型,导出为 TXT 文件,每行一条记录。
</p>
<div style="margin-bottom:15px;background:#2c3e50;padding:15px;border-radius:8px;">
<div style="margin-bottom:10px;">
<label style="cursor:pointer;display:flex;align-items:center;gap:10px;">
<input type="checkbox" id="dy-export-watched" checked>
<span>📥️ 已下载列表 (<span id="dy-export-watched-count">${watched.length}</span>)</span>
</label>
</div>
<div style="margin-bottom:10px;">
<label style="cursor:pointer;display:flex;align-items:center;gap:10px;">
<input type="checkbox" id="dy-export-blocked" checked>
<span>🚫 单部屏蔽列表 (<span id="dy-export-blocked-count">${blocked.length}</span>)</span>
</label>
</div>
<div style="margin-bottom:10px;">
<label style="cursor:pointer;display:flex;align-items:center;gap:10px;">
<input type="checkbox" id="dy-export-series" checked>
<span>📦 番头屏蔽列表 (<span id="dy-export-series-count">${blockedSeries.length}</span>)</span>
</label>
</div>
<div>
<label style="cursor:pointer;display:flex;align-items:center;gap:10px;">
<input type="checkbox" id="dy-export-keywords" checked>
<span>🔑 屏蔽关键字列表 (<span id="dy-export-keywords-count">${keywords.length}</span>)</span>
</label>
</div>
</div>
<div style="display:flex;gap:10px;justify-content:flex-end;margin-top:10px;">
<button id="dy-export-do-btn" style="padding:8px 24px;border:none;border-radius:6px;background:#2ecc71;color:#fff;cursor:pointer;font-size:14px;font-weight:bold;">📤 导出</button>
<button id="dy-close-export" style="padding:8px 24px;border:none;border-radius:6px;background:#34495e;color:#fff;cursor:pointer;font-size:14px;">关闭</button>
</div>
`;
overlay.appendChild(dialog);
document.body.appendChild(overlay);
document.getElementById('dy-close-export').onclick = () => overlay.remove();
overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); };
document.getElementById('dy-export-do-btn').onclick = function() {
const exportWatched = document.getElementById('dy-export-watched').checked;
const exportBlocked = document.getElementById('dy-export-blocked').checked;
const exportSeries = document.getElementById('dy-export-series').checked;
const exportKeywords = document.getElementById('dy-export-keywords').checked;
if (!exportWatched && !exportBlocked && !exportSeries && !exportKeywords) {
showToast('⚠️ 请至少选择一项要导出的数据');
return;
}
let content = '';
const timestamp = new Date().toLocaleString('zh-CN');
content += `# JAV Blade 导出数据\n`;
content += `# 导出时间: ${timestamp}\n`;
content += `# 注: 每行一条记录,可直接用于导入\n`;
content += `# 番头屏蔽导入时请在番号后加 【番头】 标记\n\n`;
if (exportWatched && watched.length > 0) {
content += `# ========== 已下载列表 (${watched.length}条) ==========\n`;
content += watched.join('\n');
content += '\n\n';
} else if (exportWatched) {
content += `# 已下载列表: 无数据\n\n`;
}
if (exportBlocked && blocked.length > 0) {
content += `# ========== 单部屏蔽列表 (${blocked.length}条) ==========\n`;
content += blocked.join('\n');
content += '\n\n';
} else if (exportBlocked) {
content += `# 单部屏蔽列表: 无数据\n\n`;
}
if (exportSeries && blockedSeries.length > 0) {
content += `# ========== 番头屏蔽列表 (${blockedSeries.length}条) ==========\n`;
content += blockedSeries.map(s => s.toUpperCase() + ' 【番头】').join('\n');
content += '\n\n';
} else if (exportSeries) {
content += `# 番头屏蔽列表: 无数据\n\n`;
}
if (exportKeywords && keywords.length > 0) {
content += `# ========== 屏蔽关键字列表 (${keywords.length}条) ==========\n`;
content += keywords.join('\n');
content += '\n\n';
} else if (exportKeywords) {
content += `# 屏蔽关键字列表: 无数据\n\n`;
}
const dateStr = new Date().toISOString().slice(0, 10);
const fileName = `JAV_Blade_导出数据_${dateStr}.txt`;
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = fileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
showToast(`✅ 导出成功!共导出 ${(exportWatched ? watched.length : 0) + (exportBlocked ? blocked.length : 0) + (exportSeries ? blockedSeries.length : 0) + (exportKeywords ? keywords.length : 0)} 条数据`);
overlay.remove();
};
}
// ============================================================
// 8. 添加按钮到每个影片
// ============================================================
function addDYButtons() {
const items = getMovieItems();
items.forEach(item => {
if (item.querySelector('.dy-btn-group')) return;
const carNum = getCarNumFromItem(item);
if (!carNum) return;
const title = getMovieTitle(item);
const blockEnabled = getDYBlockSwitch();
const series = extractDYSeries(carNum);
const isSeriesBlocked = series ? getDYBlockedSeries().includes(series) : false;
const isBlocked = getDYBlocked().includes(carNum);
const isWatched = getDYWatched().includes(carNum);
const blockedKeywords = getDYBlockedKeywords();
let isKeywordBlocked = false;
if (blockedKeywords.length > 0) {
const checkText = (carNum + ' ' + title).toLowerCase();
for (const kw of blockedKeywords) {
if (checkText.includes(kw)) {
isKeywordBlocked = true;
break;
}
}
}
const isBlockedDisplay = isBlocked || isSeriesBlocked || isKeywordBlocked;
const btnGroup = document.createElement('div');
btnGroup.className = 'dy-btn-group';
btnGroup.style.cssText = 'display:flex;gap:6px;margin-top:6px;flex-wrap:wrap;padding:4px 0;';
// 屏蔽番头按钮
const seriesBtn = document.createElement('button');
seriesBtn.textContent = isSeriesBlocked ? '✅ 番头已屏蔽' : '📦 屏蔽番头';
seriesBtn.style.cssText = `padding:2px 10px;font-size:11px;border:none;border-radius:4px;background:${isSeriesBlocked ? '#95a5a6' : '#e67e22'};color:#fff;cursor:pointer;font-weight:bold;`;
seriesBtn.title = blockEnabled ? '屏蔽整个系列(所有同番头影片)' : '⚠️ 屏蔽总开关已关闭';
seriesBtn.style.opacity = blockEnabled ? '1' : '0.5';
seriesBtn.onclick = (e) => {
e.stopPropagation();
e.preventDefault();
if (!blockEnabled) {
showToast('⚠️ 屏蔽总开关已关闭,请先开启');
return;
}
if (!carNum || !series) {
showToast('无法识别番头');
return;
}
const nowBlocked = getDYBlockedSeries().includes(series);
toggleDYBlockedSeries(series);
showToast(nowBlocked ? `已解除番头屏蔽: ${series.toUpperCase()}*` : `已屏蔽番头: ${series.toUpperCase()}*`);
applyDYFilter();
const isNowBlocked = getDYBlockedSeries().includes(series);
seriesBtn.textContent = isNowBlocked ? '✅ 番头已屏蔽' : '📦 屏蔽番头';
seriesBtn.style.background = isNowBlocked ? '#95a5a6' : '#e67e22';
};
btnGroup.appendChild(seriesBtn);
// 屏蔽单部按钮
const blockBtn = document.createElement('button');
blockBtn.textContent = isBlocked ? '✅ 已屏蔽' : '🚫 屏蔽这部';
blockBtn.style.cssText = `padding:2px 10px;font-size:11px;border:none;border-radius:4px;background:${isBlocked ? '#95a5a6' : '#e74c3c'};color:#fff;cursor:pointer;font-weight:bold;`;
blockBtn.title = blockEnabled ? '点击屏蔽/解除屏蔽当前这部影片' : '⚠️ 屏蔽总开关已关闭';
blockBtn.style.opacity = blockEnabled ? '1' : '0.5';
blockBtn.onclick = (e) => {
e.stopPropagation();
e.preventDefault();
if (!blockEnabled) {
showToast('⚠️ 屏蔽总开关已关闭,请先开启');
return;
}
if (!carNum) {
showToast('无法识别番号');
return;
}
const nowBlocked = getDYBlocked().includes(carNum);
toggleDYBlocked(carNum);
showToast(nowBlocked ? `已解除屏蔽: ${carNum}` : `已屏蔽: ${carNum}`);
applyDYFilter();
const isNowBlocked = getDYBlocked().includes(carNum);
blockBtn.textContent = isNowBlocked ? '✅ 已屏蔽' : '🚫 屏蔽这部';
blockBtn.style.background = isNowBlocked ? '#95a5a6' : '#e74c3c';
};
btnGroup.appendChild(blockBtn);
// 标记已下载按钮
const watchedBtn = document.createElement('button');
watchedBtn.textContent = isWatched ? '📥️ 已下载' : '📥️ 标记下载';
watchedBtn.style.cssText = `padding:2px 10px;font-size:11px;border:none;border-radius:4px;background:${isWatched ? '#2ecc71' : '#3498db'};color:#fff;cursor:pointer;font-weight:bold;`;
watchedBtn.onclick = (e) => {
e.stopPropagation();
e.preventDefault();
if (!carNum) { showToast('无法识别番号'); return; }
toggleDYWatched(carNum);
const nowWatched = getDYWatched().includes(carNum);
showToast(nowWatched ? `✅ 已标记下载: ${carNum}` : `❌ 已取消标记: ${carNum}`);
watchedBtn.textContent = nowWatched ? '📥️ 已下载' : '📥️ 标记下载';
watchedBtn.style.background = nowWatched ? '#2ecc71' : '#3498db';
if (getDYFilter()) applyDYFilter();
};
btnGroup.appendChild(watchedBtn);
// 显示番号和标记
const idLabel = document.createElement('span');
let labelText = `📌 ${carNum}`;
if (isSeriesBlocked && blockEnabled) labelText += ' 📦';
if (isKeywordBlocked && blockEnabled) labelText += ' 🔑';
idLabel.textContent = labelText;
idLabel.style.cssText = `font-size:10px;color:${(isKeywordBlocked || isSeriesBlocked) && blockEnabled ? '#f39c12' : '#888'};margin-left:4px;`;
btnGroup.appendChild(idLabel);
const targets = [
item.querySelector('.video-title'),
item.querySelector('.title'),
item.querySelector('.photo-info'),
item.querySelector('.meta'),
item.querySelector('.tags')
];
let inserted = false;
for (const target of targets) {
if (target) {
target.after(btnGroup);
inserted = true;
break;
}
}
if (!inserted) {
item.appendChild(btnGroup);
}
});
}
// ============================================================
// 9. 顶部工具栏
// ============================================================
function addDYTopBar() {
if (document.getElementById('dy-top-bar')) return;
const topBar = document.createElement('div');
topBar.id = 'dy-top-bar';
topBar.style.cssText = `
position: sticky;
top: 0;
z-index: 999998;
background: #1a1a2e;
padding: 8px 20px;
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
border-bottom: 2px solid #e94560;
font-family: 'Segoe UI', Arial, sans-serif;
`;
const logo = document.createElement('span');
logo.textContent = '🔪 JAV Blade';
logo.style.cssText = 'color:#e94560;font-weight:bold;font-size:14px;user-select:none;';
topBar.appendChild(logo);
const toggleBtn = document.createElement('button');
toggleBtn.id = 'dy-toggle-btn';
const isEnabled = getDYFilter();
toggleBtn.textContent = isEnabled ? '🟢 已下载过滤(开)' : '🔴 已下载过滤(关)';
toggleBtn.style.cssText = `padding:4px 14px;border:none;border-radius:4px;color:#fff;font-size:12px;font-weight:bold;cursor:pointer;background:${isEnabled ? '#27ae60' : '#e74c3c'};`;
toggleBtn.title = '开启后隐藏已下载的影片';
toggleBtn.onclick = () => {
const current = getDYFilter();
setDYFilter(!current);
const nowEnabled = getDYFilter();
toggleBtn.textContent = nowEnabled ? '🟢 已下载过滤(开)' : '🔴 已下载过滤(关)';
toggleBtn.style.background = nowEnabled ? '#27ae60' : '#e74c3c';
applyDYFilter();
showToast(nowEnabled ? '✅ 已开启过滤,隐藏已下载的影片' : '❌ 已关闭过滤,显示所有影片');
};
topBar.appendChild(toggleBtn);
const blockSwitchBtn = document.createElement('button');
blockSwitchBtn.id = 'dy-block-switch-btn';
const blockEnabled = getDYBlockSwitch();
blockSwitchBtn.textContent = blockEnabled ? '🟢 屏蔽总开关(开)' : '🔴 屏蔽总开关(关)';
blockSwitchBtn.style.cssText = `padding:4px 14px;border:none;border-radius:4px;color:#fff;font-size:12px;font-weight:bold;cursor:pointer;background:${blockEnabled ? '#27ae60' : '#e74c3c'};`;
blockSwitchBtn.title = '开启后屏蔽所有匹配的影片(单部屏蔽 + 番头屏蔽 + 关键字屏蔽)';
blockSwitchBtn.onclick = () => {
const current = getDYBlockSwitch();
setDYBlockSwitch(!current);
const nowEnabled = getDYBlockSwitch();
blockSwitchBtn.textContent = nowEnabled ? '🟢 屏蔽总开关(开)' : '🔴 屏蔽总开关(关)';
blockSwitchBtn.style.background = nowEnabled ? '#27ae60' : '#e74c3c';
applyDYFilter();
showToast(nowEnabled ? '✅ 已开启屏蔽总开关' : '❌ 已关闭屏蔽总开关');
};
topBar.appendChild(blockSwitchBtn);
const stats = document.createElement('span');
stats.id = 'dy-stats';
stats.style.cssText = 'color:#aaa;font-size:11px;flex:1;min-width:80px;';
topBar.appendChild(stats);
const sep1 = document.createElement('span');
sep1.textContent = '|';
sep1.style.cssText = 'color:#555;font-size:12px;';
topBar.appendChild(sep1);
const blockImportBtn = document.createElement('button');
blockImportBtn.textContent = '🚫 导入屏蔽目录';
blockImportBtn.style.cssText = 'padding:4px 12px;border:none;border-radius:4px;background:#e74c3c;color:#fff;font-size:11px;cursor:pointer;font-weight:bold;';
blockImportBtn.title = '导入TXT:番号→单部屏蔽 | 番号+【番头】→屏蔽整个系列 | 关键字→屏蔽所有包含该关键字的影片';
blockImportBtn.onclick = openBlockImportDialog;
topBar.appendChild(blockImportBtn);
const watchedImportBtn = document.createElement('button');
watchedImportBtn.textContent = '📥️ 导入已下载目录';
watchedImportBtn.style.cssText = 'padding:4px 12px;border:none;border-radius:4px;background:#3498db;color:#fff;font-size:11px;cursor:pointer;font-weight:bold;';
watchedImportBtn.onclick = openWatchedImportDialog;
topBar.appendChild(watchedImportBtn);
const exportBtn = document.createElement('button');
exportBtn.textContent = '📤 导出数据';
exportBtn.style.cssText = 'padding:4px 12px;border:none;border-radius:4px;background:#2ecc71;color:#fff;font-size:11px;cursor:pointer;font-weight:bold;';
exportBtn.title = '导出已下载、屏蔽列表到TXT文件';
exportBtn.onclick = openExportDialog;
topBar.appendChild(exportBtn);
const sep2 = document.createElement('span');
sep2.textContent = '|';
sep2.style.cssText = 'color:#555;font-size:12px;';
topBar.appendChild(sep2);
const manageBtn = document.createElement('button');
manageBtn.textContent = '⚙️ 管理列表';
manageBtn.style.cssText = 'padding:4px 12px;border:none;border-radius:4px;background:#34495e;color:#ecf0f1;font-size:11px;cursor:pointer;';
manageBtn.onclick = openDYManageDialog;
topBar.appendChild(manageBtn);
const clearBtn = document.createElement('button');
clearBtn.textContent = '🗑️ 清空';
clearBtn.style.cssText = 'padding:4px 12px;border:none;border-radius:4px;background:#c0392b;color:#fff;font-size:11px;cursor:pointer;';
clearBtn.onclick = () => {
if (confirm('确定要清空所有数据吗?(已下载列表 + 单部屏蔽列表 + 番头屏蔽列表 + 屏蔽关键字列表)')) {
localStorage.removeItem(DY_WATCHED_KEY);
localStorage.removeItem(DY_BLOCKED_KEY);
localStorage.removeItem(DY_BLOCKED_SERIES_KEY);
localStorage.removeItem(DY_BLOCKED_KEYWORDS_KEY);
localStorage.removeItem(DY_FILTER_KEY);
localStorage.removeItem(DY_BLOCK_SWITCH_KEY);
showToast('已清空所有数据,页面即将刷新');
setTimeout(() => location.reload(), 500);
}
};
topBar.appendChild(clearBtn);
const fixBtn = document.createElement('button');
fixBtn.textContent = '🔄 修复布局';
fixBtn.style.cssText = 'padding:4px 12px;border:none;border-radius:4px;background:#6c5ce7;color:#fff;font-size:11px;cursor:pointer;';
fixBtn.title = '如果出现黑屏或布局错乱,点击修复';
fixBtn.onclick = () => {
fixWaterfallLayout();
applyDYFilter();
showToast('✅ 已修复布局');
};
topBar.appendChild(fixBtn);
const targets = [
document.querySelector('.navbar'),
document.querySelector('.top-bar'),
document.querySelector('.header'),
document.querySelector('.main-nav'),
document.querySelector('.masonry')
];
let inserted = false;
for (const target of targets) {
if (target) {
target.parentNode.insertBefore(topBar, target);
inserted = true;
break;
}
}
if (!inserted) {
document.body.prepend(topBar);
}
setTimeout(applyDYFilter, 300);
}
// ============================================================
// 10. 右下角导航按钮
// ============================================================
function addScrollButtons() {
if (document.getElementById('dy-scroll-buttons')) return;
const container = document.createElement('div');
container.id = 'dy-scroll-buttons';
container.style.cssText = `
position: fixed;
bottom: 30px;
right: 20px;
z-index: 999997;
display: flex;
flex-direction: column;
gap: 8px;
`;
// 回到顶部
const topBtn = document.createElement('button');
topBtn.id = 'dy-scroll-top';
topBtn.innerHTML = '⬆';
topBtn.style.cssText = `
width: 44px;
height: 44px;
border: none;
border-radius: 50%;
background: rgba(233, 69, 96, 0.85);
color: #fff;
font-size: 22px;
cursor: pointer;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
user-select: none;
backdrop-filter: blur(4px);
`;
topBtn.title = '回到顶部';
topBtn.onmouseenter = () => {
topBtn.style.background = 'rgba(233, 69, 96, 1)';
topBtn.style.transform = 'scale(1.1)';
};
topBtn.onmouseleave = () => {
topBtn.style.background = 'rgba(233, 69, 96, 0.85)';
topBtn.style.transform = 'scale(1)';
};
topBtn.onclick = () => {
window.scrollTo({ top: 0, behavior: 'smooth' });
};
// 回到底部
const bottomBtn = document.createElement('button');
bottomBtn.id = 'dy-scroll-bottom';
bottomBtn.innerHTML = '⬇';
bottomBtn.style.cssText = `
width: 44px;
height: 44px;
border: none;
border-radius: 50%;
background: rgba(52, 152, 219, 0.85);
color: #fff;
font-size: 22px;
cursor: pointer;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
user-select: none;
backdrop-filter: blur(4px);
`;
bottomBtn.title = '回到底部';
bottomBtn.onmouseenter = () => {
bottomBtn.style.background = 'rgba(52, 152, 219, 1)';
bottomBtn.style.transform = 'scale(1.1)';
};
bottomBtn.onmouseleave = () => {
bottomBtn.style.background = 'rgba(52, 152, 219, 0.85)';
bottomBtn.style.transform = 'scale(1)';
};
bottomBtn.onclick = () => {
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
};
container.appendChild(topBtn);
container.appendChild(bottomBtn);
document.body.appendChild(container);
let hideTimeout;
window.addEventListener('scroll', function() {
const scrollY = window.scrollY;
const maxScroll = document.body.scrollHeight - window.innerHeight;
if (scrollY > 100) {
topBtn.style.opacity = '1';
topBtn.style.pointerEvents = 'auto';
} else {
topBtn.style.opacity = '0.3';
topBtn.style.pointerEvents = 'none';
}
if (maxScroll - scrollY > 100) {
bottomBtn.style.opacity = '1';
bottomBtn.style.pointerEvents = 'auto';
} else {
bottomBtn.style.opacity = '0.3';
bottomBtn.style.pointerEvents = 'none';
}
clearTimeout(hideTimeout);
container.style.opacity = '1';
hideTimeout = setTimeout(() => {
container.style.opacity = '0.6';
}, 3000);
});
container.onmouseenter = () => {
container.style.opacity = '1';
clearTimeout(hideTimeout);
};
container.onmouseleave = () => {
container.style.opacity = '0.6';
};
setTimeout(() => {
const scrollY = window.scrollY;
const maxScroll = document.body.scrollHeight - window.innerHeight;
if (scrollY <= 100) {
topBtn.style.opacity = '0.3';
topBtn.style.pointerEvents = 'none';
}
if (maxScroll - scrollY <= 100) {
bottomBtn.style.opacity = '0.3';
bottomBtn.style.pointerEvents = 'none';
}
container.style.opacity = '0.6';
}, 500);
}
// ============================================================
// 11. 管理对话框
// ============================================================
function openDYManageDialog() {
const blocked = getDYBlocked();
const blockedSeries = getDYBlockedSeries();
const keywords = getDYBlockedKeywords();
const watched = getDYWatched();
const overlay = document.createElement('div');
overlay.id = 'dy-manage-overlay';
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.7);
z-index: 9999999;
display: flex;
justify-content: center;
align-items: center;
`;
const dialog = document.createElement('div');
dialog.style.cssText = `
background: #1a1a2e;
border-radius: 12px;
padding: 30px;
max-width: 700px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
border: 1px solid #e94560;
color: #ecf0f1;
font-family: 'Segoe UI', Arial, sans-serif;
`;
const blockedHtml = blocked.length === 0
? '<span style="color:#999;">暂无屏蔽的影片</span>'
: blocked.map(v =>
`<span style="display:inline-block;background:#e74c3c;color:#fff;padding:4px 12px;border-radius:4px;margin:4px;font-size:13px;">${v} <span style="cursor:pointer;margin-left:6px;font-weight:bold;" onclick="document.dispatchEvent(new CustomEvent('dy-unblock', {detail:{video:'${v}'}}))">✕</span></span>`
).join('');
const seriesHtml = blockedSeries.length === 0
? '<span style="color:#999;">暂无屏蔽的番头</span>'
: blockedSeries.map(s =>
`<span style="display:inline-block;background:#e67e22;color:#fff;padding:4px 12px;border-radius:4px;margin:4px;font-size:13px;">📦 ${s.toUpperCase()}* <span style="cursor:pointer;margin-left:6px;font-weight:bold;" onclick="document.dispatchEvent(new CustomEvent('dy-unblock-series', {detail:{series:'${s}'}}))">✕</span></span>`
).join('');
const keywordsHtml = keywords.length === 0
? '<span style="color:#999;">暂无屏蔽的关键字</span>'
: keywords.map(k =>
`<span style="display:inline-block;background:#f39c12;color:#fff;padding:4px 12px;border-radius:4px;margin:4px;font-size:13px;">🔑 "${k}" <span style="cursor:pointer;margin-left:6px;font-weight:bold;" onclick="document.dispatchEvent(new CustomEvent('dy-unblock-keyword', {detail:{keyword:'${k}'}}))">✕</span></span>`
).join('');
const watchedHtml = watched.length === 0
? '<span style="color:#999;">暂无已下载的影片</span>'
: watched.map(id =>
`<span style="display:inline-block;background:#3498db;color:#fff;padding:4px 12px;border-radius:4px;margin:4px;font-size:13px;">${id} <span style="cursor:pointer;margin-left:6px;font-weight:bold;" onclick="document.dispatchEvent(new CustomEvent('dy-unwatch', {detail:{videoId:'${id}'}}))">✕</span></span>`
).join('');
dialog.innerHTML = `
<h2 style="color:#e94560;margin-bottom:20px;">⚙️ 管理屏蔽与下载</h2>
<div style="margin-bottom:20px;">
<h3 style="color:#e74c3c;font-size:14px;margin-bottom:10px;">🚫 单部屏蔽 (${blocked.length})</h3>
<div style="background:#2c3e50;padding:12px;border-radius:6px;min-height:40px;">${blockedHtml}</div>
</div>
<div style="margin-bottom:20px;">
<h3 style="color:#e67e22;font-size:14px;margin-bottom:10px;">📦 番头屏蔽 (${blockedSeries.length})</h3>
<div style="background:#2c3e50;padding:12px;border-radius:6px;min-height:40px;">${seriesHtml}</div>
</div>
<div style="margin-bottom:20px;">
<h3 style="color:#f39c12;font-size:14px;margin-bottom:10px;">🔑 屏蔽关键字 (${keywords.length})</h3>
<div style="background:#2c3e50;padding:12px;border-radius:6px;min-height:40px;">${keywordsHtml}</div>
</div>
<div style="margin-bottom:20px;">
<h3 style="color:#3498db;font-size:14px;margin-bottom:10px;">📥️ 已下载影片 (${watched.length})</h3>
<div style="background:#2c3e50;padding:12px;border-radius:6px;min-height:40px;">${watchedHtml}</div>
</div>
<div style="display:flex;gap:10px;justify-content:flex-end;margin-top:20px;">
<button id="dy-close-manage" style="padding:8px 24px;border:none;border-radius:6px;background:#34495e;color:#fff;cursor:pointer;font-size:14px;">关闭</button>
</div>
`;
overlay.appendChild(dialog);
document.body.appendChild(overlay);
document.getElementById('dy-close-manage').onclick = () => overlay.remove();
overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); };
document.addEventListener('dy-unblock', (e) => {
const video = e.detail.video;
let list = getDYBlocked();
list = list.filter(v => v !== video);
setDYBlocked(list);
overlay.remove();
applyDYFilter();
showToast(`已解除屏蔽: ${video}`);
openDYManageDialog();
}, { once: true });
document.addEventListener('dy-unblock-series', (e) => {
const series = e.detail.series;
let list = getDYBlockedSeries();
list = list.filter(s => s !== series);
setDYBlockedSeries(list);
overlay.remove();
applyDYFilter();
showToast(`已解除番头屏蔽: ${series.toUpperCase()}*`);
openDYManageDialog();
}, { once: true });
document.addEventListener('dy-unblock-keyword', (e) => {
const keyword = e.detail.keyword;
let list = getDYBlockedKeywords();
list = list.filter(k => k !== keyword);
setDYBlockedKeywords(list);
overlay.remove();
applyDYFilter();
showToast(`已解除屏蔽关键字: "${keyword}"`);
openDYManageDialog();
}, { once: true });
document.addEventListener('dy-unwatch', (e) => {
const videoId = e.detail.videoId;
let list = getDYWatched();
list = list.filter(id => id !== videoId);
setDYWatched(list);
overlay.remove();
applyDYFilter();
showToast(`已取消标记: ${videoId}`);
openDYManageDialog();
}, { once: true });
}
// ============================================================
// 12. Toast 提示
// ============================================================
function showToast(msg, duration = 2000) {
const old = document.getElementById('dy-toast');
if (old) old.remove();
const toast = document.createElement('div');
toast.id = 'dy-toast';
toast.textContent = msg;
toast.style.cssText = `
position: fixed;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
background: rgba(0,0,0,0.85);
color: #fff;
padding: 12px 28px;
border-radius: 8px;
font-size: 14px;
z-index: 99999999;
box-shadow: 0 4px 20px rgba(0,0,0,0.4);
font-family: 'Segoe UI', Arial, sans-serif;
max-width: 90%;
text-align: center;
animation: fadeIn 0.3s ease;
`;
const style = document.createElement('style');
style.textContent = `
@keyframes fadeIn {
from { opacity: 0; transform: translateX(-50%) translateY(20px); }
to { opacity: 1; transform: translateX(-50%) translateY(0); }
}
`;
document.head.appendChild(style);
document.body.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transition = 'opacity 0.3s';
setTimeout(() => toast.remove(), 300);
}, duration);
}
// ============================================================
// 13. 油猴菜单命令
// ============================================================
function registerMenuCommand() {
GM_registerMenuCommand('💾 快速导出备份 (JAV Blade)', function() {
const watched = getDYWatched();
const blocked = getDYBlocked();
const blockedSeries = getDYBlockedSeries();
const keywords = getDYBlockedKeywords();
let content = `# JAV Blade 数据备份\n`;
content += `# 导出时间: ${new Date().toLocaleString('zh-CN')}\n\n`;
content += `# 已下载列表 (${watched.length}条)\n${watched.join('\n')}\n\n`;
content += `# 单部屏蔽列表 (${blocked.length}条)\n${blocked.join('\n')}\n\n`;
content += `# 番头屏蔽列表 (${blockedSeries.length}条)\n${blockedSeries.map(s => s.toUpperCase()).join('\n')}\n\n`;
content += `# 屏蔽关键字列表 (${keywords.length}条)\n${keywords.join('\n')}`;
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `JAV_Blade_备份_${new Date().toISOString().slice(0,10)}.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
showToast('✅ 数据已导出,放心删除脚本吧!');
});
}
// ============================================================
// 14. 监听DOM变化
// ============================================================
let observer = null;
function observeChanges() {
if (observer) observer.disconnect();
observer = new MutationObserver(() => {
const items = getMovieItems();
let hasNew = false;
items.forEach(item => {
if (!item.querySelector('.dy-btn-group')) {
hasNew = true;
}
});
if (hasNew) {
addDYButtons();
applyDYFilter();
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
// ============================================================
// 15. 初始化
// ============================================================
function init() {
console.log('🔪 JAV Blade v3.2 加载中...');
try {
registerMenuCommand();
} catch (e) {}
let attempts = 0;
const maxAttempts = 30;
const check = setInterval(() => {
attempts++;
const hasItems = getMovieItems().length > 0;
if (hasItems || attempts >= maxAttempts) {
clearInterval(check);
console.log('✅ JAV Blade v3.2 已完全加载!');
addDYTopBar();
addScrollButtons();
setTimeout(() => {
addDYButtons();
applyDYFilter();
observeChanges();
console.log('📖 功能列表:');
console.log(' 📦 屏蔽番头 - 屏蔽整个系列');
console.log(' 🚫 屏蔽这部 - 只屏蔽当前一部影片');
console.log(' 🔑 关键字屏蔽 - 包含即屏蔽');
console.log(' 📥️ 标记下载 - 标记为已下载');
console.log(' 📤 导出数据 - 导出所有列表到TXT');
console.log(' ⬆⬇ 右下角导航 - 快速回到顶部/底部');
console.log(' 💾 油猴菜单 - 点击油猴图标 → JAV Blade → 快速导出备份');
}, 500);
}
}, 300);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
setTimeout(init, 500);
}
// ============================================================
// 16. 暴露到全局
// ============================================================
unsafeWindow.JAV_BLADE = {
getWatched: getDYWatched,
getBlocked: getDYBlocked,
getBlockedSeries: getDYBlockedSeries,
getBlockedKeywords: getDYBlockedKeywords,
getFilter: getDYFilter,
getBlockSwitch: getDYBlockSwitch,
setFilter: setDYFilter,
setBlockSwitch: setDYBlockSwitch,
toggleWatched: toggleDYWatched,
toggleBlocked: toggleDYBlocked,
toggleBlockedSeries: toggleDYBlockedSeries,
toggleBlockedKeyword: toggleDYBlockedKeyword,
applyFilter: applyDYFilter,
fixLayout: fixWaterfallLayout,
showToast: showToast,
openBlockImport: openBlockImportDialog,
openWatchedImport: openWatchedImportDialog,
openExport: openExportDialog
};
console.log('📖 在控制台输入 JAV_BLADE 可查看所有功能');
})();