Upload via GUI (28 Dateien)
This commit is contained in:
+420
-29
@@ -364,6 +364,35 @@ function loadRepos() {
|
||||
// Einmalig auf Modulebene definiert, damit die Funktion sofort ab Programmstart
|
||||
// verfügbar ist (früher versehentlich in loadRepoContents definiert -> beim
|
||||
// Klick vor dem ersten Repo-Laden "resetProjectGravurTitle is not a function").
|
||||
// Pfadanzeige "repo / ordner / unterordner". Jeder Teil ist klickbar und springt
|
||||
// direkt in diesen Ordner; der aktuelle Ordner (letzter Teil) ist nicht klickbar.
|
||||
function setProjectGravurTitle(repo, path, owner) {
|
||||
const gravurTitle = document.getElementById('project-gravur-title');
|
||||
if (!gravurTitle) return;
|
||||
gravurTitle.textContent = '';
|
||||
const parts = String(path || '').split('/').filter(Boolean);
|
||||
const addCrumb = (text, className, targetPath) => {
|
||||
const el = document.createElement('span');
|
||||
el.className = className;
|
||||
el.textContent = text;
|
||||
if (owner && targetPath !== null) {
|
||||
el.classList.add('gravur-link');
|
||||
el.title = targetPath ? `Zu "${targetPath}" springen` : `Zum Hauptordner von ${repo}`;
|
||||
el.onclick = () => loadRepoContents(owner, repo, targetPath);
|
||||
}
|
||||
gravurTitle.appendChild(el);
|
||||
};
|
||||
addCrumb(repo, 'gravur-repo', parts.length ? '' : null);
|
||||
parts.forEach((part, i) => {
|
||||
const sep = document.createElement('span');
|
||||
sep.className = 'gravur-sub';
|
||||
sep.textContent = ' / ';
|
||||
gravurTitle.appendChild(sep);
|
||||
const isLast = i === parts.length - 1;
|
||||
addCrumb(part, 'gravur-sub', isLast ? null : parts.slice(0, i + 1).join('/'));
|
||||
});
|
||||
}
|
||||
|
||||
window.resetProjectGravurTitle = function() {
|
||||
const gravurTitle = document.getElementById('project-gravur-title');
|
||||
if (gravurTitle) gravurTitle.textContent = '';
|
||||
@@ -388,6 +417,11 @@ async function loadGithubRepos(requestId = null) {
|
||||
const btnReleases = $('btnReleases');
|
||||
if (btnCommits) btnCommits.classList.add('hidden');
|
||||
if (btnReleases) btnReleases.classList.add('hidden');
|
||||
const btnCleanupIgnored = $('btnCleanupIgnored');
|
||||
if (btnCleanupIgnored) btnCleanupIgnored.classList.add('hidden');
|
||||
const btnGitignoreTemplate = $('btnGitignoreTemplate');
|
||||
if (btnGitignoreTemplate) btnGitignoreTemplate.classList.add('hidden');
|
||||
if (typeof window.updateFeatureButtons === 'function') window.updateFeatureButtons('list');
|
||||
|
||||
const grid = $('explorerGrid');
|
||||
if (grid) grid.style.gridTemplateColumns = '';
|
||||
@@ -860,16 +894,54 @@ async function uploadDroppedPaths({ paths, owner, repo, destPath = '', cloneUrl
|
||||
// viele Objekte gedroppt wurden. Das war die Ursache für die langen
|
||||
// Wartezeiten und Zeitüberschreitungen (vorher: ein kompletter Clone+Push
|
||||
// pro einzelner Datei bzw. pro Ordner).
|
||||
showProgress(15, `Bereite Upload von ${safePaths.length} Objekt(en) vor ...`);
|
||||
// Vorschau: welche Dateien gehen hoch, was sortiert die .gitignore aus, sind Geheimnisse dabei?
|
||||
let excludeFiles = [];
|
||||
if (typeof window.uploadPreviewGate === 'function') {
|
||||
const gate = await window.uploadPreviewGate({ paths: safePaths, destPath, repo });
|
||||
if (!gate.proceed) return { ok: false, cancelled: true, error: 'Upload abgebrochen' };
|
||||
excludeFiles = gate.excludeFiles || [];
|
||||
}
|
||||
|
||||
showProgress(15, `Bereite Upload von ${safePaths.length} Objekt(en) vor ...`, { cancellable: true });
|
||||
try {
|
||||
const res = await withTimeout(window.electronAPI.uploadPathsToGitea({
|
||||
let res = await withTimeout(window.electronAPI.uploadPathsToGitea({
|
||||
paths: safePaths,
|
||||
owner,
|
||||
repo,
|
||||
destPath,
|
||||
branch,
|
||||
excludeFiles,
|
||||
messagePrefix: 'Upload via GUI'
|
||||
}), 900000, 'upload-paths-to-gitea'); // 15 Min Sicherheits-Timeout
|
||||
|
||||
// Teilfehler: nur die fehlgeschlagenen Dateien erneut senden (höchstens 3 Wiederholungen).
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
const failedItems = !res?.ok && !res?.cancelled && Array.isArray(res?.retryItems) ? res.retryItems : [];
|
||||
if (failedItems.length === 0) break;
|
||||
showProgress(100, 'Upload teilweise fehlgeschlagen');
|
||||
const uploadedInfo = res.uploadedCount ? `${res.uploadedCount} Datei(en) wurden hochgeladen, ` : '';
|
||||
const again = await showActionConfirmModal({
|
||||
title: 'Upload teilweise fehlgeschlagen',
|
||||
message: `${uploadedInfo}${failedItems.length} Datei(en) nicht.\n\n${res.error || ''}\n\nNur die fehlgeschlagenen Dateien erneut senden?`,
|
||||
confirmText: 'Nur Fehlgeschlagene wiederholen',
|
||||
danger: true
|
||||
});
|
||||
if (!again) break;
|
||||
showProgress(15, `Wiederhole ${failedItems.length} Datei(en) ...`, { cancellable: true });
|
||||
res = await withTimeout(window.electronAPI.uploadPathsToGitea({
|
||||
items: failedItems,
|
||||
owner,
|
||||
repo,
|
||||
destPath,
|
||||
branch,
|
||||
messagePrefix: 'Upload via GUI (Wiederholung)'
|
||||
}), 900000, 'upload-paths-to-gitea');
|
||||
}
|
||||
|
||||
if (res?.cancelled) {
|
||||
hideProgress();
|
||||
return { ok: false, cancelled: true, error: 'Upload abgebrochen' };
|
||||
}
|
||||
showProgress(100, 'Upload abgeschlossen');
|
||||
console.log('[UPLOAD_DEBUG][renderer] uploadDroppedPaths:result', res);
|
||||
try { window.electronAPI.debugToMain('log', 'uploadDroppedPaths:result', { count: safePaths.length, ok: res?.ok, failedCount: res?.failedCount }); } catch (_) {}
|
||||
@@ -1378,6 +1450,9 @@ function openSettingsForFix(focusId = null) {
|
||||
if (!modal) return;
|
||||
modal.classList.remove('hidden');
|
||||
$('settingsWatermarkCard')?.classList.add('hidden');
|
||||
// Zugangsdaten liegen im Reiter "Allgemein"
|
||||
if (typeof window.showSettingsTab === 'function') window.showSettingsTab('general');
|
||||
if (window.gmPrefs) window.gmPrefs.takeSnapshot();
|
||||
renderSettingsHealth();
|
||||
requestAnimationFrame(() => {
|
||||
syncSettingsPanelHeights();
|
||||
@@ -2112,6 +2187,8 @@ function syncSettingsPanelHeights() {
|
||||
if (window.matchMedia('(max-width: 1120px)').matches) return;
|
||||
|
||||
if (credentialsPanel && healthPanel) {
|
||||
// Nur angleichen, wenn beide Karten nebeneinander stehen (im Bereich "Konten" stehen sie untereinander)
|
||||
if (Math.abs(credentialsPanel.getBoundingClientRect().top - healthPanel.getBoundingClientRect().top) > 4) return;
|
||||
const topRightTargetHeight = Math.ceil(credentialsPanel.getBoundingClientRect().height);
|
||||
if (topRightTargetHeight > 0) {
|
||||
healthPanel.style.minHeight = `${topRightTargetHeight}px`;
|
||||
@@ -2614,16 +2691,90 @@ function ensureProgressUI() {
|
||||
|
||||
barWrap.appendChild(bar);
|
||||
container.appendChild(barWrap);
|
||||
|
||||
// Abbrechen-Knopf: nur bei Uploads sichtbar (showProgress mit { cancellable: true }).
|
||||
const cancel = document.createElement('button');
|
||||
cancel.id = 'folderProgressCancel';
|
||||
cancel.textContent = CANCEL_LABEL;
|
||||
cancel.style.cssText = `
|
||||
display: none;
|
||||
margin-top: 10px;
|
||||
padding: 5px 14px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(255,255,255,0.25);
|
||||
background: transparent;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
`;
|
||||
cancel.addEventListener('click', async () => {
|
||||
cancel.disabled = true;
|
||||
cancel.textContent = 'Wird abgebrochen …';
|
||||
try { await window.electronAPI.cancelUpload(); } catch (_) {}
|
||||
});
|
||||
container.appendChild(cancel);
|
||||
|
||||
// Esc bricht die laufende Übertragung ab, solange kein Dialog offen ist.
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key !== 'Escape') return;
|
||||
if (container.style.display === 'none' || cancel.style.display === 'none' || cancel.disabled) return;
|
||||
if (document.querySelector('.modal:not(.hidden)')) return;
|
||||
cancel.click();
|
||||
});
|
||||
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
function showProgress(percent, text) {
|
||||
const CANCEL_LABEL = 'Abbrechen (Esc)';
|
||||
|
||||
function formatTransferBytes(bytes) {
|
||||
if (!(bytes > 0)) return '0 KB';
|
||||
if (bytes < 1024 * 1024) return `${Math.max(1, Math.round(bytes / 1024))} KB`;
|
||||
if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1).replace('.', ',')} MB`;
|
||||
return `${(bytes / 1024 / 1024 / 1024).toFixed(2).replace('.', ',')} GB`;
|
||||
}
|
||||
|
||||
function formatRemaining(seconds) {
|
||||
const s = Math.round(seconds);
|
||||
if (s < 60) return `${Math.max(1, s)} Sek.`;
|
||||
const m = Math.floor(s / 60);
|
||||
if (m < 60) return `${m} Min. ${s % 60} Sek.`;
|
||||
return `${Math.floor(m / 60)} Std. ${m % 60} Min.`;
|
||||
}
|
||||
|
||||
// Text für den Fortschrittsbalken: Dateien, übertragene MB und geschätzte Restzeit.
|
||||
// Die Restzeit ist eine Hochrechnung und erscheint erst, wenn genug Daten vorliegen.
|
||||
let transferStartedAt = 0;
|
||||
let transferLabel = '';
|
||||
function describeTransferProgress(label, p) {
|
||||
if (!transferStartedAt || transferLabel !== label || !p.percent) {
|
||||
transferStartedAt = Date.now();
|
||||
transferLabel = label;
|
||||
}
|
||||
let text = `${label}: ${p.processed}/${p.total} Dateien`;
|
||||
if (p.totalBytes > 0) {
|
||||
text += ` · ${formatTransferBytes(p.totalBytes * p.percent / 100)} / ${formatTransferBytes(p.totalBytes)}`;
|
||||
}
|
||||
const elapsed = (Date.now() - transferStartedAt) / 1000;
|
||||
if (p.percent >= 5 && p.percent < 100 && elapsed >= 3) {
|
||||
text += ` · noch ca. ${formatRemaining(elapsed * (100 - p.percent) / p.percent)}`;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
function showProgress(percent, text, { cancellable = false } = {}) {
|
||||
ensureProgressUI();
|
||||
const container = $('folderProgressContainer');
|
||||
const bar = $('folderProgressBar');
|
||||
const txt = $('folderProgressText');
|
||||
const cancel = $('folderProgressCancel');
|
||||
if (txt) txt.innerText = text || '';
|
||||
if (bar) bar.style.width = `${Math.min(100, Math.max(0, percent))}%`;
|
||||
if (cancel) {
|
||||
cancel.style.display = cancellable ? 'inline-block' : 'none';
|
||||
// Ein neuer Upload startet wieder mit aktivem Knopf; bei laufendem Abbruch bleibt er gesperrt.
|
||||
if (!cancellable) { cancel.disabled = false; cancel.textContent = CANCEL_LABEL; }
|
||||
}
|
||||
if (container) container.style.display = 'block';
|
||||
}
|
||||
|
||||
@@ -2632,6 +2783,9 @@ function hideProgress() {
|
||||
if (container) {
|
||||
setTimeout(() => {
|
||||
container.style.display = 'none';
|
||||
// Knopf für den nächsten Vorgang zurücksetzen (nach einem Abbruch ist er gesperrt).
|
||||
const cancel = $('folderProgressCancel');
|
||||
if (cancel) { cancel.disabled = false; cancel.textContent = CANCEL_LABEL; cancel.style.display = 'none'; }
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
@@ -3473,7 +3627,12 @@ async function loadGiteaRepos(preloadedData = null, requestId = null) {
|
||||
const btnReleases = $('btnReleases');
|
||||
if (btnCommits) btnCommits.classList.add('hidden');
|
||||
if (btnReleases) btnReleases.classList.add('hidden');
|
||||
|
||||
const btnCleanupIgnored = $('btnCleanupIgnored');
|
||||
if (btnCleanupIgnored) btnCleanupIgnored.classList.add('hidden');
|
||||
const btnGitignoreTemplate = $('btnGitignoreTemplate');
|
||||
if (btnGitignoreTemplate) btnGitignoreTemplate.classList.add('hidden');
|
||||
if (typeof window.updateFeatureButtons === 'function') window.updateFeatureButtons('list');
|
||||
|
||||
// WICHTIG: Grid-Layout zurücksetzen
|
||||
const grid = $('explorerGrid');
|
||||
if (grid) {
|
||||
@@ -3879,7 +4038,9 @@ async function loadGiteaRepos(preloadedData = null, requestId = null) {
|
||||
branch: getDefaultBranch(owner, repoName)
|
||||
});
|
||||
console.log('[UPLOAD_DEBUG][renderer] repoCard:dropResult', res);
|
||||
if (!res.ok) {
|
||||
if (res.cancelled) {
|
||||
setStatus('Upload abgebrochen');
|
||||
} else if (!res.ok) {
|
||||
showError('Fehler: ' + (res.error || 'Upload fehlgeschlagen'));
|
||||
await showUploadFailureModal('Upload fehlgeschlagen', res, 'Upload fehlgeschlagen');
|
||||
setStatus('Upload fehlgeschlagen');
|
||||
@@ -3916,11 +4077,9 @@ async function loadGiteaRepos(preloadedData = null, requestId = null) {
|
||||
}
|
||||
|
||||
async function loadRepoContents(owner, repo, path) {
|
||||
// Projekttitel als Gravur unterhalb der Toolbar setzen (nur Repo-Name)
|
||||
const gravurTitle = document.getElementById('project-gravur-title');
|
||||
if (gravurTitle) {
|
||||
gravurTitle.textContent = repo;
|
||||
}
|
||||
// Projekttitel als Gravur unterhalb der Toolbar setzen: Repo-Name bleibt
|
||||
// sichtbar, ein geöffneter Unterordner wird als Pfad dahinter angezeigt.
|
||||
setProjectGravurTitle(repo, path, owner);
|
||||
currentState.view = 'gitea-repo';
|
||||
currentState.owner = owner;
|
||||
currentState.repo = repo;
|
||||
@@ -3940,6 +4099,20 @@ async function loadRepoContents(owner, repo, path) {
|
||||
btnReleases.onclick = () => loadRepoReleases(owner, repo);
|
||||
}
|
||||
|
||||
// Aufräumen nutzt die Gitea-API und ist daher nur dort verfügbar.
|
||||
const btnCleanupIgnored = $('btnCleanupIgnored');
|
||||
if (btnCleanupIgnored) {
|
||||
const isGitea = currentState.platform !== 'github';
|
||||
btnCleanupIgnored.classList.toggle('hidden', !isGitea);
|
||||
btnCleanupIgnored.onclick = () => cleanupGitignoredFiles(owner, repo, currentState.path || '');
|
||||
}
|
||||
const btnGitignoreTemplate = $('btnGitignoreTemplate');
|
||||
if (btnGitignoreTemplate) {
|
||||
btnGitignoreTemplate.classList.remove('hidden');
|
||||
btnGitignoreTemplate.onclick = () => createGitignoreForRepo(owner, repo);
|
||||
}
|
||||
if (typeof window.updateFeatureButtons === 'function') window.updateFeatureButtons('repo', owner, repo);
|
||||
|
||||
// WICHTIG: Grid-Layout zurücksetzen
|
||||
const grid = $('explorerGrid');
|
||||
if (grid) {
|
||||
@@ -4063,7 +4236,9 @@ async function loadRepoContents(owner, repo, path) {
|
||||
destPath: targetPath,
|
||||
branch: getDefaultBranch(owner, repo)
|
||||
});
|
||||
if (!res.ok) {
|
||||
if (res.cancelled) {
|
||||
setStatus('Upload abgebrochen');
|
||||
} else if (!res.ok) {
|
||||
showError('Upload error: ' + (res.error || 'Unbekannter Fehler'));
|
||||
await showUploadFailureModal('Upload fehlgeschlagen', res, 'Upload fehlgeschlagen');
|
||||
}
|
||||
@@ -4123,6 +4298,7 @@ async function selectLocalFolder() {
|
||||
setStatus('Local: ' + folder);
|
||||
currentState.view = 'local';
|
||||
updateNavigationUI();
|
||||
if (typeof window.updateFeatureButtons === 'function') window.updateFeatureButtons('local');
|
||||
|
||||
await refreshLocalTree(folder);
|
||||
await loadBranches(folder);
|
||||
@@ -4745,15 +4921,20 @@ function showRepoContextMenu(ev, owner, repoName, cloneUrl, element, isPrivate =
|
||||
try {
|
||||
const sel = await window.electronAPI.selectFolder();
|
||||
if (sel) {
|
||||
showProgress(0, 'Upload...');
|
||||
const res = await window.electronAPI.uploadLocalFolderToGitea({
|
||||
localFolder: sel,
|
||||
owner,
|
||||
repo: repoName,
|
||||
destPath: '',
|
||||
showProgress(0, 'Upload...', { cancellable: true });
|
||||
const res = await window.electronAPI.uploadLocalFolderToGitea({
|
||||
localFolder: sel,
|
||||
owner,
|
||||
repo: repoName,
|
||||
destPath: '',
|
||||
branch: getDefaultBranch(owner, repoName),
|
||||
messagePrefix: 'Upload folder via GUI'
|
||||
});
|
||||
if (res?.cancelled) {
|
||||
hideProgress();
|
||||
setStatus('Upload abgebrochen');
|
||||
return;
|
||||
}
|
||||
if (!res?.ok) {
|
||||
showError('Upload fehlgeschlagen: ' + (res?.error || 'Unbekannter Fehler'));
|
||||
await showUploadFailureModal('Upload fehlgeschlagen', res, 'Upload fehlgeschlagen');
|
||||
@@ -4915,10 +5096,11 @@ function showGiteaItemContextMenu(ev, item, owner, repo) {
|
||||
});
|
||||
} else {
|
||||
addItem('📥', 'Ordner herunterladen', async () => {
|
||||
showProgress(0, `Lade ${item.name}...`);
|
||||
showProgress(0, `Lade ${item.name}...`, { cancellable: true });
|
||||
const res = await window.electronAPI.downloadGiteaFolder({ owner, repo, path: item.path });
|
||||
hideProgress();
|
||||
setStatus(res.ok ? `Gespeichert: ${res.savedTo}` : 'Download fehlgeschlagen');
|
||||
if (res.cancelled) setStatus('Download abgebrochen');
|
||||
else setStatus(res.ok ? `Gespeichert: ${res.savedTo}` : 'Download fehlgeschlagen');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5340,6 +5522,193 @@ function showActionConfirmModal({ title, message, confirmText = 'Bestätigen', d
|
||||
});
|
||||
}
|
||||
|
||||
// Entfernt Dateien aus dem Repo, die laut .gitignore des passenden lokalen Ordners
|
||||
// nicht hineingehören (z. B. build/ oder .gradle/ aus früheren Uploads).
|
||||
// Auswahldialog für die .gitignore-Vorlagen; liefert die gewählten Schlüssel oder null.
|
||||
function pickGitignoreTemplates(templates) {
|
||||
return new Promise((resolve) => {
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'modal confirm-modal';
|
||||
modal.style.zIndex = '99999';
|
||||
const card = document.createElement('div');
|
||||
card.className = 'modalContent card confirm-modal-content';
|
||||
|
||||
const titleEl = document.createElement('h2');
|
||||
titleEl.className = 'confirm-modal-title';
|
||||
titleEl.textContent = '📄 .gitignore anlegen';
|
||||
|
||||
const msgEl = document.createElement('p');
|
||||
msgEl.className = 'confirm-modal-message';
|
||||
msgEl.textContent = 'Der allgemeine Teil (Betriebssystem, Editor, .env, Logs) ist immer dabei. Zusätzlich wählen:';
|
||||
|
||||
const list = document.createElement('div');
|
||||
list.style.cssText = 'display:flex;flex-direction:column;gap:6px;margin:10px 0 14px;';
|
||||
for (const t of templates) {
|
||||
const label = document.createElement('label');
|
||||
label.style.cssText = 'display:flex;align-items:center;gap:8px;cursor:pointer;';
|
||||
const box = document.createElement('input');
|
||||
box.type = 'checkbox';
|
||||
box.value = t.key;
|
||||
label.appendChild(box);
|
||||
label.appendChild(document.createTextNode(t.label));
|
||||
list.appendChild(label);
|
||||
}
|
||||
|
||||
const btnWrap = document.createElement('div');
|
||||
btnWrap.className = 'modal-buttons confirm-modal-buttons';
|
||||
const okBtn = document.createElement('button');
|
||||
okBtn.className = 'confirm-modal-btn confirm-modal-btn--primary';
|
||||
okBtn.textContent = 'Anlegen';
|
||||
const cancelBtn = document.createElement('button');
|
||||
cancelBtn.className = 'confirm-modal-btn confirm-modal-btn--secondary';
|
||||
cancelBtn.textContent = 'Abbrechen';
|
||||
btnWrap.appendChild(okBtn);
|
||||
btnWrap.appendChild(cancelBtn);
|
||||
|
||||
card.appendChild(titleEl);
|
||||
card.appendChild(msgEl);
|
||||
card.appendChild(list);
|
||||
card.appendChild(btnWrap);
|
||||
modal.appendChild(card);
|
||||
document.body.appendChild(modal);
|
||||
|
||||
const onKey = (e) => { if (e.key === 'Escape') close(null); };
|
||||
function close(value) {
|
||||
document.removeEventListener('keydown', onKey);
|
||||
modal.remove();
|
||||
resolve(value);
|
||||
}
|
||||
document.addEventListener('keydown', onKey);
|
||||
okBtn.onclick = () => close([...list.querySelectorAll('input:checked')].map(i => i.value));
|
||||
cancelBtn.onclick = () => close(null);
|
||||
});
|
||||
}
|
||||
|
||||
// Schreibt eine Standard-.gitignore aus den gewählten Vorlagen ins Repo.
|
||||
async function createGitignoreForRepo(owner, repo) {
|
||||
const tpl = await window.electronAPI.getGitignoreTemplates();
|
||||
const keys = await pickGitignoreTemplates((tpl && tpl.templates) || []);
|
||||
if (!keys) return;
|
||||
|
||||
const built = await window.electronAPI.buildGitignore({ keys });
|
||||
const ref = getDefaultBranch(owner, repo);
|
||||
const platform = currentState.platform;
|
||||
|
||||
const existing = await window.electronAPI.getGiteaFileContent({ owner, repo, path: '.gitignore', ref, platform });
|
||||
if (existing && existing.ok) {
|
||||
const overwrite = await showActionConfirmModal({
|
||||
title: '.gitignore existiert bereits',
|
||||
message: `In "${repo}" gibt es schon eine .gitignore. Sie wird ersetzt, der alte Stand bleibt in der Commit-Historie.`,
|
||||
confirmText: 'Ersetzen',
|
||||
danger: true
|
||||
});
|
||||
if (!overwrite) return;
|
||||
}
|
||||
|
||||
showProgress(30, '.gitignore wird geschrieben...');
|
||||
const res = await window.electronAPI.writeGiteaFile({
|
||||
owner, repo, path: '.gitignore', content: built.content, ref, platform
|
||||
});
|
||||
hideProgress();
|
||||
if (res && res.ok) {
|
||||
showSuccess('.gitignore angelegt');
|
||||
loadRepoContents(owner, repo, currentState.path);
|
||||
} else {
|
||||
showError('.gitignore konnte nicht angelegt werden: ' + (res?.error || 'Unbekannter Fehler'));
|
||||
}
|
||||
}
|
||||
|
||||
// Zeigt die zuletzt aufgezeichneten Uploads; der Bestätigen-Knopf leert den Verlauf.
|
||||
async function showUploadHistory() {
|
||||
const res = await window.electronAPI.getUploadHistory();
|
||||
const history = (res && res.history) || [];
|
||||
if (history.length === 0) {
|
||||
await showInfoModal('Upload-Verlauf', 'Noch keine Uploads aufgezeichnet.');
|
||||
return;
|
||||
}
|
||||
const icons = { ok: '✅', abgebrochen: '⏹️', fehler: '❌' };
|
||||
const lines = history.slice(0, 25).map((e) => {
|
||||
const when = new Date(e.at).toLocaleString('de-DE');
|
||||
const files = e.files != null ? `${e.files} Datei(en)` : '–';
|
||||
const failed = e.failed ? `, ${e.failed} fehlgeschlagen` : '';
|
||||
const took = e.durationMs ? ` · ${formatRemaining(e.durationMs / 1000)}` : '';
|
||||
const err = e.error ? `\n ${e.error}` : '';
|
||||
return `${icons[e.status] || '•'} ${when} · ${e.repo || '?'} · ${files}${failed} · ${e.kind}${took}${err}`;
|
||||
});
|
||||
if (history.length > 25) lines.push(`… und ${history.length - 25} ältere Einträge`);
|
||||
const clear = await showActionConfirmModal({
|
||||
title: 'Upload-Verlauf',
|
||||
message: lines.join('\n'),
|
||||
confirmText: 'Verlauf leeren'
|
||||
});
|
||||
if (clear) await window.electronAPI.clearUploadHistory();
|
||||
}
|
||||
|
||||
async function cleanupGitignoredFiles(owner, repo, repoPath) {
|
||||
const where = repoPath ? `${repo}/${repoPath}` : repo;
|
||||
const proceed = await showActionConfirmModal({
|
||||
title: 'Repo aufräumen',
|
||||
message: `Wähle im nächsten Schritt den lokalen Ordner, der zu "${where}" gehört. Dessen .gitignore entscheidet, welche Dateien im Repo nicht hingehören. Vor dem Löschen siehst du die Liste.`,
|
||||
confirmText: 'Ordner wählen'
|
||||
});
|
||||
if (!proceed) return;
|
||||
|
||||
const localFolder = await window.electronAPI.selectFolder();
|
||||
if (!localFolder) return;
|
||||
|
||||
showProgress(0, 'Suche Dateien, die laut .gitignore nicht ins Repo gehören...');
|
||||
const found = await window.electronAPI.findGitignoredInRepo({
|
||||
owner, repo, repoPath, localFolder, branch: getDefaultBranch(owner, repo)
|
||||
});
|
||||
hideProgress();
|
||||
if (!found || !found.ok) {
|
||||
await showInfoModal('Aufräumen fehlgeschlagen', found?.error || 'Unbekannter Fehler', true);
|
||||
return;
|
||||
}
|
||||
if (found.files.length === 0) {
|
||||
await showInfoModal('Nichts zu tun', `Alle ${found.scanned} Dateien in "${where}" sind laut .gitignore in Ordnung.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Übersicht nach oberster Ebene gruppieren, damit auch tausende Dateien lesbar bleiben.
|
||||
const groups = new Map();
|
||||
const prefixLen = repoPath ? repoPath.length + 1 : 0;
|
||||
for (const p of found.files) {
|
||||
const rel = p.slice(prefixLen);
|
||||
const top = rel.includes('/') ? rel.slice(0, rel.indexOf('/') + 1) : rel;
|
||||
groups.set(top, (groups.get(top) || 0) + 1);
|
||||
}
|
||||
const lines = [...groups.entries()]
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 12)
|
||||
.map(([name, count]) => `• ${name} (${count} Dateien)`);
|
||||
if (groups.size > 12) lines.push(`• … und ${groups.size - 12} weitere`);
|
||||
|
||||
const ok = await showActionConfirmModal({
|
||||
title: `${found.files.length} Dateien entfernen?`,
|
||||
message: `Diese Dateien liegen in "${where}", gehören laut .gitignore aber nicht ins Repo:\n\n${lines.join('\n')}\n\nSie werden nur auf dem Server gelöscht, lokal bleibt alles erhalten. Die Löschung ist über die Commit-Historie nachvollziehbar.`,
|
||||
confirmText: 'Entfernen',
|
||||
danger: true
|
||||
});
|
||||
if (!ok) return;
|
||||
|
||||
showProgress(0, `Entferne ${found.files.length} Dateien...`);
|
||||
const res = await window.electronAPI.deleteGiteaPaths({
|
||||
owner, repo, paths: found.files, branch: found.branch,
|
||||
message: 'Remove files ignored by .gitignore via Git Manager GUI'
|
||||
});
|
||||
hideProgress();
|
||||
if (res && res.ok) {
|
||||
showSuccess(`${res.deleted} Dateien in ${res.commits} Commits entfernt`);
|
||||
} else {
|
||||
const detail = res?.failed?.length
|
||||
? `${res.deleted} entfernt, ${res.failed.length} fehlgeschlagen:\n` + res.failed.slice(0, 5).map(f => `${f.path}: ${f.error}`).join('\n')
|
||||
: (res?.error || 'Unbekannter Fehler');
|
||||
await showInfoModal('Aufräumen unvollständig', detail, true);
|
||||
}
|
||||
loadRepoContents(owner, repo, currentState.path);
|
||||
}
|
||||
|
||||
function showInfoModal(title, message, danger = false) {
|
||||
return showActionConfirmModal({
|
||||
title,
|
||||
@@ -5503,7 +5872,7 @@ function setupBackgroundContextMenu() {
|
||||
);
|
||||
} else if (clipboard.item.isLocal) {
|
||||
addItem('📋', `⬆️ Von Lokal einfügen: "${clipboard.item.name}"`, async () => {
|
||||
showProgress(0, `Lade "${clipboard.item.name}" hoch...`);
|
||||
showProgress(0, `Lade "${clipboard.item.name}" hoch...`, { cancellable: true });
|
||||
try {
|
||||
const res = clipboard.item.isDirectory
|
||||
? await window.electronAPI.uploadLocalFolderToGitea({
|
||||
@@ -5521,6 +5890,7 @@ function setupBackgroundContextMenu() {
|
||||
destPath: currentPath,
|
||||
branch: getDefaultBranch(owner, repo)
|
||||
});
|
||||
if (res?.cancelled) { setStatus('Upload abgebrochen'); return; }
|
||||
if (!res?.ok) throw new Error(res?.error || 'Upload fehlgeschlagen');
|
||||
showSuccess(`"${clipboard.item.name}" nach Gitea kopiert`);
|
||||
loadRepoContents(owner, repo, currentState.path);
|
||||
@@ -5548,14 +5918,15 @@ function setupBackgroundContextMenu() {
|
||||
);
|
||||
} else if (clipboard.item.isGitea) {
|
||||
addItem('📋', `⬇️ Von Gitea einfügen: "${clipboard.item.name}"`, async () => {
|
||||
showProgress(0, `Lade "${clipboard.item.name}" herunter...`);
|
||||
showProgress(0, `Lade "${clipboard.item.name}" herunter...`, { cancellable: true });
|
||||
try {
|
||||
await window.electronAPI.downloadGiteaFolder({
|
||||
const dl = await window.electronAPI.downloadGiteaFolder({
|
||||
owner: clipboard.item.owner,
|
||||
repo: clipboard.item.repo,
|
||||
giteaPath: clipboard.item.path,
|
||||
localPath: selectedFolder
|
||||
});
|
||||
if (dl?.cancelled) { setStatus('Download abgebrochen'); return; }
|
||||
showSuccess(`"${clipboard.item.name}" nach Lokal kopiert`);
|
||||
refreshLocalTree(selectedFolder);
|
||||
} catch(e) { showError('Cross-Paste fehlgeschlagen'); }
|
||||
@@ -5636,7 +6007,9 @@ function setupGlobalDropZone() {
|
||||
destPath: targetPath,
|
||||
branch: getDefaultBranch(owner, repo)
|
||||
});
|
||||
if (!res.ok) {
|
||||
if (res.cancelled) {
|
||||
setStatus('Upload abgebrochen');
|
||||
} else if (!res.ok) {
|
||||
console.error('Upload error:', res.error);
|
||||
showError('Error: ' + (res.error || 'Upload failed'));
|
||||
}
|
||||
@@ -5876,6 +6249,10 @@ window.addEventListener('DOMContentLoaded', async () => {
|
||||
};
|
||||
}
|
||||
|
||||
if ($('btnUploadHistory')) {
|
||||
$('btnUploadHistory').onclick = () => { showUploadHistory(); };
|
||||
}
|
||||
|
||||
if ($('btnOpenActivityLog')) {
|
||||
$('btnOpenActivityLog').onclick = () => {
|
||||
$('activityLogModal')?.classList.remove('hidden');
|
||||
@@ -5979,14 +6356,18 @@ window.addEventListener('DOMContentLoaded', async () => {
|
||||
logActivity('info', `Batch gestartet: ${action} (${repos.length} Repos)`);
|
||||
|
||||
try {
|
||||
if (action === 'clone') showProgress(0, `Klone 0/${repos.length} Repos ...`, { cancellable: true });
|
||||
const res = await window.electronAPI.runBatchRepoAction({ action, repos, options });
|
||||
if (action === 'clone') hideProgress();
|
||||
if (!res.ok) {
|
||||
showError(res.error || 'Batch-Aktion fehlgeschlagen');
|
||||
return;
|
||||
}
|
||||
|
||||
const summary = res.summary || { total: repos.length, success: 0, failed: 0 };
|
||||
if (summary.failed > 0) {
|
||||
if (res.cancelled) {
|
||||
showWarning(`Batch abgebrochen: ${summary.success} von ${summary.total} Repos erledigt.`);
|
||||
} else if (summary.failed > 0) {
|
||||
showWarning(`Batch beendet: ${summary.success}/${summary.total} erfolgreich, ${summary.failed} fehlgeschlagen.`);
|
||||
} else {
|
||||
showSuccess(`Batch erfolgreich: ${summary.success}/${summary.total}`);
|
||||
@@ -6157,7 +6538,9 @@ window.addEventListener('DOMContentLoaded', async () => {
|
||||
}
|
||||
|
||||
if ($('btnCloseSettings')) {
|
||||
$('btnCloseSettings').onclick = () => {
|
||||
$('btnCloseSettings').onclick = async () => {
|
||||
// Bei ungespeicherten Änderungen erst nachfragen (settings-extra.js)
|
||||
if (typeof window.settingsCanClose === 'function' && !(await window.settingsCanClose())) return;
|
||||
$('settingsWatermarkCard')?.classList.add('hidden');
|
||||
$('settingsModal').classList.add('hidden');
|
||||
};
|
||||
@@ -6537,11 +6920,11 @@ window.addEventListener('DOMContentLoaded', async () => {
|
||||
|
||||
// Progress listeners
|
||||
window.electronAPI.onFolderUploadProgress(p => {
|
||||
showProgress(p.percent, `Upload: ${p.processed}/${p.total}`);
|
||||
showProgress(p.percent, describeTransferProgress('Upload', p), { cancellable: true });
|
||||
});
|
||||
|
||||
|
||||
window.electronAPI.onFolderDownloadProgress(p => {
|
||||
showProgress(p.percent, `Download: ${p.processed}/${p.total}`);
|
||||
showProgress(p.percent, describeTransferProgress('Download', p), { cancellable: true });
|
||||
});
|
||||
|
||||
if (window.electronAPI.onRetryQueueUpdated) {
|
||||
@@ -6566,6 +6949,9 @@ window.addEventListener('DOMContentLoaded', async () => {
|
||||
if (!payload) return;
|
||||
if (payload.status === 'running') {
|
||||
logActivity('info', `Batch ${payload.action}: ${payload.repo} (${payload.index}/${payload.total})`);
|
||||
if (payload.action === 'clone') {
|
||||
showProgress(Math.round(((payload.index - 1) / payload.total) * 100), `Klone ${payload.repo} (${payload.index}/${payload.total})`, { cancellable: true });
|
||||
}
|
||||
} else if (payload.status === 'error') {
|
||||
logActivity('error', `Batch ${payload.action}: ${payload.repo} - ${payload.error || 'Fehler'}`);
|
||||
}
|
||||
@@ -8215,7 +8601,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
// 2. AUTOMATISCHER UPDATE-CHECK BEIM START
|
||||
// Etwas spaeter, damit initiales UI/Repo-Laden nicht ausgebremst wird.
|
||||
setTimeout(() => {
|
||||
// Lässt sich unter Einstellungen → System abschalten.
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
const res = await window.electronAPI.prefsGet();
|
||||
if (res && res.ok && res.prefs.autoUpdateCheck === false) return;
|
||||
} catch (_) {}
|
||||
window.electronAPI.checkForUpdates({ silent: true });
|
||||
}, 12000);
|
||||
});
|
||||
Reference in New Issue
Block a user