Greasy Fork 支持简体中文。

All-in-One Web Scanner [BETA]

Combines scanners, persists state, stays on top, reliably appears in SPAs, with enhanced stream detection.

  1. // ==UserScript==
  2. // @name All-in-One Web Scanner [BETA]
  3. // @namespace aio-ws-drk-beta
  4. // @icon https://darkie-matrix.vercel.app/scanner.png
  5. // @supportURL https://darkie.vercel.app/
  6. // @version 1.7.1
  7. // @description Combines scanners, persists state, stays on top, reliably appears in SPAs, with enhanced stream detection.
  8. // @author DARKIE
  9. // @match *://*/*
  10. // @exclude *://*.youtube.com/*
  11. // @grant none
  12. // @license MIT
  13. // @run-at document-start
  14. // ==/UserScript==
  15.  
  16. (function() {
  17. 'use strict';
  18.  
  19. if (window.aioScannerInstanceShort) {
  20. console.log("AIO Scanner (Short): Initialization skipped (instance already exists).");
  21. return;
  22. }
  23.  
  24. class AioScanner {
  25. // --- Constants ---
  26. _SK = 'aioScannerPopupState_v1';
  27. _Z_POP = 2147483646;
  28. _Z_TGL = 2147483645;
  29. _P_ID = 'aio-scanner-popup';
  30. _T_ID = 'aio-toggle-button';
  31. _PR_ID = 'aio-image-preview-popup';
  32.  
  33. _STRM_MIME = {
  34. 'application/vnd.apple.mpegurl': 'm3u8',
  35. 'application/x-mpegurl': 'm3u8',
  36. 'audio/mpegurl': 'm3u8',
  37. 'video/mp4': 'mp4',
  38. 'audio/mp4': 'mp4',
  39. 'text/vtt': 'vtt',
  40. 'application/x-subrip': 'srt',
  41. 'text/plain': 'srt',
  42. 'application/dash+xml': 'mpd',
  43. };
  44. _RGX_STRM = /["'](https?:\/\/[^"'\s]+?\.(m3u8|mp4|vtt|srt|mpd)(?:[?#][^"'\s]*)?)["']|["'](https?:\/\/[^"'\s]+?(?:hls|dash|manifest|playlist|\/video\/|\/audio\/|\/segment)[^"'\s]*?)["']/gi;
  45.  
  46. constructor() {
  47. console.log("AIO Scanner (Short): Constructor called.");
  48.  
  49. // --- Instance Variables ---
  50. this._urls = new Set();
  51. this._reqs = [];
  52. this._maxR = 100;
  53. this._lastImgScn = 0;
  54. this._imgScnDelay = 500;
  55. this._maxUrlLen = 70;
  56. this._prvPad = 15;
  57. this._prvMaxW = 300;
  58. this._initd = false;
  59. this._obs = null;
  60. this._lastScanTime = 0;
  61.  
  62. // --- Configuration ---
  63. this._cfg = {
  64. ui: {
  65. popup: {
  66. width: '650px',
  67. maxHeight: '85vh'
  68. }
  69. },
  70. selectors: {
  71. vtt: '#vtt-content',
  72. srt: '#srt-content',
  73. m3u8: '#m3u8-content',
  74. mp4: '#mp4-content',
  75. mpd: "#mpd-content",
  76. iframe: '#iframe-content',
  77. network: '#network-content',
  78. image: '#image-content'
  79. },
  80. linkTypes: ['vtt', 'srt', 'm3u8', 'mp4', 'mpd']
  81. };
  82.  
  83. window.allInOneScanner = this;
  84.  
  85. // --- Debounced Functions ---
  86. this._debScanAll = this._dbnc(() => {
  87. if (this._pop && this._pop.classList.contains('aio-visible')) {
  88. this._scanAll();
  89. }
  90. }, this._imgScnDelay);
  91.  
  92. this._debEnsureUI = this._dbnc(this._ensureUI, 300);
  93.  
  94. // --- Core Initialization Steps ---
  95. this._mkCSS();
  96. this._setupNetMon();
  97. this._monSetAttr();
  98. this._listenSPA();
  99. this._monBlob();
  100.  
  101. // --- Deferred UI/Observer Setup ---
  102. this._waitBodyInit();
  103. }
  104.  
  105. /* ==========================================================================
  106. Initialization Helpers
  107. ========================================================================== */
  108.  
  109. _waitBodyInit() {
  110. if (document.body) {
  111. this._initUIObs();
  112. } else {
  113. const obs = new MutationObserver((muts, observer) => {
  114. if (document.body) {
  115. observer.disconnect();
  116. this._initUIObs();
  117. }
  118. });
  119. obs.observe(document.documentElement || document, {
  120. childList: true
  121. });
  122. }
  123. }
  124.  
  125. _initUIObs() {
  126. if (this._initd) return;
  127. this._mkUI();
  128. this._obsDOM();
  129. this._chkStUI();
  130. if (this._pop ?.classList.contains('aio-visible')) {
  131. this._scanAll();
  132. }
  133. this._initd = true;
  134. console.log("AIO Scanner (Short): UI Initialized and DOM Observer attached.");
  135. }
  136.  
  137. /* ==========================================================================
  138. UI Creation and Management
  139. ========================================================================== */
  140.  
  141. _mkUI() {
  142. this._mkTglBtn();
  143. this._mkPop();
  144. this._mkPrvPop();
  145. const cached = this._cacheDOM();
  146. if (cached) {
  147. this._addEvts();
  148. this._setupKB();
  149. this._setupDelEvts();
  150. } else {
  151. console.error("AIO Scanner (Short): Failed UI init - core elements missing.");
  152. }
  153. }
  154.  
  155. _mkCSS() {
  156. const ZP = this._Z_POP;
  157. const ZT = this._Z_TGL;
  158. const ZPR = ZP + 1;
  159. const css = `
  160. .aio-popup {
  161. position: fixed; top: 20px; right: 20px; width: ${this._cfg.ui.popup.width}; max-height: ${this._cfg.ui.popup.maxHeight};
  162. background-color: rgba(35, 35, 42, 0.97); color: #e0e0e0; padding: 18px; border-radius: 14px;
  163. border: 1px solid rgba(255, 255, 255, 0.1); box-shadow: 0 6px 25px rgba(0, 0, 0, 0.4);
  164. font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
  165. font-size: 14px; z-index: ${ZP}; display: none; overflow-y: auto; backdrop-filter: blur(8px);
  166. transition: opacity 0.2s ease-in-out, transform 0.2s ease-in-out; transform: translateY(-10px); opacity: 0;
  167. }
  168. .aio-popup.aio-visible {
  169. transform: translateY(0); opacity: 1; display: block;
  170. }
  171. #aio-toggle-button {
  172. position: fixed; top: 20px; right: 20px; z-index: ${ZT}; padding: 8px 15px;
  173. background-color: rgba(35, 35, 42, 0.95); box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
  174. border: 1px solid rgba(255, 255, 255, 0.1); display: block; background-color: rgba(255, 255, 255, 0.1);
  175. border: 1px solid rgba(255, 255, 255, 0.15); color: #e0e0e0; border-radius: 8px; cursor: pointer;
  176. font-size: 12px; transition: background-color 0.2s ease, border-color 0.2s ease, transform 0.1s ease; white-space: nowrap;
  177. }
  178. #aio-toggle-button:hover {
  179. background-color: rgba(255, 255, 255, 0.18); border-color: rgba(255, 255, 255, 0.25);
  180. }
  181. #aio-toggle-button:active {
  182. transform: scale(0.97);
  183. }
  184. .aio-image-preview {
  185. position: fixed; padding: 10px; background-color: rgba(35, 35, 42, 0.98); border: 1px solid rgba(255, 255, 255, 0.15);
  186. border-radius: 8px; box-shadow: 0 5px 20px rgba(0, 0, 0, 0.5); z-index: ${ZPR}; pointer-events: none;
  187. opacity: 0; transition: opacity 0.15s ease-in-out; max-width: ${this._prvMaxW}px; max-height: 300px; display: none;
  188. }
  189. .aio-image-preview img {
  190. display: block; max-width: 100%; max-height: 280px; border-radius: 4px;
  191. }
  192. .aio-header {
  193. display: flex; justify-content: space-between; align-items: center; margin-bottom: 18px; padding-bottom: 10px;
  194. border-bottom: 1px solid rgba(255, 255, 255, 0.15); margin-left: -18px; margin-right: -18px; margin-top: -18px;
  195. padding-left: 18px; padding-right: 18px; padding-top: 18px;
  196. }
  197. .aio-title {
  198. font-size: 17px; font-weight: 600; margin: 0; color: #fff;
  199. }
  200. .aio-controls {
  201. display: flex; gap: 8px;
  202. }
  203. .aio-footer {
  204. margin-top: 20px; padding-top: 10px; border-top: 1px solid rgba(255, 255, 255, 0.1);
  205. text-align: center; font-size: 11px; color: rgba(255, 255, 255, 0.5);
  206. }
  207. .aio-footer .aio-heart {
  208. color: #e44d4d; display: inline-block; animation: aio-heartbeat 1.5s infinite ease-in-out;
  209. }
  210. @keyframes aio-heartbeat {
  211. 0%, 100% { transform: scale(1); }
  212. 50% { transform: scale(1.2); }
  213. }
  214. .aio-button {
  215. background-color: rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.15); color: #e0e0e0;
  216. padding: 6px 12px; border-radius: 8px; cursor: pointer; font-size: 12px;
  217. transition: background-color 0.2s ease, border-color 0.2s ease, transform 0.1s ease; white-space: nowrap;
  218. }
  219. .aio-button:hover {
  220. background-color: rgba(255, 255, 255, 0.18); border-color: rgba(255, 255, 255, 0.25);
  221. }
  222. .aio-button:active {
  223. transform: scale(0.97);
  224. }
  225. .aio-section {
  226. margin-bottom: 20px;
  227. }
  228. .aio-section-title {
  229. font-size: 15px; font-weight: 600; color: #fff; margin: 0 0 10px 0; padding-bottom: 6px;
  230. border-bottom: 1px solid rgba(255, 255, 255, 0.1);
  231. }
  232. .aio-content {
  233. max-height: 300px; overflow-y: auto; padding-right: 5px;
  234. }
  235. .aio-list {
  236. list-style: none; padding: 0; margin: 0;
  237. }
  238. .aio-item {
  239. padding: 10px 12px; margin: 6px 0; background-color: rgba(255, 255, 255, 0.04);
  240. border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 8px; font-size: 12px;
  241. transition: background-color 0.2s ease, border-color 0.2s ease; position: relative;
  242. }
  243. .aio-item:hover {
  244. background-color: rgba(255, 255, 255, 0.08); border-color: rgba(255, 255, 255, 0.15);
  245. }
  246. .aio-item-source, .aio-network-url {
  247. color: #fff; word-break: break-all; margin-bottom: 5px; display: block;
  248. }
  249. .aio-item-info, .aio-network-details {
  250. color: rgba(255, 255, 255, 0.65); font-size: 11px; display: flex; gap: 10px;
  251. align-items: center; flex-wrap: wrap; margin-top: 6px;
  252. }
  253. .aio-item-tag {
  254. background-color: rgba(255, 255, 255, 0.1); padding: 3px 7px; border-radius: 6px;
  255. font-size: 10px; white-space: nowrap;
  256. }
  257. .aio-empty {
  258. color: rgba(255, 255, 255, 0.6); font-style: italic; font-size: 13px;
  259. text-align: center; padding: 20px 0;
  260. }
  261. .aio-filter {
  262. width: 100%; box-sizing: border-box; padding: 9px 12px; margin-bottom: 12px;
  263. background: rgba(255, 255, 255, 0.08); border: 1px solid rgba(255, 255, 255, 0.1);
  264. border-radius: 8px; color: #fff; font-size: 13px;
  265. transition: background-color 0.2s ease, border-color 0.2s ease;
  266. }
  267. .aio-filter::placeholder {
  268. color: rgba(255, 255, 255, 0.5);
  269. }
  270. .aio-filter:focus {
  271. outline: none; background: rgba(255, 255, 255, 0.12); border-color: rgba(100, 150, 255, 0.5);
  272. }
  273. .aio-link {
  274.  
  275. }
  276. .aio-link .timestamp {
  277. font-size: 10px; color: rgba(255, 255, 255, 0.5); margin-top: 4px; display: block;
  278. }
  279. .aio-iframe-item {
  280. cursor: pointer;
  281. }
  282. .aio-network-method {
  283. display: inline-block; padding: 3px 8px; border-radius: 6px; margin-right: 8px;
  284. font-weight: bold; font-size: 10px; color: #fff;
  285. }
  286. .aio-network-method-get {
  287. background-color: rgba(64, 156, 255, 0.3); border: 1px solid rgba(64, 156, 255, 0.5);
  288. }
  289. .aio-network-method-post {
  290. background-color: rgba(50, 205, 50, 0.3); border: 1px solid rgba(50, 205, 50, 0.5);
  291. }
  292. .aio-network-method-put {
  293. background-color: rgba(255, 165, 0, 0.3); border: 1px solid rgba(255, 165, 0, 0.5);
  294. }
  295. .aio-network-method-delete {
  296. background-color: rgba(255, 69, 0, 0.3); border: 1px solid rgba(255, 69, 0, 0.5);
  297. }
  298. .aio-network-method-error {
  299. background-color: rgba(200, 0, 0, 0.3); border: 1px solid rgba(200, 0, 0, 0.5);
  300. }
  301. .aio-network-controls {
  302. margin-left: auto; display: flex; gap: 8px;
  303. }
  304. .aio-network-copy, .aio-network-show-payload {
  305. background-color: rgba(147, 112, 219, 0.2); border: 1px solid rgba(147, 112, 219, 0.4);
  306. color: #e0e0e0; padding: 3px 8px; border-radius: 6px; cursor: pointer; font-size: 10px;
  307. transition: background-color 0.2s ease, border-color 0.2s ease; white-space: nowrap;
  308. }
  309. .aio-network-copy:hover, .aio-network-show-payload:hover {
  310. background-color: rgba(147, 112, 219, 0.4); border-color: rgba(147, 112, 219, 0.6);
  311. }
  312. .aio-network-copy.copied {
  313. background-color: rgba(50, 205, 50, 0.4); border-color: rgba(50, 205, 50, 0.6); color: #fff;
  314. }
  315. .aio-network-payload {
  316. background-color: rgba(0, 0, 0, 0.3); padding: 10px; border-radius: 6px; margin-top: 10px;
  317. font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; white-space: pre-wrap;
  318. word-break: break-all; max-height: 150px; overflow-y: auto; display: none;
  319. border: 1px solid rgba(255, 255, 255, 0.1);
  320. }
  321. .aio-image-item {
  322.  
  323. }
  324. .aio-image-button-group {
  325. margin-left: auto; display: flex; gap: 6px;
  326. }
  327. .aio-image-copy, .aio-image-view, .aio-image-download {
  328. border: 1px solid transparent; color: #e0e0e0; padding: 3px 8px; border-radius: 6px;
  329. cursor: pointer; font-size: 10px; transition: background-color 0.2s ease, border-color 0.2s ease;
  330. white-space: nowrap;
  331. }
  332. .aio-image-copy {
  333. background-color: rgba(147, 112, 219, 0.2); border-color: rgba(147, 112, 219, 0.4);
  334. }
  335. .aio-image-view {
  336. background-color: rgba(64, 156, 255, 0.2); border-color: rgba(64, 156, 255, 0.4);
  337. }
  338. .aio-image-download {
  339. background-color: rgba(50, 205, 50, 0.2); border-color: rgba(50, 205, 50, 0.4);
  340. }
  341. .aio-image-copy:hover {
  342. background-color: rgba(147, 112, 219, 0.4); border-color: rgba(147, 112, 219, 0.6);
  343. }
  344. .aio-image-view:hover {
  345. background-color: rgba(64, 156, 255, 0.4); border-color: rgba(64, 156, 255, 0.6);
  346. }
  347. .aio-image-download:hover {
  348. background-color: rgba(50, 205, 50, 0.4); border-color: rgba(50, 205, 50, 0.6);
  349. }
  350. .aio-image-copy.copied {
  351. background-color: rgba(50, 205, 50, 0.4); border-color: rgba(50, 205, 50, 0.6); color: #fff;
  352. }
  353. .aio-image-truncated {
  354. cursor: pointer;
  355. }
  356. .aio-image-truncated:hover {
  357. text-decoration: underline;
  358. }
  359. .aio-popup::-webkit-scrollbar, .aio-content::-webkit-scrollbar {
  360. width: 8px;
  361. }
  362. .aio-popup::-webkit-scrollbar-track, .aio-content::-webkit-scrollbar-track {
  363. background: rgba(255, 255, 255, 0.05); border-radius: 10px;
  364. }
  365. .aio-popup::-webkit-scrollbar-thumb, .aio-content::-webkit-scrollbar-thumb {
  366. background: rgba(255, 255, 255, 0.2); border-radius: 10px; border: 2px solid transparent; background-clip: content-box;
  367. }
  368. .aio-popup::-webkit-scrollbar-thumb:hover, .aio-content::-webkit-scrollbar-thumb:hover {
  369. background: rgba(255, 255, 255, 0.4);
  370. }
  371. `;
  372. const ss = document.createElement('style');
  373. ss.textContent = css;
  374. document.head.prepend(ss);
  375. }
  376.  
  377. _mkTglBtn() {
  378. if (document.getElementById(this._T_ID)) return;
  379. this._tglBtn = document.createElement('button');
  380. this._tglBtn.id = this._T_ID;
  381. this._tglBtn.title = 'Toggle Scanner (Ctrl+Shift+A)';
  382. this._tglBtn.textContent = '🔍';
  383. this._tglBtn.className = 'aio-button';
  384. document.body.appendChild(this._tglBtn);
  385. }
  386.  
  387. _mkPop() {
  388. if (document.getElementById(this._P_ID)) return;
  389. this._pop = document.createElement('div');
  390. this._pop.className = 'aio-popup';
  391. this._pop.id = this._P_ID;
  392. this._pop.innerHTML = `
  393. <div class="aio-header">
  394. <h2 class="aio-title">All-in-One Web Scanner [BETA]</h2>
  395. <div class="aio-controls">
  396. <button class="aio-button" id="aio-clear" title="Clear all scanned data">Clear</button>
  397. <button class="aio-button" id="aio-refresh" title="Rescan the page">Refresh</button>
  398. <button class="aio-button" id="aio-close" title="Close Scanner (Ctrl+Shift+A)">✕</button>
  399. </div>
  400. </div>
  401. ${this._genSecs()}
  402. <div class="aio-footer">
  403. Made with <span class="aio-heart">❤️</span> by DARKIE | v${GM_info?.script?.version || '1.7.1'}
  404. </div>
  405. `;
  406. document.body.appendChild(this._pop);
  407. }
  408.  
  409. _genSecs() {
  410. const secs = [{
  411. id: 'stream-section',
  412. title: 'Stream Files',
  413. contentId: 'stream-content',
  414. hasFilter: false,
  415. generator: this._genStrmSecs.bind(this)
  416. }, {
  417. id: 'iframe-section',
  418. title: 'Iframe Sources',
  419. contentId: 'iframe-content',
  420. hasFilter: false,
  421. generator: this._genEmpty.bind(this, 'iframes')
  422. }, {
  423. id: 'network-section',
  424. title: 'Network Requests',
  425. contentId: 'network-content',
  426. hasFilter: true,
  427. filterPlaceholder: 'Filter requests by URL...',
  428. generator: this._genEmpty.bind(this, 'network requests')
  429. }, {
  430. id: 'image-section',
  431. title: 'Image Sources',
  432. contentId: 'image-content',
  433. hasFilter: true,
  434. filterPlaceholder: 'Filter images by URL...',
  435. generator: this._genEmpty.bind(this, 'images')
  436. }, ];
  437. return secs.map(s => `
  438. <div class="aio-section" id="${s.id}">
  439. <h3 class="aio-section-title">${s.title}</h3>
  440. ${s.hasFilter ? `<input type="text" class="aio-filter" id="${s.contentId}-filter" placeholder="${s.filterPlaceholder}">` : ''}
  441. <div class="aio-content" id="${s.contentId}">
  442. ${s.generator()}
  443. </div>
  444. </div>
  445. `).join('');
  446. }
  447.  
  448. _genStrmSecs() {
  449. return this._cfg.linkTypes.map(t => `
  450. <div class="aio-subsection" id="${t}-section">
  451. <h4 class="aio-section-subtitle" style="font-size: 13px; margin-bottom: 5px; color: #ccc;">${t.toUpperCase()}</h4>
  452. <div class="aio-list-container" id="${t}-content">
  453. <div class="aio-empty">No ${t.toUpperCase()} files detected yet...</div>
  454. </div>
  455. </div>
  456. `).join('');
  457. }
  458.  
  459. _mkPrvPop() {
  460. if (document.getElementById(this._PR_ID)) return;
  461. this._prvPop = document.createElement('div');
  462. this._prvPop.className = 'aio-image-preview';
  463. this._prvPop.id = this._PR_ID;
  464. this._prvPop.innerHTML = '<img src="" alt="Preview">';
  465. document.body.appendChild(this._prvPop);
  466. }
  467.  
  468. _cacheDOM() {
  469. this._pop = document.getElementById(this._P_ID);
  470. this._tglBtn = document.getElementById(this._T_ID);
  471. this._prvPop = document.getElementById(this._PR_ID);
  472. if (!this._pop || !this._tglBtn || !this._prvPop) {
  473. return false;
  474. }
  475. this._clrBtn = this._pop.querySelector('#aio-clear');
  476. this._clsBtn = this._pop.querySelector('#aio-close');
  477. this._refBtn = this._pop.querySelector('#aio-refresh');
  478. this._netFltIn = this._pop.querySelector('#network-content-filter');
  479. this._imgFltIn = this._pop.querySelector('#image-content-filter');
  480. return true;
  481. }
  482.  
  483. _addEvts() {
  484. this._clsBtn ?.addEventListener('click', () => this._hidePop());
  485. this._clrBtn ?.addEventListener('click', () => this._clrAll());
  486. this._refBtn ?.addEventListener('click', () => this._scanAll());
  487. this._tglBtn ?.addEventListener('click', () => this._tglPop());
  488. this._netFltIn ?.addEventListener('input', this._dbnc(this._hdlNetFlt.bind(this), 300));
  489. this._imgFltIn ?.addEventListener('input', this._dbnc(this._hdlImgFlt.bind(this), 300));
  490. }
  491.  
  492. _setupKB() {
  493. if (window.aioScannerKBLstnr) return;
  494. document.addEventListener('keydown', (e) => {
  495. if (e.ctrlKey && e.shiftKey && (e.key === 'A' || e.key === 'a')) {
  496. e.preventDefault();
  497. this._tglPop();
  498. }
  499. });
  500. window.aioScannerKBLstnr = true;
  501. }
  502.  
  503. _setupDelEvts() {
  504. if (!this._pop || window.aioScannerDelLstnr) return;
  505.  
  506. this._pop.addEventListener('click', (evt) => {
  507. const sl = evt.target.closest('.aio-link');
  508. if (sl) {
  509. this._hdlStrmCp(sl);
  510. return;
  511. }
  512. const ifi = evt.target.closest('.aio-iframe-item');
  513. if (ifi) {
  514. this._hdlIfrCp(ifi);
  515. return;
  516. }
  517. const ni = evt.target.closest('.aio-network-item');
  518. if (ni) {
  519. if (evt.target.closest('.aio-network-copy')) {
  520. this._hdlNetCp(evt.target.closest('.aio-network-copy'));
  521. } else if (evt.target.closest('.aio-network-show-payload')) {
  522. this._hdlNetPTgl(evt.target.closest('.aio-network-show-payload'));
  523. }
  524. return;
  525. }
  526. const imi = evt.target.closest('.aio-image-item');
  527. if (imi) {
  528. if (evt.target.closest('.aio-image-copy')) {
  529. this._hdlImgCp(evt.target.closest('.aio-image-copy'));
  530. } else if (evt.target.closest('.aio-image-view')) {
  531. this._hdlImgVw(evt.target.closest('.aio-image-view'));
  532. } else if (evt.target.closest('.aio-image-download')) {
  533. this._hdlImgDl(evt.target.closest('.aio-image-download'));
  534. } else if (evt.target.closest('.aio-image-truncated')) {
  535. this._hdlImgTrunClk(evt.target.closest('.aio-image-truncated'));
  536. }
  537. return;
  538. }
  539. });
  540.  
  541. const imgCont = this._pop.querySelector(this._cfg.selectors.image);
  542. if (imgCont) {
  543. imgCont.addEventListener('mouseover', (evt) => {
  544. const imi = evt.target.closest('.aio-image-item');
  545. if (imi) {
  546. this._hdlImgPrvShow(imi);
  547. }
  548. });
  549. imgCont.addEventListener('mouseout', (evt) => {
  550. const imi = evt.target.closest('.aio-image-item');
  551. const rt = evt.relatedTarget;
  552. if (imi && !imi.contains(rt) && !this._prvPop ?.contains(rt)) {
  553. this._hdlImgPrvHide();
  554. }
  555. });
  556. }
  557. window.aioScannerDelLstnr = true;
  558. }
  559.  
  560. // --- Event Handlers ---
  561.  
  562. _hdlStrmCp(el) {
  563. const u = el.dataset.url;
  564. if (u) {
  565. this._cpHlp(u, el);
  566. }
  567. }
  568.  
  569. async _hdlIfrCp(el) {
  570. const fs = el.dataset.src;
  571. if (fs) {
  572. this._cpHlp(fs, el);
  573. } else {
  574. console.warn("AIO Scanner (Short): Could not find data-src on iframe item:", el);
  575. this._flashItm(el, false);
  576. }
  577. }
  578.  
  579. async _hdlNetCp(btn) {
  580. const i = btn.dataset.index;
  581. if (i !== undefined && this._reqs[i]) {
  582. const u = this._reqs[i].url;
  583. const ok = await this._cpClip(u);
  584. this._flashBtn(btn, ok, 'Copied!', 'Copy URL');
  585. }
  586. }
  587.  
  588. _hdlNetPTgl(btn) {
  589. const i = btn.dataset.index;
  590. if (i !== undefined && this._pop) {
  591. const pDiv = this._pop.querySelector(`#payload-${i}`);
  592. if (pDiv) {
  593. const v = pDiv.style.display === 'block';
  594. pDiv.style.display = v ? 'none' : 'block';
  595. btn.textContent = v ? 'Payload' : 'Hide';
  596. }
  597. }
  598. }
  599.  
  600. async _hdlImgCp(btn) {
  601. const itm = btn.closest('.aio-image-item');
  602. if (itm) {
  603. const s = itm.dataset.src;
  604. const ok = await this._cpClip(s);
  605. this._flashBtn(btn, ok, 'Copied!', 'Copy');
  606. }
  607. }
  608.  
  609. _hdlImgVw(btn) {
  610. const itm = btn.closest('.aio-image-item');
  611. if (itm) {
  612. const s = itm.dataset.src;
  613. window.open(s, '_blank');
  614. }
  615. }
  616.  
  617. _hdlImgDl(btn) {
  618. const itm = btn.closest('.aio-image-item');
  619. if (itm) {
  620. const s = itm.dataset.src;
  621. const fn = s.split('/').pop().split(/[?#]/)[0] || 'image';
  622. this._dlImg(s, fn);
  623. }
  624. }
  625.  
  626. _hdlImgTrunClk(srcEl) {
  627. const fu = srcEl.title;
  628. if (srcEl.textContent === fu) {
  629. srcEl.textContent = this._truncUrl(fu);
  630. } else {
  631. srcEl.textContent = fu;
  632. }
  633. }
  634.  
  635. _hdlImgPrvShow(itm) {
  636. if (!this._prvPop) return;
  637. const s = itm.dataset.src;
  638. const prv = this._prvPop;
  639. const img = prv.querySelector('img');
  640. img.src = s;
  641.  
  642. const r = itm.getBoundingClientRect();
  643. const pW = this._prvMaxW;
  644. const pL = r.left - pW - this._prvPad;
  645.  
  646. if (pL >= 0) {
  647. prv.style.left = `${pL}px`;
  648. prv.style.right = 'auto';
  649. } else {
  650. const pR = r.right + this._prvPad;
  651. if (pR + pW <= window.innerWidth) {
  652. prv.style.left = `${pR}px`;
  653. prv.style.right = 'auto';
  654. } else {
  655. prv.style.left = `${this._prvPad}px`;
  656. prv.style.right = 'auto';
  657. }
  658. }
  659.  
  660. const pT = r.top;
  661. const vh = window.innerHeight;
  662. const pH = prv.offsetHeight || 200;
  663. if (pT + pH > vh - this._prvPad) {
  664. prv.style.top = `${Math.max(0, vh - pH - this._prvPad)}px`;
  665. } else {
  666. prv.style.top = `${Math.max(0, pT)}px`;
  667. }
  668.  
  669. prv.style.display = 'block';
  670. requestAnimationFrame(() => prv.style.opacity = '1');
  671. }
  672.  
  673. _hdlImgPrvHide() {
  674. if (!this._prvPop) return;
  675. this._prvPop.style.opacity = '0';
  676. setTimeout(() => {
  677. if (this._prvPop && this._prvPop.style.opacity === '0') {
  678. this._prvPop.style.display = 'none';
  679. }
  680. }, 200);
  681. }
  682.  
  683. _hdlNetFlt(evt) {
  684. if (!this._pop) return;
  685. const f = evt.target.value.toLowerCase();
  686. const nc = this._pop.querySelector(this._cfg.selectors.network);
  687. if (!nc) return;
  688. nc.querySelectorAll('.aio-network-item').forEach(itm => {
  689. const ue = itm.querySelector('.aio-network-url');
  690. const u = ue ? (ue.title || ue.textContent).toLowerCase() : '';
  691. itm.style.display = u.includes(f) ? 'block' : 'none';
  692. });
  693. }
  694.  
  695. _hdlImgFlt(evt) {
  696. if (!this._pop) return;
  697. const f = evt.target.value.toLowerCase();
  698. const ic = this._pop.querySelector(this._cfg.selectors.image);
  699. if (!ic) return;
  700. ic.querySelectorAll('.aio-image-item').forEach(itm => {
  701. const s = itm.dataset.src ? itm.dataset.src.toLowerCase() : '';
  702. itm.style.display = s.includes(f) ? 'flex' : 'none';
  703. });
  704. }
  705.  
  706. // --- State Management & SPA Handling ---
  707.  
  708. _svSt(isOpen) {
  709. try {
  710. sessionStorage.setItem(this._SK, JSON.stringify({
  711. isOpen
  712. }));
  713. } catch (e) {
  714. console.warn("AIO Scanner: Failed to save state to sessionStorage.", e);
  715. }
  716. }
  717.  
  718. _ldSt() {
  719. try {
  720. const st = sessionStorage.getItem(this._SK);
  721. return st ? JSON.parse(st) : {
  722. isOpen: false
  723. };
  724. } catch (e) {
  725. console.warn("AIO Scanner: Failed to load state from sessionStorage. Resetting.", e);
  726. sessionStorage.removeItem(this._SK);
  727. return {
  728. isOpen: false
  729. };
  730. }
  731. }
  732.  
  733. _chkStUI() {
  734. if (!this._pop || !this._tglBtn) {
  735. this._ensureUI();
  736. return;
  737. }
  738. const st = this._ldSt();
  739. const shouldVis = st.isOpen;
  740. const isVis = this._pop.classList.contains('aio-visible');
  741.  
  742. if (shouldVis && !isVis) {
  743. this._showPop();
  744. } else if (!shouldVis && isVis) {
  745. this._hidePop();
  746. } else if (!shouldVis && !isVis) {
  747. this._pop.style.display = 'none';
  748. this._tglBtn.style.display = 'block';
  749. if (this._prvPop) {
  750. this._prvPop.style.display = 'none';
  751. this._prvPop.style.opacity = '0';
  752. }
  753. }
  754. }
  755.  
  756. _listenSPA() {
  757. if (window.aioScannerNavLstnr) return;
  758.  
  759. const navHdl = () => {
  760. this._ensureUI();
  761. setTimeout(() => this._chkStUI(), 50);
  762. setTimeout(() => this._scanIfNeed(), 150);
  763. };
  764.  
  765. window.addEventListener('popstate', navHdl);
  766. window.addEventListener('hashchange', navHdl);
  767.  
  768. const self = this;
  769.  
  770. function wrapHist(m) {
  771. const orig = history[m];
  772. if (!orig) return;
  773. try {
  774. history[m] = function(st, t, u) {
  775. let res;
  776. try {
  777. res = orig.apply(this, arguments);
  778. } catch (e) {
  779. console.error(`AIO Scanner: Error in original history.${m}`, e);
  780. throw e;
  781. }
  782. try {
  783. const evt = new CustomEvent('historystatechange', {
  784. detail: {
  785. url: u,
  786. state: st,
  787. title: t,
  788. method: m
  789. }
  790. });
  791. window.dispatchEvent(evt);
  792. } catch (e) {
  793. console.warn("AIO Scanner: CustomEvent dispatch failed, using direct handler.", e);
  794. navHdl();
  795. }
  796. return res;
  797. };
  798. } catch (e) {
  799. console.error(`AIO Scanner: Failed to wrap history.${m}`, e);
  800. }
  801. }
  802.  
  803. wrapHist('pushState');
  804. wrapHist('replaceState');
  805. window.addEventListener('historystatechange', navHdl);
  806.  
  807. window.aioScannerNavLstnr = true;
  808. console.log("AIO Scanner (Short): SPA Navigation listener attached.");
  809. }
  810.  
  811. _scanIfNeed() {
  812. const popEl = document.getElementById(this._P_ID);
  813. if (popEl ?.classList.contains('aio-visible')) {
  814. this._scanAll();
  815. }
  816. }
  817.  
  818. _tglPop() {
  819. if (!this._pop) {
  820. this._ensureUI();
  821. return;
  822. }
  823. const isVis = this._pop.classList.contains('aio-visible');
  824. if (isVis) {
  825. this._hidePop();
  826. } else {
  827. this._showPop();
  828. this._scanAll();
  829. }
  830. }
  831.  
  832. _showPop() {
  833. if (!this._pop || !this._tglBtn) {
  834. this._ensureUI();
  835. return;
  836. }
  837. this._pop.style.display = 'block';
  838. requestAnimationFrame(() => {
  839. requestAnimationFrame(() => {
  840. if (this._pop) this._pop.classList.add('aio-visible');
  841. });
  842. });
  843. this._tglBtn.style.display = 'none';
  844. this._svSt(true);
  845. }
  846.  
  847. _hidePop() {
  848. if (!this._pop || !this._tglBtn) {
  849. this._ensureUI();
  850. return;
  851. }
  852. this._pop.classList.remove('aio-visible');
  853. setTimeout(() => {
  854. if (this._pop) this._pop.style.display = 'none';
  855. if (this._prvPop) {
  856. this._prvPop.style.display = 'none';
  857. this._prvPop.style.opacity = '0';
  858. }
  859. if (this._tglBtn) this._tglBtn.style.display = 'block';
  860. }, 200);
  861. this._svSt(false);
  862. }
  863.  
  864. _obsDOM() {
  865. if (this._obs) {
  866. this._obs.disconnect();
  867. }
  868. if (!document.body) {
  869. console.warn("AIO Scanner: Cannot observe DOM, document.body not ready.");
  870. return;
  871. }
  872.  
  873. try {
  874. this._obs = new MutationObserver(muts => {
  875. let changed = false;
  876. let removed = false;
  877. for (const m of muts) {
  878. if (m.type === 'childList') {
  879. if (m.addedNodes.length > 0) {
  880. changed = true;
  881. m.addedNodes.forEach(n => {
  882. if (n.nodeType === Node.ELEMENT_NODE) {
  883. this._scanElStrms(n);
  884. }
  885. });
  886. }
  887. if (m.removedNodes.length > 0) {
  888. for (const n of m.removedNodes) {
  889. if (n.id === this._P_ID || n.id === this._T_ID || n.id === this._PR_ID) {
  890. removed = true;
  891. break;
  892. }
  893. }
  894. }
  895. } else if (m.type === 'attributes') {
  896. changed = true;
  897. const tgt = m.target;
  898. const attr = m.attributeName;
  899. if (tgt && attr) {
  900. const val = tgt.getAttribute(attr);
  901. if (typeof val === 'string' && ['src', 'href', 'data', 'data-src', 'data-url', 'style'].includes(attr.toLowerCase())) {
  902. if (attr.toLowerCase() === 'style') {
  903. this._debScanAll();
  904. } else {
  905. this._hdlStrmUrl(val, `mutAttr-${attr}`);
  906. }
  907. }
  908. }
  909. }
  910. if (removed) break;
  911. }
  912.  
  913. if (removed) {
  914. console.warn("AIO Scanner: UI element removed, attempting to re-initialize.");
  915. this._ensureUI();
  916. } else {
  917. this._debEnsureUI();
  918. }
  919. });
  920.  
  921. this._obs.observe(document.body, {
  922. childList: true,
  923. subtree: true,
  924. attributes: true,
  925. attributeFilter: ['src', 'style', 'data-src', 'data-url', 'href', 'data']
  926. });
  927. } catch (e) {
  928. console.error("AIO Scanner: Failed to create or attach MutationObserver.", e);
  929. }
  930. }
  931.  
  932. _ensureUI() {
  933. const tglMiss = !document.getElementById(this._T_ID);
  934. const popMiss = !document.getElementById(this._P_ID);
  935. const prvMiss = !document.getElementById(this._PR_ID);
  936.  
  937. if (tglMiss || popMiss || prvMiss) {
  938. console.log("AIO Scanner: UI element missing, re-initializing UI.");
  939. this._initd = false;
  940. this._initUIObs();
  941. this._chkStUI();
  942. }
  943. }
  944.  
  945. /* ==========================================================================
  946. Scanning Logic
  947. ========================================================================== */
  948.  
  949. _scanAll() {
  950. const now = Date.now();
  951. if (this._lastScanTime && (now - this._lastScanTime < 200)) {
  952. return;
  953. }
  954. this._lastScanTime = now;
  955.  
  956. try {
  957. this._scanVidSrc();
  958. this._scanIfr();
  959. this._updNetDisp();
  960. this._scanImgs();
  961. this._scanInline();
  962. } catch (err) {
  963. console.error("AIO Scanner: Error during full scan:", err);
  964. }
  965. }
  966.  
  967. _scanVidSrc() {
  968. try {
  969. document.querySelectorAll(
  970. 'video, source, track, object, embed, a, [data-src], [data-url], [data-stream-url], [data-hls-url], [data-mp4-url]'
  971. ).forEach(el => {
  972. const urls = [
  973. el.src,
  974. el.currentSrc,
  975. el.href,
  976. el.data,
  977. el.dataset ?.src,
  978. el.dataset ?.url,
  979. el.getAttribute('data-stream-url'),
  980. el.getAttribute('data-hls-url'),
  981. el.getAttribute('data-mp4-url')
  982. ].filter(Boolean);
  983.  
  984. urls.forEach(u => {
  985. if (typeof u === 'string') {
  986. this._hdlStrmUrl(u, `tag-${el.tagName.toLowerCase()}`);
  987. }
  988. });
  989.  
  990. if (el.tagName === 'TRACK' && el.default && el.src) {
  991. this._hdlStrmUrl(el.src, 'tag-track-default');
  992. }
  993. });
  994. } catch (err) {
  995. console.error("AIO Scanner: Error scanning video/source elements:", err);
  996. }
  997. }
  998.  
  999. _isPotStrmUrl(u) {
  1000. if (typeof u !== 'string') return false;
  1001. const lu = u.toLowerCase();
  1002. const cu = lu.split(/[?#]/)[0];
  1003.  
  1004. if (/\.(m3u8|mp4|vtt|srt|mpd)$/i.test(cu)) return true;
  1005. if (/\/hls\/|\/dash\/|\/manifest\/|\/playlist\.m3u8|\/master\.m3u8|\/video\.mp4|\.mpd/i.test(lu)) return true;
  1006. if (/[?&](format|type|ext)=(m3u8|mp4|vtt|srt|mpd)/i.test(lu)) return true;
  1007. if (lu.startsWith('blob:')) return true;
  1008.  
  1009. return false;
  1010. }
  1011.  
  1012. _detStrmTyp(u, m = null) {
  1013. if (typeof u !== 'string') return null;
  1014. const lu = u.toLowerCase();
  1015.  
  1016. if (m) {
  1017. const lm = m.toLowerCase().split(';')[0].trim();
  1018. if (this._STRM_MIME[lm]) {
  1019. return this._STRM_MIME[lm];
  1020. }
  1021. }
  1022.  
  1023. for (const t of [...this._cfg.linkTypes, 'mpd']) {
  1024. if (lu.includes(`.${t}`)) {
  1025. return t;
  1026. }
  1027. }
  1028.  
  1029. if (lu.includes('m3u8') || lu.includes('/hls/') || lu.includes('/manifest/')) return 'm3u8';
  1030. if (lu.includes('.mpd') || lu.includes('/dash/')) return 'mpd';
  1031. if (lu.includes('mp4') || lu.includes('/video/')) return 'mp4';
  1032. if (lu.includes('vtt') || lu.includes('/caption')) return 'vtt';
  1033. if (lu.includes('srt') || lu.includes('/subtitle')) return 'srt';
  1034. if (lu.startsWith('blob:')) return 'mp4';
  1035.  
  1036. return null;
  1037. }
  1038.  
  1039. _hdlStrmUrl(u, srcT = 'unknown', m = null) {
  1040. if (typeof u !== 'string' || u.trim() === '' || u.startsWith('javascript:')) {
  1041. return;
  1042. }
  1043. const absU = this._absUrl(u);
  1044. if (!absU) return;
  1045.  
  1046. if (!this._isPotStrmUrl(absU)) {
  1047. return;
  1048. }
  1049.  
  1050. const type = this._detStrmTyp(absU, m);
  1051. if (!type || !this._cfg.linkTypes.includes(type)) {
  1052. return;
  1053. }
  1054.  
  1055. const clnU = this._clnUrl(absU);
  1056. if (!this._urls.has(clnU)) {
  1057. this._urls.add(clnU);
  1058. this._addStrmLnk(type, absU);
  1059. }
  1060. }
  1061.  
  1062. _scanInline() {
  1063. try {
  1064. document.querySelectorAll('script').forEach(scr => {
  1065. if (!scr.hasAttribute('src') && scr.textContent) {
  1066. const cont = scr.textContent;
  1067. let m;
  1068. this._RGX_STRM.lastIndex = 0;
  1069. while ((m = this._RGX_STRM.exec(cont)) !== null) {
  1070. const fUrl = m[1] || m[3];
  1071. if (fUrl) {
  1072. this._hdlStrmUrl(fUrl, 'script');
  1073. }
  1074. }
  1075. }
  1076. });
  1077. } catch (err) {
  1078. console.error("AIO Scanner: Error scanning inline scripts:", err);
  1079. }
  1080. }
  1081.  
  1082. _monBlob() {
  1083. if (window.aioBlobMon || typeof URL === 'undefined' || !URL.createObjectURL) return;
  1084.  
  1085. const self = this;
  1086. const origC = URL.createObjectURL;
  1087.  
  1088. try {
  1089. URL.createObjectURL = function(obj) {
  1090. const res = origC.apply(this, arguments);
  1091. if (obj instanceof Blob && obj.type) {
  1092. const lt = obj.type.toLowerCase();
  1093. const st = Object.entries(self._STRM_MIME).find(([m]) => lt.startsWith(m)) ?. [1];
  1094. if (st) {
  1095. // Blob found, potentially useful for debugging, but rely on src assignment
  1096. }
  1097. }
  1098. return res;
  1099. };
  1100. window.aioBlobMon = true;
  1101. console.log("AIO Scanner (Short): Blob URL monitor attached.");
  1102. } catch (err) {
  1103. console.error("AIO Scanner: Failed to monitor Blob URLs, reverting.", err);
  1104. URL.createObjectURL = origC;
  1105. }
  1106. }
  1107.  
  1108. _scanElStrms(el) {
  1109. if (!el || typeof el.querySelectorAll !== 'function') return;
  1110.  
  1111. try {
  1112. const urlsS = [
  1113. el.src,
  1114. el.href,
  1115. el.data,
  1116. el.dataset ?.src,
  1117. el.dataset ?.url,
  1118. el.getAttribute('data-stream-url')
  1119. ].filter(Boolean);
  1120. urlsS.forEach(u => this._hdlStrmUrl(u, `added-${el.tagName.toLowerCase()}`));
  1121.  
  1122. el.querySelectorAll(
  1123. 'video, source, track, object, embed, a, [data-src], [data-url], [data-stream-url]'
  1124. ).forEach(ch => {
  1125. const urlsC = [
  1126. ch.src,
  1127. ch.currentSrc,
  1128. ch.href,
  1129. ch.data,
  1130. ch.dataset ?.src,
  1131. ch.dataset ?.url,
  1132. ch.getAttribute('data-stream-url')
  1133. ].filter(Boolean);
  1134. urlsC.forEach(u => this._hdlStrmUrl(u, `added-child-${ch.tagName.toLowerCase()}`));
  1135. if (ch.tagName === 'TRACK' && ch.default && ch.src) {
  1136. this._hdlStrmUrl(ch.src, 'added-track-default');
  1137. }
  1138. });
  1139. } catch (err) {
  1140. console.error("AIO Scanner: Error scanning added element streams:", err);
  1141. }
  1142. }
  1143.  
  1144. _scanIfr() {
  1145. try {
  1146. const ifrs = Array.from(document.getElementsByTagName('iframe'));
  1147. const ifrInfos = ifrs.map(ifr => this._extIfrInf(ifr));
  1148.  
  1149. const embs = Array.from(document.getElementsByTagName('embed'));
  1150. const objs = Array.from(document.getElementsByTagName('object'));
  1151. const frms = Array.from(document.getElementsByTagName('frame'));
  1152.  
  1153. const addSrcs = [...embs, ...objs, ...frms]
  1154. .map(el => ({
  1155. src: el.src || el.data,
  1156. type: el.tagName.toLowerCase(),
  1157. attributes: []
  1158. }))
  1159. .filter(inf => inf.src);
  1160.  
  1161. const allSrcs = [...ifrInfos, ...addSrcs]
  1162. .map(inf => ({ ...inf,
  1163. src: this._absUrl(inf.src)
  1164. }))
  1165. .filter(inf => inf.src && !inf.src.startsWith('about:blank') && !inf.src.startsWith('javascript:'));
  1166.  
  1167. const uniqSrcs = Array.from(new Map(allSrcs.map(itm => [itm.src, itm])).values());
  1168.  
  1169. const cont = this._pop ?.querySelector(this._cfg.selectors.iframe);
  1170. if (cont) {
  1171. cont.innerHTML = uniqSrcs.length > 0 ?
  1172. this._genIfrList(uniqSrcs) :
  1173. this._genEmpty('iframes');
  1174. }
  1175. } catch (err) {
  1176. console.error("AIO Scanner: Error scanning iframes/embeds:", err);
  1177. }
  1178. }
  1179.  
  1180. _extIfrInf(ifr) {
  1181. let s = ifr.src || ifr.dataset.src;
  1182. let t = ifr.src ? 'direct' : (ifr.dataset.src ? 'lazy' : 'unknown');
  1183. if (!s && ifr.srcdoc) {
  1184. s = 'srcdoc';
  1185. t = 'srcdoc';
  1186. }
  1187. const a = [];
  1188. if (ifr.allow) a.push('allow: ' + ifr.allow.substring(0, 30) + (ifr.allow.length > 30 ? '...' : ''));
  1189. if (ifr.sandbox) a.push('sandbox');
  1190. if (ifr.loading) a.push('loading: ' + ifr.loading);
  1191. if (ifr.name) a.push('name: ' + ifr.name);
  1192. if (ifr.width || ifr.height) a.push(`size: ${ifr.width || '?'}x${ifr.height || '?'}`);
  1193.  
  1194. return {
  1195. src: s || '',
  1196. type: t,
  1197. attributes: a
  1198. };
  1199. }
  1200.  
  1201. _scanImgs() {
  1202. try {
  1203. const imgs = Array.from(document.querySelectorAll('img'));
  1204. const imgInfos = imgs
  1205. .map(img => this._extImgInf(img))
  1206. .filter(inf => inf.src && !inf.src.startsWith('data:'));
  1207.  
  1208. const els = document.getElementsByTagName('*');
  1209. const bgImgs = Array.from(els)
  1210. .map(el => {
  1211. try {
  1212. const st = window.getComputedStyle(el);
  1213. if (!st || !st.backgroundImage || st.backgroundImage === 'none') {
  1214. return [];
  1215. }
  1216. const urls = (st.backgroundImage.match(/url\(['"]?(.*?)['"]?\)/g) || [])
  1217. .map(m => m.replace(/url\(['"]?(.*?)['"]?\)/g, '$1'))
  1218. .filter(u => u && !u.startsWith('data:'));
  1219.  
  1220. return urls.map(u => ({
  1221. src: this._absUrl(u),
  1222. type: 'bg',
  1223. attributes: [`el: ${el.tagName.toLowerCase()}${el.id ? '#' + el.id : ''}${el.className && typeof el.className ==='string' ? '.' + el.className.split(' ').filter(Boolean).join('.') : ''}`]
  1224. }));
  1225. } catch (e) {
  1226. return [];
  1227. }
  1228. })
  1229. .flat()
  1230. .filter(Boolean);
  1231.  
  1232. const combSrcs = [...imgInfos, ...bgImgs];
  1233.  
  1234. const uniqSrcs = Array.from(new Map(combSrcs.map(itm => [itm.src, itm])).values())
  1235. .filter(itm => itm.src);
  1236.  
  1237. const cont = this._pop ?.querySelector(this._cfg.selectors.image);
  1238. if (cont) {
  1239. cont.innerHTML = uniqSrcs.length > 0 ?
  1240. this._genImgList(uniqSrcs) :
  1241. this._genEmpty('images');
  1242. if (this._imgFltIn) {
  1243. this._hdlImgFlt({
  1244. target: this._imgFltIn
  1245. });
  1246. }
  1247. }
  1248. } catch (err) {
  1249. console.error("AIO Scanner: Error scanning images:", err);
  1250. }
  1251. }
  1252.  
  1253. _extImgInf(img) {
  1254. const s = img.currentSrc || img.src;
  1255. const a = [];
  1256. if (img.alt) a.push(`alt: ${img.alt.substring(0, 30)}${img.alt.length > 30 ? '...' : ''}`);
  1257. if (img.naturalWidth > 0 || img.naturalHeight > 0) {
  1258. a.push(`actual: ${img.naturalWidth}x${img.naturalHeight}`);
  1259. } else if (img.width || img.height) {
  1260. a.push(`attr: ${img.width}x${img.height}`);
  1261. }
  1262. if (img.loading) a.push(`loading: ${img.loading}`);
  1263. if (img.srcset) a.push('srcset');
  1264.  
  1265. const absS = this._absUrl(s);
  1266. const ext = absS ?.split('.').pop().split(/[#?]/)[0];
  1267. if (ext) a.push(ext.toLowerCase());
  1268.  
  1269. return {
  1270. src: absS,
  1271. type: 'img',
  1272. attributes: a
  1273. };
  1274. }
  1275.  
  1276. /* ==========================================================================
  1277. Network Monitoring
  1278. ========================================================================== */
  1279.  
  1280. _setupNetMon() {
  1281. if (window.aioNetMon) return;
  1282.  
  1283. const self = this;
  1284.  
  1285. try {
  1286. const oF = window.fetch;
  1287. window.fetch = async function(...args) {
  1288. const req = args[0];
  1289. const opts = args[1] || {};
  1290. const u = typeof req === 'string' ? req : req ?.url;
  1291. const m = opts.method || (typeof req === 'object' ? req ?.method : 'GET') || 'GET';
  1292. const sT = Date.now();
  1293. let pld = null;
  1294.  
  1295. try {
  1296. if (opts.body) {
  1297. pld = typeof opts.body === 'string' ? opts.body : '[Non-String Body]';
  1298. } else if (req instanceof Request && req.body) {
  1299. pld = await req.clone().text().catch(() => '[Stream Body]');
  1300. }
  1301. } catch (payloadError) {
  1302. pld = '[Error Reading Body]';
  1303. console.warn("AIO Scanner: Error reading fetch request body:", payloadError);
  1304. }
  1305.  
  1306. self._hdlStrmUrl(u, 'net-fetch-req');
  1307.  
  1308. try {
  1309. const res = await oF.apply(this, args);
  1310. const eT = Date.now();
  1311. const ct = res.headers.get('Content-Type');
  1312.  
  1313. if (ct) {
  1314. self._hdlStrmUrl(u, 'net-fetch-resp', ct);
  1315. }
  1316.  
  1317. self._addNetReq({
  1318. url: self._absUrl(u),
  1319. method: m.toUpperCase(),
  1320. status: res.status,
  1321. duration: eT - sT,
  1322. payload: pld,
  1323. headers: self._hdrsToObj(res.headers),
  1324. timestamp: new Date().toISOString(),
  1325. type: 'fetch'
  1326. });
  1327. return res;
  1328. } catch (err) {
  1329. self._addNetReq({
  1330. url: self._absUrl(u),
  1331. method: m.toUpperCase(),
  1332. status: 'ERROR',
  1333. error: err.message || String(err),
  1334. payload: pld,
  1335. timestamp: new Date().toISOString(),
  1336. type: 'fetch'
  1337. });
  1338. throw err;
  1339. }
  1340. };
  1341. } catch (e) {
  1342. console.error("AIO Scanner: Failed to patch window.fetch.", e);
  1343. }
  1344.  
  1345. try {
  1346. const X = XMLHttpRequest.prototype;
  1347. const oO = X.open;
  1348. const oS = X.send;
  1349. if (!oO || !oS) throw new Error("XHR methods missing");
  1350.  
  1351. X.open = function(m, u) {
  1352. this._reqURL = self._absUrl(u);
  1353. this._reqMeth = m;
  1354. this._startT = Date.now();
  1355. self._hdlStrmUrl(this._reqURL, 'net-xhr-req');
  1356. oO.apply(this, arguments);
  1357. };
  1358.  
  1359. X.send = function(b) {
  1360. if (this._aioLdLstnr) {
  1361. this.removeEventListener('loadend', this._aioLdLstnr);
  1362. }
  1363.  
  1364. this._aioLdLstnr = () => {
  1365. const eT = Date.now();
  1366. let sP = null;
  1367.  
  1368. if (b) {
  1369. try {
  1370. if (b instanceof Document) sP = '[Document Payload]';
  1371. else if (b instanceof Blob) sP = `[Blob Payload: ${b.type}, Size: ${b.size}]`;
  1372. else if (b instanceof ArrayBuffer) sP = `[ArrayBuffer Payload: ${b.byteLength} bytes]`;
  1373. else if (b instanceof FormData) sP = '[FormData Payload]';
  1374. else if (typeof b === 'string') sP = b.substring(0, 5000) + (b.length > 5000 ? '...' : '');
  1375. else sP = '[Unknown Payload Type]';
  1376. } catch (e) {
  1377. sP = '[Error Reading Payload]';
  1378. console.warn("AIO Scanner: Error reading XHR send payload:", e);
  1379. }
  1380. }
  1381.  
  1382. const ct = this.getResponseHeader('Content-Type');
  1383. if (ct) {
  1384. self._hdlStrmUrl(this._reqURL, 'net-xhr-resp', ct);
  1385. }
  1386.  
  1387. self._addNetReq({
  1388. url: this._reqURL,
  1389. method: (this._reqMeth || 'GET').toUpperCase(),
  1390. status: this.status,
  1391. duration: eT - (this._startT || eT),
  1392. payload: sP,
  1393. headers: self._parseHdrs(this.getAllResponseHeaders()),
  1394. timestamp: new Date().toISOString(),
  1395. type: 'xhr',
  1396. error: (this.status < 200 || this.status >= 400) ? `HTTP ${this.status}` : (this.status === 0 && !this.response ? 'Network Error' : null)
  1397. });
  1398. };
  1399.  
  1400. this.addEventListener('loadend', this._aioLdLstnr);
  1401. oS.apply(this, arguments);
  1402. };
  1403. } catch (e) {
  1404. console.error("AIO Scanner: Failed to patch XMLHttpRequest.", e);
  1405. }
  1406.  
  1407. window.aioNetMon = true;
  1408. console.log("AIO Scanner (Short): Network monitoring (fetch/XHR) attached.");
  1409. }
  1410.  
  1411. _addNetReq(req) {
  1412. if (!req || !req.url || req.url.startsWith('data:') ||
  1413. req.url.startsWith('chrome-extension:') || req.url.startsWith('moz-extension:')) {
  1414. return;
  1415. }
  1416. this._reqs.unshift(req);
  1417. if (this._reqs.length > this._maxR) {
  1418. this._reqs.pop();
  1419. }
  1420. if (this._pop ?.classList.contains('aio-visible')) {
  1421. this._updNetDisp();
  1422. }
  1423. }
  1424.  
  1425. _updNetDisp() {
  1426. const cont = this._pop ?.querySelector(this._cfg.selectors.network);
  1427. if (!cont) return;
  1428.  
  1429. let list = cont.querySelector('.aio-list');
  1430. const es = cont.querySelector('.aio-empty');
  1431.  
  1432. if (this._reqs.length === 0) {
  1433. if (!es) {
  1434. cont.innerHTML = this._genEmpty('network requests');
  1435. }
  1436. if (list) list.innerHTML = '';
  1437. return;
  1438. } else {
  1439. if (!list) list = this._mkList(cont);
  1440. if (es) es.remove();
  1441. }
  1442.  
  1443. const existingKeys = new Set(Array.from(list.children).map(li => li.dataset.requestKey));
  1444. let added = 0;
  1445.  
  1446. for (let i = 0; i < this._reqs.length; i++) {
  1447. const r = this._reqs[i];
  1448. const key = `${r.url}_${r.timestamp}`;
  1449. if (!existingKeys.has(key)) {
  1450. const ni = this._mkNetLi(r, i, key);
  1451. list.prepend(ni);
  1452. added++;
  1453. }
  1454. }
  1455.  
  1456. const curCnt = list.children.length;
  1457. if (curCnt > this._maxR + 10) {
  1458. while (list.children.length > this._maxR) {
  1459. list.removeChild(list.lastChild);
  1460. }
  1461. }
  1462.  
  1463. if (added > 0 || !this._netFiltApp) {
  1464. if (this._netFltIn) {
  1465. this._hdlNetFlt({
  1466. target: this._netFltIn
  1467. });
  1468. this._netFiltApp = true;
  1469. }
  1470. }
  1471. }
  1472.  
  1473. _mkNetLi(r, i, key) {
  1474. const li = document.createElement('li');
  1475. li.className = 'aio-item aio-network-item';
  1476. li.dataset.requestKey = key;
  1477. li.dataset.url = r.url;
  1478.  
  1479. const methodClass = r.status === 'ERROR' || r.status >= 400 ?
  1480. 'error' :
  1481. r.method.toLowerCase();
  1482. const statusColor = r.status >= 400 || r.status === 'ERROR' ? '#ff8a8a' : '#a6e22e';
  1483. const statusText = `${r.status}${r.error ? ` (${r.error.substring(0,30)}...)` : ''}`;
  1484.  
  1485. li.innerHTML = `
  1486. <div>
  1487. <span class="aio-network-method aio-network-method-${methodClass}">${r.method}</span>
  1488. <span class="aio-network-url" title="${r.url}">${this._truncUrl(r.url)}</span>
  1489. </div>
  1490. <div class="aio-network-details">
  1491. <span>Status: <strong style="color: ${statusColor}">${statusText}</strong></span>
  1492. ${r.duration !== undefined ? `<span>${r.duration}ms</span>` : ''}
  1493. <span>${new Date(r.timestamp).toLocaleTimeString()}</span>
  1494. <div class="aio-network-controls">
  1495. ${r.payload ? `<button class="aio-network-show-payload" data-index="${i}" title="Show/Hide Request Payload">Payload</button>` : ''}
  1496. <button class="aio-network-copy" data-index="${i}" title="Copy URL">Copy URL</button>
  1497. </div>
  1498. </div>
  1499. ${r.payload ? `
  1500. <div class="aio-network-payload" id="payload-${i}">
  1501. <pre><code>${this._escHtml(r.payload.substring(0, 5000))} ${r.payload.length > 5000 ? '...' : ''}</code></pre>
  1502. </div>` : ''}
  1503. `;
  1504. return li;
  1505. }
  1506.  
  1507.  
  1508. /* ==========================================================================
  1509. UI Updates & List Generation
  1510. ========================================================================== */
  1511.  
  1512. _addStrmLnk(type, url) {
  1513. const cont = this._pop ?.querySelector(`#${type}-content`);
  1514. if (!cont) return;
  1515.  
  1516. const list = cont.querySelector('.aio-list') || this._mkList(cont);
  1517. const es = cont.querySelector('.aio-empty');
  1518. if (es) es.remove();
  1519.  
  1520. const li = document.createElement('li');
  1521. li.className = 'aio-item aio-link';
  1522. li.dataset.url = url;
  1523. li.title = `Click to copy: ${url}`;
  1524. li.innerHTML = `
  1525. <span class="aio-item-source">${this._truncUrl(url)}</span>
  1526. <div class="aio-item-info">
  1527. <span class="timestamp">Detected: ${new Date().toLocaleTimeString()}</span>
  1528. </div>
  1529. `;
  1530. list.prepend(li);
  1531. }
  1532.  
  1533. _genIfrList(srcs) {
  1534. return `<ul class="aio-list">${srcs.map(inf => `
  1535. <li class="aio-item aio-iframe-item" data-src="${this._escHtml(inf.src)}" title="Click to copy source: ${inf.src}">
  1536. <span class="aio-item-source" title="${inf.src}">${this._truncUrl(inf.src)}</span>
  1537. <div class="aio-item-info">
  1538. <span class="aio-item-tag">${inf.type}</span>
  1539. ${inf.attributes.map(a => `<span class="aio-item-tag" title="${a}">${a.substring(0, 40)}${a.length > 40 ? '...' : ''}</span>`).join('')}
  1540. </div>
  1541. </li>`).join('')}</ul>`;
  1542. }
  1543.  
  1544. _genImgList(srcs) {
  1545. return `<ul class="aio-list">${srcs.map((inf, i) => `
  1546. <li class="aio-item aio-image-item" data-src="${inf.src}">
  1547. <span class="aio-item-source aio-image-truncated" title="${inf.src}">${this._truncUrl(inf.src)}</span>
  1548. <div class="aio-item-info">
  1549. <span class="aio-item-tag">${inf.type}</span>
  1550. ${inf.attributes.map(a => `<span class="aio-item-tag" title="${a}">${a.substring(0, 40)}${a.length > 40 ? '...' : ''}</span>`).join('')}
  1551. <div class="aio-image-button-group">
  1552. <button class="aio-image-copy" data-index="${i}" title="Copy Image URL">Copy</button>
  1553. <button class="aio-image-view" data-index="${i}" title="Open Image in New Tab">View</button>
  1554. <button class="aio-image-download" data-index="${i}" title="Download Image">Download</button>
  1555. </div>
  1556. </div>
  1557. </li>`).join('')}</ul>`;
  1558. }
  1559.  
  1560. _genEmpty(type) {
  1561. return `<div class="aio-empty">No ${type} found yet...</div>`;
  1562. }
  1563.  
  1564. _mkList(cont) {
  1565. const list = document.createElement('ul');
  1566. list.className = 'aio-list';
  1567. cont.innerHTML = '';
  1568. cont.appendChild(list);
  1569. return list;
  1570. }
  1571.  
  1572. _clrAll() {
  1573. if (!this._pop) {
  1574. this._ensureUI();
  1575. return;
  1576. }
  1577. console.log("AIO Scanner: Clearing all data.");
  1578.  
  1579. this._cfg.linkTypes.forEach(t => {
  1580. const c = this._pop.querySelector(`#${t}-content`);
  1581. if (c) c.innerHTML = `<div class="aio-empty">No ${t.toUpperCase()} files detected yet...</div>`;
  1582. });
  1583.  
  1584. const sels = [this._cfg.selectors.iframe, this._cfg.selectors.network, this._cfg.selectors.image];
  1585. const types = ['iframes', 'network requests', 'images'];
  1586. sels.forEach((sel, i) => {
  1587. const c = this._pop.querySelector(sel);
  1588. if (c) c.innerHTML = this._genEmpty(types[i]);
  1589. });
  1590.  
  1591. this._urls.clear();
  1592. this._reqs = [];
  1593.  
  1594. if (this._netFltIn) this._netFltIn.value = '';
  1595. if (this._imgFltIn) this._imgFltIn.value = '';
  1596. this._netFiltApp = false;
  1597. }
  1598.  
  1599.  
  1600. /* ==========================================================================
  1601. Utility Helpers
  1602. ========================================================================== */
  1603.  
  1604. _absUrl(u) {
  1605. if (typeof u !== 'string') return null;
  1606. if (u.startsWith('http:') || u.startsWith('https:') || u.startsWith('//') ||
  1607. u.startsWith('data:') || u.startsWith('blob:') || u.startsWith('about:')) {
  1608. return u;
  1609. }
  1610. try {
  1611. if (u.startsWith('//')) {
  1612. return window.location.protocol + u;
  1613. }
  1614. return new URL(u, document.baseURI).href;
  1615. } catch (e) {
  1616. return null;
  1617. }
  1618. }
  1619.  
  1620. _clnUrl(u) {
  1621. if (typeof u !== 'string') return '';
  1622. return u.split(/[?#]/)[0];
  1623. }
  1624.  
  1625. _truncUrl(u, maxL = this._maxUrlLen) {
  1626. if (typeof u !== 'string' || u.length <= maxL) {
  1627. return u || '';
  1628. }
  1629.  
  1630. try {
  1631. const uo = new URL(u);
  1632. const o = uo.origin;
  1633. const p = uo.pathname;
  1634. const s = uo.search ? '?...' : '';
  1635. const h = uo.hash ? '#...' : '';
  1636.  
  1637. const lo = o.length;
  1638. const ls = s.length;
  1639. const lh = h.length;
  1640. const availP = maxL - lo - ls - lh - 5;
  1641.  
  1642. if (availP <= 5) {
  1643. if (lo + ls + lh > maxL) {
  1644. return o.substring(0, maxL - 3) + '...';
  1645. }
  1646. return o + '/...' + s + h;
  1647. }
  1648.  
  1649. const segs = p.split('/').filter(Boolean);
  1650. const fname = segs.pop() || '';
  1651.  
  1652. if (fname.length >= availP) {
  1653. const half = Math.floor(availP / 2) - 1;
  1654. if (half > 0) {
  1655. const truncF = fname.substring(0, half) + '...' + fname.substring(fname.length - half);
  1656. return `${o}/.../${truncF}${s}${h}`;
  1657. } else {
  1658. return `${o}/.../...${s}${h}`;
  1659. }
  1660. } else {
  1661. let truncP = fname;
  1662. let curLen = fname.length;
  1663. while (segs.length > 0) {
  1664. const next = segs.pop();
  1665. if (curLen + next.length + 1 <= availP) {
  1666. truncP = next + '/' + truncP;
  1667. curLen += next.length + 1;
  1668. } else {
  1669. break;
  1670. }
  1671. }
  1672. return `${o}/${segs.length > 0 ? '.../' : ''}${truncP}${s}${h}`;
  1673. }
  1674. } catch (e) {
  1675. const half = Math.floor(maxL / 2) - 2;
  1676. if (half <= 0) return u.substring(0, maxL - 3) + '...';
  1677. const start = u.substring(0, half);
  1678. const end = u.substring(u.length - half);
  1679. return `${start}...${end}`;
  1680. }
  1681. }
  1682.  
  1683. async _cpClip(txt) {
  1684. if (!txt) return false;
  1685. let ok = false;
  1686. try {
  1687. if (navigator.clipboard && window.isSecureContext) {
  1688. await navigator.clipboard.writeText(txt);
  1689. ok = true;
  1690. } else {
  1691. ok = this._cpFb(txt);
  1692. }
  1693. } catch (err) {
  1694. console.warn("AIO Scanner: Clipboard write failed, trying fallback.", err);
  1695. ok = this._cpFb(txt);
  1696. }
  1697. return ok;
  1698. }
  1699.  
  1700. _cpFb(txt) {
  1701. const ta = document.createElement('textarea');
  1702. ta.value = txt;
  1703. ta.style.position = 'fixed';
  1704. ta.style.left = '-9999px';
  1705. ta.style.top = '0px';
  1706. ta.setAttribute('readonly', '');
  1707. document.body.appendChild(ta);
  1708. let ok = false;
  1709. try {
  1710. ta.select();
  1711. ta.setSelectionRange(0, ta.value.length);
  1712. ok = document.execCommand('copy');
  1713. } catch (err) {
  1714. console.error("AIO Scanner: Fallback copy (execCommand) failed.", err);
  1715. }
  1716. document.body.removeChild(ta);
  1717. return ok;
  1718. }
  1719.  
  1720. async _cpHlp(txt, el) {
  1721. const ok = await this._cpClip(txt);
  1722. this._flashItm(el, ok);
  1723. }
  1724.  
  1725. _flashItm(el, ok) {
  1726. if (!el || typeof el.style === 'undefined') return;
  1727. const oBg = el.style.backgroundColor;
  1728. const oBd = el.style.borderColor;
  1729. el.style.transition = 'background-color 0.1s ease, border-color 0.1s ease';
  1730. if (ok) {
  1731. el.style.backgroundColor = 'rgba(50, 205, 50, 0.3)';
  1732. el.style.borderColor = 'rgba(50, 205, 50, 0.5)';
  1733. } else {
  1734. el.style.backgroundColor = 'rgba(255, 0, 0, 0.3)';
  1735. el.style.borderColor = 'rgba(255, 0, 0, 0.5)';
  1736. }
  1737. setTimeout(() => {
  1738. if (el) {
  1739. el.style.transition = '';
  1740. el.style.backgroundColor = oBg;
  1741. el.style.borderColor = oBd;
  1742. }
  1743. }, 600);
  1744. }
  1745.  
  1746. _flashBtn(btn, ok, okTxt, origTxt) {
  1747. if (!btn) return;
  1748. const oCont = origTxt || btn.textContent;
  1749. btn.disabled = true;
  1750. btn.classList.remove('copied');
  1751.  
  1752. if (ok) {
  1753. btn.textContent = okTxt;
  1754. btn.classList.add('copied');
  1755. } else {
  1756. btn.textContent = 'Error';
  1757. }
  1758.  
  1759. setTimeout(() => {
  1760. if (btn) {
  1761. btn.textContent = oCont;
  1762. btn.classList.remove('copied');
  1763. btn.disabled = false;
  1764. }
  1765. }, 1500);
  1766. }
  1767.  
  1768. _dbnc(fn, wait) {
  1769. let t;
  1770. return (...args) => {
  1771. clearTimeout(t);
  1772. t = setTimeout(() => {
  1773. try {
  1774. fn.apply(this, args)
  1775. } catch (e) {
  1776. console.error("AIO Scanner: Error in debounced function:", e);
  1777. }
  1778. }, wait);
  1779. };
  1780. }
  1781.  
  1782. _monSetAttr() {
  1783. if (window.aioSetAttrMon) return;
  1784. try {
  1785. const oS = Element.prototype.setAttribute;
  1786. Element.prototype.setAttribute = function(n, v) {
  1787. const r = oS.apply(this, arguments);
  1788. if ((n === 'src' || n === 'style' || n.startsWith('data-')) &&
  1789. typeof v === 'string' && window.allInOneScanner) {
  1790. window.allInOneScanner._debScanAll();
  1791. }
  1792. return r;
  1793. };
  1794. window.aioSetAttrMon = true;
  1795. console.log("AIO Scanner (Short): setAttribute monitor attached.");
  1796. } catch (err) {
  1797. console.error("AIO Scanner: Failed to monitor setAttribute.", err);
  1798. }
  1799. }
  1800.  
  1801. _hdrsToObj(h) {
  1802. const o = {};
  1803. if (h && typeof h.forEach === 'function') {
  1804. try {
  1805. h.forEach((v, k) => {
  1806. o[k] = v;
  1807. });
  1808. } catch (e) {
  1809. console.warn("AIO Scanner: Error converting Headers object:", e);
  1810. }
  1811. }
  1812. return o;
  1813. }
  1814.  
  1815. _parseHdrs(hStr) {
  1816. const h = {};
  1817. if (!hStr) return h;
  1818. try {
  1819. const ps = hStr.trim().split(/[\r\n]+/);
  1820. for (const p of ps) {
  1821. const i = p.indexOf(':');
  1822. if (i > 0) {
  1823. const k = p.substring(0, i).trim().toLowerCase();
  1824. const v = p.substring(i + 1).trim();
  1825. if (h[k]) {
  1826. if (Array.isArray(h[k])) {
  1827. h[k].push(v);
  1828. } else {
  1829. h[k] = [h[k], v];
  1830. }
  1831. } else {
  1832. h[k] = v;
  1833. }
  1834. }
  1835. }
  1836. } catch (e) {
  1837. console.warn("AIO Scanner: Error parsing header string:", e);
  1838. }
  1839. return h;
  1840. }
  1841.  
  1842. _escHtml(u) {
  1843. if (!u || typeof u !== 'string') return String(u);
  1844. return u.replace(/&/g, "&")
  1845. .replace(/</g, "<")
  1846. .replace(/>/g, ">")
  1847. .replace(/"/g, '"')
  1848. .replace(/'/g, "'");
  1849. }
  1850.  
  1851. async _dlImg(u, fn) {
  1852. try {
  1853. const r = await fetch(u, {
  1854. mode: 'cors',
  1855. credentials: 'omit'
  1856. });
  1857. if (!r.ok) throw new Error(`Fetch failed: ${r.status} ${r.statusText}`);
  1858. const b = await r.blob();
  1859.  
  1860. const bU = URL.createObjectURL(b);
  1861.  
  1862. const a = document.createElement('a');
  1863. a.href = bU;
  1864. a.download = fn || 'image';
  1865. document.body.appendChild(a);
  1866. a.click();
  1867.  
  1868. setTimeout(() => {
  1869. if (document.body.contains(a)) {
  1870. document.body.removeChild(a);
  1871. }
  1872. URL.revokeObjectURL(bU);
  1873. }, 100);
  1874.  
  1875. } catch (err) {
  1876. console.error(`AIO Scanner: Image download failed for ${u}. Error: ${err.message}. Trying fallback.`);
  1877. try {
  1878. const nt = window.open(u, '_blank');
  1879. if (!nt) {
  1880. alert(`Cannot open image (popup blocker?). Copy URL manually.\n\nURL: ${u}`);
  1881. }
  1882. } catch (openErr) {
  1883. alert(`Failed to download or open image.\nURL: ${u}\nError: ${err.message}`);
  1884. }
  1885. }
  1886. }
  1887.  
  1888. } // End of AioScanner class
  1889.  
  1890. // --- Initialization ---
  1891. if (!window.aioScannerInstanceShort) {
  1892. const init = () => {
  1893. if (!window.aioScannerInstanceShort && document.body) {
  1894. try {
  1895. console.log("AIO Scanner (Short): Initializing...");
  1896. window.aioScannerInstanceShort = new AioScanner();
  1897. } catch (err) {
  1898. console.error("AIO Scanner (Short): CRITICAL INITIALIZATION ERROR:", err);
  1899. }
  1900. } else if (!document.body) {
  1901. // Wait for body
  1902. } else {
  1903. // Instance exists
  1904. }
  1905. };
  1906.  
  1907. if (document.readyState === 'loading') {
  1908. document.addEventListener('DOMContentLoaded', init);
  1909. } else {
  1910. setTimeout(init, 50);
  1911. }
  1912. }
  1913.  
  1914. })();