PageSourceSearch

https://ebook-convert.netlify.app/

html ebook-convert.netlify.app collected 2026-10-03 10:40:31 UTC 130,664 bytes, 2,998 lines download raw bytes

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, '&amp;')
1533                .replace(/</g, '&lt;')
1534                .replace(/>/g, '&gt;')
1535                .replace(/"/g, '&quot;')
1536                .replace(/'/g, '&apos;');
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.