From a1f7c66f672a06e8481da33d66738c2811c53b9e Mon Sep 17 00:00:00 2001 From: M_Viper Date: Tue, 24 Mar 2026 21:38:23 +0100 Subject: [PATCH] Update from Git Manager GUI --- renderer/index.html | 117 ++++++- renderer/renderer.js | 815 ++++++++++++++++++++++++++++++++++++++++++- renderer/style.css | 463 ++++++++++++++++++++++++ 3 files changed, 1383 insertions(+), 12 deletions(-) diff --git a/renderer/index.html b/renderer/index.html index 85b8f39..133935e 100644 --- a/renderer/index.html +++ b/renderer/index.html @@ -198,26 +198,42 @@ -
+
-

System

-

Verhalten beim Windows-Start steuern.

+

πŸ’½ Lokale Backups

+

Automatische lokale Backups in einen Zielordner.

+
-
+ +
+
@@ -239,6 +255,27 @@
+
+
+
+

System

+

Verhalten beim Windows-Start steuern.

+
+
+
+ +
+
+
@@ -465,6 +502,72 @@
+ + + \ No newline at end of file diff --git a/renderer/renderer.js b/renderer/renderer.js index e6ab1fa..8bcd012 100644 --- a/renderer/renderer.js +++ b/renderer/renderer.js @@ -3,6 +3,8 @@ const $ = id => document.getElementById(id); let selectedFolder = null; let giteaCache = {}; +let currentLocalProjects = []; +let backupGitRepos = []; /* ================================================ FAVORITEN & ZULETZT GEΓ–FFNET β€” State & Helpers @@ -1415,6 +1417,132 @@ function ensureProgressUI() { document.body.appendChild(container); } +function ensureBackupRunOverlay() { + if ($('backupRunOverlay')) return; + + const overlay = document.createElement('div'); + overlay.id = 'backupRunOverlay'; + overlay.style.cssText = ` + position: fixed; + inset: 0; + z-index: 10001; + background: rgba(5, 10, 18, 0.58); + backdrop-filter: blur(4px); + display: none; + align-items: center; + justify-content: center; + padding: 16px; + `; + + const card = document.createElement('div'); + card.style.cssText = ` + width: min(520px, 96vw); + background: linear-gradient(180deg, rgba(16, 26, 44, 0.96), rgba(12, 20, 34, 0.96)); + border: 1px solid rgba(88, 213, 255, 0.26); + border-radius: 12px; + box-shadow: 0 20px 50px rgba(0, 0, 0, 0.45); + padding: 18px 18px 16px; + color: #fff; + `; + + const header = document.createElement('div'); + header.style.cssText = 'display:flex;align-items:center;gap:10px;margin-bottom:10px;font-weight:700;font-size:14px;color:#9de8ff;'; + + const spinner = document.createElement('div'); + spinner.style.cssText = ` + width: 16px; + height: 16px; + border: 2px solid rgba(88, 213, 255, 0.25); + border-top-color: #58d5ff; + border-radius: 50%; + animation: backupSpin 0.9s linear infinite; + `; + + const title = document.createElement('span'); + title.textContent = 'Backup laeuft... bitte warten'; + header.appendChild(spinner); + header.appendChild(title); + + const detail = document.createElement('div'); + detail.id = 'backupRunOverlayText'; + detail.style.cssText = 'font-size:13px;color:#d8eaff;margin-bottom:10px;min-height:20px;'; + detail.textContent = 'Initialisiere Backup...'; + + const percent = document.createElement('div'); + percent.id = 'backupRunOverlayPercent'; + percent.style.cssText = 'font-size:12px;color:#9fc6dc;margin-bottom:8px;text-align:right;'; + percent.textContent = '0%'; + + const barWrap = document.createElement('div'); + barWrap.style.cssText = 'height:10px;background:rgba(255,255,255,0.10);border-radius:999px;overflow:hidden;'; + + const bar = document.createElement('div'); + bar.id = 'backupRunOverlayBar'; + bar.style.cssText = 'height:100%;width:0%;background:linear-gradient(90deg, #58d5ff, #5c87ff);transition:width 200ms ease-out;'; + barWrap.appendChild(bar); + + card.appendChild(header); + card.appendChild(detail); + card.appendChild(percent); + card.appendChild(barWrap); + overlay.appendChild(card); + document.body.appendChild(overlay); + + if (!$('backupOverlayAnimationStyle')) { + const style = document.createElement('style'); + style.id = 'backupOverlayAnimationStyle'; + style.textContent = '@keyframes backupSpin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }'; + document.head.appendChild(style); + } +} + +function mapBackupProgressText(rawText, percentValue) { + const text = String(rawText || '').toLowerCase(); + const percent = Math.min(100, Math.max(0, Math.round(Number(percentValue) || 0))); + + if (percent >= 100) return 'Finalisiere Backup...'; + if (text.includes('upload')) return 'Upload laeuft...'; + if (text.includes('download')) return 'Dateien werden heruntergeladen...'; + if (text.includes('git')) return 'Git-Projekte werden gesammelt...'; + if (text.includes('erstelle backup') || text.includes('backup wird erstellt')) return 'Backup wird erstellt...'; + if (percent <= 8) return 'Vorbereitung laeuft...'; + if (percent <= 35) return 'Dateien werden gesammelt...'; + if (percent <= 85) return 'Daten werden verarbeitet...'; + return 'Finalisiere Backup...'; +} + +function showBackupRunOverlay(text) { + ensureBackupRunOverlay(); + const overlay = $('backupRunOverlay'); + const detail = $('backupRunOverlayText'); + const percent = $('backupRunOverlayPercent'); + const bar = $('backupRunOverlayBar'); + if (detail) detail.textContent = text || 'Backup wird vorbereitet...'; + if (percent) percent.textContent = '0%'; + if (bar) bar.style.width = '0%'; + if (overlay) overlay.style.display = 'flex'; +} + +function updateBackupRunOverlay(percentValue, text) { + const overlay = $('backupRunOverlay'); + if (!overlay || overlay.style.display === 'none') return; + const clamped = Math.min(100, Math.max(0, Math.round(Number(percentValue) || 0))); + const detail = $('backupRunOverlayText'); + const percent = $('backupRunOverlayPercent'); + const bar = $('backupRunOverlayBar'); + if (detail) detail.textContent = mapBackupProgressText(text, clamped); + if (percent) percent.textContent = `${clamped}%`; + if (bar) bar.style.width = `${clamped}%`; +} + +function hideBackupRunOverlay() { + const overlay = $('backupRunOverlay'); + if (!overlay) return; + setTimeout(() => { + overlay.style.display = 'none'; + }, 250); +} + function showProgress(percent, text) { ensureProgressUI(); const container = $('folderProgressContainer'); @@ -1423,6 +1551,7 @@ function showProgress(percent, text) { if (txt) txt.innerText = text || ''; if (bar) bar.style.width = `${Math.min(100, Math.max(0, percent))}%`; if (container) container.style.display = 'block'; + updateBackupRunOverlay(percent, text); } function hideProgress() { @@ -2730,6 +2859,11 @@ async function refreshLocalTree(folder) { exclude: ['node_modules', '.git'], maxDepth: 5 }); + + currentLocalProjects = Array.isArray(res.tree) + ? res.tree.filter(node => node && node.isDirectory) + : []; + populateBackupSourceOptions(); const grid = $('explorerGrid'); if (!grid) return; @@ -2741,6 +2875,8 @@ async function refreshLocalTree(folder) { } if (!res.tree || res.tree.length === 0) { + currentLocalProjects = []; + populateBackupSourceOptions(); grid.innerHTML = '
Keine Dateien gefunden
'; return; } @@ -2805,9 +2941,17 @@ async function pushLocalFolder() { const branch = $('branchSelect')?.value || 'main'; const repoName = $('repoName')?.value; const platform = $('platform')?.value; + const savedCreds = await window.electronAPI.loadCredentials(); + const autoBackup = Boolean(savedCreds && savedCreds.autoBackupEnabled); + const backupTarget = String((savedCreds && savedCreds.backupPrefLocalFolder) || '').trim(); setStatus('Pushing...'); - showProgress(0, 'Starting push...'); + if (autoBackup) { + showBackupRunOverlay('Auto-Backup wird erstellt...'); + showProgress(0, 'Auto-Backup wird erstellt...'); + } else { + showProgress(0, 'Starting push...'); + } try { const res = await window.electronAPI.pushProject({ @@ -2815,19 +2959,26 @@ async function pushLocalFolder() { branch, repoName, platform, - commitMessage: message + commitMessage: message, + autoBackup, + backupTarget }); if (res.ok) { - setStatus('Push succeeded'); + if (autoBackup) { + showSuccess('Vorab-Backup OK, Upload erfolgreich βœ“'); + } else { + showSuccess('Upload erfolgreich βœ“'); + } } else { - showError('Push failed: ' + (res.error || 'Unknown error')); + showError('Upload fehlgeschlagen: ' + (res.error || 'Unbekannter Fehler')); } } catch (error) { console.error('Push error:', error); - showError('Push failed'); + showError('Upload fehlgeschlagen'); } finally { hideProgress(); + hideBackupRunOverlay(); } } @@ -3492,6 +3643,47 @@ function showInputModal({ title, label, defaultValue, confirmText, onConfirm }) modal.onclick = (e) => { if (e.target === modal) modal.remove(); }; } +function showActionConfirmModal({ title, message, confirmText = 'BestΓ€tigen', danger = false }) { + return new Promise((resolve) => { + const modal = document.createElement('div'); + modal.className = 'modal confirm-modal'; + modal.style.zIndex = '99999'; + modal.innerHTML = ` +
+

${danger ? 'πŸ—‘οΈ' : 'ℹ️'} ${escapeHtml(title || 'BestΓ€tigung')}

+

${escapeHtml(message || '')}

+ +
+ `; + + document.body.appendChild(modal); + + const okBtn = modal.querySelector('#actionConfirmOk'); + const cancelBtn = modal.querySelector('#actionConfirmCancel'); + if (okBtn) okBtn.focus(); + + const closeWith = (result) => { + modal.remove(); + resolve(result); + }; + + if (okBtn) okBtn.onclick = () => closeWith(true); + if (cancelBtn) cancelBtn.onclick = () => closeWith(false); + + modal.addEventListener('keydown', (e) => { + if (e.key === 'Escape') closeWith(false); + if (e.key === 'Enter') closeWith(true); + }); + + modal.onclick = (e) => { + if (e.target === modal) closeWith(false); + }; + }); +} + /* ------------------------- HELPER FUNCTIONS ------------------------- */ @@ -4400,6 +4592,30 @@ window.addEventListener('DOMContentLoaded', async () => { showProgress(p.percent, `Download: ${p.processed}/${p.total}`); }); + if (window.electronAPI.onPrePushBackupStatus) { + window.electronAPI.onPrePushBackupStatus((payload) => { + if (!payload || !payload.stage) return; + + if (payload.stage === 'backup-start') { + setStatus('Auto-Backup wird erstellt...'); + showBackupRunOverlay('Auto-Backup wird erstellt...'); + showProgress(8, 'Auto-Backup wird erstellt...'); + } else if (payload.stage === 'backup-done') { + setStatus('Vorab-Backup erstellt. Upload startet...'); + showProgress(22, 'Vorab-Backup erstellt. Upload startet...'); + showInfo('Vorab-Backup OK, Upload startet...'); + } else if (payload.stage === 'upload-start') { + setStatus('Upload lΓ€uft...'); + showProgress(30, 'Upload lΓ€uft...'); + } else if (payload.stage === 'backup-failed') { + const errorMsg = payload.error ? `Auto-Backup fehlgeschlagen: ${payload.error}` : 'Auto-Backup fehlgeschlagen.'; + setStatus('Auto-Backup fehlgeschlagen'); + showError(errorMsg); + hideBackupRunOverlay(); + } + }); + } + if (window.electronAPI.onRetryQueueUpdated) { window.electronAPI.onRetryQueueUpdated((payload) => { const size = payload && typeof payload.size === 'number' ? payload.size : 0; @@ -4431,7 +4647,596 @@ window.addEventListener('DOMContentLoaded', async () => { setStatus('Ready'); initUpdater(); // Updater initialisieren updateNavigationUI(); + + // ═══════════════════════════════════════════════════════════ + // βœ… BACKUP MANAGEMENT MODAL - EVENT HANDLERS + // ═══════════════════════════════════════════════════════════ + + // Button zum Γ–ffnen des Backup-Modals + if ($('btnOpenBackupManagement')) { + $('btnOpenBackupManagement').onclick = async () => { + const modal = $('backupManagementModal'); + if (modal) modal.classList.remove('hidden'); + await loadBackupUiPrefs(); + await ensureBackupSourcesLoaded(); + populateBackupSourceOptions(); + const repoName = getActiveBackupRepoName(); + if (repoName) { + setStatus(`Backup-Verwaltung: ${repoName}`); + } + await refreshBackupAuthStatus(); + loadBackupList(); + }; + } + + // Schließen-Button im Modal + if ($('btnCloseBackupModal')) { + $('btnCloseBackupModal').onclick = () => { + const modal = $('backupManagementModal'); + if (modal) modal.classList.add('hidden'); + }; + } + + const localCredentialsSection = $('localCredentials'); + if (localCredentialsSection) { + localCredentialsSection.style.display = 'flex'; + } + + // πŸš€ CREATE BACKUP NOW - Button + if ($('btnCreateBackupNow')) { + $('btnCreateBackupNow').onclick = async () => { + const source = getSelectedBackupSource(); + const repoName = source?.repoName || getActiveBackupRepoName(); + if (!repoName) { + showWarning('Kein Repository erkannt. Bitte zuerst ein Repository ΓΆffnen oder einen Projektordner wΓ€hlen.'); + return; + } + + const provider = 'local'; + + const closeBackupModalForRun = () => { + const modal = $('backupManagementModal'); + if (modal) modal.classList.add('hidden'); + }; + + if (provider === 'local' && source && (source.kind === 'gitea-repo' || source.kind === 'gitea-all')) { + const destination = $('localBackupFolder')?.value?.trim() || ''; + if (!destination) { + showWarning('Bitte zuerst den Backup-Zielordner auswΓ€hlen.'); + return; + } + + closeBackupModalForRun(); + setStatus('Lade Projekte von Git und sichere lokal...'); + const btn = $('btnCreateBackupNow'); + const oldText = btn.textContent; + btn.disabled = true; + showBackupRunOverlay('Lade Projekte von Git und sichere lokal...'); + showProgress(0, 'Lade Projekte von Git...'); + + try { + const res = await window.electronAPI.exportGiteaProjectsToLocal( + source.kind === 'gitea-all' + ? { mode: 'all', destination } + : { mode: 'single', owner: source.owner, repo: source.repo, destination } + ); + + if (res?.ok) { + updateBackupRunOverlay(100, 'Finalisiere Backup...'); + const reposCount = res.repositoryCount || (Array.isArray(res.repositories) ? res.repositories.length : 0); + showSuccess(`βœ“ Git-Backup abgeschlossen: ${reposCount} Projekte in ${destination}`); + setStatus('Komplett-Backup abgeschlossen'); + } else { + showError('Komplett-Backup fehlgeschlagen: ' + (res?.error || 'Unbekannter Fehler')); + } + } catch (error) { + showError('Komplett-Backup fehlgeschlagen: ' + (error?.message || String(error))); + } finally { + hideProgress(); + hideBackupRunOverlay(); + btn.disabled = false; + btn.textContent = oldText; + } + return; + } + + const sourcePath = source?.projectPath || selectedFolder || ''; + if (!sourcePath) { + showWarning('Bitte Backup-Quelle aus der Liste auswΓ€hlen.'); + return; + } + + closeBackupModalForRun(); + setStatus('Erstelle Backup...'); + const btn = $('btnCreateBackupNow'); + const oldText = btn.textContent; + btn.disabled = true; + showBackupRunOverlay('Lokales Backup wird erstellt...'); + showProgress(0, 'Erstelle Backup...'); + + try { + const res = await window.electronAPI.createCloudBackup({ + repoName, + projectPath: sourcePath + }); + + if (res?.ok) { + updateBackupRunOverlay(100, 'Finalisiere Backup...'); + showSuccess('βœ“ Backup erfolgreich erstellt!'); + setStatus('Backup abgeschlossen'); + // Lade Backup-Liste neu + setTimeout(loadBackupList, 500); + } else { + showError('Backup fehlgeschlagen: ' + (res?.error || 'Unbekannter Fehler')); + } + } catch (error) { + showError('Fehler: ' + (error?.message || String(error))); + } finally { + hideProgress(); + hideBackupRunOverlay(); + btn.disabled = false; + btn.textContent = oldText; + } + }; + } + + // πŸ”„ REFRESH BACKUP LIST - Button + if ($('btnRefreshBackupsList')) { + $('btnRefreshBackupsList').onclick = loadBackupList; + } + + if ($('backupSourceSelect')) { + $('backupSourceSelect').addEventListener('change', async () => { + const key = $('backupSourceSelect')?.value || ''; + if (key) { + await saveBackupUiPrefs({ backupPrefSourceSelection: key }); + } + await refreshBackupAuthStatus(); + loadBackupList(); + }); + } + + if ($('btnPickLocalBackupFolder')) { + $('btnPickLocalBackupFolder').onclick = async () => { + try { + const folder = await window.electronAPI.selectFolder(); + if (!folder) return; + const input = $('localBackupFolder'); + if (input) input.value = folder; + await saveBackupUiPrefs({ backupPrefLocalFolder: folder }); + await ensureBackupSourcesLoaded(); + populateBackupSourceOptions(); + await refreshBackupAuthStatus(); + await loadBackupList(); + } catch (error) { + showError('Ordnerauswahl fehlgeschlagen: ' + (error?.message || String(error))); + } + }; + } + + // πŸ“‹ Handler fΓΌr Auto-Backup Toggle + if ($('settingAutoBackup')) { + $('settingAutoBackup').addEventListener('change', async (e) => { + try { + const enabled = e.target.checked; + const creds = await window.electronAPI.loadCredentials(); + if (creds) { + await window.electronAPI.saveCredentials({ + ...creds, + autoBackupEnabled: enabled + }); + showInfo(enabled ? 'βœ“ Auto-Backup aktiviert' : 'βœ“ Auto-Backup deaktiviert'); + } + } catch (error) { + console.error('Error toggling auto-backup:', error); + } + }); + } + + // Listener fΓΌr Backup Created Events (von IPC) + if (window.electronAPI.onBackupCreated) { + window.electronAPI.onBackupCreated((data) => { + if (data?.ok) { + showSuccess(`πŸ“¦ Backup erstellt: ${data.filename}`); + logActivity('info', `Backup erstellt: ${data.filename} (${data.size} bytes)`); + loadBackupList(); + } + }); + } + }); + +// ═══════════════════════════════════════════════════════════ +// HELPER FUNCTIONS - BACKUP +// ═══════════════════════════════════════════════════════════ + +function getBackupCredentialsFromUI(provider) { + if (!provider) return null; + + const creds = {}; + let isValid = true; + + if (provider === 'local') { + creds.basePath = $('localBackupFolder')?.value?.trim(); + isValid = !!creds.basePath; + } + + return isValid ? creds : null; +} + +async function refreshBackupAuthStatus() { + const el = $('backupAuthStatus'); + if (!el) return; + + const repoName = getActiveBackupRepoName(); + const provider = 'local'; + + if (!repoName) { + el.textContent = 'Status: Kein Repository erkannt'; + el.style.color = 'var(--warning)'; + return; + } + + try { + const res = await window.electronAPI.getBackupAuthStatus({ repoName, provider }); + if (res?.ok && res.connected) { + const sourceLabel = res.source === 'repo' + ? 'projektbezogen' + : (res.source === 'global' ? 'global gespeichert' : 'aktiv'); + el.textContent = `Status: Verbunden (${sourceLabel})`; + el.style.color = 'var(--success)'; + } else { + el.textContent = 'Status: Nicht verbunden - einmalig verbinden erforderlich'; + el.style.color = 'var(--warning)'; + } + } catch (_) { + el.textContent = 'Status: Verbindung konnte nicht geprΓΌft werden'; + el.style.color = 'var(--danger)'; + } +} + +function getActiveBackupRepoName() { + const selectedSource = getSelectedBackupSource(); + if (selectedSource && selectedSource.repoName) return selectedSource.repoName; + + if (currentState && currentState.repo) return currentState.repo; + const inputRepo = $('repoName')?.value?.trim(); + if (inputRepo) return inputRepo; + if (selectedFolder) { + const folderName = selectedFolder.split(/[\\/]/).pop(); + if (folderName) return folderName; + } + return ''; +} + +function getSelectedBackupSource() { + const key = $('backupSourceSelect')?.value || ''; + if (!key) return null; + + if (key === '__ALL_GIT__') { + return { + key, + kind: 'gitea-all', + repoName: 'all-git-projects', + label: 'Alles komplett sichern (Git)' + }; + } + + if (key.startsWith('repo::')) { + const payload = key.slice(6); + const [owner, repo] = payload.split('/'); + const name = repo || 'projekt'; + return { + key, + kind: 'gitea-repo', + owner, + repo, + repoName: name, + label: name + }; + } + + return null; +} + +async function ensureBackupSourcesLoaded() { + try { + const res = await window.electronAPI.listGiteaRepos(); + if (!res || !res.ok || !Array.isArray(res.repos)) { + backupGitRepos = []; + return; + } + + backupGitRepos = res.repos.map(r => ({ + owner: (r.owner && (r.owner.login || r.owner.username)) || '', + repo: r.name || '' + })).filter(r => r.owner && r.repo); + } catch (_) {} +} + +function populateBackupSourceOptions() { + const select = $('backupSourceSelect'); + if (!select) return; + + const prevValue = select.value || ''; + const prefValue = select.dataset.prefValue || ''; + const keepValue = prevValue || prefValue; + + const opts = []; + opts.push({ value: '', text: '-- WΓ€hle aus vorhandenen Projekten --' }); + + if (backupGitRepos.length > 0) { + opts.push({ value: '__ALL_GIT__', text: '🧩 Alles komplett sichern (alle Git-Projekte)' }); + } + + const repos = [...backupGitRepos].sort((a, b) => `${a.owner}/${a.repo}`.localeCompare(`${b.owner}/${b.repo}`, 'de')); + repos.forEach(node => { + opts.push({ + value: `repo::${node.owner}/${node.repo}`, + text: `πŸ“¦ ${node.owner}/${node.repo}` + }); + }); + + if (keepValue && keepValue.startsWith('repo::') && !opts.some(o => o.value === keepValue)) { + try { + const decoded = keepValue.slice(6); + opts.push({ value: keepValue, text: `πŸ“Œ ${decoded} (zuletzt)` }); + } catch (_) {} + } + + select.innerHTML = opts + .map(o => ``) + .join(''); + + if (keepValue && opts.some(o => o.value === keepValue)) { + select.value = keepValue; + } else if (opts.length > 1) { + select.value = opts[1].value; + } else { + select.value = ''; + } +} + +async function loadBackupUiPrefs() { + try { + const creds = await window.electronAPI.loadCredentials(); + if (!creds) return; + + const localFolder = String(creds.backupPrefLocalFolder || '').trim(); + const sourceSelection = String(creds.backupPrefSourceSelection || '').trim(); + + const localFolderEl = $('localBackupFolder'); + if (localFolderEl && localFolder && !localFolderEl.value) { + localFolderEl.value = localFolder; + } + + const sourceSelectEl = $('backupSourceSelect'); + if (sourceSelectEl && sourceSelection) { + sourceSelectEl.dataset.prefValue = sourceSelection; + } + } catch (_) {} +} + +async function saveBackupUiPrefs(patch) { + try { + const creds = await window.electronAPI.loadCredentials(); + const current = creds || {}; + await window.electronAPI.saveCredentials({ ...current, ...patch }); + } catch (_) {} +} + +async function loadBackupList() { + const repoName = getActiveBackupRepoName(); + if (!repoName) { + const container = $('backupListContainer'); + if (container) { + container.innerHTML = '
Kein Repository erkannt
'; + } + return; + } + + const provider = 'local'; + const credentials = getBackupCredentialsFromUI(provider); + + // Optionaler Komfort: Wenn neue Credentials eingetragen sind, automatisch speichern. + if (credentials) { + try { + await window.electronAPI.setupBackupProvider({ repoName, provider, credentials }); + } catch (_) { + // Wenn kein vollstΓ€ndiges Formular vorhanden ist, versuchen wir mit gespeicherter Konfiguration weiter. + } + } + + setStatus('Lade Backup-Liste...'); + showProgress(0, 'Lade Backups...'); + + try { + const res = await window.electronAPI.listCloudBackups({ repoName, provider }); + + hideProgress(); + + const container = $('backupListContainer'); + if (!container) return; + + if (!res?.ok) { + container.innerHTML = `
Fehler beim Laden: ${res?.error || 'Unbekannt'}
`; + return; + } + + const backups = res.backups || []; + + if (backups.length === 0) { + container.innerHTML = '
Noch keine Backups vorhanden
'; + return; + } + + container.innerHTML = ''; + + const baseNameCounts = new Map(); + backups.forEach((b) => { + const name = getBackupDisplayName(b.filename || b.name || ''); + if (!name) return; + baseNameCounts.set(name, (baseNameCounts.get(name) || 0) + 1); + }); + + backups.forEach((backup, index) => { + const backupFilename = backup.filename || backup.name || ''; + const baseDisplayName = getBackupDisplayName(backupFilename) || `Backup ${index + 1}`; + const duplicateCount = baseNameCounts.get(baseDisplayName) || 0; + const displaySuffix = duplicateCount > 1 ? ` β€’ ${formatTimeOnly(backup.modifiedTime || backup.date)}` : ''; + const backupDisplayName = `${baseDisplayName}${displaySuffix}`; + const item = document.createElement('div'); + item.className = 'backup-list-item'; + + const info = document.createElement('div'); + info.className = 'backup-list-info'; + + const filename = document.createElement('div'); + filename.textContent = backupDisplayName; + filename.title = backupFilename || backupDisplayName; + filename.className = 'backup-list-name'; + + const meta = document.createElement('div'); + meta.className = 'backup-list-meta'; + meta.textContent = `Grâße: ${formatBytes(backup.size)} β€’ ${new Date(backup.modifiedTime || new Date()).toLocaleString('de-DE')}`; + + info.appendChild(filename); + info.appendChild(meta); + + const buttons = document.createElement('div'); + buttons.className = 'backup-list-actions'; + + // Download-Button + const btnRestore = document.createElement('button'); + btnRestore.className = 'backup-list-action backup-list-action--restore'; + btnRestore.textContent = '⬇️ Wiederherstellen'; + btnRestore.onclick = async () => { + if (!backupFilename) { + showError('Backup-Dateiname fehlt.'); + return; + } + const proceedRestore = await showActionConfirmModal({ + title: 'Backup wiederherstellen', + message: `Backup "${backupFilename}" wirklich wiederherstellen? Vorhandene Dateien werden ΓΌberschrieben.`, + confirmText: 'Wiederherstellen' + }); + if (!proceedRestore) return; + await restoreBackup(backupFilename, repoName); + }; + + // Delete-Button + const btnDelete = document.createElement('button'); + btnDelete.className = 'backup-list-action backup-list-action--delete'; + btnDelete.textContent = 'πŸ—‘οΈ LΓΆschen'; + btnDelete.onclick = async () => { + if (!backupFilename) { + showError('Backup-Dateiname fehlt.'); + return; + } + const proceedDelete = await showActionConfirmModal({ + title: 'Backup lΓΆschen', + message: `Backup "${backupFilename}" wirklich lΓΆschen?`, + confirmText: 'LΓΆschen', + danger: true + }); + if (!proceedDelete) return; + await deleteBackup(backupFilename, repoName); + }; + + buttons.appendChild(btnRestore); + buttons.appendChild(btnDelete); + item.appendChild(info); + item.appendChild(buttons); + container.appendChild(item); + }); + + setStatus(`${backups.length} Backups gefunden`); + } catch (error) { + hideProgress(); + setStatus('Fehler beim Laden der Backups'); + const container = $('backupListContainer'); + if (container) container.innerHTML = `
Fehler: ${error?.message || 'Unbekannt'}
`; + } +} + +async function restoreBackup(filename, repoName) { + if (!selectedFolder) { + showWarning('Bitte zuerst einen lokalen Ordner auswΓ€hlen'); + return; + } + + setStatus('Stelle Backup wieder her...'); + showProgress(0, `Lade ${filename}...`); + + try { + const res = await window.electronAPI.restoreCloudBackup({ + repoName, + filename, + targetPath: selectedFolder + }); + + hideProgress(); + + if (res?.ok) { + showSuccess(`βœ“ Backup "${filename}" wiederhergestellt`); + if (selectedFolder) refreshLocalTree(selectedFolder); + } else { + showError('Wiederherstellung fehlgeschlagen: ' + (res?.error || 'Unbekannt')); + } + } catch (error) { + hideProgress(); + showError('Fehler: ' + (error?.message || String(error))); + } +} + +async function deleteBackup(filename, repoName) { + setStatus('LΓΆsche Backup...'); + + try { + const res = await window.electronAPI.deleteCloudBackup({ + repoName, + filename + }); + + if (res?.ok) { + showSuccess(`βœ“ Backup gelΓΆscht`); + loadBackupList(); + } else { + showError('LΓΆschen fehlgeschlagen: ' + (res?.error || 'Unbekannt')); + } + } catch (error) { + showError('Fehler: ' + (error?.message || String(error))); + } +} + +function formatBytes(bytes) { + if (!bytes || bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; +} + +function getBackupDisplayName(filename) { + const raw = String(filename || '').trim(); + if (!raw) return ''; + + const normalized = raw.replace(/\.zip$/i, ''); + const match = normalized.match(/^(.*?)-backup-\d{8}-\d{6}(?:-[^-]+)?$/i); + if (match && match[1]) { + return match[1]; + } + + return normalized; +} + +function formatTimeOnly(input) { + if (!input) return '--:--:--'; + const date = new Date(input); + if (Number.isNaN(date.getTime())) return '--:--:--'; + return date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', second: '2-digit' }); +} + /* ================================ RELEASE MANAGEMENT UI FUNCTIONS FΓΌge dies zu renderer.js hinzu diff --git a/renderer/style.css b/renderer/style.css index f960988..f9595db 100644 --- a/renderer/style.css +++ b/renderer/style.css @@ -1904,6 +1904,60 @@ input[type="checkbox"] { .activity-heatmap-card.collapsed .activity-heatmap-body { display: none; } +.confirm-modal .confirm-modal-content { + max-width: 460px; + border: 1px solid rgba(88, 213, 255, 0.22); + background: + linear-gradient(180deg, rgba(16, 29, 51, 0.96), rgba(10, 19, 34, 0.98)); + box-shadow: 0 20px 50px rgba(0, 0, 0, 0.38); +} +.confirm-modal-title { + margin: 0; + font-size: 20px; +} +.confirm-modal-message { + margin: 10px 0 0; + color: var(--text-secondary); + line-height: 1.5; +} +.confirm-modal-buttons { + margin-top: 18px; +} +.confirm-modal-btn { + min-height: 40px; + border-radius: var(--radius-md); + font-size: 13px; + font-weight: 700; + letter-spacing: 0.1px; +} +.confirm-modal-btn--primary { + border: 1px solid rgba(88, 213, 255, 0.34); + background: linear-gradient(135deg, rgba(88, 213, 255, 0.22), rgba(92, 135, 255, 0.2)); + color: #b9f2ff; +} +.confirm-modal-btn--primary:hover { + border-color: rgba(88, 213, 255, 0.6); + background: linear-gradient(135deg, rgba(88, 213, 255, 0.34), rgba(92, 135, 255, 0.3)); +} +.confirm-modal-btn--danger { + border: 1px solid rgba(239, 68, 68, 0.45); + background: linear-gradient(180deg, rgba(62, 22, 22, 0.92), rgba(42, 14, 14, 0.96)); + color: #ffc1c1; +} +.confirm-modal-btn--danger:hover { + border-color: rgba(239, 68, 68, 0.72); + background: linear-gradient(180deg, rgba(82, 28, 28, 0.95), rgba(54, 18, 18, 0.98)); + color: #ffe0e0; +} +.confirm-modal-btn--secondary { + border: 1px solid rgba(255, 255, 255, 0.24); + background: rgba(255, 255, 255, 0.06); + color: var(--text-primary); +} +.confirm-modal-btn--secondary:hover { + border-color: rgba(255, 255, 255, 0.4); + background: rgba(255, 255, 255, 0.12); +} .activity-heatmap-months { margin-left: calc(var(--hm-weekday-col) + var(--hm-grid-gap)); @@ -3238,4 +3292,413 @@ body.compact-mode .file-type-badge { .fav-chip[draggable="true"] { cursor: grab; +} + +/* ═══════════════════════════════════════════════════════════ + βœ… BACKUP MANAGEMENT MODAL - STYLES + ═══════════════════════════════════════════════════════════ */ + +#backupManagementModal { + display: flex; + align-items: center; + justify-content: center; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.7); + z-index: 99999; + backdrop-filter: blur(4px); +} + +#backupManagementModal.hidden { + display: none; +} + +.backup-management-card { + background: var(--bg-secondary); + border: 1px solid rgba(88, 213, 255, 0.2); + border-radius: var(--radius-lg); + width: 90%; + max-width: 700px; + max-height: 85vh; + display: flex; + flex-direction: column; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); + animation: slideUp 0.3s ease-out; + overflow: hidden; +} + +.backup-modal-header { + padding: 20px; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + display: flex; + justify-content: space-between; + align-items: center; +} + +.backup-modal-header h2 { + font-size: 18px; + font-weight: 700; + margin: 0; + color: var(--text-primary); +} + +.backup-modal-close { + background: transparent; + border: none; + color: var(--text-muted); + font-size: 20px; + cursor: pointer; + padding: 0; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 6px; + transition: all var(--transition-fast); +} + +.backup-modal-close:hover { + background: rgba(255, 255, 255, 0.1); + color: var(--accent-primary); +} + +.backup-modal-body { + flex: 1; + overflow-y: auto; + padding: 20px; + display: flex; + flex-direction: column; + gap: 16px; +} + +.backup-modal-body::-webkit-scrollbar { + width: 10px; +} + +.backup-modal-body::-webkit-scrollbar-track { + background: rgba(9, 20, 39, 0.75); + border-left: 1px solid rgba(88, 213, 255, 0.12); +} + +.backup-modal-body::-webkit-scrollbar-thumb { + background: linear-gradient(180deg, rgba(88, 213, 255, 0.55), rgba(92, 135, 255, 0.55)); + border-radius: 10px; + border: 2px solid rgba(9, 20, 39, 0.9); +} + +.backup-modal-body::-webkit-scrollbar-thumb:hover { + background: linear-gradient(180deg, rgba(88, 213, 255, 0.8), rgba(92, 135, 255, 0.8)); +} + +/* Provider Selection */ +.backup-provider-select { + display: flex; + flex-direction: column; + gap: 8px; +} + +.backup-provider-select label { + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--accent-primary); +} + +#backupProviderSelect { + padding: 10px 12px; + background: var(--bg-tertiary); + border: 1px solid rgba(88, 213, 255, 0.25); + border-radius: var(--radius-md); + color: var(--text-primary); + font-size: 13px; + font-family: inherit; + cursor: pointer; + transition: all var(--transition-fast); +} + +#backupProviderSelect:hover { + border-color: rgba(88, 213, 255, 0.4); + background: rgba(19, 33, 58, 0.8); +} + +#backupProviderSelect:focus { + outline: none; + border-color: var(--accent-primary); + box-shadow: 0 0 0 3px rgba(88, 213, 255, 0.15); + background: rgba(19, 33, 58, 0.9); +} + +#backupProviderSelect option { + background: var(--bg-secondary); + color: var(--text-primary); + padding: 8px; +} + +/* Credentials Sections */ +.backup-credentials-section { + display: none; + padding: 12px; + background: linear-gradient(135deg, rgba(88, 213, 255, 0.08) 0%, rgba(122, 81, 255, 0.05) 100%); + border: 1px solid rgba(88, 213, 255, 0.2); + border-radius: var(--radius-md); + gap: 12px; + flex-direction: column; + animation: slideDown 0.2s ease-out; +} + +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.backup-credentials-section.active { + display: flex; +} + +.backup-credentials-section input, +.backup-credentials-section textarea, +.backup-credentials-section select { + padding: 9px 10px; + background: rgba(0, 0, 0, 0.3); + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 6px; + color: var(--text-primary); + font-size: 12px; + font-family: inherit; + transition: all var(--transition-fast); +} + +.backup-credentials-section input::placeholder { + color: rgba(174, 189, 216, 0.35); +} + +.backup-credentials-section input:focus, +.backup-credentials-section textarea:focus, +.backup-credentials-section select:focus { + outline: none; + border-color: var(--accent-primary); + box-shadow: 0 0 0 3px rgba(88, 213, 255, 0.12); + background: rgba(0, 0, 0, 0.4); +} + +.backup-credentials-section select option { + background: var(--bg-secondary); + color: var(--text-primary); +} + +.backup-input-group { + display: flex; + flex-direction: column; + gap: 6px; +} + +.backup-input-group label { + font-size: 11px; + font-weight: 600; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.3px; +} + +/* Buttons */ +.backup-modal-buttons { + display: flex; + gap: 8px; + flex-wrap: wrap; + justify-content: center; + padding: 16px; + border-top: 1px solid rgba(255, 255, 255, 0.1); +} + +.backup-btn { + padding: 10px 16px; + border-radius: var(--radius-md); + border: none; + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: all var(--transition-fast); + min-width: 120px; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + font-family: inherit; +} + +.backup-btn-primary { + background: var(--accent-gradient); + color: #000; + box-shadow: 0 4px 12px rgba(88, 213, 255, 0.25); +} + +.backup-btn-primary:hover { + box-shadow: 0 8px 20px rgba(88, 213, 255, 0.35); + transform: translateY(-2px); +} + +.backup-btn-primary:active { + transform: translateY(0); + box-shadow: 0 2px 8px rgba(88, 213, 255, 0.25); +} + +.backup-btn-secondary { + background: transparent; + border: 1.5px solid rgba(88, 213, 255, 0.35); + color: var(--text-primary); + transition: all var(--transition-fast); +} + +.backup-btn-secondary:hover { + background: rgba(88, 213, 255, 0.12); + border-color: var(--accent-primary); + color: var(--accent-primary); + box-shadow: 0 4px 12px rgba(88, 213, 255, 0.15); + transform: translateY(-1px); +} + +.backup-btn-secondary:active { + transform: translateY(0); +} + +.backup-btn:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none !important; +} + +/* Backup List */ +#backupListContainer { + background: linear-gradient(135deg, rgba(0, 0, 0, 0.3) 0%, rgba(0, 0, 0, 0.15) 100%); + border: 1px solid rgba(88, 213, 255, 0.15); + border-radius: var(--radius-md); + max-height: 250px; + overflow-y: auto; + min-height: 100px; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; +} + +.backup-list-item { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + padding: 12px; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); + transition: background var(--transition-fast); + width: 100%; +} + +.backup-list-info { + flex: 1 1 auto; + min-width: 0; +} + +.backup-list-name { + font-weight: 600; + margin-bottom: 4px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.backup-list-meta { + color: var(--text-muted); + font-size: 12px; +} + +.backup-list-actions { + flex: 0 0 auto; + display: flex; + gap: 8px; + align-items: center; +} + +.backup-list-action { + appearance: none; + border-radius: 8px; + padding: 7px 12px; + font-size: 12px; + font-weight: 600; + border: 1px solid rgba(88, 213, 255, 0.28); + background: linear-gradient(180deg, rgba(20, 34, 58, 0.92), rgba(12, 24, 44, 0.92)); + color: var(--text-primary); + cursor: pointer; + transition: all var(--transition-fast); + white-space: nowrap; +} + +.backup-list-action:hover { + border-color: rgba(88, 213, 255, 0.6); + background: linear-gradient(180deg, rgba(30, 52, 86, 0.94), rgba(15, 30, 56, 0.94)); +} + +.backup-list-action--delete { + color: #ff9f9f; + border-color: rgba(239, 68, 68, 0.35); +} + +.backup-list-action--delete:hover { + color: #ffd6d6; + border-color: rgba(239, 68, 68, 0.65); + background: linear-gradient(180deg, rgba(72, 24, 24, 0.88), rgba(48, 14, 14, 0.9)); +} + +@media (max-width: 760px) { + .backup-list-item { + flex-direction: column; + align-items: stretch; + } + + .backup-list-actions { + width: 100%; + justify-content: flex-end; + flex-wrap: wrap; + } + + .backup-list-action { + flex: 1 1 auto; + text-align: center; + min-width: 130px; + } +} + +.backup-list-item:hover { + background: rgba(88, 213, 255, 0.1); +} + +.backup-list-item:last-child { + border-bottom: none; +} + +/* Scrollbar Styling */ +#backupListContainer::-webkit-scrollbar { + width: 8px; +} + +#backupListContainer::-webkit-scrollbar-track { + background: transparent; +} + +#backupListContainer::-webkit-scrollbar-thumb { + background: rgba(88, 213, 255, 0.2); + border-radius: 4px; +} + +#backupListContainer::-webkit-scrollbar-thumb:hover { + background: rgba(88, 213, 255, 0.4); } \ No newline at end of file