1<!DOCTYPE html> 2<!-- saved from url=(0034)https://ebook-convert.netlify.app/ --> 3<html lang="en"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> 4<!-- This site is hosted on Netlify. Anyone can build and deploy a site 5 like this one for free: https://netlify.new/?utm_campaign=loops&utm_source=ai-legible&utm_medium=owned&utm_content=comment&utm_id=0bb98254-6293-4df6-ad4a-7f1cded55da8 6 Netlify hosting facts for this site: static/SSR served via Netlify Edge. --> 7 8 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 9
9<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
9 10 <!-- Add encoding detection library --> 11
11<script src="https://cdn.jsdelivr.net/npm/[email protected]/encoding.min.js"></script>
11 12 <!-- Load OpenCC library from unpkg --> 13
13<script src="https://unpkg.com/[email protected]/dist/umd/full.js"></script>
13 14 15 <title>Universal Ebook Converter</title> 16 <style> 17 * { 18 margin: 0; 19 padding: 0; 20 box-sizing: border-box; 21 } 22 23 body { 24 font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; 25 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 26 min-height: 100vh; 27 padding: 20px; 28 -webkit-tap-highlight-color: transparent; 29 } 30 31 .container { 32 max-width: 1000px; 33 margin: 0 auto; 34 background: white; 35 border-radius: 15px; 36 box-shadow: 0 20px 40px rgba(0,0,0,0.1); 37 overflow: hidden; 38 } 39 40 .header { 41 background: linear-gradient(135deg, #4f46e5, #7c3aed); 42 color: white; 43 padding: 30px; 44 text-align: center; 45 } 46 47 .header h1 { 48 font-size: 2.5rem; 49 margin-bottom: 10px; 50 } 51 52 .header p { 53 opacity: 0.9; 54 font-size: 1.1rem; 55 } 56 57 .content { 58 padding: 40px; 59 } 60 61 .browser-support { 62 background: #f0fdf4; 63 border: 1px solid #bbf7d0; 64 border-radius: 12px; 65 padding: 20px; 66 margin-bottom: 30px; 67 text-align: center; 68 } 69 70 .browser-support.warning { 71 background: #fffbeb; 72 border-color: #fbbf24; 73 color: #92400e; 74 } 75 76 .directory-section { 77 background: #f8fafc; 78 border: 2px solid #e2e8f0; 79 border-radius: 12px; 80 padding: 25px; 81 margin-bottom: 30px; 82 transition: all 0.3s ease; 83 } 84 85 .directory-section.selected { 86 background: #f0fdf4; 87 border-color: #22c55e; 88 } 89 90 .directory-section h3 { 91 color: #1e293b; 92 margin-bottom: 15px; 93 display: flex; 94 align-items: center; 95 gap: 10px; 96 } 97 98 .directory-info { 99 background: #e0f2fe; 100 border: 1px solid #0284c7; 101 border-radius: 8px; 102 padding: 15px; 103 margin-top: 15px; 104 color: #0c4a6e; 105 } 106 107 .output-format-section { 108 background: #f0f9ff; 109 border: 2px solid #0ea5e9; 110 border-radius: 12px; 111 padding: 25px; 112 margin-bottom: 30px; 113 } 114 115 .output-format-section h3 { 116 color: #0c4a6e; 117 margin-bottom: 15px; 118 display: flex; 119 align-items: center; 120 gap: 10px; 121 } 122 123 .format-grid { 124 display: grid; 125 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 126 gap: 15px; 127 margin-bottom: 20px; 128 } 129 130 .format-option { 131 background: white; 132 border: 2px solid #0ea5e9; 133 border-radius: 8px; 134 padding: 15px; 135 cursor: pointer; 136 transition: all 0.3s ease; 137 text-align: center; 138 -webkit-tap-highlight-color: transparent; 139 user-select: none; 140 } 141 142 .format-option:hover { 143 border-color: #0c4a6e; 144 background: #f0f9ff; 145 } 146 147 .format-option:active { 148 transform: scale(0.98); 149 } 150 151 .format-option.selected { 152 border-color: #0c4a6e; 153 background: #dbeafe; 154 } 155 156 .format-option input[type="radio"] { 157 margin-right: 8px; 158 pointer-events: none; 159 } 160 161 .format-option .title { 162 font-weight: 600; 163 color: #0c4a6e; 164 margin-bottom: 5px; 165 } 166 167 .format-option .description { 168 font-size: 0.875rem; 169 color: #075985; 170 } 171 172 .chinese-conversion-section { 173 background: #fef7ff; 174 border: 2px solid #e879f9; 175 border-radius: 12px; 176 padding: 25px; 177 margin-bottom: 30px; 178 } 179 180 .chinese-conversion-section h3 { 181 color: #a21caf; 182 margin-bottom: 15px; 183 display: flex; 184 align-items: center; 185 gap: 10px; 186 } 187 188 .conversion-options { 189 display: grid; 190 grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); 191 gap: 15px; 192 margin-bottom: 20px; 193 } 194 195 .conversion-option { 196 background: white; 197 border: 2px solid #e879f9; 198 border-radius: 8px; 199 padding: 15px; 200 cursor: pointer; 201 transition: all 0.3s ease; 202 -webkit-tap-highlight-color: transparent; 203 user-select: none; 204 } 205 206 .conversion-option:hover { 207 border-color: #a21caf; 208 background: #fdf4ff; 209 } 210 211 .conversion-option:active { 212 transform: scale(0.98); 213 } 214 215 .conversion-option.selected { 216 border-color: #a21caf; 217 background: #fdf4ff; 218 } 219 220 .conversion-option input[type="radio"] { 221 margin-right: 10px; 222 pointer-events: none; 223 } 224 225 .conversion-option .title { 226 font-weight: 600; 227 color: #a21caf; 228 margin-bottom: 5px; 229 } 230 231 .conversion-option .description { 232 font-size: 0.875rem; 233 color: #701a75; 234 } 235 236 .conversion-option .example { 237 font-size: 0.75rem; 238 color: #86198f; 239 margin-top: 5px; 240 font-style: italic; 241 } 242 243 .conversion-pipeline { 244 background: #fffbeb; 245 border: 1px solid #fbbf24; 246 border-radius: 8px; 247 padding: 15px; 248 margin-top: 15px; 249 font-size: 0.875rem; 250 color: #92400e; 251 } 252 253 .opencc-status { 254 background: #f0fdf4; 255 border: 1px solid #bbf7d0; 256 border-radius: 8px; 257 padding: 15px; 258 margin-top: 15px; 259 font-size: 0.875rem; 260 color: #065f46; 261 } 262 263 .api-setup { 264 background: #f0fdf4; 265 border: 1px solid #bbf7d0; 266 border-radius: 12px; 267 padding: 20px; 268 margin-bottom: 30px; 269 } 270 271 .api-setup.collapsed { 272 padding: 15px; 273 } 274 275 .api-setup h3 { 276 color: #065f46; 277 margin-bottom: 15px; 278 display: flex; 279 align-items: center; 280 gap: 10px; 281 cursor: pointer; 282 -webkit-tap-highlight-color: transparent; 283 user-select: none; 284 } 285 286 .api-setup.collapsed h3 { 287 margin-bottom: 0; 288 } 289
290 .api-details { 291 overflow: hidden; 292 transition: all 0.3s ease; 293 } 294 295 .api-details.hidden { 296 max-height: 0; 297 margin: 0; 298 padding: 0; 299 } 300 301 .api-input-group { 302 display: flex; 303 gap: 10px; 304 align-items: center; 305 flex-wrap: wrap; 306 } 307 308 .api-input { 309 flex: 1; 310 min-width: 300px; 311 padding: 12px; 312 border: 2px solid #bbf7d0; 313 border-radius: 8px; 314 font-size: 1rem; 315 } 316 317 .api-input:focus { 318 outline: none; 319 border-color: #059669; 320 } 321 322 .api-status { 323 padding: 8px 16px; 324 border-radius: 20px; 325 font-size: 0.875rem; 326 font-weight: 500; 327 } 328 329 .api-valid { 330 background: #d1fae5; 331 color: #065f46; 332 } 333 334 .api-invalid { 335 background: #fee2e2; 336 color: #991b1b; 337 } 338 339 .toggle-icon { 340 transition: transform 0.3s ease; 341 } 342 343 .toggle-icon.rotated { 344 transform: rotate(180deg); 345 } 346 347 .debug-section { 348 background: #fef3c7; 349 border: 1px solid #f59e0b; 350 border-radius: 12px; 351 padding: 20px; 352 margin-bottom: 30px; 353 display: none; 354 } 355 356 .debug-section h3 { 357 color: #92400e; 358 margin-bottom: 15px; 359 } 360 361 .debug-output { 362 background: #fffbeb; 363 border: 1px solid #fbbf24; 364 border-radius: 8px; 365 padding: 15px; 366 max-height: 200px; 367 overflow-y: auto; 368 font-family: monospace; 369 font-size: 0.875rem; 370 color: #92400e; 371 } 372 373 .upload-area { 374 border: 3px dashed #d1d5db; 375 border-radius: 12px; 376 padding: 40px; 377 text-align: center; 378 margin-bottom: 30px; 379 transition: all 0.3s ease; 380 cursor: pointer; 381 -webkit-tap-highlight-color: transparent; 382 } 383 384 .upload-area:hover { 385 border-color: #4f46e5; 386 background-color: #f8fafc; 387 } 388 389 .upload-area:active { 390 transform: scale(0.99); 391 } 392 393 .upload-area.dragover { 394 border-color: #4f46e5; 395 background-color: #eef2ff; 396 } 397 398 .upload-icon { 399 font-size: 3rem; 400 color: #9ca3af; 401 margin-bottom: 15px; 402 } 403 404 .upload-text { 405 color: #6b7280; 406 font-size: 1.1rem; 407 margin-bottom: 15px; 408 } 409 410 .supported-formats-list { 411 font-size: 0.875rem; 412 color: #6b7280; 413 margin-top: 10px; 414 line-height: 1.5; 415 } 416 417 .file-input { 418 display: none; 419 } 420 421 .btn { 422 background: linear-gradient(135deg, #4f46e5, #7c3aed); 423 color: white; 424 border: none; 425 padding: 12px 30px; 426 border-radius: 8px; 427 cursor: pointer; 428 font-size: 1rem; 429 transition: all 0.3s ease; 430 display: inline-block; 431 text-decoration: none; 432 margin: 5px; 433 -webkit-tap-highlight-color: transparent; 434 user-select: none; 435 } 436 437 .btn:hover { 438 transform: translateY(-2px); 439 box-shadow: 0 10px 20px rgba(79, 70, 229, 0.3); 440 } 441 442 .btn:active { 443 transform: translateY(0); 444 } 445 446 .btn:disabled { 447 opacity: 0.6; 448 cursor: not-allowed; 449 transform: none; 450 } 451 452 .btn-success { 453 background: linear-gradient(135deg, #22c55e, #16a34a); 454 } 455 456 .btn-warning { 457 background: linear-gradient(135deg, #f59e0b, #d97706); 458 } 459 460 .btn-secondary { 461 background: linear-gradient(135deg, #059669, #047857); 462 } 463 464 .btn-danger { 465 background: linear-gradient(135deg, #dc2626, #b91c1c); 466 } 467 468 .btn-large { 469 padding: 15px 40px; 470 font-size: 1.1rem; 471 } 472 473 .btn-small { 474 padding: 8px 16px; 475 font-size: 0.875rem; 476 } 477 478 .file-list { 479 margin-top: 30px; 480 } 481 482 .file-item { 483 display: flex; 484 justify-content: space-between; 485 align-items: center; 486 padding: 15px; 487 border: 1px solid #e5e7eb; 488 border-radius: 8px; 489 margin-bottom: 10px; 490 background: #f9fafb; 491 } 492 493 .file-info { 494 flex: 1; 495 } 496 497 .file-name { 498 font-weight: 600; 499 color: #374151; 500 margin-bottom: 5px; 501 } 502 503 .file-size { 504 font-size: 0.875rem; 505 color: #6b7280; 506 } 507 508 .file-method { 509 font-size: 0.75rem; 510 background: #e0e7ff; 511 color: #3730a3; 512 padding: 2px 8px; 513 border-radius: 12px; 514 margin-top: 5px; 515 display: inline-block; 516 } 517 518 .file-method.local { 519 background: #d1fae5; 520 color: #065f46; 521 } 522 523 .file-method.opencc { 524 background: #fef3c7; 525 color: #92400e; 526 } 527 528 .file-method.batch { 529 background: #ddd6fe; 530 color: #5b21b6; 531 } 532 533 .file-actions { 534 display: flex; 535 align-items: center; 536 gap: 10px; 537 } 538 539 .file-status { 540 padding: 5px 12px; 541 border-radius: 20px; 542 font-size: 0.875rem; 543 font-weight: 500; 544 } 545 546 .status-pending { 547 background: #fef3c7; 548 color: #92400e; 549 } 550 551 .status-processing { 552 background: #dbeafe; 553 color: #1e40af; 554 } 555 556 .status-success { 557 background: #d1fae5; 558 color: #065f46; 559 } 560 561 .status-error { 562 background: #fee2e2; 563 color: #991b1b; 564 } 565 566 .progress-bar { 567 width: 100%; 568 height: 8px; 569 background: #e5e7eb; 570 border-radius: 4px; 571 margin: 20px 0; 572 overflow: hidden; 573 } 574 575 .progress-fill { 576 height: 100%; 577 background: linear-gradient(90deg, #4f46e5, #7c3aed); 578 transition: width 0.3s ease; 579 width: 0%; 580 } 581 582 .download-section { 583 background: #f0fdf4; 584 border: 1px solid #bbf7d0; 585 border-radius: 12px; 586 padding: 30px; 587 margin-top: 30px; 588 display: none; 589 } 590 591 .download-section h3 { 592 color: #065f46; 593 margin-bottom: 20px; 594 font-size: 1.25rem; 595 } 596 597 .download-actions { 598 display: flex; 599 flex-wrap: wrap; 600 gap: 10px; 601 margin-bottom: 20px; 602 } 603 604 .supported-formats { 605 background: #f0f9ff; 606 border: 1px solid #bae6fd; 607 border-radius: 8px; 608 padding: 20px; 609 margin-top: 30px; 610 } 611 612 .supported-formats h3 { 613 color: #0369a1; 614 margin-bottom: 10px; 615 } 616 617 .supported-formats ul { 618 list-style: none; 619 color: #0369a1; 620 } 621 622 .supported-formats li { 623 margin: 5px 0; 624 position: relative; 625 padding-left: 20px; 626 } 627 628 .supported-formats li:before { 629 content: "â"; 630 position: absolute; 631 left: 0; 632 color: #059669; 633 font-weight: bold; 634 } 635 636 .warning { 637 background: #fffbeb; 638 border: 1px solid #fbbf24; 639 border-radius: 8px; 640 padding: 15px; 641 margin-top: 20px; 642 color: #92400e; 643 } 644 645 @media (max-width: 768px) { 646 .container { 647 margin: 10px; 648 } 649 650 .header { 651 padding: 20px; 652 } 653 654 .header h1 { 655 font-size: 2rem; 656 } 657 658 .content { 659 padding: 20px; 660 } 661 662 .upload-area { 663 padding: 30px 20px; 664 } 665 666 .file-item { 667 flex-direction: column; 668 align-items: flex-start; 669 gap: 10px; 670 } 671 672 .file-actions { 673 width: 100%; 674 justify-content: flex-end; 675 } 676 677 .download-actions { 678 flex-direction: column; 679 } 680 681 .api-input-group { 682 flex-direction: column; 683 align-items: stretch; 684 } 685 686 .api-input { 687 min-width: unset; 688 } 689 690 .conversion-options { 691 grid-template-columns: 1fr; 692 } 693 694 .format-grid { 695 grid-template-columns: 1fr; 696 } 697 } 698 </style> 699</head> 700<body> 701 <div class="container"> 702 <div class="header"> 703 <h1>ð Universal Ebook Converter</h1> 704 <p>Convert between all ebook formats with Chinese character conversion</p> 705 </div> 706 707 <div class="content"> 708 <div class="browser-support" id="browserSupport"> 709 <strong>â Full Support:</strong> Your browser supports directory selection with universal ebook conversion.<br> 710 <small>Browser: Chrome/138.0.0.0 Safari/537.36</small> 711 </div> 712 713 <div class="debug-section" id="debugSection"> 714 <h3>ð§ Debug Information</h3> 715 <button class="btn btn-small btn-warning" onclick="toggleDebug()">Toggle Debug Mode</button> 716 <div class="debug-output" id="debugOutput"></div> 717 </div> 718 719 <div class="directory-section" id="directorySection"> 720 <h3>ð Choose Output Directory</h3> 721 <p>Select where you want to save the converted files:</p> 722 723 <div style="margin: 15px 0;"> 724 <button class="btn btn-success btn-large" id="selectDirBtn" onclick="selectOutputDirectory()"> 725 ð Choose Output Folder 726 </button> 727 728 <button class="btn btn-warning" onclick="useDefaultDownloads()"> 729 ð¾ Use Default Downloads 730 </button> 731 732 <button class="btn btn-small" onclick="showDebugSection()"> 733 ð§ Show Debug 734 </button> 735 </div> 736 737 <div class="directory-info" id="directoryInfo" style="display: none;"> 738 <strong>ð Selected Directory:</strong><br>
739 <span id="selectedPath">None selected</span> 740 </div> 741 </div> 742 743 <div class="output-format-section"> 744 <h3>ð Choose Output Format</h3> 745 <p style="margin-bottom: 15px; color: #075985;">Select the format you want to convert your ebooks to:</p> 746 747 <div class="format-grid"> 748 <div class="format-option selected" ontouchstart="" onclick="selectOutputFormat('txt', event)"> 749 <input type="radio" name="outputFormat" value="txt" checked> 750 <div class="title">ð TXT</div> 751 <div class="description">Plain Text</div> 752 </div> 753 754 <div class="format-option" ontouchstart="" onclick="selectOutputFormat('epub', event)"> 755 <input type="radio" name="outputFormat" value="epub"> 756 <div class="title">ð EPUB</div> 757 <div class="description">Electronic Publication</div> 758 </div> 759 760 <div class="format-option" ontouchstart="" onclick="selectOutputFormat('mobi', event)"> 761 <input type="radio" name="outputFormat" value="mobi"> 762 <div class="title">ð MOBI</div> 763 <div class="description">Kindle Format</div> 764 </div> 765 766 <div class="format-option" ontouchstart="" onclick="selectOutputFormat('azw3', event)"> 767 <input type="radio" name="outputFormat" value="azw3"> 768 <div class="title">ð± AZW3</div> 769 <div class="description">Kindle KF8</div> 770 </div> 771 772 <div class="format-option" ontouchstart="" onclick="selectOutputFormat('pdf', event)"> 773 <input type="radio" name="outputFormat" value="pdf"> 774 <div class="title">ð PDF</div> 775 <div class="description">Portable Document</div> 776 </div> 777 778 <div class="format-option" ontouchstart="" onclick="selectOutputFormat('fb2', event)"> 779 <input type="radio" name="outputFormat" value="fb2"> 780 <div class="title">ð FB2</div> 781 <div class="description">FictionBook</div> 782 </div> 783 784 <div class="format-option" ontouchstart="" onclick="selectOutputFormat('rtf', event)"> 785 <input type="radio" name="outputFormat" value="rtf"> 786 <div class="title">ð RTF</div> 787 <div class="description">Rich Text Format</div> 788 </div> 789 790 <div class="format-option" ontouchstart="" onclick="selectOutputFormat('docx', event)"> 791 <input type="radio" name="outputFormat" value="docx"> 792 <div class="title">ð DOCX</div> 793 <div class="description">Word Document</div> 794 </div> 795 </div> 796 </div> 797 798 <div class="chinese-conversion-section"> 799 <h3>ð Chinese Character Conversion (ç¹ç®è½¬æ¢)</h3> 800 <p style="margin-bottom: 15px; color: #701a75;">Choose how to handle Chinese text in the converted files:</p> 801 802 <div class="conversion-options"> 803 <div class="conversion-option selected" ontouchstart="" onclick="selectConversionOption('none', event)"> 804 <input type="radio" name="conversion" value="none" checked> 805 <div class="title">ð No Conversion</div> 806 <div class="description">Keep original text as-is</div> 807 <div class="example">ä¿æåæä¸è® / ä¿æåæä¸å</div> 808 </div> 809 810 <div class="conversion-option" ontouchstart="" onclick="selectConversionOption('t2s', event)"> 811 <input type="radio" name="conversion" value="t2s"> 812 <div class="title">ð Traditional to Simplified</div> 813 <div class="description">ç¹é«ä¸æ â ç®ä½ä¸æ</div> 814 <div class="example">æ¼¢å â æ±å</div> 815 </div> 816 817 <div class="conversion-option" ontouchstart="" onclick="selectConversionOption('s2t', event)"> 818 <input type="radio" name="conversion" value="s2t"> 819 <div class="title">ð Simplified to Traditional</div> 820 <div class="description">ç®ä½ä¸æ â ç¹é«ä¸æ</div> 821 <div class="example">æ±å â æ¼¢å</div> 822 </div> 823 824 <div class="conversion-option" ontouchstart="" onclick="selectConversionOption('s2tw', event)"> 825 <input type="radio" name="conversion" value="s2tw"> 826 <div class="title">ð¹ð¼ Simplified to Taiwan</div> 827 <div class="description">ç®ä½ä¸æ â å°ç£æ£é«</div> 828 <div class="example">软件 â è»é«</div> 829 </div> 830 831 <div class="conversion-option" ontouchstart="" onclick="selectConversionOption('s2hk', event)"> 832 <input type="radio" name="conversion" value="s2hk"> 833 <div class="title">ðð° Simplified to Hong Kong</div> 834 <div class="description">ç®ä½ä¸æ â 馿¸¯ç¹é«</div> 835 <div class="example">éé¢ â è£é¢</div> 836 </div> 837 </div> 838 839 <div class="conversion-pipeline" id="conversionPipeline" style="display: none;"> 840 <strong>ð Conversion Pipeline:</strong>
841 <span id="pipelineText">Input â CloudConvert to EPUB â OpenCC Chinese Conversion â CloudConvert to Output Format</span> 842 </div> 843 844 <div class="opencc-status" id="openccStatus" style="display: block;"> 845 <strong id="openccStatusText">â OpenCC Ready (Test: æµè¯ â æ¸¬è©¦)</strong> 846 </div> 847 </div> 848 849 <div class="api-setup collapsed" id="apiSetup"> 850 <h3 onclick="toggleApiSetup()"> 851 ð CloudConvert API Status 852 <span id="apiStatus" class="api-status api-valid">â Ready</span> 853 <span class="toggle-icon" id="toggleIcon">â¼</span> 854 </h3> 855 <div class="api-details hidden" id="apiDetails"> 856 <p style="color: #065f46; margin-bottom: 15px; font-size: 0.9rem;"> 857 Using pre-configured API key. CloudConvert is required for most conversions. 858 <br><strong>Note:</strong> EPUB â TXT, EPUB direct Chinese conversion, and TXT to TXT (with Chinese conversion) are processed locally! 859 </p> 860 <div class="api-input-group"> 861 <input type="password" id="apiKey" class="api-input" placeholder="Enter your CloudConvert API key..."> 862 <button onclick="validateApiKey()" class="btn btn-small">Validate</button> 863 </div> 864 </div> 865 </div> 866 867 <div class="upload-area" onclick="document.getElementById('fileInput').click()"> 868 <div class="upload-icon">ð</div> 869 <div class="upload-text"> 870 <strong>Click to select files</strong> or drag and drop here 871 </div> 872 <div class="supported-formats-list"> 873 <strong>Supported Input Formats:</strong><br> 874 TXT, AZW, AZW3, AZW4, CBC, CBR, CBZ, CHM, EPUB, FB2, HTM, HTMLZ, LIT, LRF, MOBI, PDB, PML, PRC, RB, SNB, TCR, TXTZ 875 </div> 876 </div> 877 878 <input type="file" id="fileInput" class="file-input" multiple accept=".txt,.azw,.azw3,.azw4,.cbc,.cbr,.cbz,.chm,.epub,.fb2,.htm,.html,.htmlz,.lit,.lrf,.mobi,.pdb,.pml,.prc,.rb,.snb,.tcr,.txtz"> 879 880 <div style="margin-bottom: 20px;"> 881 <button id="convertBtn" class="btn" disabled>ð Convert Selected Files</button> 882 <button id="clearBtn" class="btn btn-danger" onclick="clearFiles()" style="display: none;">ðï¸ Clear All</button> 883 </div> 884 885 <div class="progress-bar" id="progressBar" style="display: none;"> 886 <div class="progress-fill" id="progressFill"></div> 887 </div> 888 889 <div id="fileList" class="file-list"></div> 890 891 <div id="downloadSection" class="download-section"> 892 <h3>ð¥ Download Converted Files</h3> 893 <div class="download-actions"> 894 <button id="downloadAllBtn" class="btn btn-secondary">ð¦ Download All as ZIP</button> 895 <button id="downloadAllSeparateBtn" class="btn btn-secondary">ð Download All Separately</button> 896 </div> 897 <div id="individualDownloads"></div> 898 </div> 899 900 <div class="supported-formats"> 901 <h3>ð Universal Ebook Converter Features</h3> 902 <ul> 903 <li><strong>Input Formats:</strong> TXT, AZW, AZW3, AZW4, CBC, CBR, CBZ, CHM, EPUB, FB2, HTM, HTMLZ, LIT, LRF, MOBI, PDB, PML, PRC, RB, SNB, TCR, TXTZ</li> 904 <li><strong>Output Formats:</strong> TXT, EPUB, MOBI, AZW3, PDF, FB2, RTF, DOCX</li> 905 <li><strong>Chinese Conversion:</strong> Traditional â Simplified with regional variants (Taiwan, Hong Kong)</li> 906 <li><strong>Local Processing:</strong> EPUB to TXT, EPUB direct Chinese conversion, and TXT to TXT conversions with OpenCC done locally!</li> 907 <li><strong>Smart Pipeline:</strong> Uses EPUB as intermediate format for better preservation of book structure</li> 908 <li><strong>Batch Processing:</strong>
908 Multiple files processed efficiently in single API calls</li> 909 <li><strong>CloudConvert API:</strong> Professional-grade conversion for other formats</li> 910 <li><strong>Directory Selection:</strong> Choose exactly where to save files</li> 911 </ul> 912 </div> 913 914 <div class="warning"> 915 <strong>ð Advanced Conversion Pipeline:</strong> 916 EPUB to TXT, direct EPUB Chinese conversion, and TXT to TXT (with Chinese conversion) are processed locally for speed and privacy. 917 For other formats with Chinese conversion, files are converted to EPUB first to preserve book structure, then Chinese conversion is applied, 918 followed by conversion to the final format. Multiple files with similar conversion requirements are batched into single API calls for efficiency. 919 </div> 920 </div> 921 </div> 922 <h5 align="center"><a href="https://hkpsychiatry.com/">翺ç¿é«ç CHEUNG NGO MEDICAL</a></h5> 923 924
924<script src="./Universal Ebook Converter_files/jszip.min.js.download"></script>
924 925 <!-- Load OpenCC library from unpkg --> 926
926<script src="./Universal Ebook Converter_files/full.js.download"></script>
926 927
927<script> 928 let selectedFiles = []; 929 let convertedFiles = []; 930 let apiKeyValid = true; 931 let selectedDirectoryHandle = null; 932 let downloadMethod = 'none'; 933 let debugMode = false; 934 let selectedConversion = 'none'; 935 let selectedOutputFormat = 'txt'; 936 let openccConverters = {}; 937 938 // Default API key 939 const DEFAULT_API_KEY = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiIxIiwianRpIjoiYmE4N2NlY2Q2OWM3NDQzYjAwNjQ5Nzc3YzBjMTM1ZjhkMDlmOGY5OWY3MGY1ZmI5OGMyMjc2Zjc0ZmQyYmVmNjA5NDk0YzQyY2Y3ZTljZDIiLCJpYXQiOjE3NTMyMDU4OTMuNzE5ODU3LCJuYmYiOjE3NTMyMDU4OTMuNzE5ODU5LCJleHAiOjQ5MDg4Nzk0OTMuNzE1MDQxLCJzdWIiOiI3MjQ5MTgzMyIsInNjb3BlcyI6WyJ1c2VyLnJlYWQiLCJ0YXNrLnJlYWQiLCJ0YXNrLndyaXRlIiwid2ViaG9vay5yZWFkIiwid2ViaG9vay53cml0ZSIsInByZXNldC5yZWFkIiwicHJlc2V0LndyaXRlIl19.L8xgP2QQcFDbV5ni5CTSY5677ahk3i0mdiSMIWEPEXs5vulkFMxX8RXbth80FmafLag4R13XFSaUp_ZiMPaYDWk3w4Tg7f8waJX3xs6si00h0GFkVnAUE8AN8LdcUrWeuWRyO1sH49vOkHiYwL8bIjzifyLsUlELpYCt7nW8NAOn5ezN7jWBEwGwEdllUOUpLz108KFgOf5R_t0xIImUMRVRocrzIsmA_G6GlA8TaFuSfskrigXau04ac9IaHrdt_GW39rMeUkT1_Ibr1OvThoSfWEUmnHnTlvncGxFjG350iAUXWFBwSgGotSBp1UmsjYgJPGVmpQuiSoHxpJd14M4smI1Fti54n_SamG69N4dBjNWvEg5-rf7qQH9JwVwi_R7JX5BOKoXcla78s0TDhhqXIitCQ_qX5ECFCaw_ef5XlqIubEjy9lbJ1QWVlx5Qbgnbsbsnd0vabzrVePJ-ggLw-mK9fJFCJwA7sXRqApifg04QL2Zlm69J8pg9Wv-MeaYDzYU630Uegrv_JaWeFIbUGuldpA4x479yYdVsAZ5CbgMox7Ku8pA52lm_THSkaMd0wQCf34oDL0unDVICIFSYwsfJQB4eCh_xUiGyIPqiUBG51Y09xlqRHvUFdFllU0yCu1mM7CzDzmowekMNLe6FssAkkd0t45lZhOFUVL0"; 940 941 // Supported formats - INCLUDING TXT 942 const supportedInputFormats = [ 943 'txt', 'azw', 'azw3', 'azw4', 'cbc', 'cbr', 'cbz', 'chm', 'epub', 'fb2', 944 'htm', 'html', 'htmlz', 'lit', 'lrf', 'mobi', 'pdb', 'pml', 'prc', 945 'rb', 'snb', 'tcr', 'txtz' 946 ]; 947 948 // Chinese conversion configurations 949 const conversionConfigs = { 950 'none': { name: 'No Conversion', config: null }, 951 't2s': { name: 'Traditional to Simplified', config: 't2s' }, 952 's2t': { name: 'Simplified to Traditional', config: 's2t' }, 953 's2tw': { name: 'Simplified to Taiwan', config: 's2tw' }, 954 's2hk': { name: 'Simplified to Hong Kong', config: 's2hk' } 955 }; 956 957 // Output format configurations 958 const outputFormats = { 959 'txt': { name: 'Plain Text', extension: 'txt' }, 960 'epub': { name: 'EPUB', extension: 'epub' }, 961 'mobi': { name: 'MOBI', extension: 'mobi' }, 962 'azw3': { name: 'AZW3', extension: 'azw3' }, 963 'pdf': { name: 'PDF', extension: 'pdf' }, 964 'fb2': { name: 'FB2', extension: 'fb2' }, 965 'rtf': { name: 'RTF', extension: 'rtf' }, 966 'docx': { name: 'DOCX', extension: 'docx' } 967 }; 968 969 // Debug functions 970 function addDebugLog(message) { 971 if (debugMode) { 972 const debugOutput = document.getElementById('debugOutput'); 973 const timestamp = new Date().toLocaleTimeString(); 974 debugOutput.innerHTML += `[${timestamp}] ${message}\n`; 975 debugOutput.scrollTop = debugOutput.scrollHeight; 976 } 977 console.log('Debug:', message); 978 } 979 980 function showDebugSection() { 981 const debugSection = document.getElementById('debugSection'); 982 debugSection.style.display = debugSection.style.display === 'none' ? 'block' : 'none'; 983 } 984 985 function toggleDebug() { 986 debugMode = !debugMode; 987 const debugOutput = document.getElementById('debugOutput'); 988 if (debugMode) { 989 debugOutput.innerHTML = 'Debug mode enabled. Processing information will appear here.\n'; 990 addDebugLog('Debug mode activated'); 991 } else { 992 debugOutput.innerHTML = ''; 993 } 994 } 995 996 // Initialize OpenCC 997 function initializeOpenCC() { 998 const statusDiv = document.getElementById('openccStatus'); 999 const statusText = document.getElementById('openccStatusText'); 1000 1001 try { 1002 statusDiv.style.display = 'block'; 1003 statusText.textContent = 'ð Loading OpenCC...'; 1004 addDebugLog('Initializing OpenCC library...'); 1005 1006 // Check if OpenCC is loaded 1007 if (typeof OpenCC === 'undefined') {
1008 throw new Error('OpenCC library not loaded'); 1009 } 1010 1011 // Initialize converters 1012 openccConverters = { 1013 't2s': OpenCC.Converter({ from: 'tw', to: 'cn' }), 1014 's2t': OpenCC.Converter({ from: 'cn', to: 'tw' }), 1015 's2tw': OpenCC.Converter({ from: 'cn', to: 'tw' }), 1016 's2hk': OpenCC.Converter({ from: 'cn', to: 'hk' }) 1017 }; 1018 1019 // Test the converters 1020 const testResult = openccConverters['s2t']('æµè¯'); 1021 addDebugLog(`OpenCC test: æµè¯ â ${testResult}`); 1022 statusText.textContent = `â OpenCC Ready (Test: æµè¯ â ${testResult})`; 1023 1024 } catch (error) { 1025 addDebugLog(`OpenCC initialization failed: ${error.message}`); 1026 console.error('OpenCC initialization error:', error); 1027 statusText.textContent = 'â OpenCC failed to load - Chinese conversion unavailable'; 1028 } 1029 } 1030 1031 // TXT file reader with better Chinese encoding support 1032 async function readTxtFile(file) { 1033 return new Promise(async (resolve, reject) => { 1034 try { 1035 // First read as array buffer to detect encoding 1036 const arrayBuffer = await file.arrayBuffer(); 1037 const uint8Array = new Uint8Array(arrayBuffer); 1038 1039 // Try to detect encoding 1040 let detectedText = null; 1041 1042 // Check for BOM 1043 if (uint8Array[0] === 0xEF && uint8Array[1] === 0xBB && uint8Array[2] === 0xBF) { 1044 // UTF-8 with BOM 1045 const decoder = new TextDecoder('utf-8'); 1046 detectedText = decoder.decode(uint8Array.slice(3)); 1047 addDebugLog('Detected UTF-8 with BOM'); 1048 } else if (uint8Array[0] === 0xFF && uint8Array[1] === 0xFE) { 1049 // UTF-16 LE 1050 const decoder = new TextDecoder('utf-16le'); 1051 detectedText = decoder.decode(uint8Array.slice(2)); 1052 addDebugLog('Detected UTF-16 LE'); 1053 } else if (uint8Array[0] === 0xFE && uint8Array[1] === 0xFF) { 1054 // UTF-16 BE 1055 const decoder = new TextDecoder('utf-16be'); 1056 detectedText = decoder.decode(uint8Array.slice(2)); 1057 addDebugLog('Detected UTF-16 BE'); 1058 } else { 1059 // No BOM, try different encodings 1060 const encodings = ['utf-8', 'gb18030', 'gbk', 'big5', 'shift_jis']; 1061 1062 for (const encoding of encodings) { 1063 try { 1064 const decoder = new TextDecoder(encoding, { fatal: true }); 1065 const text = decoder.decode(uint8Array); 1066 1067 // Simple heuristic: check if we get valid Chinese characters 1068 const chineseRegex = /[\u4e00-\u9fa5]/; 1069 if (chineseRegex.test(text) || encoding === 'utf-8') { 1070 detectedText = text; 1071 addDebugLog(`Successfully decoded with ${encoding}`); 1072 break; 1073 } 1074 } catch (e) { 1075 // This encoding didn't work, try next 1076 continue; 1077 } 1078 } 1079 1080 // If still no success, try with encoding-japanese library if available 1081 if (!detectedText && typeof Encoding !== 'undefined') { 1082 try { 1083 const detected = Encoding.detect(uint8Array); 1084 addDebugLog(`Encoding.js detected: ${detected}`); 1085 1086 // Convert to UTF-8 1087 const utf8Array = Encoding.convert(uint8Array, { 1088 to: 'UTF8', 1089 from: detected 1090 }); 1091 1092 const decoder = new TextDecoder('utf-8'); 1093 detectedText = decoder.decode(new Uint8Array(utf8Array)); 1094 addDebugLog('Converted to UTF-8 using Encoding.js'); 1095 } catch (e) { 1096 addDebugLog('Encoding.js conversion failed'); 1097 } 1098 } 1099 } 1100 1101 if (detectedText) { 1102 resolve(detectedText); 1103 } else { 1104 // Last resort: try to read as UTF-8 and ignore errors 1105 const decoder = new TextDecoder('utf-8', { fatal: false }); 1106 const text = decoder.decode(uint8Array); 1107 addDebugLog('Fallback to UTF-8 with replacement characters'); 1108 resolve(text); 1109 } 1110 1111 } catch (error) { 1112 addDebugLog(`Error reading TXT file: ${error.message}`); 1113 reject(new Error('Failed to read TXT file: ' + error.message)); 1114 } 1115 }); 1116 } 1117 1118 // EPUB to TXT converter (local processing) - FIXED to preserve paragraphs 1119 async function convertEpubToText(file) { 1120 try { 1121 addDebugLog(`Starting local EPUB to TXT conversion: ${file.name}`); 1122 1123 // Load the EPUB file as a zip 1124 const zip = new JSZip(); 1125 const epubData = await zip.loadAsync(file); 1126 1127 // Find and parse container.xml to get the OPF file location 1128 let opfPath = null; 1129 try { 1130 const containerXml = await epubData.file('META-INF/container.xml').async('string'); 1131 const parser = new DOMParser(); 1132 const containerDoc = parser.parseFromString(containerXml, 'text/xml'); 1133 const rootfile = containerDoc.querySelector('rootfile'); 1134 opfPath = rootfile ? rootfile.getAttribute('full-path') : null; 1135 } catch (e) { 1136 addDebugLog('Failed to parse container.xml, searching for OPF file...'); 1137 } 1138 1139 // If we can't find OPF path from container, search for it 1140 if (!opfPath) { 1141 for (let filename in epubData.files) { 1142 if (filename.endsWith('.opf')) { 1143 opfPath = filename; 1144 break; 1145 } 1146 } 1147 } 1148 1149 if (!opfPath) { 1150 throw new Error('Cannot find OPF file in EPUB'); 1151 } 1152 1153 addDebugLog(`Found OPF file at: ${opfPath}`); 1154 const opfDir = opfPath.substring(0, opfPath.lastIndexOf('/')); 1155 1156 // Parse the OPF file 1157 const opfContent = await epubData.file(opfPath).async('string'); 1158 const parser = new DOMParser(); 1159 const opfDoc = parser.parseFromString(opfContent, 'text/xml'); 1160 1161 // Get the spine order 1162 const spineItems = Array.from(opfDoc.querySelectorAll('spine itemref')); 1163 const manifest = opfDoc.querySelector('manifest'); 1164 1165 // Extract text from each spine item in order 1166 let fullText = ''; 1167 let chapterCount = 0; 1168 1169 for (const itemRef of spineItems) { 1170 const idref = itemRef.getAttribute('idref'); 1171 const item = manifest.querySelector(`item[id="${idref}"]`); 1172 1173 if (!item) continue; 1174 1175 const href = item.getAttribute('href'); 1176 const mediaType = item.getAttribute('media-type'); 1177 1178 // Only process HTML/XHTML files 1179 if (!mediaType || !mediaType.includes('html')) continue; 1180 1181 // Construct the full path 1182 const fullPath = opfDir ? `${opfDir}/${href}` : href; 1183 1184 try { 1185 const htmlContent = await epubData.file(fullPath).async('string'); 1186 const htmlDoc = parser.parseFromString(htmlContent, 'text/html'); 1187 1188 // Remove script and style elements 1189 htmlDoc.querySelectorAll('script, style').forEach(el => el.remove()); 1190 1191 // Process paragraphs and preserve structure 1192 let chapterText = ''; 1193 1194 // Function to extract text while preserving paragraph breaks 1195 function extractTextWithParagraphs(element) { 1196 let text = ''; 1197 1198 // Process all block-level elements that typically contain paragraphs 1199 const blockElements = element.querySelectorAll('p, div, h1, h2, h3, h4, h5, h6, li, blockquote, pre'); 1200 1201 if (blockElements.length > 0) {
1202 blockElements.forEach(block => { 1203 const blockText = block.textContent.trim(); 1204 if (blockText) { 1205 // Add proper spacing based on element type 1206 if (block.tagName.match(/^H[1-6]$/)) { 1207 // Headers get extra spacing 1208 text += '\n\n' + blockText + '\n\n'; 1209 } else if (block.tagName === 'LI') { 1210 // List items 1211 text += '\n⢠' + blockText; 1212 } else { 1213 // Regular paragraphs 1214 text += '\n\n' + blockText; 1215 } 1216 } 1217 }); 1218 } else { 1219 // Fallback for content without clear paragraph structure 1220 // Split by <br> tags 1221 const html = element.innerHTML; 1222 const lines = html.split(/<br\s*\/?>/gi); 1223 1224 lines.forEach(line => { 1225 // Remove HTML tags and get text 1226 const tempDiv = document.createElement('div'); 1227 tempDiv.innerHTML = line; 1228 const lineText = tempDiv.textContent.trim(); 1229 if (lineText) { 1230 text += '\n\n' + lineText; 1231 } 1232 }); 1233 1234 // If still no content, just get all text 1235 if (!text.trim()) { 1236 text = element.textContent.trim(); 1237 } 1238 } 1239 1240 return text.trim(); 1241 } 1242 1243 // Extract text from body or document element 1244 if (htmlDoc.body) { 1245 chapterText = extractTextWithParagraphs(htmlDoc.body); 1246 } else if (htmlDoc.documentElement) { 1247 chapterText = extractTextWithParagraphs(htmlDoc.documentElement); 1248 } 1249 1250 // Clean up excessive newlines but preserve paragraph breaks 1251 chapterText = chapterText 1252 .replace(/\n{4,}/g, '\n\n\n') // Limit to max 3 newlines 1253 .replace(/[ \t]+/g, ' ') // Replace multiple spaces/tabs with single space 1254 .trim(); 1255 1256 if (chapterText) { 1257 chapterCount++; 1258 // Add chapter separator 1259 fullText += `\n\nââââââââââââââââââââââââââââââ\n`; 1260 fullText += `Chapter ${chapterCount}\n`; 1261 fullText += `ââââââââââââââââââââââââââââââ\n\n`; 1262 fullText += chapterText; 1263 } 1264 } catch (e) { 1265 addDebugLog(`Failed to process spine item: ${fullPath}`); 1266 } 1267 } 1268 1269 // Get metadata 1270 const title = opfDoc.querySelector('metadata title')?.textContent || 'Unknown Title'; 1271 const author = opfDoc.querySelector('metadata creator')?.textContent || 'Unknown Author'; 1272 1273 // Construct final text with metadata 1274 const finalText = `âââââââââââââââââââââââââââââââââââââââ\n` + 1275 `Title: ${title}\n` + 1276 `Author: ${author}\n` + 1277 `âââââââââââââââââââââââââââââââââââââââ\n` + 1278 fullText; 1279 1280 addDebugLog(`EPUB conversion completed: ${finalText.length} characters extracted from ${chapterCount} chapters`); 1281 1282 return finalText; 1283 1284 } catch (error) { 1285 addDebugLog(`EPUB to TXT conversion error: ${error.message}`); 1286 throw new Error(`Failed to convert EPUB: ${error.message}`); 1287 } 1288 } 1289 1290 // NEW: Convert Chinese text directly in EPUB 1291 async function convertChineseInEpub(file, conversionType) { 1292 try { 1293 addDebugLog(`Starting direct EPUB Chinese conversion: ${file.name} (${conversionType})`); 1294 1295 // Load the EPUB file as a zip 1296 const zip = new JSZip(); 1297 const epubData = await zip.loadAsync(file); 1298 1299 // Find the OPF file 1300 let opfPath = null; 1301 try { 1302 const containerXml = await epubData.file('META-INF/container.xml').async('string'); 1303 const parser = new DOMParser(); 1304 const containerDoc = parser.parseFromString(containerXml, 'text/xml'); 1305 const rootfile = containerDoc.querySelector('rootfile'); 1306 opfPath = rootfile ? rootfile.getAttribute('full-path') : null; 1307 } catch (e) { 1308 addDebugLog('Failed to parse container.xml, searching for OPF file...'); 1309 } 1310 1311 if (!opfPath) { 1312 for (let filename in epubData.files) { 1313 if (filename.endsWith('.opf')) { 1314 opfPath = filename; 1315 break; 1316 } 1317 } 1318 } 1319 1320 if (!opfPath) { 1321 throw new Error('Cannot find OPF file in EPUB'); 1322 } 1323 1324 addDebugLog(`Found OPF file at: ${opfPath}`); 1325 const opfDir = opfPath.substring(0, opfPath.lastIndexOf('/')); 1326 1327 // Parse the OPF file 1328 const opfContent = await epubData.file(opfPath).async('string'); 1329 const parser = new DOMParser(); 1330 const opfDoc = parser.parseFromString(opfContent, 'text/xml'); 1331 1332 // Convert metadata 1333 const titleElement = opfDoc.querySelector('metadata title'); 1334 if (titleElement) { 1335 titleElement.textContent = convertChineseText(titleElement.textContent, conversionType); 1336 } 1337 1338 const creatorElements = opfDoc.querySelectorAll('metadata creator');
1339 creatorElements.forEach(creator => { 1340 creator.textContent = convertChineseText(creator.textContent, conversionType); 1341 }); 1342 1343 // Update OPF in zip 1344 const serializer = new XMLSerializer(); 1345 const updatedOpf = serializer.serializeToString(opfDoc); 1346 epubData.file(opfPath, updatedOpf); 1347 1348 // Get all HTML/XHTML files from manifest 1349 const manifest = opfDoc.querySelector('manifest'); 1350 const items = manifest.querySelectorAll('item'); 1351 1352 let convertedCount = 0; 1353 for (const item of items) { 1354 const href = item.getAttribute('href'); 1355 const mediaType = item.getAttribute('media-type'); 1356 1357 // Only process HTML/XHTML files 1358 if (!mediaType || !mediaType.includes('html')) continue; 1359 1360 const fullPath = opfDir ? `${opfDir}/${href}` : href; 1361 1362 try { 1363 const htmlContent = await epubData.file(fullPath).async('string'); 1364 1365 // Parse HTML 1366 const htmlDoc = parser.parseFromString(htmlContent, 'text/html'); 1367 1368 // Function to convert text in a node and its children 1369 function convertNodeText(node) { 1370 if (node.nodeType === Node.TEXT_NODE) { 1371 if (node.textContent.trim()) { 1372 node.textContent = convertChineseText(node.textContent, conversionType); 1373 } 1374 } else if (node.nodeType === Node.ELEMENT_NODE) { 1375 // Skip script and style elements 1376 if (node.tagName !== 'SCRIPT' && node.tagName !== 'STYLE') { 1377 for (let i = 0; i < node.childNodes.length; i++) { 1378 convertNodeText(node.childNodes[i]); 1379 } 1380 } 1381 } 1382 } 1383 1384 // Convert all text nodes 1385 convertNodeText(htmlDoc.documentElement); 1386 1387 // Serialize back to string 1388 const updatedHtml = serializer.serializeToString(htmlDoc); 1389 1390 // Update the file in the zip 1391 epubData.file(fullPath, updatedHtml); 1392 convertedCount++; 1393 1394 addDebugLog(`Converted Chinese in: ${fullPath}`); 1395 1396 } catch (e) { 1397 addDebugLog(`Failed to convert Chinese in: ${fullPath} - ${e.message}`); 1398 } 1399 } 1400 1401 addDebugLog(`Direct EPUB Chinese conversion completed: ${convertedCount} files converted`); 1402 1403 // Generate the new EPUB blob 1404 const convertedEpubBlob = await epubData.generateAsync({ 1405 type: 'blob', 1406 compression: 'DEFLATE', 1407 compressionOptions: { level: 9 } 1408 }); 1409 1410 return convertedEpubBlob; 1411 1412 } catch (error) { 1413 addDebugLog(`Direct EPUB Chinese conversion error: ${error.message}`); 1414 throw new Error(`Failed to convert Chinese in EPUB: ${error.message}`); 1415 } 1416 } 1417 1418 // NEW: Convert TXT to EPUB locally 1419 async function convertTextToEpub(textContent, originalFileName) { 1420 try {
1421 addDebugLog(`Converting TXT to EPUB locally: ${originalFileName}`); 1422 1423 const zip = new JSZip(); 1424 1425 // Add mimetype (must be first file, uncompressed) 1426 zip.file('mimetype', 'application/epub+zip', { compression: 'STORE' }); 1427 1428 // Create META-INF directory 1429 const metaInf = zip.folder('META-INF'); 1430 1431 // Add container.xml 1432 const containerXml = `<?xml version="1.0" encoding="UTF-8"?> 1433<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container"> 1434 <rootfiles> 1435 <rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/> 1436 </rootfiles> 1437</container>`; 1438 metaInf.file('container.xml', containerXml); 1439 1440 // Create OEBPS directory 1441 const oebps = zip.folder('OEBPS'); 1442 1443 // Extract title from filename or first line 1444 const title = originalFileName.replace(/\.[^/.]+$/, '') || 'Untitled'; 1445 1446 // Create content.opf 1447 const contentOpf = `<?xml version="1.0" encoding="UTF-8"?> 1448<package version="3.0" xmlns="http://www.idpf.org/2007/opf" unique-identifier="uid"> 1449 <metadata xmlns:dc="http://purl.org/dc/elements/1.1/"> 1450 <dc:identifier id="uid">urn:uuid:${generateUUID()}</dc:identifier> 1451 <dc:title>${escapeXml(title)}</dc:title> 1452 <dc:language>zh</dc:language> 1453 <meta property="dcterms:modified">${new Date().toISOString().split('.')[0] + 'Z'}</meta> 1454 </metadata> 1455 <manifest> 1456 <item id="text1" href="text.xhtml" media-type="application/xhtml+xml"/> 1457 <item id="nav" href="nav.xhtml" media-type="application/xhtml+xml" properties="nav"/> 1458 </manifest> 1459 <spine> 1460 <itemref idref="text1"/> 1461 </spine> 1462</package>`; 1463 oebps.file('content.opf', contentOpf); 1464 1465 // Convert text to XHTML with proper paragraph formatting 1466 const paragraphs = textContent.split(/\n\n+/); 1467 const htmlContent = paragraphs 1468 .filter(p => p.trim()) 1469 .map(p => ` <p>${escapeXml(p.trim())}</p>`) 1470 .join('\n'); 1471 1472 const textXhtml = `<?xml version="1.0" encoding="UTF-8"?> 1473<!DOCTYPE html> 1474<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="zh" lang="zh"> 1475<head> 1476 <title>${escapeXml(title)}</title> 1477 <meta charset="UTF-8"/> 1478</head> 1479<body> 1480 <h1>${escapeXml(title)}</h1> 1481${htmlContent} 1482</body> 1483</html>`; 1484 oebps.file('text.xhtml', textXhtml); 1485 1486 // Create navigation document 1487 const navXhtml = `<?xml version="1.0" encoding="UTF-8"?> 1488<!DOCTYPE html> 1489<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="zh" lang="zh"> 1490<head> 1491 <title>Navigation</title> 1492 <meta charset="UTF-8"/> 1493</head> 1494<body> 1495 <nav epub:type="toc"> 1496 <h1>ç®å½</h1> 1497 <ol> 1498 <li><a href="text.xhtml">${escapeXml(title)}</a></li> 1499 </ol> 1500 </nav> 1501</body> 1502</html>`; 1503 oebps.file('nav.xhtml', navXhtml); 1504 1505 // Generate EPUB blob 1506 const epubBlob = await zip.generateAsync({ 1507 type: 'blob', 1508 compression: 'DEFLATE', 1509 compressionOptions: { level: 9 } 1510 }); 1511 1512 addDebugLog(`TXT to EPUB conversion completed: ${epubBlob.size} bytes`); 1513 return epubBlob; 1514 1515 } catch (error) { 1516 addDebugLog(`TXT to EPUB conversion error: ${error.message}`); 1517 throw new Error(`Failed to convert TXT to EPUB: ${error.message}`); 1518 } 1519 } 1520 1521 // Helper functions for EPUB creation 1522 function generateUUID() { 1523 return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { 1524 const r = Math.random() * 16 | 0; 1525 const v = c === 'x' ? r : (r & 0x3 | 0x8); 1526 return v.toString(16); 1527 }); 1528 } 1529 1530 function escapeXml(text) { 1531 return text 1532 .replace(/&/g, '&') 1533 .replace(/</g, '<') 1534 .replace(/>/g, '>') 1535 .replace(/"/g, '"') 1536 .replace(/'/g, '''); 1537 } 1538 1539 // Check if file should use local conversion 1540 function shouldUseLocalConversion(file, outputFormat, conversionType) { 1541 const ext = file.name.toLowerCase().split('.').pop(); 1542 1543 // EPUB to TXT 1544 if (ext === 'epub' && outputFormat === 'txt') { 1545 return true; 1546 } 1547 1548 // EPUB direct Chinese conversion 1549 if (ext === 'epub' && conversionType !== 'none' && outputFormat === 'epub') { 1550 return true; 1551 } 1552 1553 // TXT to TXT with Chinese conversion 1554 if (ext === 'txt' && outputFormat === 'txt' && conversionType !== 'none') { 1555 return true; 1556 } 1557 1558 // TXT to any format when no Chinese conversion 1559 if (ext === 'txt' && conversionType === 'none') { 1560 return false; // Use API for format conversion 1561 } 1562 1563 return false; 1564 } 1565 1566 // New function to group files by conversion requirements 1567 function groupFilesByConversionType() { 1568 const groups = []; 1569 const localFiles = []; 1570 const batchableFiles = []; 1571 const individualFiles = []; 1572
1573 selectedFiles.forEach((file, index) => { 1574 const ext = file.name.toLowerCase().split('.').pop(); 1575 const fileInfo = { file, index, ext }; 1576 1577 if (shouldUseLocalConversion(file, selectedOutputFormat, selectedConversion)) { 1578 localFiles.push(fileInfo); 1579 } else { 1580 // Determine if file can be batched 1581 const canBatch = canFileBeBatched(file, ext); 1582 if (canBatch && batchableFiles.length < 10) { // Limit batch size 1583 batchableFiles.push(fileInfo); 1584 } else { 1585 if (batchableFiles.length > 0) { 1586 // Process current batch first 1587 groups.push({ type: 'api_batch', files: [...batchableFiles] }); 1588 batchableFiles.length = 0; 1589 } 1590 individualFiles.push(fileInfo); 1591 } 1592 } 1593 }); 1594 1595 // Add remaining groups 1596 if (localFiles.length > 0) { 1597 groups.push({ type: 'local', files: localFiles }); 1598 } 1599 if (batchableFiles.length > 0) { 1600 groups.push({ type: 'api_batch', files: batchableFiles }); 1601 } 1602 if (individualFiles.length > 0) { 1603 groups.push({ type: 'individual', files: individualFiles }); 1604 } 1605 1606 addDebugLog(`Grouped ${selectedFiles.length} files into ${groups.length} processing groups`); 1607 groups.forEach((group, i) => { 1608 addDebugLog(`Group ${i + 1}: ${group.type} (${group.files.length} files)`); 1609 }); 1610 1611 return groups; 1612 } 1613 1614 // Check if file can be batched (same conversion pipeline) 1615 function canFileBeBatched(file, ext) { 1616 // Files can be batched if they have the same conversion requirements 1617 // For now, batch similar file types that need the same conversion 1618 const commonFormats = ['epub', 'mobi', 'azw3', 'pdf', 'fb2']; 1619 return commonFormats.includes(ext); 1620 } 1621 1622 // Helper function to update progress 1623 function updateProgress(processed, total) { 1624 const progress = (processed / total) * 100; 1625 document.getElementById('progressFill').style.width = progress + '%'; 1626 } 1627 1628 // Process local files 1629 async function processLocalFile(fileInfo, processedCount) { 1630 const { file, index, ext } = fileInfo; 1631 const statusElement = document.getElementById(`status-${index}`); 1632 1633 try { 1634 statusElement.textContent = 'Processing...'; 1635 statusElement.className = 'file-status status-processing'; 1636 1637 addDebugLog(`Processing local file ${processedCount + 1}: ${file.name}`); 1638 1639 let finalContent = ''; 1640 let finalExtension = outputFormats[selectedOutputFormat].extension; 1641 1642 if (ext === 'epub' && selectedOutputFormat === 'txt') { 1643 const txtContent = await convertEpubToText(file); 1644 if (selectedConversion !== 'none') { 1645 addDebugLog(`Applying Chinese conversion (${selectedConversion})`); 1646 finalContent = convertChineseText(txtContent, selectedConversion); 1647 } else { 1648 finalContent = txtContent; 1649 } 1650 } else if (ext === 'epub' && selectedOutputFormat === 'epub' && selectedConversion !== 'none') { 1651 addDebugLog(`Direct EPUB Chinese conversion (${selectedConversion})`); 1652 finalContent = await convertChineseInEpub(file, selectedConversion); 1653 } else if (ext === 'txt' && selectedOutputFormat === 'txt' && selectedConversion !== 'none') { 1654 addDebugLog(`Direct TXT to TXT conversion with Chinese conversion (${selectedConversion})`); 1655 const txtContent = await readTxtFile(file); 1656 finalContent = convertChineseText(txtContent, selectedConversion); 1657 } 1658 1659 if (finalContent) { 1660 const outputFileName = file.name.replace(/\.[^/.]+$/, '') + '.' + finalExtension; 1661 await saveConvertedFile(outputFileName, finalContent, finalExtension); 1662 1663 convertedFiles.push({ 1664 originalName: file.name, 1665 content: finalContent, 1666 fileName: outputFileName, 1667 conversion: selectedConversion, 1668 outputFormat: selectedOutputFormat 1669 }); 1670 1671 statusElement.textContent = 'Completed'; 1672 statusElement.className = 'file-status status-success'; 1673 addDebugLog(`File successfully converted and saved: ${outputFileName}`); 1674 } 1675 } catch (error) { 1676 console.error('Error processing local file:', error); 1677 addDebugLog(`Error processing ${file.name}: ${error.message}`); 1678 statusElement.textContent = `Error: ${error.message}`; 1679 statusElement.className = 'file-status status-error'; 1680 } 1681 } 1682 1683// Process multiple files in a single API batch 1684async function processBatchFiles(fileInfos, startIndex) { 1685 const apiKey = getCurrentApiKey(); 1686 1687 addDebugLog(`Processing batch of ${fileInfos.length} files via CloudConvert API`); 1688 1689 // Update all file statuses to processing 1690 fileInfos.forEach(fileInfo => { 1691 const statusElement = document.getElementById(`status-${fileInfo.index}`); 1692 statusElement.textContent = 'Processing (Batch)...'; 1693 statusElement.className = 'file-status status-processing'; 1694 });
1695 1696 try { 1697 // Step 1: Create batch job with multiple upload and convert tasks 1698 const tasks = {}; 1699 1700 // Create upload and convert tasks for each file 1701 fileInfos.forEach((fileInfo, i) => { 1702 const uploadTaskName = `upload-file-${i}`; 1703 const convertTaskName = `convert-file-${i}`; 1704 const exportTaskName = `export-file-${i}`; 1705 1706 tasks[uploadTaskName] = { 1707 operation: 'import/upload' 1708 }; 1709 1710 // Always convert to EPUB first if Chinese conversion is needed 1711 let targetFormat = selectedOutputFormat; 1712 let convertOptions = {}; 1713 1714 if (selectedConversion !== 'none') { 1715 // Always convert to EPUB first for Chinese conversion 1716 targetFormat = 'epub'; 1717 } else if (targetFormat === 'txt') { 1718 convertOptions = { output_encoding: 'utf-8' }; 1719 } 1720 1721 tasks[convertTaskName] = { 1722 operation: 'convert', 1723 input: uploadTaskName, 1724 output_format: targetFormat, 1725 options: convertOptions 1726 }; 1727 1728 tasks[exportTaskName] = { 1729 operation: 'export/url', 1730 input: convertTaskName 1731 }; 1732 }); 1733 1734 const jobResponse = await fetch('https://api.cloudconvert.com/v2/jobs', { 1735 method: 'POST', 1736 headers: { 1737 'Authorization': `Bearer ${apiKey}`, 1738 'Content-Type': 'application/json' 1739 }, 1740 body: JSON.stringify({ tasks }) 1741 }); 1742 1743 if (!jobResponse.ok) { 1744 const errorText = await jobResponse.text(); 1745 addDebugLog(`Batch job creation failed: ${jobResponse.status} - ${errorText}`); 1746 throw new Error(`Failed to create batch job: ${jobResponse.status}`); 1747 } 1748 1749 const jobData = await jobResponse.json(); 1750 const jobId = jobData.data.id; 1751 addDebugLog(`Batch job created: ${jobId} with ${fileInfos.length} files`); 1752 1753 // Step 2: Upload all files 1754 const uploadPromises = fileInfos.map(async (fileInfo, i) => { 1755 const uploadTask = jobData.data.tasks.find(task => task.name === `upload-file-${i}`); 1756 const uploadUrl = uploadTask.result.form.url; 1757 const uploadParams = uploadTask.result.form.parameters; 1758 1759 const formData = new FormData(); 1760 Object.keys(uploadParams).forEach(key => { 1761 formData.append(key, uploadParams[key]); 1762 }); 1763 formData.append('file', fileInfo.file); 1764 1765 const uploadResponse = await fetch(uploadUrl, { 1766 method: 'POST', 1767 body: formData 1768 }); 1769 1770 if (!uploadResponse.ok) { 1771 throw new Error(`Failed to upload ${fileInfo.file.name}: ${uploadResponse.status}`); 1772 } 1773 1774 addDebugLog(`Uploaded file ${i + 1}/${fileInfos.length}: ${fileInfo.file.name}`); 1775 }); 1776 1777 await Promise.all(uploadPromises); 1778 addDebugLog('All files uploaded successfully, waiting for batch conversion...'); 1779 1780 // Step 3: Wait for batch job completion 1781 let jobStatus = 'processing'; 1782 let attempts = 0; 1783 const maxAttempts = 120; // Longer timeout for batch jobs 1784 1785 while (jobStatus === 'processing' && attempts < maxAttempts) { 1786 await new Promise(resolve => setTimeout(resolve, 5000)); 1787 1788 const statusResponse = await fetch(`https://api.cloudconvert.com/v2/jobs/${jobId}`, { 1789 headers: { 'Authorization': `Bearer ${apiKey}` } 1790 }); 1791 1792 if (!statusResponse.ok) { 1793 throw new Error(`Failed to check batch job status: ${statusResponse.status}`); 1794 } 1795 1796 const statusData = await statusResponse.json(); 1797 jobStatus = statusData.data.status; 1798 attempts++; 1799 1800 addDebugLog(`Batch job status check ${attempts}: ${jobStatus}`); 1801 1802 if (jobStatus === 'error') { 1803 const errorMessage = statusData.data.message || 'Unknown error'; 1804 throw new Error(`Batch conversion failed: ${errorMessage}`); 1805 } 1806 } 1807 1808 if (jobStatus !== 'finished') { 1809 throw new Error('Batch conversion timed out'); 1810 } 1811 1812 // Step 4: Download and process all results 1813 const finalStatusResponse = await fetch(`https://api.cloudconvert.com/v2/jobs/${jobId}`, { 1814 headers: { 'Authorization': `Bearer ${apiKey}` } 1815 }); 1816 1817 const finalStatusData = await finalStatusResponse.json(); 1818 1819 // Process each file result 1820 for (let i = 0; i < fileInfos.length; i++) { 1821 const fileInfo = fileInfos[i]; 1822 const statusElement = document.getElementById(`status-${fileInfo.index}`); 1823 1824 try { 1825 const exportTask = finalStatusData.data.tasks.find(task => task.name === `export-file-${i}`); 1826 1827 if (!exportTask || !exportTask.result || !exportTask.result.files[0]) { 1828 throw new Error('No output file found'); 1829 } 1830 1831 const downloadUrl = exportTask.result.files[0].url; 1832 const response = await fetch(downloadUrl); 1833 1834 if (!response.ok) { 1835 throw new Error(`Failed to download: ${response.status}`); 1836 } 1837 1838 let finalContent; 1839 let finalExtension = outputFormats[selectedOutputFormat].extension; 1840 1841 // Handle conversion pipeline 1842 if (selectedConversion !== 'none') { 1843 // We have an EPUB that needs Chinese conversion 1844 const epubBlob = await response.blob(); 1845 const epubFile = new File([epubBlob], 'temp.epub', { type: 'application/epub+zip' }); 1846 const convertedEpub = await convertChineseInEpub(epubFile, selectedConversion); 1847 1848 if (selectedOutputFormat === 'epub') { 1849 // Final output is EPUB, we're done 1850 finalContent = convertedEpub; 1851 } else if (selectedOutputFormat === 'txt') { 1852 // Convert EPUB to TXT locally 1853 const tempFile = new File([convertedEpub], 'temp.epub', { type: 'application/epub+zip' }); 1854 finalContent = await convertEpubToText(tempFile); 1855 } else { 1856 // Need to convert to another format via API 1857 finalContent = await convertEpubToFormat(convertedEpub, selectedOutputFormat, fileInfo.file.name); 1858 } 1859 } else { 1860 // No Chinese conversion, direct result 1861 if (selectedOutputFormat === 'txt') { 1862 finalContent = await response.text(); 1863 } else { 1864 finalContent = await response.blob(); 1865 } 1866 } 1867 1868 const outputFileName = fileInfo.file.name.replace(/\.[^/.]+$/, '') + '.' + finalExtension; 1869 await saveConvertedFile(outputFileName, finalContent, finalExtension); 1870 1871 convertedFiles.push({ 1872 originalName: fileInfo.file.name, 1873 content: finalContent, 1874 fileName: outputFileName, 1875 conversion: selectedConversion, 1876 outputFormat: selectedOutputFormat 1877 }); 1878 1879 statusElement.textContent = 'Completed'; 1880 statusElement.className = 'file-status status-success';
1881 addDebugLog(`Batch file ${i + 1} completed: ${outputFileName}`); 1882 1883 } catch (error) { 1884 console.error(`Error processing batch file ${i + 1}:`, error); 1885 addDebugLog(`Error processing batch file ${fileInfo.file.name}: ${error.message}`); 1886 statusElement.textContent = `Error: ${error.message}`; 1887 statusElement.className = 'file-status status-error'; 1888 } 1889 } 1890 1891 addDebugLog(`Batch processing completed: ${fileInfos.length} files processed`); 1892 1893 } catch (error) { 1894 addDebugLog(`Batch processing error: ${error.message}`); 1895 throw error; 1896 } 1897} 1898 1899// Process individual files (for special cases and non-batchable files) 1900async function processIndividualFile(fileInfo, processedCount) { 1901 const { file, index, ext } = fileInfo; 1902 const statusElement = document.getElementById(`status-${index}`); 1903 1904 try { 1905 statusElement.textContent = 'Processing...'; 1906 statusElement.className = 'file-status status-processing'; 1907 1908 addDebugLog(`Processing individual file ${processedCount + 1}: ${file.name}`); 1909 1910 let finalContent = ''; 1911 let finalExtension = outputFormats[selectedOutputFormat].extension; 1912 1913 // Handle different conversion scenarios 1914 if (selectedConversion !== 'none') { 1915 // Need Chinese conversion 1916 addDebugLog(`Step 1: Converting ${file.name} to EPUB for Chinese processing`); 1917 1918 let epubContent; 1919 if (ext === 'epub') { 1920 epubContent = file; 1921 } else if (ext === 'txt') { 1922 // Convert TXT to EPUB locally 1923 const txtContent = await readTxtFile(file); 1924 epubContent = await convertTextToEpub(txtContent, file.name); 1925 } else { 1926 // Convert to EPUB via API 1927 epubContent = await convertWithCloudConvert(file, 'epub'); 1928 } 1929 1930 addDebugLog(`Step 2: Applying Chinese conversion (${selectedConversion})`); 1931 const epubFile = epubContent instanceof Blob ? 1932 new File([epubContent], 'temp.epub', { type: 'application/epub+zip' }) : 1933 epubContent; 1934 const convertedEpub = await convertChineseInEpub(epubFile, selectedConversion); 1935 1936 if (selectedOutputFormat === 'epub') { 1937 // Final output is EPUB, we're done 1938 finalContent = convertedEpub; 1939 addDebugLog(`Chinese conversion completed, output is EPUB`); 1940 } else if (selectedOutputFormat === 'txt') { 1941 // Convert EPUB to TXT locally 1942 addDebugLog(`Step 3: Converting processed EPUB to TXT locally`); 1943 const tempFile = new File([convertedEpub], 'temp.epub', { type: 'application/epub+zip' }); 1944 finalContent = await convertEpubToText(tempFile); 1945 } else { 1946 // Convert to other format via API 1947 addDebugLog(`Step 3: Converting processed EPUB to ${selectedOutputFormat.toUpperCase()}`); 1948 finalContent = await convertEpubToFormat(convertedEpub, selectedOutputFormat, file.name); 1949 } 1950 1951 } else { 1952 // Direct conversion without Chinese processing 1953 addDebugLog(`Direct conversion: ${file.name} to ${selectedOutputFormat.toUpperCase()}`); 1954 if (ext === 'epub' && selectedOutputFormat === 'txt') { 1955 // Local EPUB to TXT conversion 1956 finalContent = await convertEpubToText(file); 1957 } else { 1958 finalContent = await convertWithCloudConvert(file, selectedOutputFormat); 1959 } 1960 } 1961 1962 if (finalContent) { 1963 const outputFileName = file.name.replace(/\.[^/.]+$/, '') + '.' + finalExtension; 1964 await saveConvertedFile(outputFileName, finalContent, finalExtension); 1965 1966 convertedFiles.push({ 1967 originalName: file.name, 1968 content: finalContent, 1969 fileName: outputFileName, 1970 conversion: selectedConversion, 1971 outputFormat: selectedOutputFormat 1972 }); 1973 1974 statusElement.textContent = 'Completed'; 1975 statusElement.className = 'file-status status-success'; 1976 addDebugLog(`File successfully converted and saved: ${outputFileName}`); 1977 } 1978 } catch (error) { 1979 console.error('Error processing individual file:', error); 1980 addDebugLog(`Error processing ${file.name}: ${error.message}`); 1981 statusElement.textContent = `Error: ${error.message}`; 1982 statusElement.className = 'file-status status-error'; 1983 } 1984} 1985 1986 function selectOutputFormat(format, event) { 1987 if (event) { 1988 event.preventDefault(); 1989 event.stopPropagation(); 1990 } 1991 1992 selectedOutputFormat = format; 1993 addDebugLog(`Output format selected: ${outputFormats[format].name}`); 1994 1995 // Update UI 1996 document.querySelectorAll('.format-option').forEach(option => { 1997 option.classList.remove('selected'); 1998 }); 1999 const targetElement = event ? event.currentTarget : document.querySelector(`.format-option input[value="${format}"]`).closest('.format-option'); 2000 targetElement.classList.add('selected'); 2001 2002 // Update radio button 2003 document.querySelector(`input[value="${format}"]`).checked = true; 2004 2005 updateConversionPipeline(); 2006 updateConvertButton(); 2007 displayFileList(); // Refresh file list to update method indicators 2008 } 2009 2010 function selectConversionOption(type, event) { 2011 if (event) { 2012 event.preventDefault(); 2013 event.stopPropagation(); 2014 } 2015 2016 selectedConversion = type; 2017 addDebugLog(`Chinese conversion selected: ${conversionConfigs[type].name}`); 2018 2019 // Update UI 2020 document.querySelectorAll('.conversion-option').forEach(option => { 2021 option.classList.remove('selected'); 2022 }); 2023 const targetElement = event ? event.currentTarget : document.querySelector(`.conversion-option input[value="${type}"]`).closest('.conversion-option'); 2024 targetElement.classList.add('selected'); 2025 2026 // Update radio button 2027 document.querySelector(`input[value="${type}"]`).checked = true; 2028 2029 updateConversionPipeline(); 2030 updateConvertButton(); 2031 displayFileList(); // Refresh file list to update method indicators 2032 } 2033 2034 function updateConversionPipeline() { 2035 const pipelineDiv = document.getElementById('conversionPipeline'); 2036 const pipelineText = document.getElementById('pipelineText'); 2037 2038 if (selectedConversion !== 'none' && selectedOutputFormat !== 'txt' && selectedOutputFormat !== 'epub') { 2039 pipelineDiv.style.display = 'block'; 2040 pipelineText.textContent = `Input â Convert to EPUB â OpenCC (${conversionConfigs[selectedConversion].name}) â CloudConvert to ${outputFormats[selectedOutputFormat].name.toUpperCase()}`; 2041 } else if (selectedConversion !== 'none' && selectedOutputFormat === 'epub') { 2042 pipelineDiv.style.display = 'block'; 2043 pipelineText.textContent = `Input â EPUB â OpenCC Direct Conversion (${conversionConfigs[selectedConversion].name})`; 2044 } else if (selectedConversion !== 'none' && selectedOutputFormat === 'txt') { 2045 pipelineDiv.style.display = 'block'; 2046 pipelineText.textContent = `Input â Convert to TXT â OpenCC (${conversionConfigs[selectedConversion].name}) â Final TXT`; 2047 } else if (selectedConversion === 'none' && selectedOutputFormat !== 'txt') { 2048 pipelineDiv.style.display = 'block'; 2049 pipelineText.textContent = `Input â CloudConvert to ${outputFormats[selectedOutputFormat].name.toUpperCase()}`; 2050 } else { 2051 pipelineDiv.style.display = 'none'; 2052 } 2053 } 2054 2055 function convertChineseText(text, conversionType) { 2056 if (conversionType === 'none' || !text) { 2057 return text; 2058 } 2059 2060 try {
2061 addDebugLog(`Converting Chinese text: ${conversionType} (${text.length} characters)`); 2062 2063 // Sample text for debug 2064 const sample = text.substring(0, 100); 2065 addDebugLog(`Sample before conversion: "${sample}..."`); 2066 2067 const converter = openccConverters[conversionType]; 2068 if (!converter) { 2069 throw new Error(`Converter ${conversionType} not initialized`); 2070 } 2071 2072 const convertedText = converter(text); 2073 2074 const convertedSample = convertedText.substring(0, 100); 2075 addDebugLog(`Sample after conversion: "${convertedSample}..."`); 2076 addDebugLog(`Conversion completed: ${convertedText.length} characters`); 2077 2078 return convertedText; 2079 } catch (error) { 2080 addDebugLog(`Chinese conversion failed: ${error.message}`); 2081 console.error('Chinese conversion error:', error); 2082 return text; // Return original text if conversion fails 2083 } 2084 } 2085 2086 // Initialize 2087 window.addEventListener('load', function() { 2088 document.getElementById('apiKey').value = DEFAULT_API_KEY; 2089 checkBrowserSupport(); 2090 updateConversionPipeline(); 2091 2092 // Initialize OpenCC 2093 initializeOpenCC(); 2094 }); 2095 2096 function checkBrowserSupport() { 2097 const browserSupport = document.getElementById('browserSupport'); 2098 2099 // Check if we're on HTTPS or localhost 2100 const isSecureContext = window.isSecureContext; 2101 const protocol = window.location.protocol; 2102 const hostname = window.location.hostname; 2103 2104 // Check File System Access API 2105 const hasFileSystemAccess = 'showDirectoryPicker' in window; 2106 2107 addDebugLog(`Protocol: ${protocol}, Hostname: ${hostname}, Secure Context: ${isSecureContext}, File System Access: ${hasFileSystemAccess}`); 2108 2109 if (hasFileSystemAccess && isSecureContext) { 2110 browserSupport.innerHTML = ` 2111 <strong>â Full Support:</strong> Your browser supports directory selection with universal ebook conversion.<br> 2112 <small>
2112Browser: ${navigator.userAgent.split(' ').slice(-2).join(' ')}</small> 2113 `; 2114 browserSupport.className = 'browser-support'; 2115 } else if (hasFileSystemAccess && !isSecureContext) { 2116 browserSupport.innerHTML = ` 2117 <strong>â ï¸ HTTPS Required:</strong> Directory selection requires HTTPS or localhost. 2118 Files will use default Downloads folder.<br> 2119 <small>Current: ${protocol}//${hostname}</small> 2120 `; 2121 browserSupport.className = 'browser-support warning'; 2122 document.getElementById('selectDirBtn').disabled = true; 2123 useDefaultDownloads(); 2124 } else { 2125 browserSupport.innerHTML = ` 2126 <strong>â ï¸ Limited Support:</strong> Your browser doesn't support directory selection. 2127 Files will be saved to your default Downloads folder.<br> 2128 <small>For full features, use Chrome/Edge on HTTPS</small> 2129 `; 2130 browserSupport.className = 'browser-support warning'; 2131 document.getElementById('selectDirBtn').style.display = 'none'; 2132 useDefaultDownloads(); 2133 } 2134 } 2135 2136 async function selectOutputDirectory() { 2137 if (!('showDirectoryPicker' in window)) { 2138 alert('Your browser doesn\'t support directory selection. Using default Downloads folder.'); 2139 useDefaultDownloads(); 2140 return; 2141 } 2142 2143 if (!window.isSecureContext) { 2144 alert('Directory selection requires HTTPS or localhost. Using default Downloads folder.'); 2145 useDefaultDownloads(); 2146 return; 2147 } 2148 2149 const selectBtn = document.getElementById('selectDirBtn'); 2150 selectBtn.disabled = true; 2151 selectBtn.textContent = 'ð Opening folder picker...'; 2152 2153 try { 2154 addDebugLog('Attempting to open directory picker...'); 2155 selectedDirectoryHandle = await window.showDirectoryPicker({ 2156 mode: 'readwrite', 2157 startIn: 'downloads' 2158 }); 2159 2160 addDebugLog(`Directory selected: ${selectedDirectoryHandle.name}`); 2161 2162 // Test if we can actually write to the directory 2163 try { 2164 const testFileName = `test_universal_${Date.now()}.txt`; 2165 const testContent = 'Universal Ebook Converter Test: æµè¯ææ¬ ç¹é«ä¸æ ð'; 2166 const testFileHandle = await selectedDirectoryHandle.getFileHandle(testFileName, { create: true }); 2167 const writable = await testFileHandle.createWritable(); 2168 await writable.write(new Blob([testContent], { type: 'text/plain; charset=utf-8' })); 2169 await writable.close(); 2170 2171 // Clean up test file 2172 await selectedDirectoryHandle.removeEntry(testFileName); 2173 addDebugLog('Universal converter write test successful'); 2174 2175 downloadMethod = 'filesystem'; 2176 2177 const directorySection = document.getElementById('directorySection'); 2178 const directoryInfo = document.getElementById('directoryInfo'); 2179 const selectedPath = document.getElementById('selectedPath'); 2180 2181 directorySection.classList.add('selected'); 2182 directoryInfo.style.display = 'block'; 2183 selectedPath.textContent = selectedDirectoryHandle.name; 2184 2185 updateConvertButton(); 2186 2187 } catch (writeError) { 2188 addDebugLog(`Write test failed: ${writeError.message}`); 2189 throw new Error('Cannot write to selected directory: ' + writeError.message); 2190 } 2191 2192 } catch (error) { 2193 addDebugLog(`Directory selection failed: ${error.
2193name} - ${error.message}`); 2194 if (error.name === 'AbortError') { 2195 console.log('Directory selection cancelled'); 2196 } else { 2197 console.error('Directory selection error:', error); 2198 alert('Failed to select directory: ' + error.message); 2199 useDefaultDownloads(); 2200 } 2201 } finally { 2202 selectBtn.disabled = false; 2203 selectBtn.textContent = 'ð Choose Output Folder'; 2204 } 2205 } 2206 2207 function useDefaultDownloads() { 2208 selectedDirectoryHandle = null; 2209 downloadMethod = 'default'; 2210 2211 const directorySection = document.getElementById('directorySection'); 2212 const directoryInfo = document.getElementById('directoryInfo'); 2213 const selectedPath = document.getElementById('selectedPath'); 2214 2215 directorySection.classList.add('selected'); 2216 directoryInfo.style.display = 'block'; 2217 selectedPath.innerHTML = ` 2218 <strong>Default Downloads Folder</strong><br> 2219 <small>Files will be saved with proper encoding and format conversion</small> 2220 `; 2221 2222 addDebugLog('Using default downloads folder'); 2223 updateConvertButton(); 2224 } 2225 2226 function toggleApiSetup() { 2227 const apiSetup = document.getElementById('apiSetup'); 2228 const apiDetails = document.getElementById('apiDetails'); 2229 const toggleIcon = document.getElementById('toggleIcon'); 2230 2231 if (apiSetup.classList.contains('collapsed')) { 2232 apiSetup.classList.remove('collapsed'); 2233 apiDetails.classList.remove('hidden'); 2234 toggleIcon.classList.add('rotated'); 2235 } else { 2236 apiSetup.classList.add('collapsed'); 2237 apiDetails.classList.add('hidden'); 2238 toggleIcon.classList.remove('rotated'); 2239 } 2240 } 2241 2242 function getCurrentApiKey() { 2243 const inputKey = document.getElementById('apiKey').value.trim(); 2244 return inputKey || DEFAULT_API_KEY; 2245 } 2246 2247 // File input handler 2248 document.getElementById('fileInput').addEventListener('change', function(e) { 2249 handleFiles(e.target.files); 2250 }); 2251 2252 // Drag and drop handlers 2253 const uploadArea = document.querySelector('.upload-area'); 2254 2255 uploadArea.addEventListener('dragover', function(e) { 2256 e.preventDefault(); 2257 uploadArea.classList.add('dragover'); 2258 }); 2259 2260 uploadArea.addEventListener('dragleave', function(e) { 2261 e.preventDefault(); 2262 uploadArea.classList.remove('dragover'); 2263 }); 2264 2265 uploadArea.addEventListener('drop', function(e) { 2266 e.preventDefault(); 2267 uploadArea.classList.remove('dragover'); 2268 handleFiles(e.dataTransfer.files); 2269 }); 2270 2271 async function validateApiKey() { 2272 const apiKey = getCurrentApiKey(); 2273 const statusDiv = document.getElementById('apiStatus'); 2274 2275 if (!apiKey) { 2276 statusDiv.style.display = 'none'; 2277 return; 2278 } 2279 2280 statusDiv.style.display = 'inline-block'; 2281 statusDiv.textContent = 'Validating...'; 2282 statusDiv.className = 'api-status'; 2283 2284 try { 2285 const response = await fetch('https://api.cloudconvert.com/v2/users/me', { 2286 headers: { 2287 'Authorization': `Bearer ${apiKey}`, 2288 'Content-Type': 'application/json' 2289 } 2290 }); 2291 2292 if (response.ok) { 2293 const data = await response.json(); 2294 statusDiv.textContent = `â Ready`; 2295 statusDiv.className = 'api-status api-valid'; 2296 apiKeyValid = true; 2297 addDebugLog('API key validation successful'); 2298 } else { 2299 throw new Error('Invalid API key'); 2300 } 2301 } catch (error) { 2302 statusDiv.textContent = 'â Invalid'; 2303 statusDiv.className = 'api-status api-invalid'; 2304 apiKeyValid = false; 2305 addDebugLog(`API key validation failed: ${error.message}`); 2306 } 2307 2308 updateConvertButton(); 2309 } 2310 2311 function handleFiles(files) { 2312 selectedFiles = Array.from(files).filter(file => { 2313 const ext = file.name.toLowerCase().split('.').pop(); 2314 return supportedInputFormats.includes(ext); 2315 }); 2316 2317 addDebugLog(`Files selected: ${selectedFiles.length}`);
2318 selectedFiles.forEach(file => { 2319 addDebugLog(`- ${file.name} (${formatFileSize(file.size)})`); 2320 }); 2321 2322 convertedFiles = []; 2323 displayFileList(); 2324 updateConvertButton(); 2325 document.getElementById('clearBtn').style.display = selectedFiles.length > 0 ? 'inline-block' : 'none'; 2326 document.getElementById('downloadSection').style.display = 'none'; 2327 } 2328 2329 function updateConvertButton() { 2330 const convertBtn = document.getElementById('convertBtn'); 2331 const hasFiles = selectedFiles.length > 0; 2332 const hasOutputLocation = downloadMethod !== 'none'; 2333 2334 if (!hasFiles) { 2335 convertBtn.disabled = true; 2336 convertBtn.textContent = 'ð Convert Selected Files'; 2337 } else if (!hasOutputLocation) { 2338 convertBtn.disabled = true; 2339 convertBtn.textContent = 'ð Choose Output Location First'; 2340 } else if (!apiKeyValid) { 2341 // Check if all files can use local conversion 2342 const allLocalConversion = selectedFiles.every(file => 2343 shouldUseLocalConversion(file, selectedOutputFormat, selectedConversion) 2344 ); 2345 2346 if (allLocalConversion) { 2347 convertBtn.disabled = false; 2348 const conversionName = conversionConfigs[selectedConversion].name; 2349 if (selectedConversion === 'none') { 2350 convertBtn.textContent = 'ð Convert (Local)'; 2351 } else { 2352 convertBtn.textContent = `ð Convert + ${conversionName} (Local)`; 2353 } 2354 } else { 2355 convertBtn.disabled = true; 2356 convertBtn.textContent = 'ð API Key Required'; 2357 } 2358 } else { 2359 convertBtn.disabled = false; 2360 const outputName = outputFormats[selectedOutputFormat].name; 2361 const conversionName = conversionConfigs[selectedConversion].name; 2362 if (selectedConversion === 'none') { 2363 convertBtn.textContent = `ð Convert to ${outputName}`; 2364 } else { 2365 convertBtn.textContent = `ð Convert to ${outputName} + ${conversionName}`; 2366 } 2367 } 2368 } 2369 2370 function clearFiles() { 2371 selectedFiles = []; 2372 convertedFiles = []; 2373 document.getElementById('fileInput').value = ''; 2374 displayFileList(); 2375 updateConvertButton(); 2376 document.getElementById('clearBtn').style.display = 'none'; 2377 document.getElementById('downloadSection').style.display = 'none'; 2378 document.getElementById('progressBar').style.display = 'none'; 2379 addDebugLog('Files cleared'); 2380 } 2381 2382 function displayFileList() { 2383 const fileList = document.getElementById('fileList'); 2384 fileList.innerHTML = ''; 2385 2386 if (selectedFiles.length === 0) { 2387 fileList.innerHTML = '<p style="text-align: center; color: #6b7280;">No files selected</p>'; 2388 return; 2389 } 2390 2391 // Group files to show batch indicators 2392 const groups = groupFilesByConversionType(); 2393 let currentGroup = null; 2394 let groupIndex = 0; 2395 2396 selectedFiles.forEach((file, index) => { 2397 const ext = file.name.toLowerCase().split('.').pop().toUpperCase(); 2398 const isLocalConversion = shouldUseLocalConversion(file, selectedOutputFormat, selectedConversion); 2399 2400 // Find which group this file belongs to 2401 for (const group of groups) { 2402 if (group.files.some(f => f.index === index)) { 2403 currentGroup = group; 2404 break; 2405 } 2406 } 2407 2408 let methodText = ''; 2409 let methodClass = ''; 2410 2411 if (isLocalConversion) { 2412 if (ext === 'EPUB' && selectedOutputFormat === 'txt') { 2413 methodText = 'Local EPUBâTXT'; 2414 methodClass = 'local'; 2415 } else if (ext === 'EPUB' && selectedOutputFormat === 'epub' && selectedConversion !== 'none') { 2416 methodText = 'Direct EPUB Chinese'; 2417 methodClass = 'opencc'; 2418 } else if (ext === 'TXT' && selectedConversion !== 'none') { 2419 methodText = 'OpenCC Direct (TXT)'; 2420 methodClass = 'opencc'; 2421 } else { 2422 methodText = 'Local Processing'; 2423 methodClass = 'local'; 2424 } 2425 } else if (currentGroup && currentGroup.type === 'api_batch') { 2426 methodText = `CloudConvert Batch (${ext})`; 2427 methodClass = 'batch'; 2428 } else { 2429 methodText = `CloudConvert API (${ext})`; 2430 methodClass = ''; 2431 } 2432 2433 const fileItem = document.createElement('div'); 2434 fileItem.className = 'file-item'; 2435 fileItem.innerHTML = ` 2436 <div class="file-info"> 2437 <div class="file-name">${file.name}</div> 2438 <div class="file-size">${formatFileSize(file.size)}</div> 2439 <div class="file-method ${methodClass}"> 2440 ${methodText} 2441 </div> 2442 </div> 2443 <div class="file-actions"> 2444 <div class="file-status status-pending" id="status-${index}">Pending</div> 2445 </div> 2446 `;
2447 fileList.appendChild(fileItem); 2448 }); 2449 } 2450 2451 function formatFileSize(bytes) { 2452 if (bytes === 0) return '0 Bytes'; 2453 const k = 1024; 2454 const sizes = ['Bytes', 'KB', 'MB', 'GB']; 2455 const i = Math.floor(Math.log(bytes) / Math.log(k)); 2456 return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; 2457 } 2458 2459 // Convert button handler - WITH BATCHING OPTIMIZATION 2460 document.getElementById('convertBtn').addEventListener('click', async function() { 2461 const progressBar = document.getElementById('progressBar'); 2462 const progressFill = document.getElementById('progressFill'); 2463 const convertBtn = document.getElementById('convertBtn'); 2464 2465 progressBar.style.display = 'block'; 2466 convertBtn.disabled = true; 2467 convertedFiles = []; 2468 2469 addDebugLog(`Starting universal conversion: ${selectedFiles.length} files to ${outputFormats[selectedOutputFormat].name} with ${conversionConfigs[selectedConversion].name}`); 2470 2471 // Group files by conversion requirements for batching 2472 const conversionGroups = groupFilesByConversionType(); 2473 let totalProcessed = 0; 2474 2475 for (const group of conversionGroups) { 2476 try { 2477 if (group.type === 'local') { 2478 // Process local conversions individually (existing logic) 2479 for (const fileInfo of group.files) { 2480 await processLocalFile(fileInfo, totalProcessed); 2481 totalProcessed++; 2482 updateProgress(totalProcessed, selectedFiles.length); 2483 } 2484 } else if (group.type === 'api_batch') { 2485 // Process API files in batch 2486 await processBatchFiles(group.files, totalProcessed); 2487 totalProcessed += group.files.length; 2488 updateProgress(totalProcessed, selectedFiles.length); 2489 } else { 2490 // Fallback to individual processing 2491 for (const fileInfo of group.files) { 2492 await processIndividualFile(fileInfo, totalProcessed); 2493 totalProcessed++; 2494 updateProgress(totalProcessed, selectedFiles.length); 2495 } 2496 } 2497 } catch (error) { 2498 console.error('Error processing group:', error); 2499 addDebugLog(`Error processing group: ${error.message}`); 2500 // Mark failed files 2501 for (const fileInfo of group.files) { 2502 const statusElement = document.getElementById(`status-${fileInfo.index}`); 2503 statusElement.textContent = `Error: ${error.message}`; 2504 statusElement.className = 'file-status status-error'; 2505 } 2506 totalProcessed += group.files.length; 2507 updateProgress(totalProcessed, selectedFiles.length); 2508 } 2509 } 2510 2511 convertBtn.disabled = false; 2512 updateConvertButton(); 2513 2514 if (convertedFiles.length > 0) { 2515 document.getElementById('downloadSection').style.display = 'block'; 2516 setupDownloadSection(); 2517 addDebugLog(`Universal conversion complete. ${convertedFiles.length} files successfully converted.`); 2518 } else { 2519 addDebugLog('No files were successfully converted'); 2520 } 2521 }); 2522 2523 // FIXED: Save converted file function - no duplicate downloads 2524 async function saveConvertedFile(fileName, content, extension) { 2525 addDebugLog(`Preparing file: ${fileName}`); 2526 2527 let blob; 2528 if (extension === 'txt') { 2529 // Ensure text is properly encoded as UTF-8 2530 const encoder = new TextEncoder();
2531 const utf8BOM = new Uint8Array([0xEF, 0xBB, 0xBF]); 2532 const textData = encoder.encode(content); 2533 const finalData = new Uint8Array(utf8BOM.length + textData.length); 2534 finalData.set(utf8BOM); 2535 finalData.set(textData, utf8BOM.length); 2536 blob = new Blob([finalData], { type: 'text/plain; charset=utf-8' }); 2537 } else { 2538 // Binary files 2539 if (typeof content === 'string') { 2540 blob = new Blob([content], { type: 'application/octet-stream' }); 2541 } else { 2542 blob = content; // Already a blob 2543 } 2544 } 2545 2546 // ONLY save to filesystem if user selected a directory 2547 // Do NOT auto-download to default location during conversion 2548 if (downloadMethod === 'filesystem' && selectedDirectoryHandle) { 2549 addDebugLog(`Saving ${fileName} to selected directory`); 2550 const fileHandle = await selectedDirectoryHandle.getFileHandle(fileName, { create: true }); 2551 const writable = await fileHandle.createWritable(); 2552 await writable.write(blob); 2553 await writable.close(); 2554 addDebugLog(`File saved to selected directory: ${fileName}`); 2555 } else { 2556 // For default method, just prepare the file - don't auto-download 2557 addDebugLog(`File prepared for download: ${fileName}`); 2558 } 2559 2560 // Return the blob for storage in convertedFiles array 2561 return blob; 2562 } 2563 2564 async function convertEpubToFormat(epubBlob, outputFormat, originalFileName) { 2565 const apiKey = getCurrentApiKey(); 2566 2567 try { 2568 addDebugLog(`Converting processed EPUB to ${outputFormat.toUpperCase()}`); 2569 2570 // Step 1: Create a job 2571 const jobResponse = await fetch('https://api.cloudconvert.com/v2/jobs', { 2572 method: 'POST', 2573 headers: { 2574 'Authorization': `Bearer ${apiKey}`, 2575 'Content-Type': 'application/json' 2576 }, 2577 body: JSON.stringify({ 2578 tasks: { 2579 'upload-file': { 2580 operation: 'import/upload' 2581 }, 2582 'convert-file': { 2583 operation: 'convert', 2584 input: 'upload-file', 2585 output_format: outputFormat, 2586 options: { 2587 input_format: 'epub' 2588 } 2589 }, 2590 'export-file': { 2591 operation: 'export/url', 2592 input: 'convert-file' 2593 } 2594 } 2595 }) 2596 }); 2597 2598 if (!jobResponse.ok) { 2599 const errorText = await jobResponse.text(); 2600 addDebugLog(`Job creation failed: ${jobResponse.status} - ${errorText}`); 2601 throw new Error(`Failed to create job: ${jobResponse.status}`); 2602 } 2603 2604 const jobData = await jobResponse.json(); 2605 const jobId = jobData.data.id; 2606 addDebugLog(`EPUB-to-format job created: ${jobId}`); 2607 2608 // Step 2: Upload the processed EPUB 2609 const uploadTask = jobData.data.tasks.find(task => task.name === 'upload-file'); 2610 const uploadUrl = uploadTask.result.form.url; 2611 const uploadParams = uploadTask.result.form.parameters; 2612 2613 addDebugLog('Uploading processed EPUB to CloudConvert...'); 2614 2615 const formData = new FormData();
2616 Object.keys(uploadParams).forEach(key => { 2617 formData.append(key, uploadParams[key]); 2618 }); 2619 formData.append('file', epubBlob, originalFileName.replace(/\.[^/.]+$/, '') + '.epub'); 2620 2621 const uploadResponse = await fetch(uploadUrl, { 2622 method: 'POST', 2623 body: formData 2624 }); 2625 2626 if (!uploadResponse.ok) { 2627 addDebugLog(`Upload failed: ${uploadResponse.status}`); 2628 throw new Error(`Failed to upload processed EPUB: ${uploadResponse.status}`); 2629 } 2630 2631 addDebugLog('Processed EPUB uploaded, waiting for final conversion...'); 2632 2633 // Step 3: Wait for job completion 2634 let jobStatus = 'processing'; 2635 let attempts = 0; 2636 const maxAttempts = 60; 2637 2638 while (jobStatus === 'processing' && attempts < maxAttempts) { 2639 await new Promise(resolve => setTimeout(resolve, 5000)); 2640 2641 const statusResponse = await fetch(`https://api.cloudconvert.com/v2/jobs/${jobId}`, { 2642 headers: { 2643 'Authorization': `Bearer ${apiKey}` 2644 } 2645 }); 2646 2647 if (!statusResponse.ok) { 2648 addDebugLog(`Status check failed: ${statusResponse.status}`); 2649 throw new Error(`Failed to check job status: ${statusResponse.status}`); 2650 } 2651 2652 const statusData = await statusResponse.json(); 2653 jobStatus = statusData.data.status; 2654 attempts++; 2655 2656 addDebugLog(`EPUB-to-format job status check ${attempts}: ${jobStatus}`); 2657 2658 if (jobStatus === 'error') { 2659 const errorMessage = statusData.data.message || 'Unknown error'; 2660 addDebugLog(`Final conversion failed: ${errorMessage}`); 2661 throw new Error(`Final conversion failed: ${errorMessage}`); 2662 } 2663 } 2664 2665 if (jobStatus !== 'finished') { 2666 addDebugLog('Final conversion timed out'); 2667 throw new Error('Final conversion timed out'); 2668 } 2669 2670 // Step 4: Get the download URL and return as blob 2671 const finalStatusResponse = await fetch(`https://api.cloudconvert.com/v2/jobs/${jobId}`, { 2672 headers: { 2673 'Authorization': `Bearer ${apiKey}` 2674 } 2675 }); 2676 2677 const finalStatusData = await finalStatusResponse.json(); 2678 const exportTask = finalStatusData.data.tasks.find(task => task.name === 'export-file'); 2679 2680 if (!exportTask || !exportTask.result || !exportTask.result.files[0]) { 2681 addDebugLog('No output file found in final conversion'); 2682 throw new Error('No output file found in final conversion'); 2683 } 2684 2685 const downloadUrl = exportTask.result.files[0].url; 2686 addDebugLog(`Downloading final converted file from: ${downloadUrl}`); 2687 2688 const finalResponse = await fetch(downloadUrl); 2689 if (!finalResponse.ok) { 2690 addDebugLog(`Final download failed: ${finalResponse.status}`); 2691 throw new Error(`Failed to download final converted file: ${finalResponse.status}`); 2692 } 2693 2694 const finalBlob = await finalResponse.blob(); 2695 addDebugLog(`Final conversion completed: ${finalBlob.size} bytes`); 2696 2697 return finalBlob; 2698 2699 } catch (error) { 2700 addDebugLog(`EPUB-to-format conversion error: ${error.message}`); 2701 throw new Error(`EPUB-to-format conversion error: ${error.message}`); 2702 } 2703 } 2704 2705 async function convertWithCloudConvert(file, outputFormat) { 2706 const apiKey = getCurrentApiKey(); 2707 2708 try { 2709 addDebugLog(`Creating CloudConvert job: ${file.name} -> ${outputFormat.toUpperCase()}`); 2710 2711 // Step 1: Create a job 2712 const jobResponse = await fetch('https://api.cloudconvert.com/v2/jobs', { 2713 method: 'POST', 2714 headers: { 2715 'Authorization': `Bearer ${apiKey}`, 2716 'Content-Type': 'application/json' 2717 }, 2718 body: JSON.stringify({ 2719 tasks: { 2720 'upload-file': { 2721 operation: 'import/upload' 2722 }, 2723 'convert-file': { 2724 operation: 'convert', 2725 input: 'upload-file', 2726 output_format: outputFormat, 2727 options: outputFormat === 'txt' ? { output_encoding: 'utf-8' } : {} 2728 },
2729 'export-file': { 2730 operation: 'export/url', 2731 input: 'convert-file' 2732 } 2733 } 2734 }) 2735 }); 2736 2737 if (!jobResponse.ok) { 2738 const errorText = await jobResponse.text(); 2739 addDebugLog(`Job creation failed: ${jobResponse.status} - ${errorText}`); 2740 throw new Error(`Failed to create job: ${jobResponse.status}`); 2741 } 2742 2743 const jobData = await jobResponse.json(); 2744 const jobId = jobData.data.id; 2745 addDebugLog(`Job created: ${jobId}`); 2746 2747 // Step 2: Upload the file 2748 const uploadTask = jobData.data.tasks.find(task => task.name === 'upload-file'); 2749 const uploadUrl = uploadTask.result.form.url; 2750 const uploadParams = uploadTask.result.form.parameters; 2751 2752 addDebugLog('Uploading file to CloudConvert...'); 2753 2754 const formData = new FormData(); 2755 Object.keys(uploadParams).forEach(key => { 2756 formData.append(key, uploadParams[key]); 2757 }); 2758 formData.append('file', file); 2759 2760 const uploadResponse = await fetch(uploadUrl, { 2761 method: 'POST', 2762 body: formData 2763 }); 2764 2765 if (!uploadResponse.ok) { 2766 addDebugLog(`Upload failed: ${uploadResponse.status}`); 2767 throw new Error(`Failed to upload file: ${uploadResponse.status}`); 2768 } 2769 2770 addDebugLog('File uploaded successfully, waiting for conversion...'); 2771 2772 // Step 3: Wait for job completion 2773 let jobStatus = 'processing'; 2774 let attempts = 0; 2775 const maxAttempts = 60; 2776 2777 while (jobStatus === 'processing' && attempts < maxAttempts) { 2778 await new Promise(resolve => setTimeout(resolve, 5000)); 2779 2780 const statusResponse = await fetch(`https://api.cloudconvert.com/v2/jobs/${jobId}`, { 2781 headers: { 2782 'Authorization': `Bearer ${apiKey}` 2783 } 2784 }); 2785 2786 if (!statusResponse.ok) { 2787 addDebugLog(`Status check failed: ${statusResponse.status}`); 2788 throw new Error(`Failed to check job status: ${statusResponse.status}`); 2789 } 2790 2791 const statusData = await statusResponse.json(); 2792 jobStatus = statusData.data.status; 2793 attempts++; 2794 2795 addDebugLog(`Job status check ${attempts}: ${jobStatus}`); 2796 2797 if (jobStatus === 'error') { 2798 const errorMessage = statusData.data.message || 'Unknown error'; 2799 addDebugLog(`Conversion failed: ${errorMessage}`); 2800 throw new Error(`Conversion failed: ${errorMessage}`); 2801 } 2802 } 2803 2804 if (jobStatus !== 'finished') { 2805 addDebugLog('Conversion timed out'); 2806 throw new Error('Conversion timed out'); 2807 } 2808 2809 addDebugLog('Conversion completed, downloading result...'); 2810 2811 // Step 4: Get the download URL 2812 const finalStatusResponse = await fetch(`https://api.cloudconvert.com/v2/jobs/${jobId}`, { 2813 headers: { 2814 'Authorization': `Bearer ${apiKey}` 2815 } 2816 }); 2817 2818 const finalStatusData = await finalStatusResponse.json(); 2819 const exportTask = finalStatusData.data.tasks.find(task => task.name === 'export-file'); 2820 2821 if (!exportTask || !exportTask.result || !exportTask.result.files[0]) { 2822 addDebugLog('No output file found in job result'); 2823 throw new Error('No output file found'); 2824 } 2825 2826 const downloadUrl = exportTask.result.files[0].url; 2827 addDebugLog(`Downloading converted file from: ${downloadUrl}`); 2828 2829 // Step 5: Download the converted content 2830 const response = await fetch(downloadUrl); 2831 if (!response.ok) { 2832 addDebugLog(`Download failed: ${response.status}`); 2833 throw new Error(`Failed to download converted file: ${response.status}`); 2834 } 2835 2836 if (outputFormat === 'txt') { 2837 const textContent = await response.text(); 2838 addDebugLog(`Downloaded text content: ${textContent.length} characters`); 2839 return textContent; 2840 } else { 2841 const blobContent = await response.blob(); 2842 addDebugLog(`Downloaded binary content: ${blobContent.size} bytes`); 2843 return blobContent; 2844 } 2845 2846 } catch (error) { 2847 addDebugLog(`CloudConvert error: ${error.message}`); 2848 throw new Error(`CloudConvert error: ${error.message}`); 2849 } 2850 } 2851 2852 function setupDownloadSection() { 2853 const individualDownloads = document.getElementById('individualDownloads'); 2854 individualDownloads.innerHTML = '<h4 style="margin: 20px 0 10px 0; color: #374151;">Download Options:</h4>'; 2855 2856 const statusDiv = document.createElement('div'); 2857 statusDiv.style.cssText = 'background: #f0fdf4; border: 1px solid #bbf7d0; border-radius: 8px; padding: 15px; margin: 10px 0; color: #065f46;'; 2858 2859 const outputInfo = `${outputFormats[selectedOutputFormat].name}`; 2860 const conversionInfo = selectedConversion !== 'none' ? 2861 ` with ${conversionConfigs[selectedConversion].name}` : ''; 2862 2863 if (downloadMethod === 'filesystem') { 2864 statusDiv.innerHTML = `â <strong>Files saved to:</strong> ${selectedDirectoryHandle.name}<br><small>Format: ${outputInfo}${conversionInfo}</small>`; 2865 } else { 2866 statusDiv.innerHTML = `ð¾ <strong>Files downloaded to:</strong> Default Downloads folder<br><small>Format: ${outputInfo}${conversionInfo}</small>`; 2867 } 2868 2869 individualDownloads.appendChild(statusDiv); 2870 2871 const redownloadDiv = document.createElement('div'); 2872 redownloadDiv.innerHTML = '<h5 style="margin: 15px 0 10px 0; color: #374151;">Re-download individual files:</h5>'; 2873 individualDownloads.appendChild(redownloadDiv); 2874
2875 convertedFiles.forEach((file, index) => { 2876 const conversionTag = file.conversion !== 'none' ? ` [${conversionConfigs[file.conversion].name}]` : ''; 2877 const formatTag = ` [${outputFormats[file.outputFormat].name}]`; 2878 2879 const downloadItem = document.createElement('div'); 2880 downloadItem.style.cssText = 'display: flex; flex-direction: column; padding: 10px; margin: 5px 0; background: white; border-radius: 6px; border: 1px solid #e5e7eb;'; 2881 downloadItem.innerHTML = ` 2882 <div style="display: flex; justify-content: space-between; align-items: center;"> 2883 <div> 2884 <span style="color: #374151; font-weight: 500;">${file.fileName}${formatTag}${conversionTag}</span> 2885 </div> 2886 <button class="btn btn-small btn-secondary" onclick="downloadConvertedFile(${index})">ð Download</button> 2887 </div> 2888 `; 2889 individualDownloads.appendChild(downloadItem); 2890 }); 2891 } 2892 2893 // Download functions 2894 document.getElementById('downloadAllBtn').addEventListener('click', async function() { 2895 if (convertedFiles.length === 0) return; 2896 2897 const zip = new JSZip(); 2898 convertedFiles.forEach((file, index) => { 2899 if (typeof file.content === 'string') { 2900 // Ensure text is properly encoded as UTF-8 2901 const encoder = new TextEncoder(); 2902 const utf8BOM = new Uint8Array([0xEF, 0xBB, 0xBF]); 2903 const textData = encoder.encode(file.content); 2904 const finalData = new Uint8Array(utf8BOM.length + textData.length); 2905 finalData.set(utf8BOM); 2906 finalData.set(textData, utf8BOM.length); 2907 zip.file(file.fileName, finalData); 2908 } else { 2909 // Binary content (blob) 2910 zip.file(file.fileName, file.content); 2911 } 2912 }); 2913 2914 const zipBlob = await zip.generateAsync({type: 'blob'}); 2915 2916 const zipFileName = `converted_ebooks_${selectedOutputFormat}${selectedConversion !== 'none' ? '_' + selectedConversion : ''}.zip`; 2917 2918 // Always download ZIP files to browser downloads (no filesystem API for ZIP) 2919 const url = URL.createObjectURL(zipBlob); 2920 const a = document.createElement('a'); 2921 a.href = url; 2922 a.download = zipFileName; 2923 document.body.appendChild(a); 2924 a.click(); 2925 document.body.removeChild(a); 2926 URL.revokeObjectURL(url); 2927 addDebugLog('ZIP file downloaded'); 2928 }); 2929 2930 document.getElementById('downloadAllSeparateBtn').addEventListener('click', function() { 2931 convertedFiles.forEach((file, index) => { 2932 setTimeout(() => { 2933 downloadConvertedFile(index); 2934 }, index * 500); 2935 }); 2936 }); 2937 2938 function downloadConvertedFile(index) { 2939 const file = convertedFiles[index]; 2940 if (!file) return; 2941 2942 let blob; 2943 if (typeof file.content === 'string') { 2944 // Ensure text is properly encoded as UTF-8 2945 const encoder = new TextEncoder(); 2946 const utf8BOM = new Uint8Array([0xEF, 0xBB, 0xBF]); 2947 const textData = encoder.encode(file.content); 2948 const finalData = new Uint8Array(utf8BOM.length + textData.length); 2949 finalData.set(utf8BOM); 2950 finalData.set(textData, utf8BOM.length); 2951 blob = new Blob([finalData], { type: 'text/plain; charset=utf-8' }); 2952 } else { 2953 // Binary content 2954 blob = file.content; 2955 } 2956 2957 // Always download individual files to browser downloads 2958 const url = URL.createObjectURL(blob); 2959 const a = document.createElement('a'); 2960 a.href = url; 2961 a.download = file.fileName; 2962 document.body.appendChild(a); 2963 a.click(); 2964 document.body.removeChild(a); 2965 URL.revokeObjectURL(url); 2966 2967 addDebugLog(`Individual file downloaded: ${file.fileName}`); 2968 } 2969 2970 // Monitor option changes 2971 document.addEventListener('change', function(e) { 2972 if (e.target.name === 'conversion') { 2973 selectedConversion = e.target.value; 2974 updateConversionPipeline(); 2975 updateConvertButton(); 2976 displayFileList(); // Refresh to update method indicators 2977 } else if (e.target.name === 'outputFormat') { 2978 selectedOutputFormat = e.target.value; 2979 updateConversionPipeline(); 2980 updateConvertButton(); 2981 displayFileList(); // Refresh to update method indicators 2982 } 2983 }); 2984 2985 // Monitor API key changes 2986 document.getElementById('apiKey').addEventListener('input', function() { 2987 const currentKey = getCurrentApiKey(); 2988 if (currentKey === DEFAULT_API_KEY) { 2989 apiKeyValid = true; 2990 updateConvertButton(); 2991 } else { 2992 apiKeyValid = false; 2993 updateConvertButton(); 2994 } 2995 }); 2996 </script>
2996 2997 2998</body></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.