// ==UserScript== // @name 加载日记 // @namespace yuelan-resource-diary // @version 7.0 // @description 网页资源捕获工具 // @author Kimi // @match *://*/* // @license MIT // @icon https://static.vecteezy.com/system/resources/thumbnails/010/796/909/small/multiple-files-icon-with-outline-style-vector.jpg // @grant GM_download // @grant GM_addStyle // @grant unsafeWindow // ==/UserScript== (function() { 'use strict'; if (window.ResourceDiary) return; const processedElements = new WeakSet(); const processedUrls = new Set(); const CONFIG = Object.freeze({ MAX_RESOURCES: 500, MAX_URL_CACHE: 2000, UPDATE_DELAY: 150, BATCH_SIZE: 50, DEBOUNCE_TIME: 100, LAZY_LOAD_OFFSET: '100px', SCAN_INTERVAL: 3000, ITEM_HEIGHT: 80, FETCH_TIMEOUT: 30000 }); const DANGEROUS_PROTOCOLS = new Set([ 'javascript:', 'data:text/html', 'data:text/javascript', 'vbscript:', 'mocha:', 'livescript:', 'about:', 'blob:javascript', 'file:', 'ftp:', 'telnet:', 'ssh:' ]); const ALLOWED_MIMES = new Set([ 'image/', 'audio/', 'video/', 'application/json', 'application/xml', 'text/plain', 'text/css', 'text/javascript', 'application/javascript', 'application/octet-stream' ]); const EXT_TO_TYPE = Object.freeze({ mp4: 'mp4', webm: 'webm', ogg: 'ogg', ogv: 'ogv', mov: 'mov', avi: 'avi', mkv: 'mkv', flv: 'flv', m3u8: 'm3u8', mpd: 'mpd', mp3: 'mp3', wav: 'wav', flac: 'flac', aac: 'aac', m4a: 'm4a', wma: 'wma', oga: 'oga', weba: 'weba', opus: 'opus', jpg: 'jpg', jpeg: 'jpg', png: 'png', gif: 'gif', webp: 'webp', svg: 'svg', ico: 'ico', bmp: 'bmp', avif: 'avif', jxl: 'jxl', css: 'stylesheet', js: 'script', mjs: 'script', json: 'json', woff: 'font', woff2: 'font', ttf: 'font', otf: 'font', eot: 'font' }); const MIME_TO_TYPE = Object.freeze({ 'video/mp4': 'mp4', 'video/webm': 'webm', 'video/ogg': 'ogg', 'video/quicktime': 'mov', 'video/x-matroska': 'mkv', 'audio/mpeg': 'mp3', 'audio/wav': 'wav', 'audio/flac': 'flac', 'audio/aac': 'aac', 'audio/ogg': 'oga', 'audio/webm': 'weba', 'audio/opus': 'opus', 'image/jpeg': 'jpg', 'image/png': 'png', 'image/gif': 'gif', 'image/webp': 'webp', 'image/svg+xml': 'svg', 'image/avif': 'avif', 'text/css': 'stylesheet', 'application/javascript': 'script', 'text/javascript': 'script', 'application/json': 'json', 'application/xml': 'xml' }); const TYPE_COLORS = Object.freeze({ jpg: '#e91e63', jpeg: '#e91e63', png: '#9c27b0', gif: '#ff5722', webp: '#00bcd4', svg: '#ff9800', ico: '#795548', bmp: '#607d8b', avif: '#4caf50', jxl: '#2196f3', image: '#3f51b5', mp4: '#f44336', webm: '#2196f3', ogg: '#ff9800', ogv: '#ff9800', mov: '#9c27b0', avi: '#795548', mkv: '#607d8b', flv: '#e91e63', m3u8: '#4caf50', mpd: '#00bcd4', mp3: '#1a73e8', wav: '#34a853', flac: '#fbbc05', aac: '#ea4335', m4a: '#9334e6', wma: '#5f6368', oga: '#ff6d01', weba: '#4285f4', opus: '#9c27b0', audio: '#1a73e8', script: '#fbbc05', stylesheet: '#34a853', json: '#4285f4', xml: '#9aa0a6', font: '#9aa0a6', XHR: '#ea4335', fetch: '#ff6d01', datauri: '#5f6368', other: '#4285f4' }); const Icons = { box: '', file: '', image: '', video: '', audio: '', script: '', stylesheet: '', json: '', xml: '', font: '', wifi: '', download: '', play: '', copy: '', close: '', folder: '', folderOpen: '', music: '', film: '', link: '', search: '', sun: '', moon: '', monitor: '', waveform: '' }; const OriginalAPIs = { xhrOpen: XMLHttpRequest.prototype.open, xhrSend: XMLHttpRequest.prototype.send, fetch: window.fetch, pushState: history.pushState, replaceState: history.replaceState }; const SecurityUtils = { urlCache: new Map(), maxCacheSize: 1000, isSafeUrl(url) { if (!url || typeof url !== 'string') return false; const cached = this.urlCache.get(url); if (cached !== undefined) return cached; if (this.urlCache.size > this.maxCacheSize) { const entries = Array.from(this.urlCache.entries()).slice(-500); this.urlCache.clear(); entries.forEach(([k, v]) => this.urlCache.set(k, v)); } const lowerUrl = url.toLowerCase().trim(); for (const protocol of DANGEROUS_PROTOCOLS) { if (lowerUrl.startsWith(protocol)) { this.urlCache.set(url, false); return false; } } if (lowerUrl.startsWith('data:')) { const mime = lowerUrl.split(',')[0].split(':')[1]?.split(';')[0] || ''; const isSafe = Array.from(ALLOWED_MIMES).some(m => mime.startsWith(m)); this.urlCache.set(url, isSafe); return isSafe; } if (lowerUrl.startsWith('blob:')) { try { const blobUrl = new URL(url); const isSafe = blobUrl.origin === location.origin; this.urlCache.set(url, isSafe); return isSafe; } catch { this.urlCache.set(url, false); return false; } } this.urlCache.set(url, true); return true; }, sanitizeAttr(value) { if (!value) return ''; return String(value) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, ''') .trim() .slice(0, 1000); }, sanitizeFilename(filename) { const sanitized = filename.replace(/[<>:"/\\|?*\x00-\x1f]/g, '_').slice(0, 255); return sanitized || 'download'; }, createElement(tag, attrs = {}, children = []) { const el = document.createElement(tag); for (const [key, value] of Object.entries(attrs)) { if (value == null) continue; if (key.startsWith('on') || key === 'innerHTML') continue; if ((key === 'href' || key === 'src' || key === 'action') && !this.isSafeUrl(value)) { continue; } if (key === 'style' && typeof value === 'object') { Object.assign(el.style, value); } else if (key === 'textContent') { el.textContent = value; } else if (key === 'className') { el.className = value; } else if (key === 'dataset') { Object.assign(el.dataset, value); } else { el.setAttribute(key, this.sanitizeAttr(value)); } } const fragment = document.createDocumentFragment(); for (const child of children) { if (typeof child === 'string') { fragment.appendChild(document.createTextNode(child)); } else if (child instanceof Node) { fragment.appendChild(child); } } if (fragment.childNodes.length > 0) { el.appendChild(fragment); } return el; } }; const ResourceDiary = { resources: [], filteredResources: [], currentFilter: 'all', searchKeyword: '', isInitialized: false, isPanelOpen: false, isDestroyed: false, _themeMode: 'system', _rafId: null, _lastUpdate: 0, _refreshTimer: null, _statsDirty: true, _resourceObserver: null, _listeners: [], _bodyCheckInterval: null, get isDarkMode() { if (this._themeMode === 'dark') return true; if (this._themeMode === 'light') return false; return window.matchMedia('(prefers-color-scheme: dark)').matches; }, getDisplayType(url, contentType = '', initiatorType = '') { if (!SecurityUtils.isSafeUrl(url)) return 'other'; if (url.startsWith('blob:')) { const mime = contentType || ''; if (mime.includes('video')) return 'mp4'; if (mime.includes('audio')) return 'mp3'; if (mime.includes('image')) return 'image'; return 'other'; } if (url.startsWith('data:')) { const mime = url.split(',')[0].split(':')[1]?.split(';')[0] || ''; if (mime.includes('image')) { if (mime.includes('svg')) return 'svg'; if (mime.includes('gif')) return 'gif'; if (mime.includes('png')) return 'png'; if (mime.includes('webp')) return 'webp'; if (mime.includes('avif')) return 'avif'; return mime.includes('jpeg') || mime.includes('jpg') ? 'jpg' : 'image'; } if (mime.includes('video')) return 'mp4'; if (mime.includes('audio')) return 'mp3'; return 'datauri'; } if (contentType) { const mimeType = contentType.split(';')[0].toLowerCase(); if (MIME_TO_TYPE[mimeType]) return MIME_TO_TYPE[mimeType]; if (mimeType.includes('video')) return 'mp4'; if (mimeType.includes('audio')) return 'mp3'; if (mimeType.includes('image')) return 'image'; if (mimeType.includes('script')) return 'script'; if (mimeType.includes('css')) return 'stylesheet'; if (mimeType.includes('font')) return 'font'; } try { const urlObj = new URL(url); const pathname = urlObj.pathname; const ext = pathname.split('.').pop()?.toLowerCase() || ''; if (EXT_TO_TYPE[ext]) return EXT_TO_TYPE[ext]; } catch { return 'other'; } if (initiatorType === 'xmlhttprequest') return 'XHR'; if (initiatorType === 'fetch') return 'fetch'; return 'other'; }, getFilterType(displayType) { const type = displayType.toLowerCase(); if (['mp4','webm','ogg','ogv','mov','avi','mkv','flv','m3u8','mpd', 'mp3','wav','flac','aac','m4a','wma','oga','weba','opus','audio'].includes(type)) { return 'media'; } if (['jpg','jpeg','png','gif','webp','svg','ico','bmp','avif','jxl','image'].includes(type)) { return 'image'; } return 'other'; }, getPreviewUrl(resource) { if (!SecurityUtils.isSafeUrl(resource.url)) return ''; if (resource.filterType === 'image') return resource.url; if (resource.url.startsWith('data:') && resource.filterType === 'image') { return resource.url; } if (resource.url.startsWith('blob:') && resource.filterType === 'image') { return resource.url; } return ''; }, formatUrl(url) { if (!SecurityUtils.isSafeUrl(url)) return '[不安全URL已阻止]'; if (url.startsWith('data:')) { const meta = url.split(',')[0]; return SecurityUtils.sanitizeAttr(meta) + ',...'; } if (url.startsWith('blob:')) { try { const urlObj = new URL(url); return 'blob:...' + urlObj.pathname.slice(-20); } catch { return 'blob:...'; } } return SecurityUtils.sanitizeAttr(url); }, getTypeIcon(displayType) { const type = displayType.toLowerCase(); if (['mp4','webm','ogg','ogv','mov','avi','mkv','flv','m3u8','mpd'].includes(type)) return Icons.video; if (['mp3','wav','flac','aac','m4a','wma','oga','weba','opus','audio'].includes(type)) return Icons.audio; if (['jpg','jpeg','png','gif','webp','svg','ico','bmp','avif','jxl','image'].includes(type)) return Icons.image; if (['script','js','mjs'].includes(type)) return Icons.script; if (['stylesheet','css'].includes(type)) return Icons.stylesheet; if (['json'].includes(type)) return Icons.json; if (['xml'].includes(type)) return Icons.xml; if (['font','woff','woff2','ttf','otf','eot'].includes(type)) return Icons.font; if (['xhr'].includes(type)) return Icons.wifi; if (['fetch'].includes(type)) return Icons.download; if (['datauri'].includes(type)) return Icons.link; return Icons.file; }, async copyToClipboard(text) { if (!text || typeof text !== 'string') { this.showToast('无效的复制内容'); return; } try { if (navigator.clipboard?.writeText) { await navigator.clipboard.writeText(text); } else { const ta = SecurityUtils.createElement('textarea', { value: text, style: { position: 'fixed', left: '-9999px', opacity: '0' } }); document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); } this.showToast('链接复制成功'); } catch (err) { this.showToast('复制失败,手动复制'); prompt('手动复制', text); } }, async downloadResource(url, filename) { if (!SecurityUtils.isSafeUrl(url)) { this.showToast('不安全的下载地址'); return; } this.showToast('开始下载...'); try { if (typeof GM_download === 'function' && GM_download.toString().includes('native code')) { const name = filename || this.extractFilename(url); GM_download({ url: url, name: name, onload: () => this.showToast('下载完成'), onerror: () => { this.fallbackDownload(url, filename); } }); } else { await this.fallbackDownload(url, filename); } } catch (err) { this.fallbackDownload(url, filename); } }, fallbackDownload(url, filename) { return new Promise((resolve, reject) => { try { const a = document.createElement('a'); a.href = url; a.download = filename || this.extractFilename(url); a.style.display = 'none'; document.body.appendChild(a); let isSameOrigin = false; try { isSameOrigin = new URL(url, location.href).origin === location.origin; } catch { isSameOrigin = false; } if (url.startsWith('data:') || url.startsWith('blob:') || isSameOrigin) { a.click(); this.showToast('下载已开始'); setTimeout(() => { if (a.parentNode) document.body.removeChild(a); }, 100); resolve(); } else { document.body.removeChild(a); this.fetchAndDownload(url, filename).then(resolve).catch(reject); } } catch (err) { reject(err); } }); }, async fetchAndDownload(url, filename) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), CONFIG.FETCH_TIMEOUT); try { const response = await fetch(url, { credentials: 'omit', signal: controller.signal }); clearTimeout(timeout); if (!response.ok) throw new Error('Fetch failed'); const blob = await response.blob(); const blobUrl = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = blobUrl; a.download = filename || this.extractFilename(url); a.click(); setTimeout(() => URL.revokeObjectURL(blobUrl), 1000); this.showToast('下载完成'); } catch (err) { if (err.name === 'AbortError') { this.showToast('下载超时'); } else { window.open(url, '_blank', 'noopener,noreferrer'); this.showToast('已在新标签页打开'); } } }, extractFilename(url) { try { if (url.startsWith('data:')) { const ext = url.match(/data:image\/(\w+)/)?.[1] || 'bin'; return `datauri_${Date.now()}.${ext}`; } if (url.startsWith('blob:')) { return `blob_${Date.now()}.bin`; } const urlObj = new URL(url, location.href); const pathname = decodeURIComponent(urlObj.pathname); const name = pathname.split('/').pop() || 'download'; return SecurityUtils.sanitizeFilename(name); } catch { return `download_${Date.now()}`; } }, showToast(message) { const existing = document.getElementById('rd-toast'); if (existing) { existing.remove(); } const toast = SecurityUtils.createElement('div', { id: 'rd-toast', style: { position: 'fixed', top: '50%', left: '50%', transform: 'translate3d(-50%,-50%,0)', background: this.isDarkMode ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.8)', color: 'white', padding: '12px 24px', borderRadius: '16px', fontSize: '14px', zIndex: '2147483647', pointerEvents: 'none', opacity: '0', transition: 'opacity 0.3s', backdropFilter: 'blur(10px)', willChange: 'opacity, transform' } }); toast.textContent = message; document.body.appendChild(toast); requestAnimationFrame(() => { toast.style.opacity = '1'; }); setTimeout(() => { toast.style.opacity = '0'; setTimeout(() => { if (toast.parentNode) toast.remove(); }, 300); }, 1700); }, init() { if (this.isInitialized || this.isDestroyed) return; this._waitForBodyAndInit(); }, _waitForBodyAndInit() { if (document.body) { this._delayedInit(); return; } let attempts = 0; const maxAttempts = 50; if (this._bodyCheckInterval) clearInterval(this._bodyCheckInterval); this._bodyCheckInterval = setInterval(() => { attempts++; if (document.body) { clearInterval(this._bodyCheckInterval); this._bodyCheckInterval = null; this._delayedInit(); } else if (attempts >= maxAttempts) { clearInterval(this._bodyCheckInterval); this._bodyCheckInterval = null; const observer = new MutationObserver((_, obs) => { if (document.body) { obs.disconnect(); this._delayedInit(); } }); observer.observe(document.documentElement, { childList: true, subtree: true }); } }, 100); }, _delayedInit() { const runInit = () => { try { this._performInit(); } catch (e) { console.error('ResourceDiary init error:', e); this.isInitialized = false; setTimeout(() => this._waitForBodyAndInit(), 1000); } }; if (typeof requestIdleCallback !== 'undefined') { requestIdleCallback(runInit, { timeout: 2000 }); } else { setTimeout(runInit, 0); } }, _performInit() { if (this.isInitialized || this.isDestroyed) return; if (!document.getElementById('rd-floating-btn')) { this.setupUI(); } this.setupResourceCapture(); this.setupResourceObserver(); this.setupSPASupport(); this.setupKeyboardShortcuts(); const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)'); const darkModeHandler = () => { if (this._themeMode === 'system') this.rebuildUI(); }; darkModeQuery.addEventListener('change', darkModeHandler); this._listeners.push(() => darkModeQuery.removeEventListener('change', darkModeHandler)); setTimeout(() => this.scanExistingResources(), 500); this.setupPeriodicScan(); this.isInitialized = true; }, setupPeriodicScan() { if (this._refreshTimer) clearInterval(this._refreshTimer); this._refreshTimer = setInterval(() => { if (!this.isPanelOpen) return; this.scanExistingResources(); }, CONFIG.SCAN_INTERVAL); }, rebuildUI() { const wasOpen = this.isPanelOpen; const scrollTop = document.getElementById('rd-list')?.scrollTop || 0; const searchValue = document.getElementById('rd-search')?.value || ''; document.getElementById('resource-diary-container')?.remove(); document.getElementById('rd-all-styles')?.remove(); this.setupUI(); const searchInput = document.getElementById('rd-search'); if (searchInput) searchInput.value = searchValue; this.searchKeyword = searchValue; this.updateFilterButtons(); this.scrollFilterToActive(); const list = document.getElementById('rd-list'); if (list) { list.scrollTop = scrollTop; } if (wasOpen) { document.getElementById('rd-panel')?.classList.add('active'); this.isPanelOpen = true; this.renderList(); } }, updateFilterButtons() { document.querySelectorAll('.rd-filter').forEach(btn => { btn.classList.toggle('active', btn.dataset.filter === this.currentFilter); }); }, scrollFilterToActive() { requestAnimationFrame(() => { const activeBtn = document.querySelector('.rd-filter.active'); const container = document.querySelector('.rd-filters-scroll'); if (activeBtn && container) { const scrollLeft = activeBtn.offsetLeft - (container.clientWidth / 2) + (activeBtn.clientWidth / 2); container.scrollTo({ left: Math.max(0, scrollLeft), behavior: 'smooth' }); } }); }, setupUI() { this.injectStyles(); this.createFloatingButton(); this.createMainUI(); this.bindEvents(); }, injectStyles() { if (document.getElementById('rd-all-styles')) return; const style = document.createElement('style'); style.id = 'rd-all-styles'; const isDark = this.isDarkMode; const typeColorCSS = Object.entries(TYPE_COLORS).map(([type, color]) => `.rd-entry-type.${type} { background: ${color} !important; }` ).join('\n'); style.textContent = ` :root { --rd-bg-primary: ${isDark ? '#1e1e1e' : '#ffffff'}; --rd-bg-secondary: ${isDark ? '#2d2d2d' : '#f5f5f5'}; --rd-bg-tertiary: ${isDark ? '#3d3d3d' : '#e8e8e8'}; --rd-border-color: ${isDark ? '#404040' : '#e0e0e0'}; --rd-text-primary: ${isDark ? '#ffffff' : '#333333'}; --rd-text-secondary: ${isDark ? '#aaaaaa' : '#666666'}; --rd-accent: #FFBF00; --rd-accent-hover: #3367d6; --rd-download-color: #34a853; --rd-danger: #ea4335; --rd-player-bg: ${isDark ? '#1e1e1e' : '#f0f0f0'}; --rd-preview-bg: ${isDark ? '#1e1e1e' : '#f0f0f0'}; } #resource-diary-container * { -webkit-tap-highlight-color: transparent; outline: none; box-sizing: border-box; } .rd-close-btn { width: 34px; height: 34px; border: 1.5px solid var(--rd-border-color) !important; background: var(--rd-bg-secondary); color: var(--rd-text-primary); border-radius: 14px; cursor: pointer; display: flex; align-items: center; justify-content: center; padding: 0; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); box-shadow: 0 2px 4px rgba(0,0,0,0.1); flex-shrink: 0; will-change: transform, box-shadow; transform: translateZ(0); } .rd-close-btn:hover { border-color: var(--rd-accent) !important; transform: scale(1.05) translateZ(0); box-shadow: 0 4px 8px rgba(0,0,0,0.15); } .rd-close-btn:active { transform: scale(0.95) translateZ(0); } #rd-floating-btn { position: fixed; bottom: 120px; right: 16px; width: 34px; height: 34px; background: ${isDark ? 'rgba(45,45,45,0.7)' : 'rgba(255,255,255,0.7)'}; backdrop-filter: blur(15px) saturate(160%); -webkit-backdrop-filter: blur(15px) saturate(160%); border-radius: 14px; border: 1.5px solid ${isDark ? 'rgba(255,255,255,0.1)' : 'rgba(255,255,255,0.5)'}; box-shadow: 0 6px 16px rgba(0,0,0,0.12), inset 0 0 2px rgba(255,255,255,0.8); display: flex; align-items: center; justify-content: center; cursor: pointer; z-index: 2147483647; transition: all 0.2s cubic-bezier(0.34, 1.56, 0.64, 1); user-select: none; -webkit-tap-highlight-color: transparent; will-change: transform, box-shadow; transform: translateZ(0); } #rd-floating-btn:hover { box-shadow: 0 8px 24px rgba(0,0,0,0.18), inset 0 0 2px rgba(255,255,255,0.9); transform: scale(1.1) translateZ(0); } #rd-floating-btn:active { transform: scale(0.95) translateZ(0); } #rd-floating-btn svg { width: 18px; height: 18px; stroke: ${isDark ?'#ccc': '#333'}; filter: drop-shadow(0 1px 1.5px rgba(0,0,0,0.15)); transform: translateZ(0); } #rd-panel { position: fixed; top: 50%; left: 50%; transform: translate3d(-50%, -50%, 0) scale(0.95); width: 92%; max-width: 800px; height: 80vh; background: var(--rd-bg-primary); border-radius: 25px; box-shadow: 0 25px 50px -12px rgba(0,0,0,0.5); z-index: 2147483647; display: none; flex-direction: column; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; overflow: hidden; opacity: 0; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); will-change: transform, opacity; contain: layout paint; } #rd-panel.active { display: flex; opacity: 1; transform: translate3d(-50%, -50%, 0) scale(1); } .rd-header { padding: 12px 16px; background: var(--rd-bg-primary); border-bottom: 1px solid var(--rd-border-color); display: flex; align-items: center; flex-shrink: 0; gap: 12px; min-width: 0; overflow: hidden; contain: layout; } .rd-header h3 { margin: 0; font-size: 20px; font-weight: 600; color: var(--rd-text-primary); white-space: nowrap; flex-shrink: 0; display: flex; align-items: center; gap: 6px; overflow: hidden; text-overflow: ellipsis; } .rd-search-wrapper { flex: 1; max-width: 280px; min-width: 100px; position: relative; } #rd-search { width: 100%; height: 34px; padding: 0 12px 0 38px; border: 1px solid var(--rd-border-color); border-radius: 14px; font-size: 14px; background: var(--rd-bg-secondary); color: var(--rd-text-primary); transition: all 0.2s; outline: none; box-sizing: border-box; will-change: border-color, background; } #rd-search:focus { border-color: var(--rd-accent); background: var(--rd-bg-primary); } #rd-search::placeholder { color: var(--rd-text-secondary); } #rd-search-icon { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: var(--rd-text-secondary); pointer-events: none; display: flex; align-items: center; justify-content: center; width: 16px; height: 16px; } .rd-controls { display: flex; gap: 8px; flex-shrink: 0; margin-left: auto; } .rd-toolbar { background: var(--rd-bg-secondary); border-bottom: 1px solid var(--rd-border-color); flex-shrink: 0; padding: 8px 12px; } .rd-filters-scroll { overflow-x: auto; scrollbar-width: none; -ms-overflow-style: none; -webkit-overflow-scrolling: touch; transform: translateZ(0); } .rd-filters-scroll::-webkit-scrollbar { display: none; } .rd-filters { display: flex; gap: 8px; padding: 4px 0; } .rd-filter { padding: 6px 8px; border: none; background: var(--rd-bg-primary); border-radius: 14px; cursor: pointer; font-size: 14px; font-weight: 500; color: var(--rd-text-secondary); transition: all 0.2s; display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; border: 1px solid var(--rd-border-color); will-change: transform, box-shadow; transform: translateZ(0); } .rd-filter:hover { border-color: var(--rd-accent); color: var(--rd-accent); transform: translateY(-1px) translateZ(0); } .rd-filter.active { background: var(--rd-accent); color: #fff; border-color: var(--rd-accent); } .rd-filter svg { width: 16px; height: 16px; stroke: currentColor; } .rd-filter-count { font-size: 12px; opacity: 0.9; font-weight: 600; } .rd-list { flex: 1; overflow-y: auto; padding: 12px; background: var(--rd-bg-primary); scroll-behavior: smooth; -webkit-overflow-scrolling: touch; contain: layout; will-change: scroll-position; } .rd-list-content { display: flex; flex-direction: column; gap: 8px; contain: layout; } .rd-entry { display: flex; gap: 8px; padding: 8px; background: var(--rd-bg-secondary); border-radius: 16px; font-size: 12px; border: 1px solid var(--rd-border-color); transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); cursor: pointer; align-items: stretch; min-height: ${CONFIG.ITEM_HEIGHT}px; -webkit-tap-highlight-color: transparent; will-change: transform, box-shadow, border-color; transform: translateZ(0); contain: layout; } .rd-entry:hover { border-color: var(--rd-accent); transform: translateX(4px) translateZ(0); box-shadow: 0 4px 12px rgba(0,0,0,0.1); } .rd-entry-thumb-wrapper { width: 56px; height: 56px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; background: var(--rd-bg-tertiary); border-radius: 16px; overflow: hidden; align-self: center; contain: strict; } .rd-entry-thumb { width: 100%; height: 100%; object-fit: cover; will-change: transform; } .rd-entry-fallback { display: flex; align-items: center; justify-content: center; color: var(--rd-text-secondary); } .rd-entry-fallback svg { width: 24px; height: 24px; stroke: currentColor; } .rd-entry-content { flex: 1; min-width: 0; display: flex; flex-direction: column; justify-content: center; gap: 4px; padding: 2px 0; contain: layout; } .rd-entry-header { display: flex; align-items: center; gap: 8px; height: 24px; } .rd-entry-type { color: white; padding: 0 6px; border-radius: 8px; font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; height: 20px; line-height: 20px; display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; will-change: transform; transform: translateZ(0); } ${typeColorCSS} .rd-entry-actions { display: flex; gap: 4px; margin-left: auto; align-items: center; height: 100%; } .rd-action-btn { cursor: pointer; width: 28px; height: 28px; border-radius: 6px; background: transparent; transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1); border: none; display: flex; align-items: center; justify-content: center; padding: 0; color: var(--rd-text-secondary); -webkit-tap-highlight-color: transparent; will-change: transform, background; transform: translateZ(0); } .rd-action-btn:hover { background: var(--rd-bg-tertiary); transform: scale(1.1) translateZ(0); } .rd-action-btn:active { transform: scale(0.95) translateZ(0); } .rd-action-btn svg { width: 16px; height: 16px; stroke: currentColor; pointer-events: none; } .rd-download-btn { color: var(--rd-download-color); } .rd-play-btn { color: var(--rd-danger); } .rd-copy-btn { color: var(--rd-text-secondary); } .rd-entry-url { word-break: break-all; color: var(--rd-text-secondary); font-family: 'SF Mono', Monaco, Consolas, monospace; font-size: 12px; line-height: 1.5; max-height: 3em; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; contain: layout; } .rd-empty { text-align: center; color: var(--rd-text-secondary); padding: 60px 20px; font-size: 14px; display: flex; flex-direction: column; align-items: center; gap: 12px; contain: layout; } .rd-empty svg { width: 48px; height: 48px; stroke: currentColor; opacity: 0.5; } #rd-inline-player, #rd-img-preview, #rd-text-preview { position: fixed; z-index: 2147483647; will-change: transform; transform: translate3d(-50%, -50%, 0); } #rd-inline-player { top: 50%; left: 50%; width: 92%; max-width: 800px; height: 80vh; max-height: 80vh; background: ${isDark ? '#1e1e1e' : '#f0f0f0'}; border-radius: 25px; overflow: hidden; box-shadow: 0 25px 50px rgba(0,0,0,0.5); display: flex; flex-direction: column; border: 1px solid ${isDark ? '#404040' : '#e0e0e0'}; contain: layout paint; } #rd-player-header { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; background: ${isDark ? '#1e1e1e' : '#f0f0f0'}; border-bottom: 1px solid ${isDark ? '#404040' : '#e0e0e0'}; flex-shrink: 0; height: 56px; box-sizing: border-box; contain: layout; } #rd-player-title { color: ${isDark ? '#ffffff' : '#333333'}; font-size: 16px; font-weight: 600; display: flex; align-items: center; gap: 8px; } #rd-player-title svg { width: 20px; height: 20px; stroke: currentColor; } #rd-media-wrapper { flex: 1; display: flex; align-items: center; justify-content: center; background: ${isDark ? '#1e1e1e' : '#f0f0f0'}; overflow: hidden; min-height: 0; position: relative; width: 100%; contain: layout paint; } #rd-video { width: 100%; height: 100%; max-width: 100%; max-height: 100%; object-fit: contain; display: block; will-change: transform; } #rd-inline-player audio { width: 100%; padding: 40px; box-sizing: border-box; } #rd-img-preview { top: 50%; left: 50%; width: 92%; max-width: 800px; height: 80vh; max-height: 80vh; background: ${isDark ? '#1e1e1e' : '#f0f0f0'}; border-radius: 25px; display: flex; flex-direction: column; overflow: hidden; border: 1px solid ${isDark ? '#404040' : '#e0e0e0'}; box-shadow: 0 25px 50px rgba(0,0,0,0.5); contain: layout paint; } #rd-img-preview-header { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; background: ${isDark ? '#1e1e1e' : '#f0f0f0'}; border-bottom: 1px solid ${isDark ? '#404040' : '#e0e0e0'}; height: 56px; box-sizing: border-box; flex-shrink: 0; contain: layout; } #rd-img-preview-title { color: ${isDark ? '#ffffff' : '#333333'}; font-size: 16px; font-weight: 600; display: flex; align-items: center; gap: 8px; } #rd-img-preview-content { flex: 1; display: flex; align-items: center; justify-content: center; overflow: hidden; padding: 16px; background: ${isDark ? '#1e1e1e' : '#f0f0f0'}; cursor: zoom-out; contain: layout paint; } #rd-preview-img { max-width: 100%; max-height: 100%; width: auto; height: auto; object-fit: contain; border-radius: 20px; cursor: default; will-change: transform; } #rd-text-preview { top: 50%; left: 50%; width: 92%; max-width: 800px; height: 80vh; background: ${isDark ? '#1e1e1e' : '#f0f0f0'}; border-radius: 25px; box-shadow: 0 25px 50px rgba(0,0,0,0.5); display: flex; flex-direction: column; overflow: hidden; border: 1px solid ${isDark ? '#404040' : '#e0e0e0'}; contain: layout paint; } #rd-text-preview-header { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; background: ${isDark ? '#1e1e1e' : '#f0f0f0'}; border-bottom: 1px solid ${isDark ? '#404040' : '#e0e0e0'}; height: 56px; box-sizing: border-box; contain: layout; flex-shrink: 0; } #rd-text-preview-title { font-weight: 600; color: ${isDark ? '#ffffff' : '#333333'}; font-size: 16px; display: flex; align-items: center; gap: 8px; } #rd-text-preview-scroll { flex: 1; overflow: auto; padding: 16px; background: ${isDark ? '#1e1e1e' : '#f0f0f0'}; -webkit-overflow-scrolling: touch; } #rd-text-preview-content { margin: 0; padding: 0; font-family: 'SF Mono', Monaco, Consolas, 'Courier New', monospace; font-size: 13px; line-height: 1.6; color: ${isDark ? '#ffffff' : '#333333'}; white-space: pre-wrap; word-break: break-all; background: transparent; border: none; outline: none; } .rd-iframe-container { flex: 1; position: relative; background: ${isDark ? '#1e1e1e' : '#f0f0f0'}; overflow: hidden; contain: layout paint; } .rd-iframe-container iframe { width: 100%; height: 100%; border: none; background: ${isDark ? '#1e1e1e' : '#f0f0f0'}; } .rd-list::-webkit-scrollbar, #rd-text-preview-scroll::-webkit-scrollbar { width: 8px; height: 8px; } .rd-list::-webkit-scrollbar-track, #rd-text-preview-scroll::-webkit-scrollbar-track { background: var(--rd-bg-secondary); border-radius: 4px; } .rd-list::-webkit-scrollbar-thumb, #rd-text-preview-scroll::-webkit-scrollbar-thumb { background: var(--rd-border-color); border-radius: 4px; transition: background 0.2s; } .rd-list::-webkit-scrollbar-thumb:hover, #rd-text-preview-scroll::-webkit-scrollbar-thumb:hover { background: var(--rd-text-secondary); } .rd-list, #rd-text-preview-scroll { scrollbar-width: thin; scrollbar-color: var(--rd-border-color) var(--rd-bg-secondary); } `; try { document.head.appendChild(style); } catch (e) { if (typeof GM_addStyle !== 'undefined') { GM_addStyle(style.textContent); } } }, createFloatingButton() { if (document.getElementById('rd-floating-btn')) return; const btn = SecurityUtils.createElement('div', { id: 'rd-floating-btn', title: '打开资源日记' }); btn.innerHTML = Icons.box; const handler = (e) => { e.stopPropagation(); e.preventDefault(); this.togglePanel(); }; btn.addEventListener('click', handler, { passive: false }); this._listeners.push(() => btn.removeEventListener('click', handler)); document.body.appendChild(btn); }, getFilterIcon(type) { const iconMap = { all: Icons.folder, media: Icons.waveform, image: Icons.image, other: Icons.file }; return iconMap[type] || Icons.file; }, getFilterName(type) { const names = { all: '所有', media: '媒体', image: '图片', other: '其他' }; return names[type] || type; }, toggleTheme() { const modes = ['system', 'light', 'dark']; const idx = modes.indexOf(this._themeMode); this._themeMode = modes[(idx + 1) % modes.length]; this.rebuildUI(); const names = { system: '跟随系统', light: '浅色模式', dark: '深色模式' }; this.showToast(`已切换至${names[this._themeMode]}`); }, getThemeIcon() { if (this._themeMode === 'dark') return Icons.moon; if (this._themeMode === 'light') return Icons.sun; return Icons.monitor; }, createMainUI() { if (document.getElementById('resource-diary-container')) return; const container = SecurityUtils.createElement('div', { id: 'resource-diary-container' }); const panel = SecurityUtils.createElement('div', { id: 'rd-panel' }); panel.innerHTML = `

