From 54b87bec6671ebbba0346756a37bd0370d60f183 Mon Sep 17 00:00:00 2001 From: Git Manager GUI Date: Sat, 4 Apr 2026 17:48:16 +0200 Subject: [PATCH] Upload folder via GUI - renderer --- renderer/index.html | 3 + renderer/renderer.js | 527 +++++++++++++++++++++++++++++++++++++++---- renderer/style.css | 63 +++++- 3 files changed, 552 insertions(+), 41 deletions(-) diff --git a/renderer/index.html b/renderer/index.html index 176d090..5e1c8dc 100644 --- a/renderer/index.html +++ b/renderer/index.html @@ -47,6 +47,7 @@ +
@@ -81,6 +82,8 @@
+ + diff --git a/renderer/renderer.js b/renderer/renderer.js index 55ca230..90e583e 100644 --- a/renderer/renderer.js +++ b/renderer/renderer.js @@ -595,32 +595,34 @@ async function uploadDroppedPaths({ paths, owner, repo, destPath = '', cloneUrl try { let res = null; - // Fast path: treat dropped item as file upload first. - // If it is actually a directory, the call returns ok=false and we fall back to uploadAndPush. - const fileTry = await withTimeout(window.electronAPI.uploadGiteaFile({ - owner, - repo, - localPath: [p], - destPath, - branch, - platform: currentState.platform - }), 15000, 'upload-gitea-file'); + const pathType = await withTimeout(window.electronAPI.getPathType(p), 5000, 'get-path-type'); + console.log('[UPLOAD_DEBUG][renderer] uploadDroppedPaths:pathType', { path: p, pathType }); + try { window.electronAPI.debugToMain('log', 'uploadDroppedPaths:pathType', { path: p, pathType }); } catch (_) {} - console.log('[UPLOAD_DEBUG][renderer] uploadDroppedPaths:fileTry', { path: p, fileTry }); - try { window.electronAPI.debugToMain('log', 'uploadDroppedPaths:fileTry', { path: p, fileTry }); } catch (_) {} + if (!pathType?.ok) { + throw new Error(pathType?.error || 'path-type-failed'); + } - const fileFailed = !fileTry?.ok || (Array.isArray(fileTry.results) && fileTry.results.some(r => !r.ok)); - if (!fileFailed) { - res = { ok: true, via: 'upload-gitea-file', results: fileTry.results || [] }; - } else { - res = await withTimeout(window.electronAPI.uploadAndPush({ + if (pathType.type === 'file') { + res = await withTimeout(window.electronAPI.uploadGiteaFile({ + owner, + repo, + localPath: [p], + destPath, + branch, + platform: currentState.platform + }), 15000, 'upload-gitea-file'); + } else if (pathType.type === 'dir') { + res = await withTimeout(window.electronAPI.uploadLocalFolderToGitea({ localFolder: p, owner, repo, destPath, - cloneUrl, - branch - }), 30000, 'upload-and-push'); + branch, + messagePrefix: 'Upload folder via GUI' + }), 600000, 'upload-local-folder-to-gitea'); + } else { + throw new Error(`Nicht unterstuetzter Pfadtyp: ${pathType.type || 'unknown'}`); } console.log('[UPLOAD_DEBUG][renderer] uploadDroppedPaths:itemResult', { path: p, res }); @@ -628,7 +630,10 @@ async function uploadDroppedPaths({ paths, owner, repo, destPath = '', cloneUrl if (!res?.ok) { failedCount++; - errors.push(`${baseName}: ${res?.error || 'Unbekannter Fehler'}`); + const detailedFailure = Array.isArray(res?.failedFiles) && res.failedFiles.length > 0 + ? `${baseName}: ${res.failedFiles[0].targetPath} - ${res.failedFiles[0].error || 'Unbekannter Fehler'}` + : `${baseName}: ${res?.error || 'Unbekannter Fehler'}`; + errors.push(detailedFailure); continue; } @@ -951,12 +956,13 @@ function getDefaultBranch(owner, repo) { // Navigations-Status für die Explorer-Ansicht let currentState = { - view: 'none', // 'local', 'gitea-list', 'gitea-repo' + view: 'none', // 'local', 'gitea-list', 'gitea-repo', 'gitea-trash' owner: null, repo: null, path: '', platform: 'gitea' // 'gitea' | 'github' }; +let lastRepoPathBeforeTrash = ''; let repoLoadRequestId = 0; const USER_CACHE_MS = 5 * 60 * 1000; @@ -1852,6 +1858,31 @@ function showSuccess(msg) { setStatus(msg); showToast(msg, 'success', 3000); log function showWarning(msg) { setStatus(msg); showToast(msg, 'warning'); logActivity('warning', msg); } function showInfo(msg) { setStatus(msg); showToast(msg, 'info', 2500); } +function formatUploadFailureDetails(result, fallbackMessage = 'Upload fehlgeschlagen') { + const lines = []; + const summary = result?.error || fallbackMessage; + lines.push(summary); + + if (Array.isArray(result?.failedFiles) && result.failedFiles.length > 0) { + lines.push(''); + lines.push('Betroffene Dateien:'); + result.failedFiles.slice(0, 8).forEach((entry) => { + const target = entry?.targetPath || entry?.localFile || 'unbekannter Pfad'; + const error = entry?.error || 'Unbekannter Fehler'; + lines.push(`- ${target}: ${error}`); + }); + if (result.failedFiles.length > 8) { + lines.push(`- ... und ${result.failedFiles.length - 8} weitere`); + } + } + + return lines.join('\n'); +} + +async function showUploadFailureModal(title, result, fallbackMessage = 'Upload fehlgeschlagen') { + await showInfoModal(title, formatUploadFailureDetails(result, fallbackMessage), true); +} + function normalizeSearchText(value) { return String(value || '') .toLowerCase() @@ -2927,7 +2958,9 @@ function updateNavigationUI() { if (!btnBack) return; // Back Button zeigen, wenn wir in einem Repo oder tief in Ordnern sind - if (currentState.view === 'gitea-repo' || + if (currentState.view === 'gitea-repo' || + currentState.view === 'gitea-trash' || + currentState.view === 'gitea-trash-global' || (currentState.view === 'gitea-list' && currentState.path !== '')) { btnBack.classList.remove('hidden'); } else { @@ -2935,6 +2968,333 @@ function updateNavigationUI() { } } +async function loadGlobalTrashView() { + if (currentState.platform !== 'gitea') { + showError('Globaler Papierkorb ist aktuell nur für Gitea verfügbar.'); + return; + } + + currentState.view = 'gitea-trash-global'; + currentState.path = '_trash'; + updateNavigationUI(); + + const btnCommits = $('btnCommits'); + const btnReleases = $('btnReleases'); + const btnPurgeTrash = $('btnPurgeTrash'); + if (btnCommits) btnCommits.classList.add('hidden'); + if (btnReleases) btnReleases.classList.add('hidden'); + + const grid = $('explorerGrid'); + if (!grid) return; + grid.innerHTML = ''; + setStatus('Lade globalen Papierkorb...'); + + let repos = Array.isArray(currentGiteaRepos) ? currentGiteaRepos.slice() : []; + if (repos.length === 0) { + const reposRes = await window.electronAPI.listGiteaRepos(); + if (!reposRes?.ok) { + showError('Globaler Papierkorb konnte nicht geladen werden: Repo-Liste fehlgeschlagen'); + return; + } + repos = Array.isArray(reposRes.repos) ? reposRes.repos : []; + currentGiteaRepos = repos; + } + + const allItems = []; + let purgedFilesTotal = 0; + + for (const r of repos) { + const owner = r?.owner?.login || r?.owner?.username; + const repoName = r?.name; + if (!owner || !repoName) continue; + + const ref = getDefaultBranch(owner, repoName); + const res = await window.electronAPI.listGiteaTrash({ + owner, + repo: repoName, + ref, + autoPurge: true, + autoPurgeDays: 7 + }); + if (!res?.ok) continue; + + purgedFilesTotal += Number(res?.purgeSummary?.purgedFiles || 0); + const items = Array.isArray(res.items) ? res.items : []; + items.forEach(item => { + allItems.push({ + ...item, + owner, + repo: repoName, + ref + }); + }); + } + + if (btnPurgeTrash) { + btnPurgeTrash.classList.remove('hidden'); + btnPurgeTrash.textContent = '🧹 Global Purge 7d'; + btnPurgeTrash.title = 'Papierkorb älter als 7 Tage in allen Repositories leeren'; + btnPurgeTrash.onclick = async () => { + const ok = await showActionConfirmModal({ + title: 'Globaler Purge', + message: 'Alle Papierkorb-Einträge älter als 7 Tage in ALLEN Repositories löschen?', + confirmText: 'Global Purge', + danger: true + }); + if (!ok) return; + + showProgress(35, 'Globaler Purge läuft...'); + let totalPurged = 0; + for (const r of repos) { + const owner = r?.owner?.login || r?.owner?.username; + const repoName = r?.name; + if (!owner || !repoName) continue; + const ref = getDefaultBranch(owner, repoName); + const purgeRes = await window.electronAPI.purgeGiteaTrash({ + owner, + repo: repoName, + ref, + olderThanDays: 7 + }); + if (purgeRes?.ok) totalPurged += Number(purgeRes.purgedFiles || 0); + } + hideProgress(); + showSuccess(`Global Purge abgeschlossen: ${totalPurged} Dateien entfernt`); + await loadGlobalTrashView(); + }; + } + + if (purgedFilesTotal > 0) { + setStatus(`Auto-Purge (global): ${purgedFilesTotal} Dateien entfernt`); + } + + if (allItems.length === 0) { + const emptyEl = document.createElement('div'); + emptyEl.style.cssText = 'grid-column: 1/-1; text-align: center; padding: 40px; color: var(--text-muted);'; + emptyEl.textContent = '🧺 Globaler Papierkorb ist leer'; + grid.appendChild(emptyEl); + setStatus('Globaler Papierkorb ist leer'); + return; + } + + allItems.sort((a, b) => String(b.restoreTimestamp).localeCompare(String(a.restoreTimestamp))); + for (const item of allItems) { + const card = document.createElement('div'); + card.className = 'item-card'; + + const iconEl = makeFileIconEl(item.name || item.originalPath || 'item', false); + const nameEl = document.createElement('div'); + nameEl.className = 'item-name'; + nameEl.textContent = item.originalPath || item.name || '(unbekannt)'; + + const metaEl = document.createElement('div'); + metaEl.className = 'trash-card-meta'; + metaEl.textContent = `${item.owner}/${item.repo} | Trash: ${item.trashPath} | Zeit: ${item.restoreTimestamp}`; + + const actions = document.createElement('div'); + actions.className = 'trash-card-actions'; + actions.style.position = 'relative'; + actions.style.zIndex = '3'; + + const restoreBtn = document.createElement('button'); + restoreBtn.type = 'button'; + restoreBtn.className = 'trash-restore-btn'; + restoreBtn.textContent = '♻️ Restore'; + restoreBtn.style.position = 'relative'; + restoreBtn.style.zIndex = '4'; + restoreBtn.addEventListener('click', async (ev) => { + ev.stopPropagation(); + try { + restoreBtn.disabled = true; + restoreBtn.textContent = '⏳ Restore...'; + setStatus(`Restore: ${item.owner}/${item.repo} -> ${item.originalPath}`); + showProgress(25, 'Wiederherstellung läuft...'); + const restoreRes = await window.electronAPI.restoreGiteaTrashItem({ + owner: item.owner, + repo: item.repo, + trashPath: item.trashPath, + restorePath: item.originalPath, + ref: item.ref + }); + hideProgress(); + if (restoreRes?.ok) { + const warning = restoreRes.warning ? ` (${restoreRes.warning})` : ''; + showSuccess(`Wiederhergestellt: ${item.owner}/${item.repo}/${item.originalPath}${warning}`); + await loadGlobalTrashView(); + } else { + showError('Restore fehlgeschlagen: ' + (restoreRes?.error || 'Unbekannter Fehler')); + } + } catch (e) { + hideProgress(); + showError('Restore fehlgeschlagen: ' + (e?.message || String(e))); + } finally { + restoreBtn.disabled = false; + restoreBtn.textContent = '♻️ Restore'; + } + }); + + actions.appendChild(restoreBtn); + card.appendChild(iconEl); + card.appendChild(nameEl); + card.appendChild(metaEl); + card.appendChild(actions); + grid.appendChild(card); + } + + setStatus(`Globaler Papierkorb: ${allItems.length} Einträge`); +} + +async function loadTrashView(owner, repo) { + if (!owner || !repo) { + showError('Papierkorb nur innerhalb eines Repositories verfügbar.'); + return; + } + + lastRepoPathBeforeTrash = currentState.path || ''; + currentState.view = 'gitea-trash'; + currentState.owner = owner; + currentState.repo = repo; + currentState.path = '_trash'; + updateNavigationUI(); + + const btnCommits = $('btnCommits'); + const btnReleases = $('btnReleases'); + const btnPurgeTrash = $('btnPurgeTrash'); + if (btnCommits) btnCommits.classList.add('hidden'); + if (btnReleases) btnReleases.classList.add('hidden'); + if (btnPurgeTrash) { + btnPurgeTrash.classList.remove('hidden'); + btnPurgeTrash.textContent = '🧹 Purge 7d'; + btnPurgeTrash.title = 'Papierkorb älter als 7 Tage leeren'; + btnPurgeTrash.onclick = async () => { + const ok = await showActionConfirmModal({ + title: 'Papierkorb leeren', + message: 'Alle Papierkorb-Einträge älter als 7 Tage löschen?', + confirmText: 'Purge 7d', + danger: true + }); + if (!ok) return; + showProgress(30, 'Papierkorb wird bereinigt...'); + const purgeRes = await window.electronAPI.purgeGiteaTrash({ + owner, + repo, + ref, + olderThanDays: 7 + }); + hideProgress(); + if (purgeRes?.ok) { + showSuccess(`Papierkorb bereinigt: ${purgeRes.purgedFiles || 0} Dateien entfernt`); + await loadTrashView(owner, repo); + } else { + showError('Purge fehlgeschlagen: ' + (purgeRes?.error || 'Unbekannter Fehler')); + } + }; + } + + const grid = $('explorerGrid'); + if (!grid) return; + grid.innerHTML = ''; + setStatus('Lade Papierkorb...'); + + const ref = getDefaultBranch(owner, repo); + const res = await window.electronAPI.listGiteaTrash({ owner, repo, ref }); + if (!res?.ok) { + showError('Papierkorb konnte nicht geladen werden: ' + (res?.error || 'Unbekannter Fehler')); + return; + } + + if ((res?.purgeSummary?.purgedFiles || 0) > 0) { + setStatus(`Auto-Purge: ${res.purgeSummary.purgedFiles} alte Papierkorb-Dateien entfernt`); + } + + const items = Array.isArray(res.items) ? res.items : []; + if (items.length === 0) { + const emptyEl = document.createElement('div'); + emptyEl.style.cssText = 'grid-column: 1/-1; text-align: center; padding: 40px; color: var(--text-muted);'; + emptyEl.textContent = '🧺 Papierkorb ist leer'; + grid.appendChild(emptyEl); + setStatus('Papierkorb ist leer'); + return; + } + + items.forEach((item) => { + const card = document.createElement('div'); + card.className = 'item-card'; + + const iconEl = makeFileIconEl(item.name || item.originalPath || 'item', false); + const nameEl = document.createElement('div'); + nameEl.className = 'item-name'; + nameEl.textContent = item.originalPath || item.name || '(unbekannt)'; + + const metaEl = document.createElement('div'); + metaEl.className = 'trash-card-meta'; + metaEl.textContent = `Trash: ${item.trashPath} | Zeit: ${item.restoreTimestamp}`; + + const actions = document.createElement('div'); + actions.className = 'trash-card-actions'; + actions.style.position = 'relative'; + actions.style.zIndex = '3'; + + const restoreBtn = document.createElement('button'); + restoreBtn.type = 'button'; + restoreBtn.className = 'trash-restore-btn'; + restoreBtn.textContent = '♻️ Restore'; + restoreBtn.style.position = 'relative'; + restoreBtn.style.zIndex = '4'; + restoreBtn.addEventListener('click', async (ev) => { + ev.stopPropagation(); + try { + restoreBtn.disabled = true; + restoreBtn.textContent = '⏳ Restore...'; + setStatus(`Restore: ${item.originalPath}`); + showProgress(25, 'Wiederherstellung läuft...'); + if (window.electronAPI?.debugToMain) { + window.electronAPI.debugToMain('info', 'trash-restore-clicked', { + owner, + repo, + trashPath: item.trashPath, + restorePath: item.originalPath, + ref + }); + } + + const restoreRes = await window.electronAPI.restoreGiteaTrashItem({ + owner, + repo, + trashPath: item.trashPath, + restorePath: item.originalPath, + ref + }); + + hideProgress(); + if (restoreRes?.ok) { + const warning = restoreRes.warning ? ` (${restoreRes.warning})` : ''; + showSuccess(`Wiederhergestellt: ${item.originalPath}${warning}`); + await loadTrashView(owner, repo); + } else { + showError('Restore fehlgeschlagen: ' + (restoreRes?.error || 'Unbekannter Fehler')); + } + } catch (e) { + hideProgress(); + showError('Restore fehlgeschlagen: ' + (e?.message || String(e))); + console.error('trash restore click handler error', e); + } finally { + restoreBtn.disabled = false; + restoreBtn.textContent = '♻️ Restore'; + } + }); + + actions.appendChild(restoreBtn); + card.appendChild(iconEl); + card.appendChild(nameEl); + card.appendChild(metaEl); + card.appendChild(actions); + grid.appendChild(card); + }); + + setStatus(`Papierkorb: ${items.length} Einträge`); +} + /* ------------------------- GITEA CORE LOGIK (GRID) ------------------------- */ @@ -2947,8 +3307,14 @@ async function loadGiteaRepos(preloadedData = null, requestId = null) { // Verstecke Commits & Releases-Buttons in Repo-Liste const btnCommits = $('btnCommits'); const btnReleases = $('btnReleases'); + const btnTrash = $('btnTrash'); + const btnGlobalTrash = $('btnGlobalTrash'); + const btnPurgeTrash = $('btnPurgeTrash'); if (btnCommits) btnCommits.classList.add('hidden'); if (btnReleases) btnReleases.classList.add('hidden'); + if (btnTrash) btnTrash.classList.add('hidden'); + if (btnGlobalTrash) btnGlobalTrash.classList.add('hidden'); + if (btnPurgeTrash) btnPurgeTrash.classList.add('hidden'); // WICHTIG: Grid-Layout zurücksetzen const grid = $('explorerGrid'); @@ -2975,6 +3341,11 @@ async function loadGiteaRepos(preloadedData = null, requestId = null) { } currentGiteaRepos = Array.isArray(res.repos) ? res.repos : []; + const btnGlobalTrash = $('btnGlobalTrash'); + if (btnGlobalTrash && currentState.platform === 'gitea') { + btnGlobalTrash.classList.remove('hidden'); + btnGlobalTrash.onclick = () => loadGlobalTrashView(); + } if (!preloadedData) { try { const cachedName = getCachedUsername('gitea'); @@ -3328,6 +3699,7 @@ async function loadGiteaRepos(preloadedData = null, requestId = null) { console.log('[UPLOAD_DEBUG][renderer] repoCard:dropResult', res); if (!res.ok) { showError('Fehler: ' + (res.error || 'Upload fehlgeschlagen')); + await showUploadFailureModal('Upload fehlgeschlagen', res, 'Upload fehlgeschlagen'); setStatus('Upload fehlgeschlagen'); } else { setStatus('Upload abgeschlossen'); @@ -3376,6 +3748,9 @@ async function loadRepoContents(owner, repo, path) { // Zeige Commits & Releases-Buttons wenn wir in einem Repo sind const btnCommits = $('btnCommits'); const btnReleases = $('btnReleases'); + const btnTrash = $('btnTrash'); + const btnGlobalTrash = $('btnGlobalTrash'); + const btnPurgeTrash = $('btnPurgeTrash'); if (btnCommits) { btnCommits.classList.remove('hidden'); @@ -3387,6 +3762,16 @@ async function loadRepoContents(owner, repo, path) { btnReleases.onclick = () => loadRepoReleases(owner, repo); } + if (btnTrash) { + btnTrash.classList.remove('hidden'); + btnTrash.onclick = () => loadTrashView(owner, repo); + } + if (btnGlobalTrash) { + btnGlobalTrash.classList.remove('hidden'); + btnGlobalTrash.onclick = () => loadGlobalTrashView(); + } + if (btnPurgeTrash) btnPurgeTrash.classList.add('hidden'); + // WICHTIG: Grid-Layout zurücksetzen const grid = $('explorerGrid'); if (grid) { @@ -3404,7 +3789,8 @@ async function loadRepoContents(owner, repo, path) { repo, path, ref, - platform + platform, + noCache: true }); if (!res.ok) { @@ -3523,6 +3909,7 @@ loadRepos = function(...args) { }); if (!res.ok) { showError('Upload error: ' + (res.error || 'Unbekannter Fehler')); + await showUploadFailureModal('Upload fehlgeschlagen', res, 'Upload fehlgeschlagen'); } } catch (error) { console.error('Upload error:', error); @@ -3583,6 +3970,13 @@ async function selectLocalFolder() { await refreshLocalTree(folder); await loadBranches(folder); + + const btnTrash = $('btnTrash'); + if (btnTrash) btnTrash.classList.add('hidden'); + const btnGlobalTrash = $('btnGlobalTrash'); + if (btnGlobalTrash) btnGlobalTrash.classList.add('hidden'); + const btnPurgeTrash = $('btnPurgeTrash'); + if (btnPurgeTrash) btnPurgeTrash.classList.add('hidden'); } catch (error) { console.error('Error selecting folder:', error); showError('Error selecting folder'); @@ -4182,21 +4576,26 @@ function showRepoContextMenu(ev, owner, repoName, cloneUrl, element, isPrivate = const sel = await window.electronAPI.selectFolder(); if (sel) { showProgress(0, 'Upload...'); - await window.electronAPI.uploadAndPush({ + const res = await window.electronAPI.uploadLocalFolderToGitea({ localFolder: sel, owner, repo: repoName, destPath: '', - cloneUrl, - branch: getDefaultBranch(owner, repoName) + branch: getDefaultBranch(owner, repoName), + messagePrefix: 'Upload folder via GUI' }); + if (!res?.ok) { + showError('Upload fehlgeschlagen: ' + (res?.error || 'Unbekannter Fehler')); + await showUploadFailureModal('Upload fehlgeschlagen', res, 'Upload fehlgeschlagen'); + return; + } hideProgress(); setStatus('Upload complete'); } } catch (error) { console.error('Upload error:', error); hideProgress(); - showError('Upload failed'); + showError('Upload failed: ' + (error?.message || 'Unbekannter Fehler')); } }); @@ -4288,7 +4687,7 @@ function showGiteaItemContextMenu(ev, item, owner, repo) { let done = 0; for (const p of selectedItems) { const isGithub = currentState.platform === 'github'; - await window.electronAPI.deleteFile({ path: p, owner, repo, isGitea: !isGithub, isGithub, ref: getDefaultBranch(owner, repo) }); + await window.electronAPI.deleteFile({ path: p, owner, repo, isGitea: !isGithub, isGithub, ref: getDefaultBranch(owner, repo), softDelete: true }); done++; showProgress(Math.round((done / selectedItems.size) * 100), `Lösche ${done}/${selectedItems.size}`); } @@ -4366,10 +4765,10 @@ function showGiteaItemContextMenu(ev, item, owner, repo) { if (!ok) return; showProgress(0, `Lösche ${item.name}...`); const isGithub = currentState.platform === 'github'; - const res = await window.electronAPI.deleteFile({ path: item.path, owner, repo, isGitea: !isGithub, isGithub, ref: getDefaultBranch(owner, repo) }); + const res = await window.electronAPI.deleteFile({ path: item.path, owner, repo, isGitea: !isGithub, isGithub, ref: getDefaultBranch(owner, repo), softDelete: true }); hideProgress(); if (res && res.ok) { - setStatus(`${item.name} gelöscht`); + setStatus(`${item.name} gelöscht` + (res.softDeleteWarning ? ` (${res.softDeleteWarning})` : '')); loadRepoContents(owner, repo, currentState.path); } else { showError('Löschen fehlgeschlagen: ' + (res?.error || '')); @@ -4556,7 +4955,7 @@ function showNewGiteaItemModal(owner, repo, parentPath, type) { type }); if (res?.ok) { - setStatus(`"${name}" erstellt`); + setStatus(res?.exists ? `"${name}" existiert bereits` : `"${name}" erstellt`); loadRepoContents(owner, repo, currentState.path); } else { await showInfoModal('Erstellen fehlgeschlagen', 'Erstellen fehlgeschlagen:\n' + (res?.error || ''), true); @@ -4936,12 +5335,23 @@ function setupBackgroundContextMenu() { addItem('📋', `⬆️ Von Lokal einfügen: "${clipboard.item.name}"`, async () => { showProgress(0, `Lade "${clipboard.item.name}" hoch...`); try { - await window.electronAPI.uploadAndPush({ - localFolder: clipboard.item.path, - owner, repo, - destPath: currentPath, - branch: getDefaultBranch(owner, repo) - }); + const res = clipboard.item.isDirectory + ? await window.electronAPI.uploadLocalFolderToGitea({ + localFolder: clipboard.item.path, + owner, + repo, + destPath: currentPath, + branch: getDefaultBranch(owner, repo), + messagePrefix: 'Upload folder via GUI' + }) + : await window.electronAPI.uploadAndPush({ + localFolder: clipboard.item.path, + owner, + repo, + destPath: currentPath, + branch: getDefaultBranch(owner, repo) + }); + if (!res?.ok) throw new Error(res?.error || 'Upload fehlgeschlagen'); showSuccess(`"${clipboard.item.name}" nach Gitea kopiert`); loadRepoContents(owner, repo, currentState.path); } catch(e) { showError('Cross-Paste fehlgeschlagen'); } @@ -5239,10 +5649,34 @@ window.addEventListener('DOMContentLoaded', async () => { parts.pop(); loadRepoContents(currentState.owner, currentState.repo, parts.join('/')); } + } else if (currentState.view === 'gitea-trash') { + loadRepoContents(currentState.owner, currentState.repo, lastRepoPathBeforeTrash || ''); + } else if (currentState.view === 'gitea-trash-global') { + loadGiteaRepos(); } }; } + if ($('btnTrash')) { + $('btnTrash').onclick = () => { + if (!currentState.owner || !currentState.repo) { + showError('Bitte zuerst ein Repository öffnen.'); + return; + } + loadTrashView(currentState.owner, currentState.repo); + }; + } + + if ($('btnGlobalTrash')) { + $('btnGlobalTrash').onclick = () => { + if (currentState.platform !== 'gitea') { + showError('Globaler Papierkorb ist nur für Gitea verfügbar.'); + return; + } + loadGlobalTrashView(); + }; + } + // Modal controls if ($('btnWinMinimize')) $('btnWinMinimize').onclick = () => window.electronAPI.windowMinimize(); if ($('btnWinMaximize')) $('btnWinMaximize').onclick = () => window.electronAPI.windowMaximize(); @@ -5912,8 +6346,21 @@ window.addEventListener('DOMContentLoaded', async () => { if (lastSelectedItem.type === 'gitea') { const { item, owner, repo } = lastSelectedItem; showDeleteConfirm(`"${item.name}" wirklich löschen?`, async () => { - const res = await window.electronAPI.deleteFile({ path: item.path, owner, repo, isGitea: true }); - if (res?.ok) { showSuccess(`"${item.name}" gelöscht`); loadRepoContents(owner, repo, currentState.path); lastSelectedItem = null; } + const isGithub = currentState.platform === 'github'; + const res = await window.electronAPI.deleteFile({ + path: item.path, + owner, + repo, + isGitea: !isGithub, + isGithub, + ref: getDefaultBranch(owner, repo), + softDelete: true + }); + if (res?.ok) { + showSuccess(`"${item.name}" gelöscht` + (res.softDeleteWarning ? ` (${res.softDeleteWarning})` : '')); + loadRepoContents(owner, repo, currentState.path); + lastSelectedItem = null; + } else showError('Löschen fehlgeschlagen: ' + (res?.error || '')); }); } else if (lastSelectedItem.type === 'local') { diff --git a/renderer/style.css b/renderer/style.css index 12a0771..2f8156d 100644 --- a/renderer/style.css +++ b/renderer/style.css @@ -427,6 +427,11 @@ body { .tool-group--quick-actions { gap: 8px; + flex-wrap: nowrap; +} + +.tool-group--utility { + flex-wrap: nowrap; } .tool-group--repo { @@ -646,7 +651,8 @@ body { } #btnOpenRepoActions, -#btnPush { +#btnPush, +#btnGlobalTrash { min-width: 92px; } @@ -2014,6 +2020,61 @@ input[type="checkbox"] { color: #d9f7ff; } +/* =========================== + TRASH VIEW + =========================== */ +.item-card .trash-card-meta { + font-size: 11px; + opacity: 0.84; + margin-top: 8px; + line-height: 1.45; + word-break: break-all; + color: rgba(226, 232, 240, 0.9); +} + +.item-card .trash-card-actions { + display: flex; + gap: 10px; + margin-top: 12px; + align-items: center; +} + +.item-card button.trash-restore-btn { + appearance: none; + -webkit-appearance: none; + border: 1px solid rgba(56, 189, 248, 0.45); + background: linear-gradient(135deg, rgba(10, 88, 122, 0.58), rgba(30, 64, 175, 0.55)); + color: #e0f2fe; + border-radius: 9px; + padding: 7px 12px; + min-height: 32px; + font-size: 12px; + font-weight: 700; + line-height: 1; + letter-spacing: 0.2px; + cursor: pointer; + box-shadow: 0 8px 18px rgba(3, 19, 42, 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.12); + transition: transform 0.12s ease, box-shadow 0.12s ease, border-color 0.12s ease, background 0.12s ease; +} + +.item-card button.trash-restore-btn:hover { + border-color: rgba(56, 189, 248, 0.78); + background: linear-gradient(135deg, rgba(14, 116, 144, 0.64), rgba(37, 99, 235, 0.62)); + box-shadow: 0 10px 22px rgba(7, 42, 90, 0.3), 0 0 0 2px rgba(56, 189, 248, 0.16); + transform: translateY(-1px); +} + +.item-card button.trash-restore-btn:active { + transform: translateY(0); +} + +.item-card button.trash-restore-btn:disabled { + opacity: 0.62; + cursor: wait; + transform: none; + box-shadow: none; +} + .repo-owner-tab.active { border-color: rgba(88, 213, 255, 0.65); background: linear-gradient(135deg, rgba(88, 213, 255, 0.2), rgba(92, 135, 255, 0.18));