fix: no more full cli design :D

This commit is contained in:
2025-12-23 20:11:51 +01:00
parent cab061cec0
commit 6f4d943e3d
3 changed files with 569 additions and 661 deletions
+169 -304
View File
@@ -1,431 +1,296 @@
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const dom = { const dom = {
// Form & Main
form: document.getElementById('upload-form'), form: document.getElementById('upload-form'),
urlInput: document.getElementById('url'), urlInput: document.getElementById('url'),
platformInput: document.getElementById('input-platform'),
submitBtn: document.getElementById('submit-button'), submitBtn: document.getElementById('submit-button'),
platformInput: document.getElementById('input-platform'),
badgeContainer: document.getElementById('platform-badge-container'), // Detection UI
detectedIcon: document.getElementById('detected-icon'), detectionArea: document.getElementById('detection-area'),
detectedText: document.getElementById('detected-text'), detectedText: document.getElementById('detected-text'),
detectedIcon: document.getElementById('detected-icon'),
urlIcon: document.getElementById('url-icon'), urlIcon: document.getElementById('url-icon'),
// Options
optionsContainer: document.getElementById('options-container'), optionsContainer: document.getElementById('options-container'),
ytOptions: document.getElementById('youtube-options'), ytOptions: document.getElementById('youtube-options'),
codecSection: document.getElementById('codec-options-section'), codecSection: document.getElementById('codec-options-section'),
ytRadios: document.querySelectorAll('input[name="yt_format"]'),
ytFormatRadios: document.querySelectorAll('input[name="yt_format"]'),
qualityWrapper: document.getElementById('quality-wrapper'),
mp3Select: document.getElementById('mp3_bitrate'), mp3Select: document.getElementById('mp3_bitrate'),
mp4Select: document.getElementById('mp4_quality'), mp4Select: document.getElementById('mp4_quality'),
codecSwitch: document.getElementById('codec-switch'), codecSwitch: document.getElementById('codec-switch'),
codecPreference: document.getElementById('codec_preference'), codecPref: document.getElementById('codec_preference'),
progressContainer: document.getElementById('progress-container'), // Views
processView: document.getElementById('process-view'),
resultView: document.getElementById('result-view'),
formContainer: document.querySelector('.form-container'),
// Progress & Logs
statusMsg: document.getElementById('status-message'),
progressBar: document.getElementById('progress-bar'), progressBar: document.getElementById('progress-bar'),
statusMessage: document.getElementById('status-message'), logContent: document.getElementById('pseudo-log-content'),
logContent: document.getElementById('log-content'), // Für den letzten Log-Eintrag
resultContainer: document.getElementById('result-container'), // Result
resultUrl: document.getElementById('result-url'), resultUrl: document.getElementById('result-url'),
errorMessage: document.getElementById('error-message'), errorMsg: document.getElementById('error-message'),
// History Elements // History
historyTableBody: document.querySelector('#history-table tbody'), historyTableBody: document.querySelector('#history-table tbody'),
clearHistoryBtn: document.getElementById('clear-history-button'), clearHistoryBtn: document.getElementById('clear-history-button')
contextMenu: document.getElementById('context-menu'),
// Pseudo Log Elements
pseudoLogArea: document.getElementById('pseudo-log-area'),
pseudoLogContent: document.getElementById('pseudo-log-content'),
terminalLines: document.querySelectorAll('.terminal-line')
}; };
let currentJobId = null; let currentJobId = null;
let pollingInterval = null; let pollingInterval = null;
let pseudoLogInterval = null; let pseudoLogInterval = null;
let pseudoLogLines = [];
const LOG_ENTRY_DELAY = 80; // Millisekunden zwischen Buchstaben
const PSEUDO_LOG_MAX_LINES = 10;
const HISTORY_EXPIRY_DAYS = 7;
const platforms = [ const platforms = [
{ name: 'SoundCloud', pattern: /soundcloud\.com/, icon: 'fa-soundcloud', color: '#ff5500' }, { name: 'SoundCloud', pattern: /soundcloud\.com/, icon: 'fa-soundcloud', color: '#ff5500' },
{ name: 'YouTube', pattern: /(youtube\.com|youtu\.be)/, icon: 'fa-youtube', color: '#ff0000' }, { name: 'YouTube', pattern: /(youtube\.com|youtu\.be)/, icon: 'fa-youtube', color: '#ff0000' },
{ name: 'TikTok', pattern: /tiktok\.com/, icon: 'fa-tiktok', color: '#000000' }, { name: 'TikTok', pattern: /tiktok\.com/, icon: 'fa-tiktok', color: '#fe2c55' },
{ name: 'Instagram', pattern: /instagram\.com/, icon: 'fa-instagram', color: '#E1306C' }, { name: 'Instagram', pattern: /instagram\.com/, icon: 'fa-instagram', color: '#E1306C' },
{ name: 'Twitter', pattern: /(twitter\.com|x\.com)/, icon: 'fa-x-twitter', color: '#000000' } { name: 'Twitter', pattern: /(twitter\.com|x\.com)/, icon: 'fa-x-twitter', color: '#fff' }
]; ];
const pseudoLogCommands = [ const pseudoLogs = [
"Initializing environment...", "Connecting to media node...",
"Connecting to media servers...", "Handshaking with API...",
"Authenticating with unknownMedien.dl API...", "Resolving stream URL...",
"Scanning URL for media info...", "Allocating buffer...",
"Detecting stream type...", "Starting download stream...",
"Negotiating download protocol...", "Processing data chunks...",
"Allocating buffer space...", "Verifying integrity...",
"Pre-compiling conversion modules...", "Optimizing container...",
"Establishing S3 connection...", "Finalizing upload..."
"Generating secure download token...",
"Preparing download stream...",
"Initiating download sequence...",
"Syncing metadata...",
"Finalizing file handles...",
]; ];
// --- Initialisierung ---
function init() { function init() {
setupEventListeners(); setupEvents();
loadClientHistory(); loadHistory();
startPseudoLogging();
} }
// --- Event Listeners --- function setupEvents() {
function setupEventListeners() {
dom.urlInput.addEventListener('input', handleUrlInput); dom.urlInput.addEventListener('input', handleUrlInput);
dom.form.addEventListener('submit', handleFormSubmit); dom.form.addEventListener('submit', handleSubmit);
dom.ytFormatRadios.forEach(radio => radio.addEventListener('change', updateYtQualityVisibility));
dom.ytRadios.forEach(r => r.addEventListener('change', updateYtOptions));
if(dom.codecSwitch) { if(dom.codecSwitch) {
dom.codecSwitch.addEventListener('change', (e) => { dom.codecSwitch.addEventListener('change', e => {
dom.codecPreference.value = e.target.checked ? 'h264' : 'original'; dom.codecPref.value = e.target.checked ? 'h264' : 'original';
}); });
} }
if(dom.clearHistoryBtn) dom.clearHistoryBtn.addEventListener('click', clearClientHistory);
document.addEventListener('click', hideContextMenu); if(dom.clearHistoryBtn) dom.clearHistoryBtn.addEventListener('click', clearHistory);
if(dom.historyTableBody) {
dom.historyTableBody.addEventListener('contextmenu', handleHistoryRightClick);
dom.contextMenu.addEventListener('click', handleContextMenuClick);
}
} }
// --- URL Erkennung & Optionen --- // --- Detection ---
function handleUrlInput() { function handleUrlInput() {
const url = dom.urlInput.value.trim(); const url = dom.urlInput.value.trim();
const detected = platforms.find(p => p.pattern.test(url)); const detected = platforms.find(p => p.pattern.test(url));
if (detected) { if (detected) {
dom.platformInput.value = detected.name; dom.platformInput.value = detected.name;
dom.detectedText.textContent = `${detected.name} DETECTED`; dom.detectedText.textContent = detected.name + " erkannt";
dom.detectedIcon.className = `fas ${detected.icon}`; dom.detectedIcon.className = `fab ${detected.icon}`;
dom.detectedIcon.style.color = detected.color; dom.detectionArea.style.opacity = '1';
dom.badgeContainer.style.opacity = '1'; dom.detectionArea.style.transform = 'translateY(0)';
dom.urlIcon.className = `fas ${detected.icon}`;
// Icon in Input
dom.urlIcon.className = `fab ${detected.icon}`;
dom.urlIcon.style.color = detected.color; dom.urlIcon.style.color = detected.color;
showOptionsForPlatform(detected.name);
showOptions(detected.name);
} else { } else {
dom.badgeContainer.style.opacity = '0'; if(url.length === 0) {
dom.urlIcon.className = 'fas fa-keyboard'; // Default CLI icon dom.detectionArea.style.opacity = '0';
dom.urlIcon.style.color = ''; dom.detectionArea.style.transform = 'translateY(-10px)';
hideAllOptions(); dom.urlIcon.className = 'fas fa-link';
dom.urlIcon.style.color = '';
}
// Keep options hidden if no match
} }
} }
function showOptionsForPlatform(platformName) { function showOptions(platform) {
dom.optionsContainer.classList.add('show');
dom.ytOptions.classList.add('d-none'); dom.ytOptions.classList.add('d-none');
dom.codecSection.classList.add('d-none'); dom.codecSection.classList.add('d-none');
if (platformName === 'YouTube') { if (platform === 'YouTube') {
dom.ytOptions.classList.remove('d-none'); dom.ytOptions.classList.remove('d-none');
updateYtQualityVisibility(); // Initial call updateYtOptions();
} else if (['TikTok', 'Instagram', 'Twitter'].includes(platformName)) { } else if (['TikTok', 'Instagram', 'Twitter'].includes(platform)) {
dom.codecSection.classList.remove('d-none'); dom.codecSection.classList.remove('d-none');
} else {
dom.optionsContainer.classList.remove('show');
} }
} }
function hideAllOptions() { function updateYtOptions() {
dom.optionsContainer.classList.remove('show'); const format = document.querySelector('input[name="yt_format"]:checked').value;
setTimeout(() => {
dom.ytOptions.classList.add('d-none');
dom.codecSection.classList.add('d-none');
}, 500);
}
function updateYtQualityVisibility() {
const format = document.querySelector('input[name="yt_format"]:checked')?.value;
if (!format) return;
if (format === 'mp3') { if (format === 'mp3') {
dom.mp3Select.classList.remove('d-none'); dom.mp3Select.classList.remove('d-none');
dom.mp4Select.classList.add('d-none'); dom.mp4Select.classList.add('d-none');
dom.codecSection.classList.add('d-none'); } else {
} else { // mp4
dom.mp3Select.classList.add('d-none'); dom.mp3Select.classList.add('d-none');
dom.mp4Select.classList.remove('d-none'); dom.mp4Select.classList.remove('d-none');
dom.codecSection.classList.remove('d-none');
} }
} }
// --- Pseudo Logging Animation --- // --- Processing ---
function startPseudoLogging() { async function handleSubmit(e) {
let currentLine = 0;
let charIndex = 0;
let commandIndex = 0;
function displayNextLogLine() {
if (pseudoLogInterval) clearInterval(pseudoLogInterval);
if (commandIndex >= pseudoLogCommands.length) {
pseudoLogInterval = setTimeout(startPseudoLogging, 15000); // Restart after delay
return;
}
const command = pseudoLogCommands[commandIndex];
const lineElement = document.createElement('div');
lineElement.classList.add('cli-output-line');
lineElement.style.opacity = '0'; // Start hidden
dom.pseudoLogContent.appendChild(lineElement);
charIndex = 0;
function typeWriter() {
if (charIndex < command.length) {
lineElement.textContent += command.charAt(charIndex);
charIndex++;
setTimeout(typeWriter, LOG_ENTRY_DELAY);
} else {
// Fade in effect for the completed line
let fadeEffect = setInterval(() => {
if (parseFloat(lineElement.style.opacity) < 1) {
lineElement.style.opacity = (parseFloat(lineElement.style.opacity) + 0.1).toString();
} else {
clearInterval(fadeEffect);
// Add new line after a short pause
commandIndex++;
setTimeout(displayNextLogLine, 300);
}
}, 50);
// Scroll to bottom
dom.pseudoLogArea.scrollTop = dom.pseudoLogArea.scrollHeight;
}
}
typeWriter();
}
// Start the logging process
dom.pseudoLogArea.style.opacity = '1'; // Make log area visible
displayNextLogLine();
}
// --- Form Submit & Progress ---
async function handleFormSubmit(e) {
e.preventDefault(); e.preventDefault();
stopPseudoLogging(); // Stop the fake logs
// Transition to Progress State // UI Switch
dom.form.classList.add('d-none'); dom.formContainer.classList.add('d-none');
dom.progressContainer.classList.remove('d-none'); dom.processView.classList.remove('d-none');
dom.resultContainer.classList.add('d-none'); dom.resultView.classList.add('d-none');
dom.errorMessage.classList.add('d-none'); dom.errorMsg.classList.add('d-none');
dom.logContent.textContent = "INITIALIZING...";
dom.progressBar.style.width = "5%"; startPseudoLogs();
const formData = new FormData(dom.form); const formData = new FormData(dom.form);
try { try {
const response = await fetch('/start_download', { method: 'POST', body: formData }); const res = await fetch('/start_download', { method: 'POST', body: formData });
const result = await response.json(); const data = await res.json();
if (response.ok && result.job_id) { if (res.ok && data.job_id) {
currentJobId = result.job_id; currentJobId = data.job_id;
startPolling(); startPolling();
} else { } else {
showError(result.error || "Server error during submission."); throw new Error(data.error || "Start fehlgeschlagen");
} }
} catch (err) { } catch (err) {
showError(`Connection Error: ${err.message}`); showError(err.message);
} }
} }
function startPolling() { function startPolling() {
pollingInterval = setInterval(async () => { pollingInterval = setInterval(async () => {
try { try {
if (!currentJobId) { stopPolling(); return; } if(!currentJobId) return;
const res = await fetch(`/status?job_id=${currentJobId}`); const res = await fetch(`/status?job_id=${currentJobId}`);
if (res.status === 404) { showError("Job nicht gefunden"); return; }
if (res.status === 404) { stopPolling(); showError("Job not found (expired or invalid)."); return; }
if (!res.ok) { throw new Error(`HTTP error! status: ${res.status}`); }
const status = await res.json(); const status = await res.json();
updateProgressUI(status);
// Update UI
dom.progressBar.style.width = (status.progress || 0) + "%";
if(status.message) dom.statusMsg.textContent = status.message.toUpperCase();
if (status.status === 'completed') { if (status.status === 'completed') {
stopPolling(); finishJob(status.result_url);
showResult(status.result_url);
} else if (status.status === 'error' || status.error) { } else if (status.status === 'error' || status.error) {
stopPolling(); showError(status.error || status.message);
showError(status.error || status.message || "An unknown error occurred.");
} }
} catch (err) { } catch (e) {
console.error("Polling Error:", err); console.error(e);
showError(`Polling failed: ${err.message}`);
stopPolling();
} }
}, 1500); }, 1000);
} }
function stopPolling() { function finishJob(url) {
clearInterval(pollingInterval); clearInterval(pollingInterval);
pollingInterval = null; clearInterval(pseudoLogInterval);
}
function updateProgressUI(status) { dom.processView.classList.add('d-none');
const pct = status.progress || 0; dom.resultView.classList.remove('d-none');
dom.progressBar.style.width = `${pct}%`;
// Display the latest log message
if(status.logs && status.logs.length > 0) {
dom.logContent.textContent = status.logs[status.logs.length - 1];
} else {
dom.logContent.textContent = status.message || "...";
}
if(status.message) {
dom.statusMessage.textContent = status.message.toUpperCase();
}
}
// --- Result & Error Display ---
function showResult(url) {
dom.progressContainer.classList.add('d-none');
dom.resultContainer.classList.remove('d-none');
dom.resultUrl.href = url; dom.resultUrl.href = url;
dom.resultUrl.textContent = "DOWNLOAD LINK"; // Button text
saveToClientHistory(url); // Save to client-side history saveHistory(url);
} }
function showError(msg) { function showError(msg) {
dom.progressContainer.classList.add('d-none'); clearInterval(pollingInterval);
dom.form.classList.remove('d-none'); // Show form again for retry clearInterval(pseudoLogInterval);
dom.errorMessage.textContent = msg.toUpperCase();
dom.errorMessage.classList.remove('d-none'); dom.processView.classList.add('d-none');
dom.submitBtn.innerHTML = '<i class="fas fa-exclamation-triangle"></i> RETRY'; dom.formContainer.classList.remove('d-none'); // Back to form
dom.errorMsg.textContent = msg;
dom.errorMsg.classList.remove('d-none');
} }
// --- Client-Side History --- // --- Pseudo Logs (CLI Effect) ---
function getHistory() { function startPseudoLogs() {
const history = localStorage.getItem('mediaDlHistory'); dom.logContent.innerHTML = '';
if (!history) return []; let index = 0;
try {
const parsed = JSON.parse(history); function addLine() {
// Filter out expired entries if (index >= pseudoLogs.length) index = 0; // Loop or stop
const now = new Date(); const div = document.createElement('div');
return parsed.filter(item => { div.className = 'terminal-line';
const expiryDate = new Date(item.expiry); div.textContent = "> " + pseudoLogs[index];
return expiryDate > now; dom.logContent.appendChild(div);
});
} catch (e) { // Auto scroll
console.error("Error parsing history:", e); const win = document.querySelector('.terminal-window .terminal-body');
return []; win.scrollTop = win.scrollHeight;
index++;
} }
addLine(); // First one immediate
pseudoLogInterval = setInterval(addLine, 2000);
} }
function saveToClientHistory(url, title = "Unknown Title", platform = "Unknown") { // --- Local History ---
const history = getHistory(); function loadHistory() {
const expiryDate = new Date(); const raw = localStorage.getItem('mdl_history');
expiryDate.setDate(expiryDate.getDate() + HISTORY_EXPIRY_DAYS); if(!raw) {
dom.historyTableBody.innerHTML = '<tr><td colspan="4" class="text-center text-white-50 py-3">Kein Verlauf vorhanden</td></tr>';
return;
}
const newItem = { let data = JSON.parse(raw);
url: url, // Clean expired (>7 days)
title: title, const now = Date.now();
platform: platform, data = data.filter(i => (now - i.ts) < (7 * 24 * 60 * 60 * 1000));
timestamp: new Date().toISOString(), localStorage.setItem('mdl_history', JSON.stringify(data));
expiry: expiryDate.toISOString()
};
// Add new item and limit history size (e.g., last 50) renderHistory(data);
history.unshift(newItem);
if (history.length > 50) history.pop();
localStorage.setItem('mediaDlHistory', JSON.stringify(history));
renderHistory(history); // Update UI immediately
}
function loadClientHistory() {
const history = getHistory();
renderHistory(history);
} }
function renderHistory(data) { function renderHistory(data) {
if (!dom.historyTableBody) return;
dom.historyTableBody.innerHTML = ''; dom.historyTableBody.innerHTML = '';
if (!data || data.length === 0) {
dom.historyTableBody.innerHTML = '<tr><td colspan="4" class="text-center text-muted py-4">NO HISTORY YET.</td></tr>';
return;
}
data.forEach(item => { data.forEach(item => {
const row = document.createElement('tr'); const date = new Date(item.ts);
const itemDate = new Date(item.timestamp); const time = date.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'});
const expiryDate = new Date(item.expiry);
const timeStr = `${itemDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`;
const platformIcon = getPlatformIcon(item.platform);
row.innerHTML = ` const tr = document.createElement('tr');
<td class="text-muted small">${timeStr}</td> tr.innerHTML = `
<td class="text-center"><i class="${platformIcon.class}" style="color: ${platformIcon.color};"></i></td> <td class="ps-4 text-white-50 small font-monospace">${time}</td>
<td class="text-truncate" style="max-width: 180px;" title="${item.title}">${item.title.substring(0, 25)}...</td> <td class="text-center text-primary-light small">${item.platform}</td>
<td class="text-end"><a href="${item.url}" target="_blank" data-url="${item.url}" class="btn btn-sm btn-link text-success hover-underline"><i class="fas fa-link"></i></a></td> <td class="text-truncate" style="max-width: 150px;">${item.title || 'Unbekannt'}</td>
<td class="text-end pe-4">
<a href="${item.url}" target="_blank" class="text-primary-light">
<i class="fas fa-external-link-alt"></i>
</a>
</td>
`; `;
dom.historyTableBody.appendChild(row); dom.historyTableBody.appendChild(tr);
}); });
} }
function getPlatformIcon(p) { function saveHistory(url) {
const platformData = platforms.find(pl => pl.name === p); const pf = dom.platformInput.value;
if (platformData) return { class: `fas ${platformData.icon}`, color: platformData.color }; const entry = {
return { class: `fas fa-question-circle`, color: '#808080' }; url: url,
platform: pf,
title: pf + " Download", // Could be improved if backend sent title
ts: Date.now()
};
let data = JSON.parse(localStorage.getItem('mdl_history') || '[]');
data.unshift(entry);
if(data.length > 20) data.pop();
localStorage.setItem('mdl_history', JSON.stringify(data));
loadHistory();
} }
async function clearClientHistory() { function clearHistory() {
if(confirm("Clear ALL history? This cannot be undone.")) { if(confirm("Verlauf wirklich löschen?")) {
localStorage.removeItem('mediaDlHistory'); localStorage.removeItem('mdl_history');
renderHistory([]); // Clear the table loadHistory();
} }
} }
// --- Context Menu ---
let contextUrl = null;
function handleHistoryRightClick(e) {
const linkBtn = e.target.closest('a[data-url]');
if(linkBtn) {
e.preventDefault();
contextUrl = linkBtn.dataset.url;
dom.contextMenu.style.top = `${e.pageY}px`;
dom.contextMenu.style.left = `${e.pageX}px`;
dom.contextMenu.classList.remove('d-none');
}
}
function handleContextMenuClick(e) {
if(e.target.closest('[data-action="copy"]') && contextUrl) {
navigator.clipboard.writeText(contextUrl);
// Temporary notification for copy action
const notification = document.createElement('div');
notification.textContent = 'Link Copied!';
notification.style.cssText = `position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #39ff14; color: #0a0a0a; padding: 10px 20px; border-radius: 5px; z-index: 10000; font-weight: bold; opacity: 0; transition: opacity 0.5s; font-family: 'Consolas', monospace;`;
document.body.appendChild(notification);
setTimeout(() => notification.style.opacity = '1', 10);
setTimeout(() => {
notification.style.opacity = '0';
setTimeout(() => notification.remove(), 500);
}, 1500);
}
hideContextMenu();
}
function hideContextMenu() {
if(dom.contextMenu) dom.contextMenu.classList.add('d-none');
contextUrl = null;
}
// --- Stop Pseudo Logging ---
function stopPseudoLogging() {
if (pseudoLogInterval) clearInterval(pseudoLogInterval);
// Keep logs visible until progress starts
// Optionally hide it after transition: setTimeout(() => dom.pseudoLogArea.style.display = 'none', 800);
}
init(); init();
}); });
+245 -230
View File
@@ -1,281 +1,296 @@
/* Terminal/CLI Theme - Dark & Monospace */ /* --- Variables & Reset --- */
:root { :root {
--bg-dark: #0a0a0a; --bg-deep: #0f0c29;
--text-color: #00ff00; /* Classic Green */ --bg-mid: #302b63;
--text-muted: #008000; /* Darker Green */ --bg-light: #24243e;
--text-highlight: #39ff14; /* Bright Green */
--accent-primary: #00ff00; --primary: #8b5cf6; /* Violet 500 */
--accent-secondary: #39ff14; --primary-glow: #a78bfa;
--input-bg: rgba(0, 255, 0, 0.05); --accent: #d8b4fe;
--input-border: rgba(0, 255, 0, 0.2);
--card-bg: rgba(10, 10, 10, 0.7); --glass-bg: rgba(255, 255, 255, 0.05);
--card-border: rgba(0, 255, 0, 0.1); --glass-border: rgba(255, 255, 255, 0.1);
--shadow-color: rgba(0, 255, 0, 0.2); --glass-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
--progress-bg: rgba(0, 255, 0, 0.15);
--progress-bar: linear-gradient(90deg, #00ff00, #39ff14); --font-ui: 'Outfit', sans-serif;
--error-bg: rgba(255, 0, 0, 0.1); --font-mono: 'JetBrains Mono', monospace;
--error-border: rgba(255, 0, 0, 0.3);
--success-color: #39ff14;
} }
body { body {
background-color: var(--bg-dark); font-family: var(--font-ui);
color: var(--text-color); background-color: var(--bg-deep);
font-family: 'Consolas', 'Monaco', 'Andale Mono', 'Ubuntu Mono', monospace; color: #fff;
line-height: 1.5; overflow-x: hidden;
overflow-x: hidden; /* Verhindert horizontale Scrollbalken */
} }
/* Terminal Background Animation */ /* --- Animated Background --- */
.terminal-backdrop { .bg-gradient-animate {
position: fixed; position: fixed;
top: 0; top: 0; left: 0; width: 100%; height: 100%;
left: 0; background: linear-gradient(-45deg, #0f0c29, #302b63, #24243e, #1a1a2e);
width: 100%; background-size: 400% 400%;
height: 100%; animation: gradientBG 15s ease infinite;
background: var(--bg-dark); z-index: -2;
overflow: hidden; }
@keyframes gradientBG {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
.bg-glow {
position: fixed;
border-radius: 50%;
filter: blur(100px);
opacity: 0.4;
z-index: -1; z-index: -1;
animation: floatBlob 10s ease-in-out infinite;
} }
.terminal-line { .bg-glow-1 {
position: absolute; top: -10%; left: -10%; width: 50vw; height: 50vw;
background: linear-gradient(to right, rgba(0, 255, 0, 0.02), rgba(57, 255, 20, 0.01)); background: var(--primary);
animation: scanline 15s linear infinite;
}
.terminal-line:nth-child(1) { top: 10%; width: 80%; left: 10%; animation-duration: 12s; }
.terminal-line:nth-child(2) { top: 50%; width: 60%; left: 20%; animation-duration: 18s; animation-delay: -6s; }
.terminal-line:nth-child(3) { top: 80%; width: 70%; left: 15%; animation-duration: 14s; animation-delay: -4s; }
@keyframes scanline {
0% { opacity: 0; transform: translateX(-100%); }
50% { opacity: 0.5; }
100% { opacity: 0; transform: translateX(100%); }
} }
/* Main Container */ .bg-glow-2 {
.container { bottom: -10%; right: -10%; width: 40vw; height: 40vw;
z-index: 2; background: #4c1d95; /* Darker Purple */
animation-delay: -5s;
} }
/* CLI Card */ @keyframes floatBlob {
.cli-card { 0%, 100% { transform: translate(0, 0); }
background: var(--card-bg); 50% { transform: translate(30px, -20px); }
border: 1px solid var(--card-border); }
border-radius: 8px;
padding: 2rem 2.5rem; /* --- Typography & Helpers --- */
.text-primary-light { color: var(--accent) !important; }
.tracking-wide { letter-spacing: 0.05em; }
.max-w-lg { max-width: 600px; }
/* --- Hero Section --- */
.hero-wrapper {
max-width: 900px;
width: 100%; width: 100%;
max-width: 700px; padding: 0 1rem;
box-shadow: 0 0 15px var(--shadow-color);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.cli-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 30px var(--shadow-color);
} }
.logo-text { .hero-title {
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary)); font-size: 4rem;
font-weight: 700;
background: linear-gradient(to right, #fff, var(--accent));
-webkit-background-clip: text; -webkit-background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
text-shadow: 0 0 10px var(--shadow-color); text-shadow: 0 0 30px rgba(139, 92, 246, 0.3);
} }
/* CLI Input Group */ @media (max-width: 768px) {
.cli-input-group { .hero-title { font-size: 2.5rem; }
background: var(--input-bg); }
border: 1px solid var(--input-border);
border-radius: 6px; /* --- Glass Input --- */
.glass-input-wrapper {
background: rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
border: 1px solid var(--glass-border);
border-radius: 16px;
transition: transform 0.3s, box-shadow 0.3s, border-color 0.3s;
overflow: hidden; overflow: hidden;
} }
.cli-input-group .input-group-text { .glass-input-wrapper:focus-within {
background: transparent; transform: translateY(-2px);
border: none; box-shadow: 0 15px 40px rgba(139, 92, 246, 0.2);
color: var(--text-muted); border-color: var(--primary);
} }
.cli-input { .form-control::placeholder { color: rgba(255, 255, 255, 0.3); }
color: var(--text-color); .form-control:focus { box-shadow: none; }
caret-color: var(--text-highlight);
border-radius: 0; /* Ensures no rounded corners inside */ .btn-action {
background: var(--primary);
color: white;
border: none;
border-radius: 0;
transition: filter 0.2s;
min-width: 120px;
} }
.cli-input::placeholder { .btn-action:hover {
color: var(--text-muted); filter: brightness(1.2);
opacity: 0.7; color: white;
} }
.cli-button { /* --- Detection & Options --- */
background: var(--accent-primary); .glass-badge {
border: none; background: rgba(255, 255, 255, 0.1);
color: var(--bg-dark); backdrop-filter: blur(5px);
font-weight: bold; border: 1px solid var(--glass-border);
text-transform: uppercase; font-weight: 300;
letter-spacing: 1px; letter-spacing: 1px;
transition: background 0.2s ease, transform 0.2s ease;
}
.cli-button:hover {
background: var(--accent-secondary);
transform: translateX(2px);
} }
/* CLI Badge */ .options-drawer {
.cli-badge { max-width: 600px;
background: rgba(0, 255, 0, 0.1);
border: 1px solid var(--input-border);
color: var(--text-highlight);
}
.cli-icon {
color: var(--text-highlight);
} }
/* CLI Output & Options */ .option-group {
.cli-output-line { animation: slideDown 0.4s ease forwards;
font-family: 'Consolas', monospace;
text-align: left;
color: var(--text-muted);
font-size: 0.9rem;
}
.cli-highlight {
color: var(--text-highlight);
animation: blink 1s infinite step-end;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
} }
.cli-output-box { @keyframes slideDown {
background: rgba(10, 20, 10, 0.5); from { opacity: 0; transform: translateY(-10px); }
border: 1px solid var(--card-border);
padding: 1rem;
border-radius: 4px;
text-align: left;
overflow: hidden; /* Important for smooth fade-in */
}
.cli-output-box pre {
margin: 0;
white-space: pre-wrap;
word-wrap: break-word;
}
.cli-card-inner {
background: rgba(20, 40, 20, 0.4);
border-radius: 6px;
}
.cli-section {
animation: fadeInUpCli 0.5s ease-out;
}
@keyframes fadeInUpCli {
from { opacity: 0; transform: translateY(15px); }
to { opacity: 1; transform: translateY(0); } to { opacity: 1; transform: translateY(0); }
} }
/* Options Styling */ /* Custom Switch Toggle (MP3/MP4) */
.form-label.small { font-size: 0.8rem; font-weight: bold; color: var(--text-muted); } .switch-toggle {
.text-success { color: var(--accent-secondary) !important; } display: flex;
.btn-outline-success { border-color: var(--accent-primary); color: var(--accent-primary); } background: rgba(0,0,0,0.4);
.btn-outline-success:hover { background-color: var(--accent-primary); color: var(--bg-dark); } border-radius: 8px;
.btn-check:checked + .btn-outline-success { background-color: var(--accent-primary); border-color: var(--accent-primary); color: var(--bg-dark); } position: relative;
padding: 4px;
.cli-select { width: fit-content;
background: var(--input-bg); border: 1px solid var(--glass-border);
border: 1px solid var(--input-border);
color: var(--text-color);
border-radius: 4px;
} }
.switch-toggle input { display: none; }
/* Switch Button */ .switch-toggle label {
.form-switch .form-check-input { padding: 6px 16px;
background-color: var(--text-muted); cursor: pointer;
border-color: var(--input-border); z-index: 2;
transition: background-color 0.2s, border-color 0.2s; font-size: 0.9rem;
color: rgba(255,255,255,0.7);
transition: color 0.3s;
} }
.form-switch .form-check-input:checked { .toggle-bg {
background-color: var(--accent-primary); position: absolute;
border-color: var(--accent-secondary); top: 4px; left: 4px;
} height: calc(100% - 8px);
width: calc(50% - 4px); /* Approximation */
/* Progress Bar */ background: var(--primary);
.cli-progress {
height: 10px;
border-radius: 5px;
background: var(--progress-bg);
border: 1px solid var(--input-border);
}
.cli-progress-bar {
background: var(--progress-bar);
border-radius: 5px;
transition: width 0.3s ease-out;
box-shadow: 0 0 5px var(--shadow-color);
}
/* Alerts & Modals */
.cli-alert {
background: var(--error-bg);
border: 1px solid var(--error-border);
color: var(--text-color);
border-radius: 4px;
}
.cli-modal-content {
background-color: var(--bg-dark);
border: 1px solid var(--card-border);
border-radius: 12px;
}
.cli-modal-header, .cli-modal-footer {
background-color: rgba(15, 15, 15, 0.8);
border: none;
}
.cli-table {
background-color: transparent;
}
.cli-table th {
background-color: rgba(10, 20, 10, 0.6);
color: var(--text-highlight);
border: none !important;
font-size: 0.8rem;
}
.cli-table td {
border-top: 1px solid var(--card-border);
color: var(--text-muted);
}
.cli-table tbody tr:hover td {
background-color: rgba(0, 255, 0, 0.1) !important;
}
.cli-table td a {
color: var(--accent-secondary);
}
/* Context Menu */
.cli-context-menu {
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 6px; border-radius: 6px;
transition: transform 0.3s cubic-bezier(0.4, 0.0, 0.2, 1);
z-index: 1;
} }
.cli-context-menu .list-group-item { #format-mp3:checked ~ .toggle-bg { transform: translateX(0); width: 63px; }
background: transparent; #format-mp4:checked ~ .toggle-bg { transform: translateX(100%) translateX(6px); width: 63px; }
color: var(--text-color); #format-mp3:checked + label { color: white; }
border: none; #format-mp4:checked + label { color: white; }
transition: background 0.2s ease;
.glass-select {
background: rgba(0,0,0,0.4);
border: 1px solid var(--glass-border);
color: white;
border-radius: 8px;
font-size: 0.9rem;
cursor: pointer;
} }
.cli-context-menu .list-group-item:hover { .glass-select:focus {
background: rgba(0, 255, 0, 0.1); background: rgba(0,0,0,0.6);
color: white;
box-shadow: none;
border-color: var(--primary);
} }
/* General Animation */ .custom-switch:checked {
.fade-in-up { animation: fadeInUp 0.8s ease-out forwards; } background-color: var(--primary);
@keyframes fadeInUp { border-color: var(--primary);
}
/* --- Processing & Terminal --- */
.glass-progress {
height: 6px;
background: rgba(255,255,255,0.1);
border-radius: 3px;
overflow: hidden;
}
.bg-primary-gradient {
background: linear-gradient(90deg, var(--primary), var(--accent));
box-shadow: 0 0 10px var(--primary);
transition: width 0.3s ease;
}
.terminal-window {
background: rgba(10, 10, 15, 0.85); /* Very dark */
border: 1px solid rgba(255,255,255,0.1);
border-radius: 8px;
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
height: 200px;
display: flex;
flex-direction: column;
}
.dot { width: 10px; height: 10px; border-radius: 50%; }
.terminal-body {
flex: 1;
overflow-y: auto;
font-family: var(--font-mono);
opacity: 0.9;
padding-top: 0.5rem;
}
.terminal-line {
margin-bottom: 4px;
animation: typeIn 0.2s ease forwards;
}
@keyframes typeIn { from { opacity: 0; transform: translateX(-5px); } to { opacity: 1; transform: translateX(0); } }
/* --- Result View --- */
.result-card {
background: rgba(255,255,255,0.05);
backdrop-filter: blur(15px);
border: 1px solid rgba(255,255,255,0.1);
box-shadow: 0 20px 50px rgba(0,0,0,0.5);
}
.btn-primary-glow {
background: var(--primary);
color: white;
border: none;
box-shadow: 0 0 20px rgba(139, 92, 246, 0.4);
transition: transform 0.2s, box-shadow 0.2s;
}
.btn-primary-glow:hover {
transform: translateY(-2px);
box-shadow: 0 0 30px rgba(139, 92, 246, 0.6);
color: white;
}
/* --- History Modal --- */
.glass-modal {
background: rgba(20, 20, 30, 0.85);
backdrop-filter: blur(15px);
border: 1px solid var(--glass-border);
box-shadow: 0 0 50px rgba(0,0,0,0.5);
}
.border-bottom-white-10 { border-bottom: 1px solid rgba(255,255,255,0.1); }
.border-top-white-10 { border-top: 1px solid rgba(255,255,255,0.1); }
#history-table a { color: var(--accent); text-decoration: none; }
#history-table a:hover { text-decoration: underline; }
/* --- Utility Classes --- */
.btn-glass-icon {
background: rgba(255,255,255,0.1);
color: white;
border: 1px solid transparent;
border-radius: 50%;
width: 45px; height: 45px;
transition: all 0.2s;
}
.btn-glass-icon:hover {
background: rgba(255,255,255,0.2);
color: white;
border-color: rgba(255,255,255,0.3);
}
.fade-in { animation: fadeIn 0.8s ease forwards; opacity: 0; }
.delay-1 { animation-delay: 0.2s; }
.delay-2 { animation-delay: 0.4s; }
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); } from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); } to { opacity: 1; transform: translateY(0); }
} }
.animate-options { animation: fadeInCli 0.5s ease-out forwards; } .glass-alert {
background: rgba(220, 38, 38, 0.2);
/* Responsive adjustments */ border: 1px solid rgba(220, 38, 38, 0.5);
@media (max-width: 768px) { backdrop-filter: blur(5px);
.cli-card { padding: 1.5rem; }
.logo-text { font-size: 2.5rem; }
.input-group-lg .form-control, .input-group-lg .form-select, .input-group-lg .input-group-text { font-size: 1rem; }
} }
+142 -114
View File
@@ -2,172 +2,200 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>unknownMedien.dl - Smart Downloader</title> <title>unknownMedien.dl</title>
<!-- Bootstrap CSS --> <!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome --> <!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" /> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" />
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;700&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
<!-- Custom CSS --> <!-- Custom CSS -->
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head> </head>
<body> <body>
<!-- Terminal-ähnlicher Hintergrundeffekt --> <!-- Animated Background -->
<div class="terminal-backdrop"> <div class="bg-gradient-animate"></div>
<div class="terminal-line" id="terminal-line-1"></div> <div class="bg-glow bg-glow-1"></div>
<div class="terminal-line" id="terminal-line-2"></div> <div class="bg-glow bg-glow-2"></div>
<div class="terminal-line" id="terminal-line-3"></div>
</div>
<div class="container d-flex flex-column min-vh-100 justify-content-center align-items-center position-relative z-1 p-3"> <!-- Header / Nav -->
<nav class="navbar fixed-top w-100 p-4 d-flex justify-content-between align-items-center z-3">
<div class="brand fw-bold text-white fs-4 tracking-wide">
unknownMedien<span class="text-primary-light">.dl</span>
</div>
<button class="btn btn-glass-icon" data-bs-toggle="modal" data-bs-target="#historyModal" title="Verlauf">
<i class="fas fa-history"></i>
</button>
</nav>
<!-- Main Content Card --> <!-- Main Content (Hero) -->
<div class="cli-card text-center fade-in-up"> <main class="container-fluid min-vh-100 d-flex flex-column justify-content-center align-items-center position-relative z-2">
<h1 class="display-4 fw-bold mb-2 text-shadow logo-text">unknownMedien.dl</h1>
<p class="text-muted mb-4 lead">Der schnelle Weg zu deinen Medien.</p>
<form id="upload-form"> <div class="hero-wrapper w-100 text-center">
<input type="hidden" name="platform" id="input-platform" value="SoundCloud">
<div class="input-group input-group-lg shadow-sm mb-3 cli-input-group"> <!-- Headline -->
<span class="input-group-text bg-transparent border-0 text-success ps-4"><i class="fas fa-keyboard"></i></span> <h1 class="hero-title mb-3 fade-in">Media Downloader</h1>
<input type="url" class="form-control bg-transparent border-0 cli-input" id="url" name="url" required placeholder="Link hier einfügen..." autocomplete="off"> <p class="hero-subtitle text-white-50 mb-5 fade-in delay-1">
<button class="btn btn-success cli-button px-4 fw-bold" type="submit" id="submit-button"> Einfach Link einfügen. Wir übernehmen den Rest.
<i class="fas fa-terminal"></i> EXEC </p>
</button>
</div>
<!-- Detected Platform Badge (CLI Style) --> <!-- Form Area -->
<div id="platform-badge-container" class="mb-3 p-2 cli-output-line" style="height: 30px; opacity: 0; transition: all 0.3s ease;"> <div class="form-container fade-in delay-2">
<span class="badge rounded-pill bg-transparent cli-badge border px-3 py-2"> <form id="upload-form">
<i id="detected-icon" class="fas fa-question-circle me-1 cli-icon"></i> <input type="hidden" name="platform" id="input-platform" value="SoundCloud">
<span id="detected-text">SCANNING...</span>
</span>
</div>
<!-- Options Container --> <!-- Main Input -->
<div id="options-container" class="options-wrapper cli-card-body-overlay"> <div class="glass-input-wrapper d-flex align-items-center shadow-lg">
<div class="card card-body border-0 shadow-sm cli-card-inner p-3 mt-2"> <div class="icon-zone ps-4 text-white-50">
<i class="fas fa-link" id="url-icon"></i>
</div>
<input type="url" class="form-control bg-transparent border-0 text-white p-4 fs-5"
id="url" name="url" required placeholder="https://..." autocomplete="off">
<button class="btn btn-action p-4 fw-bold text-uppercase" type="submit" id="submit-button">
Start <i class="fas fa-arrow-right ms-2"></i>
</button>
</div>
<div id="youtube-options" class="d-none animate-options mb-3 pb-3 border-bottom cli-section"> <!-- Detected Platform & Options Slide-Down -->
<h6 class="fw-bold text-uppercase small text-success mb-2"><i class="fab fa-youtube text-danger me-1"></i> YOUTUBE CONFIG</h6> <div id="detection-area" class="mt-4 transition-all" style="opacity: 0; transform: translateY(-10px);">
<div class="row g-3">
<div class="col-md-6"> <!-- Badge -->
<label class="form-label small fw-bold text-info">FORMAT</label> <span class="badge glass-badge px-3 py-2 rounded-pill mb-3">
<div class="btn-group w-100" role="group"> <i id="detected-icon" class="fas fa-check me-2"></i>
<input type="radio" class="btn-check" name="yt_format" id="format-mp3" value="mp3" checked> <span id="detected-text">SoundCloud erkannt</span>
<label class="btn btn-outline-success btn-sm" for="format-mp3">AUDIO</label> </span>
<input type="radio" class="btn-check" name="yt_format" id="format-mp4" value="mp4">
<label class="btn btn-outline-success btn-sm" for="format-mp4">VIDEO</label> <!-- Dynamic Options -->
<div id="options-container" class="options-drawer mx-auto text-start text-white">
<!-- YouTube Options -->
<div id="youtube-options" class="d-none option-group">
<div class="row g-4 justify-content-center">
<div class="col-auto">
<label class="text-white-50 small text-uppercase fw-bold mb-2 d-block">Format</label>
<div class="switch-toggle">
<input type="radio" name="yt_format" id="format-mp3" value="mp3" checked>
<label for="format-mp3">MP3</label>
<input type="radio" name="yt_format" id="format-mp4" value="mp4">
<label for="format-mp4">MP4</label>
<div class="toggle-bg"></div>
</div>
</div>
<div class="col-auto" id="quality-wrapper">
<label class="text-white-50 small text-uppercase fw-bold mb-2 d-block">Qualität</label>
<select class="form-select glass-select" id="mp3_bitrate" name="mp3_bitrate">
<option>Best</option><option selected>192k</option><option>128k</option>
</select>
<select class="form-select glass-select d-none" id="mp4_quality" name="mp4_quality">
<option selected>Best</option><option>Medium</option><option>Low</option>
</select>
</div> </div>
</div> </div>
<div class="col-md-6" id="quality-wrapper">
<label class="form-label small fw-bold text-info">QUALITY</label>
<select class="form-select form-select-sm cli-select" id="mp3_bitrate" name="mp3_bitrate">
<option>BEST</option> <option selected>192k</option> <option>128k</option>
</select>
<select class="form-select form-select-sm cli-select d-none" id="mp4_quality" name="mp4_quality">
<option selected>BEST</option> <option>MEDIUM</option> <option>LOW</option>
</select>
</div>
</div> </div>
</div>
<div id="codec-options-section" class="d-none animate-options cli-section"> <!-- Codec Options -->
<div class="d-flex justify-content-between align-items-center"> <div id="codec-options-section" class="d-none option-group mt-3 text-center">
<div> <div class="d-inline-flex align-items-center glass-panel px-3 py-2 rounded">
<label class="form-label small fw-bold text-info mb-0">COMPATIBILITY MODE</label> <span class="text-white-50 small me-3">Kompatibilität (H.264)</span>
<div class="text-muted extra-small">H.264 (for max compatibility)</div> <div class="form-check form-switch m-0">
</div> <input class="form-check-input custom-switch" type="checkbox" role="switch" id="codec-switch">
<div class="form-check form-switch"> <input type="hidden" name="codec_preference" id="codec_preference" value="original">
<input class="form-check-input" type="checkbox" role="switch" id="codec-switch"> </div>
<input type="hidden" name="codec_preference" id="codec_preference" value="original">
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Error Alert -->
<div id="error-message" class="alert glass-alert mt-4 d-none text-danger fw-bold" role="alert"></div>
</form>
</div>
<!-- Processing / Terminal View (Hidden initially) -->
<div id="process-view" class="d-none w-100 max-w-lg mx-auto mt-5">
<!-- Main Status -->
<h3 id="status-message" class="text-white fw-bold mb-3 tracking-wide">INITIALISIERUNG...</h3>
<!-- Progress Bar -->
<div class="progress glass-progress mb-4">
<div id="progress-bar" class="progress-bar bg-primary-gradient" style="width: 0%"></div>
</div> </div>
<!-- Pseudo-Logs Area --> <!-- CLI Log Window -->
<div id="pseudo-log-area" class="cli-output-box shadow-sm text-start" style="max-height: 250px; opacity: 0;"> <div class="terminal-window glass-panel text-start p-3 font-monospace small">
<div id="pseudo-log-content"></div> <div class="terminal-header d-flex gap-2 mb-2 opacity-50">
<div class="dot bg-danger"></div>
<div class="dot bg-warning"></div>
<div class="dot bg-success"></div>
<span class="ms-2">worker@unknown-dl:~# process_task</span>
</div>
<div id="pseudo-log-content" class="terminal-body text-primary-light">
<!-- Logs typing here -->
</div>
</div> </div>
<!-- Error Message -->
<div id="error-message" class="alert alert-danger mt-3 d-none shadow-sm cli-alert text-start" role="alert"></div>
</form>
<!-- Progress View -->
<div id="progress-container" class="d-none mt-4 text-center">
<h5 id="status-message" class="fw-bold mb-3 cli-output-line cli-highlight"></h5>
<div class="progress cli-progress mb-2">
<div id="progress-bar" class="progress-bar cli-progress-bar" style="width: 0%"></div>
</div>
<div id="log-content" class="text-muted mt-2 extra-small text-truncate cli-output-line"></div>
</div> </div>
<!-- Result View --> <!-- Result View -->
<div id="result-container" class="d-none mt-4"> <div id="result-view" class="d-none mt-5">
<div class="success-checkmark mb-3"> <div class="result-card glass-panel p-5 d-inline-block rounded-4">
<i class="fas fa-check-circle text-success display-4"></i> <div class="icon-circle bg-success-soft mb-3 mx-auto">
<i class="fas fa-check text-success fs-2"></i>
</div>
<h2 class="text-white fw-bold mb-1">Fertig!</h2>
<p class="text-white-50 mb-4">Deine Datei steht bereit.</p>
<a id="result-url" href="#" target="_blank" class="btn btn-primary-glow btn-lg px-5 rounded-pill fw-bold">
<i class="fas fa-download me-2"></i> Herunterladen
</a>
<div class="mt-4">
<button class="btn btn-link text-white-50 text-decoration-none btn-sm" onclick="location.reload()">
<i class="fas fa-redo me-1"></i> Weitere Datei
</button>
</div>
</div> </div>
<h4 class="fw-bold">COMPLETE.</h4>
<a id="result-url" href="#" target="_blank" class="btn btn-success cli-button btn-lg mt-2 shadow-lg hover-lift">
<i class="fas fa-download me-2"></i> DOWNLOAD
</a>
<button class="btn btn-link text-muted mt-3 d-block mx-auto" onclick="location.reload()">
NEW SESSION
</button>
</div> </div>
</div> </div>
</main>
<!-- Footer -->
<div class="mt-auto py-4 text-center text-muted extra-small opacity-75">
&copy; 2025 unknownMedien.dl &bull; Links expire in 7 days.
</div>
</div>
<!-- History Modal --> <!-- History Modal -->
<div class="modal fade" id="historyModal" tabindex="-1" aria-hidden="true"> <div class="modal fade" id="historyModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered"> <div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content border-0 shadow-lg cli-modal-content"> <div class="modal-content glass-modal border-0">
<div class="modal-header border-0 bg-dark cli-modal-header"> <div class="modal-header border-bottom-white-10">
<h5 class="modal-title fw-bold text-success"><i class="fas fa-history me-2"></i>HISTORY</h5> <h5 class="modal-title text-white fw-bold">Verlauf (Lokal)</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div> </div>
<div class="modal-body p-0"> <div class="modal-body p-0">
<div class="table-responsive" style="max-height: 400px;"> <div class="table-responsive" style="max-height: 50vh;">
<table id="history-table" class="table table-hover mb-0 align-middle cli-table"> <table class="table table-dark table-hover mb-0 bg-transparent align-middle" id="history-table">
<thead class="cli-table-header sticky-top"> <thead>
<tr> <tr class="text-white-50 small text-uppercase">
<th>TIME</th> <th class="ps-4">Zeit</th>
<th class="text-center">TYPE</th> <th class="text-center">Typ</th>
<th>TITLE</th> <th>Titel</th>
<th class="text-end">ACTION</th> <th class="text-end pe-4">Link</th>
</tr> </tr>
</thead> </thead>
<tbody class="border-top-0"> <tbody>
<!-- JS fills this --> <!-- JS Fills this -->
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
<div class="modal-footer border-0 bg-dark cli-modal-footer"> <div class="modal-footer border-top-white-10 justify-content-between">
<button id="clear-history-button" class="btn btn-outline-danger btn-sm">CLEAR ALL</button> <button id="clear-history-button" class="btn btn-outline-danger btn-sm rounded-pill">Alle löschen</button>
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">CLOSE</button> <small class="text-white-50">Gespeichert im Browser für 7 Tage</small>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Context Menu -->
<ul id="context-menu" class="context-menu list-group position-absolute d-none shadow-lg cli-context-menu">
<li class="list-group-item list-group-item-action py-2 px-3" data-action="copy"><i class="fas fa-copy me-2"></i>Copy Link</li>
</ul>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="{{ url_for('static', filename='script.js') }}"></script> <script src="{{ url_for('static', filename='script.js') }}"></script>
</body> </body>