Upload via GUI (28 Dateien)
This commit is contained in:
@@ -0,0 +1,902 @@
|
||||
// renderer/features.js
|
||||
// Oberfläche für: Upload-Vorschau mit Geheimnis-Warnung, lokale Änderungen, Pull Requests &
|
||||
// Issues, Server-Diagnose und Repo-Backup. Wird nach renderer.js geladen und nutzt dessen
|
||||
// globale Helfer ($, currentState, selectedFolder, showProgress, showSuccess, ...).
|
||||
// Inhalte vom Server werden ausschließlich per textContent eingesetzt, nie als HTML.
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
/* ---------- kleine Helfer ---------- */
|
||||
function el(tag, props = {}, children = []) {
|
||||
const node = document.createElement(tag);
|
||||
for (const [k, v] of Object.entries(props)) {
|
||||
if (v == null) continue;
|
||||
if (k === 'class') node.className = v;
|
||||
else if (k === 'text') node.textContent = v;
|
||||
else if (k.startsWith('on')) node.addEventListener(k.slice(2), v);
|
||||
else if (k in node) node[k] = v;
|
||||
else node.setAttribute(k, v);
|
||||
}
|
||||
for (const c of [].concat(children)) if (c != null) node.append(c);
|
||||
return node;
|
||||
}
|
||||
|
||||
function fmtBytes(n) {
|
||||
if (n < 1024) return `${n} B`;
|
||||
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
||||
return `${(n / 1024 / 1024).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
function fmtDate(iso) {
|
||||
if (!iso) return '';
|
||||
try { return new Date(iso).toLocaleString('de-DE', { dateStyle: 'medium', timeStyle: 'short' }); } catch (_) { return iso; }
|
||||
}
|
||||
|
||||
function fxModal({ title, wide = false }) {
|
||||
const titleEl = el('h2', { class: 'fx-title', text: title });
|
||||
const closeBtn = el('button', { class: 'fx-close', type: 'button', title: 'Schließen', text: '✕' });
|
||||
const body = el('div', { class: 'fx-body' });
|
||||
const footer = el('div', { class: 'fx-footer' });
|
||||
const card = el('div', { class: `modalContent card fx-card${wide ? ' fx-card--wide' : ''}` }, [
|
||||
el('div', { class: 'fx-header' }, [titleEl, closeBtn]), body, footer
|
||||
]);
|
||||
const root = el('div', { class: 'modal fx-modal' }, [card]);
|
||||
let onClose = null;
|
||||
const close = (result) => {
|
||||
root.remove();
|
||||
document.removeEventListener('keydown', onKey);
|
||||
if (onClose) onClose(result);
|
||||
};
|
||||
const onKey = (e) => {
|
||||
if (e.key !== 'Escape') return;
|
||||
// Nur den obersten Dialog schließen (z. B. eine Rückfrage über diesem Fenster)
|
||||
if (document.querySelector('.confirm-modal') || root.nextElementSibling && root.nextElementSibling.classList.contains('fx-modal')) return;
|
||||
close(undefined);
|
||||
};
|
||||
closeBtn.onclick = () => close(undefined);
|
||||
root.addEventListener('mousedown', (e) => { if (e.target === root) close(undefined); });
|
||||
document.addEventListener('keydown', onKey);
|
||||
document.body.appendChild(root);
|
||||
return {
|
||||
root, body, footer, close,
|
||||
setTitle: (t) => { titleEl.textContent = t; },
|
||||
onClose: (fn) => { onClose = fn; }
|
||||
};
|
||||
}
|
||||
|
||||
function button(text, onclick, variant = 'secondary') {
|
||||
return el('button', { type: 'button', class: `fx-btn fx-btn--${variant}`, text, onclick });
|
||||
}
|
||||
|
||||
function busy(container, text) {
|
||||
container.textContent = '';
|
||||
container.append(el('div', { class: 'fx-busy', text }));
|
||||
}
|
||||
|
||||
/* ---------- Knöpfe je nach Ansicht ---------- */
|
||||
window.updateFeatureButtons = function (mode, owner, repo) {
|
||||
const isGitea = currentState.platform !== 'github';
|
||||
const btnPulls = $('btnPulls');
|
||||
const btnIssues = $('btnIssues');
|
||||
const btnLocal = $('btnLocalChanges');
|
||||
const showRepo = mode === 'repo' && isGitea;
|
||||
if (btnPulls) {
|
||||
btnPulls.classList.toggle('hidden', !showRepo);
|
||||
btnPulls.onclick = () => openCollab(owner, repo, 'pulls');
|
||||
}
|
||||
if (btnIssues) {
|
||||
btnIssues.classList.toggle('hidden', !showRepo);
|
||||
btnIssues.onclick = () => openCollab(owner, repo, 'issues');
|
||||
}
|
||||
if (btnLocal) {
|
||||
btnLocal.classList.toggle('hidden', mode !== 'local');
|
||||
btnLocal.onclick = () => openLocalChanges(selectedFolder);
|
||||
}
|
||||
};
|
||||
|
||||
/* ================================================================
|
||||
1. Upload-Vorschau mit Geheimnis-Warnung
|
||||
================================================================ */
|
||||
window.uploadPreviewGate = async function ({ paths, destPath, repo }) {
|
||||
showProgress(5, 'Prüfe Dateien vor dem Upload ...');
|
||||
let res;
|
||||
try {
|
||||
res = await window.electronAPI.previewUploadPaths({ paths, destPath });
|
||||
} catch (e) {
|
||||
res = { ok: false, error: String(e) };
|
||||
} finally {
|
||||
hideProgress();
|
||||
}
|
||||
if (!res || !res.ok) {
|
||||
// Die Vorschau darf einen Upload nie verhindern.
|
||||
showWarning('Upload-Vorschau nicht möglich: ' + (res && res.error ? res.error : 'unbekannter Fehler'));
|
||||
return { proceed: true, excludeFiles: [] };
|
||||
}
|
||||
|
||||
const files = res.files || [];
|
||||
const secrets = files.filter(f => f.secret);
|
||||
const ignored = res.ignored || [];
|
||||
const baseDir = paths.length ? paths[0].replace(/[\\/][^\\/]+$/, '') : '';
|
||||
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) {
|
||||
return { proceed: true, excludeFiles: [] };
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const m = fxModal({ title: '📤 Upload-Vorschau', wide: true });
|
||||
let settled = false;
|
||||
const finish = (result) => { if (!settled) { settled = true; m.close(); resolve(result); } };
|
||||
m.onClose(() => { if (!settled) { settled = true; resolve({ proceed: false }); } });
|
||||
|
||||
const target = destPath ? `${repo}/${destPath}` : repo;
|
||||
m.body.append(el('p', { class: 'fx-lead', text: `${files.length} Dateien (${fmtBytes(res.totalBytes || 0)}) werden nach „${target}“ hochgeladen.` }));
|
||||
|
||||
if (files.length > 1000) {
|
||||
m.body.append(el('div', { class: 'fx-alert fx-alert--warn', text:
|
||||
`Das sind sehr viele Dateien. Prüfe, ob Build-Ordner (z. B. build/, dist/, node_modules/) in der .gitignore stehen.` }));
|
||||
}
|
||||
if (files.length === 0) {
|
||||
m.body.append(el('div', { class: 'fx-alert fx-alert--warn', text: 'Es bleibt keine Datei zum Hochladen übrig.' }));
|
||||
}
|
||||
|
||||
const secretChecks = [];
|
||||
if (secrets.length) {
|
||||
const list = el('div', { class: 'fx-list' });
|
||||
for (const f of secrets) {
|
||||
const cb = el('input', { type: 'checkbox', checked: true });
|
||||
secretChecks.push({ cb, file: f.localFile });
|
||||
list.append(el('label', { class: 'fx-list-row fx-list-row--check' }, [
|
||||
cb,
|
||||
el('span', { class: 'fx-mono', text: f.targetPath }),
|
||||
el('span', { class: 'fx-muted', text: f.secret })
|
||||
]));
|
||||
}
|
||||
m.body.append(el('div', { class: 'fx-alert fx-alert--danger' }, [
|
||||
el('strong', { text: `⚠️ ${secrets.length} Datei(en) enthalten vermutlich Zugangsdaten` }),
|
||||
el('div', { text: 'Angehakte Dateien werden NICHT hochgeladen. Haken nur entfernen, wenn die Datei wirklich ins Repo gehört (z. B. bei einem privaten Repo bewusst gewollt).' }),
|
||||
list,
|
||||
el('div', { class: 'fx-muted', text: 'Hinweis: Dateien, die schon im Repo liegen, bleiben dort, bis du sie löschst. Ein geleakter Schlüssel sollte zusätzlich erneuert werden.' })
|
||||
]));
|
||||
}
|
||||
|
||||
if (ignored.length) {
|
||||
const list = el('div', { class: 'fx-list fx-list--compact' });
|
||||
ignored.slice(0, 25).forEach(i => list.append(el('div', { class: 'fx-list-row fx-mono', text: relLocal(i.path) + (i.isDir ? '/' : '') })));
|
||||
if (ignored.length > 25) list.append(el('div', { class: 'fx-list-row fx-muted', text: `… und ${ignored.length - 25} weitere` }));
|
||||
m.body.append(el('div', { class: 'fx-section' }, [
|
||||
el('h3', { text: `🚫 Von .gitignore aussortiert (${ignored.length})` }),
|
||||
list
|
||||
]));
|
||||
}
|
||||
|
||||
if (files.length) {
|
||||
const list = el('div', { class: 'fx-list fx-list--compact' });
|
||||
files.slice(0, 500).forEach(f => list.append(el('div', { class: 'fx-list-row' }, [
|
||||
el('span', { class: 'fx-mono', text: f.targetPath }),
|
||||
el('span', { class: 'fx-muted', text: fmtBytes(f.size) })
|
||||
])));
|
||||
if (files.length > 500) list.append(el('div', { class: 'fx-list-row fx-muted', text: `… und ${files.length - 500} weitere` }));
|
||||
m.body.append(el('details', { class: 'fx-section' }, [
|
||||
el('summary', { text: `Alle ${files.length} Dateien anzeigen` }),
|
||||
list
|
||||
]));
|
||||
}
|
||||
|
||||
m.footer.append(
|
||||
button('Abbrechen', () => finish({ proceed: false })),
|
||||
button(files.length ? 'Hochladen' : 'Schließen', () => {
|
||||
if (!files.length) return finish({ proceed: false });
|
||||
const excludeFiles = secretChecks.filter(s => s.cb.checked).map(s => s.file);
|
||||
if (excludeFiles.length >= files.length) return finish({ proceed: false });
|
||||
finish({ proceed: true, excludeFiles });
|
||||
}, 'primary')
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
/* ================================================================
|
||||
2. Lokale Änderungen: Status, Diff, Commit, Push
|
||||
================================================================ */
|
||||
async function openLocalChanges(folder) {
|
||||
if (!folder) { showWarning('Bitte zuerst einen lokalen Ordner öffnen.'); return; }
|
||||
const m = fxModal({ title: '📝 Lokale Änderungen', wide: true });
|
||||
const info = el('div', { class: 'fx-lead' });
|
||||
const listPane = el('div', { class: 'fx-pane fx-pane--list' });
|
||||
const diffPane = el('pre', { class: 'fx-pane fx-diff', text: 'Datei anklicken, um die Änderungen zu sehen.' });
|
||||
m.body.append(info, el('div', { class: 'fx-split' }, [listPane, diffPane]));
|
||||
|
||||
const msg = el('input', { type: 'text', class: 'fx-input', placeholder: 'Commit-Nachricht, z. B. „Login-Fehler behoben“' });
|
||||
const btnCommit = button('Commit', () => commit(false), 'primary');
|
||||
const btnCommitPush = button('Commit & Push', () => commit(true), 'primary');
|
||||
const btnRefresh = button('↻ Aktualisieren', () => load());
|
||||
const btnPushOnly = button('⬆ Nur pushen', () => push());
|
||||
m.footer.append(msg, btnRefresh, btnPushOnly, btnCommit, btnCommitPush);
|
||||
|
||||
let files = [];
|
||||
const checks = new Map();
|
||||
|
||||
async function load() {
|
||||
busy(listPane, 'Lade Status ...');
|
||||
const res = await window.electronAPI.localGit({ action: 'status', folder });
|
||||
if (!res.ok) {
|
||||
listPane.textContent = '';
|
||||
listPane.append(el('div', { class: 'fx-alert fx-alert--warn', text: res.notRepo
|
||||
? 'Dieser Ordner ist noch kein Git-Repository. Über „Push“ in der Werkzeugleiste kannst du ihn als Repo anlegen und hochladen.'
|
||||
: (res.error || 'Status nicht lesbar') }));
|
||||
[btnCommit, btnCommitPush, btnPushOnly].forEach(b => { b.disabled = true; });
|
||||
return;
|
||||
}
|
||||
files = res.files;
|
||||
info.textContent = `Branch: ${res.branch}` + (res.ahead ? ` · ${res.ahead} Commit(s) noch nicht gepusht` : '') + ` · ${files.length} geänderte Datei(en)`;
|
||||
btnPushOnly.disabled = !res.ahead;
|
||||
listPane.textContent = '';
|
||||
checks.clear();
|
||||
if (!files.length) {
|
||||
listPane.append(el('div', { class: 'fx-empty', text: '✅ Keine Änderungen. Alles ist committet.' }));
|
||||
diffPane.textContent = '';
|
||||
return;
|
||||
}
|
||||
const all = el('input', { type: 'checkbox', checked: true, onchange: () => { for (const cb of checks.values()) cb.checked = all.checked; } });
|
||||
listPane.append(el('label', { class: 'fx-list-row fx-list-row--check fx-list-head' }, [all, el('span', { text: 'Alle auswählen' })]));
|
||||
for (const f of files) {
|
||||
const cb = el('input', { type: 'checkbox', checked: true });
|
||||
checks.set(f.path, cb);
|
||||
const name = el('button', { type: 'button', class: 'fx-linkish fx-mono', text: f.from ? `${f.from} → ${f.path}` : f.path, onclick: () => showDiff(f, row) });
|
||||
const discard = el('button', { type: 'button', class: 'fx-icon-btn', title: 'Änderung verwerfen', text: '↺', onclick: () => discardFile(f) });
|
||||
const row = el('div', { class: 'fx-list-row fx-list-row--check' }, [
|
||||
cb, el('span', { class: `fx-badge fx-badge--${f.code === '?' ? 'new' : f.code}`, text: f.label }), name, discard
|
||||
]);
|
||||
listPane.append(row);
|
||||
}
|
||||
}
|
||||
|
||||
async function showDiff(f, row) {
|
||||
listPane.querySelectorAll('.fx-list-row.is-active').forEach(r => r.classList.remove('is-active'));
|
||||
row.classList.add('is-active');
|
||||
diffPane.textContent = 'Lade Unterschiede ...';
|
||||
const res = await window.electronAPI.localGit({ action: 'diff', folder, file: f.path, code: f.code });
|
||||
diffPane.textContent = '';
|
||||
if (!res.ok) { diffPane.textContent = res.error || 'Diff nicht verfügbar'; return; }
|
||||
for (const line of String(res.diff).split('\n')) {
|
||||
let cls = '';
|
||||
if (line.startsWith('+') && !line.startsWith('+++')) cls = 'fx-diff-add';
|
||||
else if (line.startsWith('-') && !line.startsWith('---')) cls = 'fx-diff-del';
|
||||
else if (line.startsWith('@@')) cls = 'fx-diff-hunk';
|
||||
diffPane.append(el('span', { class: cls, text: line + '\n' }));
|
||||
}
|
||||
}
|
||||
|
||||
async function discardFile(f) {
|
||||
const ok = await showActionConfirmModal({
|
||||
title: 'Änderung verwerfen',
|
||||
message: f.code === '?'
|
||||
? `Die neue Datei "${f.path}" wird gelöscht. Das lässt sich nicht rückgängig machen.`
|
||||
: `Alle nicht committeten Änderungen an "${f.path}" gehen verloren. Das lässt sich nicht rückgängig machen.`,
|
||||
confirmText: 'Verwerfen',
|
||||
danger: true
|
||||
});
|
||||
if (!ok) return;
|
||||
const res = await window.electronAPI.localGit({ action: 'discard', folder, file: f.path, code: f.code });
|
||||
if (!res.ok) showError('Verwerfen fehlgeschlagen: ' + res.error);
|
||||
await load();
|
||||
}
|
||||
|
||||
async function commit(andPush) {
|
||||
const selected = files.filter(f => checks.get(f.path) && checks.get(f.path).checked).map(f => f.path);
|
||||
if (!selected.length) { showWarning('Keine Datei ausgewählt.'); return; }
|
||||
if (!msg.value.trim()) { showWarning('Bitte eine Commit-Nachricht eingeben.'); msg.focus(); return; }
|
||||
[btnCommit, btnCommitPush].forEach(b => { b.disabled = true; });
|
||||
try {
|
||||
const res = await window.electronAPI.localGit({ action: 'commit', folder, files: selected, message: msg.value });
|
||||
if (!res.ok) { showError('Commit fehlgeschlagen: ' + res.error); return; }
|
||||
showSuccess(`${selected.length} Datei(en) committet`);
|
||||
msg.value = '';
|
||||
if (andPush) await push();
|
||||
} finally {
|
||||
[btnCommit, btnCommitPush].forEach(b => { b.disabled = false; });
|
||||
await load();
|
||||
}
|
||||
}
|
||||
|
||||
async function push() {
|
||||
showProgress(30, 'Pushe ...');
|
||||
const res = await window.electronAPI.localGit({ action: 'push', folder });
|
||||
hideProgress();
|
||||
if (res.ok) showSuccess('Push erfolgreich');
|
||||
else await showInfoModal('Push fehlgeschlagen', (res.error || '') + (res.hint ? '\n\n' + res.hint : ''), true);
|
||||
await load();
|
||||
}
|
||||
|
||||
await load();
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
3. Pull Requests & Issues
|
||||
================================================================ */
|
||||
async function openCollab(owner, repo, kind) {
|
||||
const isPulls = kind === 'pulls';
|
||||
const m = fxModal({ title: `${isPulls ? '🔀 Pull Requests' : '🐞 Issues'} · ${repo}`, wide: true });
|
||||
let state = 'open';
|
||||
let page = 1;
|
||||
|
||||
function renderList() {
|
||||
m.setTitle(`${isPulls ? '🔀 Pull Requests' : '🐞 Issues'} · ${repo}`);
|
||||
m.body.textContent = '';
|
||||
m.footer.textContent = '';
|
||||
const tabs = el('div', { class: 'fx-tabs' }, [
|
||||
['open', 'Offen'], ['closed', 'Geschlossen']
|
||||
].map(([s, label]) => el('button', {
|
||||
type: 'button', class: `fx-tab${s === state ? ' is-active' : ''}`, text: label,
|
||||
onclick: () => { state = s; page = 1; renderList(); }
|
||||
})));
|
||||
const list = el('div', { class: 'fx-list fx-list--cards' });
|
||||
m.body.append(tabs, list);
|
||||
if (!isPulls) m.footer.append(button('+ Neues Issue', () => renderNewIssue(), 'primary'));
|
||||
loadPage(list);
|
||||
}
|
||||
|
||||
async function loadPage(list) {
|
||||
const loading = el('div', { class: 'fx-busy', text: 'Lade ...' });
|
||||
list.append(loading);
|
||||
const res = await window.electronAPI.giteaCollab({ action: isPulls ? 'listPulls' : 'listIssues', owner, repo, state, page });
|
||||
loading.remove();
|
||||
if (!res.ok) { list.append(el('div', { class: 'fx-alert fx-alert--danger', text: res.error })); return; }
|
||||
if (!res.items.length && page === 1) {
|
||||
list.append(el('div', { class: 'fx-empty', text: state === 'open'
|
||||
? (isPulls ? 'Keine offenen Pull Requests.' : 'Keine offenen Issues.')
|
||||
: 'Nichts gefunden.' }));
|
||||
return;
|
||||
}
|
||||
for (const it of res.items) {
|
||||
const meta = [`#${it.number}`, it.user ? `von ${it.user.login}` : null, `aktualisiert ${fmtDate(it.updated)}`, it.comments ? `💬 ${it.comments}` : null]
|
||||
.filter(Boolean).join(' · ');
|
||||
const card = el('button', { type: 'button', class: 'fx-item', onclick: () => renderDetail(it.number) }, [
|
||||
el('div', { class: 'fx-item-title', text: (it.merged ? '🟣 ' : it.state === 'open' ? '🟢 ' : '🔴 ') + it.title }),
|
||||
el('div', { class: 'fx-muted', text: meta + (isPulls && it.head ? ` · ${it.head} → ${it.base}` : '') }),
|
||||
it.labels.length ? el('div', { class: 'fx-labels' }, it.labels.map(l => {
|
||||
const lab = el('span', { class: 'fx-label', text: l.name });
|
||||
if (/^[0-9a-f]{6}$/i.test(String(l.color).replace('#', ''))) lab.style.borderColor = '#' + String(l.color).replace('#', '');
|
||||
return lab;
|
||||
})) : null
|
||||
]);
|
||||
list.append(card);
|
||||
}
|
||||
if (res.items.length === 30) {
|
||||
const more = button('Mehr laden', () => { more.remove(); page++; loadPage(list); });
|
||||
list.append(more);
|
||||
}
|
||||
}
|
||||
|
||||
async function renderDetail(number) {
|
||||
m.body.textContent = '';
|
||||
m.footer.textContent = '';
|
||||
busy(m.body, 'Lade ...');
|
||||
const res = await window.electronAPI.giteaCollab({ action: 'get', owner, repo, index: number, isPull: isPulls });
|
||||
m.body.textContent = '';
|
||||
if (!res.ok) { m.body.append(el('div', { class: 'fx-alert fx-alert--danger', text: res.error })); m.footer.append(button('← Zurück', renderList)); return; }
|
||||
const it = res.item;
|
||||
m.setTitle(`#${it.number} ${it.title}`);
|
||||
const meta = [it.state === 'open' ? 'offen' : (it.merged ? 'gemergt' : 'geschlossen'), it.user ? `von ${it.user.login}` : null, `erstellt ${fmtDate(it.created)}`]
|
||||
.filter(Boolean).join(' · ');
|
||||
m.body.append(el('div', { class: 'fx-muted', text: meta + (isPulls && it.head ? ` · ${it.head} → ${it.base}` : '') }));
|
||||
m.body.append(el('div', { class: 'fx-text', text: it.body || '(keine Beschreibung)' }));
|
||||
|
||||
const comments = el('div', { class: 'fx-comments' });
|
||||
for (const c of res.comments) {
|
||||
comments.append(el('div', { class: 'fx-comment' }, [
|
||||
el('div', { class: 'fx-muted', text: `${c.user ? c.user.login : '?'} · ${fmtDate(c.created)}` }),
|
||||
el('div', { class: 'fx-text', text: c.body })
|
||||
]));
|
||||
}
|
||||
m.body.append(el('h3', { text: `Kommentare (${res.comments.length})` }), comments);
|
||||
|
||||
const input = el('textarea', { class: 'fx-input fx-textarea', placeholder: 'Kommentar schreiben ...' });
|
||||
const send = button('Kommentieren', async () => {
|
||||
if (!input.value.trim()) return;
|
||||
send.disabled = true;
|
||||
const r = await window.electronAPI.giteaCollab({ action: 'comment', owner, repo, index: number, body: input.value });
|
||||
send.disabled = false;
|
||||
if (r.ok) { showSuccess('Kommentar gespeichert'); renderDetail(number); } else showError('Kommentar fehlgeschlagen: ' + r.error);
|
||||
}, 'primary');
|
||||
m.body.append(input, el('div', { class: 'fx-row-end' }, [send]));
|
||||
|
||||
m.footer.append(button('← Zurück', renderList));
|
||||
if (it.url) m.footer.append(button('Im Browser öffnen', () => window.electronAPI.openExternalUrl(it.url)));
|
||||
|
||||
if (isPulls && it.state === 'open' && !it.merged) {
|
||||
const style = el('select', { class: 'fx-input fx-select' }, [
|
||||
['merge', 'Merge-Commit'], ['squash', 'Squash'], ['rebase', 'Rebase'], ['rebase-merge', 'Rebase + Merge-Commit']
|
||||
].map(([v, t]) => el('option', { value: v, text: t })));
|
||||
const delBranch = el('input', { type: 'checkbox' });
|
||||
const mergeBtn = button('Mergen', async () => {
|
||||
const ok = await showActionConfirmModal({
|
||||
title: 'Pull Request mergen',
|
||||
message: `#${it.number} „${it.title}“ wird per ${style.options[style.selectedIndex].text} in ${it.base} übernommen.` + (delBranch.checked ? `\nDer Branch ${it.head} wird danach gelöscht.` : ''),
|
||||
confirmText: 'Mergen'
|
||||
});
|
||||
if (!ok) return;
|
||||
mergeBtn.disabled = true;
|
||||
const r = await window.electronAPI.giteaCollab({ action: 'merge', owner, repo, index: number, style: style.value, deleteBranch: delBranch.checked });
|
||||
mergeBtn.disabled = false;
|
||||
if (r.ok) { showSuccess(`#${number} gemergt`); renderDetail(number); }
|
||||
else await showInfoModal('Mergen fehlgeschlagen', r.error + (it.mergeable === false ? '\n\nDer Pull Request hat Konflikte und muss erst angepasst werden.' : ''), true);
|
||||
}, 'primary');
|
||||
if (it.mergeable === false) m.footer.append(el('span', { class: 'fx-muted', text: '⚠️ Konflikte' }));
|
||||
m.footer.append(style, el('label', { class: 'fx-inline-check' }, [delBranch, el('span', { text: 'Branch löschen' })]), mergeBtn);
|
||||
}
|
||||
if (!it.merged) {
|
||||
const toState = it.state === 'open' ? 'closed' : 'open';
|
||||
m.footer.append(button(toState === 'closed' ? 'Schließen' : 'Wieder öffnen', async () => {
|
||||
const r = await window.electronAPI.giteaCollab({ action: 'setState', owner, repo, index: number, state: toState });
|
||||
if (r.ok) renderDetail(number); else showError(r.error);
|
||||
}, toState === 'closed' ? 'danger' : 'secondary'));
|
||||
}
|
||||
}
|
||||
|
||||
function renderNewIssue() {
|
||||
m.setTitle(`🐞 Neues Issue · ${repo}`);
|
||||
m.body.textContent = '';
|
||||
m.footer.textContent = '';
|
||||
const title = el('input', { type: 'text', class: 'fx-input', placeholder: 'Titel' });
|
||||
const body = el('textarea', { class: 'fx-input fx-textarea fx-textarea--tall', placeholder: 'Beschreibung (optional)' });
|
||||
m.body.append(title, body);
|
||||
m.footer.append(button('← Zurück', renderList), button('Issue anlegen', async () => {
|
||||
if (!title.value.trim()) { title.focus(); return; }
|
||||
const r = await window.electronAPI.giteaCollab({ action: 'createIssue', owner, repo, title: title.value, body: body.value });
|
||||
if (r.ok) { showSuccess(`Issue #${r.item.number} angelegt`); renderDetail(r.item.number); } else showError(r.error);
|
||||
}, 'primary'));
|
||||
title.focus();
|
||||
}
|
||||
|
||||
renderList();
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
4. Server-Diagnose
|
||||
================================================================ */
|
||||
async function openDiagnosis() {
|
||||
const m = fxModal({ title: '🩺 Server-Diagnose', wide: true });
|
||||
busy(m.body, 'Prüfe Server, Verbindung und Git-Transport ... (bis zu einer Minute)');
|
||||
m.footer.append(button('Schließen', () => m.close()));
|
||||
const inRepo = currentState.view === 'gitea-repo' && currentState.owner && currentState.repo;
|
||||
const res = await window.electronAPI.runServerDiagnosis(inRepo ? { owner: currentState.owner, repo: currentState.repo } : {});
|
||||
m.body.textContent = '';
|
||||
if (!res || !res.ok) { m.body.append(el('div', { class: 'fx-alert fx-alert--danger', text: (res && res.error) || 'Diagnose fehlgeschlagen' })); return; }
|
||||
m.body.append(el('div', { class: 'fx-muted', text: 'Verwendet die gespeicherten Zugangsdaten.' }));
|
||||
const icons = { ok: '✅', warn: '⚠️', fail: '❌', info: 'ℹ️' };
|
||||
for (const c of res.checks) {
|
||||
m.body.append(el('div', { class: `fx-check fx-check--${c.status}` }, [
|
||||
el('div', { class: 'fx-check-head' }, [el('span', { text: icons[c.status] || '•' }), el('strong', { text: c.label })]),
|
||||
el('div', { class: 'fx-text', text: c.detail || '' }),
|
||||
c.hint ? el('pre', { class: 'fx-hint', text: c.hint }) : null
|
||||
]));
|
||||
}
|
||||
const bad = res.checks.filter(c => c.status === 'fail').length;
|
||||
m.setTitle(bad ? `🩺 Server-Diagnose: ${bad} Problem(e)` : '🩺 Server-Diagnose: alles in Ordnung');
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
5. Repo-Backup (Einstellungen)
|
||||
================================================================ */
|
||||
/* ---------- Fortschrittsfenster für das Repo-Backup ---------- */
|
||||
let backupView = null;
|
||||
|
||||
function fmtDuration(ms) {
|
||||
const s = Math.max(0, Math.round(ms / 1000));
|
||||
if (s < 60) return `${s} s`;
|
||||
const m = Math.floor(s / 60);
|
||||
return m < 60 ? `${m} min ${s % 60} s` : `${Math.floor(m / 60)} h ${m % 60} min`;
|
||||
}
|
||||
|
||||
function backupPercent(p) {
|
||||
if (!p || !p.total) return 0;
|
||||
if (p.phase === 'done') return 100;
|
||||
// Laufendes Repo anteilig mitzählen, wenn die Größe des Downloads bekannt ist
|
||||
const partial = p.bytesTotal ? Math.min(0.99, p.bytes / p.bytesTotal) : 0;
|
||||
return Math.min(99, Math.floor(((p.done || 0) + partial) / p.total * 100));
|
||||
}
|
||||
|
||||
function openBackupProgress(initial) {
|
||||
if (backupView) { backupView.update(initial); return; }
|
||||
const m = fxModal({ title: '⇣ Backup läuft', wide: true });
|
||||
const percentEl = el('div', { class: 'fx-progress-percent', text: '0 %' });
|
||||
const fill = el('div', { class: 'fx-progress-fill' });
|
||||
const bar = el('div', { class: 'fx-progress-track' }, [fill]);
|
||||
const currentEl = el('div', { class: 'fx-lead', text: 'Startet …' });
|
||||
const stepEl = el('div', { class: 'fx-muted' });
|
||||
const metaEl = el('div', { class: 'fx-progress-meta' });
|
||||
const list = el('div', { class: 'fx-list fx-backup-list' });
|
||||
const warn = el('div', { class: 'fx-alert fx-alert--warn hidden', text:
|
||||
'Der Server liefert keine Git-Pakete. Die Repos werden deshalb als ZIP gesichert: nur der Standard-Branch, ohne Historie.' });
|
||||
m.body.append(el('div', { class: 'fx-progress-head' }, [percentEl, el('div', { class: 'fx-progress-now' }, [currentEl, stepEl])]),
|
||||
bar, metaEl, warn, list);
|
||||
|
||||
const cancelBtn = button('Abbrechen', async () => {
|
||||
const ok = await showActionConfirmModal({
|
||||
title: 'Backup abbrechen?',
|
||||
message: 'Bereits gesicherte Repos bleiben erhalten. Ein laufender ZIP-Download wird sofort beendet, ein laufender Git-Spiegel erst nach dem aktuellen Repo.',
|
||||
confirmText: 'Abbrechen',
|
||||
danger: true
|
||||
});
|
||||
if (ok) { cancelBtn.disabled = true; await window.electronAPI.repoBackupCancel(); }
|
||||
}, 'danger');
|
||||
const closeBtn = button('Im Hintergrund weiterlaufen lassen', () => m.close());
|
||||
m.footer.append(cancelBtn, closeBtn);
|
||||
m.onClose(() => { backupView = null; clearInterval(timer); });
|
||||
|
||||
let last = null;
|
||||
let rowsFor = '';
|
||||
const rows = new Map();
|
||||
// Verstrichene Zeit auch ohne neue Meldung weiterzählen
|
||||
const timer = setInterval(() => { if (last && last.phase !== 'done') renderMeta(last); }, 1000);
|
||||
|
||||
function renderMeta(p) {
|
||||
const elapsed = Date.now() - (p.startedAt || Date.now());
|
||||
const parts = [`${p.done || 0} von ${p.total || '?'} Repos`, `Dauer ${fmtDuration(p.summary ? p.summary.durationMs : elapsed)}`];
|
||||
if (p.phase === 'running' && p.done >= 1 && p.total > p.done) {
|
||||
parts.push(`noch ca. ${fmtDuration(elapsed / p.done * (p.total - p.done))}`);
|
||||
}
|
||||
metaEl.textContent = parts.join(' · ');
|
||||
}
|
||||
|
||||
function update(p) {
|
||||
if (!p) return;
|
||||
last = p;
|
||||
const pct = backupPercent(p);
|
||||
percentEl.textContent = `${pct} %`;
|
||||
fill.style.width = `${pct}%`;
|
||||
warn.classList.toggle('hidden', !p.mirrorBroken);
|
||||
renderMeta(p);
|
||||
|
||||
if (p.phase === 'list' || p.phase === 'start') {
|
||||
currentEl.textContent = p.step || 'Startet …';
|
||||
stepEl.textContent = p.targetDir ? `Ziel: ${p.targetDir}` : '';
|
||||
} else if (p.phase === 'running') {
|
||||
currentEl.textContent = p.repo ? `${p.current}/${p.total}: ${p.repo}` : 'Läuft …';
|
||||
let step = p.step || '';
|
||||
if (p.bytes) step += ` ${fmtBytes(p.bytes)}${p.bytesTotal ? ` von ${fmtBytes(p.bytesTotal)}` : ''}`;
|
||||
stepEl.textContent = step;
|
||||
} else if (p.phase === 'done') {
|
||||
const s = p.summary || {};
|
||||
m.setTitle(s.cancelled ? '⇣ Backup abgebrochen' : s.failed ? '⇣ Backup mit Fehlern beendet' : '⇣ Backup fertig');
|
||||
currentEl.textContent = `${s.ok} von ${s.total} Repos gesichert` + (s.failed ? `, ${s.failed} Fehler` : '') + (s.skipped ? `, ${s.skipped} übersprungen` : '');
|
||||
stepEl.textContent = `${s.mirror} als Git-Spiegel, ${s.zip} als ZIP · Protokoll: backup-protokoll.json im Backup-Ordner`;
|
||||
percentEl.textContent = s.cancelled ? '–' : '100 %';
|
||||
fill.style.width = s.cancelled ? fill.style.width : '100%';
|
||||
cancelBtn.classList.add('hidden');
|
||||
closeBtn.textContent = 'Schließen';
|
||||
} else if (p.phase === 'error') {
|
||||
m.setTitle('⇣ Backup fehlgeschlagen');
|
||||
currentEl.textContent = p.error || 'Unbekannter Fehler';
|
||||
stepEl.textContent = '';
|
||||
cancelBtn.classList.add('hidden');
|
||||
closeBtn.textContent = 'Schließen';
|
||||
}
|
||||
|
||||
// Liste aller Repos mit Zustand
|
||||
const repos = p.repos || [];
|
||||
if (repos.join('|') !== rowsFor) {
|
||||
rowsFor = repos.join('|');
|
||||
list.textContent = '';
|
||||
rows.clear();
|
||||
for (const name of repos) {
|
||||
const icon = el('span', { class: 'fx-backup-icon', text: '○' });
|
||||
const info = el('span', { class: 'fx-muted', text: 'wartet' });
|
||||
rows.set(name, { row: el('div', { class: 'fx-list-row' }, [icon, el('span', { class: 'fx-mono', text: name }), info]), icon, info });
|
||||
list.append(rows.get(name).row);
|
||||
}
|
||||
}
|
||||
const byRepo = new Map((p.results || []).map(r => [r.repo, r]));
|
||||
for (const [name, r] of rows) {
|
||||
const res = byRepo.get(name);
|
||||
r.row.classList.toggle('is-active', !res && name === p.repo && p.phase === 'running');
|
||||
if (res) {
|
||||
r.icon.textContent = res.ok ? '✅' : '❌';
|
||||
r.info.textContent = res.ok
|
||||
? (res.method === 'leer' ? 'leeres Repo' : res.method + (res.bytes ? ` · ${fmtBytes(res.bytes)}` : ''))
|
||||
: res.error;
|
||||
r.info.title = res.error || '';
|
||||
} else if (name === p.repo && p.phase === 'running') {
|
||||
r.icon.textContent = '⏳';
|
||||
r.info.textContent = 'läuft …';
|
||||
} else if (p.phase === 'done') {
|
||||
r.icon.textContent = '–';
|
||||
r.info.textContent = 'übersprungen';
|
||||
}
|
||||
}
|
||||
const active = list.querySelector('.fx-list-row.is-active');
|
||||
if (active) active.scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
|
||||
backupView = { update };
|
||||
update(initial || { phase: 'start', step: 'Startet …', startedAt: Date.now(), results: [], repos: [] });
|
||||
}
|
||||
|
||||
function renderBackupStatus(state) {
|
||||
const statusEl = $('repoBackupStatus');
|
||||
if (!statusEl) return;
|
||||
const runBtn = $('btnRepoBackupRun');
|
||||
if (runBtn) runBtn.textContent = state.running ? '⏳ Fortschritt anzeigen' : '▶️ Jetzt sichern';
|
||||
if (state.running) {
|
||||
const p = state.progress;
|
||||
statusEl.textContent = p && p.total
|
||||
? `⏳ Backup läuft: ${backupPercent(p)} % · ${p.current}/${p.total}: ${p.repo || ''}`
|
||||
: '⏳ Backup läuft …';
|
||||
return;
|
||||
}
|
||||
const r = state.config && state.config.lastResult;
|
||||
if (!r) { statusEl.textContent = 'Noch kein Backup gelaufen.'; return; }
|
||||
let text = `Letztes Backup: ${fmtDate(r.at)} · ${r.ok}/${r.total} gesichert (${r.mirror} als Git-Spiegel, ${r.zip} als ZIP)`;
|
||||
if (r.cancelled) text += ' · abgebrochen';
|
||||
if (r.durationMs) text += ` · Dauer ${fmtDuration(r.durationMs)}`;
|
||||
if (r.failed) text += ` · ${r.failed} Fehler: ` + r.errors.map(e => `${e.repo} (${e.error})`).join('; ');
|
||||
if (r.mirrorBroken) text += '\nDer Server liefert keine Git-Pakete, daher ZIP-Archive: nur der Standard-Branch, ohne Historie. Server-Diagnose zeigt die Ursache.';
|
||||
statusEl.textContent = text;
|
||||
}
|
||||
|
||||
async function loadBackupSettings() {
|
||||
try {
|
||||
const state = await window.electronAPI.repoBackupGetConfig();
|
||||
if (!state || !state.ok) return;
|
||||
const c = state.config;
|
||||
if ($('repoBackupTarget')) $('repoBackupTarget').value = c.targetDir || '';
|
||||
if ($('repoBackupInterval')) $('repoBackupInterval').value = c.interval || 'off';
|
||||
if ($('repoBackupKeep')) $('repoBackupKeep').value = c.keep || 5;
|
||||
if ($('repoBackupTime')) $('repoBackupTime').value = c.time || '';
|
||||
if ($('repoBackupNotify')) $('repoBackupNotify').value = c.notify || 'errors';
|
||||
if ($('repoBackupExclude')) $('repoBackupExclude').value = c.excludeRepos || '';
|
||||
renderBackupStatus(state);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
async function saveBackupSettings(silent) {
|
||||
const res = await window.electronAPI.repoBackupSetConfig({
|
||||
targetDir: $('repoBackupTarget').value,
|
||||
interval: $('repoBackupInterval').value,
|
||||
keep: $('repoBackupKeep').value,
|
||||
time: $('repoBackupTime') ? $('repoBackupTime').value : '',
|
||||
notify: $('repoBackupNotify') ? $('repoBackupNotify').value : 'errors',
|
||||
excludeRepos: $('repoBackupExclude') ? $('repoBackupExclude').value : ''
|
||||
});
|
||||
if (res && res.ok) {
|
||||
renderBackupStatus(res);
|
||||
if (!silent) showSuccess('Backup-Einstellung gespeichert');
|
||||
} else if (!silent) showError('Speichern fehlgeschlagen: ' + (res && res.error));
|
||||
return res;
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
6. Backup wiederherstellen
|
||||
================================================================ */
|
||||
async function openRestore() {
|
||||
const m = fxModal({ title: '↺ Backup wiederherstellen', wide: true });
|
||||
let dir = null;
|
||||
|
||||
async function renderList(customDir) {
|
||||
m.setTitle('↺ Backup wiederherstellen');
|
||||
m.footer.textContent = '';
|
||||
busy(m.body, 'Suche Sicherungen ...');
|
||||
const res = await window.electronAPI.repoBackupList(customDir ? { dir: customDir } : {});
|
||||
m.body.textContent = '';
|
||||
const pick = button('📂 Anderen Ordner wählen', async () => {
|
||||
const folder = await window.electronAPI.selectFolder();
|
||||
if (folder) renderList(folder);
|
||||
});
|
||||
m.footer.append(pick, button('Schließen', () => m.close()));
|
||||
if (!res || !res.ok) {
|
||||
m.body.append(el('div', { class: 'fx-alert fx-alert--warn', text: (res && res.error) || 'Backup-Ordner nicht lesbar' }));
|
||||
return;
|
||||
}
|
||||
dir = res.dir;
|
||||
m.body.append(el('div', { class: 'fx-muted', text: `Quelle: ${dir}` }));
|
||||
if (!res.backups.length) {
|
||||
m.body.append(el('div', { class: 'fx-empty', text: 'In diesem Ordner wurden keine Sicherungen gefunden.' }));
|
||||
return;
|
||||
}
|
||||
const list = el('div', { class: 'fx-list fx-list--cards' });
|
||||
for (const b of res.backups) {
|
||||
const parts = [];
|
||||
if (b.mirror) parts.push(`Git-Spiegel · Stand ${fmtDate(b.mirror.updated)}`);
|
||||
if (b.zips.length) parts.push(`${b.zips.length} ZIP-Stand${b.zips.length > 1 ? 'e' : ''} · neuester ${fmtDate(b.zips[0].date)}`);
|
||||
list.append(el('button', { type: 'button', class: 'fx-item', onclick: () => renderDetail(b) }, [
|
||||
el('div', { class: 'fx-item-title', text: `${b.owner}/${b.repo}` }),
|
||||
el('div', { class: 'fx-muted', text: parts.join(' · ') })
|
||||
]));
|
||||
}
|
||||
m.body.append(list);
|
||||
}
|
||||
|
||||
function renderDetail(b) {
|
||||
m.setTitle(`↺ ${b.owner}/${b.repo} wiederherstellen`);
|
||||
m.body.textContent = '';
|
||||
m.footer.textContent = '';
|
||||
|
||||
// Welche Sicherung?
|
||||
const sources = [];
|
||||
if (b.mirror) sources.push({ kind: 'mirror', path: b.mirror.path, label: `Git-Spiegel – volle Historie, alle Branches und Tags (Stand ${fmtDate(b.mirror.updated)})`, date: b.mirror.updated });
|
||||
for (const z of b.zips) sources.push({ kind: 'zip', path: z.path, label: `ZIP vom ${fmtDate(z.date)} – nur Standard-Branch, ohne Historie (${fmtBytes(z.size)})`, date: z.date });
|
||||
let chosen = sources[0];
|
||||
const srcList = el('div', { class: 'fx-list' });
|
||||
sources.forEach((src, i) => {
|
||||
const radio = el('input', { type: 'radio', name: 'fxRestoreSource', checked: i === 0, onchange: () => { chosen = src; updateMode(); } });
|
||||
srcList.append(el('label', { class: 'fx-list-row fx-list-row--check' }, [radio, el('span', { text: src.label })]));
|
||||
});
|
||||
m.body.append(el('h3', { text: '1. Sicherung wählen' }), srcList);
|
||||
|
||||
// Lokal entpacken
|
||||
const localBtn = button('📂 Zielordner wählen und entpacken', async () => {
|
||||
const parent = await window.electronAPI.selectFolder();
|
||||
if (!parent) return;
|
||||
setBusy(true, chosen.kind === 'mirror' ? 'Klone Spiegel ...' : 'Entpacke Archiv ...');
|
||||
const r = await window.electronAPI.repoRestoreLocal({ kind: chosen.kind, source: chosen.path, destParent: parent, repo: b.repo });
|
||||
setBusy(false);
|
||||
if (!r.ok) { await showInfoModal('Entpacken fehlgeschlagen', r.error, true); return; }
|
||||
showSuccess(`Wiederhergestellt nach ${r.path}`);
|
||||
showResult(`✅ Lokal wiederhergestellt: ${r.path}` + (r.branches ? ` (${r.branches} Branches)` : r.files ? ` (${r.files} Dateien)` : ''), [
|
||||
button('In der App öffnen', () => openLocalInApp(r.path, m), 'primary')
|
||||
]);
|
||||
});
|
||||
m.body.append(el('h3', { text: '2a. Lokal entpacken' }),
|
||||
el('div', { class: 'fx-muted', text: `Legt den Ordner „${b.repo}“ im gewählten Verzeichnis an. Ein vorhandener, nicht leerer Ordner wird nie überschrieben.` }),
|
||||
el('div', {}, [localBtn]));
|
||||
|
||||
// Auf Gitea zurückspielen
|
||||
const modeNew = el('input', { type: 'radio', name: 'fxRestoreMode', checked: true, onchange: updateMode });
|
||||
const modeOverwrite = el('input', { type: 'radio', name: 'fxRestoreMode', onchange: updateMode });
|
||||
const nameInput = el('input', { type: 'text', class: 'fx-input', value: `${b.repo}-wiederhergestellt` });
|
||||
const privateCb = el('input', { type: 'checkbox', checked: true });
|
||||
const overwriteNote = el('div', { class: 'fx-muted' });
|
||||
const giteaBtn = button('Auf Gitea zurückspielen', () => restoreGitea(), 'primary');
|
||||
m.body.append(el('h3', { text: '2b. Auf Gitea zurückspielen' }),
|
||||
el('label', { class: 'fx-list-row fx-list-row--check' }, [modeNew, el('span', { text: 'Als neues Repo anlegen' })]),
|
||||
el('div', { class: 'fx-row fx-indent' }, [nameInput, el('label', { class: 'fx-inline-check' }, [privateCb, el('span', { text: 'Privat' })])]),
|
||||
el('label', { class: 'fx-list-row fx-list-row--check' }, [modeOverwrite, el('span', { text: `Bestehendes Repo „${b.owner}/${b.repo}“ überschreiben` })]),
|
||||
overwriteNote,
|
||||
el('div', {}, [giteaBtn]));
|
||||
|
||||
const status = el('div', { class: 'fx-lead fx-restore-status' });
|
||||
m.body.append(status);
|
||||
m.footer.append(button('← Zurück', () => renderList(dir)));
|
||||
|
||||
let offProgress = null;
|
||||
function setBusy(on, text) {
|
||||
[localBtn, giteaBtn].forEach(btn => { btn.disabled = on; });
|
||||
status.textContent = on ? `⏳ ${text}` : '';
|
||||
if (on && !offProgress) offProgress = window.electronAPI.onRepoRestoreProgress(p => { if (p && p.text) status.textContent = `⏳ ${p.text}`; });
|
||||
if (!on && offProgress) { offProgress(); offProgress = null; }
|
||||
}
|
||||
function showResult(text, actions = []) {
|
||||
status.textContent = '';
|
||||
status.append(el('div', { text }), el('div', { class: 'fx-row' }, actions));
|
||||
}
|
||||
|
||||
function updateMode() {
|
||||
const zip = chosen.kind === 'zip';
|
||||
modeOverwrite.disabled = zip;
|
||||
if (zip && modeOverwrite.checked) modeNew.checked = true;
|
||||
nameInput.disabled = !modeNew.checked;
|
||||
privateCb.disabled = !modeNew.checked;
|
||||
overwriteNote.textContent = zip
|
||||
? 'Überschreiben geht nur mit einem Git-Spiegel, weil ein ZIP-Stand keine Historie enthält.'
|
||||
: 'Achtung: Setzt alle Branches und Tags auf den Stand der Sicherung. Neuere Commits auf Gitea gehen verloren.';
|
||||
}
|
||||
updateMode();
|
||||
|
||||
async function restoreGitea() {
|
||||
const overwrite = modeOverwrite.checked;
|
||||
const name = nameInput.value.trim();
|
||||
if (!overwrite && !name) { nameInput.focus(); return; }
|
||||
const ok = await showActionConfirmModal(overwrite ? {
|
||||
title: 'Repo überschreiben',
|
||||
message: `Alle Branches und Tags von „${b.owner}/${b.repo}“ werden auf den Stand vom ${fmtDate(chosen.date)} gesetzt.\n\nCommits, Branches und Tags, die danach auf Gitea entstanden sind, gehen verloren. Das lässt sich nur mit einer neueren Sicherung rückgängig machen.`,
|
||||
confirmText: 'Überschreiben',
|
||||
danger: true
|
||||
} : {
|
||||
title: 'Neues Repo anlegen',
|
||||
message: `Das Repo „${b.owner}/${name}“ wird ${privateCb.checked ? 'privat' : 'öffentlich'} angelegt und mit der Sicherung vom ${fmtDate(chosen.date)} befüllt.`,
|
||||
confirmText: 'Anlegen und befüllen'
|
||||
});
|
||||
if (!ok) return;
|
||||
setBusy(true, 'Starte ...');
|
||||
const r = await window.electronAPI.repoRestoreGitea({
|
||||
kind: chosen.kind, source: chosen.path, owner: b.owner, repo: b.repo,
|
||||
mode: overwrite ? 'overwrite' : 'new', newName: name, isPrivate: privateCb.checked
|
||||
});
|
||||
setBusy(false);
|
||||
if (!r.ok) { await showInfoModal('Wiederherstellen fehlgeschlagen', r.error, true); return; }
|
||||
showSuccess(`${r.repo} wiederhergestellt`);
|
||||
showResult(`✅ Auf Gitea wiederhergestellt: ${r.repo} (über ${r.method}${r.files ? `, ${r.files} Dateien` : ''})`,
|
||||
r.url ? [button('Im Browser öffnen', () => window.electronAPI.openExternalUrl(r.url))] : []);
|
||||
}
|
||||
}
|
||||
|
||||
renderList(null);
|
||||
}
|
||||
|
||||
function openLocalInApp(folder, modal) {
|
||||
modal.close();
|
||||
$('settingsModal')?.classList.add('hidden');
|
||||
selectedFolder = folder;
|
||||
currentState.view = 'local';
|
||||
setStatus('Local: ' + folder);
|
||||
if (typeof updateNavigationUI === 'function') updateNavigationUI();
|
||||
window.updateFeatureButtons('local');
|
||||
if (typeof refreshLocalTree === 'function') refreshLocalTree(folder);
|
||||
if (typeof loadBranches === 'function') loadBranches(folder);
|
||||
}
|
||||
|
||||
// Bereiche in den Einstellungen (Navigation links). "general" bleibt als alter Name für "konten" gültig.
|
||||
window.showSettingsTab = function (name) {
|
||||
if (name === 'general') name = 'konten';
|
||||
document.querySelectorAll('#settingsModal [data-settings-tab]').forEach(tab => {
|
||||
const active = tab.dataset.settingsTab === name;
|
||||
tab.classList.toggle('is-active', active);
|
||||
tab.setAttribute('aria-selected', String(active));
|
||||
});
|
||||
document.querySelectorAll('#settingsModal [data-settings-panel]').forEach(panel => {
|
||||
panel.classList.toggle('hidden', panel.dataset.settingsPanel !== name);
|
||||
});
|
||||
if (name === 'backup') loadBackupSettings();
|
||||
// Höhenabgleich der Karten braucht sichtbare Elemente
|
||||
if (name === 'konten' && typeof syncSettingsPanelHeights === 'function') requestAnimationFrame(syncSettingsPanelHeights);
|
||||
const pages = document.querySelector('#settingsModal .settings-pages');
|
||||
if (pages) pages.scrollTop = 0;
|
||||
};
|
||||
|
||||
// Für das gemeinsame "Speichern" in settings-extra.js
|
||||
window.gmBackupSettings = { load: loadBackupSettings, save: saveBackupSettings };
|
||||
|
||||
function wireSettings() {
|
||||
document.querySelectorAll('#settingsModal [data-settings-tab]').forEach(tab => {
|
||||
tab.addEventListener('click', () => window.showSettingsTab(tab.dataset.settingsTab));
|
||||
});
|
||||
const restoreBtn = $('btnRepoRestore');
|
||||
if (restoreBtn) restoreBtn.onclick = () => openRestore();
|
||||
const diag = $('btnServerDiagnosis');
|
||||
if (diag) diag.onclick = () => openDiagnosis();
|
||||
const browse = $('btnRepoBackupBrowse');
|
||||
if (browse) browse.onclick = async () => {
|
||||
const folder = await window.electronAPI.selectFolder();
|
||||
if (folder) $('repoBackupTarget').value = folder;
|
||||
};
|
||||
const save = $('btnRepoBackupSave');
|
||||
if (save) save.onclick = () => saveBackupSettings(false);
|
||||
const run = $('btnRepoBackupRun');
|
||||
if (run) run.onclick = async () => {
|
||||
const state = await window.electronAPI.repoBackupGetConfig();
|
||||
// Läuft schon ein Backup (z. B. ein geplantes), nur den Fortschritt zeigen
|
||||
if (state && state.running) { openBackupProgress(state.progress); return; }
|
||||
if (!$('repoBackupTarget').value.trim()) { showWarning('Bitte zuerst ein Backup-Ziel wählen.'); return; }
|
||||
await saveBackupSettings(true);
|
||||
openBackupProgress(null);
|
||||
const res = await window.electronAPI.repoBackupRunNow();
|
||||
if (!res.ok && !res.summary && !res.running) showError('Backup fehlgeschlagen: ' + res.error);
|
||||
};
|
||||
|
||||
// Fortschritt kommt für jedes Backup an alle Fenster – auch für geplante Backups
|
||||
window.electronAPI.onRepoBackupProgress((p) => {
|
||||
if (!p) return;
|
||||
const finished = p.phase === 'done' || p.phase === 'error';
|
||||
renderBackupStatus({ running: !finished, progress: p });
|
||||
if (backupView) backupView.update(p);
|
||||
if (finished) {
|
||||
const s = p.summary;
|
||||
if (p.phase === 'error') showError('Backup fehlgeschlagen: ' + p.error);
|
||||
else if (s && s.cancelled) showWarning(`Backup abgebrochen: ${s.ok} von ${s.total} Repos gesichert`);
|
||||
else if (s && s.failed) showWarning(`Backup mit ${s.failed} Fehler(n) beendet`);
|
||||
else if (s) showSuccess(`Backup fertig: ${s.ok}/${s.total} Repos gesichert`);
|
||||
setTimeout(loadBackupSettings, 300);
|
||||
}
|
||||
});
|
||||
// Beim Öffnen der Einstellungen den aktuellen Stand anzeigen
|
||||
const btnSettings = $('btnSettings');
|
||||
if (btnSettings) btnSettings.addEventListener('click', () => { window.showSettingsTab('konten'); setTimeout(loadBackupSettings, 50); });
|
||||
loadBackupSettings();
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', wireSettings);
|
||||
else wireSettings();
|
||||
})();
|
||||
Reference in New Issue
Block a user