feat: implement YouTube downloader UI and integration logic with renderer, CSS, and main process support
This commit is contained in:
@@ -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
@@ -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')
|
||||||
});
|
});
|
||||||
|
|||||||
+126
-99
@@ -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,113 +52,120 @@
|
|||||||
|
|
||||||
<!-- Main Container -->
|
<!-- Main Container -->
|
||||||
<main class="app-container">
|
<main class="app-container">
|
||||||
<header class="app-header text-center">
|
|
||||||
<div class="logo-wrapper">
|
<!-- STEP 1: Link Analysis -->
|
||||||
<svg class="logo-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<div id="step-1" class="wizard-step active">
|
||||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
<header class="app-header text-center">
|
||||||
<polyline points="7 10 12 15 17 10" />
|
<div class="logo-wrapper">
|
||||||
<line x1="12" y1="15" x2="12" y2="3" />
|
<svg class="logo-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
</svg>
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||||||
</div>
|
<polyline points="7 10 12 15 17 10" />
|
||||||
<h1 class="glow-text">VibeDownloader</h1>
|
<line x1="12" y1="15" x2="12" y2="3" />
|
||||||
<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">
|
|
||||||
<button id="analyze-btn" class="glow-button">
|
|
||||||
<span>Analiz Et</span>
|
|
||||||
<svg class="btn-icon animate-spin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display: none;">
|
|
||||||
<circle cx="12" cy="12" r="10" />
|
|
||||||
<path d="M2 12a10 10 0 0 1 8-9.8" />
|
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="output-folder-row">
|
|
||||||
<span class="folder-label">Kaydetme Yeri:</span>
|
|
||||||
<span id="output-path" class="folder-path">Yükleniyor...</span>
|
|
||||||
<button id="change-folder-btn" class="text-btn">Değiştir</button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Error Display -->
|
|
||||||
<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" />
|
|
||||||
<line x1="12" y1="8" x2="12" y2="12" />
|
|
||||||
<line x1="12" y1="16" x2="12.01" y2="16" />
|
|
||||||
</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>
|
||||||
<div class="video-details">
|
<h1 class="glow-text">VibeDownloader</h1>
|
||||||
<h3 id="video-title">Video Başlığı</h3>
|
<p class="subtitle">YouTube videolarını saniyeler içinde yüksek kaliteli MP4 veya MP3 olarak indirin</p>
|
||||||
<p id="video-uploader" class="details-text">Kanal Adı</p>
|
</header>
|
||||||
<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" />
|
|
||||||
</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>
|
|
||||||
|
|
||||||
<label class="radio-card">
|
<section class="glass-card url-section">
|
||||||
<input type="radio" name="format-type" value="mp4">
|
<div class="input-group">
|
||||||
<div class="radio-content">
|
<input type="text" id="url-input" placeholder="YouTube video veya oynatma listesi bağlantısı yapıştırın..." autocomplete="off">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<button id="analyze-btn" class="glow-button">
|
||||||
<rect x="2" y="2" width="20" height="20" rx="2.18" ry="2.18" />
|
<span>Analiz Et</span>
|
||||||
<line x1="7" y1="2" x2="7" y2="22" />
|
<svg class="btn-icon animate-spin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display: none;">
|
||||||
<line x1="17" y1="2" x2="17" y2="22" />
|
<circle cx="12" cy="12" r="10" />
|
||||||
<line x1="2" y1="12" x2="22" y2="12" />
|
<path d="M2 12a10 10 0 0 1 8-9.8" />
|
||||||
<line x1="2" y1="7" x2="7" y2="7" />
|
</svg>
|
||||||
<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>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button id="download-btn" class="glow-button full-width">
|
|
||||||
İndirmeyi Başlat
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="output-folder-row">
|
||||||
</section>
|
<span class="folder-label">Kaydetme Yeri:</span>
|
||||||
|
<span id="output-path" class="folder-path">Yükleniyor...</span>
|
||||||
|
<button id="change-folder-btn" class="text-btn">Değiştir</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Download Progress Section -->
|
<!-- Error Display in Step 1 -->
|
||||||
<section id="download-section" class="glass-card download-card" style="display: none;">
|
<div id="error-card" class="glass-card error-card" style="display: none;">
|
||||||
<div class="download-header">
|
<svg class="error-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<h4 id="download-status">İndiriliyor...</h4>
|
<circle cx="12" cy="12" r="10" />
|
||||||
<span id="download-percent" class="download-percent">0%</span>
|
<line x1="12" y1="8" x2="12" y2="12" />
|
||||||
|
<line x1="12" y1="16" x2="12.01" y2="16" />
|
||||||
|
</svg>
|
||||||
|
<span id="error-message">Bir hata oluştu.</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="progress-bar-container">
|
</div>
|
||||||
<div id="download-progress-bar" class="progress-bar-fill" style="width: 0%"></div>
|
|
||||||
|
<!-- 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>
|
||||||
|
<span>Geri Dön</span>
|
||||||
|
</button>
|
||||||
|
<h2 class="step-title">İndirilecek Dosya</h2>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Scrollable Track List Area -->
|
||||||
|
<div class="track-list-scroll">
|
||||||
|
<div id="track-list" class="track-list">
|
||||||
|
<!-- Dynamically populated by Javascript -->
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p id="download-subtext" class="subtext">Lütfen bekleyin...</p>
|
|
||||||
</section>
|
<!-- 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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</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>
|
||||||
|
|||||||
+164
-52
@@ -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';
|
// Toggle button loading states
|
||||||
|
|
||||||
// Set Loading Button state
|
|
||||||
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
@@ -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
@@ -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
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user