feat: implement initial UI and core structure for cross-platform media downloader with localization support
This commit is contained in:
@@ -135,6 +135,23 @@
|
||||
|
||||
<!-- Scrollable Track List Area -->
|
||||
<div class="track-list-scroll">
|
||||
<!-- Playlist Bulk Actions and Select All Header -->
|
||||
<div id="playlist-header" class="playlist-header" style="display: none;">
|
||||
<div class="playlist-select-all">
|
||||
<input type="checkbox" id="select-all-checkbox" checked style="cursor: pointer; accent-color: #7000ff; width: 16px; height: 16px;">
|
||||
<label for="select-all-checkbox" id="select-all-label" data-i18n="playlist_select_all" style="cursor: pointer; color: #fff; font-size: 0.9em; font-weight: 500; user-select: none;">Tümünü Seç</label>
|
||||
</div>
|
||||
<div class="playlist-bulk-actions">
|
||||
<button id="bulk-mp3-btn" class="bulk-action-btn">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 14px; height: 14px;"><path d="M9 18V5l12-2v13" /><circle cx="6" cy="18" r="3" /><circle cx="18" cy="16" r="3" /></svg>
|
||||
<span data-i18n="bulk_mp3">Tümünü MP3 Yap</span>
|
||||
</button>
|
||||
<button id="bulk-mp4-btn" class="bulk-action-btn">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 14px; height: 14px;"><rect x="2" y="2" width="20" height="20" rx="2.18" ry="2.18" /><line x1="7" y1="2" x2="7" y2="22" /><line x1="17" y1="2" x2="17" y2="22" /><line x1="2" y1="12" x2="22" y2="12" /></svg>
|
||||
<span data-i18n="bulk_mp4">Tümünü MP4 Yap</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="track-list" class="track-list">
|
||||
<!-- Dynamically populated by Javascript -->
|
||||
</div>
|
||||
|
||||
+7
-1
@@ -55,5 +55,11 @@
|
||||
"quality_best": "Highest",
|
||||
"quality_standard": "Standard",
|
||||
"sec_suffix": "seconds",
|
||||
"min_suffix": "minutes"
|
||||
"min_suffix": "minutes",
|
||||
"playlist_select_all": "Select All",
|
||||
"bulk_mp3": "Bulk MP3",
|
||||
"bulk_mp4": "Bulk MP4",
|
||||
"err_no_items_selected": "Please select at least one file to download.",
|
||||
"playlist_title_label": "Playlist Title:",
|
||||
"downloaded_files_label": "Downloaded Files:"
|
||||
}
|
||||
|
||||
+7
-1
@@ -55,5 +55,11 @@
|
||||
"quality_best": "En Yüksek",
|
||||
"quality_standard": "Standart",
|
||||
"sec_suffix": "saniye",
|
||||
"min_suffix": "dakika"
|
||||
"min_suffix": "dakika",
|
||||
"playlist_select_all": "Tümünü Seç",
|
||||
"bulk_mp3": "Tümünü MP3 Yap",
|
||||
"bulk_mp4": "Tümünü MP4 Yap",
|
||||
"err_no_items_selected": "Lütfen indirmek için en az bir dosya seçin.",
|
||||
"playlist_title_label": "Çalma Listesi:",
|
||||
"downloaded_files_label": "İndirilen Dosyalar:"
|
||||
}
|
||||
|
||||
+340
-60
@@ -39,6 +39,15 @@ 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 = {};
|
||||
@@ -134,6 +143,41 @@ window.addEventListener('DOMContentLoaded', async () => {
|
||||
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;
|
||||
@@ -195,6 +239,7 @@ formatMp3Btn.addEventListener('click', () => {
|
||||
formatMp4Btn.classList.remove('active');
|
||||
currentQuality = '320'; // Reset to default best quality for MP3
|
||||
renderQualityPills();
|
||||
applyBottomSettingsToSelected();
|
||||
});
|
||||
|
||||
formatMp4Btn.addEventListener('click', () => {
|
||||
@@ -204,6 +249,7 @@ formatMp4Btn.addEventListener('click', () => {
|
||||
formatMp3Btn.classList.remove('active');
|
||||
currentQuality = '1080'; // Reset to default best quality for MP4
|
||||
renderQualityPills();
|
||||
applyBottomSettingsToSelected();
|
||||
});
|
||||
|
||||
// 4. Quality Pills Renderer
|
||||
@@ -225,12 +271,50 @@ function renderQualityPills() {
|
||||
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
|
||||
@@ -239,6 +323,8 @@ backBtn.addEventListener('click', () => {
|
||||
step2.classList.remove('active');
|
||||
step1.classList.add('active');
|
||||
currentVideoData = null;
|
||||
playlistTracks = [];
|
||||
focusedTrackId = null;
|
||||
urlInput.focus();
|
||||
});
|
||||
|
||||
@@ -263,6 +349,8 @@ analyzeBtn.addEventListener('click', async () => {
|
||||
if (result.success) {
|
||||
analyzedUrl = url;
|
||||
currentVideoData = result.info;
|
||||
playlistTracks = [];
|
||||
focusedTrackId = null;
|
||||
|
||||
// Render the fetched video details inside the scrollable list container
|
||||
renderTrackList(result.info);
|
||||
@@ -284,30 +372,174 @@ analyzeBtn.addEventListener('click', async () => {
|
||||
|
||||
// 7. Render Track List Function
|
||||
function renderTrackList(info) {
|
||||
trackList.innerHTML = `
|
||||
<div class="track-item">
|
||||
<div class="track-thumb">
|
||||
<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="${info.thumbnail || ''}" onerror="this.style.display='none';" alt="Thumbnail">
|
||||
</div>
|
||||
<div class="track-details">
|
||||
<h4>${info.title}</h4>
|
||||
<p class="track-uploader">${info.uploader}</p>
|
||||
<span class="track-duration">${t('duration_label')} ${info.duration || t('duration_unknown')}</span>
|
||||
</div>
|
||||
<div class="track-status">
|
||||
<span id="track-status-badge" class="status-badge ready">${t('badge_ready')}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
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) return;
|
||||
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';
|
||||
@@ -316,59 +548,112 @@ downloadBtn.addEventListener('click', async () => {
|
||||
downloadStatus.textContent = t('status_connecting');
|
||||
downloadPercent.textContent = '0%';
|
||||
downloadProgressBar.style.width = '0%';
|
||||
downloadSubtext.textContent = currentFormat === 'mp3' ? t('subtext_mp3_download') : t('subtext_mp4_download');
|
||||
downloadSubtext.textContent = t('subtext_wait');
|
||||
|
||||
// Set track status badge as downloading
|
||||
const statusBadge = document.getElementById('track-status-badge');
|
||||
if (statusBadge) {
|
||||
statusBadge.textContent = t('badge_downloading');
|
||||
statusBadge.className = 'status-badge downloading';
|
||||
}
|
||||
// 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(({ percent, status }) => {
|
||||
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('status_downloading', { percent });
|
||||
stat = `${t('btn_downloading')}... %${percent}`;
|
||||
} else if (status.includes('İşleniyor') || status.includes('Dönüştürülüyor')) {
|
||||
stat = t('status_processing');
|
||||
}
|
||||
|
||||
downloadStatus.textContent = stat;
|
||||
downloadStatus.textContent = `[${index + 1}/${total}] ${title}`;
|
||||
downloadPercent.textContent = `${percent}%`;
|
||||
downloadProgressBar.style.width = `${percent}%`;
|
||||
|
||||
if (percent > 90) {
|
||||
downloadSubtext.textContent = currentFormat === 'mp3'
|
||||
? t('subtext_mp3_convert')
|
||||
: t('subtext_mp4_merge');
|
||||
} else {
|
||||
downloadSubtext.textContent = t('subtext_speed_note');
|
||||
}
|
||||
downloadSubtext.textContent = `${t('swal_file_size')}: ${stat}`;
|
||||
});
|
||||
|
||||
try {
|
||||
const browser = useCookiesCheckbox.checked ? cookieBrowserSelect.value : null;
|
||||
const result = await window.api.downloadMedia(analyzedUrl, currentFormat, currentQuality, browser);
|
||||
|
||||
// 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');
|
||||
|
||||
if (statusBadge) {
|
||||
statusBadge.textContent = t('badge_completed');
|
||||
statusBadge.className = 'status-badge ready';
|
||||
statusBadge.style.borderColor = 'rgba(0, 255, 135, 0.3)';
|
||||
statusBadge.style.color = '#00ff87';
|
||||
statusBadge.style.background = 'rgba(0, 255, 135, 0.1)';
|
||||
}
|
||||
|
||||
// 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)';
|
||||
|
||||
// Translate duration to localized representation
|
||||
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'));
|
||||
@@ -378,11 +663,10 @@ downloadBtn.addEventListener('click', async () => {
|
||||
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>${t('swal_file_name')}</strong> <span style="color: #60efff;">${currentVideoData.title}</span></p>
|
||||
<p style="margin-bottom: 8px;"><strong>${t('swal_platform_uploader')}</strong> <span style="color: #60efff;">${currentVideoData.uploader || t('duration_unknown')}</span></p>
|
||||
<p style="margin-bottom: 8px;"><strong>${t('swal_file_size')}</strong> <span style="color: #00ff87;">${result.stats.size}</span></p>
|
||||
<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-bottom: 8px;"><strong>${t('swal_format_quality')}</strong> <span style="color: #ff4da6; text-transform: uppercase;">${currentFormat} (${currentQuality === '320' || currentQuality === '1080' ? t('quality_best') : currentQuality})</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>
|
||||
`,
|
||||
@@ -399,7 +683,6 @@ downloadBtn.addEventListener('click', async () => {
|
||||
if (swalResult.isConfirmed) {
|
||||
window.api.openPath(result.stats.outputDir);
|
||||
} else {
|
||||
// Go back to step 1
|
||||
backBtn.click();
|
||||
urlInput.value = '';
|
||||
downloadProgressPanel.style.display = 'none';
|
||||
@@ -408,13 +691,6 @@ downloadBtn.addEventListener('click', async () => {
|
||||
} else {
|
||||
showError(result.error);
|
||||
downloadProgressPanel.style.display = 'none';
|
||||
if (statusBadge) {
|
||||
statusBadge.textContent = t('badge_error');
|
||||
statusBadge.className = 'status-badge ready';
|
||||
statusBadge.style.borderColor = 'rgba(255, 77, 77, 0.3)';
|
||||
statusBadge.style.color = '#ff4d4d';
|
||||
statusBadge.style.background = 'rgba(255, 77, 77, 0.1)';
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
showError(err.message);
|
||||
@@ -422,6 +698,10 @@ downloadBtn.addEventListener('click', async () => {
|
||||
} 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';
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
+141
@@ -899,3 +899,144 @@ html[dir="rtl"] .error-card {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
/* Playlist Header Controls */
|
||||
.playlist-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 18px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
border-radius: 12px;
|
||||
margin-bottom: 12px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.playlist-select-all {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.playlist-bulk-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.bulk-action-btn {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 8px;
|
||||
padding: 6px 12px;
|
||||
color: #dcd7eb;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.bulk-action-btn:hover {
|
||||
background: rgba(255, 0, 128, 0.1);
|
||||
border-color: rgba(255, 0, 128, 0.3);
|
||||
color: #ff0080;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Premium Playlist Track Selection & Layout */
|
||||
.track-checkbox-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.track-checkbox {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
accent-color: #7000ff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Make track item interactive and clickable */
|
||||
.track-item {
|
||||
cursor: pointer;
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.track-item:hover {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border-color: rgba(255, 255, 255, 0.12);
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
/* Highlight active/focused track */
|
||||
.track-item.focused {
|
||||
background: rgba(112, 0, 255, 0.08);
|
||||
border-color: rgba(112, 0, 255, 0.4);
|
||||
box-shadow: 0 0 15px rgba(112, 0, 255, 0.15),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
/* Custom interactive format switcher badge inside row */
|
||||
.track-format-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.format-badge-btn {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
color: #9e97b8;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
padding: 4px 10px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
min-width: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.format-badge-btn:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.format-badge-btn.mp3 {
|
||||
background: rgba(255, 0, 128, 0.1);
|
||||
color: #ff0080;
|
||||
border-color: rgba(255, 0, 128, 0.3);
|
||||
}
|
||||
|
||||
.format-badge-btn.mp4 {
|
||||
background: rgba(0, 223, 216, 0.1);
|
||||
color: #00dfd8;
|
||||
border-color: rgba(0, 223, 216, 0.3);
|
||||
}
|
||||
|
||||
/* Disabled states during downloading */
|
||||
.track-item.disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
html[dir="rtl"] .playlist-select-all {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
html[dir="rtl"] .bulk-action-btn {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
html[dir="rtl"] .track-format-selector {
|
||||
margin-right: 0;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user