// ==UserScript== // @name 鸿蒙资源日记 // @namespace harmony-resource-diary // @version 2.0.0 // @description 鸿蒙浏览器原生资源捕获工具 // @author yc // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJnIiB4MT0iMCUiIHkxPSIwJSIgeDI9IjEwMCUiIHkyPSIxMDAlIj48c3RvcCBvZmZzZXQ9IjAlIiBzdG9wLWNvbG9yPSIjMEE4NEZGIi8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjNUJDMEJFIi8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHJlY3Qgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiByeD0iNiIgZmlsbD0idXJsKCNnKSIvPjxwYXRoIGQ9Ik0zLjUgNy41TDEyIDMuNUwyMC41IDcuNUwxMiAxMS41WiIgZmlsbD0iI2ZmZiIgZmlsbC1vcGFjaXR5PSIwLjkyIi8+PHBhdGggZD0iTTMuNSA3LjVMMTIgMTEuNVYyMC41TDMuNSAxNi41WiIgZmlsbD0iI2ZmZiIgZmlsbC1vcGFjaXR5PSIwLjYiLz48cGF0aCBkPSJNMjAuNSA3LjVMMTIgMTEuNVYyMC41TDIwLjUgMTYuNVoiIGZpbGw9IiNmZmYiIGZpbGwtb3BhY2l0eT0iMC42Ii8+PHBhdGggZD0iTTMuNSA3LjVMMTIgMy41TDIwLjUgNy41TDEyIDExLjVaIiBmaWxsPSJub25lIiBzdHJva2U9IiNmZmYiIHN0cm9rZS1vcGFjaXR5PSIwLjYiIHN0cm9rZS13aWR0aD0iMC42IiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PHBhdGggZD0iTTMuNSA3LjVMMTIgMTEuNVYyMC41TDMuNSAxNi41WiIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utb3BhY2l0eT0iMC42IiBzdHJva2Utd2lkdGg9IjAuNiIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjxwYXRoIGQ9Ik0yMC41IDcuNUwxMiAxMS41VjIwLjVMMjAuNSAxNi41WiIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utb3BhY2l0eT0iMC42IiBzdHJva2Utd2lkdGg9IjAuNiIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjwvc3ZnPg== // @match *://*/* // @grant GM_download // @grant GM_setValue // @grant GM_getValue // @run-at document-end // @license MIT // ==/UserScript== (function () { 'use strict'; if (window.__HRD_2024__) return; window.__HRD_2024__ = true; const IN_HTTP = /^https?:$/.test(location.protocol); const IN_IFRAME = window.top !== window.self; const CONFIG = { MAX_RESOURCES: 1200, MAX_URL_CACHE: 6000, MAX_INVALID_CACHE: 4000, BATCH_SIZE: 60, IDLE_SCAN_DELAY: 1200, FULL_SCAN_INTERVAL: 15000, SHEET_TOP_GAP: 8, SWIPE_MIN_DX: 44, SWIPE_MAX_DY: 120, SWIPE_MAX_TIME: 1200, MAX_SOURCE_LINES: 20000, MAX_SOURCE_BYTES: 3 * 1024 * 1024, SOURCE_CHUNK: 400, LIST_RENDER_CHUNK: 24, LIST_OPEN_GUARD_MS: 600, UNWRAP_MARGIN_MS: 2000, THUMB_TIMEOUT_MS: 5500, INLINE_READ_MAX_BYTES: 12 * 1024 * 1024, MIN_TRACK_SIZE: 32, DATA_URI_MAX_LENGTH: 60000 }; const PLAYER_ASSET = { HLS: 'https://cdn.jsdelivr.net/npm/hls.js@1.5.17/dist/hls.min.js', HLS_ALT: [ 'https://unpkg.com/hls.js@1.5.17/dist/hls.min.js', 'https://cdnjs.cloudflare.com/ajax/libs/hls.js/1.5.17/hls.umd.min.js' ], CRYPTO: [ 'https://cdn.jsdelivr.net/npm/crypto-js@4.2.0/crypto-js.min.js', 'https://unpkg.com/crypto-js@4.2.0/crypto-js.min.js' ] }; const Crypto = { _lib: null, _loading: null, ready() { if (this._lib) return Promise.resolve(this._lib); if (window.CryptoJS && window.CryptoJS.AES && window.CryptoJS.lib && window.CryptoJS.lib.WordArray) { this._lib = window.CryptoJS; return Promise.resolve(this._lib); } if (this._loading) return this._loading; const self = this; const list = PLAYER_ASSET.CRYPTO.slice(0); const load = i => { if (i >= list.length) return Promise.reject(new Error('crypto-js 加载失败')); return self._inject(list[i]).then(() => { if (window.CryptoJS && window.CryptoJS.AES) { self._lib = window.CryptoJS; return self._lib; } return load(i + 1); }).catch(() => load(i + 1)); }; this._loading = load(0).then(lib => { self._loading = null; return lib; }).catch(err => { self._loading = null; throw err; }); return this._loading; }, _inject(src) { return new Promise((resolve, reject) => { try { const seg = src.split('/').pop() || src; const probe = document.querySelector('script[data-hrd-crypto="' + seg + '"]'); if (probe && probe.dataset.hrdLoaded === '1') { resolve(); return; } if (probe) { probe.addEventListener('load', () => resolve()); probe.addEventListener('error', () => reject(new Error('script error'))); return; } const s = document.createElement('script'); s.src = src; s.async = true; s.setAttribute('data-hrd-crypto', seg); s.onload = () => { s.dataset.hrdLoaded = '1'; resolve(); }; s.onerror = () => { try { s.remove(); } catch {} reject(new Error('script error')); }; (document.head || document.documentElement).appendChild(s); } catch (e) { reject(e); } }); }, get lib() { return this._lib || window.CryptoJS || null; }, decoded(v) { const L = this.lib; if (!L || !L.enc) return null; try { const s = String(v); if (/^[0-9a-f]{16}$/i.test(s) || /^[0-9a-f]{32}$/i.test(s)) { return L.enc.Hex.parse(s); } return L.enc.Utf8.parse(s); } catch { return null; } }, parsed(v) { const L = this.lib; if (!L || !L.enc) return null; try { return L.enc.Utf8.parse(String(v)); } catch { return null; } }, AES() { const L = this.lib; return L ? L.AES : null; }, mode() { const L = this.lib; return L ? L.mode : null; }, pad() { const L = this.lib; return L ? L.pad : null; }, lib_() { const L = this.lib; return L ? L.lib : null; } }; const MediaHeaders = { _config: null, _configAt: 0, _rules: [], _hits: 0, readConfig() { let el = null; try { el = document.getElementById('cdn-config'); } catch {} if (!el || !el.getAttribute) return null; const g = (k) => el.getAttribute(k) || ''; const rawKey = g('data-video_cdn_auth_key'); if (!rawKey) return null; return { m3u8: g('data-video_m3u8_cdn_enabled') === '1', other: g('data-video_other_cdn_enabled') === '1', key: rawKey, value: g('data-video_cdn_auth_value'), app: g('data-cdn_app') }; }, sync(force) { const now = Date.now(); if (!force && this._config && now - this._configAt < 5000) return; this._configAt = now; let cfg = null; try { cfg = this.readConfig(); } catch {} if (!cfg || !cfg.value) { this._rules = []; this._config = null; return; } this._config = cfg; const rules = []; if (cfg.m3u8) { rules.push({ test: /\.m3u8(?:[?#]|$)/i, key: cfg.key, value: cfg.value, app: cfg.app }); } if (cfg.other) { rules.push({ test: /\.(?:ts|key)(?:[?#]|$)/i, key: cfg.key, value: cfg.value, app: cfg.app }); } this._rules = rules; }, match(url) { if (!url || typeof url !== 'string') return null; this.sync(false); for (let i = 0; i < this._rules.length; i++) { if (this._rules[i].test.test(url)) return this._rules[i]; } return null; }, headers(url) { const rule = this.match(url); if (!rule) return null; const h = {}; h[rule.key] = rule.value; if (rule.app) h['x-cdn-app'] = rule.app; return h; }, capture(xhr, url) { try { const rule = this.match(url); if (!rule || !xhr || typeof xhr.getRequestHeader !== 'function') return; let v = ''; try { v = xhr.getRequestHeader(rule.key) || ''; } catch {} if (!v) return; if (rule.key === 'x-cdn-auth' && this._config && this._config.value !== v) { this._config.value = v; this._configAt = Date.now(); this.sync(true); } if (v && this._hits < 200) this._hits++; } catch {} }, has(force) { this.sync(!!force); return this._rules.length > 0; }, describe() { this.sync(true); if (!this._rules.length) return ''; const parts = []; if (this._config && this._config.m3u8) parts.push('m3u8'); if (this._config && this._config.other) parts.push('ts/key'); return parts.join('+'); } }; const DANGEROUS_PROTOCOLS = [ 'javascript:', 'vbscript:', 'about:', 'file:', 'ftp:', 'telnet:', 'ssh:' ]; const INVALID_URL_PATTERNS = [ /(?:^|[/_.-])(?:1x1|pixel|spacer|blank|empty|transparent|clear|tracking|track|beacon|ping)[._-]?(?:gif|png|jpg|jpeg|webp)(?:[?#]|$)/i, /google-analytics\.com/i, /googletagmanager\.com/i, /doubleclick\.net/i, /googleadservices\.com/i, /googlesyndication\.com/i, /facebook\.com\/tr/i, /connect\.facebook\.net/i, /hotjar\.com/i, /mixpanel\.com/i, /segment\.(io|com)/i, /amplitude\.com/i, /sentry\.io/i, /bugsnag\.com/i, /newrelic\.com/i, /\.clarity\.ms/i, /matomo\.(org|cloud)/i, /plausible\.io/i, /umami\.is/i, /baidu\.com\/hm\.js/i, /cnzz\.com/i, /hm\.baidu\.com/i, /\/hm\.js(?:[?#]|$)/i, /(?:^|[?&])utm_[a-z]+=/i, /^data:image\/svg\+xml.*?(?:width=['"]?1(?:px)?['"]?|height=['"]?1(?:px)?['"]?)/i ]; const ANALYTICS_HOST_RE = /(?:^|\.)(?:google-analytics|googletagmanager|doubleclick|googleadservices|googlesyndication|connect\.facebook|hotjar|mixpanel|amplitude|sentry|bugsnag|newrelic|clarity|matomo|plausible|umami|cnzz|hm\.baidu|segment)\.(?:com|io|org|cloud|net|cn|is|ms)$/i; const ANALYTICS_PATH_RE = new RegExp( '(?:^|[/_.-])hm\\.(?:js|gif)(?:[?#]|$)' + '|(?:^|/)(?:analytics|gtag|gtm|beacon|tracking|track|stat|1x1|pixel)[-_.]?[^/]*\\.(?:js|gif|png)(?:[?#]|$)' + '|(?:^|/)(?:analytics|beacon|tracking|track|stat|1x1|pixel)$', 'i'); const MIN_THUMB_SIZE = 16; const MAX_THUMB_FAILS = 2; const EXT_TO_TYPE = { mp4: 'mp4', webm: 'webm', ogg: 'ogg', ogv: 'ogv', mov: 'mov', avi: 'avi', mkv: 'mkv', flv: 'flv', m3u8: 'm3u8', mpd: 'mpd', ts: 'ts', m4v: 'm4v', '3gp': '3gp', wmv: 'wmv', m2ts: 'ts', mp3: 'mp3', wav: 'wav', flac: 'flac', aac: 'aac', m4a: 'm4a', wma: 'wma', oga: 'oga', weba: 'weba', opus: 'opus', aiff: 'aiff', mid: 'mid', midi: 'mid', ac3: 'ac3', amr: 'amr', 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', xml: 'xml', woff: 'font', woff2: 'font', ttf: 'font', otf: 'font', eot: 'font', pdf: 'other', html: 'other', htm: 'other' }; const MIME_TO_TYPE = { 'video/mp4': 'mp4', 'video/webm': 'webm', 'video/ogg': 'ogg', 'video/quicktime': 'mov', 'video/x-matroska': 'mkv', 'video/x-flv': 'flv', 'video/x-msvideo': 'avi', 'video/3gpp': '3gp', 'video/x-m4v': 'm4v', 'application/vnd.apple.mpegurl': 'm3u8', 'application/x-mpegurl': 'm3u8', 'application/dash+xml': 'mpd', 'video/mp2t': 'ts', 'audio/mpeg': 'mp3', 'audio/wav': 'wav', 'audio/x-wav': 'wav', 'audio/flac': 'flac', 'audio/aac': 'aac', 'audio/ogg': 'oga', 'audio/webm': 'weba', 'audio/opus': 'opus', 'audio/x-ms-wma': 'wma', 'audio/midi': 'mid', 'audio/x-m4a': 'm4a', 'audio/mp4': 'm4a', 'image/jpeg': 'jpg', 'image/png': 'png', 'image/gif': 'gif', 'image/webp': 'webp', 'image/svg+xml': 'svg', 'image/avif': 'avif', 'image/bmp': 'bmp', 'image/x-icon': 'ico', 'image/vnd.microsoft.icon': 'ico', 'image/tiff': 'bmp', 'image/jxl': 'jxl', 'text/css': 'stylesheet', 'application/javascript': 'script', 'text/javascript': 'script', 'application/x-javascript': 'script', 'application/json': 'json', 'application/xml': 'xml', 'text/xml': 'xml' }; const TYPE_ICON_MAP = { mp4: 'video', webm: 'video', ogg: 'video', ogv: 'video', mov: 'video', avi: 'video', mkv: 'video', flv: 'video', m3u8: 'video', mpd: 'video', ts: 'video', m4v: 'video', '3gp': 'video', wmv: 'video', video: 'video', mp3: 'audio', wav: 'audio', flac: 'audio', aac: 'audio', m4a: 'audio', wma: 'audio', oga: 'audio', weba: 'audio', opus: 'audio', audio: 'audio', aiff: 'audio', mid: 'audio', ac3: 'audio', amr: 'audio', jpg: 'image', jpeg: 'image', png: 'image', gif: 'image', webp: 'image', svg: 'image', ico: 'image', bmp: 'image', avif: 'image', jxl: 'image', image: 'image', script: 'code', js: 'code', mjs: 'code', stylesheet: 'code', css: 'code', xml: 'code', json: 'json', font: 'font', woff: 'font', woff2: 'font', ttf: 'font', otf: 'font', eot: 'font', xhr: 'link', fetch: 'link', datauri: 'link', other: 'doc', m3u8_variant: 'video' }; const TYPE_TAG_MAP = { mp4: 'MP4', webm: 'WEBM', ogg: 'OGG', ogv: 'OGV', mov: 'MOV', avi: 'AVI', mkv: 'MKV', flv: 'FLV', m3u8: 'M3U8', mpd: 'MPD', ts: 'TS', m4v: 'M4V', '3gp': '3GP', wmv: 'WMV', mp3: 'MP3', wav: 'WAV', flac: 'FLAC', aac: 'AAC', m4a: 'M4A', wma: 'WMA', oga: 'OGA', weba: 'WEBA', opus: 'OPUS', aiff: 'AIFF', mid: 'MIDI', ac3: 'AC3', amr: 'AMR', jpg: 'JPG', png: 'PNG', gif: 'GIF', webp: 'WEBP', svg: 'SVG', ico: 'ICO', bmp: 'BMP', avif: 'AVIF', jxl: 'JXL', css: 'CSS', js: 'JS', mjs: 'JS', json: 'JSON', xml: 'XML', script: 'JS', stylesheet: 'CSS', woff: 'FONT', woff2: 'FONT', ttf: 'FONT', otf: 'FONT', eot: 'FONT', font: 'FONT', xhr: 'XHR', fetch: 'FETCH', datauri: 'DATA', other: 'FILE', image: 'IMG', audio: 'AUDIO', video: 'VIDEO' }; const MEDIA_TYPES = new Set([ 'mp4', 'webm', 'ogg', 'ogv', 'mov', 'avi', 'mkv', 'flv', 'm3u8', 'mpd', 'ts', 'm4v', '3gp', 'wmv', 'mp3', 'wav', 'flac', 'aac', 'm4a', 'wma', 'oga', 'weba', 'opus', 'audio', 'aiff', 'mid', 'ac3', 'amr', 'video' ]); const IMAGE_TYPES = new Set([ 'jpg', 'png', 'gif', 'webp', 'svg', 'ico', 'bmp', 'avif', 'jxl', 'image' ]); const VIDEO_PLAY_TYPES = new Set([ 'mp4', 'webm', 'ogg', 'ogv', 'mov', 'm3u8', 'm4v', 'mkv', 'ts' ]); const SORT_MODES = [ { key: 'name-asc', label: '名称 A→Z', icon: 'sortNameAsc' }, { key: 'name-desc', label: '名称 Z→A', icon: 'sortNameDesc' }, { key: 'time-desc', label: '最新优先', icon: 'sortTimeDesc' }, { key: 'time-asc', label: '最旧优先', icon: 'sortTimeAsc' } ]; const FILTER_ORDER = ['image', 'media', 'other']; const Icons = { appIcon: ``, cube3d: ``, close: ``, check: ``, wrapText: ``, noWrapText: ``, image: ``, media: ``, doc: ``, video: ``, audio: ``, code: ``, font: ``, json: ``, link: ``, openInNew: ``, copy: ``, play: ``, download: ``, sortTimeDesc: ``, sortTimeAsc: ``, sortNameAsc: ``, sortNameDesc: ``, listView: ``, gridView: ``, warn: ``, emptyState: `` }; function hashKey(str) { let h1 = 0x811c9dc5; let h2 = 0x01000193; const s = String(str); const len = Math.min(s.length, 4096); for (let i = 0; i < len; i++) { const c = s.charCodeAt(i); h1 ^= c; h1 = (h1 + ((h1 << 1) + (h1 << 4) + (h1 << 7) + (h1 << 8) + (h1 << 24))) >>> 0; h2 = ((h2 << 5) - h2 + c) >>> 0; } return (h1.toString(36) + h2.toString(36)); } function fastInvalid(url) { const lower = url.length > 512 ? url.slice(0, 512).toLowerCase() : url.toLowerCase(); let host = ''; try { host = new URL(url, location.href).hostname; } catch { return false; } if (ANALYTICS_HOST_RE.test(host)) return true; return ANALYTICS_PATH_RE.test(lower.split('?')[0]); } function guessImageName(u) { try { const q = u.search; const inner = q.match(/[?&](?:url|src|image|img|path)=([^&#]+)/i); if (inner) { try { const dec = decodeURIComponent(inner[1]); const base = dec.split('/').pop().split('?')[0]; if (base && /\.[a-z0-9]{1,6}$/i.test(base)) return base; } catch {} } if (CDN_IMAGE_EXT_RE.test(u.pathname)) { const base = u.pathname.split('/').pop(); if (base) return base; } const isIcon = ICON_PATH_RE.test(u.pathname); const hint = q.match(/[?&](?:format|type|ext|fm|f)=([a-z0-9]{2,5})/i); if (isIcon || guessMimeIsImage(u, hint)) { const ext = hint ? hint[1].toLowerCase() : 'png'; const tag = isIcon ? 'icon' : 'img'; const stem = (u.pathname.split('/').filter(Boolean).pop() || tag) .replace(/\.[a-z0-9]{1,6}$/i, '').replace(/[^a-z0-9_-]/gi, '_').slice(0, 40); return stem + '_' + hashKey(u.pathname + q).slice(0, 6) + '.' + ext; } } catch {} return ''; } function guessMimeIsImage(u, hint) { if (!hint) return false; const v = hint[1].toLowerCase(); return v === 'png' || v === 'jpg' || v === 'jpeg' || v === 'webp' || v === 'gif' || v === 'avif' || v === 'svg' || v === 'ico'; } const ICON_PATH_RE = new RegExp( '^/?(?:api|apis)/v\\d+/(?:[a-z0-9_.-]+/)*icon(?:[?#]|$)' + '|(?:^|/)icon/[a-z0-9_-]+(?:[?#]|$)' + '|/avatar/[a-z0-9_-]+(?:[?#]|$)' + '|/logo\\.(?:png|jpe?g|svg|webp)(?:[?#]|$)', 'i'); const CDN_IMAGE_EXT_RE = /\.(?:png|jpe?g|gif|webp|avif|bmp|svg|ico|heic|heif|apng)($|[?#])/i; const DataURI = { safeMime: /^(image\/(?!svg\+xml$)|audio\/|video\/|font\/)/, safeText: new Set(['text/plain', 'text/csv', 'text/markdown']), okMime: new Set([ 'application/json', 'application/font-woff', 'application/font-woff2', 'application/vnd.ms-fontobject', 'application/x-font-ttf', 'application/x-font-opentype', 'application/octet-stream' ]), extFromMime(mime) { const sub = mime.split('/')[1] || ''; if (sub.indexOf('svg') >= 0) return 'svg'; if (sub.indexOf('jpeg') >= 0 || sub.indexOf('jpg') >= 0) return 'jpg'; if (sub.indexOf('mpeg') >= 0) return 'mp3'; if (sub.indexOf('wav') >= 0 || sub.indexOf('wave') >= 0) return 'wav'; if (sub.indexOf('woff2') >= 0) return 'woff2'; if (sub.indexOf('woff') >= 0) return 'woff'; if (sub.indexOf('ttf') >= 0) return 'ttf'; if (sub.indexOf('otf') >= 0) return 'otf'; const clean = sub.replace(/^x-/, '').replace(/\+xml$/, ''); return (clean || 'bin').replace(/[^a-z0-9]/g, '') || 'bin'; } }; const Security = { _safe: new Map(), _invalid: new Map(), isSafe(url) { if (!url || typeof url !== 'string') return false; const cached = this._safe.get(url); if (cached !== undefined) return cached; if (this._safe.size > 1200) { let drop = 600; for (const k of this._safe.keys()) { this._safe.delete(k); if (--drop <= 0) break; } } const result = this._checkSafe(url); this._safe.set(url, result); return result; }, _checkSafe(url) { const lower = url.toLowerCase().trim(); const protoEnd = lower.indexOf(':'); if (protoEnd < 0) return true; const proto = lower.slice(0, protoEnd + 1); for (let i = 0; i < DANGEROUS_PROTOCOLS.length; i++) { if (proto === DANGEROUS_PROTOCOLS[i]) return false; } if (proto !== 'data:') return true; const head = lower.slice(5, 200); const semi = head.indexOf(';'); const comma = head.indexOf(','); let mime = head; if (semi >= 0 && (comma < 0 || semi < comma)) mime = head.slice(0, semi); else if (comma >= 0) mime = head.slice(0, comma); mime = mime.trim(); if (!mime) return false; if (mime === 'image/svg+xml') return true; if (DataURI.safeMime.test(mime)) return true; if (DataURI.safeText.has(mime)) return true; const bare = mime.split(';')[0].trim(); return DataURI.okMime.has(bare); }, isInvalid(url) { if (!url || typeof url !== 'string') return true; const cached = this._invalid.get(url); if (cached !== undefined) return cached; if (this._invalid.size > CONFIG.MAX_INVALID_CACHE) { let drop = CONFIG.MAX_INVALID_CACHE / 2; for (const k of this._invalid.keys()) { this._invalid.delete(k); if (--drop <= 0) break; } } let invalid = false; if (fastInvalid(url)) { for (let i = 0; i < INVALID_URL_PATTERNS.length; i++) { if (INVALID_URL_PATTERNS[i].test(url)) { invalid = true; break; } } } this._invalid.set(url, invalid); return invalid; }, key(url) { if (!url || typeof url !== 'string') return null; const s = url.trim(); if (!s || s.length > 65536) return null; if (s.slice(0, 5).toLowerCase() === 'data:') { if (!this.isSafe(s)) return null; return 'data:' + s.length + ':' + s.slice(5, 165); } try { const u = new URL(s, location.href); if (u.protocol === 'blob:' || u.protocol === 'filesystem:') { return u.protocol + s; } u.hash = ''; if (u.protocol === 'http:' && u.port === '80') u.port = ''; if (u.protocol === 'https:' && u.port === '443') u.port = ''; return u.toString(); } catch { return null; } }, filename(url) { if (!url || typeof url !== 'string' || !url.trim()) return 'resource'; try { if (url.slice(0, 5).toLowerCase() === 'data:') { const head = url.slice(0, 200).toLowerCase(); const semi = head.indexOf(';'); const comma = head.indexOf(','); let mime = ''; if (semi >= 0) mime = head.slice(5, semi); else if (comma >= 0) mime = head.slice(5, comma); const ext = DataURI.extFromMime(mime); const body = url.slice(url.indexOf(',') + 1); return 'data_' + hashKey(body) .slice(0, 8) + '.' + ext; } const u = new URL(url, location.href); let path = u.pathname; try { path = decodeURIComponent(path); } catch {} const segs = path.split('/').filter(Boolean); let name = segs.length ? segs[segs.length - 1] : ''; if (!name || !/\.[a-z0-9]{1,6}$/i.test(name)) { const m = u.search.match(/[?&](?:file|filename|name|fn)=([^&#]+)/i); if (m) { try { name = decodeURIComponent(m[1]); } catch { name = m[1]; } } } if (!name || !/\.[a-z0-9]{1,6}$/i.test(name)) { const guess = guessImageName(u); if (guess) name = guess; } if (!name || name === '/') name = u.hostname || 'resource'; const clean = name.replace(/[<>:"/\\|?*\x00-\x1f]/g, '_').slice(0, 255); return clean || 'resource'; } catch { return 'resource_' + hashKey(String(url)).slice(0, 8); } }, displayType(url, ct) { if (ct) { const mt = ct.split(';')[0].trim().toLowerCase(); if (MIME_TO_TYPE[mt]) return MIME_TO_TYPE[mt]; if (mt.indexOf('video') >= 0) return 'mp4'; if (mt.indexOf('audio') >= 0) return 'mp3'; if (mt.indexOf('image') >= 0) return 'image'; if (mt.indexOf('script') >= 0) return 'script'; if (mt.indexOf('css') >= 0) return 'stylesheet'; if (mt.indexOf('font') >= 0 || mt.indexOf('woff') >= 0) return 'font'; } if (url.slice(0, 5).toLowerCase() === 'data:') { const head = url.slice(0, 160).toLowerCase(); const semi = head.indexOf(';'); const comma = head.indexOf(','); const mime = (semi >= 0 && (comma < 0 || semi < comma)) ? head.slice(5, semi) : (comma >= 0 ? head.slice(5, comma) : ''); if (mime.indexOf('font') >= 0 || mime.indexOf('woff') >= 0 || mime.indexOf('ttf') >= 0 || mime.indexOf('otf') >= 0) return 'font'; if (mime.indexOf('video') >= 0) return 'video'; if (mime.indexOf('audio') >= 0) return 'audio'; if (mime.indexOf('image/svg') >= 0) return 'svg'; if (mime.indexOf('image') >= 0) return 'image'; if (mime.indexOf('json') >= 0) return 'json'; if (mime.indexOf('javascript') >= 0) return 'script'; if (mime.indexOf('css') >= 0) return 'stylesheet'; return 'other'; } try { const u = new URL(url, location.href); const seg = u.pathname.split('/').pop() || ''; const dot = seg.lastIndexOf('.'); if (dot > 0) { const ext = seg.slice(dot + 1).toLowerCase(); if (EXT_TO_TYPE[ext]) return EXT_TO_TYPE[ext]; } if (ICON_PATH_RE.test(u.pathname)) return 'png'; const q = u.search.toLowerCase(); if (/(?:format|type|ext|fm)=(?:mp4|webm|mov|mkv)/.test(q)) return 'mp4'; if (/(?:format|type|ext|fm)=(?:mp3|wav|flac|aac|m4a|ogg)/.test(q)) return 'mp3'; if (/(?:format|type|ext|fm)=(?:png|jpe?g|gif|webp|avif)/.test(q)) return 'jpg'; } catch {} return 'other'; }, filterType(dt) { if (MEDIA_TYPES.has(dt)) return 'media'; if (IMAGE_TYPES.has(dt)) return 'image'; return 'other'; }, extFromMime(ct) { if (!ct || typeof ct !== 'string') return ''; const mt = ct.split(';')[0].trim().toLowerCase(); const mapped = MIME_TO_TYPE[mt]; if (mapped && mapped !== 'other' && mapped !== 'image') return mapped; if (mt === 'image/jpeg') return 'jpg'; if (mt === 'image/svg+xml') return 'svg'; if (mt.indexOf('image/') === 0) { const sub = mt.slice(6).replace(/^x-/, ''); return sub.replace(/[^a-z0-9]/g, '') || 'png'; } if (mt.indexOf('video/') === 0) return mt.slice(6).replace(/[^a-z0-9]/g, '') || 'mp4'; if (mt.indexOf('audio/') === 0) return mt.slice(6).replace(/[^a-z0-9]/g, '') || 'mp3'; return ''; }, ensureFilename(filename, url, ct) { let name = (filename || '').trim() || this.filename(url); name = name.replace(/[<>:"/\\|?*\x00-\x1f]/g, '_').slice(0, 255); if (!name) name = 'resource'; if (/\.[a-z0-9]{1,6}$/i.test(name)) return name; const ext = this.extFromMime(ct); const hash = hashKey(url).slice(0, 8); name = name === 'resource' ? ('resource_' + hash) : name; return ext ? (name + '.' + ext) : name; }, isDownloadable(u) { if (!u || typeof u !== 'string') return false; const s = u.trim(); if (!s) return false; try { const parsed = new URL(s, location.href); const proto = (parsed.protocol || '').toLowerCase(); return proto === 'http:' || proto === 'https:' || proto === 'blob:' || proto === 'data:'; } catch { return false; } }, isDirectDownloadable(u) { if (!u || typeof u !== 'string') return false; try { const parsed = new URL(u.trim(), location.href); return parsed.protocol === 'http:' || parsed.protocol === 'https:'; } catch { return false; } }, isLikelyResource(url, dt) { if (dt && dt !== 'other') return true; try { const u = new URL(url, location.href); if (u.protocol === 'data:' || u.protocol === 'blob:') return false; const seg = u.pathname.split('/').pop() || ''; if (/\.[a-z0-9]{1,6}$/i.test(seg)) return true; if (/[?&](?:file|filename|name|format|type|ext|fm)=/i.test(u.search)) return true; if (/[?&](?:url|src|image|img|path)=/i.test(u.search)) return true; if (ICON_PATH_RE.test(u.pathname)) return true; if (CDN_IMAGE_EXT_RE.test(u.pathname)) return true; return false; } catch { return false; } }, isWorthKeeping(url, dt, ct) { if (!url || typeof url !== 'string') return false; if (url.slice(0, 5).toLowerCase() === 'blob:') { return MEDIA_TYPES.has(dt) || IMAGE_TYPES.has(dt); } if (url.slice(0, 5).toLowerCase() === 'data:') { return IMAGE_TYPES.has(dt) || MEDIA_TYPES.has(dt); } return true; } }; const Lines = { escapeText(s) { return s.replace(/&/g, '&').replace(//g, '>'); }, escapeAttr(s) { return this.escapeText(s).replace(/"/g, '"'); }, tokenizeTag(tag) { const openMatch = tag.match(/^(<\/?)([a-zA-Z][a-zA-Z0-9\-:]*)/); if (!openMatch) return [{ cls: 'hl-tag', text: tag }]; const out = []; out.push({ cls: 'hl-punct', text: openMatch[1] }); out.push({ cls: 'hl-tag-name', text: openMatch[2] }); let rest = tag.slice(openMatch[0].length); let closeStr = ''; if (rest.slice(-2) === '/>') { closeStr = '/>'; rest = rest.slice(0, -2); } else if (rest.slice(-1) === '>') { closeStr = '>'; rest = rest.slice(0, -1); } const attrRe = /(\s+)([a-zA-Z_:][a-zA-Z0-9_:.\-]*)(\s*=\s*)?("[^"]*"|'[^']*'|[^\s>]+)?/g; let lastIdx = 0; let m; while ((m = attrRe.exec(rest)) !== null) { if (m.index > lastIdx) { out.push({ cls: 'hl-text', text: rest.slice(lastIdx, m.index) }); } out.push({ cls: 'hl-text', text: m[1] }); out.push({ cls: 'hl-attr-name', text: m[2] }); if (m[3]) out.push({ cls: 'hl-punct', text: m[3] }); if (m[4]) out.push({ cls: 'hl-attr-value', text: m[4] }); lastIdx = attrRe.lastIndex; } if (lastIdx < rest.length) { out.push({ cls: 'hl-text', text: rest.slice(lastIdx) }); } out.push({ cls: 'hl-punct', text: closeStr }); return out; }, build(html, maxLines) { const limit = maxLines || CONFIG.MAX_SOURCE_LINES; const parts = []; let line = ''; const flush = () => { parts.push(line); line = ''; }; const push = (cls, text) => { if (!text) return; if (parts.length >= limit) return; const chunks = text.split('\n'); for (let i = 0; i < chunks.length; i++) { if (i > 0) flush(); if (parts.length >= limit) return; if (chunks[i]) { line += '' + this.escapeText(chunks[i]) + ''; } } }; const re = /()|(]*>)|(<\/?[a-zA-Z][^>]*\/?>)|([^<]+)/gi; let lastIdx = 0; let m; while ((m = re.exec(html)) !== null) { if (parts.length >= limit) break; if (m.index > lastIdx) push('hl-text', html.slice(lastIdx, m.index)); if (m[1]) push('hl-comment', m[1]); else if (m[2]) push('hl-doctype', m[2]); else if (m[3]) { const tokens = this.tokenizeTag(m[3]); for (let i = 0; i < tokens.length; i++) { push(tokens[i].cls, tokens[i].text); } } else if (m[4]) push('hl-text', m[4]); lastIdx = re.lastIndex; } if (lastIdx < html.length && parts.length < limit) { push('hl-text', html.slice(lastIdx)); } if (line || parts.length === 0) parts.push(line); return { total: parts.length, truncated: parts.length >= limit, render(container) { const out = []; for (let i = 0; i < parts.length; i++) { out.push('
' + (i + 1) + '' + (parts[i] || '') + '
'); } container.innerHTML = out.join(''); } }; } }; const Store = { resources: [], byKey: new Map(), seen: new Map(), sortMode: 'name-asc', viewMode: 'list', currentFilter: 'image', _sortedCache: null, _sortDirty: true, invalidate() { this._sortDirty = true; }, add(url, ct, it) { if (typeof url !== 'string') return false; const trimmed = url.trim(); if (!trimmed || Security.isInvalid(trimmed)) return false; const k = Security.key(trimmed); if (!k) return false; const existing = this.byKey.get(k); if (existing) { if (ct && !existing.contentType) { existing.contentType = ct; const ndt = Security.displayType(existing.url, ct); if (ndt !== 'other') { existing.displayType = ndt; existing.filterType = Security.filterType(ndt); } } existing.lastSeen = Date.now(); return false; } let dt = Security.displayType(trimmed, ct); if (!Security.isLikelyResource(trimmed, dt)) return false; if (!Security.isWorthKeeping(trimmed, dt, ct)) return false; if (dt === 'other' && IMAGE_TYPES.has(dt)) dt = 'other'; const autoName = Security.ensureFilename(Security.filename(trimmed), trimmed, ct); const downloadUrl = Security.isDirectDownloadable(trimmed) ? trimmed : ''; const item = { url: trimmed, contentType: ct || '', initiatorType: it || 'scan', timestamp: Date.now(), lastSeen: Date.now(), displayType: dt, filterType: Security.filterType(dt), filename: autoName, autoFileName: true, thumbState: 0, downloadUrl: downloadUrl, el: null, listIdx: -1 }; this.byKey.set(k, item); this.resources.unshift(item); this.seen.set(k, 1); this._sortDirty = true; this._trimSeen(); this._evictIfNeeded(); UI.scheduleUpdate(item); return true; }, _trimSeen() { if (this.seen.size <= CONFIG.MAX_URL_CACHE) return; let drop = 2000; for (const k of this.seen.keys()) { this.seen.delete(k); if (--drop <= 0) break; } }, _evictIfNeeded() { while (this.resources.length > CONFIG.MAX_RESOURCES) { const ev = this.resources.pop(); if (!ev) break; const ek = Security.key(ev.url); if (ek) this.byKey.delete(ek); if (ev.el && ev.el.parentNode) ev.el.parentNode.removeChild(ev.el); ev.el = null; } }, remove(url) { const k = Security.key(url); if (!k) return; const item = this.byKey.get(k); if (!item) return; const idx = this.resources.indexOf(item); if (idx >= 0) this.resources.splice(idx, 1); this.byKey.delete(k); this._sortDirty = true; }, markThumbFail(url, reason) { const k = Security.key(url); if (!k) return 0; const item = this.byKey.get(k); if (!item) return 0; if (reason === 'large') item.tooLargeForInline = true; if (item.thumbState >= MAX_THUMB_FAILS) return item.thumbState; item.thumbState += 1; return item.thumbState; }, resetThumbFail(url) { const k = Security.key(url); if (!k) return; const item = this.byKey.get(k); if (!item) return; item.thumbState = 0; }, markInlineUnreadable(url) { const k = Security.key(url); if (!k) return; const item = this.byKey.get(k); if (!item) return; item.inlineUnreadable = true; }, counts() { const c = { image: 0, media: 0, other: 0 }; for (let i = 0; i < this.resources.length; i++) { const t = this.resources[i].filterType; if (c[t] !== undefined) c[t]++; } return c; }, filtered() { if (this._sortDirty || !this._sortedCache) { let arr = this.resources; if (this.currentFilter) { arr = arr.filter(r => r.filterType === this.currentFilter); } else { arr = arr.slice(); } const m = this.sortMode; if (m === 'time-desc') { arr.sort((a, b) => b.timestamp - a.timestamp); } else if (m === 'time-asc') { arr.sort((a, b) => a.timestamp - b.timestamp); } else if (m === 'name-asc') { arr.sort((a, b) => { const x = (a.filename || '').toLowerCase(); const y = (b.filename || '').toLowerCase(); return x < y ? -1 : x > y ? 1 : 0; }); } else { arr.sort((a, b) => { const x = (a.filename || '').toLowerCase(); const y = (b.filename || '').toLowerCase(); return x < y ? 1 : x > y ? -1 : 0; }); } this._sortedCache = arr; this._sortDirty = false; } return this._sortedCache; }, setFilter(f) { if (this.currentFilter === f) return; this.currentFilter = f; this._sortDirty = true; }, setSort(m) { if (this.sortMode === m) return; this.sortMode = m; this._sortDirty = true; } }; const Theme = { mq: null, _observer: null, _rafId: null, _current: null, _unwrappedAt: 0, _origThemeColor: null, _themeColorEl: null, detect() { try { if (this.mq && this.mq.matches) return 'dark'; } catch {} const html = document.documentElement; const body = document.body; const hasDarkMarker = (el) => { if (!el) return false; try { const raw = el.className; const cls = (typeof raw === 'string' ? raw : (raw && raw.baseVal) || '' ).toLowerCase(); if (/(^|\s)(dark|theme-dark|night|dark-mode|dark-theme|night-mode)(\s|$)/.test(cls)) { return true; } if (el.getAttribute) { if (el.getAttribute('data-theme') === 'dark') return true; if (el.getAttribute('color-scheme') === 'dark') return true; } if (el.hasAttribute && el.hasAttribute('dark')) return true; } catch {} return false; }; if (hasDarkMarker(html)) return 'dark'; if (hasDarkMarker(body)) return 'dark'; try { const cs = getComputedStyle(html).colorScheme || ''; if (cs.indexOf('dark') >= 0 && cs.indexOf('light') < 0) return 'dark'; } catch {} if (Date.now() - this._unwrappedAt > CONFIG.UNWRAP_MARGIN_MS) { try { const getBg = (el) => { if (!el) return null; const bg = getComputedStyle(el).backgroundColor || ''; const m = bg.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([\d.]+))?\s*\)$/); if (!m) return null; const a = m[4] !== undefined ? parseFloat(m[4]) : 1; if (a < 0.5) return null; return [Number(m[1]), Number(m[2]), Number(m[3])]; }; const rgb = getBg(body) || getBg(html); if (rgb) { const brightness = (rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000; if (brightness < 80) return 'dark'; } } catch {} } return 'light'; }, apply() { const theme = this.detect(); if (theme === this._current) return; this._current = theme; if (!UI.host) return; UI.host.setAttribute('data-theme', theme); try { if (!this._themeColorEl) { const found = document.querySelector('meta[name="theme-color"]'); if (found) { this._themeColorEl = found; this._origThemeColor = found.getAttribute('content'); } } if (!this._themeColorEl) { this._themeColorEl = document.createElement('meta'); this._themeColorEl.setAttribute('name', 'theme-color'); this._themeColorEl.setAttribute('data-hrd', '1'); document.head.appendChild(this._themeColorEl); } this._themeColorEl.setAttribute('content', theme === 'dark' ? '#141820' : '#F5F7FA'); } catch {} }, schedule() { if (this._rafId) return; this._rafId = requestAnimationFrame(() => { this._rafId = null; this.apply(); }); }, init() { try { this.mq = window.matchMedia('(prefers-color-scheme: dark)'); const onMqChange = () => this.apply(); if (this.mq.addEventListener) this.mq.addEventListener('change', onMqChange); else if (this.mq.addListener) this.mq.addListener(onMqChange); } catch {} try { this._observer = new MutationObserver(() => this.schedule()); const opt = { attributes: true, attributeFilter: ['class', 'data-theme', 'color-scheme', 'dark'] }; this._observer.observe(document.documentElement, opt); if (document.body) this._observer.observe(document.body, opt); } catch {} this.apply(); }, restore() { try { if (this._themeColorEl && this._themeColorEl.getAttribute('data-hrd') === '1') { this._themeColorEl.remove(); } else if (this._themeColorEl && this._origThemeColor !== null) { this._themeColorEl.setAttribute('content', this._origThemeColor); } } catch {} }, destroy() { try { if (this._observer) this._observer.disconnect(); } catch {} if (this._rafId) { cancelAnimationFrame(this._rafId); this._rafId = null; } } }; const SafeArea = { _top: 24, _raf: null, detect() { let h = 24; if (window.visualViewport && window.visualViewport.offsetTop > 0) { h = window.visualViewport.offsetTop; } else if (window.screen) { const diff = window.screen.height - window.innerHeight; if (diff > 0 && diff < 200) h = diff; } if (h === this._top) return; this._top = h; if (UI.host) UI.host.style.setProperty('--hrd-safe-top', h + 'px'); }, schedule() { if (this._raf) return; this._raf = requestAnimationFrame(() => { this._raf = null; this.detect(); }); }, init() { this.detect(); window.addEventListener('resize', () => this.schedule(), { passive: true }); window.addEventListener('orientationchange', () => { setTimeout(() => this.detect(), 280); }, { passive: true }); if (window.visualViewport) { window.visualViewport.addEventListener('resize', () => this.schedule(), { passive: true }); } } }; function displayUrl(url) { if (url.length <= 90) return url; return url.slice(0, 45) + ' … ' + url.slice(-40); } const LAYER_MAX = 2147483600; const TopGuard = { node: null, poll: null, ensure() { let n = this.node; if (!n || !n.isConnected) { const found = document.getElementById('__hrd_top_guard__'); if (found) { n = found; } else { try { n = document.createElement('div'); n.id = '__hrd_top_guard__'; } catch { return null; } } n.setAttribute('aria-hidden', 'true'); n.style.cssText = 'position:fixed;top:0;left:0;width:0;height:0;pointer-events:none;' + 'z-index:' + LAYER_MAX + ';'; this.node = n; } const body = document.body; if (!body) return n; if (n.parentNode !== body || n.nextSibling) { try { body.appendChild(n); } catch { return n; } } const self = this; if (!this.poll) { this.poll = setInterval(() => { try { self.ensure(); } catch {} }, 1200); } return n; } }; const UI = { host: null, root: null, panel: null, list: null, backdrop: null, countEl: null, _raf: null, _scrollLocked: false, _savedBodyOverflow: '', _savedHtmlOverflow: '', _toastTimer: null, _overlays: [], _swipeBound: false, _opening: false, _openingTimer: null, _renderToken: 0, _overlayMask: null, _anchor: null, _topGuard: null, _topologyTimer: null, _topologyObserver: null, _detailWrap: true, _itemNodes: null, _visibleLimit: CONFIG.BATCH_SIZE, _lastFocus: null, _OVERLAY_IDS: { player: 'hrd-player', preview: 'hrd-preview', detail: 'hrd-detail', source: 'hrd-source-panel' }, init() { if (this.host) return; this.host = document.createElement('div'); this.host.setAttribute('data-theme', 'light'); this.host.style.cssText = 'position:fixed;top:0;left:0;width:0;height:0;overflow:visible;pointer-events:none;' + 'z-index:' + LAYER_MAX + ';'; this.root = this.host.attachShadow({ mode: 'open' }); this.injectStyle(); this.buildUI(); if (!this._mountHost()) this._scheduleRemount(); this.bindEvents(); this.startTopGuards(); }, _mkAnchor() { if (!this._anchor) { this._anchor = document.createElement('div'); this._anchor.setAttribute('aria-hidden', 'true'); this._anchor.style.cssText = 'display:none'; } return this._anchor; }, _mountHost() { const body = document.body; if (!body) return false; if (document.readyState === 'loading') return false; try { const anchor = this._mkAnchor(); if (anchor.parentNode !== body) body.appendChild(anchor); if (this.host.parentNode !== body) body.appendChild(this.host); const g = TopGuard.ensure() || TopGuard.node; if (g && g.parentNode !== body) body.appendChild(g); this._ensureTopNode(); } catch (e) { try { console.warn('[资源日记] 挂载失败,将重试:', e); } catch {} return false; } return this.host.isConnected && this.host.parentNode === body; }, _scheduleRemount() { if (this._remountTimer) return; const self = this; let tries = 0; const attempt = () => { if (self._remountDone()) { clearInterval(self._remountTimer); self._remountTimer = null; self.applyMaxLayer(); return; } if (self._mountHost()) { clearInterval(self._remountTimer); self._remountTimer = null; self.applyMaxLayer(); return; } if (++tries > 80) { clearInterval(self._remountTimer); self._remountTimer = null; } }; this._remountTimer = setInterval(attempt, 250); window.addEventListener('load', attempt, { once: true }); document.addEventListener('readystatechange', () => { if (document.readyState !== 'loading') attempt(); }, { passive: true }); }, _remountDone() { return !!(this.host && this.host.isConnected && this.host.parentNode === document.body); }, applyMaxLayer() { if (!this.host) return; this.host.style.zIndex = String(LAYER_MAX); if (this._anchor) this._anchor.style.zIndex = String(LAYER_MAX); if (this._topGuard) this._topGuard.style.zIndex = String(LAYER_MAX); }, _ensureTopNode() { if (!this.host) return; if (!this.host.parentNode) { this._mountHost(); return; } if (this.host.nextSibling) return; try { this.host.parentNode.appendChild(this.host); } catch {} }, startTopGuards() { if (this._topologyTimer) return; this.applyMaxLayer(); const self = this; const tick = () => { try { self._ensureTopNode(); } catch {} }; this._topologyTimer = setInterval(tick, 1200); try { const mo = new MutationObserver(() => { if (self._topRaf) return; self._topRaf = requestAnimationFrame(() => { self._topRaf = null; self.applyMaxLayer(); self._ensureTopNode(); }); }); const opt = { childList: true, subtree: false }; if (document.body) mo.observe(document.body, opt); mo.observe(document.documentElement, opt); this._topologyObserver = mo; } catch {} window.addEventListener('pageshow', tick, { passive: true }); document.addEventListener('visibilitychange', () => { if (!document.hidden) tick(); }, { passive: true }); }, injectStyle() { const s = document.createElement('style'); s.textContent = ` :host { all: initial; position: fixed; top: 0; left: 0; width: 0; height: 0; overflow: visible; pointer-events: none; z-index: 2147483600; font-family: 'HarmonyOS Sans SC', 'HarmonyOS Sans', 'PingFang SC', -apple-system, BlinkMacSystemFont, sans-serif; -webkit-font-smoothing: antialiased; --hrd-safe-top: 24px; --hrd-sheet-gap: ${CONFIG.SHEET_TOP_GAP}px; --hrd-safe-bottom: max(env(safe-area-inset-bottom, 0px), 0px); --hrd-bg-primary: rgba(245, 247, 250, 0.94); --hrd-bg-solid: #F5F7FA; --hrd-bg-tertiary: #F0F1F5; --hrd-bg-elevated: #FFFFFF; --hrd-bg-mask: rgba(10, 15, 30, 0.32); --hrd-border: rgba(10, 30, 80, 0.06); --hrd-border-strong: rgba(10, 30, 80, 0.10); --hrd-control-bg: #FFFFFF; --hrd-control-bg-hover: #E8EBF2; --hrd-control-border: rgba(10, 30, 80, 0.14); --hrd-control-shadow: 0 1px 2px rgba(10, 30, 80, 0.06); --hrd-control-fg: #4A4F57; --hrd-text-primary: #1C1C1E; --hrd-text-secondary: #5F6368; --hrd-text-tertiary: #98989E; --hrd-accent: #0A84FF; --hrd-accent-soft: rgba(10, 132, 255, 0.10); --hrd-accent-glow: rgba(10, 132, 255, 0.30); --hrd-shadow-card: 0 2px 8px rgba(10, 30, 80, 0.04), 0 1px 2px rgba(10, 30, 80, 0.03); --hrd-shadow-fab: 0 8px 28px rgba(10, 132, 255, 0.32), 0 2px 8px rgba(10, 132, 255, 0.16); --hrd-shadow-panel: 0 -12px 48px rgba(10, 30, 80, 0.12); --hrd-toast-bg: rgba(20, 28, 40, 0.92); --hrd-toast-fg: #F5F5F7; --hrd-toast-border: rgba(255, 255, 255, 0.10); --hrd-handle-bg: rgba(10, 30, 80, 0.18); --hrd-src-scroll: rgba(255, 255, 255, 0.16); --hrd-src-scroll-hover: rgba(255, 255, 255, 0.30); } :host([data-theme="dark"]) { --hrd-bg-primary: rgba(20, 24, 32, 0.94); --hrd-bg-solid: #141820; --hrd-bg-tertiary: #2A2E38; --hrd-bg-elevated: #303440; --hrd-bg-mask: rgba(0, 0, 0, 0.55); --hrd-border: rgba(255, 255, 255, 0.06); --hrd-border-strong: rgba(255, 255, 255, 0.12); --hrd-control-bg: rgba(255, 255, 255, 0.09); --hrd-control-bg-hover: rgba(255, 255, 255, 0.16); --hrd-control-border: rgba(255, 255, 255, 0.16); --hrd-control-shadow: none; --hrd-control-fg: #C4C8D0; --hrd-text-primary: #F5F5F7; --hrd-text-secondary: #A8ACB3; --hrd-text-tertiary: #6C6C70; --hrd-accent-soft: rgba(10, 132, 255, 0.18); --hrd-accent-glow: rgba(10, 132, 255, 0.42); --hrd-shadow-card: 0 2px 8px rgba(0, 0, 0, 0.24), 0 1px 2px rgba(0, 0, 0, 0.16); --hrd-shadow-fab: 0 8px 28px rgba(10, 132, 255, 0.42), 0 2px 8px rgba(0, 0, 0, 0.24); --hrd-shadow-panel: 0 -12px 48px rgba(0, 0, 0, 0.48); --hrd-toast-bg: rgba(240, 240, 245, 0.92); --hrd-toast-fg: #1C1C1E; --hrd-toast-border: rgba(0, 0, 0, 0.06); --hrd-handle-bg: rgba(255, 255, 255, 0.22); --hrd-src-scroll: rgba(255, 255, 255, 0.16); --hrd-src-scroll-hover: rgba(255, 255, 255, 0.30); } :host * { box-sizing: border-box; pointer-events: auto; } :host svg { display: block; } :host([hidden]) { display: none; } button, .hrd-chip, .hrd-icon-btn, .hrd-btn, .hrd-more { -webkit-tap-highlight-color: transparent; -webkit-touch-callout: none; -webkit-appearance: none; appearance: none; outline: none; user-select: none; -webkit-user-select: none; font-family: inherit; } button::-moz-focus-inner { border: 0; padding: 0; } button:focus-visible, .hrd-chip:focus-visible, .hrd-icon-btn:focus-visible, .hrd-btn:focus-visible, .hrd-more:focus-visible { outline: 2px solid var(--hrd-accent); outline-offset: 2px; } #hrd-fab { position: fixed; bottom: calc(180px + var(--hrd-safe-bottom)); right: 20px; width: 52px; height: 52px; border-radius: 26px; background: linear-gradient(135deg, #0A84FF, #5BC0BE); border: 0.5px solid rgba(255, 255, 255, 0.32); color: #fff; cursor: pointer; padding: 0; display: flex; align-items: center; justify-content: center; box-shadow: var(--hrd-shadow-fab); transition: transform 0.32s cubic-bezier(0.34, 1.56, 0.64, 1); z-index: 10; } #hrd-fab svg { width: 30px; height: 30px; } #hrd-fab:hover { transform: scale(1.06); } #hrd-fab:active { transform: scale(0.92); background: linear-gradient(135deg, #0973DB, #4FA9A7); } #hrd-backdrop { position: fixed; inset: 0; background: var(--hrd-bg-mask); backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); opacity: 0; visibility: hidden; transition: 0.3s ease; z-index: 9; } #hrd-backdrop.on { opacity: 1; visibility: visible; } #hrd-panel { position: fixed; left: 0; right: 0; bottom: 0; height: 74%; background: var(--hrd-bg-primary); backdrop-filter: blur(24px) saturate(180%); -webkit-backdrop-filter: blur(24px) saturate(180%); border-radius: 32px 32px 0 0; border-top: 0.5px solid var(--hrd-border-strong); transform: translateY(100%); transition: transform 0.4s cubic-bezier(0.32, 0.94, 0.6, 1); z-index: 11; display: flex; flex-direction: column; overflow: hidden; box-shadow: var(--hrd-shadow-panel); will-change: transform; } #hrd-panel.on { transform: translateY(0); } .hrd-hd { display: flex; align-items: center; padding: 18px 20px 10px; gap: 8px; } .hrd-hd h3 { margin: 0; font-size: 18px; font-weight: 700; color: var(--hrd-text-primary); flex: 1; letter-spacing: -0.01em; display: flex; align-items: center; gap: 8px; min-width: 0; } .hrd-hd .hrd-title-icon { width: 26px; height: 26px; border-radius: 7px; overflow: hidden; flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; box-shadow: 0 2px 8px rgba(10, 132, 255, 0.28); } .hrd-hd .hrd-title-icon svg { width: 100%; height: 100%; display: block; } .hrd-hd .hrd-title-text { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .hrd-hd .hrd-count { font-size: 12px; font-weight: 500; color: var(--hrd-text-tertiary); flex-shrink: 0; } .hrd-icon-btn { min-width: 40px; width: 40px; height: 40px; border-radius: 20px; border: 1px solid var(--hrd-control-border); background: var(--hrd-control-bg); color: var(--hrd-control-fg); cursor: pointer; padding: 0; display: flex; align-items: center; justify-content: center; transition: background 0.2s, transform 0.2s, color 0.2s, border-color 0.2s, box-shadow 0.2s; box-shadow: var(--hrd-control-shadow); flex-shrink: 0; } .hrd-icon-btn svg { width: 18px; height: 18px; } .hrd-icon-btn:hover { background: var(--hrd-control-bg-hover); color: var(--hrd-text-primary); border-color: var(--hrd-accent); } .hrd-icon-btn:active { transform: scale(0.9); background: var(--hrd-control-bg-hover); border-color: var(--hrd-accent); } .hrd-filters { display: flex; gap: 8px; padding: 0 20px 12px; } .hrd-chip { flex: 1; min-height: 36px; padding: 8px 4px; border-radius: 999px; border: 1px solid var(--hrd-control-border); background: var(--hrd-control-bg); font-size: 13px; font-weight: 600; color: var(--hrd-text-secondary); cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 6px; transition: background 0.2s, color 0.2s, transform 0.2s, box-shadow 0.2s, border-color 0.2s; box-shadow: var(--hrd-control-shadow); overflow: hidden; } .hrd-chip svg { width: 16px; height: 16px; flex-shrink: 0; } .hrd-chip:hover { background: var(--hrd-control-bg-hover); border-color: var(--hrd-accent); color: var(--hrd-text-primary); } .hrd-chip:active { transform: scale(0.96); background: var(--hrd-control-bg-hover); } .hrd-chip.on { background: var(--hrd-accent); color: #fff; border-color: transparent; box-shadow: 0 4px 14px var(--hrd-accent-glow); } .hrd-list { flex: 1; overflow-y: auto; padding: 4px 16px calc(24px + var(--hrd-safe-bottom)); touch-action: pan-y; overscroll-behavior: contain; -webkit-overflow-scrolling: touch; } .hrd-list::-webkit-scrollbar { width: 4px; } .hrd-list::-webkit-scrollbar-thumb { background: var(--hrd-border-strong); border-radius: 4px; } .hrd-list.view-empty { display: flex; align-items: center; justify-content: center; padding: 0 16px var(--hrd-safe-bottom); touch-action: none; } .hrd-item { -webkit-tap-highlight-color: transparent; -webkit-touch-callout: none; -webkit-appearance: none; appearance: none; outline: none; } .hrd-list.view-list .hrd-item { display: flex; gap: 12px; padding: 12px; background: var(--hrd-bg-elevated); border: 0.5px solid var(--hrd-border); border-radius: 16px; margin-bottom: 8px; align-items: center; cursor: pointer; box-shadow: var(--hrd-shadow-card); transition: transform 0.2s, box-shadow 0.2s, background 0.2s, opacity 0.2s; } .hrd-list.view-list .hrd-item.hrd-anim { animation: hrdSlideIn 0.28s ease both; } @keyframes hrdSlideIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } } .hrd-list.view-list .hrd-item:hover { transform: translateY(-1px); box-shadow: 0 6px 20px rgba(10, 30, 80, 0.08); } .hrd-list.view-list .hrd-item:active { transform: scale(0.985); background: var(--hrd-accent-soft); } .hrd-list.view-list .hrd-thumb { width: 48px; height: 48px; border-radius: 12px; flex-shrink: 0; overflow: hidden; position: relative; display: flex; align-items: center; justify-content: center; background: var(--hrd-bg-tertiary); } .hrd-list.view-grid { display: flex; flex-wrap: wrap; gap: 8px; align-content: flex-start; align-items: flex-start; padding: 8px 16px calc(24px + var(--hrd-safe-bottom)); } .hrd-list.view-grid .hrd-item { flex: 0 0 calc((100% - 16px) / 3); width: calc((100% - 16px) / 3); min-width: 0; display: flex; flex-direction: column; gap: 6px; padding: 8px; background: var(--hrd-bg-elevated); border: 0.5px solid var(--hrd-border); border-radius: 14px; cursor: pointer; box-shadow: var(--hrd-shadow-card); transition: transform 0.2s, box-shadow 0.2s, opacity 0.2s; overflow: hidden; height: auto; } .hrd-list.view-grid .hrd-item.hrd-anim { animation: hrdSlideIn 0.28s ease both; } .hrd-list.view-grid .hrd-item:hover { transform: translateY(-2px); box-shadow: 0 8px 24px rgba(10, 30, 80, 0.10); } .hrd-list.view-grid .hrd-item:active { transform: scale(0.98); background: var(--hrd-accent-soft); } .hrd-list.view-grid .hrd-thumb { width: 100%; height: 74px; border-radius: 10px; overflow: hidden; position: relative; display: flex; align-items: center; justify-content: center; background: var(--hrd-bg-tertiary); flex-shrink: 0; } .hrd-list.view-grid .hrd-info { width: 100%; min-width: 0; display: flex; flex-direction: column; gap: 4px; flex-shrink: 0; } .hrd-list.view-grid .hrd-name { flex-direction: row; align-items: center; gap: 4px; font-size: 11px; line-height: 1.2; min-height: 16px; overflow: hidden; } .hrd-list.view-grid .hrd-url { display: none; } .hrd-list.view-grid .hrd-tag { height: 14px; font-size: 8px; padding: 0 4px; border-radius: 4px; margin-right: 0; flex-shrink: 0; letter-spacing: 0.2px; } .hrd-list.view-grid .hrd-name-text { flex: 1; min-width: 0; font-size: 11px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .hrd-list.view-grid .hrd-acts { display: flex; flex-direction: row; justify-content: space-around; align-items: center; width: 100%; gap: 2px; flex-shrink: 0; padding-top: 2px; } .hrd-list.view-grid .hrd-btn { width: 30px; height: 30px; flex: 0 0 auto; border-radius: 15px; } .hrd-list.view-grid .hrd-btn svg { width: 14px; height: 14px; } .hrd-thumb img { position: absolute; top: 0; left: 0; width: 100%; height: 100%; max-width: 100%; max-height: 100%; min-width: 0; min-height: 0; object-fit: cover; object-position: center; display: block; flex: none; opacity: 0; transition: opacity 0.3s; } .hrd-thumb img.loaded { opacity: 1; } .hrd-thumb .hrd-icon { width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; } .hrd-list.view-grid .hrd-thumb .hrd-icon { width: 28px; height: 28px; } .hrd-thumb .hrd-icon svg { width: 100%; height: 100%; } .hrd-thumb .hrd-icon.failed { opacity: 0.30; filter: grayscale(0.5); } .hrd-thumb[data-kind="image"] { background: rgba(91, 192, 190, 0.14); } .hrd-thumb[data-kind="image"] .hrd-icon { color: #26A69A; } .hrd-thumb[data-kind="video"] { background: rgba(255, 82, 82, 0.14); } .hrd-thumb[data-kind="video"] .hrd-icon { color: #EF5350; } .hrd-thumb[data-kind="audio"] { background: rgba(10, 132, 255, 0.14); } .hrd-thumb[data-kind="audio"] .hrd-icon { color: #0A84FF; } .hrd-thumb[data-kind="code"] { background: rgba(255, 167, 38, 0.16); } .hrd-thumb[data-kind="code"] .hrd-icon { color: #FF9800; } .hrd-thumb[data-kind="json"] { background: rgba(92, 107, 192, 0.14); } .hrd-thumb[data-kind="json"] .hrd-icon { color: #5C6BC0; } .hrd-thumb[data-kind="font"] { background: rgba(161, 136, 127, 0.16); } .hrd-thumb[data-kind="font"] .hrd-icon { color: #A1887F; } .hrd-thumb[data-kind="link"] { background: rgba(66, 165, 245, 0.14); } .hrd-thumb[data-kind="link"] .hrd-icon { color: #42A5F5; } .hrd-thumb[data-kind="doc"] { background: rgba(120, 144, 156, 0.14); } .hrd-thumb[data-kind="doc"] .hrd-icon { color: #78909C; } :host([data-theme="dark"]) .hrd-thumb[data-kind="image"] { background: rgba(38, 166, 154, 0.22); } :host([data-theme="dark"]) .hrd-thumb[data-kind="video"] { background: rgba(239, 83, 80, 0.22); } :host([data-theme="dark"]) .hrd-thumb[data-kind="audio"] { background: rgba(10, 132, 255, 0.22); } :host([data-theme="dark"]) .hrd-thumb[data-kind="code"] { background: rgba(255, 152, 0, 0.22); } :host([data-theme="dark"]) .hrd-thumb[data-kind="json"] { background: rgba(92, 107, 192, 0.24); } :host([data-theme="dark"]) .hrd-thumb[data-kind="font"] { background: rgba(161, 136, 127, 0.24); } :host([data-theme="dark"]) .hrd-thumb[data-kind="link"] { background: rgba(66, 165, 245, 0.22); } :host([data-theme="dark"]) .hrd-thumb[data-kind="doc"] { background: rgba(120, 144, 156, 0.22); } :host([data-theme="dark"]) .hrd-thumb[data-kind="image"] .hrd-icon { color: #4DB6AC; } :host([data-theme="dark"]) .hrd-thumb[data-kind="video"] .hrd-icon { color: #FF7043; } :host([data-theme="dark"]) .hrd-thumb[data-kind="code"] .hrd-icon { color: #FFB74D; } :host([data-theme="dark"]) .hrd-thumb[data-kind="json"] .hrd-icon { color: #9FA8DA; } :host([data-theme="dark"]) .hrd-thumb[data-kind="font"] .hrd-icon { color: #BCAAA4; } :host([data-theme="dark"]) .hrd-thumb[data-kind="link"] .hrd-icon { color: #64B5F6; } :host([data-theme="dark"]) .hrd-thumb[data-kind="doc"] .hrd-icon { color: #90A4AE; } .hrd-info { flex: 1; min-width: 0; } .hrd-name { display: flex; align-items: center; gap: 0; font-size: 13px; font-weight: 600; color: var(--hrd-text-primary); line-height: 1.4; min-width: 0; } .hrd-tag { display: inline-flex; align-items: center; justify-content: center; height: 18px; padding: 0 6px; margin-right: 6px; border-radius: 6px; font-size: 10px; font-weight: 700; letter-spacing: 0.4px; color: #fff; flex-shrink: 0; text-transform: uppercase; background: #78909C; } .hrd-tag[data-kind="image"] { background: #5BC0BE; } .hrd-tag[data-kind="video"] { background: #EF5350; } .hrd-tag[data-kind="audio"] { background: #0A84FF; } .hrd-tag[data-kind="code"] { background: #FF9800; } .hrd-tag[data-kind="json"] { background: #5C6BC0; } .hrd-tag[data-kind="font"] { background: #A1887F; } .hrd-tag[data-kind="link"] { background: #42A5F5; } .hrd-tag[data-kind="doc"] { background: #78909C; } :host([data-theme="dark"]) .hrd-tag[data-kind="image"] { background: #4DB6AC; } :host([data-theme="dark"]) .hrd-tag[data-kind="video"] { background: #E57373; } :host([data-theme="dark"]) .hrd-tag[data-kind="audio"] { background: #64B5F6; } :host([data-theme="dark"]) .hrd-tag[data-kind="code"] { background: #FFB74D; } :host([data-theme="dark"]) .hrd-tag[data-kind="json"] { background: #9FA8DA; } :host([data-theme="dark"]) .hrd-tag[data-kind="font"] { background: #BCAAA4; } :host([data-theme="dark"]) .hrd-tag[data-kind="link"] { background: #64B5F6; } :host([data-theme="dark"]) .hrd-tag[data-kind="doc"] { background: #90A4AE; } .hrd-name-text { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .hrd-url { font-size: 10.5px; color: var(--hrd-text-tertiary); margin-top: 3px; font-family: 'HarmonyOS Sans Mono', 'SF Mono', Monaco, Consolas, monospace; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .hrd-acts { display: flex; gap: 4px; flex-shrink: 0; } .hrd-btn { width: 36px; height: 36px; border-radius: 18px; border: 1px solid var(--hrd-control-border); background: var(--hrd-control-bg); color: var(--hrd-accent); cursor: pointer; display: flex; align-items: center; justify-content: center; transition: background 0.2s, transform 0.15s, border-color 0.2s; padding: 0; flex-shrink: 0; } .hrd-btn svg { width: 16px; height: 16px; } .hrd-btn:hover { background: var(--hrd-accent-soft); border-color: var(--hrd-accent); } .hrd-btn:active { transform: scale(0.88); background: var(--hrd-accent-soft); } .hrd-btn.disabled, .hrd-btn:disabled { opacity: 0.34; cursor: not-allowed; } .hrd-btn.disabled:active, .hrd-btn:disabled:active { transform: none; background: transparent; } .hrd-empty { text-align: center; padding: 40px 24px; color: var(--hrd-text-secondary); font-size: 14px; display: flex; flex-direction: column; align-items: center; gap: 10px; } .hrd-empty .hrd-empty-art { width: 132px; height: 132px; display: flex; align-items: center; justify-content: center; position: relative; margin-bottom: 6px; color: var(--hrd-accent); } .hrd-empty .hrd-empty-art::before { content: ''; position: absolute; inset: 14px; border-radius: 50%; background: radial-gradient(circle at 50% 46%, var(--hrd-accent-soft) 0%, rgba(10, 132, 255, 0.045) 52%, transparent 72%); } .hrd-empty .hrd-empty-art svg { position: relative; width: 108px; height: 108px; opacity: 0.92; } .hrd-empty .hrd-empty-title { font-size: 15px; font-weight: 600; color: var(--hrd-text-primary); letter-spacing: 0.2px; } .hrd-empty .sub { font-size: 12px; line-height: 1.6; color: var(--hrd-text-tertiary); max-width: 232px; } :host([data-theme="dark"]) .hrd-empty .hrd-empty-art { color: #4DB6AC; } .hrd-more { display: block; width: calc(100% - 8px); margin: 4px 4px 12px; padding: 12px; border-radius: 14px; border: 0.5px dashed var(--hrd-border-strong); background: transparent; color: var(--hrd-text-secondary); font-size: 13px; font-weight: 600; cursor: pointer; transition: background 0.2s, color 0.2s; } .hrd-more:hover { background: var(--hrd-bg-tertiary); color: var(--hrd-text-primary); } .hrd-more:active { transform: scale(0.99); background: var(--hrd-bg-tertiary); } #hrd-toast { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%) scale(0.94); background: var(--hrd-toast-bg); color: var(--hrd-toast-fg); padding: 12px 22px; border-radius: 999px; font-size: 13px; font-weight: 500; z-index: 2147483601; pointer-events: none; opacity: 0; transition: opacity 0.3s, transform 0.3s; box-shadow: 0 12px 36px rgba(0, 0, 0, 0.28); border: 0.5px solid var(--hrd-toast-border); display: flex; align-items: center; gap: 8px; max-width: min(78vw, 420px); text-align: center; } #hrd-toast.on { opacity: 1; transform: translate(-50%, -50%) scale(1); } #hrd-overlay-mask { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: var(--hrd-bg-mask); backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px); z-index: 2147483599; animation: hrdMaskIn 0.22s ease-out; } @keyframes hrdMaskIn { from { opacity: 0; } to { opacity: 1; } } #hrd-detail { z-index: 2147483600; } #hrd-source-panel { z-index: 2147483600; } #hrd-player { z-index: 2147483600; } #hrd-preview { z-index: 2147483600; } #hrd-player, #hrd-preview, #hrd-source-panel, #hrd-detail { position: fixed; top: calc(var(--hrd-safe-top) + var(--hrd-sheet-gap)); left: 0; right: 0; bottom: 0; background: var(--hrd-bg-primary); backdrop-filter: blur(24px) saturate(180%); -webkit-backdrop-filter: blur(24px) saturate(180%); border-radius: 32px 32px 0 0; border-top: 0.5px solid var(--hrd-border-strong); box-shadow: var(--hrd-shadow-panel); z-index: 2147483647; overflow: hidden; padding-bottom: var(--hrd-safe-bottom); animation: hrdSheetIn 0.38s cubic-bezier(0.32, 0.94, 0.6, 1); } @keyframes hrdSheetIn { from { opacity: 0; transform: translateY(24px); } to { opacity: 1; transform: translateY(0); } } .hrd-sheet-handle { width: 36px; height: 4px; border-radius: 2px; background: var(--hrd-handle-bg); margin: 8px auto 0; flex-shrink: 0; } #hrd-player, #hrd-detail { display: flex; flex-direction: column; } #hrd-player .hd { display: flex; align-items: center; gap: 8px; padding: 14px 20px 12px; flex-shrink: 0; } #hrd-player .hd .ttl { flex: 1; font-size: 16px; font-weight: 700; color: var(--hrd-text-primary); display: flex; align-items: center; gap: 10px; min-width: 0; } #hrd-player .hd .ttl svg { width: 22px; height: 22px; color: var(--hrd-accent); flex-shrink: 0; } #hrd-player .hd button { width: 40px; height: 40px; border-radius: 20px; border: 1px solid var(--hrd-control-border); background: var(--hrd-control-bg); color: var(--hrd-control-fg); cursor: pointer; padding: 0; display: flex; align-items: center; justify-content: center; transition: background 0.2s, transform 0.2s, color 0.2s, border-color 0.2s; box-shadow: var(--hrd-control-shadow); flex-shrink: 0; } #hrd-player .hd button:hover { background: var(--hrd-control-bg-hover); border-color: var(--hrd-accent); color: var(--hrd-text-primary); } #hrd-player .hd button:active { transform: scale(0.9); } #hrd-player .hd button svg { width: 18px; height: 18px; } #hrd-detail .hd { display: flex; align-items: center; gap: 6px; padding: 12px 16px 10px; flex-shrink: 0; } #hrd-detail .hd .ttl { flex: 1; font-size: 16px; font-weight: 700; color: var(--hrd-text-primary); display: flex; align-items: center; gap: 10px; min-width: 0; overflow: hidden; } #hrd-detail .hd .ttl > svg { width: 22px; height: 22px; color: var(--hrd-accent); flex-shrink: 0; } #hrd-detail .hd .ttl > span:first-of-type { white-space: nowrap; } #hrd-detail .hd .meta { font-size: 11px; font-weight: 500; color: var(--hrd-text-tertiary); margin-left: 4px; white-space: nowrap; flex-shrink: 0; } #hrd-detail .hd button { width: 40px; height: 40px; border-radius: 20px; border: 1px solid var(--hrd-control-border); background: var(--hrd-control-bg); color: var(--hrd-control-fg); cursor: pointer; padding: 0; flex-shrink: 0; display: flex; align-items: center; justify-content: center; transition: background 0.2s, transform 0.2s, color 0.2s, border-color 0.2s; box-shadow: var(--hrd-control-shadow); } #hrd-detail .hd button:hover { background: var(--hrd-control-bg-hover); border-color: var(--hrd-accent); color: var(--hrd-text-primary); } #hrd-detail .hd button:active { transform: scale(0.9); } #hrd-detail .hd button svg { width: 18px; height: 18px; } #hrd-detail { padding-bottom: max(env(safe-area-inset-bottom, 0px), 24px); } #hrd-source-panel, #hrd-detail { --hrd-src-bg: #1B1F27; --hrd-src-text: #E5E7EB; --hrd-src-lineno: #4A5260; --hrd-src-border: rgba(255, 255, 255, 0.06); --hrd-src-hover: rgba(10, 132, 255, 0.06); } #hrd-player .media-wrap { flex: 1; display: flex; align-items: center; justify-content: center; padding: 0 16px 16px; overflow: hidden; min-height: 0; } #hrd-player.failed .media-wrap { display: none; } #hrd-player.failed .hrd-player-fallback { margin-top: auto; margin-bottom: auto; } #hrd-player video, #hrd-player audio { max-width: 100%; max-height: 100%; border-radius: 20px; background: #000; } #hrd-player audio { width: 100%; background: transparent; } #hrd-player .hrd-hd-acts { display: flex; align-items: center; gap: 4px; flex-shrink: 0; } #hrd-player .hrd-player-status { flex-shrink: 0; margin: 0 16px 4px; padding: 7px 12px; border-radius: 10px; font-size: 12.5px; line-height: 1.5; text-align: center; color: var(--hrd-text-secondary); background: var(--hrd-bg-tertiary); word-break: break-word; } #hrd-player .hrd-player-status:empty { display: none; } #hrd-player .hrd-player-status[data-tone="ok"] { color: #1F8A46; background: rgba(52, 199, 89, 0.12); } #hrd-player .hrd-player-status[data-tone="warn"] { color: #B7791F; background: rgba(255, 179, 0, 0.14); } #hrd-player .hrd-player-fallback { flex-shrink: 0; margin: 0 16px 16px; padding: 16px; border-radius: 16px; background: var(--hrd-bg-tertiary); text-align: center; } #hrd-player .hrd-player-fallback .t { font-size: 14.5px; font-weight: 600; color: var(--hrd-text-primary); margin-bottom: 6px; } #hrd-player .hrd-player-fallback .s { font-size: 12.5px; line-height: 1.6; color: var(--hrd-text-secondary); margin-bottom: 12px; word-break: break-word; } #hrd-player .hrd-player-fallback .acts { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; } #hrd-player .hrd-player-fallback .acts button { padding: 8px 14px; border-radius: 10px; border: 1px solid var(--hrd-border-strong); background: var(--hrd-bg-elevated); color: var(--hrd-text-primary); font-size: 13px; cursor: pointer; } #hrd-player .hrd-player-fallback .acts button:active { transform: scale(0.97); } .hrd-stream-tag { flex-shrink: 0; margin-left: 4px; padding: 1px 5px; border-radius: 5px; font-size: 10px; font-weight: 600; line-height: 1.5; letter-spacing: .02em; } .hrd-stream-tag[data-kind="hls"] { color: #0A84FF; background: rgba(10, 132, 255, 0.13); } .hrd-stream-tag[data-kind="seg"] { color: #B7791F; background: rgba(255, 179, 0, 0.16); } #hrd-preview { display: flex; flex-direction: column; cursor: zoom-out; padding: 0; } #hrd-preview .hrd-preview-blobnote { position: absolute; left: 50%; bottom: 12px; transform: translateX(-50%); max-width: calc(100% - 32px); padding: 6px 12px; border-radius: 10px; font-size: 12px; line-height: 1.5; text-align: center; color: var(--hrd-text-secondary); background: var(--hrd-bg-tertiary); pointer-events: none; word-break: break-word; } #hrd-preview .hrd-preview-stage { position: relative; flex: 1 1 auto; min-height: 0; display: flex; align-items: center; justify-content: center; overflow: hidden; padding: 8px; } #hrd-preview .hrd-preview-cryptonote { position: absolute; left: 50%; bottom: 12px; transform: translateX(-50%); max-width: calc(100% - 32px); padding: 6px 12px; border-radius: 10px; font-size: 12px; line-height: 1.5; text-align: center; color: var(--hrd-text-secondary); background: var(--hrd-bg-tertiary); pointer-events: none; word-break: break-word; } #hrd-preview img { max-width: 100%; max-height: 100%; border-radius: 20px; object-fit: contain; opacity: 0; transition: opacity 0.24s; } #hrd-preview img.loaded { opacity: 1; } #hrd-preview.zoom .hrd-preview-stage { overflow: auto; align-items: flex-start; justify-content: flex-start; -webkit-overflow-scrolling: touch; } #hrd-preview.zoom img { max-width: none; max-height: none; width: auto; cursor: zoom-out; } .hrd-preview-loading { position: absolute; color: var(--hrd-text-tertiary); font-size: 12.5px; letter-spacing: 0.2px; } .hrd-preview-fallback { display: none; flex: 1 1 auto; flex-direction: column; align-items: center; justify-content: center; gap: 10px; padding: 0 32px 24px; text-align: center; } .hrd-preview-fallback.on { display: flex; } .hrd-preview-fallback .art { width: 68px; height: 68px; border-radius: 22px; display: flex; align-items: center; justify-content: center; background: var(--hrd-bg-tertiary); color: var(--hrd-text-tertiary); margin-bottom: 4px; } .hrd-preview-fallback .art svg { width: 34px; height: 34px; } .hrd-preview-fallback .t { font-size: 15px; font-weight: 600; color: var(--hrd-text-primary); } .hrd-preview-fallback .s { font-size: 12px; line-height: 1.65; color: var(--hrd-text-tertiary); max-width: 260px; } .hrd-preview-fallback .acts { display: flex; align-items: center; gap: 12px; margin-top: 12px; } .hrd-preview-fallback .acts button:not(.text) { position: static; width: 44px; height: 44px; border-radius: 22px; } .hrd-preview-fallback .acts button.text { position: static; width: auto; height: 44px; padding: 0 18px; border-radius: 22px; gap: 8px; font-size: 13px; font-weight: 600; color: var(--hrd-text-primary); } .hrd-preview-fallback .acts button.text svg { width: 16px; height: 16px; } .hrd-preview button { width: 40px; height: 40px; border-radius: 20px; border: 1px solid var(--hrd-control-border); background: var(--hrd-control-bg); color: var(--hrd-control-fg); cursor: pointer; padding: 0; display: flex; align-items: center; justify-content: center; transition: background 0.2s, transform 0.2s, color 0.2s, border-color 0.2s; box-shadow: var(--hrd-control-shadow); } #hrd-preview button:hover { background: var(--hrd-control-bg-hover); border-color: var(--hrd-accent); color: var(--hrd-text-primary); } #hrd-preview button:active { transform: scale(0.9); } #hrd-preview button svg { width: 18px; height: 18px; } .hrd-preview-close { z-index: 3; } #hrd-preview .hd { display: flex; align-items: center; gap: 6px; padding: 12px 16px 10px; flex-shrink: 0; } #hrd-preview .hd .ttl { flex: 1; font-size: 16px; font-weight: 700; color: var(--hrd-text-primary); display: flex; align-items: center; gap: 10px; min-width: 0; overflow: hidden; } #hrd-preview .hd .ttl > svg { width: 22px; height: 22px; color: var(--hrd-accent); flex-shrink: 0; } #hrd-preview .hd .ttl > span:first-of-type { white-space: nowrap; } #hrd-preview .hd button { width: 40px; height: 40px; border-radius: 20px; flex-shrink: 0; border: 1px solid var(--hrd-control-border); background: var(--hrd-control-bg); color: var(--hrd-control-fg); cursor: pointer; padding: 0; display: flex; align-items: center; justify-content: center; transition: background 0.2s, transform 0.2s, color 0.2s, border-color 0.2s; box-shadow: var(--hrd-control-shadow); } #hrd-preview .hd button:hover { background: var(--hrd-control-bg-hover); border-color: var(--hrd-accent); color: var(--hrd-text-primary); } #hrd-preview .hd button:active { transform: scale(0.9); } #hrd-preview .hd button svg { width: 18px; height: 18px; } #hrd-preview.failed .hrd-preview-fallback .acts { display: none; } #hrd-preview.failed .hrd-preview-stage { display: none; } #hrd-source-panel { display: flex; flex-direction: column; padding-bottom: max(env(safe-area-inset-bottom, 0px), 24px); } #hrd-source-panel .hd { display: flex; align-items: center; gap: 6px; padding: 12px 16px 10px; flex-shrink: 0; } #hrd-source-panel .hd .ttl { flex: 1; font-size: 16px; font-weight: 700; color: var(--hrd-text-primary); display: flex; align-items: center; gap: 10px; min-width: 0; overflow: hidden; } #hrd-source-panel .hd .ttl > svg { width: 22px; height: 22px; color: var(--hrd-accent); flex-shrink: 0; } #hrd-source-panel .hd .ttl > span:first-of-type { white-space: nowrap; } #hrd-source-panel .hd .meta { font-size: 11px; font-weight: 500; color: var(--hrd-text-tertiary); margin-left: 4px; white-space: nowrap; flex-shrink: 0; } #hrd-source-panel .hd button { width: 40px; height: 40px; border-radius: 20px; border: 1px solid var(--hrd-control-border); background: var(--hrd-control-bg); color: var(--hrd-control-fg); cursor: pointer; padding: 0; flex-shrink: 0; display: flex; align-items: center; justify-content: center; transition: background 0.2s, transform 0.2s, color 0.2s, border-color 0.2s; box-shadow: var(--hrd-control-shadow); } #hrd-source-panel .hd button:hover { background: var(--hrd-control-bg-hover); border-color: var(--hrd-accent); color: var(--hrd-text-primary); } #hrd-source-panel .hd button:active { transform: scale(0.9); } #hrd-source-panel .hd button svg { width: 18px; height: 18px; } #hrd-source-panel .hd button.active, #hrd-detail .hd button.active { color: var(--hrd-accent); background: var(--hrd-accent-soft); } #hrd-source-panel .src-container, #hrd-detail .src-container { flex: 1; min-height: 0; overflow: auto; -webkit-overflow-scrolling: touch; overscroll-behavior: contain; background: var(--hrd-src-bg); border-radius: 20px; margin: 4px 12px 8px; padding: 8px 0; font-family: 'HarmonyOS Sans Mono', 'SF Mono', 'Menlo', 'Monaco', 'Cascadia Code', 'Fira Code', Consolas, monospace; font-size: 12px; line-height: 1.55; color: var(--hrd-src-text); tab-size: 4; -moz-tab-size: 4; } @-moz-document url-prefix() { #hrd-source-panel .src-container, #hrd-detail .src-container { scrollbar-width: thin; scrollbar-color: var(--hrd-src-scroll) transparent; } } #hrd-source-panel .src-container::-webkit-scrollbar, #hrd-detail .src-container::-webkit-scrollbar { width: 6px; height: 6px; } #hrd-source-panel .src-container::-webkit-scrollbar-track, #hrd-detail .src-container::-webkit-scrollbar-track { background: transparent; } #hrd-source-panel .src-container::-webkit-scrollbar-track:vertical, #hrd-detail .src-container::-webkit-scrollbar-track:vertical { margin: 14px 0; } #hrd-source-panel .src-container::-webkit-scrollbar-track:horizontal, #hrd-detail .src-container::-webkit-scrollbar-track:horizontal { margin: 0 14px; } #hrd-source-panel .src-container::-webkit-scrollbar-thumb, #hrd-detail .src-container::-webkit-scrollbar-thumb { background: var(--hrd-src-scroll); border-radius: 3px; } #hrd-source-panel .src-container::-webkit-scrollbar-thumb:hover, #hrd-detail .src-container::-webkit-scrollbar-thumb:hover { background: var(--hrd-src-scroll-hover); } #hrd-source-panel .src-container::-webkit-scrollbar-corner, #hrd-detail .src-container::-webkit-scrollbar-corner { background: transparent; } .src-code { display: inline-block; min-width: 100%; padding: 4px 0 8px; vertical-align: top; } #hrd-source-panel.wrap .src-code, #hrd-detail.wrap .src-code { display: block; } .src-line { min-height: 1.55em; transition: background 0.15s; display: block; white-space: nowrap; } #hrd-source-panel.wrap .src-line, #hrd-detail.wrap .src-line { display: flex; align-items: flex-start; white-space: normal; } .src-line:hover { background: var(--hrd-src-hover); } .src-lineno { display: inline-block; min-width: 2.5em; padding-left: 10px; padding-right: 10px; text-align: right; color: var(--hrd-src-lineno); user-select: none; -webkit-user-select: none; border-right: 1px solid var(--hrd-src-border); font-variant-numeric: tabular-nums; position: sticky; left: 0; background: var(--hrd-src-bg); z-index: 2; vertical-align: top; } #hrd-source-panel.wrap .src-lineno, #hrd-detail.wrap .src-lineno { flex: 0 0 auto; } .src-line:hover .src-lineno { background: #22272F; } .src-line-content { display: inline-block; padding-right: 12px; vertical-align: top; white-space: pre; } #hrd-source-panel.wrap .src-line-content, #hrd-detail.wrap .src-line-content { display: block; flex: 1 1 auto; min-width: 0; white-space: pre-wrap; word-break: break-word; overflow-wrap: anywhere; } .src-more { padding: 12px 20px; text-align: center; color: #8A9199; font-size: 12px; border-top: 1px dashed var(--hrd-src-border); margin: 8px 16px 0; line-height: 1.6; } .src-more .hint { display: block; margin-top: 6px; color: #6C7480; font-size: 11px; word-break: break-all; } .hl-tag-name { color: #E06C75; font-weight: 500; } .hl-attr-name { color: #D19A66; } .hl-attr-value { color: #98C379; } .hl-punct { color: #ABB2BF; } .hl-comment { color: #5C6370; font-style: italic; } .hl-doctype { color: #C678DD; font-weight: 500; } .hl-tag { color: #E06C75; } .hl-text { color: var(--hrd-src-text); } @media (prefers-reduced-motion: reduce) { #hrd-panel, #hrd-backdrop, #hrd-toast, .hrd-item, .hrd-btn, .hrd-icon-btn, .hrd-chip, #hrd-fab { transition: none !important; animation: none !important; } .hrd-list { scroll-behavior: auto; } } `; this.root.appendChild(s); }, buildUI() { const c = document.createElement('div'); c.innerHTML = `
`; this.root.appendChild(c); this.panel = this.root.getElementById('hrd-panel'); this.list = this.root.getElementById('hrd-list'); this.backdrop = this.root.getElementById('hrd-backdrop'); this.countEl = this.root.getElementById('hrd-count'); this.renderFilters(); this.updateSortIcon(); this.updateViewIcon(); }, renderFilters() { const f = this.root.getElementById('hrd-filters'); if (!f) return; if (f.childElementCount !== 3) { const defs = [ { key: 'image', label: '图片', icon: Icons.image }, { key: 'media', label: '媒体', icon: Icons.media }, { key: 'other', label: '其他', icon: Icons.doc } ]; f.innerHTML = ''; defs.forEach(d => { const b = document.createElement('button'); b.type = 'button'; b.className = 'hrd-chip'; b.dataset.filter = d.key; b.innerHTML = `${d.icon}${d.label} 0`; b.onclick = () => { Store.setFilter(Store.currentFilter === d.key ? '' : d.key); this._visibleLimit = CONFIG.BATCH_SIZE; this.renderFilters(); this.renderList({ full: true }); }; f.appendChild(b); }); } const counts = Store.counts(); const chips = f.children; for (let i = 0; i < chips.length; i++) { const key = chips[i].dataset.filter; const label = chips[i].querySelector('.hrd-chip-label'); const base = key === 'image' ? '图片' : key === 'media' ? '媒体' : '其他'; if (label) label.textContent = base + ' ' + (counts[key] || 0); chips[i].classList.toggle('on', Store.currentFilter === key); chips[i].setAttribute('aria-pressed', Store.currentFilter === key ? 'true' : 'false'); } }, updateSortIcon() { const btn = this.root.getElementById('hrd-sort'); if (!btn) return; const mode = SORT_MODES.find(m => m.key === Store.sortMode) || SORT_MODES[0]; btn.innerHTML = Icons[mode.icon]; btn.title = '排序:' + mode.label; btn.setAttribute('aria-label', '排序:' + mode.label); }, updateViewIcon() { const btn = this.root.getElementById('hrd-view'); if (!btn) return; btn.innerHTML = Store.viewMode === 'list' ? Icons.listView : Icons.gridView; const label = Store.viewMode === 'list' ? '列表视图' : '网格视图'; btn.title = label; btn.setAttribute('aria-label', label); }, scheduleUpdate() { if (this._raf) return; this._raf = requestAnimationFrame(() => { this._raf = null; this._syncCounts(); this.renderFilters(); if (this.panel && this.panel.classList.contains('on')) { this.renderList({ full: true }); } }); }, _syncCounts() { if (this.countEl) this.countEl.textContent = Store.resources.length; const fab = this.root.getElementById('hrd-fab'); if (fab) fab.title = '资源日记 · ' + Store.resources.length; }, _overlayNode(name) { const id = UI._OVERLAY_IDS[name]; return id ? this.root.getElementById(id) : null; }, pushOverlay(name) { const node = this._overlayNode(name); if (node && node.parentNode) { if (!this._overlayMask) { const m = document.createElement('div'); m.id = 'hrd-overlay-mask'; this._overlayMask = m; } node.parentNode.insertBefore(this._overlayMask, node); } this._overlays.push(name); this._lockScroll(); }, popOverlay(name) { const idx = this._overlays.lastIndexOf(name); if (idx >= 0) this._overlays.splice(idx, 1); this._syncOverlayMask(); const node = this._overlayNode(name); if (node && node.parentNode) node.parentNode.removeChild(node); this._unlockScroll(); }, _syncOverlayMask() { const m = this._overlayMask; if (!m) return; for (let i = this._overlays.length - 1; i >= 0; i--) { const node = this._overlayNode(this._overlays[i]); if (node && node.parentNode) { node.parentNode.insertBefore(m, node); return; } } if (m.parentNode) m.parentNode.removeChild(m); }, closeTopOverlay() { const name = this._overlays[this._overlays.length - 1]; if (!name) return false; if (name === 'source') { SourceView.cancelLoad(); SourceView._el = null; } if (name === 'player') this._teardownPlayer(); this.popOverlay(name); return true; }, renderList(opts) { if (!this.list) return; opts = opts || {}; const token = ++this._renderToken; const self = this; this.list.classList.remove('view-list', 'view-grid', 'view-empty'); const items = Store.filtered(); const limit = Math.min(items.length, this._visibleLimit); if (items.length === 0) { this.list.classList.add('view-empty'); const hint = Store.currentFilter ? '当前分类下暂无资源,可点击上方标签切换' : '浏览网页时将自动捕获图片、视频、音频等资源'; this.list.innerHTML = '
' + Icons.emptyState + '
' + '
暂无捕获的资源
' + '
' + hint + '
'; this._itemNodes = new Map(); return; } this.list.classList.add(Store.viewMode === 'list' ? 'view-list' : 'view-grid'); this.list.innerHTML = ''; this._itemNodes = new Map(); const slice = items.slice(0, limit); let i = 0; const chunkSize = Math.max(1, Math.min(CONFIG.LIST_RENDER_CHUNK, Math.ceil(limit / 4) || 1)); function paintChunk() { if (token !== self._renderToken) return; const end = Math.min(i + chunkSize, limit); const frag = document.createDocumentFragment(); for (; i < end; i++) { const el = self._renderItem(slice[i]); if (el) { frag.appendChild(el); self._itemNodes.set(slice[i].url, el); } } self.list.appendChild(frag); if (i < limit) { requestAnimationFrame(paintChunk); } else { self._appendMoreButton(items.length, limit); } } requestAnimationFrame(paintChunk); }, _appendMoreButton(total, limit) { if (total <= limit) return; const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'hrd-more'; btn.textContent = `加载更多(已显示 ${limit} / ${total})`; btn.onclick = () => { this._visibleLimit = Math.min(this._visibleLimit + CONFIG.BATCH_SIZE, total); this.renderList({ full: true }); this.list.scrollTop = this.list.scrollHeight; }; this.list.appendChild(btn); }, _thumbViaCipher(url, img, thumb, r, onFail, onOk) { const canTry = r && (r.filterType === 'image' || IMAGE_TYPES.has(r.displayType)) && /^https?:/i.test(url); if (!canTry) { onFail(); return; } let done = false; const fail = () => { if (done) return; done = true; clearTimeout(guard); onFail(); }; const guard = setTimeout(fail, 25000); const go = (res) => { if (done) return; if (!res || !res.ok || !res.url) { fail(); return; } // 换用全新的 img 元素:复用已经触发过 error 的元素重新设 src 不会再次派发 load const fresh = document.createElement('img'); fresh.alt = ''; fresh.decoding = 'async'; fresh.draggable = false; fresh.referrerPolicy = 'origin'; fresh.onload = () => { if (done) return; if (!fresh.naturalWidth || !fresh.naturalHeight) { fail(); return; } done = true; clearTimeout(guard); this._clearThumb(thumb); thumb.appendChild(fresh); onOk(fresh); }; fresh.onerror = fail; fresh.src = res.url; }; try { ImageCipher.probe(url).then(go).catch(() => fail()); } catch { fail(); } }, _clearThumb(thumb) { if (!thumb) return; const nodes = thumb.querySelectorAll('img'); for (let i = 0; i < nodes.length; i++) { const n = nodes[i]; try { n.removeAttribute('src'); } catch {} if (n.parentNode) n.parentNode.removeChild(n); } }, _fallbackThumb(thumb, displayType, failed) { const node = thumb.firstElementChild; if (failed && node && node.tagName === 'IMG') { node.classList.add('failed'); return; } if (node && node.classList.contains('hrd-icon')) return; this._clearThumb(thumb); thumb.innerHTML = '
' + this._iconFor(displayType) + '
'; }, _renderItem(r) { const el = document.createElement('div'); el.className = 'hrd-item'; el.dataset.url = r.url; el.setAttribute('role', 'listitem'); const kind = TYPE_ICON_MAP[r.displayType] || 'doc'; const thumb = document.createElement('div'); thumb.className = 'hrd-thumb'; thumb.dataset.kind = kind; const url = r.url; const isDataImage = /^data:image\//i.test(url); const canTryThumb = url.length <= 8192 || isDataImage; if (canTryThumb) { const img = document.createElement('img'); img.alt = ''; img.decoding = 'async'; img.loading = 'lazy'; img.draggable = false; if (!isDataImage) img.referrerPolicy = 'origin'; let settled = false; let timer = null; const finishOk = (node) => { if (settled) return; settled = true; if (timer) { clearTimeout(timer); timer = null; } (node || img).classList.add('loaded'); }; const finishFail = (reason) => { if (settled) return; settled = true; if (timer) { clearTimeout(timer); timer = null; } Store.markThumbFail(url, reason); UI._clearThumb(thumb); UI._fallbackThumb(thumb, r.displayType, false); }; const finishIcon = () => { if (settled) return; settled = true; if (timer) { clearTimeout(timer); timer = null; } Store.resetThumbFail(url); UI._clearThumb(thumb); UI._fallbackThumb(thumb, r.displayType, false); }; img.onload = () => { if (settled) return; const tooSmall = img.naturalWidth < MIN_THUMB_SIZE || img.naturalHeight < MIN_THUMB_SIZE; const px = (img.naturalWidth || 0) * (img.naturalHeight || 0); if (tooSmall || (px > 0 && px <= 100)) { finishIcon(); return; } finishOk(); }; img.onerror = () => { if (settled) return; if (timer) { clearTimeout(timer); timer = null; } UI._thumbViaCipher(url, img, thumb, r, () => finishFail('error'), finishOk); }; timer = setTimeout(() => { if (settled) return; timer = null; UI._thumbViaCipher(url, img, thumb, r, () => finishFail('timeout'), finishOk); }, CONFIG.THUMB_TIMEOUT_MS); img.src = url; thumb.appendChild(img); } else { this._fallbackThumb(thumb, r.displayType, false); } const info = document.createElement('div'); info.className = 'hrd-info'; const nameEl = document.createElement('div'); nameEl.className = 'hrd-name'; const tagEl = document.createElement('span'); tagEl.className = 'hrd-tag'; tagEl.dataset.kind = kind; tagEl.textContent = TYPE_TAG_MAP[r.displayType] || r.displayType.toUpperCase(); nameEl.appendChild(tagEl); const streamKind = r.filterType === 'media' ? StreamDetect.kind(r.url) : 'plain'; if (streamKind !== 'plain') { const sk = document.createElement('span'); sk.className = 'hrd-stream-tag'; sk.dataset.kind = streamKind; sk.textContent = streamKind === 'hls' ? 'HLS' : 'TS分片'; sk.title = streamKind === 'hls' ? 'HLS 流媒体,播放时自动启用播放引擎' : 'HLS 分片文件,播放时尝试定位所属播放列表'; nameEl.appendChild(sk); } const nameText = document.createElement('span'); nameText.className = 'hrd-name-text'; nameText.textContent = r.filename; nameText.title = r.filename; nameEl.appendChild(nameText); info.appendChild(nameEl); const urlEl = document.createElement('div'); urlEl.className = 'hrd-url'; urlEl.textContent = displayUrl(r.url); info.appendChild(urlEl); const acts = document.createElement('div'); acts.className = 'hrd-acts'; const mkBtn = (icon, label, handler) => { const b = document.createElement('button'); b.type = 'button'; b.className = 'hrd-btn'; b.title = label; b.setAttribute('aria-label', label); b.innerHTML = icon; b.onclick = e => { e.stopPropagation(); handler(); }; return b; }; acts.appendChild(mkBtn(Icons.copy, '复制链接', () => this.copy(r.url))); if (r.filterType === 'media' && VIDEO_PLAY_TYPES.has(r.displayType)) { acts.appendChild(mkBtn(Icons.play, '播放', () => this.play(r))); } else if (r.filterType === 'media') { acts.appendChild(mkBtn(Icons.play, '播放', () => this.play(r))); } const canDownload = Security.isDownloadable(r.url); const dlBtn = mkBtn(Icons.download, canDownload ? '下载' : '该地址不支持下载', () => this.download(r)); if (!canDownload) { dlBtn.disabled = true; dlBtn.classList.add('disabled'); dlBtn.onclick = e => { e.stopPropagation(); this.toast('该地址不支持下载,可点击复制链接'); }; } acts.appendChild(dlBtn); el.appendChild(thumb); el.appendChild(info); el.appendChild(acts); el.tabIndex = 0; el.onclick = () => { if (r.filterType === 'image') this.previewImage(r); else if (r.filterType === 'media') this.play(r); else this.showDetail(r); }; el.onkeydown = (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); el.onclick(); } }; return el; }, _iconFor(displayType) { const kind = TYPE_ICON_MAP[displayType] || 'doc'; const map = { image: Icons.image, video: Icons.video, audio: Icons.audio, code: Icons.code, json: Icons.json, font: Icons.font, link: Icons.link, doc: Icons.doc }; return map[kind] || Icons.doc; }, _bindSwipe() { if (this._swipeBound) return; const el = this.list; if (!el) return; this._swipeBound = true; const self = this; let startX = 0, startY = 0, startT = 0; let tracking = false; let isHorizontal = false; const onStart = (e) => { if (e.touches.length !== 1) { tracking = false; return; } const t = e.touches[0]; startX = t.clientX; startY = t.clientY; startT = Date.now(); tracking = true; isHorizontal = false; }; const onMove = (e) => { if (!tracking) return; const t = e.touches[0]; const dx = t.clientX - startX; const dy = t.clientY - startY; if (!isHorizontal && Math.abs(dx) > 8 && Math.abs(dx) > Math.abs(dy)) { isHorizontal = true; } if (isHorizontal && Math.abs(dy) > CONFIG.SWIPE_MAX_DY) { tracking = false; } }; const onEnd = (e) => { if (!tracking) return; tracking = false; if (!isHorizontal) return; const t = e.changedTouches[0]; const dx = t.clientX - startX; const dy = t.clientY - startY; const dt = Date.now() - startT; if (Math.abs(dx) < CONFIG.SWIPE_MIN_DX) return; if (Math.abs(dy) > CONFIG.SWIPE_MAX_DY) return; if (dt > CONFIG.SWIPE_MAX_TIME) return; let curIdx = FILTER_ORDER.indexOf(Store.currentFilter); if (curIdx < 0) curIdx = 0; let nextIdx = curIdx; if (dx < 0) nextIdx = Math.min(curIdx + 1, FILTER_ORDER.length - 1); else nextIdx = Math.max(curIdx - 1, 0); if (Store.currentFilter === FILTER_ORDER[nextIdx]) return; Store.setFilter(FILTER_ORDER[nextIdx]); self._visibleLimit = CONFIG.BATCH_SIZE; self.renderFilters(); self.renderList({ full: true }); }; el.addEventListener('touchstart', onStart, { passive: true }); el.addEventListener('touchmove', onMove, { passive: true }); el.addEventListener('touchend', onEnd, { passive: true }); el.addEventListener('touchcancel', () => { tracking = false; }, { passive: true }); }, _onKeyDown(e) { if (e.key !== 'Escape') return; if (this.closeTopOverlay()) return; if (this.panel.classList.contains('on')) this.close(); }, bindEvents() { this.root.getElementById('hrd-fab').onclick = () => this.open(); this.backdrop.onclick = () => this.close(); this.root.getElementById('hrd-close').onclick = () => this.close(); this.root.getElementById('hrd-source-btn').onclick = () => SourceView.open(); this.root.getElementById('hrd-sort').onclick = () => { const idx = SORT_MODES.findIndex(m => m.key === Store.sortMode); const next = SORT_MODES[(idx + 1) % SORT_MODES.length]; Store.setSort(next.key); this.updateSortIcon(); this.renderList({ full: true }); this.toast('排序:' + next.label); }; this.root.getElementById('hrd-view').onclick = () => { Store.viewMode = Store.viewMode === 'list' ? 'grid' : 'list'; this.updateViewIcon(); this.renderList({ full: true }); this.toast(Store.viewMode === 'list' ? '已切换为列表视图' : '已切换为网格视图'); }; this._keyHandler = (e) => this._onKeyDown(e); document.addEventListener('keydown', this._keyHandler); this._bindSwipe(); }, _setOpening(on) { this._opening = !!on; if (this._openingTimer) { clearTimeout(this._openingTimer); this._openingTimer = null; } if (on) { this._openingTimer = setTimeout(() => { this._opening = false; this._openingTimer = null; }, CONFIG.LIST_OPEN_GUARD_MS); } }, open() { this._setOpening(true); this._ensureTopNode(); this.applyMaxLayer(); this._lastFocus = document.activeElement; this.panel.classList.add('on'); this.backdrop.classList.add('on'); this._lockScroll(); this.renderFilters(); this.renderList({ full: true }); requestAnimationFrame(() => { if (this.list) this.list.scrollTop = 0; }); }, _teardownPlayer() { const v = this._overlayNode('player'); if (!v) return; if (v.__hrdTimer) { clearTimeout(v.__hrdTimer); v.__hrdTimer = null; } if (v.__hrdDestroyHls) { try { v.__hrdDestroyHls(); } catch {} v.__hrdDestroyHls = null; } const m = v.querySelector('video,audio'); if (m) { try { m.pause(); } catch {} try { m.removeAttribute('src'); m.load(); } catch {} } }, close() { this._setOpening(false); while (this._overlays.length) { const name = this._overlays[this._overlays.length - 1]; if (name === 'source') { SourceView.cancelLoad(); SourceView._el = null; } if (name === 'player') this._teardownPlayer(); this.popOverlay(name); } if (this._overlayMask && this._overlayMask.parentNode) { this._overlayMask.parentNode.removeChild(this._overlayMask); } this._renderToken++; this.panel.classList.remove('on'); this.backdrop.classList.remove('on'); if (this.host && this.host.parentNode && this.host.nextSibling) { try { this.host.parentNode.appendChild(this.host); } catch {} } this._unlockScroll(); }, _lockScroll() { if (this._scrollLocked) return; this._scrollLocked = true; this._savedBodyOverflow = document.body.style.overflow; this._savedHtmlOverflow = document.documentElement.style.overflow; document.body.style.overflow = 'hidden'; document.documentElement.style.overflow = 'hidden'; }, _unlockScroll() { if (!this._scrollLocked) return; if (this._overlays.length > 0) return; if (this.panel && this.panel.classList.contains('on')) return; this._scrollLocked = false; document.body.style.overflow = this._savedBodyOverflow; document.documentElement.style.overflow = this._savedHtmlOverflow; }, toast(msg) { const t = this.root.getElementById('hrd-toast'); if (!t || !msg) return; t.textContent = msg; t.classList.add('on'); clearTimeout(this._toastTimer); this._toastTimer = setTimeout(() => t.classList.remove('on'), 1800); }, async copy(text) { try { await navigator.clipboard.writeText(text); this.toast('已复制'); return; } catch {} try { const ta = document.createElement('textarea'); ta.value = text; ta.setAttribute('readonly', ''); ta.style.cssText = 'position:fixed;left:-9999px;top:0;opacity:0;'; document.body.appendChild(ta); ta.select(); ta.setSelectionRange(0, ta.value.length); const ok = document.execCommand('copy'); document.body.removeChild(ta); this.toast(ok ? '已复制' : '复制失败'); } catch { this.toast('复制失败'); } }, async copySilent(text) { try { await navigator.clipboard.writeText(text); return true; } catch {} try { const ta = document.createElement('textarea'); ta.value = text; ta.setAttribute('readonly', ''); ta.style.cssText = 'position:fixed;left:-9999px;top:0;opacity:0;'; document.body.appendChild(ta); ta.select(); ta.setSelectionRange(0, ta.value.length); const ok = document.execCommand('copy'); document.body.removeChild(ta); return ok; } catch { return false; } }, async download(r) { const filename = r.filename || 'resource'; if (!Security.isDownloadable(r.url)) { this.toast('该地址不支持下载,可先复制链接'); return false; } const dec = await this._decryptForDownload(r); if (dec) { const ok = await this._saveBlob(dec.blob, this._decName(filename, dec.mime)); if (ok) this.toast('已解密并下载'); return ok; } if (r.url.slice(0, 5).toLowerCase() !== 'data:') { const gm = this._gmDownload(r, filename); if (gm) { const ok = await gm; if (ok) { this.toast('下载完成'); return true; } } } const ok = await this._browserDownload(r, filename); if (ok) this.toast('下载已开始'); return ok; }, _decName(filename, mime) { const ext = (mime || '').indexOf('png') >= 0 ? 'png' : (mime || '').indexOf('gif') >= 0 ? 'gif' : (mime || '').indexOf('webp') >= 0 ? 'webp' : 'jpg'; const base = String(filename || 'image').replace(/\.[a-z0-9]+$/i, '') || 'image'; return base + '.' + ext; }, async _decryptForDownload(r) { const url = r.url; if (!url || !/^https?:/i.test(url)) return null; const isImg = r.filterType === 'image' || IMAGE_TYPES.has(r.displayType); if (!isImg) return null; try { const res = await ImageCipher.probe(url); if (!res || !res.ok || !res.url) return null; const resp = await fetch(res.url); if (!resp.ok) return null; const blob = await resp.blob(); if (!blob || !blob.size) return null; return { blob: blob, mime: blob.type || 'image/jpeg' }; } catch { return null; } }, _saveBlob(blob, name) { return new Promise(resolve => { let href = ''; try { href = URL.createObjectURL(blob); } catch { resolve(false); return; } try { const a = document.createElement('a'); a.href = href; a.download = name; a.rel = 'noopener'; a.style.display = 'none'; document.body.appendChild(a); a.click(); setTimeout(() => { if (a.parentNode) a.parentNode.removeChild(a); try { URL.revokeObjectURL(href); } catch {} }, 200); resolve(true); } catch { try { URL.revokeObjectURL(href); } catch {} resolve(false); } }); }, _gmDownload(r, filename) { if (typeof GM_download !== 'function') return null; return new Promise(resolve => { let done = false; const finish = (ok) => { if (done) return; done = true; resolve(ok); }; const timer = setTimeout(() => finish(false), 8000); try { GM_download({ url: r.url, name: filename, saveAs: false, onload: () => { clearTimeout(timer); finish(true); }, onerror: () => { clearTimeout(timer); finish(false); }, ontimeout: () => { clearTimeout(timer); finish(false); } }); } catch { clearTimeout(timer); finish(false); } }); }, async _browserDownload(r, filename) { const name = filename || 'resource'; const url = r.downloadUrl || r.url; try { const a = document.createElement('a'); a.href = url; a.download = name; a.rel = 'noopener'; a.style.display = 'none'; document.body.appendChild(a); a.click(); setTimeout(() => { if (a.parentNode) a.parentNode.removeChild(a); }, 100); return true; } catch {} if (Security.isDirectDownloadable(url)) { const ok = await this._fetchToBlob(url, name); if (ok) return true; } else { try { this.toast('当前环境不支持该类型下载,已复制链接'); } catch {} } let opened = false; try { opened = !!window.open(url, '_blank', 'noopener,noreferrer'); } catch {} try { await this.copy(url); } catch {} this.toast(opened ? '已在新标签页打开,链接已复制' : '下载失败,链接已复制,可粘贴到浏览器打开'); return opened; }, _fetchToBlob(url, filename) { return new Promise(resolve => { try { const xhr = new XMLHttpRequest(); let timedOut = false; xhr.open('GET', url, true); xhr.timeout = 15000; try { xhr.responseType = 'blob'; } catch {} xhr.onload = () => { if (timedOut) return; const blob = xhr.response; if (!blob || typeof blob.size !== 'number') { resolve(false); return; } if (blob.size === 0 || blob.size > CONFIG.INLINE_READ_MAX_BYTES) { resolve(false); return; } let objectUrl = ''; try { objectUrl = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = objectUrl; a.download = filename; a.rel = 'noopener'; a.style.display = 'none'; document.body.appendChild(a); a.click(); setTimeout(() => { if (a.parentNode) a.parentNode.removeChild(a); try { URL.revokeObjectURL(objectUrl); } catch {} }, 3000); } catch { resolve(false); return; } resolve(true); }; xhr.onerror = () => { if (!timedOut) resolve(false); }; xhr.ontimeout = () => { timedOut = true; resolve(false); }; xhr.onabort = () => { if (!timedOut) resolve(false); }; xhr.send(); } catch { resolve(false); } }); }, play(r) { const existing = this.root.getElementById('hrd-player'); if (existing) this.popOverlay('player'); const isVideo = VIDEO_PLAY_TYPES.has(r.displayType.toLowerCase()); const kind = StreamDetect.kind(r.url); const isHls = kind === 'hls'; const isSeg = kind === 'seg'; const canHls = isVideo && isHls; const p = document.createElement('div'); p.id = 'hrd-player'; p.setAttribute('role', 'dialog'); p.setAttribute('aria-label', isVideo ? '视频播放' : '音频播放'); const handle = document.createElement('div'); handle.className = 'hrd-sheet-handle'; p.appendChild(handle); const hd = document.createElement('div'); hd.className = 'hd'; const ttl = document.createElement('div'); ttl.className = 'ttl'; ttl.innerHTML = `${isVideo ? Icons.video : Icons.audio}${isVideo ? '视频播放' : '音频播放'}`; hd.appendChild(ttl); const acts = document.createElement('div'); acts.className = 'hrd-hd-acts'; const mkHBtn = (icon, label, handler) => { const b = document.createElement('button'); b.type = 'button'; b.title = label; b.setAttribute('aria-label', label); b.innerHTML = icon; b.onclick = e => { e.stopPropagation(); handler(); }; return b; }; acts.appendChild(mkHBtn(Icons.copy, '复制链接', () => this.copy(r.url))); acts.appendChild(mkHBtn(Icons.openInNew, '在新标签页打开', () => { try { window.open(r.url, '_blank', 'noopener,noreferrer'); } catch {} })); acts.appendChild(mkHBtn(Icons.download, '下载', () => this.download(r))); acts.appendChild(mkHBtn(Icons.close, '关闭', () => this.closeTopOverlay())); hd.appendChild(acts); const wrap = document.createElement('div'); wrap.className = 'media-wrap'; const status = document.createElement('div'); status.className = 'hrd-player-status'; const fallback = document.createElement('div'); fallback.className = 'hrd-player-fallback'; fallback.style.display = 'none'; let media = null; let destroyHls = null; let resolvedUrl = r.url; const setStatus = (text, tone) => { if (!text) { status.textContent = ''; status.style.display = 'none'; return; } status.style.display = ''; status.textContent = text; status.dataset.tone = tone || 'info'; }; const hardFail = (title, sub) => { fallback.innerHTML = ''; const t = document.createElement('div'); t.className = 't'; t.textContent = title; const s = document.createElement('div'); s.className = 's'; s.textContent = sub || ''; const btns = document.createElement('div'); btns.className = 'acts'; const mkF = (label, handler) => { const b = document.createElement('button'); b.type = 'button'; b.textContent = label; b.onclick = e => { e.stopPropagation(); handler(); }; return b; }; btns.appendChild(mkF('新标签页打开', () => { try { window.open(resolvedUrl, '_blank', 'noopener,noreferrer'); } catch {} })); btns.appendChild(mkF('复制链接', () => this.copy(resolvedUrl))); if (isSeg) { const guess = StreamDetect.guessM3u8(resolvedUrl); if (guess) { btns.appendChild(mkF('复制推导出的 m3u8', () => this.copy(guess))); } } fallback.appendChild(t); if (sub) fallback.appendChild(s); fallback.appendChild(btns); fallback.style.display = ''; if (media) { try { media.pause(); } catch {} media.style.display = 'none'; } if (wrap) wrap.style.display = 'none'; p.classList.add('failed'); }; p.appendChild(hd); p.appendChild(wrap); p.appendChild(status); p.appendChild(fallback); this.root.appendChild(p); this.pushOverlay('player'); const finish = () => { if (isVideo) { media = document.createElement('video'); media.controls = true; media.playsInline = true; media.setAttribute('playsinline', ''); media.setAttribute('webkit-playsinline', ''); media.setAttribute('x5-playsinline', ''); media.setAttribute('x5-video-player-type', 'h5'); media.preload = 'metadata'; } else { media = document.createElement('audio'); media.controls = true; media.preload = 'metadata'; } media.addEventListener('error', () => { if (media && media.dataset.hrdHls === '1') return; hardFail('无法播放该资源', isSeg ? '这是播放列表里的单个分片,不是完整视频' : '资源可能已失效、需要页面鉴权或受防盗链限制'); }); const tryAutoPlay = () => { try { const pr = media.play(); if (pr && typeof pr.catch === 'function') { pr.catch(() => { setStatus('点击播放按钮开始播放', 'info'); }); } } catch {} }; if (!isVideo || !canHls) { media.src = r.url; wrap.appendChild(media); if (isSeg && isVideo) { const guess = StreamDetect.guessM3u8(r.url); const recoverable = guess && StreamDetect.canRecoverM3u8(r.url, guess); if (recoverable) { setStatus('检测到 HLS 分片,正在还原所属播放列表…', 'warn'); const self = this; StreamDetect.probeM3u8(guess).then(ok => { if (!self.root.getElementById('hrd-player')) return; if (ok) { self.play(Object.assign({}, r, { url: guess, displayType: 'm3u8' })); return; } setStatus('该分片无法还原为完整播放列表', 'warn'); hardFail('无法直接播放该分片', '这段 .ts 只是播放列表中的一个分片,其鉴权参数是按分片单独签发的,' + '无法反推出带有效凭据的 .m3u8。请在列表中改选同一页面的 .m3u8 资源播放。'); }); return; } setStatus('这是播放列表里的单个分片,不是完整视频', 'warn'); hardFail('无法直接播放该分片', '这段 .ts 只是播放列表中的一个分片,其鉴权参数按分片单独签发,无法反推带有效凭据的 .m3u8。' + '请在列表中改选同一页面的 .m3u8 资源播放。'); } tryAutoPlay(); return; } const cached = Capture.headersFor(r.url); const authDesc = MediaHeaders.describe(); media.dataset.hrdHls = '1'; HlsPlay.patchElement(media, r.url); wrap.appendChild(media); setStatus('正在加载 HLS 播放引擎…', 'info'); destroyHls = HlsPlay.attach(media, r.url, { headers: cached, onStatus: (st) => { if (!this.root.getElementById('hrd-player')) return; if (st === 'manifest') { setStatus(authDesc ? ('HLS 就绪 · 已注入 CDN 鉴权(' + authDesc + ')') : 'HLS 就绪', 'ok'); tryAutoPlay(); } else if (st === 'native') { setStatus('已使用系统原生 HLS 播放', 'info'); tryAutoPlay(); } else if (st === 'hls-auth') { setStatus('HLS 就绪 · 已注入 CDN 鉴权(' + authDesc + ')', 'ok'); } else if (st === 'hls') { setStatus('HLS 就绪', 'ok'); } }, onFail: (msg) => { if (!this.root.getElementById('hrd-player')) return; const expired = StreamDetect.expiryText(r.url); hardFail(msg || 'HLS 播放失败', expired ? ('链接状态:' + expired) : ''); } }); const timer = setTimeout(() => { if (!this.root.getElementById('hrd-player')) return; if (media && media.readyState === 0 && status.dataset.tone !== 'ok') { setStatus(authDesc ? '仍在加载…(已注入 CDN 鉴权)' : '仍在加载…', 'warn'); } }, 6000); p.__hrdTimer = timer; p.__hrdDestroyHls = destroyHls; }; finish(); }, previewImage(r) { const before = this.root.getElementById('hrd-preview'); if (before) this.popOverlay('preview'); const url = r.url; const filename = r.filename || 'resource'; const dtt = r.displayType; const p = document.createElement('div'); p.id = 'hrd-preview'; p.setAttribute('role', 'dialog'); p.setAttribute('aria-label', '图片预览'); const stage = document.createElement('div'); stage.className = 'hrd-preview-stage'; const img = document.createElement('img'); img.alt = '资源预览'; img.draggable = false; img.decoding = 'async'; img.referrerPolicy = 'origin'; const loading = document.createElement('div'); loading.className = 'hrd-preview-loading'; loading.textContent = '正在加载原图…'; const fallback = document.createElement('div'); fallback.className = 'hrd-preview-fallback'; fallback.innerHTML = '' + Icons.image + '' + '
原图加载失败
' + '
资源可能已失效、需登录或受防盗链限制
' + '
'; stage.appendChild(img); stage.appendChild(loading); stage.appendChild(fallback); let settled = false; let zoomed = false; let decoded = null; const isBlob = url.slice(0, 5).toLowerCase() === 'blob:'; const isData = url.slice(0, 5).toLowerCase() === 'data:'; const isImg = r.filterType === 'image' || r.displayType === 'image' || dtt === 'image' || IMAGE_TYPES.has(r.displayType) || IMAGE_TYPES.has(dtt); const isEnc = !isBlob && !isData && /^https?:/i.test(url) && isImg; const showFallback = (msg) => { if (settled) return; settled = true; if (loading.parentNode) loading.parentNode.removeChild(loading); try { img.removeAttribute('src'); } catch {} if (msg) { const s = fallback.querySelector('.s'); if (s) s.textContent = msg; } fallback.classList.add('on'); p.classList.add('failed'); }; img.onload = () => { if (settled) return; settled = true; if (loading.parentNode) loading.parentNode.removeChild(loading); const px = (img.naturalWidth || 0) * (img.naturalHeight || 0); if (px > 0 && px <= 100) { showFallback('该图片尺寸过小(' + img.naturalWidth + '×' + img.naturalHeight + '),无法预览'); return; } img.classList.add('loaded'); if (decoded) { const note = document.createElement('div'); note.className = 'hrd-preview-cryptonote'; note.textContent = '已本站解密预览 · 下载得到的是可直接打开的图片'; stage.appendChild(note); } }; img.onerror = () => { if (settled) return; if (isBlob) { showFallback('该资源是页面内存中的临时数据(blob),刷新页面后即失效,无法再次读取'); } else if (r.displayType === 'image' && !isEnc) { showFallback(''); } else { showFallback(''); } }; img.addEventListener('load', () => { if (img.naturalWidth <= 1 && img.naturalHeight <= 1) { showFallback('该资源实际尺寸为 1×1,无法预览'); } }); if (isBlob) { const intro = document.createElement('div'); intro.className = 'hrd-preview-blobnote'; intro.textContent = '页面内存临时资源(blob),仅在当前页面有效'; stage.appendChild(intro); } const mkBtn = (icon, label) => { const b = document.createElement('button'); b.type = 'button'; b.title = label; b.setAttribute('aria-label', label); b.innerHTML = icon; return b; }; const actions = fallback.querySelector('.acts'); const dlBtn = mkBtn(Icons.download, '下载原图'); const cpBtn = mkBtn(Icons.copy, '复制链接'); dlBtn.onclick = e => { e.stopPropagation(); this.download(r); }; cpBtn.onclick = e => { e.stopPropagation(); this.copy(url); }; actions.appendChild(dlBtn); actions.appendChild(cpBtn); const hd = document.createElement('div'); hd.className = 'hd'; const ttl = document.createElement('div'); ttl.className = 'ttl'; ttl.innerHTML = Icons.image + '图片预览'; const copyLinkBtn = mkBtn(Icons.copy, '复制链接'); const downloadBtn = mkBtn(Icons.download, '下载原图'); const openLinkBtn = mkBtn(Icons.openInNew, '在新标签页打开'); copyLinkBtn.onclick = e => { e.stopPropagation(); this.copy(url); }; downloadBtn.onclick = e => { e.stopPropagation(); this.download(r); }; openLinkBtn.onclick = e => { e.stopPropagation(); try { window.open(url, '_blank', 'noopener,noreferrer'); } catch {} }; const closeBtn = mkBtn(Icons.close, '关闭'); closeBtn.className = 'hrd-preview-close'; closeBtn.onclick = e => { e.stopPropagation(); this.closeTopOverlay(); }; hd.appendChild(ttl); hd.appendChild(copyLinkBtn); hd.appendChild(downloadBtn); hd.appendChild(openLinkBtn); hd.appendChild(closeBtn); const handle = document.createElement('div'); handle.className = 'hrd-sheet-handle'; p.appendChild(handle); p.appendChild(hd); p.appendChild(stage); p.appendChild(fallback); p.onclick = e => { if (e.target === p || e.target === stage) this.closeTopOverlay(); }; stage.addEventListener('click', e => { if (e.target !== img || settled) return; zoomed = !zoomed; p.classList.toggle('zoom', zoomed); }); this.root.appendChild(p); this.pushOverlay('preview'); if (isEnc) { const cached = ImageCipher.cached(url); if (cached) { decoded = { url: cached }; img.src = cached; return; } img.style.opacity = '0'; loading.textContent = '正在尝试解密图片…'; const self = this; ImageCipher.probe(url).then(res => { if (!self.root.getElementById('hrd-preview')) return; img.style.opacity = ''; if (res && res.ok) { decoded = res; img.src = res.url; return; } if (res && res.plain) { img.src = url; return; } if (res && res.dead) { showFallback('原图地址已失效(该站图片链接带有时效签名),刷新页面重新捕获后再试'); return; } if (res && res.miss) { showFallback('该图片由本站脚本加密后存储,未能解析出可用的解密方式'); return; } showFallback(''); }).catch(() => { if (!self.root.getElementById('hrd-preview')) return; img.style.opacity = ''; img.src = url; }); return; } img.src = url; }, showDetail(r) { const existing = this.root.getElementById('hrd-detail'); if (existing) this.popOverlay('detail'); const tagText = TYPE_TAG_MAP[r.displayType] || r.displayType.toUpperCase(); const p = document.createElement('div'); p.id = 'hrd-detail'; p.setAttribute('role', 'dialog'); p.setAttribute('aria-label', '资源详情'); p.classList.toggle('wrap', this._detailWrap); const handle = document.createElement('div'); handle.className = 'hrd-sheet-handle'; p.appendChild(handle); const hd = document.createElement('div'); hd.className = 'hd'; const ttl = document.createElement('div'); ttl.className = 'ttl'; ttl.innerHTML = `${this._iconFor(r.displayType)}资源详情· ${tagText}`; const container = document.createElement('div'); container.className = 'src-container'; const codeEl = document.createElement('div'); codeEl.className = 'src-code'; container.appendChild(codeEl); const mkBtn = (icon, label, handler) => { const b = document.createElement('button'); b.type = 'button'; b.title = label; b.setAttribute('aria-label', label); b.innerHTML = icon; b.onclick = handler; return b; }; const wrapBtn = mkBtn( this._detailWrap ? Icons.wrapText : Icons.noWrapText, this._detailWrap ? '切换为不换行' : '切换为换行', () => { const st = container.scrollTop; const sl = container.scrollLeft; this._detailWrap = !this._detailWrap; p.classList.toggle('wrap', this._detailWrap); wrapBtn.innerHTML = this._detailWrap ? Icons.wrapText : Icons.noWrapText; wrapBtn.title = this._detailWrap ? '切换为不换行' : '切换为换行'; wrapBtn.setAttribute('aria-label', wrapBtn.title); wrapBtn.classList.toggle('active', this._detailWrap); requestAnimationFrame(() => { container.scrollTop = st; if (!this._detailWrap) container.scrollLeft = sl; }); } ); if (this._detailWrap) wrapBtn.classList.add('active'); hd.appendChild(ttl); hd.appendChild(wrapBtn); hd.appendChild(mkBtn(Icons.copy, '复制链接', () => this.copy(r.url))); hd.appendChild(mkBtn(Icons.download, '下载', () => this.download(r))); hd.appendChild(mkBtn(Icons.close, '关闭', () => this.closeTopOverlay())); p.appendChild(hd); p.appendChild(container); this.root.appendChild(p); this.pushOverlay('detail'); this._renderDetailContent(p, codeEl, r, tagText); }, async _renderDetailContent(p, codeEl, r, tagText) { const token = ++this._renderToken; const loading = document.createElement('div'); loading.className = 'src-more'; loading.textContent = '正在读取资源内容…'; codeEl.appendChild(loading); const result = await this._fetchResourceText(r.url, r.contentType); if (!p.isConnected || token !== this._renderToken) return; if (loading.parentNode) loading.parentNode.removeChild(loading); if (result.error) { this._renderDetailMessage(codeEl, result.error, r); return; } const text = result.text; if (result.binary) { codeEl.appendChild(this._mkNote('二进制文件,无法以文本形式显示', [ '类型:' + (r.contentType || r.displayType), '大小:' + formatSize(result.size) ])); return; } const lines = Lines.build(text, CONFIG.MAX_SOURCE_LINES); lines.render(codeEl); if (lines.truncated) { codeEl.appendChild(this._mkNote( `仅显示前 ${CONFIG.MAX_SOURCE_LINES} 行`, [])); } const metaEl = p.querySelector('.hd .meta'); if (metaEl) { metaEl.textContent = `· ${tagText} · ${formatSize(result.size)}`; } metaEl.title = `${r.filename}\n来源:${r.initiatorType || 'scan'}\n类型:${r.contentType || r.displayType}`; }, _fetchBlobUrl(url) { return new Promise(resolve => { try { const xhr = new XMLHttpRequest(); let timedOut = false; xhr.open('GET', url, true); xhr.timeout = 15000; try { xhr.responseType = 'blob'; } catch {} xhr.onload = () => { if (timedOut) return; const blob = xhr.response; if (!blob || typeof blob.size !== 'number') { resolve(null); return; } if (blob.size === 0) { resolve(null); return; } if (blob.size > CONFIG.INLINE_READ_MAX_BYTES) { Store.markThumbFail(url, 'large'); resolve({ tooLarge: true }); return; } let objectUrl = ''; try { objectUrl = URL.createObjectURL(blob); } catch { resolve(null); return; } resolve({ objectUrl: objectUrl, size: blob.size, meta: formatSize(blob.size) }); }; xhr.onerror = () => { if (!timedOut) resolve(null); }; xhr.ontimeout = () => { timedOut = true; resolve(null); }; xhr.onabort = () => { if (!timedOut) resolve(null); }; xhr.send(); } catch { resolve(null); } }); }, _mkNote(main, extra) { const n = document.createElement('div'); n.className = 'src-more'; n.textContent = main; if (extra && extra.length) { const hint = document.createElement('span'); hint.className = 'hint'; hint.textContent = extra.join(' · '); n.appendChild(hint); } return n; }, _renderDetailMessage(codeEl, message, r) { const reasons = { 'cross-origin': '跨域资源受浏览器同源策略限制,无法直接读取文本内容', 'network': '网络请求失败,资源可能已失效或需要登录', 'too-large': '文件体积过大,已跳过内容读取', 'http-error': '服务器拒绝了读取请求', 'empty': '资源内容为空' }; const main = reasons[message] || '无法加载该资源内容'; const extra = [displayUrl(r.url)]; if (message === 'cross-origin') { extra.push('可在新标签页直接打开查看'); } codeEl.appendChild(this._mkNote(main, extra)); if (message === 'cross-origin' || message === 'network') { const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'hrd-more'; btn.style.marginTop = '4px'; btn.textContent = '在新标签页打开'; btn.onclick = () => { try { window.open(r.url, '_blank', 'noopener,noreferrer'); } catch {} }; codeEl.appendChild(btn); } }, async _fetchResourceText(url, ct) { if (url.slice(0, 5).toLowerCase() === 'data:') { const comma = url.indexOf(','); if (comma < 0) return { error: 'empty' }; try { const text = /;base64/i.test(url.slice(0, comma)) ? atob(url.slice(comma + 1).replace(/\s/g, '')) : decodeURIComponent(url.slice(comma + 1)); return { text, size: text.length }; } catch { return { error: 'empty' }; } } if (ct && /^(image|video|audio|font)\//i.test(ct.split(';')[0].trim())) { const head = await this._headSize(url); if (head > CONFIG.MAX_SOURCE_BYTES) { return { error: 'too-large' }; } } try { const res = await fetch(url, { credentials: 'omit', mode: 'cors', referrerPolicy: 'no-referrer' }); if (!res.ok) return { error: 'http-error' }; const text = await res.text(); const broken = (text.match(/\uFFFD/g) || []).length; if (broken > 0 && broken / Math.max(text.length, 1) > 0.01) { return { binary: true, text: '', size: text.length }; } return { text, size: text.length }; } catch (err) { try { const text = await new Promise((resolve, reject) => { const x = new XMLHttpRequest(); x.open('GET', url, true); x.onload = () => resolve(x.responseText); x.onerror = () => reject(new Error('xhr')); x.ontimeout = () => reject(new Error('timeout')); x.timeout = 15000; x.send(); }); if (typeof text !== 'string') return { error: 'cross-origin' }; const broken = (text.match(/\uFFFD/g) || []).length; if (broken > 0 && broken / Math.max(text.length, 1) > 0.01) { return { binary: true, text: '', size: text.length }; } return { text, size: text.length }; } catch { return { error: 'cross-origin' }; } } }, _headSize(url) { return new Promise((resolve) => { try { const x = new XMLHttpRequest(); x.open('HEAD', url, true); x.timeout = 6000; x.onload = () => { const len = parseInt(x.getResponseHeader('content-length') || '0', 10); resolve(isNaN(len) ? 0 : len); }; x.onerror = () => resolve(0); x.ontimeout = () => resolve(0); x.send(); } catch { resolve(0); } }); } }; function stripHash(u) { if (typeof u !== 'string') return ''; const i = u.indexOf('#'); return i < 0 ? u : u.slice(0, i); } function formatSize(bytes) { if (!bytes || bytes < 0) return '未知大小'; if (bytes < 1024) return bytes + ' B'; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; if (bytes < 1024 * 1024 * 1024) return (bytes / 1024 / 1024).toFixed(2) + ' MB'; return (bytes / 1024 / 1024 / 1024).toFixed(2) + ' GB'; } const SourceView = { _html: '', _wrap: true, _el: null, _token: 0, cancelLoad() { this._token++; }, open() { const existing = UI._overlayNode('source'); if (existing) UI.popOverlay('source'); this.cancelLoad(); const token = this._token; const p = document.createElement('div'); p.id = UI._OVERLAY_IDS.source; p.setAttribute('role', 'dialog'); p.setAttribute('aria-label', '页面源代码'); if (this._wrap) p.classList.add('wrap'); const handle = document.createElement('div'); handle.className = 'hrd-sheet-handle'; p.appendChild(handle); const hd = document.createElement('div'); hd.className = 'hd'; const ttl = document.createElement('div'); ttl.className = 'ttl'; ttl.innerHTML = `${Icons.code}源代码`; const container = document.createElement('div'); container.className = 'src-container'; const codeEl = document.createElement('div'); codeEl.className = 'src-code'; container.appendChild(codeEl); const mkBtn = (icon, label, handler) => { const b = document.createElement('button'); b.type = 'button'; b.title = label; b.setAttribute('aria-label', label); b.innerHTML = icon; b.onclick = handler; return b; }; const wrapBtn = mkBtn( this._wrap ? Icons.wrapText : Icons.noWrapText, this._wrap ? '切换为不换行' : '切换为换行', () => { const st = container.scrollTop; const sl = container.scrollLeft; this._wrap = !this._wrap; p.classList.toggle('wrap', this._wrap); wrapBtn.innerHTML = this._wrap ? Icons.wrapText : Icons.noWrapText; wrapBtn.title = this._wrap ? '切换为不换行' : '切换为换行'; wrapBtn.setAttribute('aria-label', wrapBtn.title); wrapBtn.classList.toggle('active', this._wrap); requestAnimationFrame(() => { container.scrollTop = st; if (!this._wrap) container.scrollLeft = sl; }); } ); if (this._wrap) wrapBtn.classList.add('active'); const copyBtn = mkBtn(Icons.copy, '复制源码', async () => { const ok = await UI.copySilent(this._html); if (ok) { UI.toast('源码已复制'); copyBtn.innerHTML = Icons.check; copyBtn.classList.add('active'); setTimeout(() => { copyBtn.innerHTML = Icons.copy; copyBtn.classList.remove('active'); }, 1200); } else { UI.toast('复制失败'); } }); const closeBtn = mkBtn(Icons.close, '关闭', () => UI.closeTopOverlay()); hd.appendChild(ttl); hd.appendChild(wrapBtn); hd.appendChild(copyBtn); hd.appendChild(closeBtn); p.appendChild(hd); p.appendChild(container); UI.root.appendChild(p); this._el = p; UI.pushOverlay('source'); const loading = document.createElement('div'); loading.className = 'src-more'; loading.textContent = '正在读取页面源码…'; codeEl.appendChild(loading); const idle = window.requestIdleCallback ? (fn) => window.requestIdleCallback(fn, { timeout: 800 }) : (fn) => setTimeout(fn, 16); idle(() => { if (token !== this._token || !p.isConnected) return; let html = ''; try { const dt = document.doctype; let doctypeStr = ''; if (dt) { const name = dt.name || 'html'; const pub = dt.publicId ? ` PUBLIC "${dt.publicId}"` : ''; const sys = dt.systemId ? ` "${dt.systemId}"` : ''; doctypeStr = `\n`; } html = doctypeStr + document.documentElement.outerHTML; } catch { html = ''; } this._html = html; if (loading.parentNode) loading.parentNode.removeChild(loading); const lines = Lines.build(html, CONFIG.MAX_SOURCE_LINES); lines.render(codeEl); const metaEl = p.querySelector('#hrd-source-meta'); if (metaEl) { metaEl.textContent = `· ${lines.total} 行`; } if (lines.truncated) { codeEl.appendChild(UI._mkNote(`仅显示前 ${CONFIG.MAX_SOURCE_LINES} 行`, [])); } }); }, close() { this.cancelLoad(); this._el = null; if (UI._overlays.indexOf('source') >= 0) { UI.popOverlay('source'); } else { const p = UI.root.getElementById(UI._OVERLAY_IDS.source); if (p && p.parentNode) p.parentNode.removeChild(p); } } }; const HlsPlay = { _lib: null, _loading: null, _error: '', clear() { this._lib = null; this._loading = null; this._error = ''; delete window.__HRD_HLS_LOADING__; }, status() { if (this._lib) return 'ready'; if (this._loading) return 'loading'; if (this._error) return 'error'; return 'idle'; }, load() { if (this._lib) return Promise.resolve(this._lib); if (this._loading) return this._loading; if (window.__HRD_HLS_LOADING__ && window.__HRD_HLS_LOADING__.__lib) { this._lib = window.__HRD_HLS_LOADING__.__lib; return Promise.resolve(this._lib); } let shared = window.__HRD_HLS_LOADING__; if (!shared || typeof shared !== 'object') { shared = { __shared: true, cbs: [] }; window.__HRD_HLS_LOADING__ = shared; } if (!Array.isArray(shared.cbs)) shared.cbs = []; const self = this; this._loading = new Promise((resolve, reject) => { const done = (ok) => { if (self._lib || shared.__lib) { self._lib = self._lib || shared.__lib; self._loading = null; resolve(self._lib); return; } self._loading = null; if (!ok) self._error = '播放引擎加载失败,请检查网络或稍后重试'; reject(new Error(self._error || 'load failed')); }; const attach = () => { try { if (typeof Hls === 'undefined' || !Hls) { done(false); return; } shared.__lib = Hls; const cbs = shared.cbs || []; shared.cbs = []; for (let i = 0; i < cbs.length; i++) { try { cbs[i](true, Hls); } catch {} } done(true); } catch { done(false); } }; if (typeof Hls !== 'undefined' && Hls) { attach(); return; } shared.cbs.push((ok, lib) => { if (ok) shared.__lib = lib; }); if (shared.__started) { if (shared.__lib) attach(); else if (shared.__failed) done(false); return; } shared.__started = true; const srcs = [PLAYER_ASSET.HLS].concat(PLAYER_ASSET.HLS_ALT); let idx = 0; let settled = false; const onOk = () => { if (settled) return; settled = true; shared.__done = true; attach(); }; const onFail = () => { if (settled) return; idx++; if (idx < srcs.length) { inject(); return; } settled = true; shared.__failed = true; done(false); }; const inject = () => { let s; try { s = document.createElement('script'); } catch { onFail(); return; } s.src = srcs[idx]; s.async = true; s.onload = onOk; s.onerror = onFail; try { const host = document.head || document.documentElement || document.body; if (!host) { onFail(); return; } host.appendChild(s); } catch { onFail(); } }; inject(); setTimeout(() => { if (!settled && typeof Hls === 'undefined') onFail(); }, 9000); }); return this._loading; }, supportsNativeHls(video) { try { return !!(video && (video.canPlayType('application/vnd.apple.mpegURL') || video.canPlayType('application/x-mpegURL'))); } catch { return false; } }, makeHeadersFn(url, extra) { const base = MediaHeaders.headers(url); const merged = {}; if (extra) for (const k in extra) merged[k] = extra[k]; if (base) for (const k in base) merged[k] = base[k]; let n = 0; for (const k in merged) n++; if (!n) return null; return function () { const out = {}; for (const k in merged) out[k] = merged[k]; return out; }; }, attach(video, url, opts) { opts = opts || {}; const self = this; const extra = opts.headers || null; const onStatus = typeof opts.onStatus === 'function' ? opts.onStatus : function () {}; const onFail = typeof opts.onFail === 'function' ? opts.onFail : function () {}; let destroyed = false; let hls = null; const cleanup = () => { if (hls) { try { hls.destroy(); } catch {} hls = null; } }; const startNative = () => { try { video.src = url; onStatus('native'); } catch {} }; this.load().then((lib) => { if (destroyed) return; if (!lib || typeof lib.isSupported !== 'function' || !lib.isSupported()) { if (self.supportsNativeHls(video)) { startNative(); return; } onFail('当前环境不支持 HLS 播放'); return; } const cfg = { enableWorker: true, lowLatencyMode: false }; const hdrs = self.makeHeadersFn(url, extra); if (hdrs) { cfg.xhrSetup = function (xhr, reqUrl) { let h = null; try { h = hdrs(reqUrl || ''); } catch {} if (!h) h = hdrs(url); if (!h) return; for (const k in h) { try { xhr.setRequestHeader(k, h[k]); } catch {} } }; } try { hls = new lib(cfg); } catch { if (self.supportsNativeHls(video)) { startNative(); return; } onFail('HLS 引擎初始化失败'); return; } hls.on(lib.Events.MANIFEST_PARSED, () => { if (destroyed) return; onStatus('manifest'); }); hls.on(lib.Events.ERROR, (evt, data) => { if (destroyed || !data) return; if (!data.fatal) return; const t = data.type; if (t === lib.ErrorTypes.NETWORK_ERROR) { if (data.details === lib.ErrorDetails.MANIFEST_LOAD_ERROR || data.details === lib.ErrorDetails.MANIFEST_LOAD_TIMEOUT) { cleanup(); onFail('m3u8 加载失败:资源可能已过期(auth_key 失效)或需要页面鉴权'); return; } try { hls.startLoad(); return; } catch {} cleanup(); onFail('分片加载失败:资源可能已过期或需要页面鉴权'); return; } if (t === lib.ErrorTypes.MEDIA_ERROR) { try { hls.recoverMediaError(); return; } catch {} } cleanup(); onFail('播放失败:' + (data.details || '未知错误')); }); try { hls.loadSource(url); hls.attachMedia(video); onStatus(hdrs ? 'hls-auth' : 'hls'); } catch { cleanup(); if (self.supportsNativeHls(video)) { startNative(); return; } onFail('HLS 装载失败'); } }).catch(() => { if (destroyed) return; if (self.supportsNativeHls(video)) { startNative(); return; } onFail(self._error || '播放引擎加载失败'); }); return function destroy() { destroyed = true; cleanup(); }; }, reset(video) { if (!video) return; try { video.pause(); } catch {} try { video.removeAttribute('src'); } catch {} try { video.load(); } catch {} }, patchElement(video, url) { if (!video || video.__hrdPatched || !url) return; video.__hrdPatched = true; try { const srcDesc = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'src'); Object.defineProperty(video, 'src', { configurable: true, get() { return url; }, set(v) { if (!v || v === url) return; try { srcDesc.set.call(video, v); } catch {} } }); Object.defineProperty(video, 'currentSrc', { configurable: true, get() { const c = srcDesc.get.call(video); return c || url; } }); const origLoad = video.load; video.load = function () { try { if (video.src && /^blob:/i.test(video.src)) return; } catch {} return origLoad.apply(video, arguments); }; } catch {} } }; const StreamDetect = { kind(url, r) { if (!url || typeof url !== 'string') return 'plain'; const u = stripHash(url); if (/\.m3u8(?:[?#]|$)/i.test(u)) return 'hls'; const seg = (u.split('?')[0].split('/').pop() || ''); if (/\.ts$/i.test(seg)) { if (/^\d+$/.test(seg.replace(/\.ts$/i, ''))) return 'seg'; if (/(?:index|playlist|master|media|prog|video|out)\d*\.ts$/i.test(seg)) return 'hls'; return 'seg'; } return 'plain'; }, label(kind) { if (kind === 'hls') return 'HLS 流'; if (kind === 'seg') return 'TS 分片'; return ''; }, stripAuthKey(u) { const s = stripHash(u); if (!s) return ''; const q = s.indexOf('?'); if (q < 0) return ''; const params = s.slice(q + 1).split('&'); for (let i = 0; i < params.length; i++) { if (/^auth_key=/i.test(params[i])) return params[i].slice(9); } return ''; }, authExpiry(u) { const v = this.stripAuthKey(u); if (!v) return 0; const ts = parseInt(v.split('-')[0], 10); return isNaN(ts) ? 0 : ts; }, expiryText(u) { const exp = this.authExpiry(u); if (!exp) return ''; const now = Math.floor(Date.now() / 1000); const left = exp - now; if (left <= 0) return '链接已过期'; if (left < 3600) return '剩余 ' + Math.max(1, Math.round(left / 60)) + ' 分钟'; if (left < 86400) return '剩余 ' + Math.round(left / 3600) + ' 小时'; return '剩余 ' + Math.round(left / 86400) + ' 天'; }, guessM3u8(url) { if (!url || typeof url !== 'string') return ''; if (this.kind(url) === 'hls') return url; const seg = this.kind(url) === 'seg'; if (!seg) return ''; try { const u = new URL(url, location.href); const idx = u.pathname.lastIndexOf('/'); if (idx <= 0) return ''; const name = u.pathname.slice(idx + 1); const m = name.match(/^(.*?)(\d+)\.ts$/i); if (!m || !m[1]) return ''; return u.origin + u.pathname.slice(0, idx + 1) + m[1] + '.m3u8'; } catch { return ''; } }, canRecoverM3u8(segUrl, m3u8Url) { if (!segUrl || !m3u8Url) return false; try { const a = new URL(segUrl, location.href); const b = new URL(m3u8Url, location.href); if (a.origin !== b.origin) return false; return true; } catch { return false; } }, probeM3u8(m3u8Url) { return new Promise(resolve => { if (!m3u8Url) { resolve(false); return; } try { const xhr = new XMLHttpRequest(); let done = false; const finish = (ok) => { if (done) return; done = true; resolve(ok); }; const extra = Capture.headersFor(m3u8Url); xhr.open('GET', m3u8Url, true); xhr.timeout = 8000; if (extra) { for (const k in extra) { try { xhr.setRequestHeader(k, extra[k]); } catch {} } } xhr.onload = () => { if (xhr.status < 200 || xhr.status >= 300) { finish(false); return; } let body = ''; try { body = xhr.responseText || ''; } catch {} finish(body.indexOf('#EXTM3U') >= 0); }; xhr.onerror = () => finish(false); xhr.ontimeout = () => finish(false); xhr.onabort = () => finish(false); xhr.send(); } catch { resolve(false); } }); } }; const ImageCipher = { SAMPLE: 96, PROBE_MAX: 1024, KEY_PATTERNS: [ /[\w.-]*crypto[-_]?worker[\w.-]*\.js/i, /[\w.-]*crypto[\w.-]*\.js/i, /[\w.-]*lazyload[\w.-]*\.js/i, /[\w.-]*decrypt[\w.-]*\.js/i, /[\w.-]*zzz[\w.-]*\.js/i ], KEY_SRC: /CryptoJS\s*\.\s*enc\s*\.\s*Utf8\s*\.\s*parse\s*\(\s*(['"])([0-9a-zA-Z+/=_-]{8,64})\1\s*\)/g, // 站点把 key/iv 拆成 ASCII 十进制、用下划线连接后再 fromCharCode 还原 KEY_NUM: /(['"])(\d{2,3}(?:_\d{2,3}){7,})(\1)/g, _probing: false, _cipher: null, _cached: new Map(), _probedScripts: new Set(), _q: [], _qActive: 0, _qMax: 3, _inflight: new Map(), probe(url) { const u = String(url || ''); const hit = this._inflight.get(u); if (hit) return hit; const p = this._probeRun(u).finally(() => { this._inflight.delete(u); }); this._inflight.set(u, p); return p; }, _probeRun(url) { const self = this; const cached = this._cached.get(url); if (cached) return Promise.resolve({ ok: true, url: cached[0], size: cached[1], cached: true }); if (!/^https?:/i.test(url)) return Promise.resolve(null); return new Promise(resolve => { self._q.push({ url: url, resolve: resolve }); self._qPump(); }); }, // 已解密的 HTTP 图片优先走浏览器原生缓存,避免重复下载整文件 probeFast(url) { const cached = this._cached.get(url); if (cached) return Promise.resolve({ ok: true, url: cached[0], size: cached[1], cached: true }); const u = String(url || ''); if (!/^https?:/i.test(u)) return Promise.resolve(null); return this.probe(u); }, _qPump() { const self = this; while (self._qActive < self._qMax && self._q.length) { const job = self._q.shift(); self._qActive++; self._probeCore(job.url).then(job.resolve).catch(() => job.resolve(null)).then(() => { self._qActive--; self._qPump(); }); } }, _probeCore(url) { const self = this; const go = () => self._sample(url).then(sample => { if (!sample) return { ok: false, dead: true, url: url }; const head = self.sniff(sample.bytes); if (head) return { ok: false, plain: true, url: url }; const dec = self._build(sample); if (!dec) return { ok: false, miss: true, url: url }; const blob = new Blob([dec.bytes], { type: dec.mime }); const pair = [URL.createObjectURL(blob), dec.bytes.length]; self._remember(url, pair); return { ok: true, url: pair[0], size: pair[1], key: dec.keyStr, variant: dec.tag }; }); if (this._cipher) return go(); return this._probeOnce().then(() => (self._cipher ? go() : { ok: false, miss: true, url: url })); }, active() { return !!this._cipher; }, describe() { if (!this._cipher) return ''; return this._cipher.key16 + ' / ' + this._cipher.iv16; }, _b64ToBytes(b64) { const bin = atob(String(b64 || '')); const out = new Uint8Array(bin.length); for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i); return out; }, _bytesToB64(bytes) { let bin = ''; const CH = 0x8000; for (let i = 0; i < bytes.length; i += CH) { bin += String.fromCharCode.apply(null, bytes.subarray(i, i + CH)); } return btoa(bin); }, sniff(head) { if (!head || head.length < 3) return ''; if (head[0] === 0xff && head[1] === 0xd8 && head[2] === 0xff) return 'image/jpeg'; if (head.length >= 8 && head[0] === 0x89 && head[1] === 0x50 && head[2] === 0x4e && head[3] === 0x47) return 'image/png'; if (head.length >= 3 && head[0] === 0x47 && head[1] === 0x49 && head[2] === 0x46) return 'image/gif'; if (head.length >= 12 && head[0] === 0x52 && head[1] === 0x49 && head[2] === 0x46 && head[3] === 0x46 && head[8] === 0x57 && head[9] === 0x45 && head[10] === 0x42 && head[11] === 0x50) return 'image/webp'; if (head.length >= 2 && head[0] === 0x42 && head[1] === 0x4d) return 'image/bmp'; if (head.length >= 4 && head[0] === 0x00 && head[1] === 0x00 && head[2] === 0x01 && head[3] === 0x00) return 'image/x-icon'; return ''; }, mimeFromUrl(url) { const clean = String(url || '').split('#')[0].split('?')[0].toLowerCase(); const ext = (clean.split('.').pop() || ''); if (ext === 'jpg' || ext === 'jpeg' || ext === 'jpe') return 'image/jpeg'; if (ext === 'png') return 'image/png'; if (ext === 'gif') return 'image/gif'; if (ext === 'webp') return 'image/webp'; if (ext === 'bmp') return 'image/bmp'; if (ext === 'ico') return 'image/x-icon'; if (ext === 'avif') return 'image/avif'; return 'image/png'; }, install() { const self = this; if (!this._probing) { this._probing = true; try { this._probeScripts(); } catch {} } this._cached.forEach(pair => { try { URL.revokeObjectURL(pair[0]); } catch {} }); this._cached.clear(); return Promise.resolve(); }, _probeScripts(tries) { const self = this; const n = tries || 0; if (this._cipher || n > 30) return; const list = []; let all = []; try { all = Array.prototype.slice.call(document.scripts || []); } catch {} for (let i = 0; i < all.length; i++) { const s = all[i]; const src = s && s.src ? String(s.src) : ''; if (!src || src.slice(0, 11).toLowerCase() === 'blob:') continue; let hit = false; for (let j = 0; j < this.KEY_PATTERNS.length; j++) { if (this.KEY_PATTERNS[j].test(src)) { hit = true; break; } } if (hit) list.push(src); } if (!list.length) { setTimeout(() => self._probeScripts(n + 1), 500); return; } try { const guessed = this._guessWorkerUrls(); for (let g = 0; g < guessed.length; g++) { if (list.indexOf(guessed[g]) < 0) list.push(guessed[g]); } } catch {} let left = list.length; const done = () => { if (--left <= 0 && !self._cipher) setTimeout(() => self._probeScripts(n + 1), 800); }; for (let i = 0; i < list.length; i++) { this._fetchScript(list[i]).then(text => { if (text) self._adopt(text, list[i]); done(); }).catch(done); } }, _guessWorkerUrls() { const out = []; const names = ['crypto-worker.js', 'decrypt-worker.js', 'image-worker.js']; const roots = ['/static/web/js/plugins/', '/static/web/js/', '/js/plugins/', '/js/']; try { const ss = Array.prototype.slice.call(document.scripts || []); for (let i = 0; i < ss.length; i++) { const src = ss[i] && ss[i].src ? String(ss[i].src) : ''; if (!src) continue; const m = src.match(/^(.*\/)(?:lib|plugins|js)\//i); if (m && roots.indexOf(m[1]) < 0) roots.push(m[1]); } } catch {} for (let r = 0; r < roots.length; r++) { for (let i = 0; i < names.length; i++) { let u = ''; try { u = new URL(roots[r] + names[i], location.href).href; } catch {} if (u && out.indexOf(u) < 0) out.push(u); } } return out; }, _fetchScript(url) { if (this._probedScripts.has(url)) return Promise.resolve(''); this._probedScripts.add(url); return new Promise(resolve => { try { const xhr = new XMLHttpRequest(); xhr.open('GET', url, true); xhr.timeout = 8000; xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 300) { let t = ''; try { t = xhr.responseText || ''; } catch {} resolve(t); } else resolve(''); }; xhr.onerror = () => resolve(''); xhr.ontimeout = () => resolve(''); xhr.onabort = () => resolve(''); xhr.send(); } catch { resolve(''); } }); }, _adopt(text, src) { const self = this; if (!text || this._cipher) return Promise.resolve(false); const pairs = []; // A. 明文写法 CryptoJS.enc.Utf8.parse('xxxx') const vals = []; const re = this.KEY_SRC; re.lastIndex = 0; let m; while ((m = re.exec(text)) !== null) vals.push(m[2]); for (let i = 0; i + 1 < vals.length; i++) { const a = vals[i]; const b = vals[i + 1]; if (a.length !== 16 || b.length !== 16) continue; const at = text.indexOf(a); const bt = text.indexOf(b, at + a.length); if (bt < 0) continue; const between = text.slice(at + a.length, bt); if (!/\biv\s*[:=]/i.test(between)) continue; pairs.push({ k: a, i: b }); } // B. 混淆写法 '102_53_100_...' → String.fromCharCode 还原 if (!pairs.length) { const nums = this._decodeNumericKeys(text); if (nums.length >= 2) { for (let i = 0; i + 1 < nums.length; i++) { if (nums[i].length === 16 && nums[i + 1].length === 16) { pairs.push({ k: nums[i], i: nums[i + 1] }); pairs.push({ k: nums[i + 1], i: nums[i] }); } } } } if (!pairs.length) return Promise.resolve(false); return Crypto.ready().then(() => { if (self._cipher) return true; self._cipher = { key16: pairs[0].k, iv16: pairs[0].i, source: String(src || '').split('?')[0], pairs: pairs.slice(0, 4) }; return true; }).catch(() => false); }, _decodeNumericKeys(text) { const out = []; const re = this.KEY_NUM; re.lastIndex = 0; let m; while ((m = re.exec(text)) !== null) { const raw = m[2]; const parts = raw.split('_'); let ok = true; let s = ''; for (let i = 0; i < parts.length; i++) { const n = parseInt(parts[i], 10); if (!isFinite(n) || n < 32 || n > 126) { ok = false; break; } s += String.fromCharCode(n); } if (ok && s.length >= 8 && out.indexOf(s) < 0) out.push(s); } return out; }, isCandidate(r) { if (!r || !r.url) return false; if (r.displayType !== 'image') return false; const u = String(r.url); const low = u.slice(0, 5).toLowerCase(); if (low === 'blob:' || low === 'data:') return false; if (!/^https?:/i.test(u)) return false; return true; }, _normCands(list) { const out = []; for (let i = 0; i < list.length; i++) { const item = list[i]; const v = String(item.v == null ? '' : item.v).trim(); if (!v || v.length < 6 || v.length > 160) continue; let dup = false; for (let j = 0; j < out.length; j++) { if (out[j].v === v && out[j].score === item.score) { dup = true; break; } } if (dup) continue; out.push({ v: v, score: item.score, kind: item.kind }); } out.sort((a, b) => b.score - a.score); return out.slice(0, 14); }, _hex16(seed) { let s = ''; let x = seed.charCodeAt(0); while (s.length < 16) { x = (x * 1103515245 + 12345) & 0x7fffffff; s += x.toString(16); } return s.slice(0, 16); }, _hash16(v) { let h = 0x811c9dc5; for (let i = 0; i < v.length; i++) { h ^= v.charCodeAt(i); h = (h * 0x01000193) >>> 0; } let out = ''; let x = h >>> 0; while (out.length < 16) { x = (x * 1664525 + 1013904223) >>> 0; out += x.toString(16).padStart(8, '0'); } return out.slice(0, 16); }, _remember(url, pair) { if (!url || !pair) return; if (this._cached.size > 120) { let drop = 60; for (const entry of this._cached) { try { URL.revokeObjectURL(entry[1][0]); } catch {} this._cached.delete(entry[0]); if (--drop <= 0) break; } } this._cached.set(url, pair); }, cached(url) { const hit = this._cached.get(url); return hit ? hit[0] : ''; }, _probeOnce() { const self = this; return new Promise(resolve => { if (self._cipher) { resolve(true); return; } let settled = false; const finish = () => { if (!settled) { settled = true; resolve(!!self._cipher); } }; const list = self._guessWorkerUrls().filter(u => !self._probedScripts.has(u)); if (!list.length) { finish(); return; } let left = list.length; const done = () => { if (--left <= 0) finish(); }; for (let i = 0; i < list.length; i++) { self._fetchScript(list[i]).then(text => { if (text) self._adopt(text, list[i]).then(() => done()); else done(); }).catch(() => done()); } setTimeout(finish, 4000); }); }, _sample(url) { return this._fetchBytes(url, false).then(full => { if (full && full.bytes && full.bytes.length >= 48) return full; return this._fetchBytes(url, true); }); }, _fetchBytes(url, ranged) { return new Promise(resolve => { try { const xhr = new XMLHttpRequest(); xhr.open('GET', url, true); xhr.timeout = ranged ? 9000 : 20000; try { xhr.responseType = 'arraybuffer'; } catch {} if (ranged) { try { xhr.setRequestHeader('Range', 'bytes=0-' + (this.PROBE_MAX - 1)); } catch {} } const hdrs = this.readHeaders(url, null); if (hdrs) for (const k in hdrs) { try { xhr.setRequestHeader(k, hdrs[k]); } catch {} } xhr.onload = () => { try { if (xhr.status < 200 || xhr.status >= 300) { resolve(null); return; } let buf = null; try { buf = xhr.response; } catch {} if (!buf || !buf.byteLength) { resolve(null); return; } const bytes = new Uint8Array(buf); if (bytes.length < 48) { resolve(null); return; } let hs = []; try { const lines = String(xhr.getAllResponseHeaders() || '').split(/\r?\n/); for (let i = 0; i < lines.length; i++) { const idx = lines[i].indexOf(':'); if (idx < 0) continue; hs.push([lines[i].slice(0, idx).trim(), lines[i].slice(idx + 1).trim()]); } } catch {} resolve({ raw: bytes, bytes: bytes, b64: '', lead: 0, sr: { sample: '', headers: hs, resolvedAt: url } }); } catch { resolve(null); } }; xhr.onerror = () => resolve(null); xhr.ontimeout = () => resolve(null); xhr.onabort = () => resolve(null); xhr.send(); } catch { resolve(null); } }); }, readHeaders(url, seed) { const out = {}; let n = 0; const base = seed || {}; for (const k in base) { if (base[k]) { out[k] = base[k]; n++; } } try { const extra = Capture.headersFor(url); if (extra) for (const k in extra) { if (extra[k] && !out[k]) { out[k] = extra[k]; n++; } } } catch {} return n ? out : null; }, _cands(sr, b64) { const raw = []; const push = (v, score, kind) => { if (v == null) return; raw.push({ v: String(v), score: score, kind: kind }); }; if (sr.headers && sr.headers.length) { for (let i = 0; i < sr.headers.length; i++) { const h = sr.headers[i]; const name = String(h[0] || '').toLowerCase(); const val = h[1]; if (!val) continue; if (name === 'x-key' || name === 'x-img-key' || name === 'x-image-key' || name === 'x-decrypt-key' || name === 'x-aes-key') push(val, 96, 'hdr-key'); else if (name === 'x-iv' || name === 'x-aes-iv') push(val, 92, 'hdr-iv'); else if (name === 'key' || name === 'auth-key' || name === 'x-auth-key') push(val, 68, 'hdr'); else if (name === 'x-token' || name === 'x-sign' || name === 'x-auth') push(val, 64, 'hdr'); } } let src = ''; try { src = String((sr && sr.resolvedAt) || ''); } catch {} let u = null; try { u = new URL(src, location.href); } catch {} if (u) { const names = []; u.searchParams.forEach((v, k) => names.push(k)); for (let i = 0; i < names.length; i++) { const rawName = names[i]; const nm = rawName.toLowerCase(); const val = u.searchParams.get(rawName); if (nm === 'key' || nm === 'k' || nm === 'imgkey' || nm === 'img_key' || nm === 'aeskey' || nm === 'aes_key' || nm === 'encrypt_key') push(val, 70, 'qs-key'); else if (nm === 'iv') push(val, 66, 'qs-iv'); else if (nm === 'auth_key' || nm === 'authkey' || nm === 'auth-key') push(val, 62, 'qs-auth'); else if (nm === 'token' || nm === 'sign' || nm === 'signature' || nm === 'sk') push(val, 56, 'qs'); } } if (this._cipher) push(this._cipher.key16, 90, 'static'); if (this._cipher) push(this._cipher.iv16, 90, 'static'); return this._normCands(raw); }, _build(sample) { const sr = sample.sr; const bytes = sample.bytes; if (!bytes || !bytes.length) return null; const cands = this._cands(sr, ''); const n = bytes.length; const padTail = (16 - (n % 16)) % 16; const leadMax = Math.min(64, n - 48); const leads = [0]; for (let l = 8; l <= leadMax; l += 8) leads.push(l); for (let c = 0; c < cands.length; c++) { const cand = cands[c]; const trials = this._keyIvPairs(cand.v); for (let t = 0; t < trials.length; t++) { const key = trials[t].key; const iv = trials[t].iv; for (let li = 0; li < leads.length; li++) { const lead = leads[li]; const avail = n - lead; if (avail < 48) continue; const tail = (16 - (avail % 16)) % 16; if (tail === 0) { const ct = this._wa(bytes.subarray(lead)); if (!ct) continue; const hit = this._decrypt(ct, key, iv, 'p0', trials[t].tag); if (!hit) continue; const mime = this.sniff(hit); if (mime) { const tag = (lead ? 'lead' + lead + '|' : 'exact|') + cand.kind; return { bytes: hit, mime: mime, keyStr: trials[t].tag, tag: tag }; } } else if (lead === 0) { const padded = new Uint8Array(avail + tail); padded.set(bytes.subarray(0, avail), 0); const hit = this._decryptPad(padded, key, iv); if (hit && this.sniff(hit)) { return { bytes: hit, mime: this.sniff(hit), keyStr: trials[t].tag, tag: 'pad' + tail + '|' + cand.kind }; } } } } } return null; }, _keyIvPairs(v) { const out = []; const seen = new Set(); const add = (key, iv, tag) => { if (!key || !iv) return; const variants = [ [Crypto.parsed(key), Crypto.parsed(iv), 'u'], [Crypto.decoded(key), Crypto.decoded(iv), 'h'] ]; for (let m = 0; m < variants.length; m++) { const k = variants[m][0]; const i = variants[m][1]; if (!k || !i || !k.sigBytes || !i.sigBytes) continue; const sig = k.toString() + '|' + i.toString(); if (seen.has(sig)) continue; seen.add(sig); out.push({ key: k, iv: i, tag: tag + ':' + variants[m][2] }); } }; add(this._hex16('11'), this._hex16('11'), 'fixed:11'); add(this._hex16('22'), this._hex16('22'), 'fixed:22'); if (this._cipher && this._cipher.key16 && this._cipher.iv16) { add(this._cipher.key16, this._cipher.iv16, 'cipher'); add(this._cipher.iv16, this._cipher.key16, 'cipher-swap'); } for (let p = 0; p + 32 <= v.length; p++) { add(v.slice(p, p + 16), v.slice(p + 16, p + 32), 'split@' + p); } for (let p = 0; p + 31 <= v.length; p++) { add(v.slice(p + 15, p + 31), v.slice(p, p + 15), 'swap@' + p); add(v.slice(p, p + 16), v.slice(p + 1, p + 16), 'rot@' + p); } if (v.length >= 16) { const k = v.slice(0, 16); for (let h = 0; h < 16; h++) add(k, k.slice(h) + k.slice(0, h), 'rotk@' + h); } if (v.length >= 8) { const k8 = v.slice(0, 8); const k = k8 + k8; add(k, k, 'dup8'); add(k, k8 + this._rev(k8), 'dup8r'); add(k8 + this._rev(k8), k, 'dup8r2'); add(this._rev(k8) + k8, k, 'dup8r3'); } if (v.length >= 16) { add(v.slice(0, 16), v.slice(0, 16), 'self'); add(this._hash16(v), this._hash16(v), 'h16'); add(this._hash16(v), v.slice(0, 16), 'h16a'); } return out; }, _rev(s) { let o = ''; for (let i = s.length - 1; i >= 0; i--) o += s[i]; return o; }, _hex16(seed) { let s = ''; let x = seed.charCodeAt(0); while (s.length < 16) { x = (x * 1103515245 + 12345) & 0x7fffffff; s += x.toString(16); } return s.slice(0, 16); }, _hash16(v) { let h = 0x811c9dc5; for (let i = 0; i < v.length; i++) { h ^= v.charCodeAt(i); h = (h * 0x01000193) >>> 0; } let out = ''; let x = h >>> 0; while (out.length < 16) { x = (x * 1664525 + 1013904223) >>> 0; out += x.toString(16).padStart(8, '0'); } return out.slice(0, 16); }, _wa(bytes) { const L = Crypto.lib; if (!L || !bytes || !bytes.length) return null; try { const words = []; for (let i = 0; i < bytes.length; i++) { words[i >>> 2] = (words[i >>> 2] || 0) | (bytes[i] << (24 - (i % 4) * 8)); } const wa = L.lib.WordArray.create(); wa.words = words; wa.sigBytes = bytes.length; return wa; } catch { return null; } }, _sliceWa(wa, start, len) { const L = Crypto.lib; if (!L || !wa) return null; const avail = wa.words.length - (start >>> 2); const count = Math.min(Math.ceil(len / 4), avail); const words = []; for (let i = 0; i < count; i++) words[i] = wa.words[(start >>> 2) + i]; const out = L.lib.WordArray.create(); out.words = words; out.sigBytes = len; return out; }, _waToBytes(wa) { const L = Crypto.lib; if (!L || !wa) return null; try { const plain = L.enc.Latin1.stringify(wa); const bytes = new Uint8Array(plain.length); for (let i = 0; i < plain.length; i++) bytes[i] = plain.charCodeAt(i) & 0xff; return bytes; } catch { return null; } }, _aesDecrypt(ct, key, iv, noPad) { const L = Crypto.lib; if (!L || !ct) return null; try { const out = L.AES.decrypt({ ciphertext: ct }, key, { iv: iv, mode: L.mode.CBC, padding: noPad ? L.pad.NoPadding : L.pad.Pkcs7 }); if (!out || !out.sigBytes || out.sigBytes <= 0) return null; return this._waToBytes(out); } catch { return null; } }, _toStr(bytes, n) { let s = ''; const lim = n && n < bytes.length ? n : bytes.length; for (let i = 0; i < lim; i++) s += String.fromCharCode(bytes[i]); return s; }, _decrypt(ct, key, iv, tag, keyTag) { if (!ct) return null; const n = ct.sigBytes; const base = tag.indexOf('skip') === 0 ? (parseInt(tag.slice(4), 10) || 0) : -1; const offs = base >= 0 ? [base, base + 16, base + 32] : [0]; for (let a = 0; a < offs.length; a++) { const s = offs[a]; if (s < 0 || s >= n || s % 16 !== 0) continue; const len = n - s; if (len < 32 || len % 16 !== 0) continue; const sub = this._sliceWa(ct, s, len); if (!sub) continue; const hit = this._aesDecrypt(sub, key, iv, false); if (hit && hit.length >= 8) return hit; } return null; }, _decryptPad(data, key, iv) { if (!data || !data.length || data.length % 16 !== 0) return null; const wa = this._wa(data); if (!wa) return null; return this._aesDecrypt(wa, key, iv, true); }, }; const Capture = { _reported: new Set(), _reqHeaders: new Map(), _tinyFails: new Map(), _observed: null, _scanning: false, _scanPending: false, init() { this.interceptFetch(); this.interceptXHR(); this.observeDom(); this.scanAll(); ImageCipher.install(); }, report(url, ct, it) { if (typeof url !== 'string') return; const trimmed = url.trim(); if (!trimmed || trimmed.length > 65536) return; if (!Security.isSafe(trimmed)) return; const k = stripHash(trimmed); if (this._reported.has(k)) return; if (this._reported.size > 8000) this._reported.clear(); this._reported.add(k); const added = Store.add(trimmed, ct, it); if (added) this.scheduleScan(); }, _noteHeader(url, xhr) { if (!url || typeof url !== 'string') return null; const key = url.length > 2000 ? stripHash(url.slice(0, 2000)) : stripHash(url); let bucket = this._reqHeaders.get(key); if (!bucket) { if (this._reqHeaders.size > 900) { let drop = 400; for (const kk of this._reqHeaders.keys()) { this._reqHeaders.delete(kk); if (--drop <= 0) break; } } bucket = {}; this._reqHeaders.set(key, bucket); } return bucket; }, headersFor(url) { if (!url || typeof url !== 'string') return null; const bucket = this._reqHeaders.get(stripHash(url)); if (!bucket) return null; const out = {}; let n = 0; for (const k in bucket) { if (bucket[k]) { out[k] = bucket[k]; n++; } } return n ? out : null; }, interceptFetch() { const self = this; const orig = window.fetch; if (!orig) return; const patched = function (input, init) { let url = ''; try { url = typeof input === 'string' ? input : ((input && (input.url || input.href)) || ''); } catch {} const p = orig.apply(this, arguments); try { if (url) { p.then(resp => { try { const finalUrl = resp.url || url; const ct = resp.headers.get('content-type') || ''; self.report(finalUrl, ct, 'fetch'); } catch {} }).catch(() => {}); } } catch {} return p; }; try { Object.defineProperty(patched, 'name', { value: orig.name || 'fetch' }); Object.defineProperty(patched, 'length', { value: orig.length }); } catch {} try { window.fetch = patched; } catch {} }, interceptXHR() { const self = this; const proto = XMLHttpRequest.prototype; const oOpen = proto.open; const oSend = proto.send; const oSet = proto.setRequestHeader; proto.open = function (m, url) { try { this._hrdUrl = url; } catch {} return oOpen.apply(this, arguments); }; proto.setRequestHeader = function (name, value) { try { const u = this._hrdUrl; if (u && typeof u === 'string' && typeof name === 'string') { const low = name.toLowerCase(); let keep = false; if (low === 'x-cdn-auth' || low === 'x-cdn-app' || low === 'authorization' || low === 'x-token' || low === 'referer' || low === 'origin') keep = true; if (!keep && (low.indexOf('key') >= 0 || low.indexOf('iv') >= 0 || low.indexOf('sign') >= 0 || low.indexOf('auth') >= 0)) keep = true; if (keep) { const bucket = self._noteHeader(u, this); if (bucket) bucket[low] = String(value); } } } catch {} return oSet.apply(this, arguments); }; proto.send = function () { const self2 = self; try { const url = this._hrdUrl; if (url && typeof url === 'string') { try { this.addEventListener('load', function () { try { const finalUrl = this.responseURL || url; let ct = ''; try { ct = this.getResponseHeader('content-type') || ''; } catch {} self2.report(finalUrl, ct, 'xhr'); } catch {} }); } catch {} } } catch {} return oSend.apply(this, arguments); }; }, observeDom() { const self = this; if (this._observed) return; try { this._observed = new MutationObserver(() => self.scheduleScan()); this._observed.observe(document.documentElement || document, { childList: true, subtree: true, attributes: true, attributeFilter: ['src', 'srcset', 'data-src', 'data-original', 'poster'] }); } catch {} }, _scanTimer: null, scheduleScan() { const self = this; if (this._scanTimer) return; this._scanTimer = setTimeout(() => { self._scanTimer = null; self.scanAll(); }, 320); }, scanAll() { if (this._scanning) { this._scanPending = true; return; } this._scanning = true; try { this._scanTags(); this._scanCss(); this._scanPerf(); } catch {} finally { this._scanning = false; } if (this._scanPending) { this._scanPending = false; this.scheduleScan(); } }, _scanTags() { const SEL = 'img,source,video,audio,a,link'; let nodes = null; try { if (document.querySelectorAll) nodes = document.querySelectorAll(SEL); } catch { nodes = null; } if (nodes) { const len = Math.min(nodes.length, 6000); for (let i = 0; i < len; i++) this._scanOne(nodes[i]); return; } let fallback = []; try { fallback = document.getElementsByTagName('*'); } catch { return; } const flen = Math.min(fallback.length, 6000); for (let i = 0; i < flen; i++) this._scanOne(fallback[i]); }, _scanOne(el) { if (!el || !el.tagName) return; const tag = el.tagName; if (tag === 'IMG') { this._fromAttr(el, 'currentSrc'); this._fromAttr(el, 'src'); this._fromAttr(el, 'srcset'); this._fromAttr(el, 'data-src'); this._fromAttr(el, 'data-original'); this._fromAttr(el, 'data-lazy-src'); } else if (tag === 'SOURCE') { this._fromAttr(el, 'srcset'); this._fromAttr(el, 'src'); } else if (tag === 'VIDEO' || tag === 'AUDIO') { this._fromAttr(el, 'currentSrc'); this._fromAttr(el, 'src'); this._fromAttr(el, 'poster'); } else if (tag === 'A' || tag === 'LINK') { this._fromAttr(el, 'href'); } }, _fromAttr(el, attr) { let v = ''; try { if (attr === 'currentSrc') v = el.currentSrc || ''; else if (attr === 'src' && (el.tagName === 'IMG' || el.tagName === 'VIDEO' || el.tagName === 'AUDIO')) v = el.getAttribute('src') || ''; else v = el.getAttribute(attr) || ''; } catch { return; } if (!v || typeof v !== 'string') return; if (attr === 'srcset') { const parts = v.split(','); for (let i = 0; i < parts.length; i++) { const u = parts[i].trim().split(/\s+/)[0]; if (u) this._maybeAdd(u, '', 'srcset'); } return; } this._maybeAdd(v, '', attr); }, _maybeAdd(u, ct, it) { if (!u || typeof u !== 'string') return; const s = u.trim(); if (!s) return; if (s.length > 65536) return; const low = s.slice(0, 5).toLowerCase(); if (low === 'javascript:' || low === 'mailto:' || low === 'tel:') return; if (low === 'data:') { if (!Security.isSafe(s)) return; if (s.length > CONFIG.DATA_URI_MAX_LENGTH) return; } else if (low !== 'blob:') { const p = s.slice(0, 6).toLowerCase(); if (p !== 'http:/' && p !== 'https:' && s.indexOf('://') < 0 && s.indexOf('//') !== 0 && s.indexOf('/') !== 0 && s.indexOf('.') < 0) { return; } } this.report(s, ct, it); }, _scanCss() { let sheets = []; try { sheets = Array.prototype.slice.call(document.styleSheets || []); } catch { return; } for (let i = 0; i < sheets.length && i < 80; i++) { let rules = null; try { rules = sheets[i].cssRules; } catch { continue; } if (!rules) continue; const n = Math.min(rules.length, 4000); for (let j = 0; j < n; j++) { const r = rules[j]; if (!r) continue; if (r.style && r.style.backgroundImage) { const m = r.style.backgroundImage.match(/url\((['"]?)([^'")]+)\1\)/i); if (m && m[2]) this._maybeAdd(m[2], '', 'css'); } if (r.cssRules) { const inner = Math.min(r.cssRules.length, 800); for (let k = 0; k < inner; k++) { const ir = r.cssRules[k]; if (ir && ir.style && ir.style.backgroundImage) { const m2 = ir.style.backgroundImage.match(/url\((['"]?)([^'")]+)\1\)/i); if (m2 && m2[2]) this._maybeAdd(m2[2], '', 'css'); } } } } } }, _scanPerf() { let entries = []; try { if (!window.performance || !performance.getEntriesByType) return; entries = performance.getEntriesByType('resource') || []; } catch { return; } const start = entries.length > 900 ? entries.length - 900 : 0; for (let i = start; i < entries.length; i++) { const e = entries[i]; if (!e || !e.name) continue; const it = String(e.initiatorType || ''); if (it !== 'img' && it !== 'image' && it !== 'css' && it !== 'video' && it !== 'audio' && it !== 'link' && it !== 'fetch' && it !== 'xmlhttprequest') continue; this._maybeAdd(e.name, '', it); } } }; function boot() { try { UI.init(); } catch {} try { Capture.init(); } catch {} } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', boot, { once: true }); } else { boot(); } try { window.addEventListener('load', () => { try { Capture.scanAll(); } catch {} try { Capture.scheduleScan(); } catch {} }, { once: true }); } catch {} try { Object.defineProperty(window, '__HRD_INTERNALS__', { value: { CONFIG, Icons, DataURI, Security, Lines, Store, Theme, SafeArea, UI, SourceView, Capture, formatSize, displayUrl, TopGuard, LAYER_MAX, MediaHeaders, HlsPlay, StreamDetect, ImageCipher, Crypto }, writable: false, enumerable: false, configurable: true }); } catch {} })();