feat: implement YouTube downloader UI and integration logic with renderer, CSS, and main process support

This commit is contained in:
Ümit Tunç
2026-05-28 07:09:34 +03:00
parent 723aea5b04
commit 2b303cfc9b
6 changed files with 721 additions and 317 deletions
+15 -4
View File
@@ -12,8 +12,7 @@ function createWindow() {
height: 700, height: 700,
minWidth: 800, minWidth: 800,
minHeight: 600, minHeight: 600,
frame: true, // We can use frame: true, but let's make it styled inside the HTML with gorgeous glassmorphism! frame: false, // Make window frameless
title: 'VibeDownloader - Premium YouTube & MP3 Downloader',
webPreferences: { webPreferences: {
preload: path.join(__dirname, 'preload.js'), preload: path.join(__dirname, 'preload.js'),
contextIsolation: true, contextIsolation: true,
@@ -22,6 +21,9 @@ function createWindow() {
show: false show: false
}); });
const { Menu } = require('electron');
Menu.setApplicationMenu(null); // Hide the default menu bar
mainWindow.loadFile(path.join(__dirname, 'src', 'index.html')); mainWindow.loadFile(path.join(__dirname, 'src', 'index.html'));
// Open devtools if needed in development // Open devtools if needed in development
@@ -32,6 +34,15 @@ function createWindow() {
}); });
} }
// IPC Handlers for custom titlebar controls
ipcMain.on('window-minimize', () => {
if (mainWindow) mainWindow.minimize();
});
ipcMain.on('window-close', () => {
if (mainWindow) mainWindow.close();
});
app.whenReady().then(() => { app.whenReady().then(() => {
createWindow(); createWindow();
@@ -69,9 +80,9 @@ ipcMain.handle('get-video-info', async (event, url) => {
}); });
// IPC Handler: Download Media // IPC Handler: Download Media
ipcMain.handle('download-media', async (event, { url, format }) => { ipcMain.handle('download-media', async (event, { url, format, quality }) => {
try { try {
await downloader.downloadMedia(url, format, selectedOutputDir, (percent, status) => { await downloader.downloadMedia(url, format, quality, selectedOutputDir, (percent, status) => {
if (mainWindow) { if (mainWindow) {
mainWindow.webContents.send('download-progress', { percent, status }); mainWindow.webContents.send('download-progress', { percent, status });
} }
+4 -2
View File
@@ -4,8 +4,10 @@ contextBridge.exposeInMainWorld('api', {
checkDependencies: () => ipcRenderer.invoke('check-dependencies'), checkDependencies: () => ipcRenderer.invoke('check-dependencies'),
onDependencyStatus: (callback) => ipcRenderer.on('dependency-status', (_, data) => callback(data)), onDependencyStatus: (callback) => ipcRenderer.on('dependency-status', (_, data) => callback(data)),
getVideoInfo: (url) => ipcRenderer.invoke('get-video-info', url), getVideoInfo: (url) => ipcRenderer.invoke('get-video-info', url),
downloadMedia: (url, format) => ipcRenderer.invoke('download-media', { url, format }), downloadMedia: (url, format, quality) => ipcRenderer.invoke('download-media', { url, format, quality }),
onDownloadProgress: (callback) => ipcRenderer.on('download-progress', (_, data) => callback(data)), onDownloadProgress: (callback) => ipcRenderer.on('download-progress', (_, data) => callback(data)),
selectDirectory: () => ipcRenderer.invoke('select-directory'), selectDirectory: () => ipcRenderer.invoke('select-directory'),
getDownloadsFolder: () => ipcRenderer.invoke('get-downloads-folder') getDownloadsFolder: () => ipcRenderer.invoke('get-downloads-folder'),
minimizeWindow: () => ipcRenderer.send('window-minimize'),
closeWindow: () => ipcRenderer.send('window-close')
}); });
+80 -53
View File
@@ -10,6 +10,26 @@
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
</head> </head>
<body> <body>
<!-- Custom Glassmorphic Titlebar -->
<div class="custom-titlebar">
<div class="titlebar-drag">
<svg class="titlebar-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="7 10 12 15 17 10" />
<line x1="12" y1="15" x2="12" y2="3" />
</svg>
<span>VibeDownloader</span>
</div>
<div class="titlebar-controls">
<button id="titlebar-minimize" class="control-btn" title="Simge Durumuna Getir">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="5" y1="12" x2="19" y2="12"></line></svg>
</button>
<button id="titlebar-close" class="control-btn close" title="Kapat">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
</button>
</div>
</div>
<!-- Vibrant Animated Background Elements --> <!-- Vibrant Animated Background Elements -->
<div class="bg-glow bg-glow-1"></div> <div class="bg-glow bg-glow-1"></div>
<div class="bg-glow bg-glow-2"></div> <div class="bg-glow bg-glow-2"></div>
@@ -32,6 +52,9 @@
<!-- Main Container --> <!-- Main Container -->
<main class="app-container"> <main class="app-container">
<!-- STEP 1: Link Analysis -->
<div id="step-1" class="wizard-step active">
<header class="app-header text-center"> <header class="app-header text-center">
<div class="logo-wrapper"> <div class="logo-wrapper">
<svg class="logo-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg class="logo-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@@ -44,7 +67,6 @@
<p class="subtitle">YouTube videolarını saniyeler içinde yüksek kaliteli MP4 veya MP3 olarak indirin</p> <p class="subtitle">YouTube videolarını saniyeler içinde yüksek kaliteli MP4 veya MP3 olarak indirin</p>
</header> </header>
<!-- URL Input Section -->
<section class="glass-card url-section"> <section class="glass-card url-section">
<div class="input-group"> <div class="input-group">
<input type="text" id="url-input" placeholder="YouTube video veya oynatma listesi bağlantısı yapıştırın..." autocomplete="off"> <input type="text" id="url-input" placeholder="YouTube video veya oynatma listesi bağlantısı yapıştırın..." autocomplete="off">
@@ -63,7 +85,7 @@
</div> </div>
</section> </section>
<!-- Error Display --> <!-- Error Display in Step 1 -->
<div id="error-card" class="glass-card error-card" style="display: none;"> <div id="error-card" class="glass-card error-card" style="display: none;">
<svg class="error-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg class="error-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10" /> <circle cx="12" cy="12" r="10" />
@@ -72,73 +94,78 @@
</svg> </svg>
<span id="error-message">Bir hata oluştu.</span> <span id="error-message">Bir hata oluştu.</span>
</div> </div>
<!-- Video Preview & Download Section -->
<section id="preview-section" class="glass-card preview-card" style="display: none;">
<div class="preview-layout">
<div class="thumbnail-wrapper">
<img id="video-thumbnail" src="" alt="Thumbnail">
</div> </div>
<div class="video-details">
<h3 id="video-title">Video Başlığı</h3>
<p id="video-uploader" class="details-text">Kanal Adı</p>
<p id="video-duration" class="details-text">Süre: --:--</p>
<div class="format-selection"> <!-- STEP 2: Download Options & List -->
<label class="radio-card active"> <div id="step-2" class="wizard-step">
<input type="radio" name="format-type" value="mp3" checked> <header class="step-header">
<div class="radio-content"> <button id="back-btn" class="back-button">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<path d="M9 18V5l12-2v13" /> <line x1="19" y1="12" x2="5" y2="12"></line>
<circle cx="6" cy="18" r="3" /> <polyline points="12 19 5 12 12 5"></polyline>
<circle cx="18" cy="16" r="3" />
</svg> </svg>
<div class="radio-info"> <span>Geri Dön</span>
<span class="radio-title">MP3 Ses</span> </button>
<span class="radio-desc">En yüksek kalite (320kbps)</span> <h2 class="step-title">İndirilecek Dosya</h2>
</div> </header>
</div>
</label>
<label class="radio-card"> <!-- Scrollable Track List Area -->
<input type="radio" name="format-type" value="mp4"> <div class="track-list-scroll">
<div class="radio-content"> <div id="track-list" class="track-list">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <!-- Dynamically populated by Javascript -->
<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" />
<line x1="2" y1="7" x2="7" y2="7" />
<line x1="2" y1="17" x2="7" y2="17" />
<line x1="17" y1="17" x2="22" y2="17" />
<line x1="17" y1="7" x2="22" y2="7" />
</svg>
<div class="radio-info">
<span class="radio-title">MP4 Video</span>
<span class="radio-desc">En yüksek çözünürlük</span>
</div> </div>
</div> </div>
</label>
</div>
<button id="download-btn" class="glow-button full-width"> <!-- Fixed Bottom Panel for Formats and Download Action -->
İndirmeyi Başlat <div class="fixed-footer glass-card">
<div class="footer-grid">
<!-- Formats & Quality Picker -->
<div class="options-panel">
<div class="option-row">
<span class="option-label">Format:</span>
<div class="format-toggle">
<button id="format-mp3-btn" class="format-btn active">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18V5l12-2v13" /><circle cx="6" cy="18" r="3" /><circle cx="18" cy="16" r="3" /></svg>
<span>MP3 Ses</span>
</button>
<button id="format-mp4-btn" class="format-btn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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>MP4 Video</span>
</button> </button>
</div> </div>
</div> </div>
</section>
<!-- Download Progress Section --> <div class="option-row">
<section id="download-section" class="glass-card download-card" style="display: none;"> <span class="option-label">Kalite:</span>
<div class="download-header"> <div id="quality-pills" class="quality-pills">
<h4 id="download-status">İndiriliyor...</h4> <!-- Populated dynamically: 320kbps, 256kbps, 192kbps for MP3 or 1080p, 720p, 480p for MP4 -->
<span id="download-percent" class="download-percent">0%</span> </div>
</div>
</div>
<!-- Start Button and Progress Bar -->
<div class="action-panel">
<!-- Progress Section (Active during download) -->
<div id="download-progress-panel" class="progress-panel" style="display: none;">
<div class="progress-header">
<span id="download-status">Bağlantı kuruluyor...</span>
<span id="download-percent">0%</span>
</div> </div>
<div class="progress-bar-container"> <div class="progress-bar-container">
<div id="download-progress-bar" class="progress-bar-fill" style="width: 0%"></div> <div id="download-progress-bar" class="progress-bar-fill" style="width: 0%"></div>
</div> </div>
<p id="download-subtext" class="subtext">Lütfen bekleyin...</p> <p id="download-subtext" class="subtext">Lütfen bekleyin...</p>
</section> </div>
<button id="download-btn" class="glow-button full-width">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="8 17 12 21 16 17"></polyline><line x1="12" y1="12" x2="12" y2="21"></line><path d="M20.88 18.09A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 3 16.29"></path></svg>
<span>İndirmeyi Başlat</span>
</button>
</div>
</div>
</div>
</div>
</main> </main>
<script src="renderer.js"></script> <script src="renderer.js"></script>
+160 -48
View File
@@ -13,29 +13,57 @@ const changeFolderBtn = document.getElementById('change-folder-btn');
const errorCard = document.getElementById('error-card'); const errorCard = document.getElementById('error-card');
const errorMessage = document.getElementById('error-message'); const errorMessage = document.getElementById('error-message');
const previewSection = document.getElementById('preview-section'); const step1 = document.getElementById('step-1');
const videoThumbnail = document.getElementById('video-thumbnail'); const step2 = document.getElementById('step-2');
const videoTitle = document.getElementById('video-title'); const backBtn = document.getElementById('back-btn');
const videoUploader = document.getElementById('video-uploader'); const trackList = document.getElementById('track-list');
const videoDuration = document.getElementById('video-duration');
const radioCards = document.querySelectorAll('.radio-card'); 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 downloadBtn = document.getElementById('download-btn');
const downloadSection = document.getElementById('download-section'); const downloadProgressPanel = document.getElementById('download-progress-panel');
const downloadStatus = document.getElementById('download-status'); const downloadStatus = document.getElementById('download-status');
const downloadPercent = document.getElementById('download-percent'); const downloadPercent = document.getElementById('download-percent');
const downloadProgressBar = document.getElementById('download-progress-bar'); const downloadProgressBar = document.getElementById('download-progress-bar');
const downloadSubtext = document.getElementById('download-subtext'); const downloadSubtext = document.getElementById('download-subtext');
// State Variables
let analyzedUrl = ''; let analyzedUrl = '';
let currentFormat = 'mp3'; // 'mp3' or 'mp4'
let currentQuality = '320'; // Selected quality value
let currentVideoData = null;
// 1. Check and Install Dependencies on load // Quality Configuration Data
const qualityConfigs = {
mp3: [
{ label: '320 kbps (En Yüksek)', 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 (Standart)', value: '480' }
]
};
// 1. Initial Load & Dependency Verification
window.addEventListener('DOMContentLoaded', async () => { window.addEventListener('DOMContentLoaded', async () => {
// Set default output folder path // Setup Custom Titlebar Controls
document.getElementById('titlebar-minimize').addEventListener('click', () => {
window.api.minimizeWindow();
});
document.getElementById('titlebar-close').addEventListener('click', () => {
window.api.closeWindow();
});
// Get and display default downloads folder path
const defaultPath = await window.api.getDownloadsFolder(); const defaultPath = await window.api.getDownloadsFolder();
outputPath.textContent = defaultPath; outputPath.textContent = defaultPath;
// Listen to dependency download updates // Listen to dependency checking progress
window.api.onDependencyStatus(({ message, progress }) => { window.api.onDependencyStatus(({ message, progress }) => {
depMsg.textContent = message; depMsg.textContent = message;
depProgress.style.width = `${progress}%`; depProgress.style.width = `${progress}%`;
@@ -45,7 +73,7 @@ window.addEventListener('DOMContentLoaded', async () => {
try { try {
const result = await window.api.checkDependencies(); const result = await window.api.checkDependencies();
if (result.success) { if (result.success) {
// Fade out overlay smoothly // Transition out the loading overlay
dependencyOverlay.style.opacity = '0'; dependencyOverlay.style.opacity = '0';
setTimeout(() => { setTimeout(() => {
dependencyOverlay.classList.remove('active'); dependencyOverlay.classList.remove('active');
@@ -59,36 +87,79 @@ window.addEventListener('DOMContentLoaded', async () => {
depTitle.textContent = 'Hata Oluştu'; depTitle.textContent = 'Hata Oluştu';
depMsg.textContent = err.message; depMsg.textContent = err.message;
} }
// Render initial quality pills
renderQualityPills();
}); });
// 2. Change Output Folder // 2. Output Path Folder Changer
changeFolderBtn.addEventListener('click', async () => { changeFolderBtn.addEventListener('click', async () => {
const newPath = await window.api.selectDirectory(); const newPath = await window.api.selectDirectory();
outputPath.textContent = newPath; outputPath.textContent = newPath;
}); });
// 3. Radio Card Active Selection UI // 3. Format Toggle Event Listeners
radioCards.forEach(card => { formatMp3Btn.addEventListener('click', () => {
const radio = card.querySelector('input[type="radio"]'); if (currentFormat === 'mp3') return;
radio.addEventListener('change', () => { currentFormat = 'mp3';
radioCards.forEach(c => c.classList.remove('active')); formatMp3Btn.classList.add('active');
if (radio.checked) { formatMp4Btn.classList.remove('active');
card.classList.add('active'); currentQuality = '320'; // Reset to default best quality for MP3
} renderQualityPills();
});
}); });
// 4. URL Validation & Analyze 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();
});
// 4. Quality Pills Renderer
function renderQualityPills() {
qualityPillsContainer.innerHTML = '';
const options = qualityConfigs[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;
});
qualityPillsContainer.appendChild(pill);
});
}
// 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');
urlInput.focus();
});
// 6. Link Analysis Handler
analyzeBtn.addEventListener('click', async () => { analyzeBtn.addEventListener('click', async () => {
const url = urlInput.value.trim(); const url = urlInput.value.trim();
if (!url) return; if (!url) return;
// Reset state // Reset error displays
errorCard.style.display = 'none'; errorCard.style.display = 'none';
previewSection.style.display = 'none';
downloadSection.style.display = 'none';
// Set Loading Button state // Toggle button loading states
analyzeBtn.disabled = true; analyzeBtn.disabled = true;
const analyzeSpan = analyzeBtn.querySelector('span'); const analyzeSpan = analyzeBtn.querySelector('span');
const analyzeIcon = analyzeBtn.querySelector('.btn-icon'); const analyzeIcon = analyzeBtn.querySelector('.btn-icon');
@@ -99,11 +170,14 @@ analyzeBtn.addEventListener('click', async () => {
const result = await window.api.getVideoInfo(url); const result = await window.api.getVideoInfo(url);
if (result.success) { if (result.success) {
analyzedUrl = url; analyzedUrl = url;
videoThumbnail.src = result.info.thumbnail; currentVideoData = result.info;
videoTitle.textContent = result.info.title;
videoUploader.textContent = result.info.uploader; // Render the fetched video details inside the scrollable list container
videoDuration.textContent = `Süre: ${result.info.duration}`; renderTrackList(result.info);
previewSection.style.display = 'block';
// Wizard transition: Step 1 -> Step 2
step1.classList.remove('active');
step2.classList.add('active');
} else { } else {
showError(result.error); showError(result.error);
} }
@@ -116,63 +190,101 @@ analyzeBtn.addEventListener('click', async () => {
} }
}); });
// 5. Download Process // 7. Render Track List Function
function renderTrackList(info) {
trackList.innerHTML = `
<div class="track-item">
<div class="track-thumb">
<img src="${info.thumbnail}" alt="Thumbnail">
</div>
<div class="track-details">
<h4>${info.title}</h4>
<p class="track-uploader">${info.uploader}</p>
<span class="track-duration">Süre: ${info.duration}</span>
</div>
<div class="track-status">
<span id="track-status-badge" class="status-badge ready">Hazır</span>
</div>
</div>
`;
}
// 8. Media Download Executor
downloadBtn.addEventListener('click', async () => { downloadBtn.addEventListener('click', async () => {
if (!analyzedUrl) return; if (!analyzedUrl) return;
const selectedFormat = document.querySelector('input[name="format-type"]:checked').value; // Setup download progress panel UI states
downloadProgressPanel.style.display = 'block';
// Setup download section UI
downloadSection.style.display = 'block';
downloadBtn.disabled = true; downloadBtn.disabled = true;
backBtn.disabled = true;
downloadStatus.textContent = 'Bağlantı kuruluyor...'; downloadStatus.textContent = 'Bağlantı kuruluyor...';
downloadPercent.textContent = '0%'; downloadPercent.textContent = '0%';
downloadProgressBar.style.width = '0%'; downloadProgressBar.style.width = '0%';
downloadSubtext.textContent = selectedFormat === 'mp3' ? 'Ses akışı alınıyor...' : 'Video akışı alınıyor...'; downloadSubtext.textContent = currentFormat === 'mp3' ? 'Ses akışı alınıyor...' : 'Video akışı alınıyor...';
// Smooth scroll to download progress card // Set track status badge as downloading
downloadSection.scrollIntoView({ behavior: 'smooth' }); const statusBadge = document.getElementById('track-status-badge');
if (statusBadge) {
statusBadge.textContent = 'İndiriliyor';
statusBadge.className = 'status-badge downloading';
}
// Listen to progress events // Subscribe to progress events from the main process
window.api.onDownloadProgress(({ percent, status }) => { window.api.onDownloadProgress(({ percent, status }) => {
downloadStatus.textContent = status; downloadStatus.textContent = status;
downloadPercent.textContent = `${percent}%`; downloadPercent.textContent = `${percent}%`;
downloadProgressBar.style.width = `${percent}%`; downloadProgressBar.style.width = `${percent}%`;
if (percent > 90) { if (percent > 90) {
downloadSubtext.textContent = selectedFormat === 'mp3' downloadSubtext.textContent = currentFormat === 'mp3'
? 'Dönüştürme işlemi yapılıyor (Yüksek Kaliteli MP3)...' ? 'Dönüştürme işlemi yapılıyor (Yüksek Kaliteli MP3)...'
: 'Video ve ses dosyaları birleştiriliyor...'; : 'Video ve ses dosyaları birleştiriliyor...';
} else { } else {
downloadSubtext.textContent = `İndirme hızı ve boyuta göre işlem süresi değişebilir.`; downloadSubtext.textContent = 'İndirme hızı ve dosya boyutuna göre işlem süresi değişebilir.';
} }
}); });
try { try {
const result = await window.api.downloadMedia(analyzedUrl, selectedFormat); const result = await window.api.downloadMedia(analyzedUrl, currentFormat, currentQuality);
if (result.success) { if (result.success) {
downloadStatus.textContent = 'Tamamlandı!'; downloadStatus.textContent = 'Tamamlandı!';
downloadPercent.textContent = '100%'; downloadPercent.textContent = '100%';
downloadProgressBar.style.width = '100%'; downloadProgressBar.style.width = '100%';
downloadSubtext.textContent = `Dosyanız başarıyla kaydedildi!`; downloadSubtext.textContent = 'Dosyanız başarıyla kaydedildi!';
// Beautiful visual indicator of success if (statusBadge) {
statusBadge.textContent = 'Tamamlandı';
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.background = 'linear-gradient(90deg, #00ff87 0%, #60efff 100%)';
downloadProgressBar.style.boxShadow = '0 0 15px rgba(0, 255, 135, 0.6)'; downloadProgressBar.style.boxShadow = '0 0 15px rgba(0, 255, 135, 0.6)';
} else { } else {
showError(result.error); showError(result.error);
downloadSection.style.display = 'none'; downloadProgressPanel.style.display = 'none';
if (statusBadge) {
statusBadge.textContent = 'Hata';
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) { } catch (err) {
showError(err.message); showError(err.message);
downloadSection.style.display = 'none'; downloadProgressPanel.style.display = 'none';
} finally { } finally {
downloadBtn.disabled = false; downloadBtn.disabled = false;
backBtn.disabled = false;
} }
}); });
// Helper: Show Error Alert inside Step 1
function showError(msg) { function showError(msg) {
errorMessage.textContent = `Hata: ${msg}`; errorMessage.textContent = `Hata: ${msg}`;
errorCard.style.display = 'flex'; errorCard.style.display = 'flex';
errorCard.scrollIntoView({ behavior: 'smooth' });
} }
+406 -156
View File
@@ -10,14 +10,84 @@
body { body {
background: #09060f; background: #09060f;
color: #f1f0f5; color: #f1f0f5;
min-height: 100vh; height: 100vh;
display: flex; display: flex;
justify-content: center; flex-direction: column;
align-items: center; overflow: hidden; /* NO SCROLL FOR THE MAIN WINDOW */
overflow-x: hidden;
position: relative; position: relative;
} }
/* Custom Glassmorphic Titlebar */
.custom-titlebar {
display: flex;
justify-content: space-between;
align-items: center;
height: 38px;
background: rgba(12, 8, 20, 0.7);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 9999;
}
.titlebar-drag {
flex: 1;
-webkit-app-region: drag;
height: 100%;
display: flex;
align-items: center;
padding-left: 16px;
font-size: 0.8rem;
font-weight: 600;
color: #b3aeca;
letter-spacing: 0.5px;
gap: 8px;
}
.titlebar-icon {
width: 14px;
height: 14px;
color: #ff0080;
}
.titlebar-controls {
display: flex;
height: 100%;
-webkit-app-region: no-drag;
}
.control-btn {
background: none;
border: none;
width: 46px;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #b3aeca;
cursor: pointer;
transition: all 0.2s ease;
}
.control-btn:hover {
background: rgba(255, 255, 255, 0.08);
color: #ffffff;
}
.control-btn.close:hover {
background: #ff4d4d;
color: #ffffff;
}
.control-btn svg {
width: 14px;
height: 14px;
}
/* Vibrant Animated Background Glows */ /* Vibrant Animated Background Glows */
.bg-glow { .bg-glow {
position: absolute; position: absolute;
@@ -58,15 +128,9 @@ body {
} }
@keyframes float-glow { @keyframes float-glow {
0% { 0% { transform: translate(0, 0) scale(1); }
transform: translate(0, 0) scale(1); 50% { transform: translate(80px, 50px) scale(1.1); }
} 100% { transform: translate(-40px, -60px) scale(0.9); }
50% {
transform: translate(80px, 50px) scale(1.1);
}
100% {
transform: translate(-40px, -60px) scale(0.9);
}
} }
/* Premium Glassmorphic Cards */ /* Premium Glassmorphic Cards */
@@ -79,7 +143,6 @@ body {
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.4), box-shadow: 0 15px 35px rgba(0, 0, 0, 0.4),
inset 0 1px 0 rgba(255, 255, 255, 0.1); inset 0 1px 0 rgba(255, 255, 255, 0.1);
padding: 24px; padding: 24px;
margin-bottom: 20px;
position: relative; position: relative;
z-index: 10; z-index: 10;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
@@ -93,23 +156,50 @@ body {
/* Main Container Layout */ /* Main Container Layout */
.app-container { .app-container {
flex: 1;
width: 100%; width: 100%;
max-width: 800px; max-width: 860px;
margin: 38px auto 0 auto; /* Under custom titlebar */
padding: 20px; padding: 20px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
z-index: 10; z-index: 10;
animation: fade-in 0.8s ease-out; position: relative;
overflow: hidden;
}
/* Wizard step switcher */
.wizard-step {
display: none;
flex-direction: column;
height: 100%;
width: 100%;
animation: fade-in 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.wizard-step.active {
display: flex;
} }
@keyframes fade-in { @keyframes fade-in {
from { opacity: 0; transform: translateY(20px); } from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); } to { opacity: 1; transform: translateY(0); }
} }
/* Header Styles */ /* Step 1 Styles */
#step-1 {
justify-content: center;
align-items: center;
max-width: 720px;
margin: 0 auto;
}
#step-1 .url-section {
width: 100%;
}
.app-header { .app-header {
margin-bottom: 30px; margin-bottom: 24px;
} }
.logo-wrapper { .logo-wrapper {
@@ -129,7 +219,7 @@ body {
} }
.glow-text { .glow-text {
font-size: 2.5rem; font-size: 2.3rem;
font-weight: 800; font-weight: 800;
letter-spacing: -0.5px; letter-spacing: -0.5px;
background: linear-gradient(to right, #ffffff, #dcd7eb, #ff0080); background: linear-gradient(to right, #ffffff, #dcd7eb, #ff0080);
@@ -141,7 +231,7 @@ body {
.subtitle { .subtitle {
color: #b3aeca; color: #b3aeca;
font-size: 1rem; font-size: 0.95rem;
font-weight: 400; font-weight: 400;
} }
@@ -174,7 +264,7 @@ input[type="text"]:focus {
background: rgba(0, 0, 0, 0.4); background: rgba(0, 0, 0, 0.4);
} }
/* Glowing Dynamic Buttons */ /* Glowing Buttons */
.glow-button { .glow-button {
background: linear-gradient(135deg, #7928ca 0%, #ff0080 100%); background: linear-gradient(135deg, #7928ca 0%, #ff0080 100%);
color: #ffffff; color: #ffffff;
@@ -207,20 +297,25 @@ input[type="text"]:focus {
} }
.glow-button:disabled { .glow-button:disabled {
background: rgba(255, 255, 255, 0.1); background: rgba(255, 255, 255, 0.1) !important;
color: #6e6a82; color: #6e6a82 !important;
box-shadow: none; box-shadow: none !important;
cursor: not-allowed; cursor: not-allowed;
transform: none; transform: none !important;
} }
/* Path Row */ .glow-button svg {
width: 18px;
height: 18px;
}
/* Output Folder Info Bar */
.output-folder-row { .output-folder-row {
margin-top: 14px; margin-top: 14px;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
font-size: 0.85rem; font-size: 0.82rem;
} }
.folder-label { .folder-label {
@@ -233,7 +328,7 @@ input[type="text"]:focus {
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
max-width: 480px; max-width: 420px;
} }
.text-btn { .text-btn {
@@ -250,7 +345,7 @@ input[type="text"]:focus {
text-decoration: underline; text-decoration: underline;
} }
/* Spinner & Icon Animation */ /* Spinner Icon */
.animate-spin { .animate-spin {
animation: spin 1s linear infinite; animation: spin 1s linear infinite;
width: 18px; width: 18px;
@@ -262,146 +357,326 @@ input[type="text"]:focus {
to { transform: rotate(360deg); } to { transform: rotate(360deg); }
} }
/* Preview Card Layout */ /* Step 2 Styles (Wizard Flow) */
.preview-layout { .step-header {
display: flex; display: flex;
gap: 24px;
align-items: center; align-items: center;
justify-content: space-between;
margin-bottom: 16px;
} }
.thumbnail-wrapper { .back-button {
width: 260px; background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 10px;
padding: 8px 16px;
color: #dcd7eb;
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.2s ease;
}
.back-button:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.15);
color: #ffffff;
transform: translateX(-2px);
}
.back-button svg {
width: 14px;
height: 14px;
}
.step-title {
font-size: 1.25rem;
font-weight: 700;
color: #ffffff;
letter-spacing: -0.3px;
}
/* Scrollable Track/File List Container */
.track-list-scroll {
flex: 1;
overflow-y: auto;
padding-right: 6px;
margin-bottom: 10px;
}
/* Custom Scrollbar for Track List */
.track-list-scroll::-webkit-scrollbar {
width: 6px;
}
.track-list-scroll::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.15);
border-radius: 10px;
}
.track-list-scroll::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
}
.track-list-scroll::-webkit-scrollbar-thumb:hover {
background: rgba(255, 0, 128, 0.3);
}
/* List Structure */
.track-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.track-item {
display: flex;
align-items: center;
gap: 18px;
padding: 16px;
border-radius: 16px;
background: rgba(18, 14, 28, 0.3);
}
.track-thumb {
width: 130px;
aspect-ratio: 16/9; aspect-ratio: 16/9;
border-radius: 12px; border-radius: 8px;
overflow: hidden; overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.08);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.4); box-shadow: 0 4px 12px rgba(0,0,0,0.3);
flex-shrink: 0;
} }
.thumbnail-wrapper img { .track-thumb img {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
} }
.video-details { .track-details {
flex: 1; flex: 1;
min-width: 0;
} }
.video-details h3 { .track-details h4 {
font-size: 1.15rem; font-size: 0.98rem;
font-weight: 600; font-weight: 600;
margin-bottom: 6px;
line-height: 1.4;
color: #ffffff; color: #ffffff;
}
.details-text {
font-size: 0.85rem;
color: #9e97b8;
margin-bottom: 4px; margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
/* Format Cards Selection */ .track-uploader {
.format-selection { font-size: 0.8rem;
display: flex;
gap: 12px;
margin: 16px 0 20px 0;
}
.radio-card {
flex: 1;
position: relative;
cursor: pointer;
}
.radio-card input[type="radio"] {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.radio-content {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
transition: all 0.25s;
}
.radio-content svg {
width: 22px;
height: 22px;
color: #9e97b8; color: #9e97b8;
margin-bottom: 2px;
} }
.radio-info { .track-duration {
display: flex; font-size: 0.76rem;
flex-direction: column;
}
.radio-title {
font-size: 0.85rem;
font-weight: 600;
color: #f1f0f5;
}
.radio-desc {
font-size: 0.72rem;
color: #6e6a82; color: #6e6a82;
} }
.radio-card input[type="radio"]:checked + .radio-content { .track-status {
background: rgba(255, 0, 128, 0.08); flex-shrink: 0;
border-color: #ff0080;
box-shadow: 0 0 15px rgba(255, 0, 128, 0.15);
} }
.radio-card input[type="radio"]:checked + .radio-content svg { .status-badge {
font-size: 0.76rem;
font-weight: 600;
padding: 6px 12px;
border-radius: 20px;
letter-spacing: 0.3px;
}
.status-badge.ready {
background: rgba(0, 223, 216, 0.1);
color: #00dfd8;
border: 1px solid rgba(0, 223, 216, 0.2);
}
.status-badge.downloading {
background: rgba(255, 0, 128, 0.1);
color: #ff0080; color: #ff0080;
border: 1px solid rgba(255, 0, 128, 0.2);
animation: pulse-glow 1.5s infinite alternate ease-in-out;
} }
/* Progress Bars */ @keyframes pulse-glow {
.progress-bar-container { 0% { opacity: 0.7; }
width: 100%; 100% { opacity: 1; box-shadow: 0 0 8px rgba(255, 0, 128, 0.3); }
height: 8px;
background: rgba(255, 255, 255, 0.08);
border-radius: 4px;
overflow: hidden;
margin-top: 14px;
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.4);
} }
.progress-bar-fill { /* Fixed Bottom Panel Footer */
height: 100%; .fixed-footer {
background: linear-gradient(90deg, #00dfd8 0%, #ff0080 100%); flex-shrink: 0;
border-radius: 4px; padding: 18px 24px;
width: 0%; border-radius: 18px;
box-shadow: 0 0 10px rgba(0, 223, 216, 0.5); margin-top: auto;
transition: width 0.3s ease-out; border-color: rgba(255, 255, 255, 0.1);
background: rgba(14, 10, 22, 0.6);
box-shadow: 0 -10px 30px rgba(0,0,0,0.3);
} }
/* Loading Overlay */ .footer-grid {
display: grid;
grid-template-columns: 1.2fr 1fr;
gap: 24px;
align-items: center;
}
/* Options (Format & Quality) */
.options-panel {
display: flex;
flex-direction: column;
gap: 12px;
}
.option-row {
display: flex;
align-items: center;
gap: 14px;
}
.option-label {
font-size: 0.85rem;
font-weight: 600;
color: #9e97b8;
width: 55px;
flex-shrink: 0;
}
/* Format Switch Button Group */
.format-toggle {
display: flex;
background: rgba(0,0,0,0.3);
padding: 4px;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.05);
}
.format-btn {
background: none;
border: none;
padding: 8px 16px;
border-radius: 8px;
color: #9e97b8;
font-size: 0.8rem;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.2s ease;
}
.format-btn svg {
width: 14px;
height: 14px;
}
.format-btn.active {
background: rgba(255, 0, 128, 0.12);
color: #ff0080;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05);
}
/* Quality Pills Selector */
.quality-pills {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.quality-pill {
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.08);
color: #dcd7eb;
padding: 6px 12px;
border-radius: 8px;
font-size: 0.78rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.quality-pill:hover {
background: rgba(255, 255, 255, 0.07);
border-color: rgba(255, 255, 255, 0.15);
}
.quality-pill.active {
background: rgba(0, 223, 216, 0.12);
border-color: #00dfd8;
color: #00dfd8;
box-shadow: 0 0 10px rgba(0, 223, 216, 0.1);
}
/* Action Panel (Download progress and Action Button) */
.action-panel {
display: flex;
flex-direction: column;
justify-content: center;
}
/* Progress Panel */
.progress-panel {
background: rgba(0, 0, 0, 0.25);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 12px;
padding: 10px 14px;
margin-bottom: 10px;
}
.progress-header {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.78rem;
font-weight: 600;
margin-bottom: 4px;
}
.progress-header span:first-child {
color: #dcd7eb;
}
.progress-header span:last-child {
color: #00dfd8;
font-weight: 800;
}
.progress-panel .subtext {
font-size: 0.7rem;
color: #6e6a82;
margin-top: 4px;
text-align: left;
}
/* Loader Overlay Panel */
.overlay { .overlay {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background: rgba(6, 4, 10, 0.8); background: rgba(6, 4, 10, 0.85);
backdrop-filter: blur(20px); backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
z-index: 1000; z-index: 10000;
opacity: 0; opacity: 0;
pointer-events: none; pointer-events: none;
transition: opacity 0.4s; transition: opacity 0.4s ease;
} }
.overlay.active { .overlay.active {
@@ -453,59 +728,34 @@ input[type="text"]:focus {
text-align: center; text-align: center;
} }
/* Error Alert */ /* Error Display Card */
.error-card { .error-card {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 12px;
border-color: rgba(255, 77, 77, 0.2); border-color: rgba(255, 77, 77, 0.2);
background: rgba(255, 77, 77, 0.06); background: rgba(255, 77, 77, 0.06);
padding: 16px 20px; padding: 14px 18px;
margin-top: 14px;
border-radius: 12px;
} }
.error-icon { .error-icon {
width: 22px; width: 20px;
height: 22px; height: 20px;
color: #ff4d4d; color: #ff4d4d;
} }
#error-message { #error-message {
font-size: 0.88rem; font-size: 0.85rem;
color: #ffb3b3; color: #ffb3b3;
font-weight: 500; font-weight: 500;
} }
/* Download Card Details */
.download-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.download-header h4 {
font-size: 0.95rem;
font-weight: 600;
}
.download-percent {
font-size: 1.05rem;
font-weight: 800;
color: #00dfd8;
}
.download-card .subtext {
margin-top: 8px;
font-size: 0.8rem;
color: #9e97b8;
}
/* Responsive Customization */ /* Responsive Customization */
@media (max-width: 640px) { @media (max-width: 640px) {
.preview-layout { .footer-grid {
flex-direction: column; grid-template-columns: 1fr;
align-items: flex-start; gap: 14px;
}
.thumbnail-wrapper {
width: 100%;
} }
} }
+6 -4
View File
@@ -206,27 +206,29 @@ function getVideoInfo(url) {
} }
// Download video or convert to MP3 // Download video or convert to MP3
function downloadMedia(url, format, outputDir, onProgress) { function downloadMedia(url, format, quality, outputDir, onProgress) {
console.log(`[DEBUG] Media download requested. Format: ${format}, Target Dir: ${outputDir}`); console.log(`[DEBUG] Media download requested. Format: ${format}, Quality: ${quality}, Target Dir: ${outputDir}`);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let args = []; let args = [];
const outputTemplate = path.join(outputDir, '%(title)s.%(ext)s'); const outputTemplate = path.join(outputDir, '%(title)s.%(ext)s');
if (format === 'mp3') { if (format === 'mp3') {
const q = quality ? `${quality}K` : '320K'; // '320K', '256K', '192K'
args = [ args = [
'--no-playlist', '--no-playlist',
'--ffmpeg-location', FFMPEG_PATH, '--ffmpeg-location', FFMPEG_PATH,
'-x', '-x',
'--audio-format', 'mp3', '--audio-format', 'mp3',
'--audio-quality', '320K', '--audio-quality', q,
'-o', outputTemplate, '-o', outputTemplate,
url url
]; ];
} else { } else {
const res = quality || '1080'; // '1080', '720', '480'
args = [ args = [
'--no-playlist', '--no-playlist',
'--ffmpeg-location', FFMPEG_PATH, '--ffmpeg-location', FFMPEG_PATH,
'-f', 'bv*[ext=mp4]+ba[ext=m4a]/b[ext=mp4] / bv*+ba/b', '-f', `bv*[height<=${res}][ext=mp4]+ba[ext=m4a]/b[height<=${res}][ext=mp4] / bv*[height<=${res}]+ba/b[height<=${res}]`,
'-o', outputTemplate, '-o', outputTemplate,
url url
]; ];