Upload via GUI (85 Dateien)
This commit is contained in:
+17
-1
@@ -121,7 +121,8 @@
|
||||
const relLocal = (p) => (baseDir && p.startsWith(baseDir) ? p.slice(baseDir.length + 1) : p).split('\\').join('/');
|
||||
|
||||
// Einstellung "Upload-Vorschau: nur bei Auffälligkeiten" – ohne Zugangsdaten und ohne Dateiflut direkt hochladen
|
||||
if (res.previewMode === 'warnings' && secrets.length === 0 && files.length > 0 && files.length <= 1000) {
|
||||
const blocked = res.blocked || [];
|
||||
if (res.previewMode === 'warnings' && secrets.length === 0 && blocked.length === 0 && files.length > 0 && files.length <= 1000) {
|
||||
return { proceed: true, excludeFiles: [] };
|
||||
}
|
||||
|
||||
@@ -142,6 +143,21 @@
|
||||
m.body.append(el('div', { class: 'fx-alert fx-alert--warn', text: 'Es bleibt keine Datei zum Hochladen übrig.' }));
|
||||
}
|
||||
|
||||
if (blocked.length) {
|
||||
const list = el('div', { class: 'fx-list' });
|
||||
for (const b of blocked.slice(0, 25)) {
|
||||
list.append(el('div', { class: 'fx-list-row' }, [
|
||||
el('span', { class: 'fx-mono', text: b.targetPath || b.path }),
|
||||
el('span', { class: 'fx-muted', text: b.reason })
|
||||
]));
|
||||
}
|
||||
m.body.append(el('div', { class: 'fx-alert fx-alert--danger' }, [
|
||||
el('strong', { text: `🛡️ ${blocked.length} Datei(en) gesperrt und werden nicht hochgeladen` }),
|
||||
el('div', { text: 'Dateien mit Zugangsdaten werden nie hochgeladen. Die Sperre lässt sich unter Einstellungen → Uploads abschalten.' }),
|
||||
list
|
||||
]));
|
||||
}
|
||||
|
||||
const secretChecks = [];
|
||||
if (secrets.length) {
|
||||
const list = el('div', { class: 'fx-list' });
|
||||
|
||||
+28
-1
@@ -95,7 +95,7 @@
|
||||
<div class="tool-group tool-group--repo-tools">
|
||||
<span class="tool-group-title">Werkzeuge</span>
|
||||
<button id="btnCleanupIgnored" class="hidden" title="Dateien entfernen, die laut .gitignore nicht ins Repo gehören">🧹 Aufräumen</button>
|
||||
<button id="btnGitignoreTemplate" class="hidden" title="Standard-.gitignore (Node, .NET, Electron) im Repo anlegen">📄 .gitignore</button>
|
||||
<button id="btnGitignoreTemplate" class="hidden" title="Standard-.gitignore (Node, .NET, Java, Python …) im Repo anlegen">📄 .gitignore</button>
|
||||
<button id="btnTrash" class="hidden" style="display:none" title=""><!-- removed --></button>
|
||||
<button id="btnPurgeTrash" class="hidden" style="display:none" title=""><!-- removed --></button>
|
||||
</div>
|
||||
@@ -394,6 +394,16 @@
|
||||
<span class="toggle-track"></span>
|
||||
</span>
|
||||
</label>
|
||||
<label class="settings-toggle-row" for="prefBlockSecrets">
|
||||
<span class="settings-toggle-info">
|
||||
<span class="settings-toggle-title">🛡️ Zugangsdaten nie hochladen</span>
|
||||
<span class="settings-toggle-desc">Dateien wie credentials.json, .env oder private Schlüssel werden auf allen Upload-Wegen ausgelassen, auch wenn sie in der Vorschau nicht abgewählt wurden. Empfohlen bei öffentlichen Repos.</span>
|
||||
</span>
|
||||
<span class="toggle-switch">
|
||||
<input type="checkbox" id="prefBlockSecrets" checked>
|
||||
<span class="toggle-track"></span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="settings-backup-card">
|
||||
<div class="input-group">
|
||||
@@ -403,6 +413,13 @@
|
||||
<option value="warnings">Nur bei Zugangsdaten oder sehr vielen Dateien</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label for="prefNotifyTransfers">Windows-Meldung bei Upload-Ende</label>
|
||||
<select id="prefNotifyTransfers">
|
||||
<option value="hidden">Nur wenn die App nicht im Vordergrund ist</option>
|
||||
<option value="never">Nie</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="input-group input-group--wide">
|
||||
<label for="prefUploadExcludes">Zusätzlich nie hochladen (zusätzlich zur .gitignore)</label>
|
||||
<textarea id="prefUploadExcludes" class="settings-textarea" rows="6" spellcheck="false" placeholder="Ein Muster pro Zeile, z. B. *.log dist/ .idea/ *.psd"></textarea>
|
||||
@@ -471,6 +488,16 @@
|
||||
<span class="toggle-track"></span>
|
||||
</span>
|
||||
</label>
|
||||
<label class="settings-toggle-row" for="prefShowSplash">
|
||||
<span class="settings-toggle-info">
|
||||
<span class="settings-toggle-title">✨ Startanimation</span>
|
||||
<span class="settings-toggle-desc">Beim Öffnen dreht sich das Logo, danach baut sich das Fenster von der Mitte aus auf. Beim automatischen Start mit Windows (im Tray) gibt es sie nie.</span>
|
||||
</span>
|
||||
<span class="toggle-switch">
|
||||
<input type="checkbox" id="prefShowSplash" checked>
|
||||
<span class="toggle-track"></span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="settings-backup-card">
|
||||
<div class="input-group">
|
||||
|
||||
+343
-56
@@ -862,6 +862,46 @@ function showChipContextMenu(ev, entry, type) {
|
||||
setTimeout(() => document.addEventListener('click', () => menu.remove(), { once: true }), 10);
|
||||
}
|
||||
|
||||
// Teilfehler eines Uploads: fragt nach und sendet nur die fehlgeschlagenen Dateien erneut
|
||||
// (höchstens 3 Wiederholungen). `retryCall(failedItems)` führt den Upload erneut aus.
|
||||
async function retryFailedUploads(res, retryCall) {
|
||||
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 retryCall(failedItems);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
// Hinweis, wenn die feste Sperre Dateien mit Zugangsdaten vom Upload ausgenommen hat.
|
||||
function warnBlockedFiles(res) {
|
||||
const blocked = Array.isArray(res?.blockedFiles) ? res.blockedFiles : [];
|
||||
if (blocked.length === 0) return;
|
||||
const names = blocked.slice(0, 3).map(b => ppathBasename(b.path)).join(', ');
|
||||
showWarning(`🛡️ ${blocked.length} Datei(en) mit Zugangsdaten wurden nicht hochgeladen: ${names}${blocked.length > 3 ? ' …' : ''}`);
|
||||
}
|
||||
|
||||
// Ordner-Upload mit Wiederholung der fehlgeschlagenen Dateien.
|
||||
async function uploadFolderWithRetry(params) {
|
||||
const res = await window.electronAPI.uploadLocalFolderToGitea(params);
|
||||
warnBlockedFiles(res);
|
||||
return retryFailedUploads(res, (failedItems) => window.electronAPI.uploadLocalFolderToGitea({
|
||||
...params,
|
||||
items: failedItems,
|
||||
messagePrefix: `${params.messagePrefix || 'Upload folder via GUI'} (Wiederholung)`
|
||||
}));
|
||||
}
|
||||
|
||||
async function uploadDroppedPaths({ paths, owner, repo, destPath = '', cloneUrl = null, branch = 'HEAD' }) {
|
||||
try { window.electronAPI.debugToMain('log', 'uploadDroppedPaths:start', { owner, repo, destPath, branch, pathCount: Array.isArray(paths) ? paths.length : 0 }); } catch (_) {}
|
||||
console.log('[UPLOAD_DEBUG][renderer] uploadDroppedPaths:start', {
|
||||
@@ -913,30 +953,16 @@ async function uploadDroppedPaths({ paths, owner, repo, destPath = '', cloneUrl
|
||||
excludeFiles,
|
||||
messagePrefix: 'Upload via GUI'
|
||||
}), 900000, 'upload-paths-to-gitea'); // 15 Min Sicherheits-Timeout
|
||||
warnBlockedFiles(res);
|
||||
|
||||
// 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');
|
||||
}
|
||||
res = await retryFailedUploads(res, (failedItems) => withTimeout(window.electronAPI.uploadPathsToGitea({
|
||||
items: failedItems,
|
||||
owner,
|
||||
repo,
|
||||
destPath,
|
||||
branch,
|
||||
messagePrefix: 'Upload via GUI (Wiederholung)'
|
||||
}), 900000, 'upload-paths-to-gitea'));
|
||||
|
||||
if (res?.cancelled) {
|
||||
hideProgress();
|
||||
@@ -2712,7 +2738,24 @@ function ensureProgressUI() {
|
||||
cancel.textContent = 'Wird abgebrochen …';
|
||||
try { await window.electronAPI.cancelUpload(); } catch (_) {}
|
||||
});
|
||||
container.appendChild(cancel);
|
||||
// Pause-Knopf: hält die Übertragung an den nächsten Kontrollpunkten an und setzt sie fort.
|
||||
const pause = document.createElement('button');
|
||||
pause.id = 'folderProgressPause';
|
||||
pause.textContent = PAUSE_LABEL;
|
||||
pause.style.cssText = cancel.style.cssText;
|
||||
pause.addEventListener('click', async () => {
|
||||
pause.disabled = true;
|
||||
try {
|
||||
if (transferPaused) await window.electronAPI.resumeUpload();
|
||||
else await window.electronAPI.pauseUpload();
|
||||
} catch (_) {}
|
||||
pause.disabled = false;
|
||||
});
|
||||
|
||||
const buttonRow = document.createElement('div');
|
||||
buttonRow.style.cssText = 'display:flex;gap:8px;';
|
||||
buttonRow.append(pause, cancel);
|
||||
container.appendChild(buttonRow);
|
||||
|
||||
// Esc bricht die laufende Übertragung ab, solange kein Dialog offen ist.
|
||||
document.addEventListener('keydown', (e) => {
|
||||
@@ -2726,6 +2769,22 @@ function ensureProgressUI() {
|
||||
}
|
||||
|
||||
const CANCEL_LABEL = 'Abbrechen (Esc)';
|
||||
const PAUSE_LABEL = 'Pausieren';
|
||||
const RESUME_LABEL = 'Fortsetzen';
|
||||
const PAUSED_SUFFIX = ' · ⏸ pausiert';
|
||||
let transferPaused = false;
|
||||
|
||||
// Zeigt den Pausen-Zustand im Fortschrittsbalken (Knopfbeschriftung und Hinweis im Text).
|
||||
function applyPauseState(paused) {
|
||||
transferPaused = !!paused;
|
||||
const pause = $('folderProgressPause');
|
||||
const txt = $('folderProgressText');
|
||||
if (pause) pause.textContent = transferPaused ? RESUME_LABEL : PAUSE_LABEL;
|
||||
if (txt) {
|
||||
const base = txt.innerText.replace(PAUSED_SUFFIX, '');
|
||||
txt.innerText = transferPaused ? base + PAUSED_SUFFIX : base;
|
||||
}
|
||||
}
|
||||
|
||||
function formatTransferBytes(bytes) {
|
||||
if (!(bytes > 0)) return '0 KB';
|
||||
@@ -2768,8 +2827,10 @@ function showProgress(percent, text, { cancellable = false } = {}) {
|
||||
const bar = $('folderProgressBar');
|
||||
const txt = $('folderProgressText');
|
||||
const cancel = $('folderProgressCancel');
|
||||
if (txt) txt.innerText = text || '';
|
||||
const pause = $('folderProgressPause');
|
||||
if (txt) txt.innerText = (text || '') + (transferPaused && cancellable ? PAUSED_SUFFIX : '');
|
||||
if (bar) bar.style.width = `${Math.min(100, Math.max(0, percent))}%`;
|
||||
if (pause) pause.style.display = cancellable ? 'inline-block' : 'none';
|
||||
if (cancel) {
|
||||
cancel.style.display = cancellable ? 'inline-block' : 'none';
|
||||
// Ein neuer Upload startet wieder mit aktivem Knopf; bei laufendem Abbruch bleibt er gesperrt.
|
||||
@@ -2786,6 +2847,10 @@ function hideProgress() {
|
||||
// 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'; }
|
||||
const pause = $('folderProgressPause');
|
||||
if (pause) { pause.disabled = false; pause.style.display = 'none'; }
|
||||
transferPaused = false;
|
||||
if (pause) pause.textContent = PAUSE_LABEL;
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
@@ -4922,7 +4987,7 @@ function showRepoContextMenu(ev, owner, repoName, cloneUrl, element, isPrivate =
|
||||
const sel = await window.electronAPI.selectFolder();
|
||||
if (sel) {
|
||||
showProgress(0, 'Upload...', { cancellable: true });
|
||||
const res = await window.electronAPI.uploadLocalFolderToGitea({
|
||||
const res = await uploadFolderWithRetry({
|
||||
localFolder: sel,
|
||||
owner,
|
||||
repo: repoName,
|
||||
@@ -5618,30 +5683,148 @@ async function createGitignoreForRepo(owner, repo) {
|
||||
}
|
||||
}
|
||||
|
||||
// Zeigt die zuletzt aufgezeichneten Uploads; der Bestätigen-Knopf leert den Verlauf.
|
||||
// Zeigt die zuletzt aufgezeichneten Uploads, filterbar nach Ergebnis und Repo-Name.
|
||||
// Ein Klick auf einen Eintrag öffnet das Repo, „CSV exportieren“ speichert die gefilterte Liste.
|
||||
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();
|
||||
|
||||
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';
|
||||
card.style.cssText = 'width:min(760px,92vw);max-width:92vw;';
|
||||
|
||||
const titleEl = document.createElement('h2');
|
||||
titleEl.className = 'confirm-modal-title';
|
||||
titleEl.textContent = '🕘 Upload-Verlauf';
|
||||
|
||||
const filterRow = document.createElement('div');
|
||||
filterRow.style.cssText = 'display:flex;gap:8px;margin:10px 0;flex-wrap:wrap;';
|
||||
const statusSel = document.createElement('select');
|
||||
statusSel.style.cssText = 'width:auto;flex:0 0 160px;';
|
||||
for (const [value, label] of [['alle', 'Alle'], ['ok', 'Erfolgreich'], ['abgebrochen', 'Abgebrochen'], ['fehler', 'Fehler']]) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = value;
|
||||
opt.textContent = label;
|
||||
statusSel.appendChild(opt);
|
||||
}
|
||||
const search = document.createElement('input');
|
||||
search.type = 'search';
|
||||
search.placeholder = 'Repo suchen …';
|
||||
search.style.cssText = 'width:auto;flex:1;min-width:160px;';
|
||||
filterRow.appendChild(statusSel);
|
||||
filterRow.appendChild(search);
|
||||
|
||||
const listEl = document.createElement('div');
|
||||
listEl.className = 'confirm-modal-message';
|
||||
listEl.style.cssText = 'max-height:50vh;overflow:auto;font-size:12px;line-height:1.6;';
|
||||
|
||||
const hint = document.createElement('div');
|
||||
hint.style.cssText = 'font-size:11px;opacity:0.7;margin-top:6px;';
|
||||
hint.textContent = 'Ein Klick auf einen Eintrag öffnet das Repo.';
|
||||
|
||||
const btnWrap = document.createElement('div');
|
||||
btnWrap.className = 'modal-buttons confirm-modal-buttons';
|
||||
const closeBtn = document.createElement('button');
|
||||
closeBtn.className = 'confirm-modal-btn confirm-modal-btn--primary';
|
||||
closeBtn.textContent = 'Schließen';
|
||||
const exportBtn = document.createElement('button');
|
||||
exportBtn.className = 'confirm-modal-btn confirm-modal-btn--secondary';
|
||||
exportBtn.textContent = 'CSV exportieren';
|
||||
const clearBtn = document.createElement('button');
|
||||
clearBtn.className = 'confirm-modal-btn confirm-modal-btn--secondary';
|
||||
clearBtn.textContent = 'Verlauf leeren';
|
||||
btnWrap.append(closeBtn, exportBtn, clearBtn);
|
||||
|
||||
card.append(titleEl, filterRow, listEl, hint, btnWrap);
|
||||
modal.appendChild(card);
|
||||
document.body.appendChild(modal);
|
||||
|
||||
const currentFilter = () => ({ status: statusSel.value, query: search.value });
|
||||
|
||||
const render = async () => {
|
||||
const res = await window.electronAPI.getUploadHistory(currentFilter());
|
||||
const history = (res && res.history) || [];
|
||||
listEl.textContent = '';
|
||||
exportBtn.disabled = history.length === 0;
|
||||
if (history.length === 0) {
|
||||
listEl.textContent = statusSel.value === 'alle' && !search.value.trim()
|
||||
? 'Noch keine Uploads aufgezeichnet.'
|
||||
: 'Keine Einträge für diesen Filter.';
|
||||
return;
|
||||
}
|
||||
for (const e of history.slice(0, 40)) {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'upload-history-row';
|
||||
const [owner, repo] = String(e.repo || '').split('/');
|
||||
const canOpen = !!(owner && repo);
|
||||
if (canOpen) row.title = `${e.repo} öffnen`;
|
||||
else row.style.cursor = 'default';
|
||||
|
||||
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 main = document.createElement('div');
|
||||
main.textContent = `${icons[e.status] || '•'} ${when} · ${e.repo || '?'} · ${files}${failed} · ${e.kind}${took}`;
|
||||
row.appendChild(main);
|
||||
if (e.error) {
|
||||
const err = document.createElement('div');
|
||||
err.className = 'upload-history-err';
|
||||
err.textContent = e.error;
|
||||
row.appendChild(err);
|
||||
}
|
||||
if (canOpen) {
|
||||
row.addEventListener('click', () => {
|
||||
close();
|
||||
loadRepoContents(owner, repo, '');
|
||||
});
|
||||
}
|
||||
listEl.appendChild(row);
|
||||
}
|
||||
if (history.length > 40) {
|
||||
const more = document.createElement('div');
|
||||
more.style.cssText = 'opacity:0.7;padding:6px 8px;';
|
||||
more.textContent = `… und ${history.length - 40} ältere Einträge (im CSV-Export enthalten)`;
|
||||
listEl.appendChild(more);
|
||||
}
|
||||
};
|
||||
|
||||
const onKey = (e) => { if (e.key === 'Escape') close(); };
|
||||
function close() {
|
||||
document.removeEventListener('keydown', onKey);
|
||||
modal.remove();
|
||||
}
|
||||
document.addEventListener('keydown', onKey);
|
||||
closeBtn.onclick = close;
|
||||
|
||||
exportBtn.onclick = async () => {
|
||||
exportBtn.disabled = true;
|
||||
try {
|
||||
const res = await window.electronAPI.exportUploadHistoryCsv(currentFilter());
|
||||
if (res && res.ok) showSuccess(`Verlauf exportiert: ${res.count} Einträge → ${res.savedTo}`);
|
||||
else if (res && !res.canceled) showError('Export fehlgeschlagen: ' + (res.error || 'Unbekannter Fehler'));
|
||||
} finally {
|
||||
exportBtn.disabled = false;
|
||||
}
|
||||
};
|
||||
|
||||
clearBtn.onclick = async () => {
|
||||
const ok = await showActionConfirmModal({
|
||||
title: 'Verlauf leeren',
|
||||
message: 'Alle aufgezeichneten Uploads werden gelöscht.',
|
||||
confirmText: 'Leeren',
|
||||
danger: true
|
||||
});
|
||||
if (!ok) return;
|
||||
await window.electronAPI.clearUploadHistory();
|
||||
render();
|
||||
};
|
||||
statusSel.onchange = render;
|
||||
let searchTimer = null;
|
||||
search.oninput = () => { clearTimeout(searchTimer); searchTimer = setTimeout(render, 150); };
|
||||
render();
|
||||
}
|
||||
|
||||
async function cleanupGitignoredFiles(owner, repo, repoPath) {
|
||||
@@ -5875,7 +6058,7 @@ function setupBackgroundContextMenu() {
|
||||
showProgress(0, `Lade "${clipboard.item.name}" hoch...`, { cancellable: true });
|
||||
try {
|
||||
const res = clipboard.item.isDirectory
|
||||
? await window.electronAPI.uploadLocalFolderToGitea({
|
||||
? await uploadFolderWithRetry({
|
||||
localFolder: clipboard.item.path,
|
||||
owner,
|
||||
repo,
|
||||
@@ -5890,6 +6073,7 @@ function setupBackgroundContextMenu() {
|
||||
destPath: currentPath,
|
||||
branch: getDefaultBranch(owner, repo)
|
||||
});
|
||||
warnBlockedFiles(res);
|
||||
if (res?.cancelled) { setStatus('Upload abgebrochen'); return; }
|
||||
if (!res?.ok) throw new Error(res?.error || 'Upload fehlgeschlagen');
|
||||
showSuccess(`"${clipboard.item.name}" nach Gitea kopiert`);
|
||||
@@ -6918,6 +7102,16 @@ window.addEventListener('DOMContentLoaded', async () => {
|
||||
}
|
||||
});
|
||||
|
||||
// Pause/Fortsetzen (auch vom Tray-Menü ausgelöst)
|
||||
if (window.electronAPI.onTransferPauseState) {
|
||||
window.electronAPI.onTransferPauseState(({ paused }) => applyPauseState(paused));
|
||||
}
|
||||
|
||||
// Tray-Menü "Upload-Verlauf"
|
||||
if (window.electronAPI.onOpenUploadHistory) {
|
||||
window.electronAPI.onOpenUploadHistory(() => { showUploadHistory(); });
|
||||
}
|
||||
|
||||
// Progress listeners
|
||||
window.electronAPI.onFolderUploadProgress(p => {
|
||||
showProgress(p.percent, describeTransferProgress('Upload', p), { cancellable: true });
|
||||
@@ -7438,6 +7632,12 @@ function showCreateReleaseModal(owner, repo) {
|
||||
assetGroup.appendChild(assetRow);
|
||||
card.appendChild(assetGroup);
|
||||
|
||||
// Ergebnis der Prüfung des gewählten Installers (Prüfsumme, Abgleich mit der .sha256-Datei)
|
||||
const assetInfoBox = document.createElement('div');
|
||||
assetInfoBox.id = 'releaseAssetInfo';
|
||||
assetInfoBox.style.cssText = 'display:none;margin:-4px 0 12px;padding:10px 12px;border-radius:8px;font-size:12px;line-height:1.5;border:1px solid rgba(255,255,255,0.12);';
|
||||
card.appendChild(assetInfoBox);
|
||||
|
||||
const optionsGroup = document.createElement('div');
|
||||
optionsGroup.className = 'input-group';
|
||||
optionsGroup.style.cssText = 'display: flex; gap: 20px;';
|
||||
@@ -7480,17 +7680,67 @@ function showCreateReleaseModal(owner, repo) {
|
||||
|
||||
// Variable zum Speichern des gewählten Dateipfads
|
||||
let selectedAssetPath = null;
|
||||
// Prüfergebnis zum gewählten Asset (Name, SHA-256, Version, .sha256-Datei) oder null
|
||||
let assetInfo = null;
|
||||
|
||||
const showAssetInfo = (kind, lines) => {
|
||||
const colors = {
|
||||
ok: ['rgba(74,222,128,0.12)', 'rgba(74,222,128,0.4)'],
|
||||
warn: ['rgba(251,191,36,0.12)', 'rgba(251,191,36,0.4)'],
|
||||
error: ['rgba(248,113,113,0.14)', 'rgba(248,113,113,0.5)']
|
||||
}[kind];
|
||||
assetInfoBox.style.display = 'block';
|
||||
assetInfoBox.style.background = colors[0];
|
||||
assetInfoBox.style.borderColor = colors[1];
|
||||
assetInfoBox.textContent = '';
|
||||
for (const line of lines) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = line;
|
||||
assetInfoBox.appendChild(div);
|
||||
}
|
||||
};
|
||||
|
||||
// Prüft die gewählte Datei (Prüfsumme, Version, .sha256-Datei) und füllt Tag und Name vor.
|
||||
const applyAsset = async (filePath) => {
|
||||
selectedAssetPath = filePath;
|
||||
assetInfo = null;
|
||||
const fileName = filePath.split(/[\\/]/).pop();
|
||||
$('releaseAssetInput').value = fileName;
|
||||
$('releaseAssetInput').style.color = 'var(--text-primary)';
|
||||
$('releaseAssetInput').style.borderColor = 'var(--accent-primary)';
|
||||
showAssetInfo('warn', ['Prüfe Datei und berechne SHA-256 …']);
|
||||
|
||||
const info = await window.electronAPI.inspectReleaseFile({ filePath });
|
||||
if (!info || !info.ok) {
|
||||
showAssetInfo('error', [`Datei konnte nicht geprüft werden: ${info?.error || 'Unbekannter Fehler'}`]);
|
||||
return;
|
||||
}
|
||||
assetInfo = info;
|
||||
|
||||
if (info.version) {
|
||||
if (!$('releaseTag').value.trim()) $('releaseTag').value = `v${info.version}`;
|
||||
if (!$('releaseName').value.trim()) $('releaseName').value = `Version ${info.version}`;
|
||||
}
|
||||
|
||||
const lines = [`${info.name} · ${formatTransferBytes(info.size)}`, `SHA-256: ${info.sha256}`];
|
||||
if (info.sidecarMatches === true) {
|
||||
lines.push('✓ Stimmt mit der Datei .sha256 überein. Beide werden hochgeladen.');
|
||||
showAssetInfo('ok', lines);
|
||||
} else if (info.sidecarMatches === false) {
|
||||
lines.push(`✗ Die Datei .sha256 nennt eine andere Prüfsumme (${info.sidecarSha256}). Der Installer wurde nach dem Bauen verändert oder die Prüfsumme gehört zu einer anderen Version. Das Release wird so nicht erstellt.`);
|
||||
showAssetInfo('error', lines);
|
||||
} else {
|
||||
lines.push('Keine .sha256-Datei neben dem Installer gefunden. Die Prüfsumme wird nur in den Release-Text eingetragen.');
|
||||
showAssetInfo('warn', lines);
|
||||
}
|
||||
};
|
||||
|
||||
// Event Listener: Datei auswählen
|
||||
$('btnSelectReleaseAsset').onclick = async () => {
|
||||
try {
|
||||
const res = await window.electronAPI.selectFile();
|
||||
if (res.ok && res.files && res.files.length > 0) {
|
||||
selectedAssetPath = res.files[0];
|
||||
const fileName = selectedAssetPath.split(/[\\/]/).pop();
|
||||
$('releaseAssetInput').value = fileName;
|
||||
$('releaseAssetInput').style.color = 'var(--text-primary)';
|
||||
$('releaseAssetInput').style.borderColor = 'var(--accent-primary)';
|
||||
await applyAsset(res.files[0]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Auswählen der Datei:', error);
|
||||
@@ -7511,7 +7761,30 @@ function showCreateReleaseModal(owner, repo) {
|
||||
await showInfoModal('Eingabe fehlt', 'Tag Version ist erforderlich!');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Mit gewähltem Installer: Prüfsumme abgleichen, Tag gegen vorhandene Releases prüfen und die
|
||||
// Prüfsumme in den Release-Text schreiben, damit der Updater das Release findet und verifiziert.
|
||||
let releaseBody = body;
|
||||
if (assetInfo) {
|
||||
if (assetInfo.sidecarMatches === false) {
|
||||
await showInfoModal('Prüfsumme stimmt nicht', 'Die Datei .sha256 passt nicht zum Installer. Bitte neu bauen (npm run build) und den Installer erneut wählen.', true);
|
||||
return;
|
||||
}
|
||||
const prep = await window.electronAPI.prepareRelease({
|
||||
owner,
|
||||
repo,
|
||||
tag,
|
||||
notes: body,
|
||||
assets: [{ name: assetInfo.name, sha256: assetInfo.sha256 }],
|
||||
platform: currentState.platform
|
||||
});
|
||||
if (!prep || !prep.ok) {
|
||||
await showInfoModal('Release nicht möglich', prep?.error || 'Unbekannter Fehler', true);
|
||||
return;
|
||||
}
|
||||
releaseBody = prep.body;
|
||||
}
|
||||
|
||||
setStatus('Creating release...');
|
||||
|
||||
try {
|
||||
@@ -7521,7 +7794,7 @@ function showCreateReleaseModal(owner, repo) {
|
||||
repo,
|
||||
tag_name: tag,
|
||||
name,
|
||||
body,
|
||||
body: releaseBody,
|
||||
target_commitish: target,
|
||||
prerelease,
|
||||
draft,
|
||||
@@ -7545,6 +7818,20 @@ function showCreateReleaseModal(owner, repo) {
|
||||
});
|
||||
|
||||
if (uploadRes.ok) {
|
||||
// Prüfsummen-Datei neben den Installer legen, so sucht der Updater sie.
|
||||
if (assetInfo && assetInfo.sidecarPath) {
|
||||
showProgress(80, 'Lade Prüfsummen-Datei hoch…');
|
||||
const sidecarRes = await window.electronAPI.uploadReleaseAsset({
|
||||
owner,
|
||||
repo,
|
||||
releaseId: res.release.id,
|
||||
filePath: assetInfo.sidecarPath,
|
||||
fileName: assetInfo.sidecarPath.split(/[\\/]/).pop()
|
||||
});
|
||||
if (!sidecarRes || !sidecarRes.ok) {
|
||||
showWarning('Release erstellt, aber die .sha256-Datei konnte nicht hochgeladen werden. Die Prüfsumme steht im Release-Text.');
|
||||
}
|
||||
}
|
||||
setStatus(`Release "${tag}" und Asset erstellt!`);
|
||||
} else {
|
||||
console.error('Asset Upload fehlgeschlagen:', uploadRes.error);
|
||||
|
||||
@@ -38,10 +38,13 @@
|
||||
set('prefZoom', String(p.zoom));
|
||||
if ($('prefZoom') && !$('prefZoom').value) $('prefZoom').value = '100';
|
||||
set('prefAutoUpdate', p.autoUpdateCheck);
|
||||
set('prefShowSplash', p.showSplash);
|
||||
set('prefCloseAction', p.closeAction);
|
||||
set('prefCloneDir', p.defaultCloneDir);
|
||||
set('prefSecretScan', p.uploadSecretScan);
|
||||
set('prefBlockSecrets', p.uploadBlockSecrets);
|
||||
set('prefUploadPreview', p.uploadPreview);
|
||||
set('prefNotifyTransfers', p.notifyTransfers);
|
||||
set('prefUploadExcludes', p.uploadExtraExcludes);
|
||||
}
|
||||
|
||||
@@ -54,10 +57,13 @@
|
||||
theme: val('prefTheme'),
|
||||
zoom: Number(val('prefZoom')),
|
||||
autoUpdateCheck: chk('prefAutoUpdate'),
|
||||
showSplash: chk('prefShowSplash'),
|
||||
closeAction: val('prefCloseAction'),
|
||||
defaultCloneDir: val('prefCloneDir'),
|
||||
uploadSecretScan: chk('prefSecretScan'),
|
||||
uploadBlockSecrets: chk('prefBlockSecrets'),
|
||||
uploadPreview: val('prefUploadPreview'),
|
||||
notifyTransfers: val('prefNotifyTransfers'),
|
||||
uploadExtraExcludes: val('prefUploadExcludes')
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user