// ==UserScript==
// @name 加载日记
// @namespace yuelan-resource-diary
// @version 7.7
// @description 网页资源捕获工具
// @author Kimi && DeepSeek
// @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',
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;
document.getElementById('resource-diary-container')?.remove();
document.getElementById('rd-all-styles')?.remove();
this.setupUI();
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 ? 'rgba(28,28,28,0.85)' : 'rgba(255,255,255,0.85)'};
--rd-bg-secondary: ${isDark ? 'rgba(44,44,44,0.9)' : 'rgba(245,245,245,0.9)'};
--rd-bg-tertiary: ${isDark ? 'rgba(60,60,60,0.9)' : 'rgba(235,235,235,0.9)'};
--rd-border-color: ${isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.08)'};
--rd-text-primary: ${isDark ? '#ffffff' : '#1f1f1f'};
--rd-text-secondary: ${isDark ? '#b0b0b0' : '#5e5e5e'};
--rd-accent: #0A59F7;
--rd-accent-hover: #0842c0;
--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: 40px;
height: 40px;
border: none !important;
background: var(--rd-bg-secondary);
color: var(--rd-text-primary);
border-radius: 24px;
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 8px rgba(0,0,0,0.05);
flex-shrink: 0;
will-change: transform, box-shadow;
transform: translateZ(0);
backdrop-filter: blur(10px);
}
.rd-close-btn:hover {
border-color: var(--rd-accent) !important;
transform: scale(1.05) translateZ(0);
box-shadow: 0 6px 16px rgba(0,0,0,0.1);
}
.rd-close-btn:active {
transform: scale(0.95) translateZ(0);
}
#rd-floating-btn {
position: fixed;
bottom: 140px;
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;
bottom: 0;
left: 0;
right: 0;
top: 10px;
transform: translateY(100%);
transition: transform 0.4s cubic-bezier(0.2, 0.9, 0.3, 1);
width: 100%;
height: calc(100vh - 10px);
background: var(--rd-bg-primary);
border-radius: 28px 28px 0 0;
box-shadow: 0 -8px 30px rgba(0,0,0,0.12);
z-index: 2147483647;
display: flex;
flex-direction: column;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
overflow: hidden;
backdrop-filter: blur(30px) saturate(180%);
-webkit-backdrop-filter: blur(30px) saturate(180%);
will-change: transform;
contain: layout paint;
}
#rd-panel.active {
transform: translateY(0);
}
.rd-header {
padding: 16px 20px 8px 20px;
background: transparent;
display: flex;
align-items: center;
flex-shrink: 0;
gap: 12px;
min-width: 0;
overflow: hidden;
contain: layout;
}
.rd-header h3 {
margin: 0;
font-size: 22px;
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-controls {
display: flex;
gap: 8px;
flex-shrink: 0;
margin-left: auto;
}
.rd-toolbar {
background: transparent;
flex-shrink: 0;
padding: 8px 20px;
}
.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: 10px;
padding: 4px 0;
}
.rd-filter {
padding: 8px 16px;
background: var(--rd-bg-secondary);
border-radius: 30px;
cursor: pointer;
font-size: 15px;
font-weight: 500;
color: var(--rd-text-secondary);
transition: all 0.2s;
display: inline-flex;
align-items: center;
gap: 8px;
white-space: nowrap;
border: none;
will-change: transform, box-shadow;
transform: translateZ(0);
backdrop-filter: blur(10px);
}
.rd-filter:hover {
color: var(--rd-accent);
transform: translateY(-1px) translateZ(0);
}
.rd-filter.active {
background: var(--rd-accent);
color: #fff;
}
.rd-filter svg {
width: 18px;
height: 18px;
display: inline-block;
flex-shrink: 0;
vertical-align: middle;
}
.rd-filter .rd-filter-count {
display: inline-block;
vertical-align: middle;
margin-left: 4px;
font-size: 13px;
font-weight: 600;
}
.rd-filter-count {
font-size: 13px;
opacity: 0.9;
font-weight: 600;
}
.rd-list {
flex: 1;
overflow-y: auto;
padding: 16px 20px 24px 20px;
background: transparent;
scroll-behavior: smooth;
-webkit-overflow-scrolling: touch;
contain: layout;
will-change: scroll-position;
}
.rd-list-content {
display: flex;
flex-direction: column;
gap: 12px;
contain: layout;
}
.rd-entry {
display: flex;
gap: 12px;
padding: 12px;
background: var(--rd-bg-secondary);
border-radius: 24px;
font-size: 13px;
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;
backdrop-filter: blur(10px);
}
.rd-entry:hover {
transform: translateX(4px) translateZ(0);
box-shadow: 0 8px 20px rgba(0,0,0,0.06);
}
.rd-entry-thumb-wrapper {
width: 60px;
height: 60px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
background: var(--rd-bg-tertiary);
border-radius: 20px;
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: 28px;
height: 28px;
stroke: currentColor;
}
.rd-entry-content {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
justify-content: center;
gap: 6px;
padding: 2px 0;
contain: layout;
}
.rd-entry-header {
display: flex;
align-items: center;
gap: 10px;
height: 26px;
}
.rd-entry-type {
color: white;
padding: 2px 10px;
border-radius: 30px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
height: 22px;
line-height: 18px;
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: 6px;
margin-left: auto;
align-items: center;
height: 100%;
}
.rd-action-btn {
cursor: pointer;
width: 32px;
height: 32px;
border-radius: 10px;
background: var(--rd-bg-tertiary);
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);
backdrop-filter: blur(5px);
}
.rd-action-btn:hover {
background: var(--rd-bg-secondary);
transform: scale(1.1) translateZ(0);
}
.rd-action-btn:active {
transform: scale(0.95) translateZ(0);
}
.rd-action-btn svg {
width: 18px;
height: 18px;
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: 15px;
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
contain: layout;
}
.rd-empty svg {
width: 56px;
height: 56px;
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);
backdrop-filter: blur(30px) saturate(180%);
-webkit-backdrop-filter: blur(30px) saturate(180%);
}
#rd-inline-player {
top: 50%;
left: 50%;
width: 92%;
max-width: 780px;
height: 78vh;
max-height: 78vh;
background: var(--rd-player-bg);
border-radius: 32px;
overflow: hidden;
box-shadow: 0 30px 60px rgba(0,0,0,0.3);
display: flex;
flex-direction: column;
contain: layout paint;
}
#rd-player-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
background: transparent;
flex-shrink: 0;
height: 64px;
box-sizing: border-box;
contain: layout;
}
#rd-player-title {
color: var(--rd-text-primary);
font-size: 18px;
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
}
#rd-player-title svg {
width: 22px;
height: 22px;
stroke: currentColor;
}
#rd-media-wrapper {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
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: 780px;
height: 78vh;
max-height: 78vh;
background: var(--rd-player-bg);
border-radius: 32px;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 30px 60px rgba(0,0,0,0.3);
contain: layout paint;
}
#rd-img-preview-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
background: transparent;
height: 64px;
box-sizing: border-box;
flex-shrink: 0;
contain: layout;
}
#rd-img-preview-title {
color: var(--rd-text-primary);
font-size: 18px;
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: 20px;
background: transparent;
cursor: zoom-out;
contain: layout paint;
}
#rd-preview-img {
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
object-fit: contain;
border-radius: 24px;
cursor: default;
will-change: transform;
}
#rd-text-preview {
top: 50%;
left: 50%;
width: 92%;
max-width: 780px;
height: 78vh;
background: var(--rd-player-bg);
border-radius: 32px;
box-shadow: 0 30px 60px rgba(0,0,0,0.3);
display: flex;
flex-direction: column;
overflow: hidden;
contain: layout paint;
}
#rd-text-preview-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
background: transparent;
height: 64px;
box-sizing: border-box;
contain: layout;
flex-shrink: 0;
}
#rd-text-preview-title {
font-weight: 600;
color: var(--rd-text-primary);
font-size: 18px;
display: flex;
align-items: center;
gap: 8px;
}
#rd-text-preview-scroll {
flex: 1;
overflow: auto;
padding: 20px;
background: transparent;
-webkit-overflow-scrolling: touch;
}
#rd-text-preview-content {
margin: 0;
padding: 0;
font-family: 'SF Mono', Monaco, Consolas, 'Courier New', monospace;
font-size: 14px;
line-height: 1.6;
color: var(--rd-text-primary);
white-space: pre-wrap;
word-break: break-all;
background: transparent;
border: none;
outline: none;
}
.rd-iframe-container {
flex: 1;
position: relative;
background: transparent;
overflow: hidden;
contain: layout paint;
}
.rd-iframe-container iframe {
width: 100%;
height: 100%;
border: none;
background: transparent;
}
.rd-list::-webkit-scrollbar,
#rd-text-preview-scroll::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.rd-list::-webkit-scrollbar-track,
#rd-text-preview-scroll::-webkit-scrollbar-track {
background: transparent;
border-radius: 3px;
}
.rd-list::-webkit-scrollbar-thumb,
#rd-text-preview-scroll::-webkit-scrollbar-thumb {
background: var(--rd-border-color);
border-radius: 3px;
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) transparent;
}
`;
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;
},
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 = `
`;
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;
}
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 list = document.getElementById('rd-list');
if (list) {
let touchStartX = 0, touchStartY = 0, isHorizontalSwipe = false, 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');
});
}
const blobMatches = bg.match(/url\(["']?(blob:[^"')]+)["']?\)/g);
if (blobMatches) {
blobMatches.forEach(match => {
const url = match.replace(/url\(["']?/, '').replace(/["']?\)$/, '');
if (url.startsWith('blob:')) this.tryAddBlobUrl(url, 'css-bg', 'image');
});
}
}
} 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', 'image');
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', 'image');
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, suggestedType) {
if (!SecurityUtils.isSafeUrl(url)) return;
if (processedUrls.has(url)) return;
processedUrls.add(url);
let displayType;
if (suggestedType) {
displayType = suggestedType;
} else {
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() {
this.filteredResources = this.resources.filter(r => {
if (this.currentFilter !== 'all' && r.filterType !== this.currentFilter) 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 = ``;
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 });
})();