Files
VibeDownloader/src/renderer.js
T

722 lines
24 KiB
JavaScript

// DOM Elements
const dependencyOverlay = document.getElementById('dependency-overlay');
const depTitle = document.getElementById('dep-title');
const depMsg = document.getElementById('dep-msg');
const depProgress = document.getElementById('dep-progress');
const depPercent = document.getElementById('dep-percent');
const urlInput = document.getElementById('url-input');
const analyzeBtn = document.getElementById('analyze-btn');
const outputPath = document.getElementById('output-path');
const changeFolderBtn = document.getElementById('change-folder-btn');
const useCookiesCheckbox = document.getElementById('use-cookies');
const cookieBrowserSelect = document.getElementById('cookie-browser');
const errorCard = document.getElementById('error-card');
const errorMessage = document.getElementById('error-message');
const step1 = document.getElementById('step-1');
const step2 = document.getElementById('step-2');
const backBtn = document.getElementById('back-btn');
const trackList = document.getElementById('track-list');
const formatMp3Btn = document.getElementById('format-mp3-btn');
const formatMp4Btn = document.getElementById('format-mp4-btn');
const qualityPillsContainer = document.getElementById('quality-pills');
const downloadBtn = document.getElementById('download-btn');
const downloadProgressPanel = document.getElementById('download-progress-panel');
const downloadStatus = document.getElementById('download-status');
const downloadPercent = document.getElementById('download-percent');
const downloadProgressBar = document.getElementById('download-progress-bar');
const downloadSubtext = document.getElementById('download-subtext');
const languageSelect = document.getElementById('language-select');
// State Variables
let analyzedUrl = '';
let currentFormat = 'mp3'; // 'mp3' or 'mp4'
let currentQuality = '320'; // Selected quality value
let currentVideoData = null;
// Playlist State Variables
const playlistHeader = document.getElementById('playlist-header');
const selectAllCheckbox = document.getElementById('select-all-checkbox');
const bulkMp3Btn = document.getElementById('bulk-mp3-btn');
const bulkMp4Btn = document.getElementById('bulk-mp4-btn');
let playlistTracks = [];
let focusedTrackId = null;
let isPlaylistMode = false;
// Localization State
let currentLang = 'tr';
let translations = {};
// 0. Translation Engine Functions
async function loadTranslations(lang) {
try {
const res = await fetch(`locales/${lang}.json`);
translations = await res.json();
currentLang = lang;
localStorage.setItem('vibe_lang', lang);
// Toggle direction attribute for Arabic (RTL)
if (lang === 'ar') {
document.documentElement.setAttribute('dir', 'rtl');
document.documentElement.setAttribute('lang', 'ar');
} else {
document.documentElement.removeAttribute('dir');
document.documentElement.setAttribute('lang', lang);
}
// Apply translations to data-i18n attributes
applyTranslations();
} catch (err) {
console.error('Error loading language file:', err);
}
}
function t(key, vars = {}) {
let text = translations[key] || key;
for (const [k, v] of Object.entries(vars)) {
text = text.replace(`%{${k}}`, v);
}
return text;
}
function applyTranslations() {
// Elements with data-i18n attribute
document.querySelectorAll('[data-i18n]').forEach(el => {
const key = el.dataset.i18n;
el.textContent = t(key);
});
// Elements with data-i18n-placeholder attribute
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
const key = el.dataset.i18nPlaceholder;
el.placeholder = t(key);
});
// Elements with data-i18n-title attribute
document.querySelectorAll('[data-i18n-title]').forEach(el => {
const key = el.dataset.i18nTitle;
el.title = t(key);
});
// Dynamically redraw active quality pills based on current locale
renderQualityPills();
// If track is rendered, translate its status and duration labels
if (currentVideoData) {
renderTrackList(currentVideoData);
}
}
// Quality Configuration Generator (Dynamic for Localization)
function getQualityConfigs() {
return {
mp3: [
{ label: `320 kbps (${t('quality_best')})`, value: '320' },
{ label: '256 kbps', value: '256' },
{ label: '192 kbps', value: '192' }
],
mp4: [
{ label: '1080p (Full HD)', value: '1080' },
{ label: '720p (HD)', value: '720' },
{ label: `480p (${t('quality_standard')})`, value: '480' }
]
};
}
// 1. Initial Load & Dependency Verification
window.addEventListener('DOMContentLoaded', async () => {
// Setup Custom Titlebar Controls
document.getElementById('titlebar-minimize').addEventListener('click', () => {
window.api.minimizeWindow();
});
document.getElementById('titlebar-close').addEventListener('click', () => {
window.api.closeWindow();
});
// Setup Language Selector Change Handler
languageSelect.addEventListener('change', async (e) => {
await loadTranslations(e.target.value);
});
// Select All Checkbox Handler
selectAllCheckbox.addEventListener('change', () => {
const checked = selectAllCheckbox.checked;
playlistTracks.forEach(track => {
track.checked = checked;
});
renderTrackList(currentVideoData);
});
// Bulk MP3 Button Handler
bulkMp3Btn.addEventListener('click', () => {
playlistTracks.forEach(track => {
if (track.checked) {
track.format = 'mp3';
track.quality = '320';
}
});
const focusedTrack = playlistTracks.find(t => t.id === focusedTrackId);
if (focusedTrack) syncBottomPanelWithTrack(focusedTrack);
renderTrackList(currentVideoData);
});
// Bulk MP4 Button Handler
bulkMp4Btn.addEventListener('click', () => {
playlistTracks.forEach(track => {
if (track.checked) {
track.format = 'mp4';
track.quality = '1080';
}
});
const focusedTrack = playlistTracks.find(t => t.id === focusedTrackId);
if (focusedTrack) syncBottomPanelWithTrack(focusedTrack);
renderTrackList(currentVideoData);
});
// Get and load persisted or default language
const savedLang = localStorage.getItem('vibe_lang') || 'tr';
languageSelect.value = savedLang;
await loadTranslations(savedLang);
// Get and display default downloads folder path
const defaultPath = await window.api.getDownloadsFolder();
outputPath.textContent = defaultPath;
// Listen to dependency checking progress
window.api.onDependencyStatus(({ message, progress }) => {
let msg = message;
if (message.includes('Downloading yt-dlp')) {
const pctMatch = message.match(/(\d+)%/);
msg = pctMatch ? `${t('btn_downloading')} yt-dlp... ${pctMatch[1]}%` : `${t('btn_downloading')} yt-dlp...`;
} else if (message.includes('Downloading ffmpeg')) {
const pctMatch = message.match(/(\d+)%/);
msg = pctMatch ? `${t('btn_downloading')} ffmpeg... ${pctMatch[1]}%` : `${t('btn_downloading')} ffmpeg...`;
} else if (message.includes('Extracting ffmpeg')) {
msg = `${t('status_processing')} ffmpeg...`;
} else if (message.includes('Dependencies ready')) {
msg = t('status_completed');
}
depMsg.textContent = msg;
depProgress.style.width = `${progress}%`;
depPercent.textContent = `${progress}%`;
});
try {
const result = await window.api.checkDependencies();
if (result.success) {
// Transition out the loading overlay
dependencyOverlay.style.opacity = '0';
setTimeout(() => {
dependencyOverlay.classList.remove('active');
}, 400);
} else {
depTitle.textContent = t('badge_error');
depMsg.textContent = `${t('error_default')} ${result.error}`;
depPercent.textContent = t('badge_error');
}
} catch (err) {
depTitle.textContent = t('badge_error');
depMsg.textContent = err.message;
}
});
// 2. Output Path Folder Changer
changeFolderBtn.addEventListener('click', async () => {
const newPath = await window.api.selectDirectory();
outputPath.textContent = newPath;
});
// 3. Format Toggle Event Listeners
formatMp3Btn.addEventListener('click', () => {
if (currentFormat === 'mp3') return;
currentFormat = 'mp3';
formatMp3Btn.classList.add('active');
formatMp4Btn.classList.remove('active');
currentQuality = '320'; // Reset to default best quality for MP3
renderQualityPills();
applyBottomSettingsToSelected();
});
formatMp4Btn.addEventListener('click', () => {
if (currentFormat === 'mp4') return;
currentFormat = 'mp4';
formatMp4Btn.classList.add('active');
formatMp3Btn.classList.remove('active');
currentQuality = '1080'; // Reset to default best quality for MP4
renderQualityPills();
applyBottomSettingsToSelected();
});
// 4. Quality Pills Renderer
function renderQualityPills() {
qualityPillsContainer.innerHTML = '';
const configs = getQualityConfigs();
const options = configs[currentFormat];
options.forEach(opt => {
const pill = document.createElement('button');
pill.className = 'quality-pill';
if (opt.value === currentQuality) {
pill.classList.add('active');
}
pill.textContent = opt.label;
pill.dataset.value = opt.value;
pill.addEventListener('click', () => {
document.querySelectorAll('.quality-pill').forEach(p => p.classList.remove('active'));
pill.classList.add('active');
currentQuality = opt.value;
applyBottomSettingsToSelected();
});
qualityPillsContainer.appendChild(pill);
});
}
function syncBottomPanelWithTrack(track) {
currentFormat = track.format;
currentQuality = track.quality;
if (currentFormat === 'mp3') {
formatMp3Btn.classList.add('active');
formatMp4Btn.classList.remove('active');
} else {
formatMp4Btn.classList.add('active');
formatMp3Btn.classList.remove('active');
}
renderQualityPills();
}
function applyBottomSettingsToSelected() {
if (!currentVideoData) return;
const checkedTracks = playlistTracks.filter(t => t.checked);
if (checkedTracks.length > 0) {
checkedTracks.forEach(track => {
track.format = currentFormat;
track.quality = currentQuality;
});
} else if (focusedTrackId) {
const focusedTrack = playlistTracks.find(t => t.id === focusedTrackId);
if (focusedTrack) {
focusedTrack.format = currentFormat;
focusedTrack.quality = currentQuality;
}
}
renderTrackList(currentVideoData);
}
function updateSelectAllCheckboxState() {
const checkedCount = playlistTracks.filter(t => t.checked).length;
selectAllCheckbox.checked = checkedCount === playlistTracks.length;
}
// 5. Back Button (Step 2 to Step 1 Transition)
backBtn.addEventListener('click', () => {
// Reset any error card
errorCard.style.display = 'none';
// Fade out Step 2 and show Step 1
step2.classList.remove('active');
step1.classList.add('active');
currentVideoData = null;
playlistTracks = [];
focusedTrackId = null;
urlInput.focus();
});
// 6. Link Analysis Handler
analyzeBtn.addEventListener('click', async () => {
const url = urlInput.value.trim();
if (!url) return;
// Reset error displays
errorCard.style.display = 'none';
// Toggle button loading states
analyzeBtn.disabled = true;
const analyzeSpan = analyzeBtn.querySelector('span');
const analyzeIcon = analyzeBtn.querySelector('.btn-icon');
analyzeSpan.textContent = t('btn_analyzing');
analyzeIcon.style.display = 'inline-block';
try {
const browser = useCookiesCheckbox.checked ? cookieBrowserSelect.value : null;
const result = await window.api.getVideoInfo(url, browser);
if (result.success) {
analyzedUrl = url;
currentVideoData = result.info;
playlistTracks = [];
focusedTrackId = null;
// Render the fetched video details inside the scrollable list container
renderTrackList(result.info);
// Wizard transition: Step 1 -> Step 2
step1.classList.remove('active');
step2.classList.add('active');
} else {
showError(result.error);
}
} catch (err) {
showError(err.message);
} finally {
analyzeBtn.disabled = false;
analyzeSpan.textContent = t('btn_analyze');
analyzeIcon.style.display = 'none';
}
});
// 7. Render Track List Function
function renderTrackList(info) {
trackList.innerHTML = '';
if (info.isPlaylist) {
isPlaylistMode = true;
playlistHeader.style.display = 'flex';
} else {
isPlaylistMode = false;
playlistHeader.style.display = 'none';
}
if (info.entries) {
info.entries.forEach(entry => {
const existing = playlistTracks.find(t => t.id === entry.id);
if (!existing) {
playlistTracks.push({
id: entry.id,
title: entry.title,
thumbnail: entry.thumbnail,
duration: entry.duration,
uploader: entry.uploader,
url: entry.url,
format: currentFormat,
quality: currentQuality,
status: 'ready',
progress: 0,
checked: true
});
}
});
}
if (playlistTracks.length > 0 && !focusedTrackId) {
focusedTrackId = playlistTracks[0].id;
}
const focusedTrack = playlistTracks.find(t => t.id === focusedTrackId);
if (focusedTrack) {
syncBottomPanelWithTrack(focusedTrack);
}
playlistTracks.forEach(track => {
const itemEl = document.createElement('div');
itemEl.className = `track-item ${track.id === focusedTrackId ? 'focused' : ''}`;
itemEl.dataset.id = track.id;
const checkboxWrapper = document.createElement('div');
checkboxWrapper.className = 'track-checkbox-wrapper';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.className = 'track-checkbox';
checkbox.checked = track.checked;
checkbox.addEventListener('change', (e) => {
e.stopPropagation();
track.checked = checkbox.checked;
updateSelectAllCheckboxState();
});
checkboxWrapper.appendChild(checkbox);
itemEl.appendChild(checkboxWrapper);
const thumbEl = document.createElement('div');
thumbEl.className = 'track-thumb';
thumbEl.innerHTML = `
<svg class="track-thumb-fallback" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polygon points="23 7 16 12 23 17 23 7"></polygon>
<rect x="1" y="5" width="15" height="14" rx="2" ry="2"></rect>
</svg>
<img src="${track.thumbnail || ''}" onerror="this.style.display='none';" alt="Thumbnail">
`;
itemEl.appendChild(thumbEl);
const detailsEl = document.createElement('div');
detailsEl.className = 'track-details';
detailsEl.innerHTML = `
<h4>${track.title}</h4>
<p class="track-uploader">${track.uploader}</p>
<span class="track-duration">${t('duration_label')} ${track.duration || t('duration_unknown')}</span>
`;
itemEl.appendChild(detailsEl);
const formatSelectorEl = document.createElement('div');
formatSelectorEl.className = 'track-format-selector';
const formatBadgeBtn = document.createElement('button');
formatBadgeBtn.className = `format-badge-btn ${track.format}`;
formatBadgeBtn.textContent = track.format.toUpperCase();
formatBadgeBtn.addEventListener('click', (e) => {
e.stopPropagation();
track.format = track.format === 'mp3' ? 'mp4' : 'mp3';
track.quality = track.format === 'mp3' ? '320' : '1080';
if (track.id === focusedTrackId) {
syncBottomPanelWithTrack(track);
}
renderTrackList(currentVideoData);
});
formatSelectorEl.appendChild(formatBadgeBtn);
itemEl.appendChild(formatSelectorEl);
const statusEl = document.createElement('div');
statusEl.className = 'track-status';
let statusText = t('badge_ready');
let badgeClass = 'ready';
if (track.status === 'downloading') {
statusText = track.progress > 0 ? `${t('badge_downloading')} %${track.progress}` : t('badge_downloading');
badgeClass = 'downloading';
} else if (track.status === 'completed') {
statusText = t('badge_completed');
badgeClass = 'completed';
} else if (track.status === 'error') {
statusText = t('badge_error');
badgeClass = 'error';
} else if (track.status === 'waiting') {
statusText = t('status_connecting');
badgeClass = 'ready';
}
const statusBadge = document.createElement('span');
statusBadge.id = `badge-${track.id}`;
statusBadge.className = `status-badge ${badgeClass}`;
if (track.status === 'completed') {
statusBadge.style.borderColor = 'rgba(0, 255, 135, 0.3)';
statusBadge.style.color = '#00ff87';
statusBadge.style.background = 'rgba(0, 255, 135, 0.1)';
} else if (track.status === 'error') {
statusBadge.style.borderColor = 'rgba(255, 77, 77, 0.3)';
statusBadge.style.color = '#ff4d4d';
statusBadge.style.background = 'rgba(255, 77, 77, 0.1)';
}
statusBadge.textContent = statusText;
statusEl.appendChild(statusBadge);
itemEl.appendChild(statusEl);
itemEl.addEventListener('click', () => {
focusedTrackId = track.id;
syncBottomPanelWithTrack(track);
renderTrackList(currentVideoData);
});
trackList.appendChild(itemEl);
});
}
// 8. Media Download Executor
downloadBtn.addEventListener('click', async () => {
if (!analyzedUrl || playlistTracks.length === 0) return;
const selectedItems = playlistTracks.filter(t => t.checked);
if (selectedItems.length === 0) {
Swal.fire({
title: t('badge_error'),
text: t('err_no_items_selected') || 'Lütfen indirmek için en az bir dosya seçin.',
icon: 'warning',
background: 'rgba(23, 20, 38, 0.95)',
color: '#fff',
confirmButtonColor: '#7000ff'
});
return;
}
// Setup download progress panel UI states
downloadProgressPanel.style.display = 'block';
downloadBtn.disabled = true;
backBtn.disabled = true;
downloadStatus.textContent = t('status_connecting');
downloadPercent.textContent = '0%';
downloadProgressBar.style.width = '0%';
downloadSubtext.textContent = t('subtext_wait');
// Disable UI interactive components during download
document.querySelectorAll('.track-checkbox, .format-badge-btn, .quality-pill, .format-btn').forEach(el => {
el.disabled = true;
el.style.pointerEvents = 'none';
});
// Mark selected items as waiting
selectedItems.forEach(track => {
track.status = 'waiting';
track.progress = 0;
const badge = document.getElementById(`badge-${track.id}`);
if (badge) {
badge.textContent = t('status_connecting');
badge.className = 'status-badge ready';
}
});
// Subscribe to progress events from the main process
window.api.onDownloadProgress(({ itemId, index, total, percent, status, title, failed }) => {
const track = playlistTracks.find(t => t.id === itemId);
if (track) {
if (percent === 100) {
track.status = 'completed';
track.progress = 100;
} else if (failed) {
track.status = 'error';
} else {
track.status = 'downloading';
track.progress = percent;
}
}
// Dynamic row updates
const badge = document.getElementById(`badge-${itemId}`);
if (badge) {
if (percent === 100) {
badge.textContent = t('badge_completed');
badge.className = 'status-badge completed';
badge.style.borderColor = 'rgba(0, 255, 135, 0.3)';
badge.style.color = '#00ff87';
badge.style.background = 'rgba(0, 255, 135, 0.1)';
} else if (failed) {
badge.textContent = t('badge_error');
badge.className = 'status-badge error';
badge.style.borderColor = 'rgba(255, 77, 77, 0.3)';
badge.style.color = '#ff4d4d';
badge.style.background = 'rgba(255, 77, 77, 0.1)';
} else {
badge.textContent = `${t('badge_downloading')} %${percent}`;
badge.className = 'status-badge downloading';
}
}
// Update main footer progress panel
let stat = status;
if (status.includes('İndiriliyor')) {
stat = `${t('btn_downloading')}... %${percent}`;
} else if (status.includes('İşleniyor') || status.includes('Dönüştürülüyor')) {
stat = t('status_processing');
}
downloadStatus.textContent = `[${index + 1}/${total}] ${title}`;
downloadPercent.textContent = `${percent}%`;
downloadProgressBar.style.width = `${percent}%`;
downloadSubtext.textContent = `${t('swal_file_size')}: ${stat}`;
});
try {
const browser = useCookiesCheckbox.checked ? cookieBrowserSelect.value : null;
// Map selected items into a simplified API task schema
const downloadTasks = selectedItems.map(t => ({
id: t.id,
title: t.title,
url: t.url,
format: t.format,
quality: t.quality
}));
const result = await window.api.downloadMedia(downloadTasks, browser);
if (result.success) {
downloadStatus.textContent = t('status_completed');
downloadPercent.textContent = '100%';
downloadProgressBar.style.width = '100%';
downloadSubtext.textContent = t('subtext_success');
// Success gradient transitions
downloadProgressBar.style.background = 'linear-gradient(90deg, #00ff87 0%, #60efff 100%)';
downloadProgressBar.style.boxShadow = '0 0 15px rgba(0, 255, 135, 0.6)';
const displayTitle = isPlaylistMode
? `${currentVideoData.playlistTitle || 'Playlist'} (${result.stats.completed}/${result.stats.totalItems})`
: selectedItems[0].title;
const displayUploader = isPlaylistMode
? 'Playlist Queue'
: (selectedItems[0].uploader || t('duration_unknown'));
const displaySize = isPlaylistMode
? `${result.stats.completed} ${t('badge_completed').toLowerCase()}`
: (result.stats.results[0]?.size || 'Bilinmiyor');
const localizedDuration = result.stats.duration
.replace('saniye', t('sec_suffix'))
.replace('dakika', t('min_suffix'));
// Trigger premium SweetAlert2 modal
Swal.fire({
title: t('swal_success_title'),
html: `
<div style="text-align: ${currentLang === 'ar' ? 'right' : 'left'}; direction: ${currentLang === 'ar' ? 'rtl' : 'ltr'}; font-family: 'Outfit', sans-serif; color: #fff; line-height: 1.6; font-size: 14px;">
<p style="margin-bottom: 8px;"><strong>${isPlaylistMode ? t('playlist_title_label') || 'Çalma Listesi:' : t('swal_file_name')}</strong> <span style="color: #60efff;">${displayTitle}</span></p>
<p style="margin-bottom: 8px;"><strong>${t('swal_platform_uploader')}</strong> <span style="color: #60efff;">${displayUploader}</span></p>
<p style="margin-bottom: 8px;"><strong>${isPlaylistMode ? t('downloaded_files_label') || 'İndirilen Dosyalar:' : t('swal_file_size')}</strong> <span style="color: #00ff87;">${displaySize}</span></p>
<p style="margin-bottom: 8px;"><strong>${t('swal_duration')}</strong> <span style="color: #00ff87;">${localizedDuration}</span></p>
<p style="margin-top: 15px; font-size: 12px; opacity: 0.8; word-break: break-all;"><strong>${t('swal_output_dir')}</strong> <br>${result.stats.outputDir}</p>
</div>
`,
icon: 'success',
background: 'rgba(23, 20, 38, 0.95)',
color: '#fff',
confirmButtonText: t('swal_btn_open_folder'),
showCancelButton: true,
cancelButtonText: t('swal_btn_new_download'),
confirmButtonColor: '#7000ff',
cancelButtonColor: 'rgba(255, 255, 255, 0.1)',
backdrop: 'rgba(0, 0, 0, 0.6)'
}).then((swalResult) => {
if (swalResult.isConfirmed) {
window.api.openPath(result.stats.outputDir);
} else {
backBtn.click();
urlInput.value = '';
downloadProgressPanel.style.display = 'none';
}
});
} else {
showError(result.error);
downloadProgressPanel.style.display = 'none';
}
} catch (err) {
showError(err.message);
downloadProgressPanel.style.display = 'none';
} finally {
downloadBtn.disabled = false;
backBtn.disabled = false;
document.querySelectorAll('.track-checkbox, .format-badge-btn, .quality-pill, .format-btn').forEach(el => {
el.disabled = false;
el.style.pointerEvents = 'auto';
});
}
});
// Helper: Show Error Alert inside Step 1
function showError(msg) {
let friendlyMsg = msg;
if (msg.includes('Could not copy') && msg.includes('cookie database')) {
friendlyMsg = t('err_cookies_locked');
} else if (msg.includes('You need to log in to access this content')) {
friendlyMsg = t('err_login_required');
} else if (msg.includes('Failed to decrypt with DPAPI')) {
friendlyMsg = t('err_dpapi_fail');
}
errorMessage.textContent = `${t('err_prefix')}${friendlyMsg}`;
errorCard.style.display = 'flex';
}