// ==UserScript==
// @name 网页资源检测工具 (终极优化版)
// @namespace https://viayoo.com/
// @version 1.4
// @description 采用高对比度分页控件,按钮状态一目了然。静默扫描,交互完美,为您带来最终的、极致的网页资源检测体验。
// @author Doubao (Optimized by Gemini)
// @run-at document-idle
// @match *://*/*
// @grant GM_registerMenuCommand
// @grant GM_download
// @grant GM_openInTab
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_notification
// @grant GM_setClipboard
// @downloadURL none
// ==/UserScript==
(function() {
'use strict';
// --- CSS样式 ---
GM_addStyle(`
#resourceDetectorBall {
position: fixed; top: 80px; left: -40px; /* 默认隐藏在左侧 */
width: 55px; height: 42px;
border-radius: 0 21px 21px 0;
background: linear-gradient(135deg, #4285f4, #34a853);
color: white; display: flex; align-items: center; justify-content: flex-end;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); z-index: 9999;
cursor: pointer; transition: all 0.35s cubic-bezier(0.25, 0.8, 0.25, 1);
font-weight: 500; font-size: 14px; text-align: center;
padding-right: 8px; user-select: none; border: 2px solid white; border-left: none;
}
#resourceDetectorBall.visible { left: 0; } /* 可见状态 */
#resourceDetectorBall:hover { transform: scale(1.05); }
#resourceBallBadge {
position: absolute; top: -1px; right: -1px; width: 10px; height: 10px;
background-color: #FF3B30; border-radius: 50%; border: 2px solid #fff; display: none;
}
.tab-badge {
display: inline-block; min-width: 22px; height: 22px; border-radius: 11px;
background-color: #FF3B30; color: white; font-size: 12px; line-height: 22px;
text-align: center; margin-left: 8px; font-weight: bold;
}
#resourceDetectorPanel {
position: fixed; top: 65px; left: 15px; width: 320px;
background: rgba(255, 255, 255, 0.98); backdrop-filter: blur(12px);
border-radius: 14px; box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
z-index: 9998; display: none; overflow: hidden;
border: 1px solid rgba(230, 236, 245, 0.7); opacity: 0;
transform: translateX(-20px) scale(0.95);
transition: opacity 0.3s ease, transform 0.3s ease;
max-height: 75vh; font-family: 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
}
#resourceDetectorPanel.active {
display: block; opacity: 1; transform: translateX(0) scale(1);
}
.panel-header {
padding: 14px; font-size: 17px; font-weight: 600; color: #2c3e50;
display: flex; justify-content: space-between; align-items: center;
border-bottom: 1px solid #f0f3f7; background-color: rgba(245, 248, 255, 0.8);
}
.close-btn {
width: 26px; height: 26px; border-radius: 50%; display: flex; align-items: center;
justify-content: center; background: #f1f3f9; color: #7f8c8d;
font-size: 17px; cursor: pointer; transition: all 0.2s;
}
.close-btn:hover { background: #e5e9f2; color: #e74c3c; }
.category-tabs { display: flex; background: #f5f8ff; border-bottom: 1px solid #eef2f7; padding: 0 8px; }
.tab-btn {
flex: 1; text-align: center; padding: 12px 8px; font-size: 13px; font-weight: 500;
color: #6b7c93; cursor: pointer; transition: all 0.2s ease;
border-bottom: 3px solid transparent; display: flex; justify-content: center; align-items: center;
}
.tab-btn.active { color: #4285f4; border-bottom-color: #4285f4; background: rgba(66, 133, 244, 0.05); }
.tab-content { max-height: calc(75vh - 115px); overflow-y: auto; padding: 8px; }
.resource-list { list-style: none; padding: 0; margin: 0; }
.resource-item {
padding: 10px 14px; border-bottom: 1px solid #f0f3f7; display: flex;
align-items: center; transition: background-color 0.2s;
}
.resource-item:hover { background-color: rgba(235, 245, 255, 0.6); }
.resource-icon { font-size: 18px; width: 30px; height: 30px; border-radius: 7px; display: flex; align-items: center; justify-content: center; margin-right: 10px; flex-shrink: 0; }
.video-icon { background-color: rgba(234, 67, 53, 0.15); color: #ea4335; }
.audio-icon { background-color: rgba(52, 168, 83, 0.15); color: #34a853; }
.image-icon { background-color: rgba(66, 133, 244, 0.15); color: #4285f4; }
.resource-info { flex: 1; min-width: 0; }
.resource-name { font-weight: 500; font-size: 13px; color: #2d3748; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-bottom: 3px; }
.resource-meta { font-size: 11px; color: #718096; display: flex; gap: 8px; }
.resource-actions { margin-left: 8px; flex-shrink: 0; display: flex; gap: 5px; }
.action-btn {
width: 28px; height: 28px; border-radius: 7px; display: inline-flex; align-items: center;
justify-content: center; background: #f5f7fa; color: #5c6bc0; border: none;
cursor: pointer; transition: all 0.2s; font-size: 14px;
}
.action-btn:hover { background: #e8ebf0; transform: scale(1.05); }
.empty-message { padding: 30px; text-align: center; color: #718096; font-size: 13px; }
.loading-indicator { text-align: center; padding: 20px; color: #4285f4; font-size: 13px; display: flex; align-items: center; justify-content: center; }
.refresh-btn { display: flex; align-items: center; justify-content: center; width: calc(100% - 16px); margin: 8px auto 0; padding: 9px; background: #f5f7fa; border: none; border-radius: 7px; color: #4285f4; cursor: pointer; font-weight: 500; transition: all 0.2s; }
.refresh-btn:hover:not(:disabled) { background: #e8ebf0; }
.refresh-btn:disabled { cursor: not-allowed; background: #f5f7fa; color: #a0a0a0; }
.site-optimized { display: inline-block; background: rgba(66, 133, 244, 0.1); color: #4285f4; padding: 2px 7px; border-radius: 5px; font-size: 10px; margin-left: 7px; vertical-align: middle; }
/* --- 全新高对比度分页样式 --- */
.pagination {
display: flex;
justify-content: center;
gap: 8px;
margin-top: 12px;
padding-bottom: 5px;
}
.pagination-btn {
padding: 6px 12px;
border-radius: 7px;
font-size: 12px;
font-weight: 500;
border: 1px solid;
transition: all 0.2s ease;
}
/* 可点击的按钮: 蓝字,蓝边框 */
.pagination-btn:not(:disabled):not(.active) {
color: #4285f4;
border-color: #4285f4;
background: #ffffff;
cursor: pointer;
}
/* 悬停在可点击按钮上 */
.pagination-btn:not(:disabled):not(.active):hover {
background: rgba(66, 133, 244, 0.08);
}
/* 当前页指示器: 蓝底白字 */
.pagination-btn.active {
background: #4285f4;
color: white;
border-color: #4285f4;
cursor: default;
}
/* 不可点击的按钮: 灰字灰边框 */
.pagination-btn:disabled {
color: #cccccc;
border-color: #eeeeee;
background: #fafafa;
cursor: not-allowed;
}
.image-preview-overlay {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background-color: rgba(0,0,0,0.85); backdrop-filter: blur(5px);
z-index: 10000; display: flex; justify-content: center; align-items: center; padding: 15px;
}
.image-preview-content { max-width: 90vw; max-height: 90vh; display: flex; flex-direction: column; }
.image-preview-content img { max-width: 100%; max-height: calc(90vh - 80px); object-fit: contain; border-radius: 7px; }
.image-preview-info {
background: rgba(30,30,30,0.8); padding: 10px; border-radius: 7px; font-size: 13px; color: #fff;
margin-top: 10px; text-align: center; word-break: break-all;
}
.image-preview-controls { display: flex; justify-content: center; padding-top: 12px; gap: 12px; }
.preview-action-btn {
padding: 8px 16px; border-radius: 7px; background: #4a4a4a; color: #fff;
border: 1px solid #666; cursor: pointer; transition: all 0.2s; font-size: 14px;
}
.preview-action-btn:hover { background: #5a5a5a; border-color: #888; }
.loader {
width: 18px; height: 18px; border: 3px solid rgba(66, 133, 244, 0.2);
border-radius: 50%; border-top-color: #4285f4;
animation: spin 1s ease-in-out infinite; display: inline-block; vertical-align: middle; margin-right: 8px;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* 图片预览网格 */
.preview-container { padding: 12px; }
.preview-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 10px; margin-top: 8px; }
.image-preview {
border-radius: 7px; overflow: hidden; position: relative; padding-top: 100%;
background-color: #f0f2f5; box-shadow: 0 3px 8px rgba(0,0,0,0.05); cursor: pointer;
}
.image-preview img { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; transition: transform 0.3s; }
.image-preview:hover img { transform: scale(1.1); }
.image-info { position: absolute; bottom: 0; left: 0; right: 0; background: rgba(0, 0, 0, 0.6); color: white; padding: 5px 7px; font-size: 10px; line-height: 1.3; text-align: center; }
`);
// --- 配置信息 ---
const RESOURCE_TYPES = {
video: { extensions: ['m3u8', 'm3u', 'mp4', 'webm', 'avi', 'mov', 'flv', 'wmv', 'mpd', 'ts', 'f4v', 'mkv'], label: '视频', icon: '📹' },
audio: { extensions: ['mp3', 'wav', 'ogg', 'aac', 'm4a', 'flac', 'wma', 'opus'], label: '音频', icon: '🎵' },
image: { extensions: ['png', 'jpg', 'jpeg', 'gif', 'ico', 'webp', 'svg', 'bmp'], label: '图片', icon: '🖼️' }
};
const SUPPORTED_SITES = {
'douyin.com': '抖音', 'tiktok.com': 'TikTok', 'ixigua.com': '西瓜视频', 'kuaishou.com': '快手',
'v.qq.com': '腾讯视频', 'iqiyi.com': '爱奇艺', 'mgtv.com': '芒果TV', 'youtube.com': 'YouTube',
'youtu.be': 'YouTube', 'bilibili.com': '哔哩哔哩', 'b23.tv': '哔哩哔哩', 'youku.com': '优酷',
'twitter.com': 'Twitter', 'instagram.com': 'Instagram', 'google.com': 'Google Images', 'baidu.com': '百度图片'
};
// --- 全局状态变量 ---
let floatingBall = null, resourcePanel = null;
let resources = { video: [], audio: [], image: [] };
let isPanelVisible = false;
let currentTab = 'video';
let isScanning = false;
const currentDomain = location.hostname;
let currentPage = { video: 1, audio: 1, image: 1 };
const pageSize = 20;
let scanTimeout = null;
const foundUrls = new Set(); // 用于高效去重
// --- 核心功能函数 ---
function init() {
createUI();
setupEventListeners();
// 延迟进行首次扫描
setTimeout(() => performScan('full'), 1500);
}
function createUI() {
floatingBall = document.createElement('div');
floatingBall.id = 'resourceDetectorBall';
floatingBall.innerHTML = `资源`;
document.body.appendChild(floatingBall);
resourcePanel = document.createElement('div');
resourcePanel.id = 'resourceDetectorPanel';
const siteName = Object.keys(SUPPORTED_SITES).find(domain => currentDomain.includes(domain));
const optimizedTag = siteName ? `${SUPPORTED_SITES[siteName]} 优化` : '';
resourcePanel.innerHTML = `
${Object.entries(RESOURCE_TYPES).map(([type, config]) => `
${config.label} 0
`).join('')}
`;
document.body.appendChild(resourcePanel);
}
function setupEventListeners() {
floatingBall.addEventListener('click', togglePanelVisibility);
resourcePanel.querySelector('.close-btn').addEventListener('click', () => togglePanelVisibility(false));
resourcePanel.querySelectorAll('.tab-btn').forEach(tab => {
tab.addEventListener('click', () => switchTab(tab.dataset.type));
});
GM_registerMenuCommand('手动全量扫描资源', () => performScan('full'));
GM_registerMenuCommand('打开/关闭面板', () => togglePanelVisibility());
const observer = new MutationObserver(() => debounceScan());
observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['src', 'href'] });
document.addEventListener('play', (event) => {
if (event.target.src) {
const type = getResourceTypeByUrl(event.target.src);
if (type === 'video' || type === 'audio') {
if (addResource(event.target.src, type, event.target)) {
updateUI(); // 仅在添加成功后更新UI
}
}
}
}, true);
}
function togglePanelVisibility(forceShow = null) {
isPanelVisible = forceShow !== null ? forceShow : !isPanelVisible;
if (isPanelVisible) {
resourcePanel.classList.add('active');
renderTabContent(currentTab); // 打开时刷新内容
} else {
resourcePanel.classList.remove('active');
}
}
function switchTab(type) {
currentTab = type;
currentPage[type] = 1;
resourcePanel.querySelectorAll('.tab-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.type === type);
});
renderTabContent(type);
}
function renderTabContent(type) {
const contentContainer = resourcePanel.querySelector('.tab-content');
if (isScanning && contentContainer.dataset.scanMode === 'full') {
contentContainer.innerHTML = ``;
return;
}
const items = resources[type];
const config = RESOURCE_TYPES[type];
if (items.length === 0) {
contentContainer.innerHTML = `
未检测到${config.label}资源
${createRefreshButtonHTML('full')}
`;
contentContainer.querySelector('.refresh-btn').onclick = () => performScan('full');
return;
}
if (type === 'image') {
renderImagePreviewGrid(contentContainer, items, type);
} else {
renderResourceList(contentContainer, items, type);
}
}
function renderResourceList(container, items, type) {
const config = RESOURCE_TYPES[type];
const pageItems = paginate(items, currentPage[type], pageSize);
container.innerHTML = `
${pageItems.map(item => `
-
${config.icon}
${getFileName(item.url)}
${getFileType(item.url)}
${item.size ? `${item.size}` : ''}
${item.duration ? `${formatDuration(item.duration)}` : ''}
`).join('')}
`;
container.appendChild(createPagination(items.length, currentPage[type], pageSize, (page) => {
currentPage[type] = page;
renderResourceList(container, items, type);
}));
container.insertAdjacentHTML('beforeend', createRefreshButtonHTML('full'));
container.querySelector('.refresh-btn').onclick = () => performScan('full');
container.querySelectorAll('.resource-item').forEach(item => {
const url = item.dataset.url;
item.querySelector('[data-action="copy"]').onclick = (e) => { e.stopPropagation(); copyToClipboard(url, '链接已复制'); };
item.querySelector('[data-action="download"]').onclick = (e) => { e.stopPropagation(); GM_download(url, getFileName(url)); };
item.querySelector('[data-action="open"]').onclick = (e) => { e.stopPropagation(); GM_openInTab(url, { active: true }); };
});
}
function renderImagePreviewGrid(container, items, type) {
const pageItems = paginate(items, currentPage[type], pageSize);
container.innerHTML = `
${pageItems.map(item => `
${item.size || '未知尺寸'}
`).join('')}
`;
container.appendChild(createPagination(items.length, currentPage[type], pageSize, (page) => {
currentPage[type] = page;
renderImagePreviewGrid(container, items, type);
}));
container.insertAdjacentHTML('beforeend', createRefreshButtonHTML('full'));
container.querySelector('.refresh-btn').onclick = () => performScan('full');
container.querySelectorAll('.image-preview').forEach(item => {
item.onclick = () => showImagePreviewModal(item.dataset.url, item.dataset.size);
});
}
function showImagePreviewModal(url, size) {
const overlay = document.createElement('div');
overlay.className = 'image-preview-overlay';
overlay.innerHTML = `
尺寸: ${size} | 格式: ${getFileType(url)}
`;
document.body.appendChild(overlay);
overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); };
overlay.querySelector('#copyPreviewBtn').onclick = () => copyToClipboard(url, '图片链接已复制');
overlay.querySelector('#downloadPreviewBtn').onclick = () => GM_download(url, getFileName(url));
}
/**
* 执行资源扫描
* @param {'full' | 'additive'} mode - 扫描模式
*/
async function performScan(mode = 'additive') {
if (isScanning) return;
isScanning = true;
let newResourcesFound = 0;
// 如果是完整扫描,清空资源并更新UI
if (mode === 'full') {
foundUrls.clear();
resources = { video: [], audio: [], image: [] };
const contentContainer = resourcePanel.querySelector('.tab-content');
if(contentContainer) contentContainer.dataset.scanMode = 'full';
updateUI(); // 进入加载状态
}
const scanTasks = [
() => document.querySelectorAll('video, audio, source').forEach(el => { if(addResource(el.src, getResourceTypeByUrl(el.src), el)) newResourcesFound++; }),
() => document.querySelectorAll('img, image, [style*="background-image"]').forEach(el => {
let src = el.src || el.dataset.src || el.dataset.original || el.href?.baseVal;
if (el.style.backgroundImage) {
const match = el.style.backgroundImage.match(/url$['"]?(.*?)['"]?$/);
if (match) src = match[1];
}
if (src && getResourceTypeByUrl(src) === 'image' && addResource(src, 'image', el)) newResourcesFound++;
}),
() => document.querySelectorAll('a[href]').forEach(link => { if(addResource(link.href, getResourceTypeByUrl(link.href))) newResourcesFound++; }),
() => document.querySelectorAll('script').forEach(script => { if(scanTextForUrls(script.textContent)) newResourcesFound++; }),
() => performance.getEntriesByType('resource').forEach(res => { if(addResource(res.name, getResourceTypeByUrl(res.name))) newResourcesFound++; }),
() => { // 特定网站优化
if (currentDomain.includes('google.com')) document.querySelectorAll('img[data-iurl]').forEach(img => { if(addResource(img.dataset.iurl, 'image', img)) newResourcesFound++; });
if (currentDomain.includes('baidu.com')) document.querySelectorAll('img[data-imgurl]').forEach(img => { if(addResource(img.dataset.imgurl, 'image', img)) newResourcesFound++; });
}
];
for (const task of scanTasks) {
await new Promise(resolve => requestAnimationFrame(() => { task(); resolve(); }));
}
isScanning = false;
resourcePanel.querySelector('.tab-content').dataset.scanMode = '';
if (mode === 'full') {
GM_notification({ title: '全量扫描完成', text: `共发现 ${foundUrls.size} 个资源`, timeout: 2500 });
} else if (newResourcesFound > 0) {
console.log(`[资源检测] 增量扫描发现 ${newResourcesFound} 个新资源。`);
}
updateUI(); // 扫描结束后,最后更新一次UI
}
function scanTextForUrls(text) {
const urlRegex = /https?:\/\/[^\s"'<>]+/g;
const matches = text.match(urlRegex) || [];
let found = false;
matches.forEach(url => {
if(addResource(url, getResourceTypeByUrl(url))) found = true;
});
return found;
}
/**
* 添加资源到列表,返回true表示添加成功,false表示已存在
*/
function addResource(url, type, element = null) {
if (!url || !type || typeof url !== 'string') return false;
const cleanUrl = url.split('?')[0];
if (foundUrls.has(cleanUrl)) return false;
foundUrls.add(cleanUrl);
const resource = { url, type };
if (type === 'image' && element?.naturalWidth > 1) resource.size = `${element.naturalWidth}×${element.naturalHeight}`;
if ((type === 'video' || type === 'audio') && element?.duration) resource.duration = element.duration;
resources[type].push(resource);
resources[type].sort((a, b) => a.url.localeCompare(b.url));
return true;
}
function updateUI() {
const total = getTotalResources();
floatingBall.classList.toggle('visible', total > 0);
document.getElementById('resourceBallBadge').style.display = total > 0 ? 'block' : 'none';
Object.keys(RESOURCE_TYPES).forEach(type => {
const badge = document.getElementById(`badge-${type}`);
if (badge) {
const count = resources[type].length;
badge.textContent = count;
badge.style.display = count > 0 ? 'inline-block' : 'none';
}
});
if (isPanelVisible) {
renderTabContent(currentTab);
}
}
// --- 辅助与工具函数 ---
const debounceScan = (delay = 1200) => { clearTimeout(scanTimeout); scanTimeout = setTimeout(() => performScan('additive'), delay); };
const getTotalResources = () => foundUrls.size;
const getResourceTypeByUrl = (url) => {
if (!url) return null;
try {
const ext = new URL(url, location.href).pathname.split('.').pop().toLowerCase();
return Object.keys(RESOURCE_TYPES).find(type => RESOURCE_TYPES[type].extensions.includes(ext)) || null;
} catch (e) { return null; }
};
const getFileName = (url) => { try { return decodeURIComponent(new URL(url, location.href).pathname.split('/').pop() || '未命名资源'); } catch (e) { return '未命名资源'; }};
const getFileType = (url) => { try { return new URL(url, location.href).pathname.split('.').pop().toUpperCase() || '未知'; } catch (e) { return '未知'; }};
const formatDuration = (sec) => {
if (!sec || sec === Infinity) return '';
const m = Math.floor(sec / 60);
const s = Math.floor(sec % 60);
return `${m}:${s < 10 ? '0' : ''}${s}`;
};
const copyToClipboard = (text, message) => GM_setClipboard(text, 'text/plain', () => GM_notification({ title: '操作成功', text: message, timeout: 2000 }));
const paginate = (items, page, size) => items.slice((page - 1) * size, page * size);
/**
* 创建分页控件的函数 (高对比度最终版)
*/
function createPagination(totalItems, currentPage, pageSize, onPageChange) {
const totalPages = Math.ceil(totalItems / pageSize);
if (totalPages <= 1) return document.createDocumentFragment();
const pagination = document.createElement('div');
pagination.className = 'pagination';
// 创建上一页按钮
const prevBtn = document.createElement('button');
prevBtn.className = 'pagination-btn';
prevBtn.textContent = '‹ 上一页';
prevBtn.onclick = () => onPageChange(currentPage - 1);
if (currentPage === 1) {
prevBtn.disabled = true;
}
pagination.appendChild(prevBtn);
// 创建页码信息显示
const pageInfo = document.createElement('span');
pageInfo.className = 'pagination-btn active';
pageInfo.textContent = `${currentPage} / ${totalPages}`;
pagination.appendChild(pageInfo);
// 创建下一页按钮
const nextBtn = document.createElement('button');
nextBtn.className = 'pagination-btn';
nextBtn.textContent = '下一页 ›';
nextBtn.onclick = () => onPageChange(currentPage + 1);
if (currentPage === totalPages) {
nextBtn.disabled = true;
}
pagination.appendChild(nextBtn);
return pagination;
}
function createRefreshButtonHTML(mode) {
if (isScanning && mode ==='full') {
return `