1/** 2 * IMEI Scanner â Barcode + OCR from camera/photo 3 * Strategy: barcode-detector polyfill first (fast) â OCR fallback (slower) 4 * Attaches to existing input #imeiFld 5 * Dependencies: All lazy-loaded from CDN only when needed 6 */ 7(function () { 8 'use strict'; 9 10 // ââ Config ââ 11 var INPUT_ID = 'imeiFld'; 12 var TESSERACT_CDN = 'https://cdnjs.cloudflare.com/ajax/libs/tesseract.js/5.0.4/tesseract.min.js'; 13 var BARCODE_CDN = 'https://fastly.jsdelivr.net/npm/barcode-detector@3/dist/iife/index.min.js'; 14 15 var _worker = null; 16 var _workerReady = false; 17 var _selectedIMEI = null; 18 var _barcodeDetector = null; 19 var _barcodeLoading = null; 20 21 // ââ Validation â 22 function luhn(imei) { 23 if (!/^\d{15}$/.test(imei)) return false; 24 var s = 0; 25 for (var i = 0; i < 15; i++) { 26 var d = +imei[i]; 27 if (i % 2 === 1) { d *= 2; if (d > 9) d -= 9; } 28 s += d; 29 } 30 return s % 10 === 0; 31 } 32 33 function validPrefix(imei) { 34 return /^(35|01|86)/.test(imei); 35 } 36 37 function fmtIMEI(imei) { 38 return imei.replace(/(\d{2})(\d{6})(\d{6})(\d)/, '$1 $2 $3 $4'); 39 } 40 41 // ââ OCR character fix (only between digits) ââ 42 function ocrFix(text) { 43 return text 44 .replace(/(\d)%(\d)/g, '$19$2') 45 .replace(/(\d)[oO](\d)/g, '$10$2') 46 .replace(/(\d)[lI|](\d)/g, '$11$2') 47 .replace(/(\d)[Ss](\d)/g, '$15$2') 48 .replace(/(\d)[Bb](\d)/g, '$18$2') 49 .replace(/(\d)[Gg](\d)/g, '$19$2') 50 .replace(/(\d)[Zz](\d)/g, '$12$2'); 51 } 52 53 // ââ IMEI extraction from text ââ 54 function extractIMEIs(text) { 55 var results = new Map(); 56 var lines = text.split(/[\n\r]+/); 57 for (var li = 0; li < lines.length; li++) { 58 var line = ocrFix(lines[li]); 59 var groups = line.match(/\d+/g); 60 if (!groups) continue; 61 for (var gi = 0; gi < groups.length; gi++) { 62 if (groups[gi].length === 15 && validPrefix(groups[gi]) && luhn(groups[gi])) { 63 results.set(groups[gi], true); 64 } 65 } 66 for (var start = 0; start < groups.length; start++) { 67 var combined = ''; 68 for (var end = start; end < groups.length; end++) { 69 combined += groups[end]; 70 if (combined.length > 20) break; 71 if (combined.length >= 15) { 72 for (var i = 0; i <= combined.length - 15; i++) { 73 var c = combined.substring(i, i + 15); 74 if (validPrefix(c) && luhn(c)) results.set(c, true); 75 } 76 break; 77 } 78 } 79 } 80 } 81 var out = []; 82 results.forEach(function (_, imei) { out.push(imei); }); 83 return out; 84 } 85 86 // ââ Extract IMEI from barcode raw value ââ 87 function extractIMEIsFromBarcode(rawValue) { 88 if (!rawValue) return []; 89 var digits = rawValue.replace(/\D/g, ''); 90 var found = []; 91 if (digits.length === 15 && validPrefix(digits) && luhn(digits)) { 92 found.push(digits); 93 } 94 if (digits.length > 15) { 95 for (var i = 0; i <= digits.length - 15; i++) { 96 var c = digits.substring(i, i + 15); 97 if (validPrefix(c) && luhn(c) && found.indexOf(c) === -1) found.push(c); 98 } 99 } 100 return found; 101 } 102 103 // âââââââââââââââââââââââââââââââââââââââââ
104 // ââ PHASE 1: BARCODE (barcode-detector polyfill) ââ 105 // âââââââââââââââââââââââââââââââââââââââââ 106 107 function loadBarcodeDetector() { 108 if (_barcodeDetector) return Promise.resolve(true); 109 if (_barcodeLoading) return _barcodeLoading; 110 _barcodeLoading = new Promise(function (resolve) { 111 // Load polyfill script first 112 var s = document.createElement('script'); 113 s.src = BARCODE_CDN; 114 s.onload = function () { 115 try { 116 // Always prefer polyfill (zxing-wasm) â native BarcodeDetector is unreliable 117 var BD = (window.BarcodeDetectionAPI && window.BarcodeDetectionAPI.BarcodeDetector) || window.BarcodeDetector; 118 if (BD) { 119 _barcodeDetector = new BD({ 120 formats: ['code_128', 'code_39', 'code_93', 'ean_13', 'ean_8', 'itf', 'codabar', 'upc_a', 'upc_e'] 121 }); 122 console.log('[IMEI Scanner] Using barcode-detector polyfill'); 123 // Warmup: trigger WASM load so first real scan is instant 124 var wc = document.createElement('canvas'); 125 wc.width = 1; wc.height = 1; 126 _barcodeDetector.detect(wc).then(function () { 127 resolve(true); 128 }).catch(function () { 129 resolve(true); // warmup error is fine, detector still works 130 }); 131 } else {
132 console.warn('[IMEI Scanner] barcode-detector loaded but no BarcodeDetector found'); 133 resolve(false); 134 } 135 } catch (e) { 136 console.error('[IMEI Scanner] BarcodeDetector init error:', e); 137 resolve(false); 138 } 139 }; 140 s.onerror = function () { 141 console.warn('[IMEI Scanner] Failed to load barcode-detector CDN'); 142 resolve(false); 143 }; 144 document.head.appendChild(s); 145 }); 146 return _barcodeLoading; 147 } 148 149 async function scanBarcode(imgElement) { 150 if (!_barcodeDetector) return []; 151 try { 152 var barcodes = await _barcodeDetector.detect(imgElement); 153 console.log('[IMEI Scanner] Barcodes found:', barcodes.length, barcodes.map(function(b){ return b.format + ':' + b.rawValue; })); 154 var imeis = []; 155 for (var i = 0; i < barcodes.length; i++) { 156 var found = extractIMEIsFromBarcode(barcodes[i].rawValue); 157 for (var j = 0; j < found.length; j++) { 158 if (imeis.indexOf(found[j]) === -1) imeis.push(found[j]); 159 } 160 } 161 return imeis; 162 } catch (e) { 163 console.error('[IMEI Scanner] Barcode detect error:', e); 164 return []; 165 } 166 } 167 168 // âââââââââââââââââââââââââââââââââââââââââ 169 // ââ PHASE 2: OCR FALLBACK ââ 170 // âââââââââââââââââââââââââââââââââââââââââ 171 172 function toContrast(source) { 173 var c = document.createElement('canvas'); 174 c.width = source.width; c.height = source.height; 175 var ctx = c.getContext('2d'); ctx.drawImage(source, 0, 0); 176 var img = ctx.getImageData(0, 0, c.width, c.height); var d = img.data; 177 for (var i = 0; i < d.length; i += 4) { 178 var g = .299 * d[i] + .587 * d[i + 1] + .114 * d[i + 2]; 179 g = Math.max(0, Math.min(255, ((g - 128) * 2) + 128)); 180 d[i] = d[i + 1] = d[i + 2] = g; 181 } 182 ctx.putImageData(img, 0, 0); return c; 183 } 184 185 function toBinary(source, t) { 186 var c = document.createElement('canvas'); 187 c.width = source.width; c.height = source.height; 188 var ctx = c.getContext('2d'); ctx.drawImage(source, 0, 0); 189 var img = ctx.getImageData(0, 0, c.width, c.height); var d = img.data; 190 for (var i = 0; i < d.length; i += 4) { 191 var g = .299 * d[i] + .587 * d[i + 1] + .114 * d[i + 2]; 192 g = g > t ? 255 : 0; 193 d[i] = d[i + 1] = d[i + 2] = g; 194 } 195 ctx.putImageData(img, 0, 0); return c; 196 } 197 198 function toInvertBinary(source, t) { 199 var c = document.createElement('canvas'); 200 c.width = source.width; c.height = source.height; 201 var ctx = c.getContext('2d'); ctx.drawImage(source, 0, 0); 202 var img = ctx.getImageData(0, 0, c.width, c.height); var d = img.data; 203 for (var i = 0; i < d.length; i += 4) { 204 var g = 255 - Math.round(.299 * d[i] + .587 * d[i + 1] + .114 * d[i + 2]); 205 g = g > t ? 255 : 0; 206 d[i] = d[i + 1] = d[i + 2] = g; 207 } 208 ctx.putImageData(img, 0, 0); return c; 209 } 210 211 function loadTesseract(cb) { 212 if (window.Tesseract) return cb(); 213 var s = document.createElement('script'); 214 s.src = TESSERACT_CDN; 215 s.onload = cb; 216 s.onerror = function () { cb(); }; 217 document.head.appendChild(s); 218 } 219 220 async function initOCR() { 221 if (_worker) return; 222 _worker = await Tesseract.createWorker('eng', 1, { logger: function () { } }); 223 await _worker.setParameters({ tessedit_pageseg_mode: '6' }); 224 _workerReady = true; 225 } 226 227 async function ocrScan(canvas) { 228 var foundIMEIs = new Map(); 229 var variants = [ 230 { msg: 'Reading label...', fn: function (s) { return toBinary(s, 150); } }, 231 { msg: 'Adjusting brightness...', fn: toContrast }, 232 { msg: 'Trying harder...', fn: function (s) { return toInvertBinary(s, 80); } } 233 ]; 234 for (var i = 0; i < variants.length; i++) { 235 setLoadingText(variants[i].msg); 236 try { 237 var processed = variants[i].fn(canvas); 238 var r = await _worker.recognize(processed); 239 var text = r.data.text || ''; 240 if (text.trim()) { 241 var imeis = extractIMEIs(text); 242 for (var j = 0; j < imeis.length; j++) { 243 var ex = foundIMEIs.get(imeis[j]); 244 foundIMEIs.set(imeis[j], (ex || 0) + 1); 245 } 246 if (foundIMEIs.size > 0) break; 247 } 248 } catch (e) { } 249 } 250 var out = []; 251 foundIMEIs.forEach(function (count, imei) { out.push({ imei: imei, count: count }); }); 252 return out; 253 } 254 255 // âââââââââââââââââââââââââââââââââââââââââ 256 // ââ MAIN PROCESSING PIPELINE ââ 257 // ââââââââââââââââââââââââââââââââââââââââââ 258 259 async function processPhoto(file) { 260 showLoading('Scanning barcode...'); 261 262 // Start loading image + barcode detector + Tesseract ALL in parallel 263 var imgPromise = loadImage(file); 264 var barcodePromise = loadBarcodeDetector(); 265 var tesseractPreload = new Promise(function (resolve) { loadTesseract(resolve); }); 266 267 var imgEl = await imgPromise; 268 269 // Phase 1: Barcode scan (fast path) 270 await barcodePromise; 271 var barcodeIMEIs = await scanBarcode(imgEl); 272 273 // If barcode found 2+ IMEIs, return immediately (fast) 274 if (barcodeIMEIs.length >= 2) { 275 hideLoading(); 276 console.log('[IMEI Scanner] Found 2+ via barcode:', barcodeIMEIs); 277 return barcodeIMEIs.map(function (imei) { return { imei: imei, count: 10, source: 'barcode' }; }); 278 } 279 280 // Phase 2: OCR â either barcode found 0 or only 1 (might be missing the other) 281 if (barcodeIMEIs.length === 1) { 282 console.log('[IMEI Scanner] Barcode found 1, running OCR to check for second IMEI');
283 setLoadingText('Checking for more IMEIs...'); 284 } else { 285 console.log('[IMEI Scanner] No barcode IMEI found, falling back to OCR'); 286 setLoadingText('Almost there...'); 287 } 288 289 await tesseractPreload; 290 if (!window.Tesseract) { 291 hideLoading(); 292 // If we had 1 barcode IMEI but Tesseract failed, return what we have 293 if (barcodeIMEIs.length === 1) { 294 return barcodeIMEIs.map(function (imei) { return { imei: imei, count: 10, source: 'barcode' }; }); 295 } 296 return []; 297 } 298 if (!_workerReady) await initOCR(); 299 300 var canvas = document.createElement('canvas'); 301 var ctx = canvas.getContext('2d'); 302 var w = imgEl.naturalWidth || imgEl.width; 303 var h = imgEl.naturalHeight || imgEl.height; 304 if (w > 1200) { h = Math.round(h * 1200 / w); w = 1200; } 305 canvas.width = w; canvas.height = h; 306 ctx.drawImage(imgEl, 0, 0, w, h); 307 308 var ocrResults = await ocrScan(canvas); 309 310 // Merge barcode + OCR results, dedup by IMEI 311 var allIMEIs = new Map(); 312 for (var bi = 0; bi < barcodeIMEIs.length; bi++) { 313 allIMEIs.set(barcodeIMEIs[bi], { imei: barcodeIMEIs[bi], count: 10, source: 'barcode' }); 314 } 315 for (var oi = 0; oi < ocrResults.length; oi++) { 316 if (!allIMEIs.has(ocrResults[oi].imei)) { 317 allIMEIs.set(ocrResults[oi].imei, { imei: ocrResults[oi].imei, count: ocrResults[oi].count, source: 'ocr' }); 318 } 319 } 320 321 hideLoading(); 322 var merged = []; 323 allIMEIs.forEach(function (entry) { merged.push(entry); }); 324 return merged; 325 } 326 327 function loadImage(file) { 328 return new Promise(function (resolve, reject) { 329 var img = new Image(); 330 var url = URL.createObjectURL(file); 331 img.onload = function () { URL.revokeObjectURL(url); resolve(img); }; 332 img.onerror = function () { URL.revokeObjectURL(url); reject(new Error('Image load failed')); }; 333 img.src = url; 334 }); 335 } 336 337 // ââââââââââââââââââââââââââââââââââââââââââ 338 // ââ UI ââ 339 // âââââââââââââââââââââââââââââââââââââââââ 340 341 function injectUI() { 342 var input = document.getElementById(INPUT_ID); 343 if (!input) return; 344 345 // Find or create wrapper 346 var wrapper = input.parentNode; 347 if (!wrapper.classList || !wrapper.classList.contains('imei-scan-wrap')) { 348 var newWrapper = document.createElement('div'); 349 newWrapper.className = 'imei-scan-wrap'; 350 newWrapper.style.cssText = 'position:relative;display:flex;align-items:center;'; 351 wrapper.insertBefore(newWrapper, input); 352 newWrapper.appendChild(input); 353 wrapper = newWrapper; 354 } 355 356 // Camera button â click triggers hidden file input 357 var scanBtn = document.createElement('div'); 358 scanBtn.className = 'imei-scan-btn'; 359 scanBtn.title = 'Scan IMEI from photo'; 360 scanBtn.innerHTML = 361 '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' + 362 '<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/>' + 363 '<circle cx="12" cy="13" r="4"/></svg>'; 364 scanBtn.addEventListener('click', function () { 365 fileInput.click(); 366 }); 367 wrapper.appendChild(scanBtn); 368 369 // Hidden file input â separate from button to avoid double-click 370 var fileInput = document.createElement('input'); 371 fileInput.type = 'file'; 372 fileInput.accept = 'image/*'; 373 fileInput.setAttribute('capture', 'environment'); 374 fileInput.style.display = 'none'; 375 fileInput.addEventListener('change', function () { 376 handleFileSelect(this); 377 }); 378 wrapper.appendChild(fileInput); 379 380 var loadingEl = document.createElement('div'); 381 loadingEl.id = 'imeiScanLoading'; 382 loadingEl.className = 'imei-scan-loading'; 383 loadingEl.innerHTML = 384 '<div class="imei-scan-loading-inner">' + 385 '<div class="imei-scan-spinner"></div>' + 386 '<div class="imei-scan-loading-text">Scanning barcode...</div>' + 387 '</div>'; 388 document.body.appendChild(loadingEl); 389 390 var modalEl = document.createElement('div'); 391 modalEl.id = 'imeiScanModal'; 392 modalEl.className = 'imei-scan-modal-bg'; 393 modalEl.innerHTML = 394 '<div class="imei-scan-modal">' + 395 '<div class="imei-scan-modal-handle"></div>' + 396 '<div class="imei-scan-modal-title" id="imeiScanModalTitle">IMEI detected</div>' + 397 '<div class="imei-scan-modal-sub">Select the correct IMEI</div>' + 398 '<div id="imeiScanModalList"></div>' + 399 '<div class="imei-scan-modal-btns">' + 400 '<button type="button" class="imei-scan-mbtn imei-scan-mbtn-cancel" id="imeiScanCancel">Cancel</button>' + 401 '<button type="button" class="imei-scan-mbtn imei-scan-mbtn-use" id="imeiScanUse" disabled>Use</button>' + 402 '</div>' + 403 '</div>'; 404 document.body.appendChild(modalEl); 405 406 var toastEl = document.createElement('div'); 407 toastEl.id = 'imeiScanToast'; 408 toastEl.className = 'imei-scan-toast'; 409 document.body.appendChild(toastEl); 410 411 document.getElementById('imeiScanCancel').addEventListener('click', closePickerModal); 412 document.getElementById('imeiScanUse').addEventListener('click', useSelectedIMEI); 413 modalEl.addEventListener('click', function (e) { 414 if (e.target === modalEl) closePickerModal(); 415 });
416 } 417 418 function handleFileSelect(inputEl) { 419 var file = inputEl.files[0]; 420 if (!file) return; 421 inputEl.value = ''; 422 423 processPhoto(file).then(function (entries) { 424 if (entries.length === 1) { 425 applyIMEI(entries[0].imei); 426 if (navigator.vibrate) navigator.vibrate([40, 20, 40]); 427 showToast('IMEI detected' + (entries[0].source === 'barcode' ? ' from barcode' : '') + '!'); 428 } else if (entries.length > 1) { 429 if (navigator.vibrate) navigator.vibrate([40, 20, 40]); 430 showPickerModal(entries); 431 } else { 432 showToast('No valid IMEI detected. Try a clearer photo.'); 433 } 434 }).catch(function (err) { 435 console.error('[IMEI Scanner] processPhoto error:', err); 436 hideLoading(); 437 showToast('Error processing image. Please try again.'); 438 }); 439 } 440 441 function applyIMEI(imei) { 442 var input = document.getElementById(INPUT_ID); 443 if (input) { 444 input.value = imei; 445 input.dispatchEvent(new Event('input', { bubbles: true })); 446 input.dispatchEvent(new Event('change', { bubbles: true })); 447 input.focus(); 448 } 449 } 450 451 function showLoading(text) { 452 var el = document.getElementById('imeiScanLoading'); 453 el.querySelector('.imei-scan-loading-text').textContent = text; 454 el.classList.add('active'); 455 } 456 457 function setLoadingText(text) { 458 var el = document.getElementById('imeiScanLoading'); 459 el.querySelector('.imei-scan-loading-text').textContent = text; 460 } 461 462 function hideLoading() { 463 document.getElementById('imeiScanLoading').classList.remove('active'); 464 } 465 466 function showPickerModal(entries) { 467 var list = document.getElementById('imeiScanModalList'); 468 var btn = document.getElementById('imeiScanUse'); 469 list.innerHTML = ''; 470 _selectedIMEI = null; 471 btn.disabled = true; 472 btn.textContent = 'Use'; 473 474 entries.sort(function (a, b) { 475 if (a.imei < b.imei) return -1; 476 if (a.imei > b.imei) return 1; 477 return 0; 478 }); 479 480 document.getElementById('imeiScanModalTitle').textContent = 481 entries.length === 1 ? '1 IMEI detected' : entries.length + ' IMEIs detected'; 482 483 entries.forEach(function (item, i) { 484 var div = document.createElement('div'); 485 div.className = 'imei-scan-pick'; 486 var badge = item.source === 'barcode' 487 ? '<span class="imei-scan-pick-badge barcode">â Barcode</span>' 488 : '<span class="imei-scan-pick-badge">â Valid</span>'; 489 div.innerHTML = 490 '<span class="imei-scan-pick-num">' + fmtIMEI(item.imei) + '</span>' + badge; 491 div.addEventListener('click', function () { 492 var all = list.querySelectorAll('.imei-scan-pick'); 493 for (var k = 0; k < all.length; k++) all[k].classList.remove('selected'); 494 div.classList.add('selected'); 495 _selectedIMEI = item.imei; 496 btn.disabled = false; 497 btn.textContent = 'Use ' + fmtIMEI(item.imei); 498 }); 499 if (i === 0) { 500 div.classList.add('selected'); 501 _selectedIMEI = item.imei; 502 btn.disabled = false; 503 btn.textContent = 'Use ' + fmtIMEI(item.imei); 504 } 505 list.appendChild(div); 506 }); 507 508 if (entries.length >= 2) { 509 var tip = document.createElement('div'); 510 tip.className = 'imei-scan-tip'; 511 tip.innerHTML = '<strong>Tip:</strong> For dual-IMEI devices, use <strong>IMEI 1</strong> for checks, especially blacklist checks.'; 512 list.appendChild(tip); 513 } 514 515 document.getElementById('imeiScanModal').classList.add('active'); 516 } 517 518 function closePickerModal() { 519 document.getElementById('imeiScanModal').classList.remove('active'); 520 _selectedIMEI = null; 521 } 522 523 function useSelectedIMEI() { 524 if (!_selectedIMEI) return; 525 applyIMEI(_selectedIMEI); 526 closePickerModal(); 527 } 528 529 function showToast(msg) { 530 var t = document.getElementById('imeiScanToast'); 531 t.textContent = msg; 532 t.classList.add('show'); 533 setTimeout(function () { t.classList.remove('show'); }, 3500); 534 } 535
536 function init() { 537 if (!document.getElementById(INPUT_ID)) return; 538 if (document.querySelector('.imei-scan-btn')) return; // already initialized 539 injectCSS(); 540 injectUI(); 541 // Preload barcode detector + warmup WASM in background 542 loadBarcodeDetector(); 543 } 544 545 function injectCSS() { 546 if (document.getElementById('imeiScanStyles')) return; 547 var style = document.createElement('style'); 548 style.id = 'imeiScanStyles'; 549 style.textContent = '' + 550 '.imei-scan-btn{' + 551 'position:absolute;right:6px;top:50%;transform:translateY(-50%);' + 552 'width:38px;height:38px;display:none;align-items:center;justify-content:center;' + 553 'border-radius:8px;cursor:pointer;color:#888;transition:all .15s;overflow:hidden;z-index:2;' + 554 '}' + 555 '.imei-scan-btn:hover{color:#007bff;background:rgba(0,123,255,.08);}' + 556 '.imei-scan-btn:active{transform:translateY(-50%) scale(.9);}' + 557 '.imei-scan-btn svg{width:20px;height:20px;pointer-events:none;}' + 558 '@media(pointer:coarse){.imei-scan-btn{display:flex;}#' + INPUT_ID + '{padding-right:48px!important;}}' + 559 560 '.imei-scan-loading{' + 561 'display:none;position:fixed;inset:0;z-index:10000;' + 562 'background:rgba(255,255,255,.88);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);' + 563 'align-items:center;justify-content:center;' + 564 '}' + 565 '.imei-scan-loading.active{display:flex;}' + 566 '.imei-scan-loading-inner{text-align:center;}' + 567 '.imei-scan-spinner{' + 568 'width:36px;height:36px;margin:0 auto 14px;' + 569 'border:3px solid #e0e0e0;border-top-color:#007bff;border-radius:50%;' + 570 'animation:imeiScanSpin .8s linear infinite;' + 571 '}' + 572 '@keyframes imeiScanSpin{to{transform:rotate(360deg)}}' + 573 '.imei-scan-loading-text{font-size:14px;color:#888;font-weight:500;}' + 574 575 '.imei-scan-modal-bg{' + 576 'display:none;position:fixed;inset:0;z-index:10001;' + 577 'background:rgba(0,0,0,.45);backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);' + 578 'align-items:flex-end;justify-content:center;' + 579 '}' + 580 '.imei-scan-modal-bg.active{display:flex;}' + 581 '.imei-scan-modal{' + 582 'background:#fff;width:100%;max-width:480px;' + 583 'border-radius:18px 18px 0 0;padding:20px 18px calc(28px + env(safe-area-inset-bottom, 0px));' + 584 'max-height:70vh;overflow-y:auto;' + 585 'animation:imeiScanSlideUp .35s cubic-bezier(.2,.8,.2,1);' + 586 '}' + 587 '@keyframes imeiScanSlideUp{from{transform:translateY(100%)}to{transform:translateY(0)}}' + 588 '.imei-scan-modal-handle{width:36px;height:4px;background:#ddd;border-radius:2px;margin:0 auto 16px;}' + 589 '.imei-scan-modal-title{font-size:17px;font-weight:700;margin-bottom:4px;color:#222;}' + 590 '.imei-scan-modal-sub{font-size:13px;color:#999;margin-bottom:14px;}' + 591 592 '.imei-scan-pick{' + 593 'background:#f8f8fa;border:1.5px solid #e5e5ea;border-radius:12px;' + 594 'padding:16px 16px;margin-bottom:8px;display:flex;align-items:center;justify-content:space-between;' + 595 'cursor:pointer;transition:all .15s;min-height:54px;' + 596 '}' + 597 '.imei-scan-pick:active{transform:scale(.98);}' + 598 '.imei-scan-pick.selected{border-color:#007bff;background:rgba(0,123,255,.06);}' + 599 '.imei-scan-pick-num{' + 600 'font-family:"Consolas","Monaco",monospace;font-size:16px;font-weight:600;letter-spacing:.04em;color:#222;' + 601 '}' + 602 '.imei-scan-pick-badge{' + 603 'font-size:11px;font-weight:600;padding:3px 10px;border-radius:6px;' + 604 'background:rgba(52,199,89,.1);color:#34c759;' + 605 '}' + 606 '.imei-scan-pick-badge.barcode{background:rgba(0,123,255,.1);color:#007bff;}' + 607 608 '.imei-scan-modal-btns{display:flex;gap:10px;margin-top:14px;}' + 609 610 '.imei-scan-tip{' + 611 'font-size:11px;color:#999;line-height:1.5;padding:8px 12px;margin-top:4px;' + 612 'background:rgba(0,123,255,.04);border-radius:8px;border-left:2px solid rgba(0,123,255,.3);' + 613 '}' +
614 '.imei-scan-tip strong{color:#777;}' + 615 '.imei-scan-mbtn{' + 616 'flex:1;padding:15px;border:none;border-radius:12px;' + 617 'font-size:15px;font-weight:600;cursor:pointer;transition:transform .1s;font-family:inherit;min-height:50px;' + 618 '}' + 619 '.imei-scan-mbtn:active{transform:scale(.97);}' + 620 '.imei-scan-mbtn-cancel{background:#f0f0f2;color:#888;}' + 621 '.imei-scan-mbtn-use{background:#007bff;color:#fff;}' + 622 '.imei-scan-mbtn-use:disabled{opacity:.4;}' + 623 624 '.imei-scan-toast{' + 625 'position:fixed;top:calc(20px + env(safe-area-inset-top, 0px));left:50%;transform:translateX(-50%) translateY(-80px);' + 626 'background:#333;color:#fff;padding:12px 24px;border-radius:12px;' + 627 'font-size:14px;font-weight:500;z-index:99999;opacity:0;' + 628 'transition:all .4s cubic-bezier(.2,.8,.2,1);pointer-events:none;' + 629 'box-shadow:0 4px 20px rgba(0,0,0,.15);max-width:90%;text-align:center;' + 630 '}' + 631 '.imei-scan-toast.show{opacity:1;transform:translateX(-50%) translateY(0);}' + 632 633 'ins.adsbygoogle,iframe[id^="aswift"],iframe[id^="google_ads"]{z-index:1!important;}' + 634 ''; 635 document.head.appendChild(style); 636 } 637 638 if (document.readyState === 'loading') { 639 document.addEventListener('DOMContentLoaded', init); 640 } else { 641 init(); 642 } 643 644})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.