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,
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 });
}
+4 -2
View File
@@ -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')
});
+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">
</head>
<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 -->
<div class="bg-glow bg-glow-1"></div>
<div class="bg-glow bg-glow-2"></div>
@@ -32,6 +52,9 @@
<!-- Main Container -->
<main class="app-container">
<!-- STEP 1: Link Analysis -->
<div id="step-1" class="wizard-step active">
<header class="app-header text-center">
<div class="logo-wrapper">
<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>
</header>
<!-- URL Input Section -->
<section class="glass-card url-section">
<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">
@@ -63,7 +85,7 @@
</div>
</section>
<!-- Error Display -->
<!-- Error Display in Step 1 -->
<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">
<circle cx="12" cy="12" r="10" />
@@ -72,73 +94,78 @@
</svg>
<span id="error-message">Bir hata oluştu.</span>
</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 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">
<label class="radio-card active">
<input type="radio" name="format-type" value="mp3" checked>
<div class="radio-content">
<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" />
<!-- STEP 2: Download Options & List -->
<div id="step-2" class="wizard-step">
<header class="step-header">
<button id="back-btn" class="back-button">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<line x1="19" y1="12" x2="5" y2="12"></line>
<polyline points="12 19 5 12 12 5"></polyline>
</svg>
<div class="radio-info">
<span class="radio-title">MP3 Ses</span>
<span class="radio-desc">En yüksek kalite (320kbps)</span>
</div>
</div>
</label>
<span>Geri Dön</span>
</button>
<h2 class="step-title">İndirilecek Dosya</h2>
</header>
<label class="radio-card">
<input type="radio" name="format-type" value="mp4">
<div class="radio-content">
<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" />
<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>
<!-- Scrollable Track List Area -->
<div class="track-list-scroll">
<div id="track-list" class="track-list">
<!-- Dynamically populated by Javascript -->
</div>
</div>
</label>
</div>
<button id="download-btn" class="glow-button full-width">
İndirmeyi Başlat
<!-- Fixed Bottom Panel for Formats and Download Action -->
<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>
</div>
</div>
</section>
<!-- Download Progress Section -->
<section id="download-section" class="glass-card download-card" style="display: none;">
<div class="download-header">
<h4 id="download-status">İndiriliyor...</h4>
<span id="download-percent" class="download-percent">0%</span>
<div class="option-row">
<span class="option-label">Kalite:</span>
<div id="quality-pills" class="quality-pills">
<!-- Populated dynamically: 320kbps, 256kbps, 192kbps for MP3 or 1080p, 720p, 480p for MP4 -->
</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 class="progress-bar-container">
<div id="download-progress-bar" class="progress-bar-fill" style="width: 0%"></div>
</div>
<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>
<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 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 = `
<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 () => {
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'
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!`;
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.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' });
}
+406 -156
View File
@@ -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;
}
}
+6 -4
View File
@@ -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
];