- YouTube video/playlist download as MP3 (yt-dlp) - Audio transcription with OpenAI (gpt-4o-transcribe, whisper-1) - Translation with GPT-4o-mini (chunking for long texts) - Web interface with progress bars and drag & drop - CLI and REST API interfaces - Linux shell scripts 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
637 lines
23 KiB
JavaScript
637 lines
23 KiB
JavaScript
// API Base URL
|
||
const API_URL = '';
|
||
|
||
// Tab switching
|
||
document.querySelectorAll('.tab').forEach(tab => {
|
||
tab.addEventListener('click', () => {
|
||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
||
tab.classList.add('active');
|
||
document.getElementById(tab.dataset.tab).classList.add('active');
|
||
});
|
||
});
|
||
|
||
// Helper: Show result
|
||
function showResult(elementId, success, content) {
|
||
const el = document.getElementById(elementId);
|
||
el.className = `result show ${success ? 'success' : 'error'}`;
|
||
el.innerHTML = content;
|
||
}
|
||
|
||
// Helper: Set loading state
|
||
function setLoading(button, loading) {
|
||
button.disabled = loading;
|
||
button.classList.toggle('loading', loading);
|
||
}
|
||
|
||
// Format seconds to MM:SS or HH:MM:SS
|
||
function formatTime(seconds) {
|
||
if (!seconds || seconds < 0) return '--:--';
|
||
const hrs = Math.floor(seconds / 3600);
|
||
const mins = Math.floor((seconds % 3600) / 60);
|
||
const secs = Math.floor(seconds % 60);
|
||
if (hrs > 0) {
|
||
return `${hrs}:${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
|
||
}
|
||
return `${mins}:${String(secs).padStart(2, '0')}`;
|
||
}
|
||
|
||
// Format file size
|
||
function formatSize(bytes) {
|
||
if (!bytes) return '';
|
||
const units = ['B', 'KB', 'MB', 'GB'];
|
||
let i = 0;
|
||
while (bytes >= 1024 && i < units.length - 1) {
|
||
bytes /= 1024;
|
||
i++;
|
||
}
|
||
return `${bytes.toFixed(1)} ${units[i]}`;
|
||
}
|
||
|
||
// ==================== DOWNLOAD TAB ====================
|
||
|
||
const progressContainer = document.getElementById('download-progress');
|
||
const progressFill = document.getElementById('progress-fill');
|
||
const progressPercent = document.getElementById('progress-percent');
|
||
const progressTitle = document.getElementById('progress-title');
|
||
const progressEta = document.getElementById('progress-eta');
|
||
const progressInfo = document.getElementById('progress-info');
|
||
const progressSpeed = document.getElementById('progress-speed');
|
||
const progressCurrent = document.getElementById('progress-current');
|
||
|
||
function updateDownloadProgress(data) {
|
||
progressFill.style.width = `${data.percent}%`;
|
||
progressPercent.textContent = `${data.percent}%`;
|
||
|
||
if (data.totalVideos > 1) {
|
||
progressInfo.textContent = `Video ${data.currentVideo}/${data.totalVideos}`;
|
||
} else {
|
||
progressInfo.textContent = '';
|
||
}
|
||
|
||
if (data.speed) progressSpeed.textContent = data.speed;
|
||
if (data.estimatedRemaining) {
|
||
progressEta.textContent = `ETA: ${formatTime(data.estimatedRemaining)}`;
|
||
} else if (data.eta) {
|
||
progressEta.textContent = `ETA: ${data.eta}`;
|
||
}
|
||
if (data.title) {
|
||
progressCurrent.innerHTML = `Downloading: <span class="video-title">${data.title}</span>`;
|
||
}
|
||
}
|
||
|
||
function resetDownloadProgress() {
|
||
progressFill.style.width = '0%';
|
||
progressPercent.textContent = '0%';
|
||
progressTitle.textContent = 'Downloading...';
|
||
progressEta.textContent = '';
|
||
progressInfo.textContent = '';
|
||
progressSpeed.textContent = '';
|
||
progressCurrent.textContent = '';
|
||
}
|
||
|
||
document.getElementById('download-form').addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
const button = e.target.querySelector('button[type="submit"]');
|
||
const url = document.getElementById('download-url').value;
|
||
const resultDiv = document.getElementById('download-result');
|
||
|
||
setLoading(button, true);
|
||
resetDownloadProgress();
|
||
progressContainer.style.display = 'block';
|
||
resultDiv.classList.remove('show');
|
||
|
||
const eventSource = new EventSource(`${API_URL}/download-stream?url=${encodeURIComponent(url)}`);
|
||
|
||
eventSource.addEventListener('status', (e) => {
|
||
progressTitle.textContent = JSON.parse(e.data).message;
|
||
});
|
||
|
||
eventSource.addEventListener('info', (e) => {
|
||
const data = JSON.parse(e.data);
|
||
progressTitle.textContent = data.totalVideos > 1
|
||
? `Downloading playlist: ${data.playlistTitle} (${data.totalVideos} videos)`
|
||
: `Downloading: ${data.title}`;
|
||
});
|
||
|
||
eventSource.addEventListener('progress', (e) => updateDownloadProgress(JSON.parse(e.data)));
|
||
|
||
eventSource.addEventListener('video-complete', (e) => {
|
||
const data = JSON.parse(e.data);
|
||
progressCurrent.innerHTML = `Completed: <span class="video-title">${data.title}</span> (${data.videosCompleted}/${data.totalVideos})`;
|
||
});
|
||
|
||
eventSource.addEventListener('complete', (e) => {
|
||
const data = JSON.parse(e.data);
|
||
eventSource.close();
|
||
progressFill.style.width = '100%';
|
||
progressPercent.textContent = '100%';
|
||
progressTitle.textContent = 'Download Complete!';
|
||
progressEta.textContent = `Total: ${formatTime(data.totalTime)}`;
|
||
|
||
showResult('download-result', true, `
|
||
<h3>Download Complete!</h3>
|
||
<p>${data.successCount}/${data.totalVideos} videos downloaded</p>
|
||
${data.playlistTitle ? `<p>Playlist: ${data.playlistTitle}</p>` : ''}
|
||
<ul>${data.videos.map(v => `
|
||
<li>
|
||
<span class="${v.success ? 'icon-success' : 'icon-error'}">${v.success ? '✓' : '✗'}</span>
|
||
${v.title}
|
||
${v.success && v.fileUrl ? `<a href="${v.fileUrl}" target="_blank">Download</a>` : ''}
|
||
${v.error ? `<small>(${v.error})</small>` : ''}
|
||
</li>
|
||
`).join('')}</ul>
|
||
`);
|
||
setLoading(button, false);
|
||
});
|
||
|
||
eventSource.addEventListener('error', (e) => {
|
||
let errorMsg = 'Download failed';
|
||
try { errorMsg = JSON.parse(e.data).message || errorMsg; } catch {}
|
||
eventSource.close();
|
||
progressContainer.style.display = 'none';
|
||
showResult('download-result', false, `<h3>Error</h3><p>${errorMsg}</p>`);
|
||
setLoading(button, false);
|
||
});
|
||
|
||
eventSource.onerror = () => {
|
||
eventSource.close();
|
||
progressContainer.style.display = 'none';
|
||
showResult('download-result', false, `<h3>Error</h3><p>Connection lost</p>`);
|
||
setLoading(button, false);
|
||
};
|
||
});
|
||
|
||
// ==================== TRANSCRIBE TAB (Drag & Drop) ====================
|
||
|
||
let selectedFiles = [];
|
||
const dropZone = document.getElementById('drop-zone');
|
||
const fileInput = document.getElementById('file-input');
|
||
const selectedFilesDiv = document.getElementById('selected-files');
|
||
const filesList = document.getElementById('files-list');
|
||
const transcribeBtn = document.getElementById('transcribe-btn');
|
||
const clearFilesBtn = document.getElementById('clear-files');
|
||
|
||
function updateFilesList() {
|
||
if (selectedFiles.length === 0) {
|
||
selectedFilesDiv.style.display = 'none';
|
||
transcribeBtn.disabled = true;
|
||
return;
|
||
}
|
||
|
||
selectedFilesDiv.style.display = 'block';
|
||
transcribeBtn.disabled = false;
|
||
|
||
filesList.innerHTML = selectedFiles.map((file, index) => `
|
||
<li>
|
||
<span class="file-name">${file.name}</span>
|
||
<span class="file-size">${formatSize(file.size)}</span>
|
||
<button type="button" class="remove-file" data-index="${index}">×</button>
|
||
</li>
|
||
`).join('');
|
||
|
||
// Add remove handlers
|
||
filesList.querySelectorAll('.remove-file').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
selectedFiles.splice(parseInt(btn.dataset.index), 1);
|
||
updateFilesList();
|
||
});
|
||
});
|
||
}
|
||
|
||
function addFiles(files) {
|
||
const audioFiles = Array.from(files).filter(f =>
|
||
f.type.startsWith('audio/') || f.name.match(/\.(mp3|wav|m4a|ogg|flac)$/i)
|
||
);
|
||
selectedFiles = [...selectedFiles, ...audioFiles];
|
||
updateFilesList();
|
||
}
|
||
|
||
// Drag & Drop events
|
||
dropZone.addEventListener('click', () => fileInput.click());
|
||
|
||
dropZone.addEventListener('dragover', (e) => {
|
||
e.preventDefault();
|
||
dropZone.classList.add('drag-over');
|
||
});
|
||
|
||
dropZone.addEventListener('dragleave', () => {
|
||
dropZone.classList.remove('drag-over');
|
||
});
|
||
|
||
dropZone.addEventListener('drop', (e) => {
|
||
e.preventDefault();
|
||
dropZone.classList.remove('drag-over');
|
||
addFiles(e.dataTransfer.files);
|
||
});
|
||
|
||
fileInput.addEventListener('change', () => {
|
||
addFiles(fileInput.files);
|
||
fileInput.value = '';
|
||
});
|
||
|
||
clearFilesBtn.addEventListener('click', () => {
|
||
selectedFiles = [];
|
||
updateFilesList();
|
||
});
|
||
|
||
// Transcribe form submit
|
||
document.getElementById('transcribe-form').addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
if (selectedFiles.length === 0) return;
|
||
|
||
const button = transcribeBtn;
|
||
const language = document.getElementById('transcribe-lang').value;
|
||
const model = document.getElementById('transcribe-model').value;
|
||
|
||
const transcribeProgress = document.getElementById('transcribe-progress');
|
||
const transcribeProgressFill = document.getElementById('transcribe-progress-fill');
|
||
const transcribeProgressTitle = document.getElementById('transcribe-progress-title');
|
||
const transcribeProgressPercent = document.getElementById('transcribe-progress-percent');
|
||
const transcribeProgressInfo = document.getElementById('transcribe-progress-info');
|
||
const transcribeProgressCurrent = document.getElementById('transcribe-progress-current');
|
||
|
||
setLoading(button, true);
|
||
transcribeProgress.style.display = 'block';
|
||
transcribeProgressFill.style.width = '0%';
|
||
transcribeProgressTitle.textContent = 'Uploading and transcribing...';
|
||
transcribeProgressPercent.textContent = '0%';
|
||
transcribeProgressInfo.textContent = `0/${selectedFiles.length} files`;
|
||
document.getElementById('transcribe-result').classList.remove('show');
|
||
|
||
const formData = new FormData();
|
||
selectedFiles.forEach(file => formData.append('files', file));
|
||
if (language) formData.append('language', language);
|
||
formData.append('model', model);
|
||
|
||
try {
|
||
const response = await fetch(`${API_URL}/upload-transcribe`, {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (!response.ok) throw new Error(data.error || 'Transcription failed');
|
||
|
||
transcribeProgressFill.style.width = '100%';
|
||
transcribeProgressPercent.textContent = '100%';
|
||
transcribeProgressTitle.textContent = 'Transcription Complete!';
|
||
transcribeProgressInfo.textContent = `${data.successCount}/${data.totalFiles} files`;
|
||
|
||
showResult('transcribe-result', true, `
|
||
<h3>Transcription Complete!</h3>
|
||
<p>${data.successCount}/${data.totalFiles} files transcribed</p>
|
||
<ul>${data.results.map(r => `
|
||
<li>
|
||
<span class="${r.success ? 'icon-success' : 'icon-error'}">${r.success ? '✓' : '✗'}</span>
|
||
${r.fileName}
|
||
${r.success && r.transcriptionUrl ? `<a href="${r.transcriptionUrl}" target="_blank">View</a>` : ''}
|
||
${r.error ? `<small>(${r.error})</small>` : ''}
|
||
</li>
|
||
`).join('')}</ul>
|
||
${data.results[0]?.text ? `
|
||
<h4>Preview (first file):</h4>
|
||
<div class="preview">${data.results[0].text.substring(0, 1000)}${data.results[0].text.length > 1000 ? '...' : ''}</div>
|
||
` : ''}
|
||
`);
|
||
|
||
selectedFiles = [];
|
||
updateFilesList();
|
||
|
||
} catch (error) {
|
||
transcribeProgress.style.display = 'none';
|
||
showResult('transcribe-result', false, `<h3>Error</h3><p>${error.message}</p>`);
|
||
} finally {
|
||
setLoading(button, false);
|
||
}
|
||
});
|
||
|
||
// ==================== PROCESS TAB (Download + Transcribe) ====================
|
||
|
||
const processProgress = document.getElementById('process-progress');
|
||
const processProgressFill = document.getElementById('process-progress-fill');
|
||
const processProgressTitle = document.getElementById('process-progress-title');
|
||
const processProgressPercent = document.getElementById('process-progress-percent');
|
||
const processProgressPhase = document.getElementById('process-progress-phase');
|
||
const processProgressSpeed = document.getElementById('process-progress-speed');
|
||
const processProgressCurrent = document.getElementById('process-progress-current');
|
||
const processProgressEta = document.getElementById('process-progress-eta');
|
||
|
||
function resetProcessProgress() {
|
||
processProgressFill.style.width = '0%';
|
||
processProgressPercent.textContent = '0%';
|
||
processProgressTitle.textContent = 'Processing...';
|
||
processProgressPhase.textContent = '';
|
||
processProgressSpeed.textContent = '';
|
||
processProgressCurrent.textContent = '';
|
||
processProgressEta.textContent = '';
|
||
}
|
||
|
||
document.getElementById('process-form').addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
const button = e.target.querySelector('button[type="submit"]');
|
||
const url = document.getElementById('process-url').value;
|
||
const language = document.getElementById('process-lang').value;
|
||
const model = document.getElementById('process-model').value;
|
||
const resultDiv = document.getElementById('process-result');
|
||
|
||
setLoading(button, true);
|
||
resetProcessProgress();
|
||
processProgress.style.display = 'block';
|
||
resultDiv.classList.remove('show');
|
||
|
||
const params = new URLSearchParams({ url });
|
||
if (language) params.append('language', language);
|
||
params.append('model', model);
|
||
|
||
const eventSource = new EventSource(`${API_URL}/process-stream?${params}`);
|
||
|
||
eventSource.addEventListener('status', (e) => {
|
||
const data = JSON.parse(e.data);
|
||
processProgressTitle.textContent = data.message;
|
||
if (data.phase === 'transcribing') {
|
||
processProgressPhase.textContent = 'Transcribing';
|
||
}
|
||
});
|
||
|
||
eventSource.addEventListener('info', (e) => {
|
||
const data = JSON.parse(e.data);
|
||
processProgressTitle.textContent = data.totalVideos > 1
|
||
? `Processing playlist: ${data.playlistTitle} (${data.totalVideos} videos)`
|
||
: `Processing: ${data.title}`;
|
||
});
|
||
|
||
eventSource.addEventListener('progress', (e) => {
|
||
const data = JSON.parse(e.data);
|
||
processProgressFill.style.width = `${data.percent}%`;
|
||
processProgressPercent.textContent = `${Math.round(data.percent)}%`;
|
||
processProgressPhase.textContent = data.phaseLabel || '';
|
||
if (data.speed) processProgressSpeed.textContent = data.speed;
|
||
if (data.title) {
|
||
processProgressCurrent.innerHTML = `${data.phaseLabel}: <span class="video-title">${data.title}</span>`;
|
||
}
|
||
if (data.totalVideos > 1) {
|
||
processProgressCurrent.innerHTML += ` (${data.currentVideo}/${data.totalVideos})`;
|
||
}
|
||
});
|
||
|
||
eventSource.addEventListener('video-complete', (e) => {
|
||
const data = JSON.parse(e.data);
|
||
processProgressCurrent.innerHTML = `Downloaded: <span class="video-title">${data.title}</span>`;
|
||
});
|
||
|
||
eventSource.addEventListener('transcribe-complete', (e) => {
|
||
const data = JSON.parse(e.data);
|
||
processProgressCurrent.innerHTML = `Transcribed: <span class="video-title">${data.title}</span> (${data.videosCompleted}/${data.totalFiles})`;
|
||
});
|
||
|
||
eventSource.addEventListener('complete', (e) => {
|
||
const data = JSON.parse(e.data);
|
||
eventSource.close();
|
||
processProgressFill.style.width = '100%';
|
||
processProgressPercent.textContent = '100%';
|
||
processProgressTitle.textContent = 'Processing Complete!';
|
||
processProgressPhase.textContent = '';
|
||
processProgressEta.textContent = `Total: ${formatTime(data.totalTime)}`;
|
||
|
||
showResult('process-result', true, `
|
||
<h3>Processing Complete!</h3>
|
||
${data.playlistTitle ? `<p>Playlist: ${data.playlistTitle}</p>` : ''}
|
||
<p>Downloaded: ${data.downloadedCount}/${data.totalVideos}</p>
|
||
<p>Transcribed: ${data.transcribedCount}/${data.totalVideos}</p>
|
||
<ul>${data.results.map(r => `
|
||
<li>
|
||
<span class="${r.transcriptionSuccess ? 'icon-success' : 'icon-error'}">${r.transcriptionSuccess ? '✓' : '✗'}</span>
|
||
${r.title}
|
||
${r.audioUrl ? `<a href="${r.audioUrl}" target="_blank">MP3</a>` : ''}
|
||
${r.transcriptionUrl ? `<a href="${r.transcriptionUrl}" target="_blank">TXT</a>` : ''}
|
||
${r.error ? `<small>(${r.error})</small>` : ''}
|
||
</li>
|
||
`).join('')}</ul>
|
||
${data.results[0]?.text ? `
|
||
<h4>Preview (first file):</h4>
|
||
<div class="preview">${data.results[0].text.substring(0, 1000)}${data.results[0].text.length > 1000 ? '...' : ''}</div>
|
||
` : ''}
|
||
`);
|
||
setLoading(button, false);
|
||
});
|
||
|
||
eventSource.addEventListener('error', (e) => {
|
||
let errorMsg = 'Processing failed';
|
||
try { errorMsg = JSON.parse(e.data).message || errorMsg; } catch {}
|
||
eventSource.close();
|
||
processProgress.style.display = 'none';
|
||
showResult('process-result', false, `<h3>Error</h3><p>${errorMsg}</p>`);
|
||
setLoading(button, false);
|
||
});
|
||
|
||
eventSource.onerror = () => {
|
||
eventSource.close();
|
||
processProgress.style.display = 'none';
|
||
showResult('process-result', false, `<h3>Error</h3><p>Connection lost</p>`);
|
||
setLoading(button, false);
|
||
};
|
||
});
|
||
|
||
// ==================== TRANSLATE CHECKBOXES (Transcribe & Process tabs) ====================
|
||
|
||
// Transcribe tab checkbox
|
||
const transcribeTranslateCheckbox = document.getElementById('transcribe-translate');
|
||
const transcribeTranslateLang = document.getElementById('transcribe-translate-lang');
|
||
|
||
transcribeTranslateCheckbox.addEventListener('change', () => {
|
||
transcribeTranslateLang.disabled = !transcribeTranslateCheckbox.checked;
|
||
});
|
||
|
||
// Process tab checkbox
|
||
const processTranslateCheckbox = document.getElementById('process-translate');
|
||
const processTranslateLang = document.getElementById('process-translate-lang');
|
||
|
||
processTranslateCheckbox.addEventListener('change', () => {
|
||
processTranslateLang.disabled = !processTranslateCheckbox.checked;
|
||
});
|
||
|
||
// ==================== TRANSLATE TAB ====================
|
||
|
||
// Mode switching
|
||
const translateModeBtns = document.querySelectorAll('.mode-btn');
|
||
const translateTextMode = document.getElementById('translate-text-mode');
|
||
const translateFileMode = document.getElementById('translate-file-mode');
|
||
|
||
translateModeBtns.forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
translateModeBtns.forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
|
||
if (btn.dataset.mode === 'text') {
|
||
translateTextMode.style.display = 'block';
|
||
translateFileMode.style.display = 'none';
|
||
} else {
|
||
translateTextMode.style.display = 'none';
|
||
translateFileMode.style.display = 'block';
|
||
}
|
||
});
|
||
});
|
||
|
||
// Text translation form
|
||
document.getElementById('translate-text-form').addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
const button = document.getElementById('translate-text-btn');
|
||
const text = document.getElementById('translate-input').value;
|
||
const sourceLang = document.getElementById('translate-source').value;
|
||
const targetLang = document.getElementById('translate-target').value;
|
||
|
||
if (!text.trim()) {
|
||
showResult('translate-text-result', false, '<h3>Error</h3><p>Please enter text to translate</p>');
|
||
return;
|
||
}
|
||
|
||
setLoading(button, true);
|
||
document.getElementById('translate-text-result').classList.remove('show');
|
||
|
||
try {
|
||
const response = await fetch(`${API_URL}/translate`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ text, targetLang, sourceLang: sourceLang || null })
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (!response.ok) throw new Error(data.error || 'Translation failed');
|
||
|
||
showResult('translate-text-result', true, `
|
||
<h3>Translation Complete!</h3>
|
||
<p><strong>From:</strong> ${data.sourceLanguage} <strong>To:</strong> ${data.targetLanguage}</p>
|
||
<div class="translation-output">${data.translatedText}</div>
|
||
`);
|
||
} catch (error) {
|
||
showResult('translate-text-result', false, `<h3>Error</h3><p>${error.message}</p>`);
|
||
} finally {
|
||
setLoading(button, false);
|
||
}
|
||
});
|
||
|
||
// File translation - Drag & Drop
|
||
let translateSelectedFiles = [];
|
||
const translateDropZone = document.getElementById('translate-drop-zone');
|
||
const translateFileInput = document.getElementById('translate-file-input');
|
||
const translateSelectedFilesDiv = document.getElementById('translate-selected-files');
|
||
const translateFilesList = document.getElementById('translate-files-list');
|
||
const translateFileBtn = document.getElementById('translate-file-btn');
|
||
const translateClearFilesBtn = document.getElementById('translate-clear-files');
|
||
|
||
function updateTranslateFilesList() {
|
||
if (translateSelectedFiles.length === 0) {
|
||
translateSelectedFilesDiv.style.display = 'none';
|
||
translateFileBtn.disabled = true;
|
||
return;
|
||
}
|
||
|
||
translateSelectedFilesDiv.style.display = 'block';
|
||
translateFileBtn.disabled = false;
|
||
|
||
translateFilesList.innerHTML = translateSelectedFiles.map((file, index) => `
|
||
<li>
|
||
<span class="file-name">${file.name}</span>
|
||
<span class="file-size">${formatSize(file.size)}</span>
|
||
<button type="button" class="remove-file" data-index="${index}">x</button>
|
||
</li>
|
||
`).join('');
|
||
|
||
translateFilesList.querySelectorAll('.remove-file').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
translateSelectedFiles.splice(parseInt(btn.dataset.index), 1);
|
||
updateTranslateFilesList();
|
||
});
|
||
});
|
||
}
|
||
|
||
function addTranslateFiles(files) {
|
||
const textFiles = Array.from(files).filter(f =>
|
||
f.type === 'text/plain' || f.name.endsWith('.txt')
|
||
);
|
||
translateSelectedFiles = [...translateSelectedFiles, ...textFiles];
|
||
updateTranslateFilesList();
|
||
}
|
||
|
||
translateDropZone.addEventListener('click', () => translateFileInput.click());
|
||
|
||
translateDropZone.addEventListener('dragover', (e) => {
|
||
e.preventDefault();
|
||
translateDropZone.classList.add('drag-over');
|
||
});
|
||
|
||
translateDropZone.addEventListener('dragleave', () => {
|
||
translateDropZone.classList.remove('drag-over');
|
||
});
|
||
|
||
translateDropZone.addEventListener('drop', (e) => {
|
||
e.preventDefault();
|
||
translateDropZone.classList.remove('drag-over');
|
||
addTranslateFiles(e.dataTransfer.files);
|
||
});
|
||
|
||
translateFileInput.addEventListener('change', () => {
|
||
addTranslateFiles(translateFileInput.files);
|
||
translateFileInput.value = '';
|
||
});
|
||
|
||
translateClearFilesBtn.addEventListener('click', () => {
|
||
translateSelectedFiles = [];
|
||
updateTranslateFilesList();
|
||
});
|
||
|
||
// File translation form submit
|
||
document.getElementById('translate-file-form').addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
if (translateSelectedFiles.length === 0) return;
|
||
|
||
const button = translateFileBtn;
|
||
const sourceLang = document.getElementById('translate-file-source').value;
|
||
const targetLang = document.getElementById('translate-file-target').value;
|
||
|
||
setLoading(button, true);
|
||
document.getElementById('translate-file-result').classList.remove('show');
|
||
|
||
const formData = new FormData();
|
||
translateSelectedFiles.forEach(file => formData.append('files', file));
|
||
formData.append('targetLang', targetLang);
|
||
if (sourceLang) formData.append('sourceLang', sourceLang);
|
||
|
||
try {
|
||
const response = await fetch(`${API_URL}/translate-file`, {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (!response.ok) throw new Error(data.error || 'Translation failed');
|
||
|
||
showResult('translate-file-result', true, `
|
||
<h3>Translation Complete!</h3>
|
||
<p>${data.successCount}/${data.totalFiles} files translated</p>
|
||
<ul>${data.results.map(r => `
|
||
<li>
|
||
<span class="${r.success ? 'icon-success' : 'icon-error'}">${r.success ? '✓' : '✗'}</span>
|
||
${r.fileName || r.originalPath}
|
||
${r.success && r.translationUrl ? `<a href="${r.translationUrl}" target="_blank">View</a>` : ''}
|
||
${r.error ? `<small>(${r.error})</small>` : ''}
|
||
</li>
|
||
`).join('')}</ul>
|
||
${data.results[0]?.translatedText ? `
|
||
<h4>Preview (first file):</h4>
|
||
<div class="translation-output">${data.results[0].translatedText.substring(0, 1000)}${data.results[0].translatedText.length > 1000 ? '...' : ''}</div>
|
||
` : ''}
|
||
`);
|
||
|
||
translateSelectedFiles = [];
|
||
updateTranslateFilesList();
|
||
|
||
} catch (error) {
|
||
showResult('translate-file-result', false, `<h3>Error</h3><p>${error.message}</p>`);
|
||
} finally {
|
||
setLoading(button, false);
|
||
}
|
||
});
|