// ==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 = `
${Icons.appIcon}
资源日记
0
`;
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 {}
})();