feat: one-pager design

This commit is contained in:
2025-12-23 19:54:56 +01:00
parent a2c4f29172
commit eff8c3ee2f
3 changed files with 569 additions and 919 deletions
+257 -465
View File
@@ -1,520 +1,312 @@
document.addEventListener('DOMContentLoaded', () => {
// --- Selektoren & Globale Variablen ---
const selectors = {
form: '#upload-form',
submitButton: '#submit-button',
platformRadios: 'input[name="platform"]',
ytOptionsDiv: '#youtube-options',
ytQualityDiv: '#youtube-quality',
ytFormatRadios: 'input[name="yt_format"]',
mp3QualitySection: '#mp3-quality-section',
mp4QualitySection: '#mp4-quality-section',
codecOptionsSection: '#codec-options-section',
progressBar: '#progress-bar',
statusMessage: '#status-message',
logContent: '#log-content',
logOutput: '#log-output',
resultUrlArea: '#result-url-area',
resultUrlLink: '#result-url',
copyResultUrlLink: '#copy-result-url',
errorMessage: '#error-message',
historyTableBody: '#history-table tbody',
clearHistoryButton: '#clear-history-button',
contextMenu: '#context-menu',
queueInfo: '#queue-info',
statsTotalJobs: '#stats-total-jobs',
statsAvgDuration: '#stats-avg-duration',
statsTotalSize: '#stats-total-size',
urlHelpText: '#urlHelp',
// Selektoren für das Overlay
processingOverlay: '#processing-overlay',
overlayMessage: '#overlay-message',
// NEU: Selektoren für Status im Overlay
overlayStatusText: '#overlay-status-text',
overlayProgressBar: '#overlay-progress-bar',
// --- Selektoren ---
const dom = {
form: document.getElementById('upload-form'),
urlInput: document.getElementById('url'),
platformInput: document.getElementById('input-platform'),
submitBtn: document.getElementById('submit-button'),
// Detection Feedback
badgeContainer: document.getElementById('platform-badge-container'),
detectedIcon: document.getElementById('detected-icon'),
detectedText: document.getElementById('detected-text'),
urlIcon: document.getElementById('url-icon'),
// Options
optionsContainer: document.getElementById('options-container'),
ytOptions: document.getElementById('youtube-options'),
codecSection: document.getElementById('codec-options-section'),
// YouTube Specifics
ytFormatRadios: document.querySelectorAll('input[name="yt_format"]'),
qualityWrapper: document.getElementById('quality-wrapper'),
mp3Select: document.getElementById('mp3_bitrate'),
mp4Select: document.getElementById('mp4_quality'),
// Codec Switch
codecSwitch: document.getElementById('codec-switch'),
codecPreference: document.getElementById('codec_preference'),
// Progress & Result
progressContainer: document.getElementById('progress-container'),
progressBar: document.getElementById('progress-bar'),
statusMessage: document.getElementById('status-message'),
logContent: document.getElementById('log-content'),
resultContainer: document.getElementById('result-container'),
resultUrl: document.getElementById('result-url'),
errorMessage: document.getElementById('error-message'),
// History
historyTableBody: document.querySelector('#history-table tbody'),
clearHistoryBtn: document.getElementById('clear-history-button'),
contextMenu: document.getElementById('context-menu')
};
const dom = {};
let pollingInterval = null;
let currentJobId = null;
let isPolling = false;
const historyEnabled = !!document.querySelector(selectors.clearHistoryButton);
const videoPlatforms = ['YouTube', 'TikTok', 'Instagram', 'Twitter'];
let pollingInterval = null;
const memeMessages = [
"Hacking the mainframe...",
"Route Gibson durch die Firewall...",
"Einen Moment, ich Binge gerade das Internet durch...",
"Lade 1.21 Gigawatt herunter...",
"Komprimiere die Daten... mit purer Willenskraft.",
"Die Bits und Bytes tanzen Cha-Cha-Cha.",
"Frage die NSA nach dem schnellsten Weg...",
"Polishing the pixels...",
"Die Leitung glüht, alles nach Plan!",
"Füttere den Hamster im Serverraum...",
"Kalibriere den Fluxkompensator...",
"Optimiere den Warp-Antrieb...",
// --- Plattform Definitionen (Regex & Farben) ---
const platforms = [
{ name: 'SoundCloud', pattern: /soundcloud\.com/, icon: 'fa-soundcloud', color: '#ff5500' },
{ name: 'YouTube', pattern: /(youtube\.com|youtu\.be)/, icon: 'fa-youtube', color: '#ff0000' },
{ name: 'TikTok', pattern: /tiktok\.com/, icon: 'fa-tiktok', color: '#000000' },
{ name: 'Instagram', pattern: /instagram\.com/, icon: 'fa-instagram', color: '#E1306C' },
{ name: 'Twitter', pattern: /(twitter\.com|x\.com)/, icon: 'fa-x-twitter', color: '#000000' }
];
// --- Initialisierung ---
// --- Init ---
function init() {
for (const key in selectors) {
dom[key] = document.querySelector(selectors[key]);
if (!dom[key] && !['copyResultUrlLink', 'clearHistoryButton', 'historyTableBody', 'contextMenu'].includes(key)) {
console.warn(`DOM-Element nicht gefunden: ${selectors[key]}`);
}
}
dom.platformRadios = document.querySelectorAll(selectors.platformRadios);
dom.ytFormatRadios = document.querySelectorAll(selectors.ytFormatRadios);
setupEventListeners();
updateDynamicOptionsVisibility();
if (historyEnabled) {
fetchHistory();
} else {
if(dom.historyTableBody) {
dom.historyTableBody.innerHTML = '<tr><td colspan="5" class="text-center text-muted">- Verlauf deaktiviert -</td></tr>';
}
}
resetUIState();
fetchStats();
console.log("Uploader UI initialisiert.");
loadHistory();
}
// --- Event Listener Setup ---
function setupEventListeners() {
if (dom.form) dom.form.addEventListener('submit', handleFormSubmit);
dom.platformRadios.forEach(radio => radio.addEventListener('change', updateDynamicOptionsVisibility));
dom.ytFormatRadios.forEach(radio => radio.addEventListener('change', updateYoutubeQualityVisibility));
if (dom.clearHistoryButton) dom.clearHistoryButton.addEventListener('click', handleClearHistory);
document.addEventListener('click', hideContextMenu);
if (dom.contextMenu) dom.contextMenu.addEventListener('click', handleContextMenuClick);
if (dom.historyTableBody && historyEnabled) {
dom.historyTableBody.addEventListener('contextmenu', (event) => {
const targetLink = event.target.closest('a[data-url]');
if (targetLink) showContextMenu(event, targetLink.dataset.url);
// Auto-Detect bei Eingabe
dom.urlInput.addEventListener('input', handleUrlInput);
// Form Submit
dom.form.addEventListener('submit', handleFormSubmit);
// YouTube Format Toggle (MP3/MP4)
dom.ytFormatRadios.forEach(radio => {
radio.addEventListener('change', updateYtQualityVisibility);
});
// Codec Switch Toggle
if(dom.codecSwitch) {
dom.codecSwitch.addEventListener('change', (e) => {
dom.codecPreference.value = e.target.checked ? 'h264' : 'original';
});
}
}
// --- UI Update Funktionen ---
function resetUIState() {
if (dom.submitButton) {
dom.submitButton.disabled = false;
dom.submitButton.innerHTML = '<i class="fas fa-cloud-upload-alt"></i> Download starten';
}
if (dom.errorMessage) dom.errorMessage.classList.add('d-none');
if (dom.resultUrlArea) dom.resultUrlArea.classList.add('d-none');
if (dom.logContent) dom.logContent.textContent = '';
if (dom.queueInfo) dom.queueInfo.classList.add('d-none');
updateAllStatusMessages('Bereit.');
updateAllProgressBars(0, false, false, false);
currentJobId = null;
isPolling = false;
hideProcessingOverlay();
stopPolling();
console.log("UI State reset.");
}
function setUIProcessing(isStarting = false) {
if (dom.submitButton) dom.submitButton.disabled = true;
if (dom.errorMessage) dom.errorMessage.classList.add('d-none');
updateAllStatusMessages('Sende Auftrag...');
if (isStarting) {
if (dom.resultUrlArea) dom.resultUrlArea.classList.add('d-none');
if (dom.logContent) dom.logContent.textContent = '';
updateAllProgressBars(0, false, false, false);
// History
if(dom.clearHistoryBtn) dom.clearHistoryBtn.addEventListener('click', clearHistory);
document.addEventListener('click', hideContextMenu);
if(dom.historyTableBody) {
dom.historyTableBody.addEventListener('contextmenu', handleHistoryRightClick);
dom.contextMenu.addEventListener('click', handleContextMenuClick);
}
}
// NEU: Funktion, die BEIDE Fortschrittsbalken aktualisiert
function updateAllProgressBars(value, isError = false, isRunning = false, isQueued = false) {
const percentage = Math.max(0, Math.min(100, Math.round(value)));
// Funktion zur Aktualisierung eines einzelnen Balkens
const updateSingleBar = (barElement) => {
if (!barElement) return;
barElement.style.width = `${percentage}%`;
barElement.textContent = `${percentage}%`;
// --- Logik: URL Erkennung ---
function handleUrlInput() {
const url = dom.urlInput.value.trim();
const detected = platforms.find(p => p.pattern.test(url));
if (detected) {
// UI Feedback
dom.platformInput.value = detected.name;
dom.detectedText.textContent = `${detected.name} erkannt`;
dom.detectedIcon.className = `fab ${detected.icon}`;
dom.detectedIcon.style.color = detected.color;
dom.badgeContainer.style.opacity = '1';
// Spezifische Klassen für den Haupt-Balken
if (barElement.id === 'progress-bar') {
barElement.setAttribute('aria-valuenow', percentage);
barElement.classList.remove('bg-success', 'bg-danger', 'bg-info', 'bg-secondary', 'progress-bar-animated', 'progress-bar-striped');
if (isError) barElement.classList.add('bg-danger');
else if (isQueued) barElement.classList.add('bg-secondary');
else if (percentage === 100 && !isRunning) barElement.classList.add('bg-success');
else if (isRunning) barElement.classList.add('bg-info', 'progress-bar-striped', 'progress-bar-animated');
else barElement.classList.add('bg-info');
}
// Icon im Input Feld färben
dom.urlIcon.className = `fab ${detected.icon}`;
dom.urlIcon.style.color = detected.color;
// Spezifische Klassen für den Overlay-Balken (hat eigene CSS-Stile)
if (barElement.id === 'overlay-progress-bar') {
barElement.classList.remove('progress-bar-striped', 'progress-bar-animated');
if (isRunning) {
barElement.classList.add('progress-bar-striped', 'progress-bar-animated');
}
}
};
updateSingleBar(dom.progressBar);
updateSingleBar(dom.overlayProgressBar);
}
// NEU: Funktion, die BEIDE Status-Nachrichten aktualisiert
function updateAllStatusMessages(message, position = null, totalQueued = null) {
let displayMessage = message || '...';
if (position !== null && totalQueued !== null) {
displayMessage = `${message} (Position ${position} von ${totalQueued})`;
}
if (dom.statusMessage) dom.statusMessage.textContent = displayMessage;
if (dom.overlayStatusText) dom.overlayStatusText.textContent = displayMessage;
}
function appendLog(message, type = 'log') {
if (!dom.logContent || !message) return;
const timestamp = new Date().toLocaleTimeString();
const logLine = `${timestamp} - ${message.trim()}\n`;
dom.logContent.textContent += logLine;
if (dom.logOutput) dom.logOutput.scrollTop = dom.logOutput.scrollHeight;
if (type === 'error') console.error(message);
else if (type === 'warn') console.warn(message);
}
function showError(message) {
if (!dom.errorMessage) return;
let displayMessage = message || "Unbekannter Fehler.";
// ... (Fehlertext-Logik bleibt unverändert)
const lowerCaseMessage = message ? message.toLowerCase() : "";
if (lowerCaseMessage.includes("login is required") || lowerCaseMessage.includes("age-restricted") || lowerCaseMessage.includes("instagramloginrequired") || lowerCaseMessage.includes("twitterloginrequired")) {
displayMessage = "Dieser Inhalt erfordert eine Anmeldung oder ist altersbeschränkt und kann nicht direkt heruntergeladen werden.";
} else if (lowerCaseMessage.includes("video unavailable")) {
displayMessage = "Fehler: Dieses Video ist nicht (mehr) verfügbar.";
} else if (lowerCaseMessage.includes("unsupported url")) {
displayMessage = "Fehler: Die eingegebene URL wird nicht unterstützt oder ist kein gültiger Link für die gewählte Plattform.";
} else if (lowerCaseMessage.includes("403") || lowerCaseMessage.includes("access denied")) {
displayMessage = "Fehler: Zugriff auf den Inhalt verweigert (403).";
} else if (lowerCaseMessage.includes("404") || lowerCaseMessage.includes("not found")) {
displayMessage = "Fehler: Inhalt nicht gefunden (404).";
} else if (lowerCaseMessage.includes("already processed")) {
displayMessage = message;
} else if (lowerCaseMessage.includes("worker-fehler") || lowerCaseMessage.includes("schwerer worker-fehler")) {
displayMessage = `Interner Serverfehler: ${message}`;
} else if (lowerCaseMessage.includes("job nicht gefunden")) {
displayMessage = message;
} else if (lowerCaseMessage.includes("ungültige url für instagram")) {
displayMessage = "Fehler: Ungültige URL für Instagram. Es werden nur Reel-Links unterstützt (z.B. .../reel/...).";
} else if (lowerCaseMessage.includes("ungültige url für twitter")) {
displayMessage = "Fehler: Ungültige URL für Twitter/X. Es werden nur Tweet-Links unterstützt (z.B. .../status/...).";
}
dom.errorMessage.textContent = displayMessage;
dom.errorMessage.classList.remove('d-none');
updateAllStatusMessages('Fehler!');
const currentProgress = dom.progressBar ? parseInt(dom.progressBar.getAttribute('aria-valuenow')) : 0;
updateAllProgressBars(currentProgress, true, false, false);
appendLog(`Fehler angezeigt: ${displayMessage}`, 'error');
}
function showResult(url) {
if (!dom.resultUrlArea || !dom.resultUrlLink || !url) return;
dom.resultUrlLink.href = url;
dom.resultUrlLink.textContent = url;
dom.resultUrlArea.classList.remove('d-none');
if (dom.errorMessage) dom.errorMessage.classList.add('d-none');
}
// --- Overlay Funktionen ---
function showProcessingOverlay() {
if (!dom.processingOverlay || !dom.overlayMessage) return;
const randomIndex = Math.floor(Math.random() * memeMessages.length);
dom.overlayMessage.textContent = memeMessages[randomIndex];
dom.processingOverlay.classList.add('visible');
}
function hideProcessingOverlay() {
if (!dom.processingOverlay) return;
dom.processingOverlay.classList.remove('visible');
}
// --- Dynamische Optionen (unverändert) ---
function updateDynamicOptionsVisibility() {
const selectedPlatform = document.querySelector('input[name="platform"]:checked')?.value;
if (!dom.ytOptionsDiv || !dom.codecOptionsSection || !dom.urlHelpText) return;
dom.ytOptionsDiv.classList.add('d-none');
dom.codecOptionsSection.classList.add('d-none');
if(dom.mp3QualitySection) dom.mp3QualitySection.classList.add('d-none');
if(dom.mp4QualitySection) dom.mp4QualitySection.classList.add('d-none');
if (selectedPlatform === 'Instagram' || selectedPlatform === 'Twitter') {
dom.urlHelpText.classList.remove('d-none');
// Optionen anzeigen
showOptionsForPlatform(detected.name);
} else {
dom.urlHelpText.classList.add('d-none');
}
if (selectedPlatform === 'YouTube') {
dom.ytOptionsDiv.classList.remove('d-none');
updateYoutubeQualityVisibility();
}
else if (videoPlatforms.includes(selectedPlatform) && selectedPlatform !== 'YouTube') {
dom.codecOptionsSection.classList.remove('d-none');
}
}
function updateYoutubeQualityVisibility() {
const selectedFormat = document.querySelector('input[name="yt_format"]:checked')?.value;
if (!dom.mp3QualitySection || !dom.mp4QualitySection || !dom.ytQualityDiv || !dom.codecOptionsSection) return;
dom.ytQualityDiv.classList.remove('d-none');
dom.mp3QualitySection.classList.add('d-none');
dom.mp4QualitySection.classList.add('d-none');
dom.codecOptionsSection.classList.add('d-none');
if (selectedFormat === 'mp3') {
dom.mp3QualitySection.classList.remove('d-none');
} else if (selectedFormat === 'mp4') {
dom.mp4QualitySection.classList.remove('d-none');
dom.codecOptionsSection.classList.remove('d-none');
// Reset wenn unbekannt oder leer
dom.badgeContainer.style.opacity = '0';
dom.urlIcon.className = 'fas fa-link text-muted';
dom.urlIcon.style.color = '';
hideAllOptions();
}
}
// --- Event Handler ---
async function handleFormSubmit(event) {
event.preventDefault();
if (isPolling) {
alert("Bitte warte, bis der aktuelle Auftrag abgeschlossen ist, bevor du einen neuen startest.");
return;
function showOptionsForPlatform(platformName) {
dom.optionsContainer.classList.add('show');
// Reset specific sections
dom.ytOptions.classList.add('d-none');
dom.codecSection.classList.add('d-none');
if (platformName === 'YouTube') {
dom.ytOptions.classList.remove('d-none');
} else if (['TikTok', 'Instagram', 'Twitter'].includes(platformName)) {
dom.codecSection.classList.remove('d-none');
} else {
// SoundCloud braucht keine extra Optionen in diesem Design (Default ist MP3 Best)
dom.optionsContainer.classList.remove('show');
}
resetUIState();
setUIProcessing(true);
showProcessingOverlay();
}
function hideAllOptions() {
dom.optionsContainer.classList.remove('show');
setTimeout(() => {
dom.ytOptions.classList.add('d-none');
dom.codecSection.classList.add('d-none');
}, 500); // Warten bis Animation fertig
}
function updateYtQualityVisibility() {
const format = document.querySelector('input[name="yt_format"]:checked').value;
if (format === 'mp3') {
dom.mp3Select.classList.remove('d-none');
dom.mp4Select.classList.add('d-none');
dom.codecSection.classList.add('d-none');
} else {
dom.mp3Select.classList.add('d-none');
dom.mp4Select.classList.remove('d-none');
dom.codecSection.classList.remove('d-none');
}
}
// --- Logik: Submit & Polling ---
async function handleFormSubmit(e) {
e.preventDefault();
// Validierung
if(dom.badgeContainer.style.opacity === '0' && dom.urlInput.value.length > 0) {
// Fallback für unbekannte URLs -> Standard SoundCloud probieren oder Warnen
dom.platformInput.value = "SoundCloud";
}
// UI Transition
dom.form.classList.add('d-none');
dom.progressContainer.classList.remove('d-none');
dom.resultContainer.classList.add('d-none');
dom.errorMessage.classList.add('d-none');
dom.logContent.textContent = "Verbindung wird hergestellt...";
dom.progressBar.style.width = "5%";
const formData = new FormData(dom.form);
try {
const response = await fetch('/start_download', { method: 'POST', body: formData });
const result = await response.json();
if (response.ok && result.job_id) {
currentJobId = result.job_id;
updateAllStatusMessages(result.message || 'Auftrag gesendet...');
appendLog(`Auftrag ${currentJobId} gestartet: ${result.message || ''}`, 'info');
startPolling();
} else {
throw new Error(result.error || `Serverfehler: ${response.status}`);
}
} catch (error) {
console.error('Fehler beim Starten des Downloads:', error);
showError(`Fehler beim Start: ${error.message}`);
resetUIState();
}
}
async function handleClearHistory() {
if (!dom.clearHistoryButton || !dom.historyTableBody) return;
if (confirm('Möchtest du wirklich den gesamten Verlauf löschen?')) {
dom.clearHistoryButton.disabled = true;
try {
const response = await fetch('/clear_history', { method: 'POST' });
if (response.ok) {
appendLog('Verlauf erfolgreich gelöscht.', 'info');
dom.historyTableBody.innerHTML = '<tr><td colspan="5" class="text-center text-muted">Verlauf wurde gelöscht.</td></tr>';
} else {
const result = await response.json(); throw new Error(result.error || 'Unbekannter Fehler.');
}
} catch (error) {
console.error('Fehler beim Löschen des Verlaufs:', error); showError(`Fehler beim Löschen: ${error.message}`);
} finally {
dom.clearHistoryButton.disabled = false;
showError(result.error || "Serverfehler beim Starten.");
}
} catch (err) {
showError(`Verbindungsfehler: ${err.message}`);
}
}
// --- Polling ---
function startPolling() {
if (!currentJobId) return;
stopPolling();
isPolling = true;
if (dom.submitButton) {
dom.submitButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Verarbeite...';
dom.submitButton.disabled = true;
}
pollingInterval = setInterval(fetchStatus, 2000);
fetchStatus();
console.log(`Polling gestartet für Job ${currentJobId}.`);
pollingInterval = setInterval(async () => {
try {
const res = await fetch(`/status?job_id=${currentJobId}`);
if (res.status === 404) { stopPolling(); showError("Job verloren gegangen."); return; }
const status = await res.json();
updateProgressUI(status);
if (status.status === 'completed') {
stopPolling();
showResult(status.result_url);
} else if (status.status === 'error' || status.error) {
stopPolling();
showError(status.error || status.message);
}
} catch (err) {
console.error(err);
}
}, 1500);
}
function stopPolling() {
if (pollingInterval) {
clearInterval(pollingInterval);
pollingInterval = null;
isPolling = false;
console.log("Polling gestoppt.");
clearInterval(pollingInterval);
}
function updateProgressUI(status) {
const pct = status.progress || 0;
dom.progressBar.style.width = `${pct}%`;
if(status.message) dom.statusMessage.textContent = status.message;
if(status.logs && status.logs.length > 0) {
dom.logContent.textContent = status.logs[status.logs.length - 1];
}
}
async function fetchStatus() {
if (!currentJobId || !isPolling) {
stopPolling();
function showResult(url) {
dom.progressContainer.classList.add('d-none');
dom.resultContainer.classList.remove('d-none');
dom.resultUrl.href = url;
loadHistory(); // Verlauf aktualisieren
}
function showError(msg) {
dom.progressContainer.classList.add('d-none');
dom.form.classList.remove('d-none'); // Form wieder zeigen
dom.errorMessage.textContent = msg;
dom.errorMessage.classList.remove('d-none');
}
// --- History Logic (Minimal) ---
async function loadHistory() {
if(!dom.historyTableBody) return;
try {
const res = await fetch('/history');
const data = await res.json();
renderHistory(data);
} catch(e) { console.error("History Error", e); }
}
function renderHistory(data) {
dom.historyTableBody.innerHTML = '';
if(!data || !data.length) {
dom.historyTableBody.innerHTML = '<tr><td colspan="4" class="text-center text-muted">Kein Verlauf vorhanden.</td></tr>';
return;
}
try {
const response = await fetch(`/status?job_id=${currentJobId}`);
if (response.status === 404) {
const status = await response.json();
showError(status.error || "Auftrag nicht gefunden (möglicherweise zu alt).");
stopPolling();
hideProcessingOverlay();
resetSubmitButton();
return;
}
if (!response.ok) throw new Error(`Status-Serverfehler: ${response.status}`);
const status = await response.json();
if (dom.logContent && Array.isArray(status.logs)) {
dom.logContent.textContent = status.logs.join('\n') + '\n';
if (dom.logOutput) dom.logOutput.scrollTop = dom.logOutput.scrollHeight;
}
const isRunning = status.running === true;
const isQueued = status.status === 'queued';
const isCompleted = status.status === 'completed';
const isError = !!status.error || status.status === 'error';
const isNotFound = status.status === 'not_found';
// Status und Fortschritt an beide UI-Teile senden
updateAllProgressBars(status.progress || 0, isError, isRunning, isQueued);
if (isQueued && status.position !== undefined && status.total_queued !== undefined) {
updateAllStatusMessages(status.message || 'In Warteschlange...', status.position, status.total_queued);
} else {
updateAllStatusMessages(status.message || '...');
}
if (isCompleted || isError || isNotFound) {
stopPolling();
hideProcessingOverlay();
if (isError) {
showError(status.error || status.message);
} else if (isCompleted) {
updateAllStatusMessages('Abgeschlossen!');
updateAllProgressBars(100, false, false, false);
if (status.result_url) showResult(status.result_url);
if (historyEnabled) fetchHistory();
fetchStats();
} else {
showError(status.error || status.message || "Auftrag beendet, aber Status unklar.");
}
resetSubmitButton();
} else if (isRunning || isQueued) {
if (dom.submitButton && !dom.submitButton.disabled) {
dom.submitButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Verarbeite...';
dom.submitButton.disabled = true;
}
}
} catch (error) {
console.error('Polling-Fehler:', error);
appendLog(`Polling fehlgeschlagen: ${error.message}`, 'error');
showError(`Polling-Fehler: ${error.message}.`);
stopPolling();
resetUIState();
}
}
function resetSubmitButton() {
if (dom.submitButton) {
dom.submitButton.disabled = false;
dom.submitButton.innerHTML = '<i class="fas fa-cloud-upload-alt"></i> Download starten';
}
}
// --- History (unverändert) ---
async function fetchHistory() {
if (!dom.historyTableBody || !historyEnabled) return;
dom.historyTableBody.innerHTML = '<tr><td colspan="5" class="text-center text-muted"><i class="fas fa-spinner fa-spin"></i> Lade Verlauf...</td></tr>';
try {
const response = await fetch('/history');
if (!response.ok) throw new Error(`Serverfehler History: ${response.status}`);
const history = await response.json();
renderHistory(history);
} catch (error) {
console.error('Fehler Laden Verlauf:', error);
dom.historyTableBody.innerHTML = `<tr><td colspan="5" class="text-center text-danger">Fehler Laden Verlauf: ${error.message}</td></tr>`;
}
}
function renderHistory(historyData) {
if (!dom.historyTableBody || !historyEnabled) return;
dom.historyTableBody.innerHTML = '';
if (!historyData || !Array.isArray(historyData) || historyData.length === 0) {
dom.historyTableBody.innerHTML = '<tr><td colspan="5" class="text-center text-muted">Keine Einträge im Verlauf.</td></tr>'; return;
}
historyData.forEach(entry => {
const row = dom.historyTableBody.insertRow();
row.insertCell().textContent = entry.timestamp || '';
const platformCell = row.insertCell(); platformCell.classList.add('text-center');
let platformIcon = '<i class="fas fa-question-circle text-muted" title="Unbekannt"></i>';
const platform = entry.platform || 'Unbekannt';
if (platform === 'SoundCloud') platformIcon = '<i class="fab fa-soundcloud text-warning" title="SoundCloud"></i>';
else if (platform === 'YouTube') platformIcon = '<i class="fab fa-youtube text-danger" title="YouTube"></i>';
else if (platform === 'TikTok') platformIcon = '<i class="fab fa-tiktok" title="TikTok"></i>';
else if (platform === 'Instagram') platformIcon = '<i class="fab fa-instagram" title="Instagram Reel"></i>';
else if (platform === 'Twitter') platformIcon = '<i class="fab fa-x-twitter" title="Twitter/X"></i>';
platformCell.innerHTML = platformIcon;
const titleCell = row.insertCell(); titleCell.textContent = entry.title || 'N/A'; titleCell.title = entry.title || '';
createLinkCell(row.insertCell(), entry['source_url']);
createLinkCell(row.insertCell(), entry['s3_url']);
data.forEach(item => {
const row = document.createElement('tr');
row.innerHTML = `
<td class="small text-muted">${item.timestamp.split(' ')[1]}</td>
<td class="text-center"><i class="fab fa-${getIconForPlatform(item.platform)}"></i></td>
<td class="text-truncate" style="max-width: 150px;" title="${item.title}">${item.title}</td>
<td class="text-end"><a href="${item.s3_url}" target="_blank" class="btn btn-sm btn-light border" data-url="${item.s3_url}"><i class="fas fa-download"></i></a></td>
`;
dom.historyTableBody.appendChild(row);
});
}
function createLinkCell(cell, url) {
if (!cell || !url) { cell.textContent = url || 'N/A'; return; }
const link = document.createElement('a'); link.href = url;
link.textContent = url.length > 50 ? url.substring(0, 47) + '...' : url;
link.title = url; link.target = "_blank"; link.rel = "noopener noreferrer";
link.dataset.url = url;
cell.appendChild(link);
function getIconForPlatform(p) {
if(p === 'SoundCloud') return 'soundcloud text-warning';
if(p === 'YouTube') return 'youtube text-danger';
if(p === 'TikTok') return 'tiktok';
if(p === 'Instagram') return 'instagram text-danger';
if(p === 'Twitter') return 'x-twitter';
return 'question';
}
// --- Kontextmenü (unverändert) ---
let currentContextMenuUrl = null;
function showContextMenu(event, url) {
event.preventDefault();
if (!dom.contextMenu || !url) return;
currentContextMenuUrl = url;
dom.contextMenu.style.top = `${event.clientY}px`;
dom.contextMenu.style.left = `${event.clientX}px`;
dom.contextMenu.classList.remove('d-none');
async function clearHistory() {
if(confirm("Verlauf wirklich löschen?")) {
await fetch('/clear_history', {method: 'POST'});
loadHistory();
}
}
// Context Menu Logic
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);
dom.contextMenu.classList.add('d-none');
}
}
function hideContextMenu() {
if (dom.contextMenu) dom.contextMenu.classList.add('d-none');
currentContextMenuUrl = null;
}
function handleContextMenuClick(event) {
const action = event.target.closest('[data-action]')?.dataset.action;
if (action && currentContextMenuUrl) {
if (action === 'open') window.open(currentContextMenuUrl, '_blank');
else if (action === 'copy') { copyToClipboard(currentContextMenuUrl); appendLog("URL aus History kopiert.", "info"); }
}
hideContextMenu();
if(dom.contextMenu) dom.contextMenu.classList.add('d-none');
}
// --- Statistik (unverändert) ---
async function fetchStats() {
if (!dom.statsTotalJobs || !dom.statsAvgDuration || !dom.statsTotalSize) return;
try {
const response = await fetch('/stats');
if (!response.ok) throw new Error(`Statistik-Serverfehler: ${response.status}`);
const stats = await response.json();
dom.statsTotalJobs.textContent = stats.total_jobs ?? 'N/A';
dom.statsAvgDuration.textContent = stats.average_duration_seconds ?? 'N/A';
dom.statsTotalSize.textContent = stats.total_size_formatted ?? 'N/A';
} catch (error) {
console.error('Fehler Laden Statistiken:', error);
}
}
// --- Hilfsfunktionen (unverändert) ---
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(() => appendLog("Link kopiert.", "info"))
.catch(err => { console.error('Async Copy failed:', err); alert("Kopieren fehlgeschlagen."); });
}
// --- Start ---
init();
}); // Ende DOMContentLoaded
});
+143 -205
View File
@@ -1,242 +1,180 @@
/* Custom Styles (Ergänzung zu Bootstrap) */
html {
height: 100%; /* Wichtig für min-height im body */
/* Modern Reset & Base */
:root {
--primary-color: #6366f1; /* Indigo */
--secondary-color: #a855f7; /* Purple */
--bg-dark: #0f172a;
--text-main: #1e293b;
}
body {
background-color: #e9ecef; /* Hellerer Hintergrund */
display: flex; /* Flexbox aktivieren */
flex-direction: column; /* Hauptachse vertikal */
min-height: 100vh; /* Mindesthöhe des Viewports */
font-family: 'Inter', sans-serif;
background-color: #f1f5f9;
color: var(--text-main);
overflow-x: hidden;
}
/* Hauptinhalts-Container soll wachsen */
.main-content {
flex: 1 0 auto; /* flex-grow: 1, flex-shrink: 0, flex-basis: auto */
/* Kein margin-bottom hier, der Footer kümmert sich um den Abstand */
/* Animated Background Shapes */
.bg-shape {
position: fixed;
border-radius: 50%;
filter: blur(80px);
z-index: -1;
opacity: 0.6;
animation: float 10s infinite ease-in-out;
}
/* Footer soll nicht schrumpfen und hat eigenen Abstand/Styling */
.page-footer {
flex-shrink: 0; /* Verhindert, dass der Footer schrumpft */
background-color: #f8f9fa; /* Optional: Leichter Hintergrund für Footer */
/* mt-auto im HTML sorgt für den Push nach unten */
/* padding-top und padding-bottom im HTML oder hier definieren */
.shape-1 {
top: -10%;
left: -10%;
width: 600px;
height: 600px;
background: linear-gradient(to right, var(--primary-color), var(--secondary-color));
}
.page-footer hr {
margin-top: 0; /* Abstand der Linie anpassen */
margin-bottom: 1rem;
.shape-2 {
bottom: -10%;
right: -10%;
width: 500px;
height: 500px;
background: linear-gradient(to left, #3b82f6, #06b6d4);
animation-delay: -5s;
}
.page-footer p {
margin-bottom: 0.5rem;
@keyframes float {
0%, 100% { transform: translate(0, 0); }
50% { transform: translate(30px, 20px); }
}
.page-footer i {
margin-right: 5px;
/* Hero Card */
.hero-card {
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.5);
border-radius: 24px;
padding: 3rem;
width: 100%;
max-width: 650px;
box-shadow: 0 20px 40px -10px rgba(0, 0, 0, 0.1);
}
.page-footer strong {
color: #343a40; /* Etwas dunklerer Text für Werte */
.text-gradient {
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
/* --- Restliche Styles --- */
/* Verbessere Lesbarkeit im Log */
#log-output {
max-height: 250px;
overflow-y: auto; /* Vertikales Scrollen bei Bedarf */
overflow-x: auto; /* NEU: Horizontales Scrollen bei Bedarf (Fallback) */
background-color: #f8f8f8;
border: 1px solid #ddd;
padding: 10px;
border-radius: 3px;
font-size: 0.85rem;
/* Input Styling */
.main-input-group {
border-radius: 16px;
overflow: hidden;
transition: transform 0.2s, box-shadow 0.2s;
}
#log-content {
white-space: pre-wrap; /* Wichtig: Behält Zeilenumbrüche bei UND erlaubt Umbruch langer Zeilen */
overflow-wrap: break-word;/* Wichtig: Bricht lange Wörter/URLs um, um Überlauf zu verhindern */
word-wrap: break-word; /* Älterer Alias für overflow-wrap */
margin: 0;
font-family: monospace;
/* Entferne explizite Breiten- oder Overflow-Regeln hier, der Container steuert das */
.main-input-group:focus-within {
transform: translateY(-2px);
box-shadow: 0 10px 25px -5px rgba(99, 102, 241, 0.3) !important;
}
/* Tabelle responsiver machen */
.table-responsive {
max-height: 500px; /* Höhe begrenzen */
.main-input-group input {
font-size: 1.1rem;
background: white;
}
.main-input-group input:focus {
box-shadow: none;
background: white;
}
#history-table th,
#history-table td {
vertical-align: middle; /* Vertikal zentrieren */
font-size: 0.85rem;
word-break: break-all;
#submit-button {
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
border: none;
transition: filter 0.2s;
}
#submit-button:hover {
filter: brightness(110%);
}
#history-table th:nth-child(2), /* Plattform Icon Spalte */
#history-table td:nth-child(2) {
text-align: center;
width: 40px; /* Feste Breite für Icon */
/* Options Animation */
.options-wrapper {
overflow: hidden;
max-height: 0;
opacity: 0;
transition: max-height 0.5s ease-in-out, opacity 0.4s ease-in-out;
}
#history-table td a {
/* Bootstrap übernimmt Link-Styling, Cursor wird per JS gesetzt */
cursor: pointer;
.options-wrapper.show {
max-height: 500px; /* Groß genug */
opacity: 1;
}
/* Kontextmenü (Bootstrap-ähnlich) */
.animate-options {
animation: fadeIn 0.5s ease;
}
/* Glass Button */
.btn-white-glass {
background: rgba(255, 255, 255, 0.5);
border: 1px solid rgba(255, 255, 255, 0.2);
backdrop-filter: blur(5px);
color: var(--text-main);
transition: all 0.2s;
}
.btn-white-glass:hover {
background: white;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0,0,0,0.05);
}
/* Progress Bar */
.bg-gradient-primary {
background: linear-gradient(90deg, var(--primary-color), #06b6d4);
background-size: 200% 100%;
animation: gradientMove 2s linear infinite;
}
@keyframes gradientMove {
0% { background-position: 100% 0; }
100% { background-position: -100% 0; }
}
.animate-pulse {
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* Helpers */
.hover-lift:hover {
transform: translateY(-3px);
}
.fade-in-up {
animation: fadeInUp 0.8s ease-out;
}
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.extra-small {
font-size: 0.75rem;
}
/* Context Menu */
.context-menu {
z-index: 1050; /* Über anderen Elementen */
z-index: 9999;
min-width: 150px;
border-radius: 8px;
overflow: hidden;
border: none;
}
.context-menu .list-group-item {
cursor: pointer;
font-size: 0.9rem;
border: none;
}
.context-menu .list-group-item i {
width: 1.2em; /* Platz für Icons */
}
/* Versteckte Elemente */
.d-none {
display: none !important; /* Wichtig, um Bootstrap zu überschreiben, falls nötig */
}
/* Fortschrittsbalken Text zentrieren */
.progress {
position: relative;
}
.progress-bar {
/* Textfarbe anpassen, falls nötig */
/* color: #212529; */
font-weight: bold;
display: flex; /* Ermöglicht Zentrierung */
justify-content: center;
align-items: center;
overflow: hidden; /* Verhindert, dass Text überläuft */
}
/* Optional: Fehlerhafter Fortschrittsbalken */
.progress-bar.bg-danger {
color: white;
}
/* Kleinere Anpassungen für Radio-Buttons */
.form-check-label i {
margin-right: 4px;
width: 1em; /* Platz für Icon */
}
/* Anpassung für Comboboxen */
#youtube-quality .form-select-sm {
max-width: 180px; /* Breite begrenzen */
display: inline-block; /* Nebeneinander mit Label */
width: auto; /* Automatische Breite */
}
#youtube-quality label {
margin-right: 5px;
}
#mp3-quality-section, #mp4-quality-section {
margin-right: 15px; /* Abstand zwischen Qualitätsoptionen */
}
/* Plattform Icons im Formular */
.form-check-label i.fa-soundcloud { color: #ff5500; }
.form-check-label i.fa-youtube { color: #ff0000; }
.form-check-label i.fa-tiktok { color: #000000; } /* Oder #fe2c55, #00f2ea */
.form-check-label i.fa-instagram { color: #E1306C; } /* Instagram Pink */
.form-check-label i.fa-x-twitter { color: #000000; } /* Twitter/X Schwarz */
.form-check-label i.fa-twitter { color: #1DA1F2; } /* Altes Twitter Blau (Fallback) */
/* Plattform Icons in der History Tabelle */
#history-table td i.fa-soundcloud { color: #ff5500; }
#history-table td i.fa-youtube { color: #ff0000; }
#history-table td i.fa-tiktok { color: #000000; } /* Oder #fe2c55, #00f2ea */
#history-table td i.fa-instagram { color: #E1306C; } /* Instagram Pink */
#history-table td i.fa-x-twitter { color: #000000; } /* Twitter/X Schwarz */
#history-table td i.fa-twitter { color: #1DA1F2; } /* Altes Twitter Blau (Fallback) */
/* Hilfetext für URL-Eingabe */
#urlHelp {
font-size: 0.8rem;
}
/* --- Fullscreen Processing Overlay --- */
#processing-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: rgba(10, 25, 47, 0.85); /* Dunkelblauer, transparenter Hintergrund */
backdrop-filter: blur(5px); /* Hintergrund unscharf machen */
-webkit-backdrop-filter: blur(5px); /* Für Safari-Kompatibilität */
z-index: 2000; /* Über allem anderen */
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: #64ffda; /* Cyan/Türkis für den Text */
font-family: 'Courier New', Courier, monospace;
text-align: center;
opacity: 0;
visibility: hidden;
transition: opacity 0.4s ease, visibility 0.4s ease;
}
#processing-overlay.visible {
opacity: 1;
visibility: visible;
}
#processing-overlay img {
max-width: 90%;
width: 350px;
height: auto;
border-radius: 8px;
box-shadow: 0 0 25px rgba(100, 255, 218, 0.5); /* Passender Leuchteffekt */
margin-bottom: 20px;
}
#processing-overlay #overlay-message {
font-size: 1.5rem;
text-shadow: 0 0 10px #64ffda; /* Leuchteffekt für Text */
padding: 0 20px;
margin-bottom: 25px; /* Mehr Abstand nach unten */
}
/* NEU: Stile für den Status-Container im Overlay */
.overlay-status-container {
width: 80%;
max-width: 600px;
}
#overlay-status-text {
font-size: 1rem;
color: #ccd6f6; /* Heller, aber nicht so grell wie die Hauptfarbe */
margin-bottom: 10px;
min-height: 1.2em; /* Verhindert Springen bei Textänderung */
word-wrap: break-word;
}
/* NEU: Stile für den Fortschrittsbalken im Overlay */
#processing-overlay .progress {
height: 20px;
background-color: rgba(100, 255, 218, 0.1); /* Hintergrund des Balkens */
border: 1px solid rgba(100, 255, 218, 0.3);
border-radius: 5px;
padding: 2px;
}
#processing-overlay .progress-bar {
background-color: #64ffda !important; /* Wichtig, um Bootstrap zu überschreiben */
color: #0a192f; /* Dunkler Text für Kontrast */
font-weight: bold;
transition: width 0.3s ease-in-out; /* Weicherer Übergang */
.context-menu .list-group-item:hover {
background-color: #f3f4f6;
}