diff --git a/main.js b/main.js index 5269344..68df403 100644 --- a/main.js +++ b/main.js @@ -12,8 +12,7 @@ function createWindow() { height: 700, minWidth: 800, minHeight: 600, - frame: true, // We can use frame: true, but let's make it styled inside the HTML with gorgeous glassmorphism! - title: 'VibeDownloader - Premium YouTube & MP3 Downloader', + frame: false, // Make window frameless webPreferences: { preload: path.join(__dirname, 'preload.js'), contextIsolation: true, @@ -22,6 +21,9 @@ function createWindow() { show: false }); + const { Menu } = require('electron'); + Menu.setApplicationMenu(null); // Hide the default menu bar + mainWindow.loadFile(path.join(__dirname, 'src', 'index.html')); // 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(() => { createWindow(); @@ -69,9 +80,9 @@ ipcMain.handle('get-video-info', async (event, url) => { }); // IPC Handler: Download Media -ipcMain.handle('download-media', async (event, { url, format }) => { +ipcMain.handle('download-media', async (event, { url, format, quality }) => { try { - await downloader.downloadMedia(url, format, selectedOutputDir, (percent, status) => { + await downloader.downloadMedia(url, format, quality, selectedOutputDir, (percent, status) => { if (mainWindow) { mainWindow.webContents.send('download-progress', { percent, status }); } diff --git a/preload.js b/preload.js index efe87c6..737c22c 100644 --- a/preload.js +++ b/preload.js @@ -4,8 +4,10 @@ contextBridge.exposeInMainWorld('api', { checkDependencies: () => ipcRenderer.invoke('check-dependencies'), onDependencyStatus: (callback) => ipcRenderer.on('dependency-status', (_, data) => callback(data)), 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)), 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') }); diff --git a/src/index.html b/src/index.html index 2188705..d0fa45e 100644 --- a/src/index.html +++ b/src/index.html @@ -10,6 +10,26 @@ + +
+
+ + + + + + VibeDownloader +
+
+ + +
+
+
@@ -32,113 +52,120 @@
-
-
- - - - - -
-

VibeDownloader

-

YouTube videolarını saniyeler içinde yüksek kaliteli MP4 veya MP3 olarak indirin

-
- - -
-
- - -
-
- Kaydetme Yeri: - Yükleniyor... - -
-
- - - - - - +
+ Kaydetme Yeri: + Yükleniyor... + +
+ - - + + + + +
diff --git a/src/renderer.js b/src/renderer.js index 1b8b7bd..6f09434 100644 --- a/src/renderer.js +++ b/src/renderer.js @@ -13,29 +13,57 @@ const changeFolderBtn = document.getElementById('change-folder-btn'); const errorCard = document.getElementById('error-card'); const errorMessage = document.getElementById('error-message'); -const previewSection = document.getElementById('preview-section'); -const videoThumbnail = document.getElementById('video-thumbnail'); -const videoTitle = document.getElementById('video-title'); -const videoUploader = document.getElementById('video-uploader'); -const videoDuration = document.getElementById('video-duration'); -const radioCards = document.querySelectorAll('.radio-card'); +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 downloadSection = document.getElementById('download-section'); +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'); +// State Variables 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 () => { - // 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(); outputPath.textContent = defaultPath; - // Listen to dependency download updates + // Listen to dependency checking progress window.api.onDependencyStatus(({ message, progress }) => { depMsg.textContent = message; depProgress.style.width = `${progress}%`; @@ -45,7 +73,7 @@ window.addEventListener('DOMContentLoaded', async () => { try { const result = await window.api.checkDependencies(); if (result.success) { - // Fade out overlay smoothly + // Transition out the loading overlay dependencyOverlay.style.opacity = '0'; setTimeout(() => { dependencyOverlay.classList.remove('active'); @@ -59,36 +87,79 @@ window.addEventListener('DOMContentLoaded', async () => { depTitle.textContent = 'Hata Oluştu'; depMsg.textContent = err.message; } + + // Render initial quality pills + renderQualityPills(); }); -// 2. Change Output Folder +// 2. Output Path Folder Changer changeFolderBtn.addEventListener('click', async () => { const newPath = await window.api.selectDirectory(); outputPath.textContent = newPath; }); -// 3. Radio Card Active Selection UI -radioCards.forEach(card => { - const radio = card.querySelector('input[type="radio"]'); - radio.addEventListener('change', () => { - radioCards.forEach(c => c.classList.remove('active')); - if (radio.checked) { - card.classList.add('active'); - } - }); +// 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(); }); -// 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 () => { const url = urlInput.value.trim(); if (!url) return; - // Reset state + // Reset error displays errorCard.style.display = 'none'; - previewSection.style.display = 'none'; - downloadSection.style.display = 'none'; - - // Set Loading Button state + + // Toggle button loading states analyzeBtn.disabled = true; const analyzeSpan = analyzeBtn.querySelector('span'); const analyzeIcon = analyzeBtn.querySelector('.btn-icon'); @@ -99,11 +170,14 @@ analyzeBtn.addEventListener('click', async () => { const result = await window.api.getVideoInfo(url); if (result.success) { analyzedUrl = url; - videoThumbnail.src = result.info.thumbnail; - videoTitle.textContent = result.info.title; - videoUploader.textContent = result.info.uploader; - videoDuration.textContent = `Süre: ${result.info.duration}`; - previewSection.style.display = 'block'; + currentVideoData = result.info; + + // 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); } @@ -116,63 +190,101 @@ analyzeBtn.addEventListener('click', async () => { } }); -// 5. Download Process +// 7. Render Track List Function +function renderTrackList(info) { + trackList.innerHTML = ` +
+
+ Thumbnail +
+
+

${info.title}

+

${info.uploader}

+ Süre: ${info.duration} +
+
+ Hazır +
+
+ `; +} + +// 8. Media Download Executor downloadBtn.addEventListener('click', async () => { if (!analyzedUrl) return; - const selectedFormat = document.querySelector('input[name="format-type"]:checked').value; - - // Setup download section UI - downloadSection.style.display = 'block'; + // Setup download progress panel UI states + downloadProgressPanel.style.display = 'block'; downloadBtn.disabled = true; + backBtn.disabled = true; downloadStatus.textContent = 'Bağlantı kuruluyor...'; downloadPercent.textContent = '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 - downloadSection.scrollIntoView({ behavior: 'smooth' }); + // Set track status badge as downloading + 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 }) => { downloadStatus.textContent = status; downloadPercent.textContent = `${percent}%`; downloadProgressBar.style.width = `${percent}%`; - + if (percent > 90) { - downloadSubtext.textContent = selectedFormat === 'mp3' - ? 'Dönüştürme işlemi yapılıyor (Yüksek Kaliteli MP3)...' + downloadSubtext.textContent = currentFormat === 'mp3' + ? 'Dönüştürme işlemi yapılıyor (Yüksek Kaliteli MP3)...' : 'Video ve ses dosyaları birleştiriliyor...'; } 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 { - const result = await window.api.downloadMedia(analyzedUrl, selectedFormat); + const result = await window.api.downloadMedia(analyzedUrl, currentFormat, currentQuality); if (result.success) { downloadStatus.textContent = 'Tamamlandı!'; downloadPercent.textContent = '100%'; downloadProgressBar.style.width = '100%'; - downloadSubtext.textContent = `Dosyanız başarıyla kaydedildi!`; - - // Beautiful visual indicator of success + downloadSubtext.textContent = 'Dosyanız başarıyla kaydedildi!'; + + 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.boxShadow = '0 0 15px rgba(0, 255, 135, 0.6)'; } else { 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) { showError(err.message); - downloadSection.style.display = 'none'; + downloadProgressPanel.style.display = 'none'; } finally { downloadBtn.disabled = false; + backBtn.disabled = false; } }); +// Helper: Show Error Alert inside Step 1 function showError(msg) { errorMessage.textContent = `Hata: ${msg}`; errorCard.style.display = 'flex'; - errorCard.scrollIntoView({ behavior: 'smooth' }); } diff --git a/src/style.css b/src/style.css index 4a66a7e..be04ad7 100644 --- a/src/style.css +++ b/src/style.css @@ -10,14 +10,84 @@ body { background: #09060f; color: #f1f0f5; - min-height: 100vh; + height: 100vh; display: flex; - justify-content: center; - align-items: center; - overflow-x: hidden; + flex-direction: column; + overflow: hidden; /* NO SCROLL FOR THE MAIN WINDOW */ 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 */ .bg-glow { position: absolute; @@ -58,15 +128,9 @@ body { } @keyframes float-glow { - 0% { - transform: translate(0, 0) scale(1); - } - 50% { - transform: translate(80px, 50px) scale(1.1); - } - 100% { - transform: translate(-40px, -60px) scale(0.9); - } + 0% { transform: translate(0, 0) scale(1); } + 50% { transform: translate(80px, 50px) scale(1.1); } + 100% { transform: translate(-40px, -60px) scale(0.9); } } /* Premium Glassmorphic Cards */ @@ -79,7 +143,6 @@ body { box-shadow: 0 15px 35px rgba(0, 0, 0, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.1); padding: 24px; - margin-bottom: 20px; position: relative; z-index: 10; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); @@ -93,23 +156,50 @@ body { /* Main Container Layout */ .app-container { + flex: 1; width: 100%; - max-width: 800px; + max-width: 860px; + margin: 38px auto 0 auto; /* Under custom titlebar */ padding: 20px; display: flex; flex-direction: column; 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 { - from { opacity: 0; transform: translateY(20px); } + from { opacity: 0; transform: translateY(12px); } 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 { - margin-bottom: 30px; + margin-bottom: 24px; } .logo-wrapper { @@ -129,7 +219,7 @@ body { } .glow-text { - font-size: 2.5rem; + font-size: 2.3rem; font-weight: 800; letter-spacing: -0.5px; background: linear-gradient(to right, #ffffff, #dcd7eb, #ff0080); @@ -141,7 +231,7 @@ body { .subtitle { color: #b3aeca; - font-size: 1rem; + font-size: 0.95rem; font-weight: 400; } @@ -174,7 +264,7 @@ input[type="text"]:focus { background: rgba(0, 0, 0, 0.4); } -/* Glowing Dynamic Buttons */ +/* Glowing Buttons */ .glow-button { background: linear-gradient(135deg, #7928ca 0%, #ff0080 100%); color: #ffffff; @@ -207,20 +297,25 @@ input[type="text"]:focus { } .glow-button:disabled { - background: rgba(255, 255, 255, 0.1); - color: #6e6a82; - box-shadow: none; + background: rgba(255, 255, 255, 0.1) !important; + color: #6e6a82 !important; + box-shadow: none !important; cursor: not-allowed; - transform: none; + transform: none !important; } -/* Path Row */ +.glow-button svg { + width: 18px; + height: 18px; +} + +/* Output Folder Info Bar */ .output-folder-row { margin-top: 14px; display: flex; align-items: center; gap: 8px; - font-size: 0.85rem; + font-size: 0.82rem; } .folder-label { @@ -233,7 +328,7 @@ input[type="text"]:focus { text-overflow: ellipsis; overflow: hidden; white-space: nowrap; - max-width: 480px; + max-width: 420px; } .text-btn { @@ -250,7 +345,7 @@ input[type="text"]:focus { text-decoration: underline; } -/* Spinner & Icon Animation */ +/* Spinner Icon */ .animate-spin { animation: spin 1s linear infinite; width: 18px; @@ -262,146 +357,326 @@ input[type="text"]:focus { to { transform: rotate(360deg); } } -/* Preview Card Layout */ -.preview-layout { +/* Step 2 Styles (Wizard Flow) */ +.step-header { display: flex; - gap: 24px; align-items: center; + justify-content: space-between; + margin-bottom: 16px; } -.thumbnail-wrapper { - width: 260px; +.back-button { + 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; - border-radius: 12px; + border-radius: 8px; overflow: hidden; - border: 1px solid rgba(255, 255, 255, 0.1); - box-shadow: 0 8px 20px rgba(0, 0, 0, 0.4); + border: 1px solid rgba(255, 255, 255, 0.08); + box-shadow: 0 4px 12px rgba(0,0,0,0.3); + flex-shrink: 0; } -.thumbnail-wrapper img { +.track-thumb img { width: 100%; height: 100%; object-fit: cover; } -.video-details { +.track-details { flex: 1; + min-width: 0; } -.video-details h3 { - font-size: 1.15rem; +.track-details h4 { + font-size: 0.98rem; font-weight: 600; - margin-bottom: 6px; - line-height: 1.4; color: #ffffff; -} - -.details-text { - font-size: 0.85rem; - color: #9e97b8; margin-bottom: 4px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } -/* Format Cards Selection */ -.format-selection { - 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; +.track-uploader { + font-size: 0.8rem; color: #9e97b8; + margin-bottom: 2px; } -.radio-info { - display: flex; - flex-direction: column; -} - -.radio-title { - font-size: 0.85rem; - font-weight: 600; - color: #f1f0f5; -} - -.radio-desc { - font-size: 0.72rem; +.track-duration { + font-size: 0.76rem; color: #6e6a82; } -.radio-card input[type="radio"]:checked + .radio-content { - background: rgba(255, 0, 128, 0.08); - border-color: #ff0080; - box-shadow: 0 0 15px rgba(255, 0, 128, 0.15); +.track-status { + flex-shrink: 0; } -.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; + border: 1px solid rgba(255, 0, 128, 0.2); + animation: pulse-glow 1.5s infinite alternate ease-in-out; } -/* Progress Bars */ -.progress-bar-container { - width: 100%; - 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); +@keyframes pulse-glow { + 0% { opacity: 0.7; } + 100% { opacity: 1; box-shadow: 0 0 8px rgba(255, 0, 128, 0.3); } } -.progress-bar-fill { - height: 100%; - background: linear-gradient(90deg, #00dfd8 0%, #ff0080 100%); - border-radius: 4px; - width: 0%; - box-shadow: 0 0 10px rgba(0, 223, 216, 0.5); - transition: width 0.3s ease-out; +/* Fixed Bottom Panel Footer */ +.fixed-footer { + flex-shrink: 0; + padding: 18px 24px; + border-radius: 18px; + margin-top: auto; + 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 { position: fixed; top: 0; left: 0; width: 100%; height: 100%; - background: rgba(6, 4, 10, 0.8); + background: rgba(6, 4, 10, 0.85); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); display: flex; justify-content: center; align-items: center; - z-index: 1000; + z-index: 10000; opacity: 0; pointer-events: none; - transition: opacity 0.4s; + transition: opacity 0.4s ease; } .overlay.active { @@ -453,59 +728,34 @@ input[type="text"]:focus { text-align: center; } -/* Error Alert */ +/* Error Display Card */ .error-card { display: flex; align-items: center; gap: 12px; border-color: rgba(255, 77, 77, 0.2); background: rgba(255, 77, 77, 0.06); - padding: 16px 20px; + padding: 14px 18px; + margin-top: 14px; + border-radius: 12px; } .error-icon { - width: 22px; - height: 22px; + width: 20px; + height: 20px; color: #ff4d4d; } #error-message { - font-size: 0.88rem; + font-size: 0.85rem; color: #ffb3b3; 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 */ @media (max-width: 640px) { - .preview-layout { - flex-direction: column; - align-items: flex-start; - } - .thumbnail-wrapper { - width: 100%; + .footer-grid { + grid-template-columns: 1fr; + gap: 14px; } } diff --git a/utils/downloader.js b/utils/downloader.js index b299590..694a68f 100644 --- a/utils/downloader.js +++ b/utils/downloader.js @@ -206,27 +206,29 @@ function getVideoInfo(url) { } // Download video or convert to MP3 -function downloadMedia(url, format, outputDir, onProgress) { - console.log(`[DEBUG] Media download requested. Format: ${format}, Target Dir: ${outputDir}`); +function downloadMedia(url, format, quality, outputDir, onProgress) { + console.log(`[DEBUG] Media download requested. Format: ${format}, Quality: ${quality}, Target Dir: ${outputDir}`); return new Promise((resolve, reject) => { let args = []; const outputTemplate = path.join(outputDir, '%(title)s.%(ext)s'); if (format === 'mp3') { + const q = quality ? `${quality}K` : '320K'; // '320K', '256K', '192K' args = [ '--no-playlist', '--ffmpeg-location', FFMPEG_PATH, '-x', '--audio-format', 'mp3', - '--audio-quality', '320K', + '--audio-quality', q, '-o', outputTemplate, url ]; } else { + const res = quality || '1080'; // '1080', '720', '480' args = [ '--no-playlist', '--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, url ];