${Icons.box} 资源日记

${Icons.search}
${['all','media','image','other'].map(f => ` `).join('')}
`; container.appendChild(panel); document.body.appendChild(container); }, bindEvents() { const panel = document.getElementById('rd-panel'); if (!panel) return; const clickHandler = (e) => { const target = e.target; if (target.closest('#rd-close')) { this.hidePanel(); return; } if (target.closest('#rd-theme-toggle')) { this.toggleTheme(); return; } const filterBtn = target.closest('.rd-filter'); if (filterBtn) { this.setFilter(filterBtn.dataset.filter); return; } const actionBtn = target.closest('.rd-action-btn'); if (actionBtn) { e.stopPropagation(); const url = actionBtn.dataset.url; if (!url || !SecurityUtils.isSafeUrl(url)) return; if (actionBtn.classList.contains('rd-download-btn')) { this.downloadResource(url); } else if (actionBtn.classList.contains('rd-play-btn')) { this.playMedia(url); } else if (actionBtn.classList.contains('rd-copy-btn')) { this.copyToClipboard(url); } return; } const thumb = target.closest('.rd-entry-thumb-wrapper'); if (thumb) { e.stopPropagation(); const url = thumb.dataset.url; if (url && SecurityUtils.isSafeUrl(url)) { this.previewImage(url); } return; } const entry = target.closest('.rd-entry'); if (entry && !target.closest('.rd-entry-actions')) { const url = entry.dataset.url; const displayType = entry.dataset.displayType; const filterType = this.getFilterType(displayType); if (!url || !SecurityUtils.isSafeUrl(url)) return; if (filterType === 'media') this.playMedia(url); else if (filterType === 'image') this.previewImage(url); else this.previewTextContent(url); } }; panel.addEventListener('click', clickHandler); this._listeners.push(() => panel.removeEventListener('click', clickHandler)); const searchInput = document.getElementById('rd-search'); if (searchInput) { let searchTimer; const inputHandler = (e) => { this.searchKeyword = e.target.value; clearTimeout(searchTimer); searchTimer = setTimeout(() => this.scheduleUpdate(), CONFIG.DEBOUNCE_TIME); }; searchInput.addEventListener('input', inputHandler); this._listeners.push(() => searchInput.removeEventListener('input', inputHandler)); } const list = document.getElementById('rd-list'); if (list) { let touchStartX = 0; let touchStartY = 0; let isHorizontalSwipe = false; let touchStartTime = 0; const touchStartHandler = (e) => { touchStartX = e.changedTouches[0].screenX; touchStartY = e.changedTouches[0].screenY; touchStartTime = Date.now(); isHorizontalSwipe = false; }; const touchMoveHandler = (e) => { if (!touchStartX) return; const x = e.changedTouches[0].screenX; const y = e.changedTouches[0].screenY; const diffX = Math.abs(touchStartX - x); const diffY = Math.abs(touchStartY - y); if (diffX > diffY && diffX > 10) { isHorizontalSwipe = true; } }; const touchEndHandler = (e) => { if (!isHorizontalSwipe) return; const diffX = touchStartX - e.changedTouches[0].screenX; const timeDiff = Date.now() - touchStartTime; if (Math.abs(diffX) > 50 && timeDiff < 300) { const filters = ['all', 'media', 'image', 'other']; const currentIdx = filters.indexOf(this.currentFilter); const newIdx = diffX > 0 ? Math.min(currentIdx + 1, filters.length - 1) : Math.max(currentIdx - 1, 0); if (newIdx !== currentIdx) { this.setFilter(filters[newIdx]); setTimeout(() => this.scrollFilterToActive(), 50); } } touchStartX = 0; isHorizontalSwipe = false; }; list.addEventListener('touchstart', touchStartHandler, { passive: true }); list.addEventListener('touchmove', touchMoveHandler, { passive: true }); list.addEventListener('touchend', touchEndHandler, { passive: true }); this._listeners.push(() => { list.removeEventListener('touchstart', touchStartHandler); list.removeEventListener('touchmove', touchMoveHandler); list.removeEventListener('touchend', touchEndHandler); }); } }, setupKeyboardShortcuts() { const keyHandler = (e) => { if (e.key === 'Escape') { document.getElementById('rd-inline-player')?.remove(); document.getElementById('rd-img-preview')?.remove(); document.getElementById('rd-text-preview')?.remove(); if (this.isPanelOpen) this.hidePanel(); } }; document.addEventListener('keydown', keyHandler); this._listeners.push(() => document.removeEventListener('keydown', keyHandler)); }, setFilter(filter) { this.currentFilter = filter; this.updateFilterButtons(); this.scrollFilterToActive(); this.scheduleUpdate(); }, togglePanel() { const panel = document.getElementById('rd-panel'); if (!panel) return; const isActive = panel.classList.contains('active'); if (isActive) { this.hidePanel(); } else { panel.classList.add('active'); this.isPanelOpen = true; this.manualRefresh(); this.scheduleUpdate(); this.scrollFilterToActive(); } }, hidePanel() { const panel = document.getElementById('rd-panel'); if (panel) panel.classList.remove('active'); this.isPanelOpen = false; setTimeout(() => { document.getElementById('rd-inline-player')?.remove(); document.getElementById('rd-img-preview')?.remove(); document.getElementById('rd-text-preview')?.remove(); }, 300); }, previewImage(url) { if (!SecurityUtils.isSafeUrl(url)) { this.showToast('不安全的图片地址'); return; } document.getElementById('rd-img-preview')?.remove(); const preview = SecurityUtils.createElement('div', { id: 'rd-img-preview' }); const header = SecurityUtils.createElement('div', { id: 'rd-img-preview-header' }); const title = SecurityUtils.createElement('span', { id: 'rd-img-preview-title' }); title.innerHTML = Icons.image + ' 图片预览'; const closeBtn = SecurityUtils.createElement('button', { className: 'rd-close-btn', id: 'rd-close-preview', title: '关闭' }); closeBtn.innerHTML = Icons.close; header.appendChild(title); header.appendChild(closeBtn); const content = SecurityUtils.createElement('div', { id: 'rd-img-preview-content' }); const img = document.createElement('img'); img.id = 'rd-preview-img'; img.src = url; img.alt = '预览'; content.appendChild(img); preview.appendChild(header); preview.appendChild(content); const closeHandler = () => preview.remove(); closeBtn.addEventListener('click', closeHandler); preview.addEventListener('click', (e) => { if (e.target === preview || e.target.id === 'rd-img-preview-content' || e.target.id === 'rd-img-preview-header') { closeHandler(); } }); document.body.appendChild(preview); }, async previewTextContent(url) { if (!SecurityUtils.isSafeUrl(url)) { this.showToast('不安全的资源地址'); return; } this.showToast('加载中...'); const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), CONFIG.FETCH_TIMEOUT); try { const response = await fetch(url, { credentials: 'omit', signal: controller.signal, headers: { 'Accept': 'text/plain,text/html,application/json' } }); clearTimeout(timeout); const contentType = response.headers.get('content-type') || ''; if (!/text\/|json|xml|javascript/.test(contentType)) { this.showIframePreview(url); return; } const text = await response.text(); const displayText = text.length > 50000 ? text.slice(0, 50000) + '\n\n... (内容过长已截断)' : text; document.getElementById('rd-text-preview')?.remove(); const isDark = this.isDarkMode; const preview = SecurityUtils.createElement('div', { id: 'rd-text-preview' }); const header = SecurityUtils.createElement('div', { id: 'rd-text-preview-header' }); const title = SecurityUtils.createElement('span', { id: 'rd-text-preview-title' }); title.innerHTML = Icons.file + ' 内容预览'; const closeBtn = SecurityUtils.createElement('button', { className: 'rd-close-btn', id: 'rd-text-preview-close', title: '关闭' }); closeBtn.innerHTML = Icons.close; header.appendChild(title); header.appendChild(closeBtn); preview.appendChild(header); const scrollContainer = SecurityUtils.createElement('div', { id: 'rd-text-preview-scroll' }); const pre = document.createElement('pre'); pre.id = 'rd-text-preview-content'; pre.textContent = displayText; pre.style.cssText = ` margin: 0 !important; padding: 0 !important; font-family: 'SF Mono', Monaco, Consolas, 'Courier New', monospace !important; font-size: 13px !important; line-height: 1.6 !important; color: ${isDark ? '#ffffff' : '#333333'} !important; white-space: pre-wrap !important; word-break: break-all !important; background: transparent !important; border: none !important; outline: none !important; box-shadow: none !important; `; scrollContainer.appendChild(pre); preview.appendChild(scrollContainer); const closeHandler = () => preview.remove(); closeBtn.addEventListener('click', closeHandler); preview.addEventListener('click', (e) => { if (e.target === preview || e.target.id === 'rd-text-preview-scroll') { closeHandler(); } }); document.body.appendChild(preview); } catch (e) { this.showIframePreview(url); } }, showIframePreview(url) { document.getElementById('rd-text-preview')?.remove(); const preview = SecurityUtils.createElement('div', { id: 'rd-text-preview', style: { display: 'flex', flexDirection: 'column' } }); const header = SecurityUtils.createElement('div', { id: 'rd-text-preview-header' }); const title = SecurityUtils.createElement('span', { id: 'rd-text-preview-title' }); title.innerHTML = Icons.file + ' 内容预览'; const controls = SecurityUtils.createElement('div', { style: { display: 'flex', gap: '8px', alignItems: 'center' } }); const openBtn = SecurityUtils.createElement('button', { className: 'rd-close-btn', title: '在新窗口打开' }); openBtn.innerHTML = Icons.link; const closeBtn = SecurityUtils.createElement('button', { className: 'rd-close-btn', id: 'rd-text-preview-close', title: '关闭' }); closeBtn.innerHTML = Icons.close; controls.appendChild(openBtn); controls.appendChild(closeBtn); header.appendChild(title); header.appendChild(controls); const container = SecurityUtils.createElement('div', { className: 'rd-iframe-container' }); const iframe = SecurityUtils.createElement('iframe', { src: url, sandbox: 'allow-same-origin allow-scripts allow-popups allow-downloads' }); container.appendChild(iframe); preview.appendChild(header); preview.appendChild(container); const closeHandler = () => preview.remove(); closeBtn.addEventListener('click', closeHandler); openBtn.addEventListener('click', () => { window.open(url, '_blank', 'noopener,noreferrer'); }); preview.addEventListener('click', (e) => { if (e.target === preview || e.target.className === 'rd-iframe-container') { closeHandler(); } }); document.body.appendChild(preview); }, setupSPASupport() { let lastUrl = location.href; const checkUrl = () => { if (location.href !== lastUrl) { lastUrl = location.href; this.resources = []; this.filteredResources = []; processedUrls.clear(); this._statsDirty = true; this.scheduleUpdate(); setTimeout(() => this.scanExistingResources(), 500); if (!document.getElementById('rd-floating-btn')) { this.createFloatingButton(); } } }; const originalPush = OriginalAPIs.pushState; const originalReplace = OriginalAPIs.replaceState; history.pushState = function(...args) { originalPush.apply(this, args); setTimeout(checkUrl, 50); }; history.replaceState = function(...args) { originalReplace.apply(this, args); setTimeout(checkUrl, 50); }; const popStateHandler = () => setTimeout(checkUrl, 50); const hashChangeHandler = checkUrl; window.addEventListener('popstate', popStateHandler); window.addEventListener('hashchange', hashChangeHandler); this._listeners.push(() => { window.removeEventListener('popstate', popStateHandler); window.removeEventListener('hashchange', hashChangeHandler); }); }, scanExistingResources() { const processImages = () => { const images = document.querySelectorAll('img[src], img[srcset]'); for (let i = 0; i < Math.min(images.length, CONFIG.BATCH_SIZE); i++) { this.captureImageElement(images[i]); } }; const processMedia = () => { const videos = document.querySelectorAll('video[src], video source[src], video[data-src]'); const audios = document.querySelectorAll('audio[src], audio source[src], audio[data-src]'); [...videos, ...audios].slice(0, CONFIG.BATCH_SIZE).forEach(el => { if (el.src) this.tryAddMediaUrl(el.src, el.tagName.toLowerCase()); if (el.dataset?.src) this.tryAddMediaUrl(el.dataset.src, el.tagName.toLowerCase()); }); }; const processShadowDOM = () => { const allElements = document.querySelectorAll('*'); allElements.forEach(el => { if (el.shadowRoot) { const videos = el.shadowRoot.querySelectorAll('video[src], video source[src]'); videos.forEach(v => { if (v.src) this.tryAddMediaUrl(v.src, 'video'); if (v.dataset?.src) this.tryAddMediaUrl(v.dataset.src, 'video'); }); } }); }; const processIframes = () => { const iframes = document.querySelectorAll('iframe'); iframes.forEach(iframe => { try { const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document; if (iframeDoc) { const videos = iframeDoc.querySelectorAll('video[src], video source[src]'); videos.forEach(v => { if (v.src) this.tryAddMediaUrl(v.src, 'video'); }); } } catch (e) {} }); }; const processBgImages = () => { const elements = document.querySelectorAll('body, body *'); let index = 0; const processBatch = () => { const end = Math.min(index + CONFIG.BATCH_SIZE, elements.length); for (; index < end; index++) { try { const el = elements[index]; const bg = window.getComputedStyle(el).backgroundImage; if (bg && bg !== 'none' && bg.includes('url(')) { const matches = bg.match(/url\(["']?(data:[^"')]+)["']?\)/g); if (matches) { matches.forEach(match => { const url = match.replace(/url\(["']?/, '').replace(/["']?\)$/, ''); if (url.startsWith('data:')) this.tryAddDataUri(url, 'css-bg'); }); } } } catch (e) {} } if (index < elements.length) { requestIdleCallback ? requestIdleCallback(processBatch) : setTimeout(processBatch, 10); } }; processBatch(); }; if (typeof requestIdleCallback !== 'undefined') { requestIdleCallback(processImages); requestIdleCallback(processMedia); requestIdleCallback(processShadowDOM); requestIdleCallback(processIframes); setTimeout(() => requestIdleCallback(processBgImages), 1000); } else { setTimeout(processImages, 1); setTimeout(processMedia, 100); setTimeout(processShadowDOM, 200); setTimeout(processIframes, 300); setTimeout(processBgImages, 1000); } }, captureImageElement(img) { if (processedElements.has(img)) return; processedElements.add(img); const src = img.currentSrc || img.src; if (src) { if (src.startsWith('data:')) this.tryAddDataUri(src, 'img'); else if (src.startsWith('blob:')) this.tryAddBlobUrl(src, 'img'); else this.tryAddImageUrl(src); } if (img.srcset) { img.srcset.split(',').forEach(s => { const url = s.trim().split(' ')[0]; if (url) { if (url.startsWith('data:')) this.tryAddDataUri(url, 'srcset'); else if (url.startsWith('blob:')) this.tryAddBlobUrl(url, 'srcset'); else this.tryAddImageUrl(url); } }); } }, tryAddMediaUrl(url, tagName) { if (!url) return; if (processedUrls.has(url)) return; const isAudio = tagName === 'audio'; const ext = url.split('.').pop()?.split('?')[0].toLowerCase() || ''; const audioExts = new Set(['mp3','wav','flac','aac','m4a','wma','oga','weba','opus']); const videoExts = new Set(['mp4','webm','ogg','ogv','mov','avi','mkv','flv','m3u8','mpd']); let displayType; if (isAudio || audioExts.has(ext)) { displayType = EXT_TO_TYPE[ext] || 'mp3'; } else if (videoExts.has(ext)) { displayType = EXT_TO_TYPE[ext] || 'mp4'; } else { return; } processedUrls.add(url); this.addResource({ url, displayType, filterType: 'media', status: 200 }); }, tryAddBlobUrl(url, source) { if (!SecurityUtils.isSafeUrl(url)) return; if (processedUrls.has(url)) return; processedUrls.add(url); const displayType = this.getDisplayType(url, ''); const filterType = this.getFilterType(displayType); this.addResource({ url, displayType, filterType, status: 200, size: 0, method: 'BLOB', source }); }, tryAddImageUrl(url) { if (!url || processedUrls.has(url)) return; const ext = url.split('.').pop()?.split('?')[0].toLowerCase() || ''; const imageExts = new Set(['jpg','jpeg','png','gif','webp','svg','ico','bmp','avif','jxl']); if (imageExts.has(ext)) { processedUrls.add(url); this.addResource({ url, displayType: EXT_TO_TYPE[ext] || 'image', filterType: 'image', status: 200 }); } }, tryAddDataUri(url, source) { if (!SecurityUtils.isSafeUrl(url)) return; if (processedUrls.has(url)) return; processedUrls.add(url); const displayType = this.getDisplayType(url, ''); const filterType = this.getFilterType(displayType); this.addResource({ url, displayType, filterType, status: 200, size: url.length, method: 'DATA', source }); }, setupResourceObserver() { if (this._resourceObserver) { this._resourceObserver.disconnect(); } this._resourceObserver = new MutationObserver(mutations => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType !== 1) continue; if (node.tagName === 'IMG') { this.captureImageElement(node); } else if (node.tagName === 'VIDEO' || node.tagName === 'AUDIO') { this.captureMediaElement(node); } else if (node.tagName === 'IFRAME') { setTimeout(() => { try { const iframeDoc = node.contentDocument || node.contentWindow?.document; if (iframeDoc) { iframeDoc.querySelectorAll('video[src], video source[src]').forEach(v => { if (v.src) this.tryAddMediaUrl(v.src, 'video'); }); } } catch (e) {} }, 1000); } else if (node.querySelectorAll) { node.querySelectorAll('img[src], img[srcset]').forEach(img => this.captureImageElement(img)); node.querySelectorAll('video, audio').forEach(el => this.captureMediaElement(el)); node.querySelectorAll('*').forEach(el => { if (el.shadowRoot) { el.shadowRoot.querySelectorAll('video[src], video source[src]').forEach(v => { if (v.src) this.tryAddMediaUrl(v.src, 'video'); }); } }); } } if (mutation.type === 'attributes' && (mutation.target.tagName === 'VIDEO' || mutation.target.tagName === 'AUDIO')) { const newSrc = mutation.target.src || mutation.target.dataset?.src; if (newSrc) this.tryAddMediaUrl(newSrc, mutation.target.tagName.toLowerCase()); } } }); this._resourceObserver.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['src', 'data-src'] }); }, captureMediaElement(el) { if (processedElements.has(el)) return; processedElements.add(el); const observers = []; const processSources = () => { const src = el.currentSrc || el.src || el.dataset?.src; if (src) this.tryAddMediaUrl(src, el.tagName.toLowerCase()); el.querySelectorAll('source').forEach(source => { const src = source.src || source.dataset?.src; if (!src || processedUrls.has(src)) return; const type = source.type || ''; const ext = src.split('.').pop()?.split('?')[0].toLowerCase() || ''; const audioExts = new Set(['mp3','wav','flac','aac','m4a','wma','oga','weba','opus']); const videoExts = new Set(['mp4','webm','ogg','ogv','mov','avi','mkv','flv','m3u8','mpd']); let displayType; if (type.includes('audio') || audioExts.has(ext)) { displayType = EXT_TO_TYPE[ext] || 'mp3'; } else if (type.includes('video') || videoExts.has(ext)) { displayType = EXT_TO_TYPE[ext] || 'mp4'; } else { return; } processedUrls.add(src); this.addResource({ url: src, displayType, filterType: 'media', status: 200 }); }); }; processSources(); const loadHandler = () => processSources(); el.addEventListener('loadstart', loadHandler, { once: true }); el.addEventListener('loadedmetadata', loadHandler, { once: true }); const attrObserver = new MutationObserver(mutations => { mutations.forEach(mutation => { if (mutation.attributeName === 'src' || mutation.attributeName === 'data-src') { const newSrc = el.src || el.dataset?.src; if (newSrc) this.tryAddMediaUrl(newSrc, el.tagName.toLowerCase()); } }); }); attrObserver.observe(el, { attributes: true, attributeFilter: ['src', 'data-src'] }); observers.push(attrObserver); if ('IntersectionObserver' in window) { const io = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { processSources(); } }); }, { rootMargin: CONFIG.LAZY_LOAD_OFFSET }); io.observe(el); observers.push(io); } const cleanupObserver = new MutationObserver((mutations) => { mutations.forEach(mutation => { mutation.removedNodes.forEach(node => { if (node === el || (node.contains && node.contains(el))) { observers.forEach(obs => obs.disconnect()); cleanupObserver.disconnect(); } }); }); }); if (document.body) { cleanupObserver.observe(document.body, { childList: true, subtree: true }); } }, setupResourceCapture() { const processEntry = (entry) => { if (!entry.name) return; const url = entry.name; if (!SecurityUtils.isSafeUrl(url)) return; if (processedUrls.has(url)) return; const displayType = this.getDisplayType(url, entry.responseContentType, entry.initiatorType); const filterType = this.getFilterType(displayType); if (filterType === 'other' && (url.includes('.m3u8') || url.includes('.mpd') || url.includes('manifest') || entry.responseContentType?.includes('mpegurl'))) { const ext = url.includes('.m3u8') ? 'm3u8' : url.includes('.mpd') ? 'mpd' : 'mp4'; processedUrls.add(url); this.addResource({ url, displayType: ext, filterType: 'media', status: entry.responseStatus || 200, size: entry.transferSize || entry.encodedBodySize || 0, duration: entry.duration || 0, cached: entry.transferSize === 0 && entry.encodedBodySize > 0 }); return; } if (filterType === 'other' && !entry.responseContentType?.includes('video') && !entry.responseContentType?.includes('audio')) { return; } processedUrls.add(url); let size = entry.transferSize || entry.encodedBodySize || 0; if ((url.startsWith('data:') || url.startsWith('blob:')) && size === 0) size = url.length; this.addResource({ url, displayType, filterType, status: entry.responseStatus || 200, size, duration: entry.duration || 0, cached: entry.transferSize === 0 && entry.encodedBodySize > 0 }); }; performance.getEntriesByType('resource').forEach(processEntry); try { const perfObserver = new PerformanceObserver(list => { list.getEntries().forEach(processEntry); }); perfObserver.observe({ type: 'resource', buffered: true }); this._listeners.push(() => perfObserver.disconnect()); } catch (e) {} const bufferHandler = () => { performance.clearResourceTimings(); }; performance.addEventListener('resourcetimingbufferfull', bufferHandler); this._listeners.push(() => performance.removeEventListener('resourcetimingbufferfull', bufferHandler)); const self = this; XMLHttpRequest.prototype.open = function(method, url) { this._rdData = { method, url: String(url), startTime: performance.now() }; return OriginalAPIs.xhrOpen.apply(this, arguments); }; XMLHttpRequest.prototype.send = function() { const xhr = this; const data = xhr._rdData; if (!data) return OriginalAPIs.xhrSend.apply(this, arguments); const loadHandler = () => { try { if (self.isDestroyed) return; const responseUrl = xhr.responseURL || data.url; if (!SecurityUtils.isSafeUrl(responseUrl)) return; if (processedUrls.has(responseUrl)) return; const displayType = self.getDisplayType(responseUrl, xhr.getResponseHeader('content-type'), 'xmlhttprequest'); const filterType = self.getFilterType(displayType); processedUrls.add(responseUrl); self.addResource({ url: responseUrl, displayType, filterType, status: xhr.status, size: xhr.responseText?.length || 0, duration: performance.now() - data.startTime, method: data.method }); } catch (e) {} }; xhr.addEventListener('load', loadHandler, { once: true }); return OriginalAPIs.xhrSend.apply(this, arguments); }; window.fetch = async (input, init) => { if (self.isDestroyed) { return OriginalAPIs.fetch(input, init); } const url = typeof input === 'string' ? input : input?.url || input; if (!url || !SecurityUtils.isSafeUrl(url)) { return OriginalAPIs.fetch(input, init); } const method = init?.method || 'GET'; const startTime = performance.now(); const response = await OriginalAPIs.fetch(input, init); Promise.resolve().then(async () => { try { if (!response.ok || response.type === 'opaque') return; if (processedUrls.has(url)) return; const cloned = response.clone(); const displayType = self.getDisplayType(url, cloned.headers.get('content-type'), 'fetch'); const filterType = self.getFilterType(displayType); processedUrls.add(url); let size = 0; try { const blob = await cloned.blob(); size = blob.size; } catch (e) { size = parseInt(cloned.headers.get('content-length')) || 0; } self.addResource({ url, displayType, filterType, status: cloned.status, size, duration: performance.now() - startTime, method }); } catch (e) {} }); return response; }; }, addResource(resource) { if (!SecurityUtils.isSafeUrl(resource.url)) return; const id = `${Date.now()}_${Math.random().toString(36).slice(2)}_${Math.random().toString(36).slice(2)}`; this.resources.unshift({ id, url: resource.url, displayType: resource.displayType || 'other', filterType: resource.filterType || 'other', status: resource.status || 0, size: resource.size || 0, duration: resource.duration || 0, method: resource.method || 'GET', timestamp: new Date().toLocaleTimeString('zh-CN'), cached: resource.cached || false }); if (this.resources.length > CONFIG.MAX_RESOURCES) { const removed = this.resources.splice(CONFIG.MAX_RESOURCES); removed.forEach(r => processedUrls.delete(r.url)); } if (processedUrls.size > CONFIG.MAX_URL_CACHE) { const recentResources = this.resources.slice(0, Math.floor(CONFIG.MAX_URL_CACHE / 2)); processedUrls.clear(); recentResources.forEach(r => processedUrls.add(r.url)); } this._statsDirty = true; this.scheduleUpdate(); }, scheduleUpdate() { if (this._rafId) return; const now = performance.now(); const elapsed = now - this._lastUpdate; if (elapsed < CONFIG.UPDATE_DELAY) { this._rafId = requestAnimationFrame(() => { this._rafId = null; if (performance.now() - this._lastUpdate >= CONFIG.UPDATE_DELAY) { this._doUpdate(); } else { this.scheduleUpdate(); } }); } else { this._doUpdate(); } }, _doUpdate() { this._lastUpdate = performance.now(); this.filterResources(); this.renderList(); this.updateStats(); }, filterResources() { const keyword = this.searchKeyword.toLowerCase(); this.filteredResources = this.resources.filter(r => { if (this.currentFilter !== 'all' && r.filterType !== this.currentFilter) return false; if (keyword && !r.url.toLowerCase().includes(keyword)) return false; return true; }); }, renderList() { const list = document.getElementById('rd-list'); if (!list) return; const scrollTop = list.scrollTop; const content = list.querySelector('.rd-list-content'); if (!content) return; content.innerHTML = ''; if (this.filteredResources.length === 0) { content.innerHTML = `
${Icons.folderOpen}
暂无资源
`; return; } const fragment = document.createDocumentFragment(); this.filteredResources.forEach((r) => { fragment.appendChild(this.createEntryElement(r)); }); content.appendChild(fragment); if (scrollTop > 0) { requestAnimationFrame(() => { list.scrollTop = scrollTop; }); } }, createEntryElement(r) { const preview = this.getPreviewUrl(r); const escUrl = SecurityUtils.sanitizeAttr(r.url); const dispUrl = this.formatUrl(r.url); const icon = this.getTypeIcon(r.displayType); const isMedia = r.filterType === 'media'; const isImage = r.filterType === 'image'; const isDataUri = r.displayType === 'datauri'; const entry = document.createElement('div'); entry.className = 'rd-entry'; entry.dataset.url = r.url; entry.dataset.displayType = r.displayType; entry.title = isMedia ? '点击播放' : (isImage || isDataUri) ? '点击查看图片' : '点击查看内容'; const thumbWrapper = document.createElement('div'); thumbWrapper.className = 'rd-entry-thumb-wrapper'; thumbWrapper.dataset.url = r.url; if ((isImage || isDataUri) && preview) { const img = document.createElement('img'); img.className = 'rd-entry-thumb'; img.src = preview; img.loading = 'lazy'; img.decoding = 'async'; img.onerror = function() { this.style.display = 'none'; this.nextElementSibling.style.display = 'flex'; }; thumbWrapper.appendChild(img); } const fallback = document.createElement('div'); fallback.className = 'rd-entry-fallback'; fallback.style.display = (isImage || isDataUri) && preview ? 'none' : 'flex'; fallback.innerHTML = icon; thumbWrapper.appendChild(fallback); const content = document.createElement('div'); content.className = 'rd-entry-content'; const header = document.createElement('div'); header.className = 'rd-entry-header'; const typeSpan = document.createElement('span'); typeSpan.className = `rd-entry-type ${r.displayType}`; typeSpan.textContent = r.displayType; const actions = document.createElement('div'); actions.className = 'rd-entry-actions'; if (isMedia) { const playBtn = document.createElement('button'); playBtn.className = 'rd-action-btn rd-play-btn'; playBtn.dataset.url = r.url; playBtn.title = '播放'; playBtn.innerHTML = Icons.play; actions.appendChild(playBtn); } const copyBtn = document.createElement('button'); copyBtn.className = 'rd-action-btn rd-copy-btn'; copyBtn.dataset.url = r.url; copyBtn.title = '复制'; copyBtn.innerHTML = Icons.copy; const downloadBtn = document.createElement('button'); downloadBtn.className = 'rd-action-btn rd-download-btn'; downloadBtn.dataset.url = r.url; downloadBtn.title = '下载'; downloadBtn.innerHTML = Icons.download; actions.appendChild(copyBtn); actions.appendChild(downloadBtn); header.appendChild(typeSpan); header.appendChild(actions); const urlDiv = document.createElement('div'); urlDiv.className = 'rd-entry-url'; urlDiv.textContent = dispUrl; content.appendChild(header); content.appendChild(urlDiv); entry.appendChild(thumbWrapper); entry.appendChild(content); return entry; }, updateStats() { if (!this._statsDirty) return; this._statsDirty = false; const stats = { all: 0, media: 0, image: 0, other: 0 }; for (const r of this.resources) { stats.all++; stats[r.filterType]++; } document.querySelectorAll('.rd-filter-count').forEach(el => { const type = el.dataset.count; if (stats.hasOwnProperty(type)) { el.textContent = stats[type]; } }); }, extractRealMediaUrl(url) { try { if (url.match(/^https?:\/\/[^\s&]+$/)) { return url; } const urlObj = new URL(url, location.href); const params = urlObj.searchParams; const urlParams = ['url', 'src', 'link', 'file', 'play', 'video', 'audio', 'stream', 'source', 'v', 'u']; for (const param of urlParams) { const value = params.get(param); if (value) { const decoded = decodeURIComponent(value); if (decoded.startsWith('http')) { return decoded; } } } if (url.includes('play') || url.includes('player') || url.includes('embed')) { const matches = url.match(/https?:\/\/[^\s&"']+\.(m3u8|mp4|webm|mkv|flv|mp3|aac)/i); if (matches) return matches[0]; } return url; } catch (e) { return url; } }, playMedia(url) { if (!SecurityUtils.isSafeUrl(url)) { this.showToast('不安全的媒体地址'); return; } const realUrl = this.extractRealMediaUrl(url); document.getElementById('rd-inline-player')?.remove(); const isAudio = /\.(mp3|wav|flac|aac|ogg|m4a|weba|oga|opus)$/i.test(realUrl); const mime = this.getMimeType(realUrl); const player = SecurityUtils.createElement('div', { id: 'rd-inline-player' }); const header = SecurityUtils.createElement('div', { id: 'rd-player-header' }); const title = SecurityUtils.createElement('span', { id: 'rd-player-title' }); title.innerHTML = (isAudio ? Icons.music : Icons.film) + (isAudio ? ' 音频播放器' : ' 媒体播放器'); const closeBtn = SecurityUtils.createElement('button', { className: 'rd-close-btn', id: 'rd-close-player', title: '关闭' }); closeBtn.innerHTML = Icons.close; header.appendChild(title); header.appendChild(closeBtn); player.appendChild(header); const wrapper = SecurityUtils.createElement('div', { id: 'rd-media-wrapper' }); if (isAudio) { wrapper.style.padding = '40px'; const audio = document.createElement('audio'); audio.controls = true; audio.autoplay = true; audio.style.width = '100%'; const source = document.createElement('source'); source.src = realUrl; source.type = mime; audio.appendChild(source); audio.appendChild(document.createTextNode('不支持该格式')); wrapper.appendChild(audio); } else { const video = document.createElement('video'); video.id = 'rd-video'; video.controls = true; video.autoplay = true; video.playsInline = true; const source = document.createElement('source'); source.src = realUrl; source.type = mime; video.appendChild(source); video.appendChild(document.createTextNode('不支持该格式')); wrapper.appendChild(video); const adjustVideoSize = () => { const container = wrapper; if (!container || !video) return; const containerWidth = container.clientWidth; const containerHeight = container.clientHeight; const videoRatio = video.videoWidth / video.videoHeight; const containerRatio = containerWidth / containerHeight; if (videoRatio > containerRatio) { video.style.width = '100%'; video.style.height = 'auto'; } else { video.style.width = 'auto'; video.style.height = '100%'; } }; video.addEventListener('loadedmetadata', adjustVideoSize, { once: true }); video.addEventListener('canplay', adjustVideoSize, { once: true }); const resizeHandler = () => { if (document.getElementById('rd-inline-player')) { adjustVideoSize(); } else { window.removeEventListener('resize', resizeHandler); } }; window.addEventListener('resize', resizeHandler, { passive: true }); this._listeners.push(() => window.removeEventListener('resize', resizeHandler)); } player.appendChild(wrapper); const closeHandler = () => player.remove(); closeBtn.addEventListener('click', closeHandler); document.body.appendChild(player); }, getMimeType(url) { try { const urlObj = new URL(url, location.href); const ext = urlObj.pathname.split('.').pop()?.toLowerCase() || ''; const mimeTypes = { mp4: 'video/mp4', webm: 'video/webm', ogg: 'video/ogg', ogv: 'video/ogg', mov: 'video/quicktime', avi: 'video/x-msvideo', mkv: 'video/x-matroska', flv: 'video/x-flv', m3u8: 'application/x-mpegURL', mpd: 'application/dash+xml', mp3: 'audio/mpeg', wav: 'audio/wav', flac: 'audio/flac', aac: 'audio/aac', m4a: 'audio/mp4', wma: 'audio/x-ms-wma', oga: 'audio/ogg', weba: 'audio/webm', opus: 'audio/opus' }; return mimeTypes[ext] || 'video/mp4'; } catch { return 'video/mp4'; } }, manualRefresh() { const entries = performance.getEntriesByType('resource'); const processed = new Set(); entries.forEach(entry => { if (!entry.name || processed.has(entry.name)) return; processed.add(entry.name); const url = entry.name; if (!SecurityUtils.isSafeUrl(url)) return; if (processedUrls.has(url)) return; const status = entry.responseStatus || 200; if (status >= 400) return; const displayType = this.getDisplayType(url, entry.responseContentType || '', entry.initiatorType); const filterType = this.getFilterType(displayType); processedUrls.add(url); let size = entry.transferSize || entry.encodedBodySize || 0; if ((url.startsWith('data:') || url.startsWith('blob:')) && size === 0) size = url.length; this.addResource({ url, displayType, filterType, status, size, duration: entry.duration || 0, cached: entry.transferSize === 0 && entry.encodedBodySize > 0 }); }); }, destroy() { this.isDestroyed = true; if (this._bodyCheckInterval) { clearInterval(this._bodyCheckInterval); this._bodyCheckInterval = null; } if (this._refreshTimer) { clearInterval(this._refreshTimer); this._refreshTimer = null; } if (this._rafId) { cancelAnimationFrame(this._rafId); this._rafId = null; } if (this._resourceObserver) { this._resourceObserver.disconnect(); this._resourceObserver = null; } this._listeners.forEach(cleanup => cleanup()); this._listeners = []; XMLHttpRequest.prototype.open = OriginalAPIs.xhrOpen; XMLHttpRequest.prototype.send = OriginalAPIs.xhrSend; window.fetch = OriginalAPIs.fetch; history.pushState = OriginalAPIs.pushState; history.replaceState = OriginalAPIs.replaceState; const elementsToRemove = [ 'rd-floating-btn', 'resource-diary-container', 'rd-all-styles', 'rd-toast', 'rd-inline-player', 'rd-img-preview', 'rd-text-preview' ]; elementsToRemove.forEach(id => { const el = document.getElementById(id); if (el) el.remove(); }); this.resources = []; this.filteredResources = []; processedUrls.clear(); delete window.ResourceDiary; this.isInitialized = false; } }; ResourceDiary.init(); window.ResourceDiary = ResourceDiary; window.addEventListener('beforeunload', () => { if (window.ResourceDiary) { window.ResourceDiary.destroy(); } }, { once: true }); })();