1<!DOCTYPE html> 2<html lang="ru"> 3<head> 4<meta charset="UTF-8"> 5<meta name="viewport" content="width=device-width, initial-scale=1.0"> 6<title>ÐидеоконвеÑÑеÑ</title> 7<style> 8* { margin: 0; padding: 0; box-sizing: border-box; } 9body { 10 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 11 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 12 min-height: 100vh; 13 padding: 20px; 14} 15.container { max-width: 800px; margin: 0 auto; } 16.card { 17 background: white; 18 border-radius: 20px; 19 padding: 40px; 20 box-shadow: 0 20px 60px rgba(0,0,0,0.3); 21 margin-bottom: 20px; 22} 23h1 { 24 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 25 -webkit-background-clip: text; 26 -webkit-text-fill-color: transparent; 27 font-size: 32px; 28 margin-bottom: 10px; 29} 30.subtitle { color: #666; margin-bottom: 30px; } 31 32/* Drop-zone */ 33.drop-zone { 34 border: 3px dashed #cbd5e0; 35 border-radius: 16px; 36 padding: 60px 30px; 37 text-align: center; 38 cursor: pointer; 39 transition: all 0.3s; 40 background: #f7fafc; 41} 42.drop-zone:hover, .drop-zone.dragover { 43 border-color: #667eea; 44 background: #edf2ff; 45} 46.drop-zone .icon { font-size: 64px; margin-bottom: 15px; } 47.drop-zone .hint { color: #718096; font-size: 14px; margin-top: 8px; } 48 49/* ÐнÑоÑмаÑÐ¸Ñ Ð¾ Ñайле */ 50.file-info { 51 display: none; 52 background: #f0f4ff; 53 border-radius: 12px; 54 padding: 20px; 55 margin-top: 20px; 56} 57.file-info.show { display: block; } 58.file-info h3 { color: #4c51bf; margin-bottom: 15px; font-size: 16px; } 59.info-grid { 60 display: grid; 61 grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); 62 gap: 12px; 63} 64.info-item { 65 background: white; 66 border-radius: 8px; 67 padding: 10px 14px; 68} 69.info-item .label { font-size: 11px; color: #718096; text-transform: uppercase; } 70.info-item .value { font-size: 15px; font-weight: 600; color: #2d3748; margin-top: 4px; } 71 72/* СекÑии */ 73.section { margin-top: 25px; } 74.section h3 { font-size: 16px; color: #2d3748; margin-bottom: 12px; } 75.section .desc { font-size: 13px; color: #718096; margin-bottom: 12px; } 76 77/* ÐаÑеÑÑва */ 78.quality-grid { 79 display: grid; 80 grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); 81 gap: 10px; 82} 83.quality-option { 84 padding: 15px; 85 border: 2px solid #e2e8f0; 86 border-radius: 12px; 87 cursor: pointer; 88 text-align: center; 89 transition: all 0.2s; 90 background: white; 91} 92.quality-option:hover:not(.disabled) { 93 border-color: #667eea; 94 background: #f0f4ff; 95} 96.quality-option.selected { 97 border-color: #667eea; 98 background: linear-gradient(135deg, rgba(102, 126, 234, 0.15) 0%, rgba(118, 75, 162, 0.15) 100%); 99} 100.quality-option.disabled { 101 opacity: 0.4; 102 cursor: not-allowed; 103 background: #f7fafc; 104} 105.quality-option .qname { font-weight: 600; font-size: 16px; color: #2d3748; } 106.quality-option .qres { font-size: 11px; color: #718096; margin-top: 4px; } 107.quality-option .qbit { font-size: 10px; color: #a0aec0; margin-top: 2px; } 108 109/* ФоÑмаÑÑ */ 110.format-list { display: flex; flex-direction: column; gap: 8px; } 111.format-option { 112 display: flex;
113 align-items: center; 114 padding: 12px 16px; 115 border: 2px solid #e2e8f0; 116 border-radius: 10px; 117 cursor: pointer; 118 transition: all 0.2s; 119} 120.format-option:hover { border-color: #667eea; background: #f0f4ff; } 121.format-option.selected { 122 border-color: #667eea; 123 background: linear-gradient(135deg, rgba(102, 126, 234, 0.15) 0%, rgba(118, 75, 162, 0.15) 100%); 124} 125.format-option input { margin-right: 12px; } 126 127/* ÐиÑÑate */ 128.bitrate-slider { 129 width: 100%; 130 margin-top: 8px; 131} 132.bitrate-values { 133 display: flex; 134 justify-content: space-between; 135 font-size: 11px; 136 color: #718096; 137 margin-top: 4px; 138} 139.bitrate-current { 140 text-align: center; 141 font-weight: 600; 142 color: #667eea; 143 font-size: 15px; 144 margin-top: 8px; 145} 146 147/* ЧекбокÑÑ */ 148.checkbox-row { 149 display: flex; 150 align-items: center; 151 padding: 10px 0; 152 cursor: pointer; 153} 154.checkbox-row input { margin-right: 10px; } 155 156/* Ðнопка */ 157.btn { 158 width: 100%; 159 padding: 16px; 160 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 161 color: white; 162 border: none; 163 border-radius: 12px; 164 font-size: 16px; 165 font-weight: 600; 166 cursor: pointer; 167 transition: all 0.2s; 168 margin-top: 25px; 169} 170.btn:hover:not(:disabled) { 171 transform: translateY(-2px); 172 box-shadow: 0 10px 25px rgba(102, 126, 234, 0.4); 173} 174.btn:disabled { opacity: 0.5; cursor: not-allowed; } 175 176/* ÐÑогÑеÑÑ */ 177.progress-section { display: none; margin-top: 25px; } 178.progress-section.show { display: block; } 179.progress-bar-wrap { 180 height: 12px; 181 background: #e2e8f0; 182 border-radius: 6px; 183 overflow: hidden; 184 margin: 10px 0; 185} 186.progress-bar-fill { 187 height: 100%; 188 background: linear-gradient(90deg, #667eea, #764ba2); 189 width: 0%; 190 transition: width 0.3s; 191 border-radius: 6px; 192} 193.progress-status { 194 font-size: 13px; 195 color: #4a5568; 196 margin-top: 8px; 197} 198.progress-percent { 199 font-size: 20px; 200 font-weight: 700; 201 color: #667eea; 202 text-align: center; 203 margin-bottom: 8px; 204} 205 206/* Ðог */ 207.log-box { 208 background: #1a202c; 209 color: #68d391; 210 font-family: 'Courier New', monospace; 211 font-size: 11px; 212 padding: 15px; 213 border-radius: 10px; 214 max-height: 200px; 215 overflow-y: auto; 216 margin-top: 15px; 217 white-space: pre-wrap; 218 word-wrap: break-word; 219} 220 221/* РезÑлÑÑÐ°Ñ */ 222.result-section { display: none; text-align: center; padding: 20px; } 223.result-section.show { display: block; } 224.result-section h2 { color: #38a169; margin-bottom: 15px; } 225.download-btn { 226 display: inline-block; 227 padding: 15px 40px; 228 background: #38a169; 229 color: white; 230 text-decoration: none; 231 border-radius: 12px; 232 font-weight: 600; 233 margin-top: 15px; 234 transition: all 0.2s; 235} 236.download-btn:hover { 237 background: #2f855a; 238 transform: translateY(-2px); 239 box-shadow: 0 10px 25px rgba(56, 161, 105, 0.4); 240} 241 242/* ÐÑибка */ 243.error-box { 244 display: none; 245 background: #fed7d7; 246 color: #c53030; 247 padding: 15px; 248 border-radius: 10px; 249 margin-top: 15px; 250} 251.error-box.show { display: block; } 252 253@media (max-width: 600px) { 254 .card { padding: 20px; } 255 h1 { font-size: 24px; } 256 .quality-grid { grid-template-columns: repeat(2, 1fr); } 257} 258</style> 259</head> 260<body> 261<div class="container"> 262 <div class="card"> 263 <h1>ð¬ ÐидеоконвеÑÑеÑ</h1> 264 <p class="subtitle">ÐагÑÑзиÑе видео, вÑбеÑиÑе каÑеÑÑво и ÑоÑÐ¼Ð°Ñ â полÑÑиÑе гоÑовÑй Ñайл</p> 265 266 <div class="drop-zone" id="dropZone"> 267 <div class="icon">ð</div> 268 <div><strong>ÐеÑеÑаÑиÑе видео ÑÑда</strong> или нажмиÑе Ð´Ð»Ñ Ð²ÑбоÑа</div> 269 <div class="hint">MP4, AVI, MOV, MKV, WEBM, FLV, WMV, 3GP â до 2 ÐÐ</div> 270 </div> 271 <input type="file" id="fileInput" accept="video/*,audio/*" style="display:none"> 272 273 <!-- ÐнÑоÑмаÑÐ¸Ñ Ð¾ Ñайле --> 274 <div class="file-info" id="fileInfo"> 275 <h3>ð ÐнÑоÑмаÑÐ¸Ñ Ð¾ видео</h3> 276 <div class="info-grid" id="infoGrid"></div> 277 </div> 278 279 <!-- ÐаÑеÑÑва --> 280 <div class="section" id="qualitySection" style="display:none;"> 281 <h3>ð¯ ÐÑбеÑиÑе каÑеÑÑво</h3> 282 <p class="desc">ÐедоÑÑÑпнÑе каÑеÑÑва вÑÑе иÑÑ Ð¾Ð´Ð½Ð¾Ð³Ð¾ заблокиÑованÑ</p> 283 <div class="quality-grid" id="qualityGrid"></div> 284 </div> 285 286 <!-- ФоÑÐ¼Ð°Ñ --> 287 <div class="section" id="formatSection" style="display:none;"> 288 <h3>ð¦ ФоÑÐ¼Ð°Ñ Ð²Ñвода</h3> 289 <div class="format-list" id="formatList"> 290 <label class="format-option" data-format="mp4"> 291 <input type="radio" name="format" value="mp4" checked>
292 <span>MP4 (H.264 + AAC) â ÑнивеÑÑалÑнÑй</span> 293 </label> 294 <label class="format-option" data-format="webm"> 295 <input type="radio" name="format" value="webm" > 296 <span>WEBM (VP9 + Opus) â Ð´Ð»Ñ Ð²ÐµÐ±Ð°</span> 297 </label> 298 <label class="format-option" data-format="mkv"> 299 <input type="radio" name="format" value="mkv" > 300 <span>MKV (H.265 + AAC) â Ð´Ð»Ñ Ð°ÑÑ Ð¸Ð²Ð°</span> 301 </label> 302 <label class="format-option" data-format="mp3"> 303 <input type="radio" name="format" value="mp3" > 304 <span>MP3 â ÑолÑко звÑк</span> 305 </label> 306 <label class="format-option" data-format="gif"> 307 <input type="radio" name="format" value="gif" > 308 <span>GIF â анимаÑиÑ</span> 309 </label> 310 </div> 311 </div> 312 313 <!-- ÐиÑÑÐµÐ¹Ñ --> 314 <div class="section" id="bitrateSection" style="display:none;"> 315 <h3>âï¸ ÐаÑеÑÑво кодиÑованиÑ</h3> 316 <p class="desc">Снижение биÑÑейÑа ÑменÑÑÐ¸Ñ ÑÐ°Ð·Ð¼ÐµÑ Ñайла, но и каÑеÑÑво</p> 317 <input type="range" class="bitrate-slider" id="bitrateSlider" min="30" max="100" value="100" step="5"> 318 <div class="bitrate-values"> 319 <span>30% (менÑÑе ÑазмеÑ)</span> 320 <span>100% (лÑÑÑее каÑеÑÑво)</span> 321 </div> 322 <div class="bitrate-current" id="bitrateCurrent">100% Ð¾Ñ ÑÑандаÑÑного биÑÑейÑа</div> 323 </div> 324 325 <!-- Ðоп. опÑии --> 326 <div class="section" id="optionsSection" style="display:none;"> 327 <h3>ð§ ÐополниÑелÑно</h3> 328 <label class="checkbox-row"> 329 <input type="checkbox" id="noAudio"> УбÑаÑÑ Ð·Ð²Ñк 330 </label> 331 </div> 332 333 <!-- Ðнопка --> 334 <button class="btn" id="convertBtn" disabled>ð¬ ÐонвеÑÑиÑоваÑÑ</button> 335 336 <!-- ÐÑибка --> 337 <div class="error-box" id="errorBox"></div> 338 339 <!-- ÐÑогÑеÑÑ --> 340 <div class="progress-section" id="progressSection"> 341 <div class="progress-percent" id="progressPercent">0%</div> 342 <div class="progress-bar-wrap"> 343 <div class="progress-bar-fill" id="progressFill"></div> 344 </div> 345 <div class="progress-status" id="progressStatus">Ðжидание...</div> 346 <div class="log-box" id="logBox" style="display:none;"></div> 347 </div> 348 349 <!-- РезÑлÑÑÐ°Ñ --> 350 <div class="result-section" id="resultSection"> 351 <h2>â ÐоÑово!</h2> 352 <div id="resultInfo"></div> 353 <a href="#" class="download-btn" id="downloadBtn">â¬ï¸ СкаÑаÑÑ Ñайл</a> 354 </div> 355 </div> 356</div> 357
358<script> 359(function() { 360 'use strict'; 361 362 // ============= DOM ============= 363 const dropZone = document.getElementById('dropZone'); 364 const fileInput = document.getElementById('fileInput'); 365 const fileInfo = document.getElementById('fileInfo'); 366 const infoGrid = document.getElementById('infoGrid'); 367 const qualitySection = document.getElementById('qualitySection'); 368 const qualityGrid = document.getElementById('qualityGrid'); 369 const formatSection = document.getElementById('formatSection'); 370 const formatList = document.getElementById('formatList'); 371 const bitrateSection = document.getElementById('bitrateSection'); 372 const bitrateSlider = document.getElementById('bitrateSlider'); 373 const bitrateCurrent = document.getElementById('bitrateCurrent'); 374 const optionsSection = document.getElementById('optionsSection'); 375 const noAudio = document.getElementById('noAudio'); 376 const convertBtn = document.getElementById('convertBtn'); 377 const errorBox = document.getElementById('errorBox'); 378 const progressSection = document.getElementById('progressSection'); 379 const progressPercent = document.getElementById('progressPercent'); 380 const progressFill = document.getElementById('progressFill'); 381 const progressStatus = document.getElementById('progressStatus'); 382 const logBox = document.getElementById('logBox'); 383 const resultSection = document.getElementById('resultSection'); 384 const resultInfo = document.getElementById('resultInfo'); 385 const downloadBtn = document.getElementById('downloadBtn'); 386 387 // ============= СоÑÑоÑние ============= 388 let selectedFile = null; 389 let videoInfo = null; 390 let selectedQuality = null; 391 let jobId = null; 392 let eventSource = null; 393 394 // ============= ÐаÑеÑÑва ============= 395 const QUALITIES = { 396 '240p': { height: 240, bitrate: 400, audio: 96 }, 397 '360p': { height: 360, bitrate: 700, audio: 128 }, 398 '480p': { height: 480, bitrate: 1200, audio: 128 }, 399 '720p': { height: 720, bitrate: 2500, audio: 128 }, 400 '1080p': { height: 1080, bitrate: 5000, audio: 192 }, 401 }; 402 403 // ============= Drag & drop ============= 404 dropZone.addEventListener('click', () => fileInput.click()); 405 dropZone.addEventListener('dragover', (e) => { 406 e.preventDefault(); 407 dropZone.classList.add('dragover'); 408 }); 409 dropZone.addEventListener('dragleave', () => dropZone.classList.remove('dragover')); 410 dropZone.addEventListener('drop', (e) => { 411 e.preventDefault(); 412 dropZone.classList.remove('dragover'); 413 if (e.dataTransfer.files.length > 0) handleFile(e.dataTransfer.files[0]); 414 }); 415 fileInput.addEventListener('change', (e) => { 416 if (e.target.files.length > 0) handleFile(e.target.files[0]); 417 }); 418 419 // ============= ÐбÑабоÑка Ñайла ============= 420 async function handleFile(file) { 421 selectedFile = file; 422 423 // ÐÑовеÑки 424 const allowedExts = ['mp4','avi','mov','mkv','webm','flv','wmv','m4v','mpg','mpeg','3gp']; 425 const ext = file.name.split('.').pop().toLowerCase(); 426 if (!allowedExts.includes(ext)) { 427 showError('ÐеподдеÑживаемÑй ÑоÑмаÑ: .' + ext); 428 return; 429 } 430 if (file.size > 2 * 1024 * 1024 * 1024) { 431 showError('Файл ÑлиÑком болÑÑой (макÑ. 2 ÐÐ)'); 432 return; 433 } 434 435 hideError(); 436 437 // ÐоказÑваем в drop-zone 438 dropZone.querySelector('.icon').textContent = 'ð¥'; 439 dropZone.querySelector('strong').textContent = file.name; 440 dropZone.querySelector('.hint').textContent = formatBytes(file.size) + ' â загÑÑзка и анализ...'; 441 442 // ÐÑпÑавлÑем на ÑеÑÐ²ÐµÑ Ð´Ð»Ñ Ð°Ð½Ð°Ð»Ð¸Ð·Ð° 443 const fd = new FormData(); 444 fd.append('video_file', file); 445 fd.append('action', 'probe'); 446 447 try { 448 const resp = await fetch('convert.php', { method: 'POST', body: fd }); 449 const data = await resp.json(); 450 451 if (!data.success) { 452 showError(data.error || 'ÐÑибка анализа'); 453 return; 454 } 455 456 videoInfo = data.info; 457 jobId = data.job_id; 458 renderFileInfo(); 459 renderQualityOptions(); 460 renderFormatOptions(); 461 462 qualitySection.style.display = 'block'; 463 formatSection.style.display = 'block'; 464 bitrateSection.style.display = 'block'; 465 optionsSection.style.display = 'block'; 466 convertBtn.disabled = false; 467 468 dropZone.querySelector('.hint').textContent = 'ÐоÑово â вÑбеÑиÑе паÑамеÑÑÑ Ð½Ð¸Ð¶Ðµ'; 469 470 } catch (err) { 471 showError('ÐÑибка ÑоединениÑ: ' + err.message); 472 } 473 } 474 475 // ============= Ð ÐµÐ½Ð´ÐµÑ Ð¸Ð½ÑоÑмаÑии ============= 476 function renderFileInfo() { 477 const v = videoInfo; 478 const items = [ 479 { label: 'РазÑеÑение', value: v.video ? `${v.video.width}Ã${v.video.height}` : 'â' }, 480 { label: 'ÐлиÑелÑноÑÑÑ', value: formatDuration(v.duration) }, 481 { label: 'РазмеÑ', value: formatBytes(v.size) }, 482 { label: 'Ðидеокодек', value: v.video ? v.video.codec.toUpperCase() : 'â' }, 483 { label: 'FPS', value: v.video ? v.video.fps : 'â' }, 484 { label: 'ÐÑдио', value: v.audio ? `${v.audio.codec.toUpperCase()} ${v.audio.channels}ch` : 'ÐеÑ' }, 485 { label: 'ÐиÑÑейÑ', value: v.bitrate ? Math.round(v.bitrate / 1000) + ' kbps' : 'â' }, 486 { label: 'ÐовоÑоÑ', value: v.video && v.video.rotation ? v.video.rotation + '°' : 'ÐеÑ' }, 487 ]; 488 infoGrid.innerHTML = items.map(i => 489 `<div class="info-item"><div class="label">${i.label}</div><div class="value">${i.value}</div></div>` 490 ).join(''); 491 fileInfo.classList.add('show'); 492 } 493 494 // ============= Ð ÐµÐ½Ð´ÐµÑ ÐºÐ°ÑеÑÑв ============= 495 function renderQualityOptions() { 496 const srcHeight = videoInfo.video ? videoInfo.video.height : 0; 497 const srcWidth = videoInfo.video ? videoInfo.video.width : 0; 498 const isPortrait = srcHeight > srcWidth; 499 const srcMinDim = isPortrait ? srcWidth : srcHeight; 500 501 qualityGrid.innerHTML = ''; 502 503 // ÐлÑÑ "ÐÑигинал" 504 const origHtml = ` 505 <div class="quality-option selected" data-quality="original"> 506 <div class="qname">ÐÑигинал</div> 507 <div class="qres">${srcWidth}Ã${srcHeight}</div> 508 <div class="qbit">без изменений</div> 509 </div>`; 510 qualityGrid.insertAdjacentHTML('beforeend', origHtml); 511 selectedQuality = 'original'; 512
513 Object.entries(QUALITIES).forEach(([key, q]) => { 514 // ÐÑовеÑÑем: доÑÑÑпно ли ÑÑо каÑеÑÑво? 515 const available = isPortrait 516 ? q.height <= srcWidth // Ð´Ð»Ñ Ð²ÐµÑÑикалÑнÑÑ ÑÑавниваем Ñ ÑиÑиной 517 : q.height <= srcHeight; 518 519 const cls = available ? 'quality-option' : 'quality-option disabled'; 520 const dims = isPortrait 521 ? `${Math.round(q.height * (srcWidth / srcHeight))}Ã${q.height}` 522 : `${Math.round(q.height * (srcWidth / srcHeight))}Ã${q.height}`; 523 524 const html = ` 525 <div class="${cls}" data-quality="${key}"> 526 <div class="qname">${key}</div> 527 <div class="qres">${dims}</div> 528 <div class="qbit">${q.bitrate} kbps</div> 529 </div>`; 530 qualityGrid.insertAdjacentHTML('beforeend', html); 531 }); 532 533 // Ðлик по каÑеÑÑвам 534 qualityGrid.querySelectorAll('.quality-option:not(.disabled)').forEach(el => { 535 el.addEventListener('click', () => { 536 qualityGrid.querySelectorAll('.quality-option').forEach(o => o.classList.remove('selected')); 537 el.classList.add('selected'); 538 selectedQuality = el.dataset.quality; 539 }); 540 }); 541 } 542 543 // ============= Ð ÐµÐ½Ð´ÐµÑ ÑоÑмаÑов ============= 544 function renderFormatOptions() { 545 formatList.querySelectorAll('.format-option').forEach(el => { 546 el.addEventListener('click', () => { 547 formatList.querySelectorAll('.format-option').forEach(o => o.classList.remove('selected')); 548 el.classList.add('selected'); 549 el.querySelector('input').checked = true; 550 }); 551 }); 552 formatList.querySelector('.format-option').classList.add('selected'); 553 } 554 555 // ============= ÐиÑÑÐµÐ¹Ñ ============= 556 bitrateSlider.addEventListener('input', () => { 557 const val = bitrateSlider.value; 558 bitrateCurrent.textContent = val + '% Ð¾Ñ ÑÑандаÑÑного биÑÑейÑа'; 559 }); 560 561 // ============= Ðнопка "ÐонвеÑÑиÑоваÑÑ" ============= 562 convertBtn.addEventListener('click', async () => { 563 if (!selectedFile || !videoInfo) return; 564 565 const format = document.querySelector('input[name="format"]:checked').value; 566 const bitrateScale = parseInt(bitrateSlider.value) / 100; 567 const removeAudio = noAudio.checked; 568 569 convertBtn.disabled = true; 570 convertBtn.textContent = 'â³ ÐÑпÑавка...'; 571 progressSection.classList.add('show'); 572 resultSection.classList.remove('show'); 573 logBox.style.display = 'block'; 574 logBox.textContent = 'ÐÑпÑавка заданиÑ...'; 575 576 // ÐÑпÑавлÑем запÑÐ¾Ñ Ð½Ð° запÑÑк конвеÑÑаÑии 577 const fd = new FormData(); 578 fd.append('action', 'convert'); 579 fd.append('job_id', jobId); 580 fd.append('quality', selectedQuality); 581 fd.append('format', format); 582 fd.append('bitrate_scale', bitrateScale); 583 fd.append('remove_audio', removeAudio ? '1' : '0'); 584 585 try { 586 const resp = await fetch('convert.php', { method: 'POST', body: fd }); 587 const data = await resp.json(); 588 589 if (!data.success) { 590 showError(data.error || 'ÐÑибка запÑÑка'); 591 convertBtn.disabled = false; 592 convertBtn.textContent = 'ð¬ ÐонвеÑÑиÑоваÑÑ'; 593 return; 594 } 595 596 // ÐапÑÑкаем SSE 597 startSSE(jobId); 598 599 } catch (err) { 600 showError('ÐÑибка: ' + err.message); 601 convertBtn.disabled = false; 602 convertBtn.textContent = 'ð¬ ÐонвеÑÑиÑоваÑÑ'; 603 } 604 }); 605 606 // ============= SSE ============= 607 function startSSE(id) { 608 if (eventSource) eventSource.close(); 609 610 progressStatus.textContent = 'ÐодклÑÑение...'; 611 progressFill.style.width = '0%'; 612 progressPercent.textContent = '0%'; 613 614 eventSource = new EventSource('progress.php?id=' + id); 615
616 eventSource.addEventListener('progress', (e) => { 617 try { 618 const data = JSON.parse(e.data); 619 620 if (typeof data.progress === 'number') { 621 const p = Math.max(0, Math.min(100, data.progress)); 622 progressFill.style.width = p + '%'; 623 progressPercent.textContent = p + '%'; 624 } 625 if (data.status) progressStatus.textContent = data.status; 626 if (data.log) logBox.textContent = data.log; 627 } catch (err) { console.error(err); } 628 }); 629 630 eventSource.addEventListener('complete', (e) => { 631 try { 632 const data = JSON.parse(e.data); 633 progressFill.style.width = '100%'; 634 progressPercent.textContent = '100%'; 635 progressStatus.textContent = 'â ÐоÑово!'; 636 637 resultInfo.innerHTML = ` 638 <p><strong>Файл:</strong> ${data.filename}</p> 639 <p><strong>РазмеÑ:</strong> ${data.filesize}</p> 640 <p><strong>ÐÑемÑ:</strong> ${data.elapsed} Ñек</p> 641 `; 642 downloadBtn.href = 'download.php?id=' + id; 643 resultSection.classList.add('show'); 644 convertBtn.textContent = 'ð¬ ÐонвеÑÑиÑоваÑÑ ÐµÑÑ'; 645 646 eventSource.close(); 647 eventSource = null; 648 } catch (err) { console.error(err); } 649 }); 650 651 eventSource.addEventListener('error', (e) => { 652 if (e.data) { 653 try { 654 const data = JSON.parse(e.data); 655 showError('ÐÑибка: ' + (data.error || 'неизвеÑÑнаÑ')); 656 progressStatus.textContent = 'â ÐÑибка'; 657 convertBtn.disabled = false; 658 convertBtn.textContent = 'ð¬ ÐопÑобоваÑÑ Ñнова'; 659 } catch (err) {} 660 } 661 if (eventSource && eventSource.readyState === EventSource.CLOSED) { 662 eventSource.close(); 663 eventSource = null; 664 } 665 }); 666 } 667 668 // ============= УÑилиÑÑ ============= 669 function showError(msg) { 670 errorBox.textContent = 'â ' + msg; 671 errorBox.classList.add('show'); 672 setTimeout(() => errorBox.classList.remove('show'), 8000); 673 } 674 function hideError() { errorBox.classList.remove('show'); } 675 676 function formatBytes(bytes) { 677 if (bytes < 1024) return bytes + ' B'; 678 if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; 679 if (bytes < 1024 * 1024 * 1024) return (bytes / 1024 / 1024).toFixed(2) + ' MB'; 680 return (bytes / 1024 / 1024 / 1024).toFixed(2) + ' GB'; 681 } 682 683 function formatDuration(seconds) { 684 const h = Math.floor(seconds / 3600); 685 const m = Math.floor((seconds % 3600) / 60); 686 const s = Math.floor(seconds % 60); 687 return h > 0 688 ? `${h}:${String(m).padStart(2, '0')}
688:${String(s).padStart(2, '0')}` 689 : `${m}:${String(s).padStart(2, '0')}`; 690 } 691})(); 692</script>
692 693</body> 694</html>
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.