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.detectionArea.style.transform = 'translateY(-10px)';
dom.urlIcon.className = 'fas fa-link';
dom.urlIcon.style.color = ''; dom.urlIcon.style.color = '';
hideAllOptions(); }
// 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++;
} }
function saveToClientHistory(url, title = "Unknown Title", platform = "Unknown") { addLine(); // First one immediate
const history = getHistory(); pseudoLogInterval = setInterval(addLine, 2000);
const expiryDate = new Date();
expiryDate.setDate(expiryDate.getDate() + HISTORY_EXPIRY_DAYS);
const newItem = {
url: url,
title: title,
platform: platform,
timestamp: new Date().toISOString(),
expiry: expiryDate.toISOString()
};
// Add new item and limit history size (e.g., last 50)
history.unshift(newItem);
if (history.length > 50) history.pop();
localStorage.setItem('mediaDlHistory', JSON.stringify(history));
renderHistory(history); // Update UI immediately
} }
function loadClientHistory() { // --- Local History ---
const history = getHistory(); function loadHistory() {
renderHistory(history); const raw = localStorage.getItem('mdl_history');
if(!raw) {
dom.historyTableBody.innerHTML = '<tr><td colspan="4" class="text-center text-white-50 py-3">Kein Verlauf vorhanden</td></tr>';
return;
}
let data = JSON.parse(raw);
// Clean expired (>7 days)
const now = Date.now();
data = data.filter(i => (now - i.ts) < (7 * 24 * 60 * 60 * 1000));
localStorage.setItem('mdl_history', JSON.stringify(data));
renderHistory(data);
} }
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; }
} }
+126 -98
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>
<!-- 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> </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>
<div class="container d-flex flex-column min-vh-100 justify-content-center align-items-center position-relative z-1 p-3"> <!-- Main Content (Hero) -->
<main class="container-fluid min-vh-100 d-flex flex-column justify-content-center align-items-center position-relative z-2">
<!-- Main Content Card --> <div class="hero-wrapper w-100 text-center">
<div class="cli-card text-center fade-in-up">
<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>
<!-- Headline -->
<h1 class="hero-title mb-3 fade-in">Media Downloader</h1>
<p class="hero-subtitle text-white-50 mb-5 fade-in delay-1">
Einfach Link einfügen. Wir übernehmen den Rest.
</p>
<!-- Form Area -->
<div class="form-container fade-in delay-2">
<form id="upload-form"> <form id="upload-form">
<input type="hidden" name="platform" id="input-platform" value="SoundCloud"> <input type="hidden" name="platform" id="input-platform" value="SoundCloud">
<div class="input-group input-group-lg shadow-sm mb-3 cli-input-group"> <!-- Main Input -->
<span class="input-group-text bg-transparent border-0 text-success ps-4"><i class="fas fa-keyboard"></i></span> <div class="glass-input-wrapper d-flex align-items-center shadow-lg">
<input type="url" class="form-control bg-transparent border-0 cli-input" id="url" name="url" required placeholder="Link hier einfügen..." autocomplete="off"> <div class="icon-zone ps-4 text-white-50">
<button class="btn btn-success cli-button px-4 fw-bold" type="submit" id="submit-button"> <i class="fas fa-link" id="url-icon"></i>
<i class="fas fa-terminal"></i> EXEC </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> </button>
</div> </div>
<!-- Detected Platform Badge (CLI Style) --> <!-- Detected Platform & Options Slide-Down -->
<div id="platform-badge-container" class="mb-3 p-2 cli-output-line" style="height: 30px; opacity: 0; transition: all 0.3s ease;"> <div id="detection-area" class="mt-4 transition-all" style="opacity: 0; transform: translateY(-10px);">
<span class="badge rounded-pill bg-transparent cli-badge border px-3 py-2">
<i id="detected-icon" class="fas fa-question-circle me-1 cli-icon"></i> <!-- Badge -->
<span id="detected-text">SCANNING...</span> <span class="badge glass-badge px-3 py-2 rounded-pill mb-3">
<i id="detected-icon" class="fas fa-check me-2"></i>
<span id="detected-text">SoundCloud erkannt</span>
</span> </span>
</div>
<!-- Options Container --> <!-- Dynamic Options -->
<div id="options-container" class="options-wrapper cli-card-body-overlay"> <div id="options-container" class="options-drawer mx-auto text-start text-white">
<div class="card card-body border-0 shadow-sm cli-card-inner p-3 mt-2">
<div id="youtube-options" class="d-none animate-options mb-3 pb-3 border-bottom cli-section"> <!-- YouTube Options -->
<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="youtube-options" class="d-none option-group">
<div class="row g-3"> <div class="row g-4 justify-content-center">
<div class="col-md-6"> <div class="col-auto">
<label class="form-label small fw-bold text-info">FORMAT</label> <label class="text-white-50 small text-uppercase fw-bold mb-2 d-block">Format</label>
<div class="btn-group w-100" role="group"> <div class="switch-toggle">
<input type="radio" class="btn-check" name="yt_format" id="format-mp3" value="mp3" checked> <input type="radio" name="yt_format" id="format-mp3" value="mp3" checked>
<label class="btn btn-outline-success btn-sm" for="format-mp3">AUDIO</label> <label for="format-mp3">MP3</label>
<input type="radio" class="btn-check" name="yt_format" id="format-mp4" value="mp4"> <input type="radio" name="yt_format" id="format-mp4" value="mp4">
<label class="btn btn-outline-success btn-sm" for="format-mp4">VIDEO</label> <label for="format-mp4">MP4</label>
<div class="toggle-bg"></div>
</div> </div>
</div> </div>
<div class="col-md-6" id="quality-wrapper"> <div class="col-auto" id="quality-wrapper">
<label class="form-label small fw-bold text-info">QUALITY</label> <label class="text-white-50 small text-uppercase fw-bold mb-2 d-block">Qualität</label>
<select class="form-select form-select-sm cli-select" id="mp3_bitrate" name="mp3_bitrate"> <select class="form-select glass-select" id="mp3_bitrate" name="mp3_bitrate">
<option>BEST</option> <option selected>192k</option> <option>128k</option> <option>Best</option><option selected>192k</option><option>128k</option>
</select> </select>
<select class="form-select form-select-sm cli-select d-none" id="mp4_quality" name="mp4_quality"> <select class="form-select glass-select d-none" id="mp4_quality" name="mp4_quality">
<option selected>BEST</option> <option>MEDIUM</option> <option>LOW</option> <option selected>Best</option><option>Medium</option><option>Low</option>
</select> </select>
</div> </div>
</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 class="form-check-input" type="checkbox" role="switch" id="codec-switch">
<input type="hidden" name="codec_preference" id="codec_preference" value="original"> <input type="hidden" name="codec_preference" id="codec_preference" value="original">
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Pseudo-Logs Area --> <!-- Error Alert -->
<div id="pseudo-log-area" class="cli-output-box shadow-sm text-start" style="max-height: 250px; opacity: 0;"> <div id="error-message" class="alert glass-alert mt-4 d-none text-danger fw-bold" role="alert"></div>
<div id="pseudo-log-content"></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> </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>
<div id="log-content" class="text-muted mt-2 extra-small text-truncate cli-output-line"></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>
<!-- CLI Log Window -->
<div class="terminal-window glass-panel text-start p-3 font-monospace small">
<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> </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> </div>
<h4 class="fw-bold">COMPLETE.</h4> <h2 class="text-white fw-bold mb-1">Fertig!</h2>
<a id="result-url" href="#" target="_blank" class="btn btn-success cli-button btn-lg mt-2 shadow-lg hover-lift"> <p class="text-white-50 mb-4">Deine Datei steht bereit.</p>
<i class="fas fa-download me-2"></i> DOWNLOAD
<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> </a>
<button class="btn btn-link text-muted mt-3 d-block mx-auto" onclick="location.reload()">
NEW SESSION <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> </button>
</div> </div>
</div> </div>
<!-- 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>
</div> </div>
</main>
<!-- 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>