4495 lines
157 KiB
JavaScript
4495 lines
157 KiB
JavaScript
// renderer.js — Grid-UI + Navigation + Drag'n'Drop mit Fehlerbehandlung
|
||
const $ = id => document.getElementById(id);
|
||
|
||
let selectedFolder = null;
|
||
let giteaCache = {};
|
||
|
||
/* ================================================
|
||
FAVORITEN & ZULETZT GEÖFFNET — State & Helpers
|
||
================================================ */
|
||
|
||
let favorites = []; // [{ owner, repo, cloneUrl, addedAt }]
|
||
let recentRepos = []; // [{ owner, repo, cloneUrl, openedAt }]
|
||
|
||
// Feature-Flags (aus Settings, persistent via Credentials)
|
||
let featureFavorites = true;
|
||
let featureRecent = true;
|
||
let compactMode = false;
|
||
|
||
async function loadFavoritesAndRecent() {
|
||
try {
|
||
const [favRes, recRes] = await Promise.all([
|
||
window.electronAPI.loadFavorites(),
|
||
window.electronAPI.loadRecent()
|
||
]);
|
||
if (favRes && favRes.ok) favorites = favRes.favorites || [];
|
||
if (recRes && recRes.ok) recentRepos = recRes.recent || [];
|
||
} catch(e) {
|
||
console.error('loadFavoritesAndRecent:', e);
|
||
}
|
||
}
|
||
|
||
function isFavorite(owner, repo) {
|
||
return favorites.some(f => f.owner === owner && f.repo === repo);
|
||
}
|
||
|
||
async function toggleFavorite(owner, repo, cloneUrl) {
|
||
if (isFavorite(owner, repo)) {
|
||
favorites = favorites.filter(f => !(f.owner === owner && f.repo === repo));
|
||
} else {
|
||
favorites.unshift({ owner, repo, cloneUrl, addedAt: new Date().toISOString() });
|
||
}
|
||
await window.electronAPI.saveFavorites(favorites);
|
||
}
|
||
|
||
async function addToRecent(owner, repo, cloneUrl) {
|
||
if (!featureRecent) return;
|
||
recentRepos = recentRepos.filter(r => !(r.owner === owner && r.repo === repo));
|
||
recentRepos.unshift({ owner, repo, cloneUrl, openedAt: new Date().toISOString() });
|
||
recentRepos = recentRepos.slice(0, 20);
|
||
await window.electronAPI.saveRecent(recentRepos);
|
||
}
|
||
|
||
function formatRelDate(iso) {
|
||
if (!iso) return '';
|
||
const diff = Date.now() - new Date(iso).getTime();
|
||
const m = Math.floor(diff / 60000);
|
||
const h = Math.floor(diff / 3600000);
|
||
const d = Math.floor(diff / 86400000);
|
||
if (m < 1) return 'Gerade eben';
|
||
if (m < 60) return `vor ${m} Min.`;
|
||
if (h < 24) return `vor ${h} Std.`;
|
||
if (d < 7) return `vor ${d} Tag${d > 1 ? 'en' : ''}`;
|
||
return new Date(iso).toLocaleDateString('de-DE');
|
||
}
|
||
|
||
/* Rendert Favoriten + Zuletzt-geöffnet-Bereich in ein beliebiges Container-Element */
|
||
// Collapse-Zustand (wird in Credentials persistiert)
|
||
const favSectionCollapsed = { favorites: false, recent: false };
|
||
|
||
function makeFavSectionBlock(type, allRepos) {
|
||
const isFav = type === 'favorites';
|
||
const icon = isFav ? '⭐' : '🕐';
|
||
const label = isFav ? 'Favoriten' : 'Zuletzt geöffnet';
|
||
|
||
const sec = document.createElement('div');
|
||
sec.style.cssText = `margin-bottom: ${isFav ? '20' : '24'}px;`;
|
||
|
||
// ── Header (klickbar) ──────────────────────────────
|
||
const hdr = document.createElement('div');
|
||
hdr.className = 'fav-section-header fav-section-header--toggle';
|
||
|
||
const iconEl = document.createElement('span');
|
||
iconEl.className = 'fav-section-icon';
|
||
if (isFav) iconEl.style.color = '#f59e0b';
|
||
iconEl.textContent = icon;
|
||
|
||
const labelEl = document.createElement('span');
|
||
labelEl.textContent = label;
|
||
|
||
const arrow = document.createElement('span');
|
||
arrow.className = 'fav-collapse-arrow';
|
||
arrow.textContent = favSectionCollapsed[type] ? '▶' : '▼';
|
||
|
||
hdr.appendChild(iconEl);
|
||
hdr.appendChild(labelEl);
|
||
hdr.appendChild(arrow);
|
||
sec.appendChild(hdr);
|
||
|
||
// ── Inhalt ────────────────────────────────────────
|
||
const row = document.createElement('div');
|
||
row.className = 'fav-chips-row';
|
||
row.style.cssText = favSectionCollapsed[type]
|
||
? 'display:none;'
|
||
: 'display:flex;flex-wrap:wrap;gap:8px;';
|
||
|
||
const items = isFav ? favorites : recentRepos.slice(0, 8);
|
||
items.forEach(entry => row.appendChild(makeChip(entry, isFav ? 'favorite' : 'recent', allRepos)));
|
||
sec.appendChild(row);
|
||
|
||
// ── Toggle-Logik ──────────────────────────────────
|
||
hdr.addEventListener('click', () => {
|
||
favSectionCollapsed[type] = !favSectionCollapsed[type];
|
||
const collapsed = favSectionCollapsed[type];
|
||
row.style.display = collapsed ? 'none' : 'flex';
|
||
arrow.textContent = collapsed ? '▶' : '▼';
|
||
// Zustand persistent speichern
|
||
window.electronAPI.loadCredentials().then(c => {
|
||
if (c && c.ok) {
|
||
window.electronAPI.saveCredentials({
|
||
...c,
|
||
favCollapsedFavorites: favSectionCollapsed.favorites,
|
||
favCollapsedRecent: favSectionCollapsed.recent
|
||
});
|
||
}
|
||
}).catch(() => {});
|
||
});
|
||
|
||
return sec;
|
||
}
|
||
|
||
function renderFavRecentSection(container, allRepos) {
|
||
container.innerHTML = '';
|
||
const showFav = featureFavorites && favorites.length > 0;
|
||
const showRec = featureRecent && recentRepos.length > 0;
|
||
if (!showFav && !showRec) return;
|
||
|
||
if (showFav) container.appendChild(makeFavSectionBlock('favorites', allRepos));
|
||
if (showRec) container.appendChild(makeFavSectionBlock('recent', allRepos));
|
||
|
||
// Trennlinie
|
||
const div = document.createElement('div');
|
||
div.className = 'fav-divider';
|
||
container.appendChild(div);
|
||
}
|
||
|
||
function makeChip(entry, type, allRepos) {
|
||
const isFav = type === 'favorite';
|
||
const chip = document.createElement('div');
|
||
chip.className = `fav-chip${isFav ? ' fav-chip--star' : ''}`;
|
||
chip.title = `${entry.owner}/${entry.repo}`;
|
||
|
||
const icon = document.createElement('span');
|
||
icon.className = 'fav-chip-icon';
|
||
icon.textContent = isFav ? '⭐' : '🕐';
|
||
|
||
const label = document.createElement('span');
|
||
label.className = 'fav-chip-label';
|
||
label.textContent = `${entry.owner}/${entry.repo}`;
|
||
|
||
chip.appendChild(icon);
|
||
chip.appendChild(label);
|
||
|
||
if (!isFav && entry.openedAt) {
|
||
const time = document.createElement('span');
|
||
time.className = 'fav-chip-time';
|
||
time.textContent = formatRelDate(entry.openedAt);
|
||
chip.appendChild(time);
|
||
}
|
||
|
||
chip.onclick = () => {
|
||
addToRecent(entry.owner, entry.repo, entry.cloneUrl);
|
||
loadRepoContents(entry.owner, entry.repo, '');
|
||
};
|
||
|
||
chip.oncontextmenu = (ev) => {
|
||
ev.preventDefault();
|
||
ev.stopPropagation();
|
||
showChipContextMenu(ev, entry, type);
|
||
};
|
||
|
||
// Drag-Reorder (nur für Favoriten)
|
||
if (isFav) {
|
||
chip.draggable = true;
|
||
chip.dataset.owner = entry.owner;
|
||
chip.dataset.repo = entry.repo;
|
||
|
||
chip.addEventListener('dragstart', (ev) => {
|
||
ev.dataTransfer.effectAllowed = 'move';
|
||
ev.dataTransfer.setData('text/fav-owner', entry.owner);
|
||
ev.dataTransfer.setData('text/fav-repo', entry.repo);
|
||
chip.classList.add('fav-chip--dragging');
|
||
});
|
||
chip.addEventListener('dragend', () => chip.classList.remove('fav-chip--dragging'));
|
||
|
||
chip.addEventListener('dragover', (ev) => {
|
||
ev.preventDefault();
|
||
ev.dataTransfer.dropEffect = 'move';
|
||
chip.classList.add('fav-chip--drop-target');
|
||
});
|
||
chip.addEventListener('dragleave', () => chip.classList.remove('fav-chip--drop-target'));
|
||
|
||
chip.addEventListener('drop', async (ev) => {
|
||
ev.preventDefault();
|
||
chip.classList.remove('fav-chip--drop-target');
|
||
const srcOwner = ev.dataTransfer.getData('text/fav-owner');
|
||
const srcRepo = ev.dataTransfer.getData('text/fav-repo');
|
||
if (srcOwner === entry.owner && srcRepo === entry.repo) return;
|
||
|
||
const fromIdx = favorites.findIndex(f => f.owner === srcOwner && f.repo === srcRepo);
|
||
const toIdx = favorites.findIndex(f => f.owner === entry.owner && f.repo === entry.repo);
|
||
if (fromIdx < 0 || toIdx < 0) return;
|
||
|
||
// Reorder
|
||
const [moved] = favorites.splice(fromIdx, 1);
|
||
favorites.splice(toIdx, 0, moved);
|
||
await window.electronAPI.saveFavorites(favorites);
|
||
|
||
// Sektion neu rendern
|
||
const sec = $('favRecentSection');
|
||
if (sec) {
|
||
// allRepos fehlt hier, daher einfach neu laden
|
||
const favBlock = sec.querySelector('.fav-chips-row');
|
||
if (favBlock) {
|
||
const allChips = Array.from(favBlock.querySelectorAll('.fav-chip'));
|
||
const movedChip = allChips.find(c => c.dataset.owner === srcOwner && c.dataset.repo === srcRepo);
|
||
const targetChip = allChips.find(c => c.dataset.owner === entry.owner && c.dataset.repo === entry.repo);
|
||
if (movedChip && targetChip) {
|
||
favBlock.insertBefore(movedChip, fromIdx > toIdx ? targetChip : targetChip.nextSibling);
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
return chip;
|
||
}
|
||
|
||
function showChipContextMenu(ev, entry, type) {
|
||
const old = $('ctxMenu');
|
||
if (old) old.remove();
|
||
const menu = document.createElement('div');
|
||
menu.id = 'ctxMenu';
|
||
menu.className = 'context-menu';
|
||
menu.style.left = Math.min(ev.clientX, window.innerWidth - 240) + 'px';
|
||
menu.style.top = Math.min(ev.clientY, window.innerHeight - 160) + 'px';
|
||
|
||
const addItem = (icon, text, cb, color) => {
|
||
const el = document.createElement('div');
|
||
el.className = 'context-item';
|
||
el.innerHTML = `${icon} ${text}`;
|
||
if (color) el.style.color = color;
|
||
el.onclick = () => { menu.remove(); cb(); };
|
||
menu.appendChild(el);
|
||
};
|
||
|
||
addItem('📂', 'Öffnen', () => {
|
||
addToRecent(entry.owner, entry.repo, entry.cloneUrl);
|
||
loadRepoContents(entry.owner, entry.repo, '');
|
||
});
|
||
|
||
// Separator
|
||
const sep = document.createElement('div');
|
||
sep.style.cssText = 'height:1px;background:rgba(255,255,255,0.1);margin:4px 0;';
|
||
menu.appendChild(sep);
|
||
|
||
if (type === 'favorite') {
|
||
addItem('⭐', 'Aus Favoriten entfernen', async () => {
|
||
await toggleFavorite(entry.owner, entry.repo, entry.cloneUrl);
|
||
loadGiteaRepos();
|
||
}, '#f59e0b');
|
||
} else {
|
||
addItem('⭐', 'Zu Favoriten hinzufügen', async () => {
|
||
await toggleFavorite(entry.owner, entry.repo, entry.cloneUrl);
|
||
loadGiteaRepos();
|
||
});
|
||
addItem('✕', 'Aus Verlauf entfernen', async () => {
|
||
recentRepos = recentRepos.filter(r => !(r.owner === entry.owner && r.repo === entry.repo));
|
||
await window.electronAPI.saveRecent(recentRepos);
|
||
loadGiteaRepos();
|
||
}, '#ef4444');
|
||
}
|
||
|
||
document.body.appendChild(menu);
|
||
setTimeout(() => document.addEventListener('click', () => menu.remove(), { once: true }), 10);
|
||
}
|
||
|
||
// Speichert den default_branch pro Repo (owner/repo -> 'main' oder 'master')
|
||
let repoDefaultBranches = {};
|
||
|
||
function getDefaultBranch(owner, repo) {
|
||
return repoDefaultBranches[`${owner}/${repo}`] || 'HEAD';
|
||
}
|
||
|
||
// Navigations-Status für die Explorer-Ansicht
|
||
let currentState = {
|
||
view: 'none', // 'local', 'gitea-list', 'gitea-repo'
|
||
owner: null,
|
||
repo: null,
|
||
path: ''
|
||
};
|
||
|
||
// Clipboard für Cut & Paste
|
||
let clipboard = {
|
||
item: null, // { path, name, type, owner, repo, isGitea, isLocal, nodePath }
|
||
action: null // 'cut'
|
||
};
|
||
|
||
// Mehrfachauswahl
|
||
let selectedItems = new Set(); // Set von item-Pfaden
|
||
let isMultiSelectMode = false;
|
||
|
||
// Zuletzt angeklicktes Item (für F2/Entf)
|
||
let lastSelectedItem = null; // { type:'gitea', item, owner, repo } | { type:'local', node }
|
||
|
||
// Feature-Flag für farbige Icons
|
||
let featureColoredIcons = true;
|
||
|
||
function setStatus(txt) {
|
||
const s = $('status');
|
||
if (s) s.innerText = txt || '';
|
||
}
|
||
|
||
/* -------------------------
|
||
TOAST NOTIFICATIONS
|
||
------------------------- */
|
||
function showToast(message, type = 'info', duration = 4000) {
|
||
const container = (() => {
|
||
let c = $('toastContainer');
|
||
if (!c) {
|
||
c = document.createElement('div');
|
||
c.id = 'toastContainer';
|
||
c.style.cssText = `
|
||
position: fixed;
|
||
bottom: 24px;
|
||
right: 24px;
|
||
z-index: 99999;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
pointer-events: none;
|
||
`;
|
||
document.body.appendChild(c);
|
||
}
|
||
return c;
|
||
})();
|
||
|
||
const colors = {
|
||
error: { bg: 'rgba(239,68,68,0.15)', border: '#ef4444', icon: '✗' },
|
||
success: { bg: 'rgba(34,197,94,0.15)', border: '#22c55e', icon: '✓' },
|
||
info: { bg: 'rgba(0,212,255,0.12)', border: '#00d4ff', icon: 'ℹ' },
|
||
warning: { bg: 'rgba(245,158,11,0.15)', border: '#f59e0b', icon: '⚠' },
|
||
};
|
||
const c = colors[type] || colors.info;
|
||
|
||
const toast = document.createElement('div');
|
||
toast.style.cssText = `
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 10px;
|
||
padding: 12px 16px;
|
||
background: ${c.bg};
|
||
border: 1px solid ${c.border};
|
||
border-left: 3px solid ${c.border};
|
||
border-radius: 10px;
|
||
backdrop-filter: blur(12px);
|
||
box-shadow: 0 4px 24px rgba(0,0,0,0.4);
|
||
color: #fff;
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
max-width: 360px;
|
||
pointer-events: auto;
|
||
cursor: pointer;
|
||
opacity: 0;
|
||
transform: translateX(20px);
|
||
transition: opacity 220ms ease, transform 220ms ease;
|
||
line-height: 1.4;
|
||
`;
|
||
|
||
const iconEl = document.createElement('span');
|
||
iconEl.style.cssText = `color: ${c.border}; font-weight: 700; font-size: 15px; flex-shrink: 0; margin-top: 1px;`;
|
||
iconEl.textContent = c.icon;
|
||
|
||
const msgEl = document.createElement('span');
|
||
msgEl.textContent = message;
|
||
|
||
toast.appendChild(iconEl);
|
||
toast.appendChild(msgEl);
|
||
container.appendChild(toast);
|
||
|
||
// Einblenden
|
||
requestAnimationFrame(() => {
|
||
toast.style.opacity = '1';
|
||
toast.style.transform = 'translateX(0)';
|
||
});
|
||
|
||
const dismiss = () => {
|
||
toast.style.opacity = '0';
|
||
toast.style.transform = 'translateX(20px)';
|
||
setTimeout(() => toast.remove(), 220);
|
||
};
|
||
|
||
toast.addEventListener('click', dismiss);
|
||
setTimeout(dismiss, duration);
|
||
}
|
||
|
||
// Kurzformen
|
||
function showError(msg) { setStatus(msg); showToast(msg, 'error'); }
|
||
function showSuccess(msg) { setStatus(msg); showToast(msg, 'success', 3000); }
|
||
function showWarning(msg) { setStatus(msg); showToast(msg, 'warning'); }
|
||
|
||
// Löschen-Bestätigung als Toast (ersetzt confirm())
|
||
function showDeleteConfirm(message, onConfirm) {
|
||
const container = (() => {
|
||
let c = $('toastContainer');
|
||
if (!c) { c = document.createElement('div'); c.id = 'toastContainer'; c.style.cssText = 'position:fixed;bottom:24px;right:24px;z-index:99999;display:flex;flex-direction:column;gap:10px;pointer-events:none;'; document.body.appendChild(c); }
|
||
return c;
|
||
})();
|
||
|
||
const toast = document.createElement('div');
|
||
toast.style.cssText = `
|
||
padding: 14px 16px;
|
||
background: rgba(239,68,68,0.15);
|
||
border: 1px solid #ef4444;
|
||
border-left: 3px solid #ef4444;
|
||
border-radius: 10px;
|
||
backdrop-filter: blur(12px);
|
||
box-shadow: 0 4px 24px rgba(0,0,0,0.4);
|
||
color: #fff;
|
||
font-size: 13px;
|
||
max-width: 360px;
|
||
pointer-events: auto;
|
||
opacity: 0;
|
||
transform: translateX(20px);
|
||
transition: opacity 220ms ease, transform 220ms ease;
|
||
`;
|
||
|
||
const msgEl = document.createElement('div');
|
||
msgEl.style.cssText = 'font-weight:600;margin-bottom:10px;';
|
||
msgEl.textContent = '🗑️ ' + message;
|
||
|
||
const btns = document.createElement('div');
|
||
btns.style.cssText = 'display:flex;gap:8px;justify-content:flex-end;';
|
||
|
||
const cancelBtn = document.createElement('button');
|
||
cancelBtn.textContent = 'Abbrechen';
|
||
cancelBtn.style.cssText = 'padding:5px 12px;border-radius:6px;border:1px solid rgba(255,255,255,0.2);background:transparent;color:#fff;cursor:pointer;font-size:12px;';
|
||
|
||
const confirmBtn = document.createElement('button');
|
||
confirmBtn.textContent = 'Löschen';
|
||
confirmBtn.style.cssText = 'padding:5px 12px;border-radius:6px;border:none;background:#ef4444;color:#fff;cursor:pointer;font-size:12px;font-weight:600;';
|
||
|
||
btns.appendChild(cancelBtn);
|
||
btns.appendChild(confirmBtn);
|
||
toast.appendChild(msgEl);
|
||
toast.appendChild(btns);
|
||
container.appendChild(toast);
|
||
|
||
requestAnimationFrame(() => { toast.style.opacity = '1'; toast.style.transform = 'translateX(0)'; });
|
||
|
||
const dismiss = () => { toast.style.opacity = '0'; toast.style.transform = 'translateX(20px)'; setTimeout(() => toast.remove(), 220); };
|
||
|
||
cancelBtn.addEventListener('click', dismiss);
|
||
confirmBtn.addEventListener('click', () => { dismiss(); onConfirm(); });
|
||
setTimeout(dismiss, 8000);
|
||
}
|
||
|
||
/* -------------------------
|
||
PROGRESS UI
|
||
------------------------- */
|
||
function ensureProgressUI() {
|
||
if ($('folderProgressContainer')) return;
|
||
const container = document.createElement('div');
|
||
container.id = 'folderProgressContainer';
|
||
container.style.cssText = `
|
||
position: fixed;
|
||
left: 50%;
|
||
top: 12px;
|
||
transform: translateX(-50%);
|
||
z-index: 10000;
|
||
width: 480px;
|
||
max-width: 90%;
|
||
padding: 12px 16px;
|
||
background: rgba(20,20,30,0.98);
|
||
border-radius: 12px;
|
||
box-shadow: 0 8px 32px rgba(0,0,0,0.6);
|
||
color: #fff;
|
||
font-family: sans-serif;
|
||
display: none;
|
||
backdrop-filter: blur(10px);
|
||
border: 1px solid rgba(0, 212, 255, 0.2);
|
||
`;
|
||
|
||
const text = document.createElement('div');
|
||
text.id = 'folderProgressText';
|
||
text.style.cssText = `
|
||
margin-bottom: 8px;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: #00d4ff;
|
||
`;
|
||
container.appendChild(text);
|
||
|
||
const barWrap = document.createElement('div');
|
||
barWrap.style.cssText = `
|
||
width: 100%;
|
||
height: 12px;
|
||
background: rgba(255,255,255,0.1);
|
||
border-radius: 6px;
|
||
overflow: hidden;
|
||
position: relative;
|
||
`;
|
||
|
||
const bar = document.createElement('div');
|
||
bar.id = 'folderProgressBar';
|
||
bar.style.cssText = `
|
||
width: 0%;
|
||
height: 100%;
|
||
background: linear-gradient(90deg, #00d4ff, #8b5cf6);
|
||
transition: width 200ms ease-out;
|
||
border-radius: 6px;
|
||
`;
|
||
|
||
barWrap.appendChild(bar);
|
||
container.appendChild(barWrap);
|
||
document.body.appendChild(container);
|
||
}
|
||
|
||
function showProgress(percent, text) {
|
||
ensureProgressUI();
|
||
const container = $('folderProgressContainer');
|
||
const bar = $('folderProgressBar');
|
||
const txt = $('folderProgressText');
|
||
if (txt) txt.innerText = text || '';
|
||
if (bar) bar.style.width = `${Math.min(100, Math.max(0, percent))}%`;
|
||
if (container) container.style.display = 'block';
|
||
}
|
||
|
||
function hideProgress() {
|
||
const container = $('folderProgressContainer');
|
||
if (container) {
|
||
setTimeout(() => {
|
||
container.style.display = 'none';
|
||
}, 500);
|
||
}
|
||
}
|
||
|
||
/* -------------------------
|
||
ADVANCED FILE EDITOR - WITH TABS, UNDO/REDO, AUTO-SAVE, LINE NUMBERS
|
||
------------------------- */
|
||
|
||
// Editor State
|
||
let openTabs = {}; // { filePath: { name, content, originalContent, dirty, icon, history, historyIndex } }
|
||
let currentActiveTab = null;
|
||
let autoSaveTimer = null;
|
||
let autoSaveInterval = 3000; // 3 sekunden
|
||
|
||
// Initialize editor
|
||
function initEditor() {
|
||
const textarea = $('fileEditorContent');
|
||
if (!textarea) return;
|
||
|
||
textarea.addEventListener('input', () => {
|
||
updateCurrentTab();
|
||
updateLineNumbers();
|
||
updateEditorStats();
|
||
triggerAutoSave();
|
||
});
|
||
|
||
textarea.addEventListener('scroll', () => {
|
||
const lineNumbers = $('lineNumbers');
|
||
if (lineNumbers) lineNumbers.scrollTop = textarea.scrollTop;
|
||
});
|
||
|
||
textarea.addEventListener('keydown', (e) => {
|
||
// Ctrl+Z - Undo
|
||
if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
|
||
e.preventDefault();
|
||
undoChange();
|
||
}
|
||
// Ctrl+Shift+Z or Ctrl+Y - Redo
|
||
if (((e.ctrlKey || e.metaKey) && e.key === 'z' && e.shiftKey) ||
|
||
((e.ctrlKey || e.metaKey) && e.key === 'y')) {
|
||
e.preventDefault();
|
||
redoChange();
|
||
}
|
||
// Tab insertion
|
||
if (e.key === 'Tab') {
|
||
e.preventDefault();
|
||
const start = textarea.selectionStart;
|
||
const end = textarea.selectionEnd;
|
||
textarea.value = textarea.value.substring(0, start) + '\t' + textarea.value.substring(end);
|
||
textarea.selectionStart = textarea.selectionEnd = start + 1;
|
||
updateCurrentTab();
|
||
updateLineNumbers();
|
||
}
|
||
});
|
||
}
|
||
|
||
// Add new tab
|
||
function addTab(filePath, fileName, content, isGitea = false, owner = null, repo = null) {
|
||
openTabs[filePath] = {
|
||
name: fileName,
|
||
content: content,
|
||
originalContent: content,
|
||
dirty: false,
|
||
icon: getFileIcon(fileName),
|
||
isGitea,
|
||
owner,
|
||
repo,
|
||
history: [content],
|
||
historyIndex: 0
|
||
};
|
||
|
||
currentActiveTab = filePath;
|
||
renderTabs();
|
||
updateEditor(); // Kann async sein
|
||
}
|
||
|
||
// Remove tab
|
||
function removeTab(filePath) {
|
||
delete openTabs[filePath];
|
||
|
||
if (currentActiveTab === filePath) {
|
||
const paths = Object.keys(openTabs);
|
||
currentActiveTab = paths.length > 0 ? paths[0] : null;
|
||
}
|
||
|
||
if (Object.keys(openTabs).length === 0) {
|
||
closeFileEditor();
|
||
} else {
|
||
renderTabs();
|
||
updateEditor();
|
||
}
|
||
}
|
||
|
||
// Switch tab
|
||
function switchTab(filePath) {
|
||
if (openTabs[filePath]) {
|
||
currentActiveTab = filePath;
|
||
renderTabs();
|
||
updateEditor(); // Kann async sein, aber wir warten nicht
|
||
}
|
||
}
|
||
|
||
// Render tabs
|
||
function renderTabs() {
|
||
const tabsContainer = $('fileEditorTabs');
|
||
if (!tabsContainer) return;
|
||
|
||
tabsContainer.innerHTML = '';
|
||
|
||
Object.entries(openTabs).forEach(([filePath, tab]) => {
|
||
const tabEl = document.createElement('div');
|
||
tabEl.className = `editor-tab ${currentActiveTab === filePath ? 'active' : ''}`;
|
||
|
||
const nameEl = document.createElement('div');
|
||
nameEl.className = 'editor-tab-name';
|
||
const iconEl = document.createElement('span');
|
||
iconEl.textContent = tab.icon;
|
||
const nameSpan = document.createElement('span');
|
||
nameSpan.textContent = tab.name;
|
||
if (tab.dirty) {
|
||
const dirtyEl = document.createElement('span');
|
||
dirtyEl.className = 'editor-tab-dirty';
|
||
dirtyEl.textContent = '●';
|
||
nameEl.appendChild(iconEl);
|
||
nameEl.appendChild(nameSpan);
|
||
nameEl.appendChild(dirtyEl);
|
||
} else {
|
||
nameEl.appendChild(iconEl);
|
||
nameEl.appendChild(nameSpan);
|
||
}
|
||
|
||
const closeBtn = document.createElement('button');
|
||
closeBtn.className = 'editor-tab-close';
|
||
closeBtn.textContent = '✕';
|
||
closeBtn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
if (tab.dirty && !confirm(`${tab.name} hat ungespeicherte Änderungen. Wirklich schließen?`)) {
|
||
return;
|
||
}
|
||
removeTab(filePath);
|
||
});
|
||
|
||
tabEl.appendChild(nameEl);
|
||
tabEl.appendChild(closeBtn);
|
||
tabEl.addEventListener('click', () => switchTab(filePath));
|
||
|
||
tabsContainer.appendChild(tabEl);
|
||
});
|
||
}
|
||
|
||
// Update current tab content
|
||
function updateCurrentTab() {
|
||
if (!currentActiveTab) return;
|
||
|
||
const textarea = $('fileEditorContent');
|
||
const content = textarea.value;
|
||
const tab = openTabs[currentActiveTab];
|
||
|
||
if (!tab) return;
|
||
|
||
tab.content = content;
|
||
tab.dirty = (content !== tab.originalContent);
|
||
|
||
renderTabs();
|
||
}
|
||
|
||
// Update editor display
|
||
async function updateEditor() {
|
||
if (!currentActiveTab || !openTabs[currentActiveTab]) return;
|
||
|
||
const tab = openTabs[currentActiveTab];
|
||
const textarea = $('fileEditorContent');
|
||
const imagePreview = $('imagePreview');
|
||
|
||
// Prüfe ob es eine Bilddatei ist
|
||
const isImage = /\.(png|jpg|jpeg|gif|webp|svg)$/i.test(currentActiveTab);
|
||
|
||
if (isImage) {
|
||
// Zeige Bild statt Textarea
|
||
if (textarea) textarea.classList.add('hidden');
|
||
if (imagePreview) {
|
||
imagePreview.classList.remove('hidden');
|
||
|
||
let imgSrc = '';
|
||
|
||
if (tab.isGitea) {
|
||
// Gitea-Bild: Lade via API
|
||
try {
|
||
const filePath = currentActiveTab.replace(`gitea://${tab.owner}/${tab.repo}/`, '');
|
||
const response = await window.electronAPI.readGiteaFile({
|
||
owner: tab.owner,
|
||
repo: tab.repo,
|
||
path: filePath,
|
||
ref: getDefaultBranch(tab.owner, tab.repo)
|
||
});
|
||
|
||
if (response.ok) {
|
||
// Content ist Base64 Text, konvertiere zu Data URL
|
||
const imageData = response.content;
|
||
const ext = filePath.split('.').pop().toLowerCase();
|
||
const mimeType = {
|
||
'png': 'image/png',
|
||
'jpg': 'image/jpeg',
|
||
'jpeg': 'image/jpeg',
|
||
'gif': 'image/gif',
|
||
'webp': 'image/webp',
|
||
'svg': 'image/svg+xml'
|
||
}[ext] || 'image/png';
|
||
|
||
imgSrc = `data:${mimeType};base64,${imageData}`;
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading Gitea image:', error);
|
||
imagePreview.innerHTML = '<div style="color: var(--text-muted); text-align: center;">Fehler beim Laden des Bildes</div>';
|
||
return;
|
||
}
|
||
} else {
|
||
// Lokale Datei
|
||
imgSrc = 'file:///' + currentActiveTab.replace(/\\/g, '/');
|
||
}
|
||
|
||
// Erstelle Bild-Element mit verbesserter Darstellung
|
||
const img = document.createElement('img');
|
||
img.src = imgSrc;
|
||
img.alt = tab.name;
|
||
img.style.cssText = `
|
||
max-width: 100%;
|
||
max-height: 85vh;
|
||
width: auto;
|
||
height: auto;
|
||
display: block;
|
||
margin: 0 auto;
|
||
object-fit: contain;
|
||
cursor: zoom-in;
|
||
`;
|
||
|
||
// Click zum Zoomen (Original-Größe)
|
||
let isZoomed = false;
|
||
img.onclick = function() {
|
||
if (isZoomed) {
|
||
img.style.maxWidth = '100%';
|
||
img.style.maxHeight = '85vh';
|
||
img.style.cursor = 'zoom-in';
|
||
isZoomed = false;
|
||
} else {
|
||
img.style.maxWidth = 'none';
|
||
img.style.maxHeight = 'none';
|
||
img.style.cursor = 'zoom-out';
|
||
isZoomed = true;
|
||
}
|
||
};
|
||
|
||
img.onerror = function() {
|
||
imagePreview.innerHTML = '<div style="color: var(--text-muted); text-align: center; padding: 40px;">Bild konnte nicht geladen werden</div>';
|
||
};
|
||
|
||
// Container für zentrierte Anzeige
|
||
imagePreview.innerHTML = '';
|
||
imagePreview.style.cssText = `
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
min-height: 400px;
|
||
overflow: auto;
|
||
padding: 20px;
|
||
`;
|
||
imagePreview.appendChild(img);
|
||
}
|
||
} else {
|
||
// Zeige Text Editor
|
||
if (textarea) {
|
||
textarea.classList.remove('hidden');
|
||
textarea.value = tab.content;
|
||
updateLineNumbers();
|
||
updateEditorStats();
|
||
}
|
||
if (imagePreview) imagePreview.classList.add('hidden');
|
||
}
|
||
|
||
updateTabInfo();
|
||
}
|
||
|
||
// Update tab info header
|
||
function updateTabInfo() {
|
||
const tab = openTabs[currentActiveTab];
|
||
if (!tab) return;
|
||
|
||
$('fileEditorName').textContent = tab.name;
|
||
$('fileEditorIcon').textContent = tab.icon;
|
||
|
||
const pathText = tab.isGitea ? `Gitea: ${tab.owner}/${tab.repo}/${currentActiveTab}` : `Pfad: ${currentActiveTab}`;
|
||
$('fileEditorPath').textContent = pathText;
|
||
|
||
const lines = tab.content.split('\n').length;
|
||
const bytes = new Blob([tab.content]).size;
|
||
$('fileEditorStats').textContent = `${lines} Zeilen • ${bytes} Bytes`;
|
||
}
|
||
|
||
// Update line numbers
|
||
function updateLineNumbers() {
|
||
const textarea = $('fileEditorContent');
|
||
const lineNumbers = $('lineNumbers');
|
||
if (!textarea || !lineNumbers) return;
|
||
|
||
const lines = textarea.value.split('\n').length;
|
||
let html = '';
|
||
for (let i = 1; i <= lines; i++) {
|
||
html += i + '\n';
|
||
}
|
||
lineNumbers.textContent = html;
|
||
lineNumbers.scrollTop = textarea.scrollTop;
|
||
}
|
||
|
||
// Update editor stats (cursor position)
|
||
function updateEditorStats() {
|
||
const textarea = $('fileEditorContent');
|
||
if (!textarea) return;
|
||
|
||
const lines = textarea.value.split('\n').length;
|
||
const startPos = textarea.selectionStart;
|
||
const textBeforeCursor = textarea.value.substring(0, startPos);
|
||
const line = textBeforeCursor.split('\n').length;
|
||
const col = startPos - textBeforeCursor.lastIndexOf('\n');
|
||
|
||
$('fileEditorCursor').textContent = `Zeile ${line}, Spalte ${col}`;
|
||
}
|
||
|
||
// Undo
|
||
function undoChange() {
|
||
if (!currentActiveTab) return;
|
||
|
||
const tab = openTabs[currentActiveTab];
|
||
if (tab.historyIndex > 0) {
|
||
tab.historyIndex--;
|
||
const textarea = $('fileEditorContent');
|
||
textarea.value = tab.history[tab.historyIndex];
|
||
updateCurrentTab();
|
||
updateLineNumbers();
|
||
updateEditorStats();
|
||
}
|
||
}
|
||
|
||
// Redo
|
||
function redoChange() {
|
||
if (!currentActiveTab) return;
|
||
|
||
const tab = openTabs[currentActiveTab];
|
||
if (tab.historyIndex < tab.history.length - 1) {
|
||
tab.historyIndex++;
|
||
const textarea = $('fileEditorContent');
|
||
textarea.value = tab.history[tab.historyIndex];
|
||
updateCurrentTab();
|
||
updateLineNumbers();
|
||
updateEditorStats();
|
||
}
|
||
}
|
||
|
||
// Push to history
|
||
function pushToHistory(content) {
|
||
if (!currentActiveTab) return;
|
||
|
||
const tab = openTabs[currentActiveTab];
|
||
// Remove any redo history
|
||
tab.history = tab.history.slice(0, tab.historyIndex + 1);
|
||
tab.history.push(content);
|
||
tab.historyIndex++;
|
||
|
||
// Limit history to 50 items
|
||
if (tab.history.length > 50) {
|
||
tab.history.shift();
|
||
tab.historyIndex--;
|
||
}
|
||
}
|
||
|
||
// Auto-Save
|
||
function triggerAutoSave() {
|
||
clearTimeout(autoSaveTimer);
|
||
autoSaveTimer = setTimeout(() => {
|
||
saveCurrentFile(true);
|
||
}, autoSaveInterval);
|
||
}
|
||
|
||
function showAutoSaveIndicator() {
|
||
const indicator = $('autoSaveStatus');
|
||
if (indicator) {
|
||
indicator.style.display = 'inline';
|
||
setTimeout(() => {
|
||
indicator.style.display = 'none';
|
||
}, 2000);
|
||
}
|
||
}
|
||
|
||
function closeFileEditor() {
|
||
// Überprüfe auf ungespeicherte Änderungen
|
||
const unsaved = Object.entries(openTabs).filter(([_, tab]) => tab.dirty);
|
||
|
||
if (unsaved.length > 0) {
|
||
if (!confirm(`${unsaved.length} Datei(en) haben ungespeicherte Änderungen. Wirklich schließen?`)) {
|
||
return;
|
||
}
|
||
}
|
||
|
||
openTabs = {};
|
||
currentActiveTab = null;
|
||
clearTimeout(autoSaveTimer);
|
||
const modal = $('fileEditorModal');
|
||
if (modal) modal.classList.add('hidden');
|
||
}
|
||
|
||
async function openFileEditor(filePath, fileName) {
|
||
try {
|
||
console.log('🔍 Opening file:', filePath);
|
||
|
||
// Wenn bereits offen, nur switchen
|
||
if (openTabs[filePath]) {
|
||
switchTab(filePath);
|
||
const modal = $('fileEditorModal');
|
||
if (modal) modal.classList.remove('hidden');
|
||
return;
|
||
}
|
||
|
||
// Prüfe ob es eine Bilddatei ist
|
||
const isImage = /\.(png|jpg|jpeg|gif|webp|svg)$/i.test(fileName);
|
||
|
||
if (isImage) {
|
||
// Für Bilder brauchen wir keinen Content zu lesen
|
||
addTab(filePath, fileName, '', false, null, null);
|
||
} else {
|
||
// Lese Text-Datei
|
||
const response = await window.electronAPI.readFile({ path: filePath });
|
||
|
||
if (response.ok) {
|
||
addTab(filePath, fileName, response.content);
|
||
} else {
|
||
alert(`Fehler: ${response.error}`);
|
||
return;
|
||
}
|
||
}
|
||
|
||
const modal = $('fileEditorModal');
|
||
if (modal) {
|
||
modal.classList.remove('hidden');
|
||
initEditor();
|
||
$('fileEditorContent').focus();
|
||
}
|
||
|
||
setStatus(`Editiere: ${fileName}`);
|
||
console.log('✅ File opened');
|
||
} catch (error) {
|
||
console.error('Error opening file:', error);
|
||
alert('Fehler beim Öffnen der Datei');
|
||
}
|
||
}
|
||
|
||
async function openGiteaFileInEditor(owner, repo, filePath, fileName) {
|
||
try {
|
||
console.log('🔍 Loading Gitea file:', owner, repo, filePath);
|
||
setStatus('Lädt Datei...');
|
||
|
||
// Wenn bereits offen, nur switchen
|
||
const vPath = `gitea://${owner}/${repo}/${filePath}`;
|
||
if (openTabs[vPath]) {
|
||
switchTab(vPath);
|
||
const modal = $('fileEditorModal');
|
||
if (modal) modal.classList.remove('hidden');
|
||
return;
|
||
}
|
||
|
||
// Lade Datei-Content vom Gitea Handler
|
||
const response = await window.electronAPI.readGiteaFile({
|
||
owner,
|
||
repo,
|
||
path: filePath,
|
||
ref: getDefaultBranch(owner, repo)
|
||
});
|
||
|
||
if (response.ok) {
|
||
addTab(vPath, fileName, response.content, true, owner, repo);
|
||
|
||
const modal = $('fileEditorModal');
|
||
if (modal) {
|
||
modal.classList.remove('hidden');
|
||
initEditor();
|
||
$('fileEditorContent').focus();
|
||
}
|
||
|
||
setStatus(`Editiere: ${fileName}`);
|
||
console.log('✅ Gitea file opened');
|
||
} else {
|
||
alert(`Fehler: ${response.error}`);
|
||
showError('Fehler beim Laden der Datei');
|
||
}
|
||
} catch (error) {
|
||
console.error('Error opening Gitea file:', error);
|
||
alert('Fehler beim Öffnen der Datei');
|
||
showError('Fehler');
|
||
}
|
||
}
|
||
|
||
// Farbige Icons pro Dateityp (emoji + Farb-Overlay via CSS-Klassen)
|
||
const FILE_ICONS = {
|
||
// Web
|
||
js: { icon: '📄', color: '#f7df1e', label: 'JS' },
|
||
jsx: { icon: '📄', color: '#61dafb', label: 'JSX' },
|
||
ts: { icon: '📄', color: '#3178c6', label: 'TS' },
|
||
tsx: { icon: '📄', color: '#3178c6', label: 'TSX' },
|
||
html: { icon: '📄', color: '#e34c26', label: 'HTML' },
|
||
css: { icon: '📄', color: '#264de4', label: 'CSS' },
|
||
scss: { icon: '📄', color: '#cd6799', label: 'SCSS' },
|
||
vue: { icon: '📄', color: '#42b883', label: 'VUE' },
|
||
svelte:{ icon: '📄', color: '#ff3e00', label: 'SVE' },
|
||
// Backend
|
||
py: { icon: '📄', color: '#3572a5', label: 'PY' },
|
||
java: { icon: '📄', color: '#b07219', label: 'JAVA' },
|
||
rb: { icon: '📄', color: '#701516', label: 'RB' },
|
||
php: { icon: '📄', color: '#4f5d95', label: 'PHP' },
|
||
go: { icon: '📄', color: '#00add8', label: 'GO' },
|
||
rs: { icon: '📄', color: '#dea584', label: 'RS' },
|
||
cs: { icon: '📄', color: '#178600', label: 'C#' },
|
||
cpp: { icon: '📄', color: '#f34b7d', label: 'C++' },
|
||
c: { icon: '📄', color: '#555555', label: 'C' },
|
||
// Config
|
||
json: { icon: '📄', color: '#fbc02d', label: 'JSON' },
|
||
yaml: { icon: '📄', color: '#cb171e', label: 'YAML' },
|
||
yml: { icon: '📄', color: '#cb171e', label: 'YAML' },
|
||
toml: { icon: '📄', color: '#9c4221', label: 'TOML' },
|
||
env: { icon: '📄', color: '#ecd53f', label: 'ENV' },
|
||
xml: { icon: '📄', color: '#f60', label: 'XML' },
|
||
// Docs
|
||
md: { icon: '📄', color: '#083fa1', label: 'MD' },
|
||
txt: { icon: '📄', color: '#888', label: 'TXT' },
|
||
pdf: { icon: '📄', color: '#e53935', label: 'PDF' },
|
||
// Shell
|
||
sh: { icon: '📄', color: '#89e051', label: 'SH' },
|
||
bat: { icon: '📄', color: '#c1f12e', label: 'BAT' },
|
||
// Images
|
||
png: { icon: '🖼️', color: '#4caf50', label: 'PNG' },
|
||
jpg: { icon: '🖼️', color: '#4caf50', label: 'JPG' },
|
||
jpeg: { icon: '🖼️', color: '#4caf50', label: 'JPG' },
|
||
gif: { icon: '🖼️', color: '#4caf50', label: 'GIF' },
|
||
svg: { icon: '🖼️', color: '#ff9800', label: 'SVG' },
|
||
webp: { icon: '🖼️', color: '#4caf50', label: 'WEBP' },
|
||
// Archives
|
||
zip: { icon: '📦', color: '#ff9800', label: 'ZIP' },
|
||
tar: { icon: '📦', color: '#ff9800', label: 'TAR' },
|
||
gz: { icon: '📦', color: '#ff9800', label: 'GZ' },
|
||
};
|
||
|
||
function getFileIcon(fileName) {
|
||
const ext = fileName.split('.').pop()?.toLowerCase();
|
||
const info = FILE_ICONS[ext];
|
||
if (!info) return '📄';
|
||
return info.icon;
|
||
}
|
||
|
||
// Gibt ein DOM-Element für das Explorer-Icon zurück (mit farbigem Badge wenn aktiviert)
|
||
function makeFileIconEl(fileName, isDir = false) {
|
||
const wrapper = document.createElement('div');
|
||
wrapper.className = 'item-icon';
|
||
|
||
if (isDir) {
|
||
wrapper.textContent = '📁';
|
||
return wrapper;
|
||
}
|
||
|
||
const ext = fileName.split('.').pop()?.toLowerCase();
|
||
const info = featureColoredIcons ? FILE_ICONS[ext] : null;
|
||
|
||
wrapper.textContent = info ? info.icon : '📄';
|
||
|
||
if (info) {
|
||
const badge = document.createElement('span');
|
||
badge.className = 'file-type-badge';
|
||
badge.textContent = info.label;
|
||
badge.style.background = info.color;
|
||
// Helligkeit prüfen für Textfarbe
|
||
const hex = info.color.replace('#','');
|
||
const r = parseInt(hex.slice(0,2)||'88',16);
|
||
const g = parseInt(hex.slice(2,4)||'88',16);
|
||
const b = parseInt(hex.slice(4,6)||'88',16);
|
||
const lum = (0.299*r + 0.587*g + 0.114*b) / 255;
|
||
badge.style.color = lum > 0.55 ? '#111' : '#fff';
|
||
wrapper.appendChild(badge);
|
||
}
|
||
|
||
return wrapper;
|
||
}
|
||
|
||
/* -------------------------
|
||
SEARCH & REPLACE
|
||
------------------------- */
|
||
function toggleSearch() {
|
||
const searchBar = $('searchBar');
|
||
if (searchBar.classList.contains('hidden')) {
|
||
searchBar.classList.remove('hidden');
|
||
$('searchInput').focus();
|
||
} else {
|
||
searchBar.classList.add('hidden');
|
||
}
|
||
}
|
||
|
||
function performSearch() {
|
||
const searchTerm = $('searchInput').value;
|
||
const textarea = $('fileEditorContent');
|
||
|
||
if (!searchTerm || !textarea) return;
|
||
|
||
const text = textarea.value;
|
||
const regex = new RegExp(searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
|
||
const matches = [...text.matchAll(regex)];
|
||
|
||
$('searchInfo').textContent = matches.length > 0 ? `${matches.length} gefunden` : '0 gefunden';
|
||
|
||
if (matches.length > 0) {
|
||
const firstMatch = matches[0];
|
||
textarea.setSelectionRange(firstMatch.index, firstMatch.index + firstMatch[0].length);
|
||
textarea.focus();
|
||
}
|
||
}
|
||
|
||
function replaceOnce() {
|
||
const searchTerm = $('searchInput').value;
|
||
const replaceTerm = $('replaceInput').value;
|
||
const textarea = $('fileEditorContent');
|
||
|
||
if (!searchTerm || !textarea) return;
|
||
|
||
const text = textarea.value;
|
||
const newText = text.replace(new RegExp(searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i'), replaceTerm);
|
||
|
||
textarea.value = newText;
|
||
pushToHistory(newText);
|
||
updateCurrentTab();
|
||
updateLineNumbers();
|
||
performSearch();
|
||
}
|
||
|
||
function replaceAll() {
|
||
const searchTerm = $('searchInput').value;
|
||
const replaceTerm = $('replaceInput').value;
|
||
const textarea = $('fileEditorContent');
|
||
|
||
if (!searchTerm || !textarea) return;
|
||
|
||
const text = textarea.value;
|
||
const newText = text.replace(new RegExp(searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi'), replaceTerm);
|
||
|
||
textarea.value = newText;
|
||
pushToHistory(newText);
|
||
updateCurrentTab();
|
||
updateLineNumbers();
|
||
performSearch();
|
||
}
|
||
|
||
async function saveCurrentFile(isAutoSave = false) {
|
||
if (!currentActiveTab) return;
|
||
|
||
const tab = openTabs[currentActiveTab];
|
||
|
||
// Prüfe ob es eine Bilddatei ist
|
||
if (/\.(png|jpg|jpeg|gif|webp|svg)$/i.test(currentActiveTab)) {
|
||
alert('Bilder können nicht bearbeitet werden');
|
||
return;
|
||
}
|
||
|
||
const textarea = $('fileEditorContent');
|
||
const content = textarea.value;
|
||
|
||
if (!isAutoSave) setStatus('Speichert...');
|
||
|
||
try {
|
||
let response;
|
||
|
||
// Prüfe ob es eine Gitea-Datei ist
|
||
if (tab.isGitea) {
|
||
response = await window.electronAPI.writeGiteaFile({
|
||
owner: tab.owner,
|
||
repo: tab.repo,
|
||
path: currentActiveTab.replace(`gitea://${tab.owner}/${tab.repo}/`, ''),
|
||
content: content,
|
||
ref: getDefaultBranch(tab.owner, tab.repo)
|
||
});
|
||
} else {
|
||
// Lokale Datei
|
||
response = await window.electronAPI.writeFile({
|
||
path: currentActiveTab,
|
||
content: content
|
||
});
|
||
}
|
||
|
||
if (response.ok) {
|
||
tab.originalContent = content;
|
||
tab.dirty = false;
|
||
// Push current state to history
|
||
pushToHistory(content);
|
||
renderTabs();
|
||
|
||
if (isAutoSave) {
|
||
showAutoSaveIndicator();
|
||
} else {
|
||
setStatus(`✓ Gespeichert: ${tab.name}`);
|
||
}
|
||
console.log('✅ File saved');
|
||
} else {
|
||
alert(`Fehler: ${response.error}`);
|
||
}
|
||
} catch (error) {
|
||
console.error('Error saving file:', error);
|
||
alert('Fehler beim Speichern');
|
||
}
|
||
}
|
||
|
||
function updateEditorStats() {
|
||
const textarea = $('fileEditorContent');
|
||
if (!textarea) return;
|
||
|
||
const lines = textarea.value.split('\n').length;
|
||
const startPos = textarea.selectionStart;
|
||
const textBeforeCursor = textarea.value.substring(0, startPos);
|
||
const line = textBeforeCursor.split('\n').length;
|
||
const col = startPos - textBeforeCursor.lastIndexOf('\n');
|
||
|
||
$('fileEditorCursor').textContent = `Zeile ${line}, Spalte ${col}`;
|
||
}
|
||
|
||
/* -------------------------
|
||
MARKDOWN PARSER
|
||
------------------------- */
|
||
function parseMarkdownToHTML(markdown) {
|
||
if (!markdown) return '';
|
||
|
||
let html = markdown;
|
||
|
||
// Check if content already contains HTML (starts with < or has closing tags)
|
||
const hasHTML = /<[a-zA-Z][\s\S]*>/.test(html);
|
||
|
||
if (!hasHTML) {
|
||
// Only escape and parse if no HTML present
|
||
html = html.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>');
|
||
}
|
||
|
||
// Convert markdown patterns
|
||
// Headings: ### Title → <h3>Title</h3>
|
||
html = html.replace(/^###### (.*?)$/gm, '<h6>$1</h6>');
|
||
html = html.replace(/^##### (.*?)$/gm, '<h5>$1</h5>');
|
||
html = html.replace(/^#### (.*?)$/gm, '<h4>$1</h4>');
|
||
html = html.replace(/^### (.*?)$/gm, '<h3>$1</h3>');
|
||
html = html.replace(/^## (.*?)$/gm, '<h2>$1</h2>');
|
||
html = html.replace(/^# (.*?)$/gm, '<h1>$1</h1>');
|
||
|
||
// Horizontal rule: --- or *** or ___
|
||
html = html.replace(/^\-{3,}$/gm, '<hr>');
|
||
html = html.replace(/^\*{3,}$/gm, '<hr>');
|
||
html = html.replace(/^_{3,}$/gm, '<hr>');
|
||
|
||
// Bold: **text** or __text__
|
||
html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
|
||
html = html.replace(/__(.*?)__/g, '<strong>$1</strong>');
|
||
|
||
// Italic: *text* or _text_ (but not within words)
|
||
html = html.replace(/\s\*(.*?)\*\s/g, ' <em>$1</em> ');
|
||
html = html.replace(/\s_(.*?)_\s/g, ' <em>$1</em> ');
|
||
|
||
// Convert line breaks to <br>
|
||
html = html.replace(/\n/g, '<br>');
|
||
|
||
// Wrap plain text in paragraphs (text not already in tags)
|
||
let lines = html.split('<br>');
|
||
lines = lines.map(line => {
|
||
line = line.trim();
|
||
if (line && !line.match(/^</) && line.length > 0) {
|
||
// Only wrap if not already a tag
|
||
if (!line.match(/^<(h[1-6]|hr|p|div|ul|ol|li|em|strong|b|i)/)) {
|
||
return '<p>' + line + '</p>';
|
||
}
|
||
}
|
||
return line;
|
||
});
|
||
|
||
html = lines.join('<br>');
|
||
|
||
return html;
|
||
}
|
||
|
||
/* -------------------------
|
||
NAVIGATION & UI UPDATES
|
||
------------------------- */
|
||
function updateNavigationUI() {
|
||
const btnBack = $('btnBack');
|
||
if (!btnBack) return;
|
||
|
||
// Back Button zeigen, wenn wir in einem Repo oder tief in Ordnern sind
|
||
if (currentState.view === 'gitea-repo' ||
|
||
(currentState.view === 'gitea-list' && currentState.path !== '')) {
|
||
btnBack.classList.remove('hidden');
|
||
} else {
|
||
btnBack.classList.add('hidden');
|
||
}
|
||
}
|
||
|
||
/* -------------------------
|
||
GITEA CORE LOGIK (GRID)
|
||
------------------------- */
|
||
async function loadGiteaRepos() {
|
||
currentState.view = 'gitea-list';
|
||
currentState.path = '';
|
||
updateNavigationUI();
|
||
|
||
// Verstecke Commits & Releases-Buttons in Repo-Liste
|
||
const btnCommits = $('btnCommits');
|
||
const btnReleases = $('btnReleases');
|
||
if (btnCommits) btnCommits.classList.add('hidden');
|
||
if (btnReleases) btnReleases.classList.add('hidden');
|
||
|
||
// WICHTIG: Grid-Layout zurücksetzen
|
||
const grid = $('explorerGrid');
|
||
if (grid) {
|
||
grid.style.gridTemplateColumns = '';
|
||
}
|
||
|
||
setStatus('Loading Gitea repos...');
|
||
|
||
try {
|
||
const res = await window.electronAPI.listGiteaRepos();
|
||
if (!res.ok) {
|
||
showError('Failed to load repos: ' + (res.error || 'Unknown error'));
|
||
return;
|
||
}
|
||
|
||
const grid = $('explorerGrid');
|
||
if (!grid) return;
|
||
grid.innerHTML = '';
|
||
|
||
if (!res.repos || res.repos.length === 0) {
|
||
grid.innerHTML = '<div style="grid-column: 1/-1; text-align: center; padding: 40px; color: var(--text-muted);">Keine Repositories gefunden</div>';
|
||
setStatus('No repositories found');
|
||
return;
|
||
}
|
||
|
||
// --- NEU: Suchfeld für Projekte ---
|
||
const searchContainer = document.createElement('div');
|
||
searchContainer.style.cssText = 'grid-column: 1/-1; margin-bottom: 20px;';
|
||
|
||
const searchInput = document.createElement('input');
|
||
searchInput.type = 'text';
|
||
searchInput.placeholder = '🔍 Projekt suchen...';
|
||
searchInput.style.cssText = `
|
||
width: 100%;
|
||
padding: 12px 16px;
|
||
border-radius: var(--radius-md);
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
background: var(--bg-tertiary);
|
||
color: var(--text-primary);
|
||
font-size: 14px;
|
||
outline: none;
|
||
box-sizing: border-box;
|
||
`;
|
||
|
||
// Search Focus Effekt
|
||
searchInput.addEventListener('focus', () => {
|
||
searchInput.style.borderColor = 'var(--accent-primary)';
|
||
});
|
||
searchInput.addEventListener('blur', () => {
|
||
searchInput.style.borderColor = 'rgba(255, 255, 255, 0.1)';
|
||
});
|
||
|
||
searchContainer.appendChild(searchInput);
|
||
grid.appendChild(searchContainer);
|
||
|
||
// Search Logic
|
||
searchInput.addEventListener('input', (e) => {
|
||
const val = e.target.value.toLowerCase();
|
||
const cards = grid.querySelectorAll('.item-card');
|
||
cards.forEach(card => {
|
||
const name = card.querySelector('.item-name').textContent.toLowerCase();
|
||
if (name.includes(val)) {
|
||
card.style.display = 'flex';
|
||
} else {
|
||
card.style.display = 'none';
|
||
}
|
||
});
|
||
});
|
||
// -----------------------------------
|
||
|
||
// ── Favoriten & Zuletzt geöffnet ──
|
||
// Sektion IMMER ins DOM einfügen (auch wenn leer),
|
||
// damit der Stern-Klick später $('favRecentSection') findet
|
||
if (featureFavorites || featureRecent) {
|
||
const favSection = document.createElement('div');
|
||
favSection.id = 'favRecentSection';
|
||
favSection.style.cssText = 'grid-column: 1/-1;';
|
||
grid.appendChild(favSection);
|
||
renderFavRecentSection(favSection, res.repos);
|
||
}
|
||
|
||
res.repos.forEach(repo => {
|
||
let owner = (repo.owner && (repo.owner.login || repo.owner.username)) || null;
|
||
let repoName = repo.name;
|
||
let cloneUrl = repo.clone_url || repo.clone_url_ssh;
|
||
|
||
// default_branch speichern (main ODER master je nach Repo)
|
||
const defaultBranch = repo.default_branch || 'HEAD';
|
||
repoDefaultBranches[`${owner}/${repoName}`] = defaultBranch;
|
||
|
||
const card = document.createElement('div');
|
||
card.className = 'item-card';
|
||
card.style.position = 'relative';
|
||
card.dataset.cloneUrl = cloneUrl;
|
||
|
||
// Stern-Button (nur wenn Favoriten-Feature aktiv)
|
||
if (featureFavorites) {
|
||
const starBtn = document.createElement('button');
|
||
starBtn.className = 'fav-star-btn' + (isFavorite(owner, repoName) ? ' active' : '');
|
||
starBtn.title = isFavorite(owner, repoName) ? 'Aus Favoriten entfernen' : 'Zu Favoriten hinzufügen';
|
||
starBtn.textContent = isFavorite(owner, repoName) ? '⭐' : '☆';
|
||
starBtn.addEventListener('click', async (e) => {
|
||
e.stopPropagation();
|
||
await toggleFavorite(owner, repoName, cloneUrl);
|
||
const nowFav = isFavorite(owner, repoName);
|
||
starBtn.textContent = nowFav ? '⭐' : '☆';
|
||
starBtn.title = nowFav ? 'Aus Favoriten entfernen' : 'Zu Favoriten hinzufügen';
|
||
starBtn.classList.toggle('active', nowFav);
|
||
// Favoriten-Sektion live aktualisieren
|
||
const sec = $('favRecentSection');
|
||
if (sec) renderFavRecentSection(sec, res.repos);
|
||
});
|
||
card.appendChild(starBtn);
|
||
}
|
||
|
||
const iconEl = document.createElement('div');
|
||
iconEl.className = 'item-icon';
|
||
iconEl.textContent = '📦';
|
||
card.appendChild(iconEl);
|
||
const nameEl = document.createElement('div');
|
||
nameEl.className = 'item-name';
|
||
nameEl.textContent = repoName;
|
||
card.appendChild(nameEl);
|
||
|
||
// Repo-Größe anzeigen
|
||
if (repo.size != null) {
|
||
const sizeEl = document.createElement('div');
|
||
sizeEl.className = 'repo-size-badge';
|
||
const kb = repo.size;
|
||
sizeEl.textContent = kb >= 1024
|
||
? `${(kb / 1024).toFixed(1)} MB`
|
||
: `${kb} KB`;
|
||
card.appendChild(sizeEl);
|
||
}
|
||
|
||
// --- Nativer Drag Start (Download) ---
|
||
card.draggable = true;
|
||
card.addEventListener('dragstart', async (ev) => {
|
||
ev.preventDefault();
|
||
setStatus(`Preparing download for ${repoName}...`);
|
||
showProgress(0, `Preparing ${repoName}...`);
|
||
|
||
try {
|
||
const resDrag = await window.electronAPI.prepareDownloadDrag({
|
||
owner,
|
||
repo: repoName,
|
||
path: ''
|
||
});
|
||
|
||
if (resDrag.ok) {
|
||
window.electronAPI.startNativeDrag(resDrag.tempPath);
|
||
setStatus('Ready to drag');
|
||
} else {
|
||
showError('Download preparation failed');
|
||
}
|
||
} catch (error) {
|
||
console.error('Drag preparation error:', error);
|
||
showError('Error preparing download');
|
||
} finally {
|
||
hideProgress();
|
||
}
|
||
});
|
||
|
||
// --- Nativer Drop (Upload in Repo Root) ---
|
||
card.addEventListener('dragover', (ev) => {
|
||
ev.preventDefault();
|
||
ev.stopPropagation();
|
||
card.classList.add('drag-target');
|
||
});
|
||
|
||
card.addEventListener('dragleave', (ev) => {
|
||
ev.preventDefault();
|
||
ev.stopPropagation();
|
||
card.classList.remove('drag-target');
|
||
});
|
||
|
||
card.addEventListener('drop', async (ev) => {
|
||
ev.preventDefault();
|
||
ev.stopPropagation();
|
||
card.classList.remove('drag-target');
|
||
|
||
const files = ev.dataTransfer.files;
|
||
if (!files || files.length === 0) {
|
||
showWarning("Keine Dateien zum Upload gefunden.");
|
||
return;
|
||
}
|
||
|
||
const paths = Array.from(files).map(f => f.path);
|
||
setStatus(`Starte Upload von ${paths.length} Elementen...`);
|
||
|
||
for (const p of paths) {
|
||
const baseName = p.split(/[\\/]/).pop();
|
||
showProgress(0, `Sende: ${baseName}`);
|
||
|
||
try {
|
||
const res = await window.electronAPI.uploadAndPush({
|
||
localFolder: p,
|
||
owner,
|
||
repo: repoName,
|
||
destPath: '',
|
||
cloneUrl,
|
||
branch: getDefaultBranch(owner, repoName)
|
||
});
|
||
|
||
if (!res.ok) {
|
||
console.error("Upload Fehler:", res.error);
|
||
showError("Fehler: " + res.error);
|
||
}
|
||
} catch (err) {
|
||
console.error("Kritischer Upload Fehler:", err);
|
||
setStatus("Upload fehlgeschlagen");
|
||
}
|
||
}
|
||
|
||
hideProgress();
|
||
setStatus('Upload abgeschlossen');
|
||
});
|
||
|
||
card.onclick = () => {
|
||
addToRecent(owner, repoName, cloneUrl);
|
||
loadRepoContents(owner, repoName, '');
|
||
};
|
||
card.oncontextmenu = (ev) => showRepoContextMenu(ev, owner, repoName, cloneUrl, card);
|
||
grid.appendChild(card);
|
||
});
|
||
|
||
setStatus(`Loaded ${res.repos.length} repos`);
|
||
} catch (error) {
|
||
console.error('Error loading repos:', error);
|
||
showError('Error loading repositories');
|
||
}
|
||
}
|
||
|
||
async function loadRepoContents(owner, repo, path) {
|
||
currentState.view = 'gitea-repo';
|
||
currentState.owner = owner;
|
||
currentState.repo = repo;
|
||
currentState.path = path;
|
||
updateNavigationUI();
|
||
|
||
// Zeige Commits & Releases-Buttons wenn wir in einem Repo sind
|
||
const btnCommits = $('btnCommits');
|
||
const btnReleases = $('btnReleases');
|
||
|
||
if (btnCommits) {
|
||
btnCommits.classList.remove('hidden');
|
||
btnCommits.onclick = () => loadCommitHistory(owner, repo, getDefaultBranch(owner, repo));
|
||
}
|
||
|
||
if (btnReleases) {
|
||
btnReleases.classList.remove('hidden');
|
||
btnReleases.onclick = () => loadRepoReleases(owner, repo);
|
||
}
|
||
|
||
// WICHTIG: Grid-Layout zurücksetzen
|
||
const grid = $('explorerGrid');
|
||
if (grid) {
|
||
grid.style.gridTemplateColumns = '';
|
||
}
|
||
|
||
setStatus(`Loading: /${path || 'root'}`);
|
||
|
||
const ref = getDefaultBranch(owner, repo);
|
||
|
||
try {
|
||
const res = await window.electronAPI.getGiteaRepoContents({
|
||
owner,
|
||
repo,
|
||
path,
|
||
ref
|
||
});
|
||
|
||
if (!res.ok) {
|
||
showError('Error: ' + (res.error || 'Unknown error'));
|
||
return;
|
||
}
|
||
|
||
const grid = $('explorerGrid');
|
||
if (!grid) return;
|
||
grid.innerHTML = '';
|
||
|
||
if (!res.items || res.items.length === 0) {
|
||
const emptyMsg = res.empty
|
||
? '📭 Leeres Repository — noch keine Commits'
|
||
: '📂 Leerer Ordner';
|
||
grid.innerHTML = `<div style="grid-column: 1/-1; text-align: center; padding: 40px; color: var(--text-muted);">${emptyMsg}</div>`;
|
||
setStatus(res.empty ? 'Leeres Repository' : 'Leerer Ordner');
|
||
return;
|
||
}
|
||
|
||
res.items.forEach(item => {
|
||
const card = document.createElement('div');
|
||
card.className = 'item-card';
|
||
// Farbiges Icon-Element
|
||
const iconEl = makeFileIconEl(item.name, item.type === 'dir');
|
||
const nameEl = document.createElement('div');
|
||
nameEl.className = 'item-name';
|
||
nameEl.textContent = item.name;
|
||
card.appendChild(iconEl);
|
||
card.appendChild(nameEl);
|
||
// lastSelectedItem tracken
|
||
card.addEventListener('click', () => { lastSelectedItem = { type: 'gitea', item, owner, repo }; });
|
||
|
||
// Drag für Files und Folders
|
||
if (item.type === 'dir') {
|
||
card.draggable = true;
|
||
card.addEventListener('dragstart', async (ev) => {
|
||
ev.preventDefault();
|
||
showProgress(0, `Preparing ${item.name}...`);
|
||
|
||
try {
|
||
const resDrag = await window.electronAPI.prepareDownloadDrag({
|
||
owner,
|
||
repo,
|
||
path: item.path
|
||
});
|
||
|
||
if (resDrag.ok) {
|
||
window.electronAPI.startNativeDrag(resDrag.tempPath);
|
||
}
|
||
} catch (error) {
|
||
console.error('Drag error:', error);
|
||
} finally {
|
||
hideProgress();
|
||
}
|
||
});
|
||
}
|
||
|
||
// Drop in Ordner
|
||
card.addEventListener('dragover', (ev) => {
|
||
ev.preventDefault();
|
||
ev.stopPropagation();
|
||
if (item.type === 'dir') card.classList.add('drag-target');
|
||
});
|
||
|
||
card.addEventListener('dragleave', (ev) => {
|
||
ev.preventDefault();
|
||
ev.stopPropagation();
|
||
card.classList.remove('drag-target');
|
||
});
|
||
|
||
card.addEventListener('drop', async (ev) => {
|
||
ev.preventDefault();
|
||
ev.stopPropagation();
|
||
card.classList.remove('drag-target');
|
||
|
||
if (item.type !== 'dir') return;
|
||
|
||
const files = ev.dataTransfer.files;
|
||
if (!files || files.length === 0) return;
|
||
|
||
const paths = Array.from(files).map(f => f.path);
|
||
const targetPath = item.path;
|
||
|
||
for (const p of paths) {
|
||
const baseName = p.split(/[\\/]/).pop();
|
||
showProgress(0, `Uploading ${baseName} to ${targetPath}...`);
|
||
|
||
try {
|
||
await window.electronAPI.uploadAndPush({
|
||
localFolder: p,
|
||
owner,
|
||
repo,
|
||
destPath: targetPath,
|
||
branch: getDefaultBranch(owner, repo)
|
||
});
|
||
} catch (error) {
|
||
console.error('Upload error:', error);
|
||
}
|
||
}
|
||
|
||
hideProgress();
|
||
loadRepoContents(owner, repo, path);
|
||
});
|
||
|
||
if (item.type === 'dir') {
|
||
card.onclick = (e) => {
|
||
if (e.ctrlKey || e.metaKey) {
|
||
if (selectedItems.has(item.path)) { selectedItems.delete(item.path); card.classList.remove('selected'); }
|
||
else { selectedItems.add(item.path); card.classList.add('selected'); }
|
||
return;
|
||
}
|
||
selectedItems.clear();
|
||
document.querySelectorAll('.item-card.selected').forEach(c => c.classList.remove('selected'));
|
||
loadRepoContents(owner, repo, item.path);
|
||
};
|
||
} else {
|
||
card.onclick = (e) => {
|
||
if (e.ctrlKey || e.metaKey) {
|
||
if (selectedItems.has(item.path)) { selectedItems.delete(item.path); card.classList.remove('selected'); }
|
||
else { selectedItems.add(item.path); card.classList.add('selected'); }
|
||
return;
|
||
}
|
||
selectedItems.clear();
|
||
document.querySelectorAll('.item-card.selected').forEach(c => c.classList.remove('selected'));
|
||
openGiteaFileInEditor(owner, repo, item.path, item.name);
|
||
};
|
||
}
|
||
|
||
card.oncontextmenu = (ev) => showGiteaItemContextMenu(ev, item, owner, repo);
|
||
grid.appendChild(card);
|
||
});
|
||
|
||
setStatus(`Loaded ${res.items.length} items`);
|
||
} catch (error) {
|
||
console.error('Error loading repo contents:', error);
|
||
showError('Error loading contents');
|
||
}
|
||
}
|
||
|
||
/* -------------------------
|
||
LOKALE LOGIK
|
||
------------------------- */
|
||
async function selectLocalFolder() {
|
||
try {
|
||
const folder = await window.electronAPI.selectFolder();
|
||
if (!folder) return;
|
||
|
||
selectedFolder = folder;
|
||
setStatus('Local: ' + folder);
|
||
currentState.view = 'local';
|
||
updateNavigationUI();
|
||
|
||
await refreshLocalTree(folder);
|
||
await loadBranches(folder);
|
||
} catch (error) {
|
||
console.error('Error selecting folder:', error);
|
||
showError('Error selecting folder');
|
||
}
|
||
}
|
||
|
||
async function refreshLocalTree(folder) {
|
||
try {
|
||
const res = await window.electronAPI.getFileTree({
|
||
folder,
|
||
exclude: ['node_modules', '.git'],
|
||
maxDepth: 5
|
||
});
|
||
|
||
const grid = $('explorerGrid');
|
||
if (!grid) return;
|
||
grid.innerHTML = '';
|
||
|
||
if (!res.ok) {
|
||
showError('Error loading local files');
|
||
return;
|
||
}
|
||
|
||
if (!res.tree || res.tree.length === 0) {
|
||
grid.innerHTML = '<div style="grid-column: 1/-1; text-align: center; padding: 40px; color: var(--text-muted);">Keine Dateien gefunden</div>';
|
||
return;
|
||
}
|
||
|
||
res.tree.forEach(node => {
|
||
const card = document.createElement('div');
|
||
card.className = 'item-card';
|
||
card.dataset.path = node.path;
|
||
// Farbiges Icon + Name
|
||
const nodeIconEl = makeFileIconEl(node.name, node.isDirectory);
|
||
const nodeNameEl = document.createElement('div');
|
||
nodeNameEl.className = 'item-name';
|
||
nodeNameEl.textContent = node.name;
|
||
card.appendChild(nodeIconEl);
|
||
card.appendChild(nodeNameEl);
|
||
// lastSelectedItem tracken
|
||
card.addEventListener('click', () => { lastSelectedItem = { type: 'local', node }; });
|
||
|
||
card.onclick = async (e) => {
|
||
if (e.ctrlKey || e.metaKey) {
|
||
// Mehrfachauswahl
|
||
if (selectedItems.has(node.path)) {
|
||
selectedItems.delete(node.path);
|
||
card.classList.remove('selected');
|
||
} else {
|
||
selectedItems.add(node.path);
|
||
card.classList.add('selected');
|
||
}
|
||
return;
|
||
}
|
||
// Normale Auswahl
|
||
selectedItems.clear();
|
||
grid.querySelectorAll('.item-card.selected').forEach(c => c.classList.remove('selected'));
|
||
if (!node.isDirectory) {
|
||
openFileEditor(node.path, node.name);
|
||
}
|
||
};
|
||
|
||
card.oncontextmenu = (ev) => showLocalItemContextMenu(ev, node);
|
||
|
||
grid.appendChild(card);
|
||
});
|
||
} catch (error) {
|
||
console.error('Error refreshing tree:', error);
|
||
showError('Error loading file tree');
|
||
}
|
||
}
|
||
|
||
/* -------------------------
|
||
GIT ACTIONS
|
||
------------------------- */
|
||
async function pushLocalFolder() {
|
||
if (!selectedFolder) {
|
||
alert('Select local folder first');
|
||
return;
|
||
}
|
||
|
||
// Commit-Nachricht abfragen
|
||
const message = await showCommitMessageModal();
|
||
if (message === null) return; // Abgebrochen
|
||
|
||
const branch = $('branchSelect')?.value || 'main';
|
||
const repoName = $('repoName')?.value;
|
||
const platform = $('platform')?.value;
|
||
|
||
setStatus('Pushing...');
|
||
showProgress(0, 'Starting push...');
|
||
|
||
try {
|
||
const res = await window.electronAPI.pushProject({
|
||
folder: selectedFolder,
|
||
branch,
|
||
repoName,
|
||
platform,
|
||
commitMessage: message
|
||
});
|
||
|
||
if (res.ok) {
|
||
setStatus('Push succeeded');
|
||
} else {
|
||
showError('Push failed: ' + (res.error || 'Unknown error'));
|
||
}
|
||
} catch (error) {
|
||
console.error('Push error:', error);
|
||
showError('Push failed');
|
||
} finally {
|
||
hideProgress();
|
||
}
|
||
}
|
||
|
||
// Modal für Commit-Nachricht
|
||
function showCommitMessageModal() {
|
||
return new Promise((resolve) => {
|
||
const modal = document.createElement('div');
|
||
modal.className = 'modal';
|
||
modal.style.zIndex = '99999';
|
||
modal.innerHTML = `
|
||
<div class="modalContent card" style="max-width: 500px;">
|
||
<h2>💬 Commit-Nachricht</h2>
|
||
<div class="input-group">
|
||
<label>Was wurde geändert?</label>
|
||
<input id="commitMsgInput" type="text" placeholder="z.B. Fix: Button-Farbe angepasst"
|
||
style="font-size: 15px;" autocomplete="off">
|
||
</div>
|
||
<div style="margin-top: 8px; display: flex; flex-wrap: wrap; gap: 8px;" id="commitQuickBtns">
|
||
${['🐛 Fix Bug', '✨ Neues Feature', '📝 Dokumentation', '♻️ Refactoring', '🚀 Release'].map(t =>
|
||
`<button class="commit-quick-btn" style="
|
||
background: var(--bg-tertiary);
|
||
border: 1px solid rgba(255,255,255,0.1);
|
||
color: var(--text-secondary);
|
||
padding: 6px 12px;
|
||
border-radius: 20px;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
">${t}</button>`
|
||
).join('')}
|
||
</div>
|
||
<div class="modal-buttons" style="margin-top: 20px;">
|
||
<button id="btnCommitOk" class="accent-btn">⬆️ Pushen</button>
|
||
<button id="btnCommitCancel" class="secondary">Abbrechen</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
document.body.appendChild(modal);
|
||
|
||
const input = modal.querySelector('#commitMsgInput');
|
||
input.focus();
|
||
|
||
// Quick-Buttons
|
||
modal.querySelectorAll('.commit-quick-btn').forEach(btn => {
|
||
btn.onclick = () => {
|
||
input.value = btn.textContent.trim();
|
||
input.focus();
|
||
};
|
||
});
|
||
|
||
modal.querySelector('#btnCommitOk').onclick = () => {
|
||
const val = input.value.trim() || 'Update via Git Manager GUI';
|
||
modal.remove();
|
||
resolve(val);
|
||
};
|
||
|
||
modal.querySelector('#btnCommitCancel').onclick = () => {
|
||
modal.remove();
|
||
resolve(null);
|
||
};
|
||
|
||
input.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Enter') modal.querySelector('#btnCommitOk').click();
|
||
if (e.key === 'Escape') modal.querySelector('#btnCommitCancel').click();
|
||
});
|
||
});
|
||
}
|
||
|
||
async function loadBranches(folder) {
|
||
try {
|
||
const res = await window.electronAPI.getBranches({ folder });
|
||
const sel = $('branchSelect');
|
||
if (!sel) return;
|
||
|
||
sel.innerHTML = '';
|
||
|
||
if (res.ok && res.branches) {
|
||
res.branches.forEach(b => {
|
||
const option = document.createElement('option');
|
||
option.value = b;
|
||
option.textContent = b;
|
||
sel.appendChild(option);
|
||
});
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading branches:', error);
|
||
}
|
||
}
|
||
|
||
async function loadCommitLogs(folder) {
|
||
try {
|
||
const res = await window.electronAPI.getCommitLogs({ folder });
|
||
const container = $('logs');
|
||
if (!container) return;
|
||
|
||
container.innerHTML = '';
|
||
|
||
if (res.ok && res.logs) {
|
||
res.logs.forEach(l => {
|
||
const d = document.createElement('div');
|
||
d.className = 'log-item';
|
||
d.innerText = l;
|
||
container.appendChild(d);
|
||
});
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading logs:', error);
|
||
}
|
||
}
|
||
|
||
/* -------------------------
|
||
CONTEXT MENÜS
|
||
------------------------- */
|
||
function showRepoContextMenu(ev, owner, repoName, cloneUrl, element) {
|
||
ev.preventDefault();
|
||
ev.stopPropagation();
|
||
|
||
const old = $('ctxMenu');
|
||
if (old) old.remove();
|
||
|
||
const menu = document.createElement('div');
|
||
menu.id = 'ctxMenu';
|
||
menu.className = 'context-menu';
|
||
menu.style.left = ev.clientX + 'px';
|
||
menu.style.top = ev.clientY + 'px';
|
||
|
||
const createMenuItem = (icon, text, onClick, color = null) => {
|
||
const item = document.createElement('div');
|
||
item.className = 'context-item';
|
||
item.innerHTML = `${icon} ${text}`;
|
||
if (color) item.style.color = color;
|
||
item.onclick = onClick;
|
||
return item;
|
||
};
|
||
|
||
// ── Favorit ──
|
||
if (featureFavorites) {
|
||
const isFav = isFavorite(owner, repoName);
|
||
const favItem = createMenuItem(
|
||
isFav ? '⭐' : '☆',
|
||
isFav ? 'Aus Favoriten entfernen' : 'Zu Favoriten hinzufügen',
|
||
async () => {
|
||
menu.remove();
|
||
await toggleFavorite(owner, repoName, cloneUrl);
|
||
loadGiteaRepos();
|
||
},
|
||
isFav ? '#f59e0b' : null
|
||
);
|
||
menu.appendChild(favItem);
|
||
|
||
const sep = document.createElement('div');
|
||
sep.style.cssText = 'height:1px;background:rgba(255,255,255,0.08);margin:4px 0;';
|
||
menu.appendChild(sep);
|
||
}
|
||
|
||
const uploadItem = createMenuItem('🚀', 'Folder hier hochladen', async () => {
|
||
menu.remove();
|
||
try {
|
||
const sel = await window.electronAPI.selectFolder();
|
||
if (sel) {
|
||
showProgress(0, 'Upload...');
|
||
await window.electronAPI.uploadAndPush({
|
||
localFolder: sel,
|
||
owner,
|
||
repo: repoName,
|
||
destPath: '',
|
||
cloneUrl,
|
||
branch: getDefaultBranch(owner, repoName)
|
||
});
|
||
hideProgress();
|
||
setStatus('Upload complete');
|
||
}
|
||
} catch (error) {
|
||
console.error('Upload error:', error);
|
||
hideProgress();
|
||
showError('Upload failed');
|
||
}
|
||
});
|
||
|
||
const deleteItem = createMenuItem('🗑️', 'Repo löschen', async () => {
|
||
menu.remove();
|
||
if (confirm(`Delete ${repoName}?`)) {
|
||
try {
|
||
const res = await window.electronAPI.deleteGiteaRepo({ owner, repo: repoName });
|
||
if (res.ok) {
|
||
element.remove();
|
||
showSuccess('Repository deleted');
|
||
} else {
|
||
showError('Delete failed: ' + res.error);
|
||
}
|
||
} catch (error) {
|
||
console.error('Delete error:', error);
|
||
showError('Delete failed');
|
||
}
|
||
}
|
||
}, '#ef4444');
|
||
|
||
menu.appendChild(uploadItem);
|
||
menu.appendChild(deleteItem);
|
||
document.body.appendChild(menu);
|
||
|
||
setTimeout(() => {
|
||
document.addEventListener('click', () => menu.remove(), { once: true });
|
||
}, 10);
|
||
}
|
||
|
||
function showGiteaItemContextMenu(ev, item, owner, repo) {
|
||
ev.preventDefault();
|
||
ev.stopPropagation();
|
||
|
||
const old = $('ctxMenu');
|
||
if (old) old.remove();
|
||
|
||
const menu = document.createElement('div');
|
||
menu.id = 'ctxMenu';
|
||
menu.className = 'context-menu';
|
||
|
||
// Menü positionieren (nicht außerhalb des Fensters)
|
||
const menuW = 220, menuH = 360;
|
||
const x = Math.min(ev.clientX, window.innerWidth - menuW);
|
||
const y = Math.min(ev.clientY, window.innerHeight - menuH);
|
||
menu.style.left = x + 'px';
|
||
menu.style.top = y + 'px';
|
||
|
||
const addSep = () => {
|
||
const s = document.createElement('div');
|
||
s.style.cssText = 'height:1px;background:rgba(255,255,255,0.1);margin:4px 0;';
|
||
menu.appendChild(s);
|
||
};
|
||
|
||
const addItem = (icon, text, onClick, color = null) => {
|
||
const el = document.createElement('div');
|
||
el.className = 'context-item';
|
||
el.innerHTML = `${icon} ${text}`;
|
||
if (color) el.style.color = color;
|
||
el.onclick = () => { menu.remove(); onClick(); };
|
||
menu.appendChild(el);
|
||
};
|
||
|
||
// Mehrfachauswahl-Info
|
||
if (selectedItems.size > 1 && selectedItems.has(item.path)) {
|
||
const infoEl = document.createElement('div');
|
||
infoEl.style.cssText = 'padding:8px 14px;font-size:11px;color:var(--accent-primary);font-weight:600;';
|
||
infoEl.textContent = `${selectedItems.size} Elemente ausgewählt`;
|
||
menu.appendChild(infoEl);
|
||
addSep();
|
||
|
||
addItem('🗑️', `Alle ${selectedItems.size} löschen`, async () => {
|
||
if (!confirm(`${selectedItems.size} Elemente wirklich löschen?`)) return;
|
||
showProgress(0, 'Lösche...');
|
||
let done = 0;
|
||
for (const p of selectedItems) {
|
||
await window.electronAPI.deleteFile({ path: p, owner, repo, isGitea: true });
|
||
done++;
|
||
showProgress(Math.round((done / selectedItems.size) * 100), `Lösche ${done}/${selectedItems.size}`);
|
||
}
|
||
selectedItems.clear();
|
||
hideProgress();
|
||
setStatus('Bulk-Delete abgeschlossen');
|
||
loadRepoContents(owner, repo, currentState.path);
|
||
}, '#ef4444');
|
||
|
||
document.body.appendChild(menu);
|
||
setTimeout(() => document.addEventListener('click', () => menu.remove(), { once: true }), 10);
|
||
return;
|
||
}
|
||
|
||
// --- ÖFFNEN ---
|
||
addItem(
|
||
item.type === 'dir' ? '📂' : '✏️',
|
||
item.type === 'dir' ? 'Öffnen' : 'Im Editor öffnen',
|
||
() => {
|
||
if (item.type === 'dir') loadRepoContents(owner, repo, item.path);
|
||
else openGiteaFileInEditor(owner, repo, item.path, item.name);
|
||
}
|
||
);
|
||
|
||
addSep();
|
||
|
||
// --- NEU ERSTELLEN (immer sichtbar) ---
|
||
addItem('📄', 'Neue Datei erstellen', () => showNewGiteaItemModal(owner, repo, item.type === 'dir' ? item.path : currentState.path, 'file'));
|
||
addItem('📁', 'Neuen Ordner erstellen', () => showNewGiteaItemModal(owner, repo, item.type === 'dir' ? item.path : currentState.path, 'folder'));
|
||
|
||
addSep();
|
||
|
||
// --- UMBENENNEN ---
|
||
addItem('✏️', 'Umbenennen', () => showGiteaRenameModal(item, owner, repo));
|
||
|
||
// --- CUT & PASTE ---
|
||
addItem('✂️', 'Ausschneiden (Cut)', () => {
|
||
clipboard = { item: { ...item, owner, repo, isGitea: true }, action: 'cut' };
|
||
setStatus(`✂️ "${item.name}" ausgeschnitten — Zielordner öffnen und Einfügen wählen`);
|
||
});
|
||
|
||
if (clipboard.item && clipboard.item.isGitea && item.type === 'dir') {
|
||
addItem('📋', `Einfügen: "${clipboard.item.name}"`, async () => {
|
||
await pasteGiteaItem(owner, repo, item.path);
|
||
});
|
||
}
|
||
|
||
addSep();
|
||
|
||
// --- DOWNLOAD ---
|
||
if (item.type === 'file') {
|
||
addItem('📥', 'Herunterladen', async () => {
|
||
const res = await window.electronAPI.downloadGiteaFile({ owner, repo, path: item.path });
|
||
setStatus(res.ok ? `Gespeichert: ${res.savedTo}` : 'Download fehlgeschlagen');
|
||
});
|
||
} else {
|
||
addItem('📥', 'Ordner herunterladen', async () => {
|
||
showProgress(0, `Lade ${item.name}...`);
|
||
const res = await window.electronAPI.downloadGiteaFolder({ owner, repo, path: item.path });
|
||
hideProgress();
|
||
setStatus(res.ok ? `Gespeichert: ${res.savedTo}` : 'Download fehlgeschlagen');
|
||
});
|
||
}
|
||
|
||
addSep();
|
||
|
||
// --- LÖSCHEN ---
|
||
addItem('🗑️', 'Löschen', async () => {
|
||
if (!confirm(`"${item.name}" wirklich löschen?`)) return;
|
||
showProgress(0, `Lösche ${item.name}...`);
|
||
const res = await window.electronAPI.deleteFile({ path: item.path, owner, repo, isGitea: true });
|
||
hideProgress();
|
||
if (res && res.ok) {
|
||
setStatus(`${item.name} gelöscht`);
|
||
loadRepoContents(owner, repo, currentState.path);
|
||
} else {
|
||
showError('Löschen fehlgeschlagen: ' + (res?.error || ''));
|
||
alert('Löschen fehlgeschlagen:\n' + (res?.error || 'Unbekannter Fehler'));
|
||
}
|
||
}, '#ef4444');
|
||
|
||
document.body.appendChild(menu);
|
||
setTimeout(() => document.addEventListener('click', () => menu.remove(), { once: true }), 10);
|
||
}
|
||
|
||
/* -------------------------
|
||
LOKALES KONTEXT-MENÜ
|
||
------------------------- */
|
||
function showLocalItemContextMenu(ev, node) {
|
||
ev.preventDefault();
|
||
ev.stopPropagation();
|
||
|
||
const old = $('ctxMenu');
|
||
if (old) old.remove();
|
||
|
||
const menu = document.createElement('div');
|
||
menu.id = 'ctxMenu';
|
||
menu.className = 'context-menu';
|
||
|
||
const menuW = 220, menuH = 360;
|
||
const x = Math.min(ev.clientX, window.innerWidth - menuW);
|
||
const y = Math.min(ev.clientY, window.innerHeight - menuH);
|
||
menu.style.left = x + 'px';
|
||
menu.style.top = y + 'px';
|
||
|
||
const addSep = () => {
|
||
const s = document.createElement('div');
|
||
s.style.cssText = 'height:1px;background:rgba(255,255,255,0.1);margin:4px 0;';
|
||
menu.appendChild(s);
|
||
};
|
||
|
||
const addItem = (icon, text, onClick, color = null) => {
|
||
const el = document.createElement('div');
|
||
el.className = 'context-item';
|
||
el.innerHTML = `${icon} ${text}`;
|
||
if (color) el.style.color = color;
|
||
el.onclick = () => { menu.remove(); onClick(); };
|
||
menu.appendChild(el);
|
||
};
|
||
|
||
// Mehrfachauswahl-Bulk-Delete
|
||
if (selectedItems.size > 1 && selectedItems.has(node.path)) {
|
||
const infoEl = document.createElement('div');
|
||
infoEl.style.cssText = 'padding:8px 14px;font-size:11px;color:var(--accent-primary);font-weight:600;';
|
||
infoEl.textContent = `${selectedItems.size} Elemente ausgewählt`;
|
||
menu.appendChild(infoEl);
|
||
addSep();
|
||
|
||
addItem('🗑️', `Alle ${selectedItems.size} löschen`, async () => {
|
||
if (!confirm(`${selectedItems.size} Elemente wirklich löschen?`)) return;
|
||
for (const p of selectedItems) {
|
||
await window.electronAPI.deleteFile({ path: p });
|
||
}
|
||
selectedItems.clear();
|
||
setStatus('Bulk-Delete abgeschlossen');
|
||
if (selectedFolder) refreshLocalTree(selectedFolder);
|
||
}, '#ef4444');
|
||
|
||
document.body.appendChild(menu);
|
||
setTimeout(() => document.addEventListener('click', () => menu.remove(), { once: true }), 10);
|
||
return;
|
||
}
|
||
|
||
// --- ÖFFNEN ---
|
||
if (!node.isDirectory) {
|
||
addItem('✏️', 'Im Editor öffnen', () => openFileEditor(node.path, node.name));
|
||
addSep();
|
||
}
|
||
|
||
// --- NEU ERSTELLEN ---
|
||
const targetDir = node.isDirectory ? node.path : require('path').dirname(node.path);
|
||
addItem('📄', 'Neue Datei erstellen', () => showNewLocalItemModal(targetDir, 'file'));
|
||
addItem('📁', 'Neuen Ordner erstellen', () => showNewLocalItemModal(targetDir, 'folder'));
|
||
|
||
addSep();
|
||
|
||
// --- UMBENENNEN ---
|
||
addItem('✏️', 'Umbenennen', () => showLocalRenameModal(node));
|
||
|
||
// --- CUT & PASTE ---
|
||
addItem('✂️', 'Ausschneiden (Cut)', () => {
|
||
clipboard = { item: { ...node, isLocal: true }, action: 'cut' };
|
||
setStatus(`✂️ "${node.name}" ausgeschnitten`);
|
||
});
|
||
|
||
if (clipboard.item && clipboard.item.isLocal && node.isDirectory) {
|
||
addItem('📋', `Einfügen: "${clipboard.item.name}"`, async () => {
|
||
await pasteLocalItem(node.path);
|
||
});
|
||
}
|
||
|
||
addSep();
|
||
|
||
// --- DOWNLOAD / KOPIEREN ---
|
||
addItem('📥', 'Kopieren nach...', async () => {
|
||
const destFolder = await window.electronAPI.selectFolder();
|
||
if (!destFolder) return;
|
||
const res = await window.electronAPI.copyLocalItem({ src: node.path, destDir: destFolder });
|
||
setStatus(res?.ok ? `Kopiert nach: ${destFolder}` : 'Kopieren fehlgeschlagen: ' + (res?.error || ''));
|
||
});
|
||
|
||
addSep();
|
||
|
||
// --- LÖSCHEN ---
|
||
addItem('🗑️', 'Löschen', async () => {
|
||
if (!confirm(`"${node.name}" wirklich löschen?`)) return;
|
||
const res = await window.electronAPI.deleteFile({ path: node.path });
|
||
if (res && res.ok) {
|
||
setStatus(`${node.name} gelöscht`);
|
||
if (selectedFolder) refreshLocalTree(selectedFolder);
|
||
} else {
|
||
showError('Löschen fehlgeschlagen: ' + (res?.error || ''));
|
||
}
|
||
}, '#ef4444');
|
||
|
||
document.body.appendChild(menu);
|
||
setTimeout(() => document.addEventListener('click', () => menu.remove(), { once: true }), 10);
|
||
}
|
||
|
||
/* =========================================
|
||
MODAL HELPER FUNKTIONEN
|
||
========================================= */
|
||
|
||
// Gitea: Umbenennen
|
||
function showGiteaRenameModal(item, owner, repo) {
|
||
showInputModal({
|
||
title: `✏️ Umbenennen`,
|
||
label: 'Neuer Name',
|
||
defaultValue: item.name,
|
||
confirmText: 'Umbenennen',
|
||
onConfirm: async (newName) => {
|
||
if (!newName || newName === item.name) return;
|
||
setStatus('Umbenennen...');
|
||
const parentPath = item.path.split('/').slice(0, -1).join('/');
|
||
const newPath = parentPath ? `${parentPath}/${newName}` : newName;
|
||
const res = await window.electronAPI.renameGiteaItem({
|
||
owner, repo,
|
||
oldPath: item.path,
|
||
newPath,
|
||
isDir: item.type === 'dir'
|
||
});
|
||
if (res?.ok) {
|
||
setStatus(`Umbenannt in "${newName}"`);
|
||
loadRepoContents(owner, repo, currentState.path);
|
||
} else {
|
||
alert('Umbenennen fehlgeschlagen:\n' + (res?.error || 'Unbekannter Fehler'));
|
||
showError('Fehler beim Umbenennen');
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// Gitea: Neue Datei / Ordner
|
||
function showNewGiteaItemModal(owner, repo, parentPath, type) {
|
||
showInputModal({
|
||
title: type === 'file' ? '📄 Neue Datei' : '📁 Neuer Ordner',
|
||
label: type === 'file' ? 'Dateiname (z.B. README.md)' : 'Ordnername',
|
||
defaultValue: '',
|
||
confirmText: 'Erstellen',
|
||
onConfirm: async (name) => {
|
||
if (!name) return;
|
||
const targetPath = parentPath ? `${parentPath}/${name}` : name;
|
||
const res = await window.electronAPI.createGiteaItem({
|
||
owner, repo,
|
||
path: targetPath,
|
||
type
|
||
});
|
||
if (res?.ok) {
|
||
setStatus(`"${name}" erstellt`);
|
||
loadRepoContents(owner, repo, currentState.path);
|
||
} else {
|
||
alert('Erstellen fehlgeschlagen:\n' + (res?.error || ''));
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// Lokal: Umbenennen
|
||
function showLocalRenameModal(node) {
|
||
showInputModal({
|
||
title: `✏️ Umbenennen`,
|
||
label: 'Neuer Name',
|
||
defaultValue: node.name,
|
||
confirmText: 'Umbenennen',
|
||
onConfirm: async (newName) => {
|
||
if (!newName || newName === node.name) return;
|
||
const res = await window.electronAPI.renameLocalItem({ oldPath: node.path, newName });
|
||
if (res?.ok) {
|
||
setStatus(`Umbenannt in "${newName}"`);
|
||
if (selectedFolder) refreshLocalTree(selectedFolder);
|
||
} else {
|
||
alert('Umbenennen fehlgeschlagen:\n' + (res?.error || ''));
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// Lokal: Neue Datei / Ordner
|
||
function showNewLocalItemModal(parentDir, type) {
|
||
showInputModal({
|
||
title: type === 'file' ? '📄 Neue Datei' : '📁 Neuer Ordner',
|
||
label: type === 'file' ? 'Dateiname (z.B. README.md)' : 'Ordnername',
|
||
defaultValue: '',
|
||
confirmText: 'Erstellen',
|
||
onConfirm: async (name) => {
|
||
if (!name) return;
|
||
const res = await window.electronAPI.createLocalItem({ parentDir, name, type });
|
||
if (res?.ok) {
|
||
setStatus(`"${name}" erstellt`);
|
||
if (selectedFolder) refreshLocalTree(selectedFolder);
|
||
} else {
|
||
alert('Erstellen fehlgeschlagen:\n' + (res?.error || ''));
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// Gitea: Einfügen nach Cut
|
||
async function pasteGiteaItem(owner, repo, destFolderPath) {
|
||
if (!clipboard.item || !clipboard.item.isGitea) return;
|
||
const src = clipboard.item;
|
||
const newPath = destFolderPath ? `${destFolderPath}/${src.name}` : src.name;
|
||
setStatus(`Verschiebe "${src.name}"...`);
|
||
showProgress(0, `Verschiebe...`);
|
||
const res = await window.electronAPI.renameGiteaItem({
|
||
owner, repo,
|
||
oldPath: src.path,
|
||
newPath,
|
||
isDir: src.type === 'dir'
|
||
});
|
||
hideProgress();
|
||
if (res?.ok) {
|
||
clipboard = { item: null, action: null };
|
||
setStatus(`"${src.name}" verschoben`);
|
||
loadRepoContents(owner, repo, currentState.path);
|
||
} else {
|
||
alert('Verschieben fehlgeschlagen:\n' + (res?.error || ''));
|
||
showError('Fehler beim Verschieben');
|
||
}
|
||
}
|
||
|
||
// Lokal: Einfügen nach Cut
|
||
async function pasteLocalItem(destDir) {
|
||
if (!clipboard.item || !clipboard.item.isLocal) return;
|
||
const src = clipboard.item;
|
||
setStatus(`Verschiebe "${src.name}"...`);
|
||
const res = await window.electronAPI.moveLocalItem({ srcPath: src.path, destDir });
|
||
if (res?.ok) {
|
||
clipboard = { item: null, action: null };
|
||
setStatus(`"${src.name}" verschoben`);
|
||
if (selectedFolder) refreshLocalTree(selectedFolder);
|
||
} else {
|
||
alert('Verschieben fehlgeschlagen:\n' + (res?.error || ''));
|
||
}
|
||
}
|
||
|
||
// Generic Input Modal
|
||
function showInputModal({ title, label, defaultValue, confirmText, onConfirm }) {
|
||
const modal = document.createElement('div');
|
||
modal.className = 'modal';
|
||
modal.style.zIndex = '99999';
|
||
modal.innerHTML = `
|
||
<div class="modalContent card" style="max-width: 420px;">
|
||
<h2>${title}</h2>
|
||
<div class="input-group">
|
||
<label>${label}</label>
|
||
<input id="inputModalField" type="text" value="${escapeHtml(defaultValue)}" autocomplete="off">
|
||
</div>
|
||
<div class="modal-buttons" style="margin-top: 16px;">
|
||
<button id="inputModalOk" class="accent-btn">${confirmText}</button>
|
||
<button id="inputModalCancel" class="secondary">Abbrechen</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
document.body.appendChild(modal);
|
||
const input = modal.querySelector('#inputModalField');
|
||
input.focus();
|
||
input.select();
|
||
|
||
modal.querySelector('#inputModalOk').onclick = () => {
|
||
const val = input.value.trim();
|
||
modal.remove();
|
||
onConfirm(val);
|
||
};
|
||
modal.querySelector('#inputModalCancel').onclick = () => modal.remove();
|
||
input.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Enter') modal.querySelector('#inputModalOk').click();
|
||
if (e.key === 'Escape') modal.querySelector('#inputModalCancel').click();
|
||
});
|
||
modal.onclick = (e) => { if (e.target === modal) modal.remove(); };
|
||
}
|
||
|
||
/* -------------------------
|
||
HELPER FUNCTIONS
|
||
------------------------- */
|
||
function ppathBasename(p) {
|
||
try {
|
||
return p.split(/[\\/]/).pop();
|
||
} catch (_) {
|
||
return p;
|
||
}
|
||
}
|
||
|
||
async function previewGiteaFile(owner, repo, filePath) {
|
||
try {
|
||
const res = await window.electronAPI.getGiteaFileContent({
|
||
owner,
|
||
repo,
|
||
path: filePath,
|
||
ref: getDefaultBranch(owner, repo)
|
||
});
|
||
|
||
if (res.ok) {
|
||
console.log("Content of", filePath, ":", res.content);
|
||
setStatus(`Previewed: ${filePath}`);
|
||
} else {
|
||
showError('Preview failed');
|
||
}
|
||
} catch (error) {
|
||
console.error('Preview error:', error);
|
||
showError('Preview failed');
|
||
}
|
||
}
|
||
|
||
async function createRepoHandler() {
|
||
const name = $('repoName')?.value?.trim();
|
||
if (!name) {
|
||
alert('Name required');
|
||
return;
|
||
}
|
||
|
||
setStatus('Creating repository...');
|
||
|
||
try {
|
||
const res = await window.electronAPI.createRepo({
|
||
name,
|
||
platform: $('platform').value,
|
||
license: $('licenseSelect')?.value || '',
|
||
autoInit: $('createReadme')?.checked || true
|
||
});
|
||
|
||
if (res.ok) {
|
||
$('repoActionModal')?.classList.add('hidden');
|
||
showSuccess('Repository created');
|
||
loadGiteaRepos();
|
||
} else {
|
||
showError('Create failed: ' + (res.error || 'Unknown error'));
|
||
}
|
||
} catch (error) {
|
||
console.error('Create repo error:', error);
|
||
showError('Create failed');
|
||
}
|
||
}
|
||
|
||
/* -------------------------
|
||
GLOBALER DROP-HANDLER FÜR REPO-ANSICHT
|
||
------------------------- */
|
||
/* -------------------------
|
||
HINTERGRUND KONTEXT-MENÜ (Rechtsklick auf leere Fläche)
|
||
------------------------- */
|
||
function setupBackgroundContextMenu() {
|
||
// Listener auf #main statt explorerGrid, da Grid kleiner als sichtbarer Bereich sein kann
|
||
const mainEl = $('main');
|
||
if (!mainEl) return;
|
||
|
||
mainEl.addEventListener('contextmenu', (ev) => {
|
||
// Nicht auslösen wenn auf eine Karte oder interaktives Element geklickt wird
|
||
if (ev.target.closest('.item-card, .release-card, .commit-card, .fav-chip, .fav-star-btn, button, input, textarea, select, a')) return;
|
||
|
||
// Nur in Repo- oder Lokal-Ansicht
|
||
if (currentState.view !== 'gitea-repo' && currentState.view !== 'local') return;
|
||
|
||
ev.preventDefault();
|
||
ev.stopPropagation();
|
||
|
||
const old = $('ctxMenu');
|
||
if (old) old.remove();
|
||
|
||
const menu = document.createElement('div');
|
||
menu.id = 'ctxMenu';
|
||
menu.className = 'context-menu';
|
||
|
||
const menuW = 220, menuH = 160;
|
||
const x = Math.min(ev.clientX, window.innerWidth - menuW);
|
||
const y = Math.min(ev.clientY, window.innerHeight - menuH);
|
||
menu.style.left = x + 'px';
|
||
menu.style.top = y + 'px';
|
||
|
||
// Aktuelle Pfad-Info als Header
|
||
const header = document.createElement('div');
|
||
header.style.cssText = 'padding:8px 14px 4px;font-size:11px;color:var(--text-muted);letter-spacing:0.5px;';
|
||
const currentPath = currentState.view === 'gitea-repo'
|
||
? (currentState.path || 'Root')
|
||
: (selectedFolder ? selectedFolder.split(/[\\/]/).pop() : 'Lokal');
|
||
header.textContent = `📂 ${currentPath}`;
|
||
menu.appendChild(header);
|
||
|
||
const sep = document.createElement('div');
|
||
sep.style.cssText = 'height:1px;background:rgba(255,255,255,0.1);margin:4px 0;';
|
||
menu.appendChild(sep);
|
||
|
||
const addItem = (icon, text, onClick) => {
|
||
const el = document.createElement('div');
|
||
el.className = 'context-item';
|
||
el.innerHTML = `${icon} ${text}`;
|
||
el.onclick = () => { menu.remove(); onClick(); };
|
||
menu.appendChild(el);
|
||
};
|
||
|
||
if (currentState.view === 'gitea-repo') {
|
||
const { owner, repo, path: currentPath } = currentState;
|
||
|
||
addItem('📄', 'Neue Datei erstellen', () =>
|
||
showNewGiteaItemModal(owner, repo, currentPath, 'file')
|
||
);
|
||
addItem('📁', 'Neuen Ordner erstellen', () =>
|
||
showNewGiteaItemModal(owner, repo, currentPath, 'folder')
|
||
);
|
||
|
||
// Einfügen: gleiche Quelle ODER Cross-Paste von Lokal
|
||
if (clipboard.item) {
|
||
const sep2 = document.createElement('div');
|
||
sep2.style.cssText = 'height:1px;background:rgba(255,255,255,0.1);margin:4px 0;';
|
||
menu.appendChild(sep2);
|
||
if (clipboard.item.isGitea) {
|
||
addItem('📋', `Einfügen: "${clipboard.item.name}"`, () =>
|
||
pasteGiteaItem(owner, repo, currentPath)
|
||
);
|
||
} else if (clipboard.item.isLocal) {
|
||
addItem('📋', `⬆️ Von Lokal einfügen: "${clipboard.item.name}"`, async () => {
|
||
showProgress(0, `Lade "${clipboard.item.name}" hoch...`);
|
||
try {
|
||
await window.electronAPI.uploadAndPush({
|
||
localFolder: clipboard.item.path,
|
||
owner, repo,
|
||
destPath: currentPath,
|
||
branch: getDefaultBranch(owner, repo)
|
||
});
|
||
showSuccess(`"${clipboard.item.name}" nach Gitea kopiert`);
|
||
loadRepoContents(owner, repo, currentState.path);
|
||
} catch(e) { showError('Cross-Paste fehlgeschlagen'); }
|
||
finally { hideProgress(); }
|
||
});
|
||
}
|
||
}
|
||
|
||
} else if (currentState.view === 'local' && selectedFolder) {
|
||
addItem('📄', 'Neue Datei erstellen', () =>
|
||
showNewLocalItemModal(selectedFolder, 'file')
|
||
);
|
||
addItem('📁', 'Neuen Ordner erstellen', () =>
|
||
showNewLocalItemModal(selectedFolder, 'folder')
|
||
);
|
||
|
||
if (clipboard.item) {
|
||
const sep2 = document.createElement('div');
|
||
sep2.style.cssText = 'height:1px;background:rgba(255,255,255,0.1);margin:4px 0;';
|
||
menu.appendChild(sep2);
|
||
if (clipboard.item.isLocal) {
|
||
addItem('📋', `Einfügen: "${clipboard.item.name}"`, () =>
|
||
pasteLocalItem(selectedFolder)
|
||
);
|
||
} else if (clipboard.item.isGitea) {
|
||
addItem('📋', `⬇️ Von Gitea einfügen: "${clipboard.item.name}"`, async () => {
|
||
showProgress(0, `Lade "${clipboard.item.name}" herunter...`);
|
||
try {
|
||
await window.electronAPI.downloadGiteaFolder({
|
||
owner: clipboard.item.owner,
|
||
repo: clipboard.item.repo,
|
||
giteaPath: clipboard.item.path,
|
||
localPath: selectedFolder
|
||
});
|
||
showSuccess(`"${clipboard.item.name}" nach Lokal kopiert`);
|
||
refreshLocalTree(selectedFolder);
|
||
} catch(e) { showError('Cross-Paste fehlgeschlagen'); }
|
||
finally { hideProgress(); }
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
document.body.appendChild(menu);
|
||
setTimeout(() => document.addEventListener('click', () => menu.remove(), { once: true }), 10);
|
||
});
|
||
}
|
||
|
||
function setupGlobalDropZone() {
|
||
const main = $('main');
|
||
if (!main) return;
|
||
|
||
// Visual feedback beim Drag über das Fenster
|
||
let dragCounter = 0;
|
||
|
||
main.addEventListener('dragenter', (ev) => {
|
||
// Nur in Repo-Ansicht aktiv
|
||
if (currentState.view !== 'gitea-repo') return;
|
||
|
||
dragCounter++;
|
||
if (dragCounter === 1) {
|
||
main.classList.add('drop-active');
|
||
}
|
||
});
|
||
|
||
main.addEventListener('dragleave', (ev) => {
|
||
if (currentState.view !== 'gitea-repo') return;
|
||
|
||
dragCounter--;
|
||
if (dragCounter === 0) {
|
||
main.classList.remove('drop-active');
|
||
}
|
||
});
|
||
|
||
main.addEventListener('dragover', (ev) => {
|
||
if (currentState.view !== 'gitea-repo') return;
|
||
ev.preventDefault();
|
||
});
|
||
|
||
main.addEventListener('drop', async (ev) => {
|
||
if (currentState.view !== 'gitea-repo') return;
|
||
|
||
ev.preventDefault();
|
||
ev.stopPropagation();
|
||
|
||
dragCounter = 0;
|
||
main.classList.remove('drop-active');
|
||
|
||
const files = ev.dataTransfer.files;
|
||
if (!files || files.length === 0) {
|
||
showWarning("Keine Dateien zum Upload gefunden.");
|
||
return;
|
||
}
|
||
|
||
// Upload in aktuellen Pfad
|
||
const owner = currentState.owner;
|
||
const repo = currentState.repo;
|
||
const targetPath = currentState.path || '';
|
||
|
||
const paths = Array.from(files).map(f => f.path);
|
||
setStatus(`Uploading ${paths.length} items to /${targetPath || 'root'}...`);
|
||
|
||
for (const p of paths) {
|
||
const baseName = p.split(/[\\/]/).pop();
|
||
showProgress(0, `Uploading: ${baseName}`);
|
||
|
||
try {
|
||
const res = await window.electronAPI.uploadAndPush({
|
||
localFolder: p,
|
||
owner,
|
||
repo,
|
||
destPath: targetPath,
|
||
branch: getDefaultBranch(owner, repo)
|
||
});
|
||
|
||
if (!res.ok) {
|
||
console.error("Upload error:", res.error);
|
||
showError("Error: " + res.error);
|
||
} else {
|
||
setStatus(`Uploaded: ${baseName}`);
|
||
}
|
||
} catch (err) {
|
||
console.error("Critical upload error:", err);
|
||
showError("Upload failed");
|
||
}
|
||
}
|
||
|
||
hideProgress();
|
||
|
||
// Refresh current view
|
||
setTimeout(() => {
|
||
loadRepoContents(owner, repo, targetPath);
|
||
}, 1000);
|
||
});
|
||
}
|
||
|
||
/* -------------------------
|
||
INITIALISIERUNG
|
||
------------------------- */
|
||
window.addEventListener('DOMContentLoaded', async () => {
|
||
// Favoriten & Verlauf vorladen
|
||
await loadFavoritesAndRecent();
|
||
// Prevent default drag/drop on document (except in repo view)
|
||
document.addEventListener('dragover', e => {
|
||
if (currentState.view !== 'gitea-repo') {
|
||
e.preventDefault();
|
||
}
|
||
});
|
||
|
||
document.addEventListener('drop', e => {
|
||
if (currentState.view !== 'gitea-repo') {
|
||
e.preventDefault();
|
||
}
|
||
});
|
||
|
||
// Load credentials and auto-login if available
|
||
try {
|
||
const creds = await window.electronAPI.loadCredentials();
|
||
if (creds) {
|
||
// Fülle Settings-Felder
|
||
if ($('githubToken')) $('githubToken').value = creds.githubToken || '';
|
||
if ($('giteaToken')) $('giteaToken').value = creds.giteaToken || '';
|
||
if ($('giteaURL')) $('giteaURL').value = creds.giteaURL || '';
|
||
|
||
// Feature-Flags aus gespeicherten Einstellungen
|
||
if (typeof creds.featureFavorites === 'boolean') featureFavorites = creds.featureFavorites;
|
||
if (typeof creds.featureRecent === 'boolean') featureRecent = creds.featureRecent;
|
||
if (typeof creds.compactMode === 'boolean') compactMode = creds.compactMode;
|
||
if (typeof creds.featureColoredIcons === 'boolean') featureColoredIcons = creds.featureColoredIcons;
|
||
document.body.classList.toggle('compact-mode', compactMode);
|
||
|
||
// Collapse-Zustand wiederherstellen
|
||
if (typeof creds.favCollapsedFavorites === 'boolean') favSectionCollapsed.favorites = creds.favCollapsedFavorites;
|
||
if (typeof creds.favCollapsedRecent === 'boolean') favSectionCollapsed.recent = creds.favCollapsedRecent;
|
||
|
||
// Settings-Checkboxen befüllen
|
||
const cbFav = $('settingFavorites');
|
||
const cbRec = $('settingRecent');
|
||
const cbCompact = $('settingCompact');
|
||
if (cbFav) cbFav.checked = featureFavorites;
|
||
if (cbRec) cbRec.checked = featureRecent;
|
||
if (cbCompact) cbCompact.checked = compactMode;
|
||
const cbColorIcons = $('settingColoredIcons');
|
||
if (cbColorIcons) cbColorIcons.checked = featureColoredIcons;
|
||
|
||
// 🆕 AUTO-LOGIN: Wenn Gitea-Credentials vorhanden sind, lade sofort die Repos
|
||
if (creds.giteaToken && creds.giteaURL) {
|
||
console.log('✅ Credentials gefunden - Auto-Login wird gestartet...');
|
||
setStatus('Lade deine Projekte...');
|
||
|
||
// Kurze Verzögerung damit UI fertig geladen ist
|
||
setTimeout(() => {
|
||
loadGiteaRepos();
|
||
}, 500);
|
||
} else {
|
||
console.log('ℹ️ Keine vollständigen Gitea-Credentials - bitte in Settings eintragen');
|
||
setStatus('Bereit - bitte Settings konfigurieren');
|
||
}
|
||
} else {
|
||
console.log('ℹ️ Keine Credentials gespeichert');
|
||
setStatus('Bereit - bitte Settings konfigurieren');
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading credentials:', error);
|
||
showError('Fehler beim Laden der Einstellungen');
|
||
}
|
||
|
||
// Rest of Event Handlers... (bleibt unverändert)
|
||
|
||
// Event Handlers
|
||
if ($('btnLoadGiteaRepos')) {
|
||
$('btnLoadGiteaRepos').onclick = loadGiteaRepos;
|
||
}
|
||
|
||
if ($('btnSelectFolder')) {
|
||
$('btnSelectFolder').onclick = selectLocalFolder;
|
||
}
|
||
|
||
if ($('btnPush')) {
|
||
$('btnPush').onclick = pushLocalFolder;
|
||
}
|
||
|
||
if ($('btnCreateRepo')) {
|
||
$('btnCreateRepo').onclick = createRepoHandler;
|
||
}
|
||
|
||
if ($('btnBack')) {
|
||
$('btnBack').onclick = () => {
|
||
if (currentState.view === 'gitea-repo') {
|
||
if (currentState.path === '' || currentState.path === '/') {
|
||
loadGiteaRepos();
|
||
} else {
|
||
const parts = currentState.path.split('/').filter(p => p);
|
||
parts.pop();
|
||
loadRepoContents(currentState.owner, currentState.repo, parts.join('/'));
|
||
}
|
||
}
|
||
};
|
||
}
|
||
|
||
// Modal controls
|
||
if ($('btnSettings')) {
|
||
$('btnSettings').onclick = () => {
|
||
$('settingsModal').classList.remove('hidden');
|
||
};
|
||
}
|
||
|
||
if ($('btnCloseSettings')) {
|
||
$('btnCloseSettings').onclick = () => {
|
||
$('settingsModal').classList.add('hidden');
|
||
};
|
||
}
|
||
|
||
if ($('btnOpenRepoActions')) {
|
||
$('btnOpenRepoActions').onclick = () => {
|
||
$('repoActionModal').classList.remove('hidden');
|
||
};
|
||
}
|
||
|
||
if ($('btnCloseRepoActions')) {
|
||
$('btnCloseRepoActions').onclick = () => {
|
||
$('repoActionModal').classList.add('hidden');
|
||
};
|
||
}
|
||
|
||
if ($('btnSaveSettings')) {
|
||
$('btnSaveSettings').onclick = async () => {
|
||
try {
|
||
// Feature-Flags aus Checkboxen lesen
|
||
const cbFav = $('settingFavorites');
|
||
const cbRec = $('settingRecent');
|
||
const cbCompact = $('settingCompact');
|
||
featureFavorites = cbFav ? cbFav.checked : true;
|
||
featureRecent = cbRec ? cbRec.checked : true;
|
||
compactMode = cbCompact ? cbCompact.checked : false;
|
||
const cbColorIcons2 = $('settingColoredIcons');
|
||
featureColoredIcons = cbColorIcons2 ? cbColorIcons2.checked : true;
|
||
document.body.classList.toggle('compact-mode', compactMode);
|
||
|
||
const data = {
|
||
githubToken: $('githubToken').value,
|
||
giteaToken: $('giteaToken').value,
|
||
giteaURL: $('giteaURL').value,
|
||
featureFavorites,
|
||
featureRecent,
|
||
compactMode,
|
||
featureColoredIcons,
|
||
favCollapsedFavorites: favSectionCollapsed.favorites,
|
||
favCollapsedRecent: favSectionCollapsed.recent
|
||
};
|
||
await window.electronAPI.saveCredentials(data);
|
||
$('settingsModal').classList.add('hidden');
|
||
showSuccess('Settings saved');
|
||
// Ansicht aktualisieren falls Feature-Flags geändert
|
||
loadGiteaRepos();
|
||
} catch (error) {
|
||
console.error('Error saving settings:', error);
|
||
showError('Save failed');
|
||
}
|
||
};
|
||
}
|
||
|
||
// FILE EDITOR EVENT LISTENERS
|
||
if ($('btnCloseEditor')) {
|
||
$('btnCloseEditor').onclick = closeFileEditor;
|
||
}
|
||
|
||
if ($('btnEditorSave')) {
|
||
$('btnEditorSave').onclick = () => saveCurrentFile(false);
|
||
}
|
||
|
||
if ($('btnEditorSearch')) {
|
||
$('btnEditorSearch').onclick = toggleSearch;
|
||
}
|
||
|
||
if ($('btnReplace')) {
|
||
$('btnReplace').onclick = replaceOnce;
|
||
}
|
||
|
||
if ($('btnReplaceAll')) {
|
||
$('btnReplaceAll').onclick = replaceAll;
|
||
}
|
||
|
||
if ($('btnCloseSearch')) {
|
||
$('btnCloseSearch').onclick = () => {
|
||
$('searchBar').classList.add('hidden');
|
||
};
|
||
}
|
||
|
||
if ($('searchInput')) {
|
||
$('searchInput').addEventListener('input', performSearch);
|
||
$('searchInput').addEventListener('keydown', (e) => {
|
||
if (e.key === 'Enter') {
|
||
performSearch();
|
||
}
|
||
});
|
||
}
|
||
|
||
if ($('btnDiscardEdit')) {
|
||
$('btnDiscardEdit').onclick = () => {
|
||
const tab = openTabs[currentActiveTab];
|
||
if (tab) {
|
||
tab.content = tab.originalContent;
|
||
tab.dirty = false;
|
||
tab.history = [tab.originalContent];
|
||
tab.historyIndex = 0;
|
||
updateEditor();
|
||
}
|
||
};
|
||
}
|
||
|
||
|
||
// Keyboard shortcuts
|
||
document.addEventListener('keydown', (e) => {
|
||
if (e.ctrlKey || e.metaKey) {
|
||
// Ctrl+S - Save
|
||
if (e.key === 's') {
|
||
e.preventDefault();
|
||
if (currentActiveTab) {
|
||
saveCurrentFile(false);
|
||
}
|
||
}
|
||
// Ctrl+F - Search
|
||
if (e.key === 'f') {
|
||
e.preventDefault();
|
||
if (currentActiveTab) {
|
||
toggleSearch();
|
||
}
|
||
}
|
||
// Ctrl+H - Replace
|
||
if (e.key === 'h') {
|
||
e.preventDefault();
|
||
if (currentActiveTab) {
|
||
toggleSearch();
|
||
$('replaceInput').focus();
|
||
}
|
||
}
|
||
}
|
||
// ESC - Close search
|
||
if (e.key === 'Escape') {
|
||
if (!$('searchBar').classList.contains('hidden')) {
|
||
$('searchBar').classList.add('hidden');
|
||
}
|
||
}
|
||
|
||
// F2 - Umbenennen
|
||
if (e.key === 'F2' && lastSelectedItem && !currentActiveTab) {
|
||
e.preventDefault();
|
||
if (lastSelectedItem.type === 'gitea') {
|
||
showGiteaRenameModal(lastSelectedItem.item, lastSelectedItem.owner, lastSelectedItem.repo);
|
||
} else if (lastSelectedItem.type === 'local') {
|
||
showLocalRenameModal(lastSelectedItem.node);
|
||
}
|
||
}
|
||
|
||
// Entf - Löschen mit Bestätigungs-Toast
|
||
if (e.key === 'Delete' && lastSelectedItem && !currentActiveTab) {
|
||
e.preventDefault();
|
||
if (lastSelectedItem.type === 'gitea') {
|
||
const { item, owner, repo } = lastSelectedItem;
|
||
showDeleteConfirm(`"${item.name}" wirklich löschen?`, async () => {
|
||
const res = await window.electronAPI.deleteFile({ path: item.path, owner, repo, isGitea: true });
|
||
if (res?.ok) { showSuccess(`"${item.name}" gelöscht`); loadRepoContents(owner, repo, currentState.path); lastSelectedItem = null; }
|
||
else showError('Löschen fehlgeschlagen: ' + (res?.error || ''));
|
||
});
|
||
} else if (lastSelectedItem.type === 'local') {
|
||
const { node } = lastSelectedItem;
|
||
showDeleteConfirm(`"${node.name}" wirklich löschen?`, async () => {
|
||
const res = await window.electronAPI.deleteFile({ path: node.path });
|
||
if (res?.ok) { showSuccess(`"${node.name}" gelöscht`); if (selectedFolder) refreshLocalTree(selectedFolder); lastSelectedItem = null; }
|
||
else showError('Löschen fehlgeschlagen: ' + (res?.error || ''));
|
||
});
|
||
}
|
||
}
|
||
});
|
||
|
||
// Progress listeners
|
||
window.electronAPI.onFolderUploadProgress(p => {
|
||
showProgress(p.percent, `Upload: ${p.processed}/${p.total}`);
|
||
});
|
||
|
||
window.electronAPI.onFolderDownloadProgress(p => {
|
||
showProgress(p.percent, `Download: ${p.processed}/${p.total}`);
|
||
});
|
||
|
||
// Setup globalen Drop-Handler für Repo-Ansicht
|
||
setupGlobalDropZone();
|
||
setupBackgroundContextMenu();
|
||
|
||
setStatus('Ready');
|
||
initUpdater(); // Updater initialisieren
|
||
updateNavigationUI();
|
||
});
|
||
/* ================================
|
||
RELEASE MANAGEMENT UI FUNCTIONS
|
||
Füge dies zu renderer.js hinzu
|
||
================================ */
|
||
|
||
let currentReleaseView = {
|
||
owner: null,
|
||
repo: null
|
||
};
|
||
|
||
/* -------------------------
|
||
RELEASES LADEN & ANZEIGEN
|
||
------------------------- */
|
||
async function loadRepoReleases(owner, repo) {
|
||
currentReleaseView.owner = owner;
|
||
currentReleaseView.repo = repo;
|
||
|
||
setStatus('Loading releases...');
|
||
|
||
try {
|
||
const res = await window.electronAPI.listReleases({ owner, repo });
|
||
|
||
if (!res.ok) {
|
||
showError('Error loading releases: ' + res.error);
|
||
return;
|
||
}
|
||
|
||
const grid = $('explorerGrid');
|
||
if (!grid) return;
|
||
|
||
// Header mit "New Release" Button
|
||
grid.innerHTML = `
|
||
<div style="grid-column: 1/-1; display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
||
<h2 style="margin: 0; color: var(--text-primary);">📦 Releases für ${repo}</h2>
|
||
<button class="btn-new-release" style="
|
||
background: var(--accent-gradient);
|
||
color: #000;
|
||
border: none;
|
||
padding: 10px 20px;
|
||
border-radius: var(--radius-md);
|
||
font-weight: 700;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
">
|
||
🚀 New Release
|
||
</button>
|
||
</div>
|
||
`;
|
||
|
||
// Event-Listener MUSS VOR innerHTML += gesetzt werden
|
||
const newBtn = grid.querySelector('.btn-new-release');
|
||
if (newBtn) {
|
||
newBtn.onclick = () => {
|
||
console.log('New Release button clicked');
|
||
showCreateReleaseModal(owner, repo);
|
||
};
|
||
} else {
|
||
console.error('New Release button not found in DOM');
|
||
}
|
||
|
||
if (!res.releases || res.releases.length === 0) {
|
||
// WICHTIG: appendChild statt innerHTML +=, um Event-Listener zu erhalten
|
||
const emptyMsg = document.createElement('div');
|
||
emptyMsg.style.cssText = 'grid-column: 1/-1; text-align: center; padding: 60px; color: var(--text-muted); font-size: 16px;';
|
||
emptyMsg.textContent = '📭 Noch keine Releases veröffentlicht';
|
||
grid.appendChild(emptyMsg);
|
||
setStatus('No releases');
|
||
return;
|
||
}
|
||
|
||
// Releases als Cards darstellen
|
||
res.releases.forEach((release, index) => {
|
||
const card = createReleaseCard(release, index === 0);
|
||
grid.appendChild(card);
|
||
});
|
||
|
||
setStatus(`${res.releases.length} release(s) loaded`);
|
||
|
||
} catch (error) {
|
||
console.error('Error loading releases:', error);
|
||
showError('Failed to load releases');
|
||
}
|
||
}
|
||
|
||
function createReleaseCard(release, isLatest) {
|
||
const card = document.createElement('div');
|
||
card.className = 'release-card';
|
||
card.style.cssText = `
|
||
grid-column: 1/-1;
|
||
background: var(--bg-tertiary);
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
border-radius: var(--radius-lg);
|
||
padding: var(--spacing-xl);
|
||
margin-bottom: var(--spacing-lg);
|
||
transition: all var(--transition-normal);
|
||
`;
|
||
|
||
// Header mit Tag und Badges
|
||
const header = document.createElement('div');
|
||
header.style.cssText = 'display: flex; gap: 10px; align-items: center; margin-bottom: 12px;';
|
||
|
||
const tag = document.createElement('span');
|
||
tag.textContent = release.tag_name;
|
||
tag.style.cssText = `
|
||
background: var(--accent-gradient);
|
||
color: #000;
|
||
padding: 6px 16px;
|
||
border-radius: 20px;
|
||
font-weight: 700;
|
||
font-size: 14px;
|
||
`;
|
||
header.appendChild(tag);
|
||
|
||
if (isLatest) {
|
||
const latestBadge = document.createElement('span');
|
||
latestBadge.textContent = 'LATEST';
|
||
latestBadge.style.cssText = `
|
||
background: var(--success);
|
||
color: #000;
|
||
padding: 4px 12px;
|
||
border-radius: 12px;
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
`;
|
||
header.appendChild(latestBadge);
|
||
}
|
||
|
||
if (release.prerelease) {
|
||
const preBadge = document.createElement('span');
|
||
preBadge.textContent = 'PRE-RELEASE';
|
||
preBadge.style.cssText = `
|
||
background: var(--warning);
|
||
color: #000;
|
||
padding: 4px 12px;
|
||
border-radius: 12px;
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
`;
|
||
header.appendChild(preBadge);
|
||
}
|
||
|
||
if (release.draft) {
|
||
const draftBadge = document.createElement('span');
|
||
draftBadge.textContent = 'DRAFT';
|
||
draftBadge.style.cssText = `
|
||
background: rgba(255, 255, 255, 0.2);
|
||
color: var(--text-primary);
|
||
padding: 4px 12px;
|
||
border-radius: 12px;
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
`;
|
||
header.appendChild(draftBadge);
|
||
}
|
||
|
||
card.appendChild(header);
|
||
|
||
// Title
|
||
const title = document.createElement('h3');
|
||
title.textContent = release.name || release.tag_name;
|
||
title.style.cssText = 'margin: 0 0 12px 0; color: var(--text-primary); font-size: 20px;';
|
||
card.appendChild(title);
|
||
|
||
// Body (Release Notes)
|
||
if (release.body) {
|
||
const body = document.createElement('div');
|
||
body.className = 'release-body';
|
||
body.innerHTML = parseMarkdownToHTML(release.body);
|
||
card.appendChild(body);
|
||
}
|
||
|
||
// Assets
|
||
if (release.assets && release.assets.length > 0) {
|
||
const assetsContainer = document.createElement('div');
|
||
assetsContainer.style.cssText = `
|
||
margin-top: 16px;
|
||
padding-top: 16px;
|
||
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
||
`;
|
||
|
||
const assetsTitle = document.createElement('div');
|
||
assetsTitle.textContent = '📦 Assets';
|
||
assetsTitle.style.cssText = 'font-weight: 600; margin-bottom: 12px; color: var(--text-primary);';
|
||
assetsContainer.appendChild(assetsTitle);
|
||
|
||
release.assets.forEach(asset => {
|
||
const assetItem = document.createElement('div');
|
||
assetItem.style.cssText = `
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 10px;
|
||
background: rgba(255, 255, 255, 0.03);
|
||
border-radius: var(--radius-sm);
|
||
margin-bottom: 8px;
|
||
`;
|
||
|
||
const assetName = document.createElement('span');
|
||
assetName.textContent = `📎 ${asset.name}`;
|
||
assetName.style.cssText = 'color: var(--text-primary);';
|
||
|
||
const assetSize = document.createElement('span');
|
||
assetSize.textContent = formatBytes(asset.size || 0);
|
||
assetSize.style.cssText = 'color: var(--text-muted); font-size: 12px; margin-left: 12px;';
|
||
|
||
const downloadBtn = document.createElement('button');
|
||
downloadBtn.textContent = '⬇️ Download';
|
||
downloadBtn.style.cssText = `
|
||
background: var(--bg-secondary);
|
||
color: var(--text-primary);
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
padding: 6px 12px;
|
||
border-radius: var(--radius-sm);
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
`;
|
||
downloadBtn.onclick = () => {
|
||
if (asset.browser_download_url) {
|
||
window.open(asset.browser_download_url, '_blank');
|
||
}
|
||
};
|
||
|
||
const deleteAssetBtn = document.createElement('button');
|
||
deleteAssetBtn.textContent = '🗑️';
|
||
deleteAssetBtn.style.cssText = `
|
||
background: transparent;
|
||
color: var(--danger);
|
||
border: 1px solid var(--danger);
|
||
padding: 6px 10px;
|
||
border-radius: var(--radius-sm);
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
margin-left: 8px;
|
||
`;
|
||
deleteAssetBtn.onclick = async () => {
|
||
if (confirm(`Delete asset "${asset.name}"?`)) {
|
||
const res = await window.electronAPI.deleteReleaseAsset({
|
||
owner: currentReleaseView.owner,
|
||
repo: currentReleaseView.repo,
|
||
assetId: asset.id
|
||
});
|
||
if (res.ok) {
|
||
assetItem.remove();
|
||
setStatus('Asset deleted');
|
||
}
|
||
}
|
||
};
|
||
|
||
const leftSide = document.createElement('div');
|
||
leftSide.style.cssText = 'display: flex; align-items: center; gap: 12px;';
|
||
leftSide.appendChild(assetName);
|
||
leftSide.appendChild(assetSize);
|
||
|
||
const rightSide = document.createElement('div');
|
||
rightSide.style.cssText = 'display: flex; gap: 8px;';
|
||
rightSide.appendChild(downloadBtn);
|
||
rightSide.appendChild(deleteAssetBtn);
|
||
|
||
assetItem.appendChild(leftSide);
|
||
assetItem.appendChild(rightSide);
|
||
assetsContainer.appendChild(assetItem);
|
||
});
|
||
|
||
card.appendChild(assetsContainer);
|
||
}
|
||
|
||
// Meta Info
|
||
const meta = document.createElement('div');
|
||
meta.style.cssText = `
|
||
display: flex;
|
||
gap: 20px;
|
||
margin-top: 16px;
|
||
padding-top: 16px;
|
||
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
||
color: var(--text-muted);
|
||
font-size: 12px;
|
||
`;
|
||
|
||
const date = new Date(release.created_at);
|
||
const dateStr = date.toLocaleDateString('de-DE', {
|
||
year: 'numeric',
|
||
month: 'long',
|
||
day: 'numeric'
|
||
});
|
||
|
||
meta.innerHTML = `
|
||
<span>📅 ${dateStr}</span>
|
||
<span>👤 ${release.author?.login || 'Unknown'}</span>
|
||
`;
|
||
card.appendChild(meta);
|
||
|
||
// Action Buttons
|
||
const actions = document.createElement('div');
|
||
actions.style.cssText = 'display: flex; gap: 12px; margin-top: 16px;';
|
||
|
||
const downloadArchiveBtn = document.createElement('button');
|
||
downloadArchiveBtn.textContent = '📦 Download ZIP';
|
||
downloadArchiveBtn.style.cssText = `
|
||
background: var(--bg-secondary);
|
||
color: var(--text-primary);
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
padding: 8px 16px;
|
||
border-radius: var(--radius-md);
|
||
cursor: pointer;
|
||
font-weight: 600;
|
||
`;
|
||
downloadArchiveBtn.onclick = async () => {
|
||
const res = await window.electronAPI.downloadReleaseArchive({
|
||
owner: currentReleaseView.owner,
|
||
repo: currentReleaseView.repo,
|
||
tag: release.tag_name
|
||
});
|
||
if (res.ok) {
|
||
setStatus(`Downloaded to ${res.savedTo}`);
|
||
}
|
||
};
|
||
|
||
const addAssetBtn = document.createElement('button');
|
||
addAssetBtn.textContent = '📎 Add Asset';
|
||
addAssetBtn.style.cssText = `
|
||
background: var(--bg-secondary);
|
||
color: var(--text-primary);
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
padding: 8px 16px;
|
||
border-radius: var(--radius-md);
|
||
cursor: pointer;
|
||
font-weight: 600;
|
||
`;
|
||
addAssetBtn.onclick = () => showUploadAssetDialog(release);
|
||
|
||
const deleteBtn = document.createElement('button');
|
||
deleteBtn.textContent = '🗑️ Delete';
|
||
deleteBtn.style.cssText = `
|
||
background: transparent;
|
||
color: var(--danger);
|
||
border: 1px solid var(--danger);
|
||
padding: 8px 16px;
|
||
border-radius: var(--radius-md);
|
||
cursor: pointer;
|
||
font-weight: 600;
|
||
margin-left: auto;
|
||
`;
|
||
deleteBtn.onclick = async () => {
|
||
if (confirm(`Delete release "${release.name || release.tag_name}"?`)) {
|
||
const res = await window.electronAPI.deleteRelease({
|
||
owner: currentReleaseView.owner,
|
||
repo: currentReleaseView.repo,
|
||
releaseId: release.id
|
||
});
|
||
if (res.ok) {
|
||
card.remove();
|
||
setStatus('Release deleted');
|
||
}
|
||
}
|
||
};
|
||
|
||
actions.appendChild(downloadArchiveBtn);
|
||
actions.appendChild(addAssetBtn);
|
||
actions.appendChild(deleteBtn);
|
||
card.appendChild(actions);
|
||
|
||
return card;
|
||
}
|
||
|
||
/* -------------------------
|
||
CREATE RELEASE MODAL (MIT DATEI-UPLOAD)
|
||
------------------------- */
|
||
function showCreateReleaseModal(owner, repo) {
|
||
const modal = document.createElement('div');
|
||
modal.className = 'modal';
|
||
modal.innerHTML = `
|
||
<div class="card" style="width: 600px; max-width: 90vw;">
|
||
<h2>🚀 Neues Release erstellen</h2>
|
||
|
||
<div class="input-group">
|
||
<label>Tag Version *</label>
|
||
<input id="releaseTag" type="text" placeholder="v1.0.0">
|
||
</div>
|
||
|
||
<div class="input-group">
|
||
<label>Release Name</label>
|
||
<input id="releaseName" type="text" placeholder="Version 1.0.0">
|
||
</div>
|
||
|
||
<div class="input-group">
|
||
<label>Release Notes</label>
|
||
<textarea id="releaseBody" rows="8" placeholder="## Was ist neu?
|
||
|
||
- Feature 1
|
||
- Feature 2
|
||
- Bug Fixes" style="
|
||
width: 100%;
|
||
padding: 12px;
|
||
border-radius: var(--radius-md);
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
background: var(--bg-tertiary);
|
||
color: var(--text-primary);
|
||
font-family: monospace;
|
||
resize: vertical;
|
||
"></textarea>
|
||
</div>
|
||
|
||
<div class="input-group">
|
||
<label>Target Branch</label>
|
||
<input id="releaseTarget" type="text" value="main" placeholder="main">
|
||
</div>
|
||
|
||
<!-- NEU: Datei Upload -->
|
||
<div class="input-group">
|
||
<label>Release Asset (Optional)</label>
|
||
<div style="display: flex; gap: 10px;">
|
||
<input id="releaseAssetInput" type="text" readonly placeholder="Keine Datei gewählt" style="
|
||
flex: 1;
|
||
padding: 10px;
|
||
border-radius: var(--radius-md);
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
background: rgba(0, 0, 0, 0.2);
|
||
color: var(--text-muted);
|
||
cursor: not-allowed;
|
||
">
|
||
<button id="btnSelectReleaseAsset" type="button" style="
|
||
padding: 10px 20px;
|
||
border-radius: var(--radius-md);
|
||
background: var(--bg-secondary);
|
||
color: var(--text-primary);
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
cursor: pointer;
|
||
font-weight: 600;
|
||
">📎 Datei wählen</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="input-group" style="display: flex; gap: 20px;">
|
||
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
|
||
<input type="checkbox" id="releasePrerelease"> Pre-Release
|
||
</label>
|
||
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
|
||
<input type="checkbox" id="releaseDraft"> Draft (nicht veröffentlichen)
|
||
</label>
|
||
</div>
|
||
|
||
<div class="modal-buttons">
|
||
<button id="btnCreateRelease">Erstellen & Veröffentlichen</button>
|
||
<button id="btnCancelRelease">Abbrechen</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
document.body.appendChild(modal);
|
||
|
||
// Variable zum Speichern des gewählten Dateipfads
|
||
let selectedAssetPath = null;
|
||
|
||
// 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)';
|
||
}
|
||
} catch (error) {
|
||
console.error('Fehler beim Auswählen der Datei:', error);
|
||
alert('Konnte Dateidialog nicht öffnen.');
|
||
}
|
||
};
|
||
|
||
// Event Listener: Release erstellen
|
||
$('btnCreateRelease').onclick = async () => {
|
||
const tag = $('releaseTag').value.trim();
|
||
const name = $('releaseName').value.trim() || tag;
|
||
const body = $('releaseBody').value.trim();
|
||
const target = $('releaseTarget').value.trim() || 'main';
|
||
const prerelease = $('releasePrerelease').checked;
|
||
const draft = $('releaseDraft').checked;
|
||
|
||
if (!tag) {
|
||
alert('Tag Version ist erforderlich!');
|
||
return;
|
||
}
|
||
|
||
setStatus('Creating release...');
|
||
|
||
try {
|
||
// 1. Release erstellen
|
||
const res = await window.electronAPI.createRelease({
|
||
owner,
|
||
repo,
|
||
tag_name: tag,
|
||
name,
|
||
body,
|
||
target_commitish: target,
|
||
prerelease,
|
||
draft
|
||
});
|
||
|
||
if (res.ok) {
|
||
// 2. Falls eine Datei ausgewählt wurde, direkt hochladen
|
||
if (selectedAssetPath) {
|
||
setStatus('Release erstellt. Lade Datei hoch...');
|
||
showProgress(50, 'Uploading Asset...');
|
||
|
||
try {
|
||
const fileName = selectedAssetPath.split(/[\\/]/).pop();
|
||
const uploadRes = await window.electronAPI.uploadReleaseAsset({
|
||
owner,
|
||
repo,
|
||
releaseId: res.release.id,
|
||
filePath: selectedAssetPath,
|
||
fileName
|
||
});
|
||
|
||
if (uploadRes.ok) {
|
||
setStatus(`Release "${tag}" und Asset erstellt!`);
|
||
} else {
|
||
console.error('Asset Upload fehlgeschlagen:', uploadRes.error);
|
||
alert(`Release erstellt, aber Asset Upload fehlgeschlagen: ${uploadRes.error}`);
|
||
showWarning('Release erstellt (Upload Fehler)');
|
||
}
|
||
} catch (uploadErr) {
|
||
console.error('Upload error:', uploadErr);
|
||
alert('Release erstellt, aber Fehler beim Hochladen der Datei.');
|
||
showWarning('Release erstellt (Upload Fehler)');
|
||
} finally {
|
||
hideProgress();
|
||
}
|
||
} else {
|
||
setStatus('Release created!');
|
||
}
|
||
|
||
modal.remove();
|
||
loadRepoReleases(owner, repo); // Liste neu laden
|
||
} else {
|
||
showError('Failed: ' + res.error);
|
||
alert('Fehler beim Erstellen des Releases: ' + res.error);
|
||
}
|
||
} catch (error) {
|
||
console.error('Create release error:', error);
|
||
showError('Create failed');
|
||
alert('Ein unerwarteter Fehler ist aufgetreten.');
|
||
}
|
||
};
|
||
|
||
$('btnCancelRelease').onclick = () => modal.remove();
|
||
|
||
// Close on background click
|
||
modal.onclick = (e) => {
|
||
if (e.target === modal) modal.remove();
|
||
};
|
||
}
|
||
/* -------------------------
|
||
UPLOAD ASSET DIALOG
|
||
------------------------- */
|
||
async function showUploadAssetDialog(release) {
|
||
try {
|
||
const res = await window.electronAPI.selectFile();
|
||
|
||
if (!res.ok || !res.files || res.files.length === 0) {
|
||
return;
|
||
}
|
||
|
||
const filePath = res.files[0];
|
||
const fileName = filePath.split(/[\\/]/).pop();
|
||
|
||
setStatus(`Uploading ${fileName}...`);
|
||
showProgress(0, `Uploading ${fileName}...`);
|
||
|
||
const uploadRes = await window.electronAPI.uploadReleaseAsset({
|
||
owner: currentReleaseView.owner,
|
||
repo: currentReleaseView.repo,
|
||
releaseId: release.id,
|
||
filePath,
|
||
fileName
|
||
});
|
||
|
||
hideProgress();
|
||
|
||
if (uploadRes.ok) {
|
||
setStatus('Asset uploaded!');
|
||
// Reload releases to show new asset
|
||
loadRepoReleases(currentReleaseView.owner, currentReleaseView.repo);
|
||
} else {
|
||
showError('Upload failed: ' + uploadRes.error);
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Upload asset error:', error);
|
||
hideProgress();
|
||
showError('Upload failed');
|
||
}
|
||
}
|
||
|
||
/* -------------------------
|
||
HELPER FUNCTIONS
|
||
------------------------- */
|
||
function formatBytes(bytes) {
|
||
if (bytes === 0) return '0 B';
|
||
const k = 1024;
|
||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
||
}
|
||
|
||
/* -------------------------
|
||
INTEGRATION IN REPO VIEW
|
||
Füge "Releases" Tab zum Repo hinzu
|
||
------------------------- */
|
||
|
||
// Modifiziere die loadRepoContents Funktion um einen Releases-Button hinzuzufügen:
|
||
// Nach dem Laden eines Repos, zeige einen Button "View Releases" an
|
||
/* ================================
|
||
COMMIT HISTORY VISUALIZATION UI
|
||
Füge dies zu renderer.js hinzu
|
||
================================ */
|
||
|
||
let currentCommitView = {
|
||
owner: null,
|
||
repo: null,
|
||
branch: 'HEAD',
|
||
commits: [],
|
||
selectedCommit: null
|
||
};
|
||
|
||
/* -------------------------
|
||
COMMIT HISTORY LADEN
|
||
------------------------- */
|
||
async function loadCommitHistory(owner, repo, branch = 'main') {
|
||
currentCommitView.owner = owner;
|
||
currentCommitView.repo = repo;
|
||
currentCommitView.branch = branch;
|
||
|
||
setStatus('Loading commit history...');
|
||
|
||
try {
|
||
const res = await window.electronAPI.getCommits({
|
||
owner,
|
||
repo,
|
||
branch,
|
||
limit: 100
|
||
});
|
||
|
||
if (!res.ok) {
|
||
showError('Error loading commits: ' + res.error);
|
||
return;
|
||
}
|
||
|
||
currentCommitView.commits = res.commits;
|
||
renderCommitHistoryView();
|
||
setStatus(`${res.commits.length} commits loaded`);
|
||
|
||
} catch (error) {
|
||
console.error('Error loading commit history:', error);
|
||
showError('Failed to load commits');
|
||
}
|
||
}
|
||
|
||
function renderCommitHistoryView() {
|
||
const grid = $('explorerGrid');
|
||
if (!grid) return;
|
||
|
||
grid.innerHTML = '';
|
||
grid.style.gridTemplateColumns = '1fr';
|
||
|
||
// Header mit Search und Branch-Selector
|
||
const header = document.createElement('div');
|
||
header.style.cssText = `
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 24px;
|
||
gap: 16px;
|
||
flex-wrap: wrap;
|
||
`;
|
||
|
||
header.innerHTML = `
|
||
<h2 style="margin: 0; color: var(--text-primary); display: flex; align-items: center; gap: 12px;">
|
||
📊 Commit History
|
||
<span style="font-size: 14px; color: var(--text-muted); font-weight: 400;">
|
||
${currentCommitView.repo} / ${currentCommitView.branch}
|
||
</span>
|
||
</h2>
|
||
|
||
<div style="display: flex; gap: 12px; flex: 1; max-width: 600px;">
|
||
<input
|
||
type="text"
|
||
id="commitSearch"
|
||
placeholder="🔍 Search commits (message, author)..."
|
||
style="
|
||
flex: 1;
|
||
padding: 10px 16px;
|
||
border-radius: var(--radius-md);
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
background: var(--bg-tertiary);
|
||
color: var(--text-primary);
|
||
font-size: 14px;
|
||
"
|
||
/>
|
||
<button id="btnClearSearch" style="
|
||
padding: 10px 16px;
|
||
border-radius: var(--radius-md);
|
||
background: var(--bg-secondary);
|
||
color: var(--text-primary);
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
cursor: pointer;
|
||
font-weight: 600;
|
||
">Clear</button>
|
||
</div>
|
||
`;
|
||
|
||
grid.appendChild(header);
|
||
|
||
// Search-Handler
|
||
const searchInput = header.querySelector('#commitSearch');
|
||
const clearBtn = header.querySelector('#btnClearSearch');
|
||
|
||
let searchTimeout;
|
||
searchInput.addEventListener('input', (e) => {
|
||
clearTimeout(searchTimeout);
|
||
searchTimeout = setTimeout(() => {
|
||
handleCommitSearch(e.target.value);
|
||
}, 300);
|
||
});
|
||
|
||
clearBtn.onclick = () => {
|
||
searchInput.value = '';
|
||
renderCommitTimeline(currentCommitView.commits);
|
||
};
|
||
|
||
// Timeline Container
|
||
const timelineContainer = document.createElement('div');
|
||
timelineContainer.id = 'commitTimeline';
|
||
timelineContainer.style.cssText = `
|
||
position: relative;
|
||
max-width: 100%;
|
||
`;
|
||
|
||
grid.appendChild(timelineContainer);
|
||
|
||
// Initial render
|
||
renderCommitTimeline(currentCommitView.commits);
|
||
}
|
||
|
||
function renderCommitTimeline(commits) {
|
||
const container = $('commitTimeline');
|
||
if (!container) return;
|
||
|
||
container.innerHTML = '';
|
||
|
||
if (!commits || commits.length === 0) {
|
||
container.innerHTML = '<div style="text-align: center; padding: 60px; color: var(--text-muted); font-size: 16px;">📭 No commits found</div>';
|
||
return;
|
||
}
|
||
|
||
// Timeline mit Cards
|
||
commits.forEach((commit, index) => {
|
||
const card = createCommitCard(commit, index);
|
||
container.appendChild(card);
|
||
});
|
||
}
|
||
|
||
function createCommitCard(commit, index) {
|
||
const card = document.createElement('div');
|
||
card.className = 'commit-card';
|
||
card.dataset.sha = commit.sha;
|
||
|
||
const isEven = index % 2 === 0;
|
||
|
||
card.style.cssText = `
|
||
position: relative;
|
||
padding-left: 60px;
|
||
margin-bottom: 32px;
|
||
cursor: pointer;
|
||
transition: all var(--transition-normal);
|
||
`;
|
||
|
||
// Timeline dot
|
||
const dot = document.createElement('div');
|
||
dot.style.cssText = `
|
||
position: absolute;
|
||
left: 18px;
|
||
top: 0;
|
||
width: 16px;
|
||
height: 16px;
|
||
background: var(--accent-primary);
|
||
border: 3px solid var(--bg-primary);
|
||
border-radius: 50%;
|
||
z-index: 2;
|
||
box-shadow: 0 0 0 4px var(--bg-tertiary);
|
||
`;
|
||
card.appendChild(dot);
|
||
|
||
// Timeline line
|
||
if (index < currentCommitView.commits.length - 1) {
|
||
const line = document.createElement('div');
|
||
line.style.cssText = `
|
||
position: absolute;
|
||
left: 25px;
|
||
top: 16px;
|
||
width: 2px;
|
||
height: calc(100% + 32px);
|
||
background: linear-gradient(180deg, var(--accent-primary) 0%, rgba(0, 212, 255, 0.2) 100%);
|
||
z-index: 1;
|
||
`;
|
||
card.appendChild(line);
|
||
}
|
||
|
||
// Content card
|
||
const content = document.createElement('div');
|
||
content.className = 'commit-content';
|
||
content.style.cssText = `
|
||
background: var(--bg-tertiary);
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
border-radius: var(--radius-lg);
|
||
padding: var(--spacing-lg);
|
||
transition: all var(--transition-normal);
|
||
`;
|
||
|
||
// Commit message
|
||
const message = commit.commit?.message || commit.message || 'No message';
|
||
const shortMessage = message.split('\n')[0]; // First line only
|
||
|
||
const messageEl = document.createElement('div');
|
||
messageEl.style.cssText = `
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: var(--text-primary);
|
||
margin-bottom: 12px;
|
||
line-height: 1.4;
|
||
`;
|
||
messageEl.textContent = shortMessage;
|
||
content.appendChild(messageEl);
|
||
|
||
// Meta info
|
||
const meta = document.createElement('div');
|
||
meta.style.cssText = `
|
||
display: flex;
|
||
gap: 20px;
|
||
flex-wrap: wrap;
|
||
font-size: 13px;
|
||
color: var(--text-muted);
|
||
margin-bottom: 12px;
|
||
`;
|
||
|
||
const author = commit.commit?.author?.name || commit.author?.login || 'Unknown';
|
||
const date = new Date(commit.commit?.author?.date || commit.created_at);
|
||
const dateStr = formatRelativeTime(date);
|
||
const sha = commit.sha?.substring(0, 7) || '???????';
|
||
|
||
meta.innerHTML = `
|
||
<span style="display: flex; align-items: center; gap: 6px;">
|
||
👤 <strong>${author}</strong>
|
||
</span>
|
||
<span style="display: flex; align-items: center; gap: 6px;">
|
||
🕐 ${dateStr}
|
||
</span>
|
||
<span style="display: flex; align-items: center; gap: 6px; font-family: monospace; background: rgba(255,255,255,0.05); padding: 2px 8px; border-radius: 4px;">
|
||
#${sha}
|
||
</span>
|
||
`;
|
||
content.appendChild(meta);
|
||
|
||
// Stats (if available)
|
||
if (commit.stats) {
|
||
const stats = document.createElement('div');
|
||
stats.style.cssText = `
|
||
display: flex;
|
||
gap: 16px;
|
||
font-size: 12px;
|
||
padding-top: 12px;
|
||
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
||
`;
|
||
|
||
stats.innerHTML = `
|
||
<span style="color: var(--success);">+${commit.stats.additions || 0}</span>
|
||
<span style="color: var(--danger);">-${commit.stats.deletions || 0}</span>
|
||
<span style="color: var(--text-muted);">${commit.stats.total || 0} changes</span>
|
||
`;
|
||
content.appendChild(stats);
|
||
}
|
||
|
||
card.appendChild(content);
|
||
|
||
// Hover effect
|
||
card.addEventListener('mouseenter', () => {
|
||
content.style.borderColor = 'var(--accent-primary)';
|
||
content.style.transform = 'translateX(4px)';
|
||
content.style.boxShadow = 'var(--shadow-md)';
|
||
});
|
||
|
||
card.addEventListener('mouseleave', () => {
|
||
content.style.borderColor = 'rgba(255, 255, 255, 0.1)';
|
||
content.style.transform = 'translateX(0)';
|
||
content.style.boxShadow = 'none';
|
||
});
|
||
|
||
// Click to show details
|
||
card.onclick = () => showCommitDetails(commit);
|
||
|
||
return card;
|
||
}
|
||
|
||
/* -------------------------
|
||
COMMIT DETAILS & DIFF VIEWER
|
||
------------------------- */
|
||
async function showCommitDetails(commit) {
|
||
currentCommitView.selectedCommit = commit;
|
||
|
||
const modal = document.createElement('div');
|
||
modal.className = 'modal commit-modal';
|
||
modal.innerHTML = `
|
||
<div class="card" style="width: 90vw; max-width: 1200px; max-height: 90vh; overflow: hidden; display: flex; flex-direction: column;">
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
||
<h2 style="margin: 0;">📋 Commit Details</h2>
|
||
<button id="btnCloseCommitModal" style="
|
||
background: transparent;
|
||
border: none;
|
||
font-size: 24px;
|
||
cursor: pointer;
|
||
color: var(--text-muted);
|
||
">✕</button>
|
||
</div>
|
||
|
||
<div style="overflow-y: auto; flex: 1;">
|
||
<div id="commitDetailsContent" style="padding-bottom: 20px;">
|
||
<div style="text-align: center; padding: 40px; color: var(--text-muted);">
|
||
Loading commit details...
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
document.body.appendChild(modal);
|
||
|
||
$('btnCloseCommitModal').onclick = () => modal.remove();
|
||
modal.onclick = (e) => {
|
||
if (e.target === modal) modal.remove();
|
||
};
|
||
|
||
// Load commit details
|
||
await loadCommitDetailsContent(commit);
|
||
}
|
||
|
||
async function loadCommitDetailsContent(commit) {
|
||
const container = $('commitDetailsContent');
|
||
if (!container) return;
|
||
|
||
try {
|
||
// Check if this is local git or Gitea repo
|
||
let diffRes, filesRes;
|
||
|
||
if (selectedFolder) {
|
||
// Local Git repository
|
||
const details = await window.electronAPI.getLocalCommitDetails({
|
||
folderPath: selectedFolder,
|
||
sha: commit.sha || commit.hash
|
||
});
|
||
|
||
diffRes = { ok: true, diff: details?.diff || '' };
|
||
filesRes = { ok: true, files: details?.fileChanges?.files || [], stats: { additions: details?.fileChanges?.insertions || 0, deletions: details?.fileChanges?.deletions || 0 } };
|
||
} else {
|
||
// Gitea repository
|
||
const [diff, files] = await Promise.all([
|
||
window.electronAPI.getCommitDiff({
|
||
owner: currentCommitView.owner,
|
||
repo: currentCommitView.repo,
|
||
sha: commit.sha
|
||
}),
|
||
window.electronAPI.getCommitFiles({
|
||
owner: currentCommitView.owner,
|
||
repo: currentCommitView.repo,
|
||
sha: commit.sha
|
||
})
|
||
]);
|
||
|
||
diffRes = diff;
|
||
filesRes = files;
|
||
}
|
||
|
||
container.innerHTML = '';
|
||
|
||
// Commit info header
|
||
const header = document.createElement('div');
|
||
header.style.cssText = `
|
||
background: var(--bg-tertiary);
|
||
padding: var(--spacing-xl);
|
||
border-radius: var(--radius-lg);
|
||
margin-bottom: 24px;
|
||
`;
|
||
|
||
const message = commit.commit?.message || commit.message || 'No message';
|
||
const author = commit.commit?.author?.name || commit.author?.login || 'Unknown';
|
||
const email = commit.commit?.author?.email || '';
|
||
const date = new Date(commit.commit?.author?.date || commit.created_at);
|
||
const sha = commit.sha || '';
|
||
|
||
header.innerHTML = `
|
||
<h3 style="margin: 0 0 16px 0; font-size: 20px; line-height: 1.4;">${escapeHtml(message)}</h3>
|
||
<div style="display: flex; gap: 24px; font-size: 14px; color: var(--text-muted); flex-wrap: wrap;">
|
||
<span>👤 <strong>${escapeHtml(author)}</strong> ${email ? `<${escapeHtml(email)}>` : ''}</span>
|
||
<span>🕐 ${date.toLocaleString()}</span>
|
||
<span style="font-family: monospace; background: rgba(255,255,255,0.05); padding: 4px 12px; border-radius: 6px;">${sha.substring(0, 7)}</span>
|
||
</div>
|
||
`;
|
||
container.appendChild(header);
|
||
|
||
// File changes summary
|
||
if (filesRes.ok && filesRes.files && Array.isArray(filesRes.files) && filesRes.files.length > 0) {
|
||
const filesHeader = document.createElement('div');
|
||
filesHeader.style.cssText = `
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 16px;
|
||
`;
|
||
|
||
filesHeader.innerHTML = `
|
||
<h4 style="margin: 0; display: flex; align-items: center; gap: 8px;">
|
||
📁 Changed Files (${filesRes.files.length})
|
||
</h4>
|
||
<div style="display: flex; gap: 16px; font-size: 13px;">
|
||
<span style="color: var(--success);">+${filesRes.stats?.additions || 0}</span>
|
||
<span style="color: var(--danger);">-${filesRes.stats?.deletions || 0}</span>
|
||
</div>
|
||
`;
|
||
container.appendChild(filesHeader);
|
||
|
||
// File list
|
||
const fileList = document.createElement('div');
|
||
fileList.style.cssText = 'margin-bottom: 24px;';
|
||
|
||
filesRes.files.forEach(file => {
|
||
const fileItem = document.createElement('div');
|
||
fileItem.style.cssText = `
|
||
display: flex;
|
||
justify-content: space-between;
|
||
padding: 8px 12px;
|
||
background: rgba(255, 255, 255, 0.03);
|
||
border-radius: var(--radius-sm);
|
||
margin-bottom: 4px;
|
||
font-size: 13px;
|
||
`;
|
||
|
||
const changeType = file.changes === file.insertions ? 'added' :
|
||
file.changes === file.deletions ? 'deleted' : 'modified';
|
||
|
||
const icon = changeType === 'added' ? '🆕' :
|
||
changeType === 'deleted' ? '🗑️' : '📝';
|
||
|
||
fileItem.innerHTML = `
|
||
<span style="font-family: monospace; color: var(--text-primary);">${icon} ${escapeHtml(file.file)}</span>
|
||
<span style="color: var(--text-muted);">
|
||
<span style="color: var(--success);">+${file.insertions}</span>
|
||
<span style="color: var(--danger); margin-left: 8px;">-${file.deletions}</span>
|
||
</span>
|
||
`;
|
||
fileList.appendChild(fileItem);
|
||
});
|
||
|
||
container.appendChild(fileList);
|
||
}
|
||
|
||
// Diff viewer
|
||
if (diffRes.ok && diffRes.diff) {
|
||
const diffHeader = document.createElement('h4');
|
||
diffHeader.textContent = '📝 Changes (Diff)';
|
||
diffHeader.style.marginBottom = '12px';
|
||
container.appendChild(diffHeader);
|
||
|
||
const diffContainer = document.createElement('div');
|
||
diffContainer.className = 'diff-viewer';
|
||
diffContainer.style.cssText = `
|
||
background: #1e1e1e;
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
border-radius: var(--radius-md);
|
||
padding: 16px;
|
||
overflow-x: auto;
|
||
font-family: 'Courier New', monospace;
|
||
font-size: 13px;
|
||
line-height: 1.6;
|
||
max-height: 600px;
|
||
overflow-y: auto;
|
||
`;
|
||
|
||
diffContainer.innerHTML = formatDiff(diffRes.diff);
|
||
container.appendChild(diffContainer);
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Error loading commit details:', error);
|
||
console.error('Stack:', error.stack);
|
||
console.error('selectedFolder:', selectedFolder);
|
||
console.error('commit:', commit);
|
||
|
||
const errorMsg = error.message || String(error);
|
||
const isLocalGit = selectedFolder ? 'Local Git' : 'Gitea';
|
||
|
||
container.innerHTML = `<div style="text-align: center; padding: 40px; color: var(--danger);">
|
||
<p>❌ Error loading commit details</p>
|
||
<p style="font-size: 12px; color: var(--text-muted); margin-top: 12px;">
|
||
Source: ${isLocalGit}<br>
|
||
Error: ${escapeHtml(errorMsg)}
|
||
</p>
|
||
</div>`;
|
||
}
|
||
}
|
||
|
||
function formatDiff(diffText) {
|
||
const lines = diffText.split('\n');
|
||
let html = '';
|
||
|
||
lines.forEach(line => {
|
||
let color = '#d4d4d4';
|
||
let bgColor = 'transparent';
|
||
|
||
if (line.startsWith('+++') || line.startsWith('---')) {
|
||
color = '#569cd6'; // Blue
|
||
} else if (line.startsWith('+')) {
|
||
color = '#4ec9b0'; // Green
|
||
bgColor = 'rgba(78, 201, 176, 0.1)';
|
||
} else if (line.startsWith('-')) {
|
||
color = '#f48771'; // Red
|
||
bgColor = 'rgba(244, 135, 113, 0.1)';
|
||
} else if (line.startsWith('@@')) {
|
||
color = '#c586c0'; // Purple
|
||
} else if (line.startsWith('diff')) {
|
||
color = '#dcdcaa'; // Yellow
|
||
}
|
||
|
||
html += `<div style="color: ${color}; background: ${bgColor}; padding: 2px 4px;">${escapeHtml(line)}</div>`;
|
||
});
|
||
|
||
return html;
|
||
}
|
||
|
||
/* -------------------------
|
||
COMMIT SEARCH
|
||
------------------------- */
|
||
async function handleCommitSearch(query) {
|
||
if (!query || query.trim().length === 0) {
|
||
renderCommitTimeline(currentCommitView.commits);
|
||
return;
|
||
}
|
||
|
||
setStatus('Searching commits...');
|
||
|
||
try {
|
||
const res = await window.electronAPI.searchCommits({
|
||
owner: currentCommitView.owner,
|
||
repo: currentCommitView.repo,
|
||
branch: currentCommitView.branch,
|
||
query: query.trim()
|
||
});
|
||
|
||
if (res.ok) {
|
||
renderCommitTimeline(res.commits);
|
||
setStatus(`Found ${res.commits.length} commits`);
|
||
} else {
|
||
setStatus('Search failed');
|
||
}
|
||
} catch (error) {
|
||
console.error('Search error:', error);
|
||
setStatus('Search error');
|
||
}
|
||
}
|
||
|
||
/* -------------------------
|
||
HELPER FUNCTIONS
|
||
------------------------- */
|
||
function formatRelativeTime(date) {
|
||
const now = new Date();
|
||
const diffMs = now - date;
|
||
const diffMins = Math.floor(diffMs / 60000);
|
||
|
||
const diffHours = Math.floor(diffMs / 3600000);
|
||
const diffDays = Math.floor(diffMs / 86400000);
|
||
|
||
if (diffMins < 1) return 'just now';
|
||
if (diffMins < 60) return `${diffMins} min ago`;
|
||
if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`;
|
||
if (diffDays < 7) return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`;
|
||
if (diffDays < 30) return `${Math.floor(diffDays / 7)} week${Math.floor(diffDays / 7) > 1 ? 's' : ''} ago`;
|
||
if (diffDays < 365) return `${Math.floor(diffDays / 30)} month${Math.floor(diffDays / 30) > 1 ? 's' : ''} ago`;
|
||
return `${Math.floor(diffDays / 365)} year${Math.floor(diffDays / 365) > 1 ? 's' : ''} ago`;
|
||
}
|
||
|
||
function escapeHtml(text) {
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
/* -------------------------
|
||
EVENT LISTENERS
|
||
------------------------- */
|
||
|
||
// File Editor Event Listeners
|
||
setTimeout(() => {
|
||
// Buttons
|
||
const btnClose = $('btnCloseEditor');
|
||
const btnSave = $('btnEditorSave');
|
||
const btnSearch = $('btnEditorSearch');
|
||
const btnDiscard = $('btnDiscardEdit');
|
||
const btnFileActions = $('btnFileActions');
|
||
const modal = $('fileEditorModal');
|
||
|
||
// Close button
|
||
if (btnClose) btnClose.addEventListener('click', closeFileEditor);
|
||
|
||
// Save button
|
||
if (btnSave) btnSave.addEventListener('click', saveCurrentFile);
|
||
|
||
// Search button
|
||
if (btnSearch) btnSearch.addEventListener('click', toggleSearch);
|
||
|
||
// Discard button
|
||
if (btnDiscard) btnDiscard.addEventListener('click', closeFileEditor);
|
||
|
||
// File actions menu
|
||
if (btnFileActions) {
|
||
btnFileActions.addEventListener('click', (e) => {
|
||
const menu = $('fileActionsMenu');
|
||
if (menu) {
|
||
menu.classList.toggle('hidden');
|
||
const rect = btnFileActions.getBoundingClientRect();
|
||
menu.style.top = (rect.bottom + 4) + 'px';
|
||
menu.style.right = '20px';
|
||
}
|
||
});
|
||
}
|
||
|
||
// Search & Replace
|
||
const searchInput = $('searchInput');
|
||
if (searchInput) {
|
||
searchInput.addEventListener('keyup', performSearch);
|
||
searchInput.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Enter') {
|
||
replaceOnce();
|
||
}
|
||
});
|
||
}
|
||
|
||
const btnReplace = $('btnReplace');
|
||
if (btnReplace) btnReplace.addEventListener('click', replaceOnce);
|
||
|
||
const btnReplaceAll = $('btnReplaceAll');
|
||
if (btnReplaceAll) btnReplaceAll.addEventListener('click', replaceAll);
|
||
|
||
const btnCloseSearch = $('btnCloseSearch');
|
||
if (btnCloseSearch) btnCloseSearch.addEventListener('click', () => {
|
||
const searchBar = $('searchBar');
|
||
if (searchBar) searchBar.classList.add('hidden');
|
||
});
|
||
|
||
// Textarea events
|
||
const textarea = $('fileEditorContent');
|
||
if (textarea) {
|
||
textarea.addEventListener('input', () => {
|
||
updateEditorContent(textarea.value);
|
||
});
|
||
|
||
textarea.addEventListener('scroll', () => {
|
||
const lineNumbers = $('lineNumbers');
|
||
if (lineNumbers) lineNumbers.scrollTop = textarea.scrollTop;
|
||
});
|
||
|
||
textarea.addEventListener('click', updateEditorStats);
|
||
textarea.addEventListener('keyup', updateEditorStats);
|
||
}
|
||
|
||
// Close modal on background click
|
||
if (modal) {
|
||
modal.addEventListener('click', (e) => {
|
||
if (e.target === modal) {
|
||
closeFileEditor();
|
||
}
|
||
});
|
||
}
|
||
|
||
console.log('✅ Advanced editor event listeners registered');
|
||
}, 100);
|
||
|
||
// Global keyboard shortcuts
|
||
document.addEventListener('keydown', (e) => {
|
||
const modal = $('fileEditorModal');
|
||
if (!modal || modal.classList.contains('hidden')) return;
|
||
|
||
// Ctrl+S / Cmd+S - Save
|
||
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
||
e.preventDefault();
|
||
saveCurrentFile();
|
||
}
|
||
|
||
// Ctrl+F / Cmd+F - Search
|
||
if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
|
||
e.preventDefault();
|
||
toggleSearch();
|
||
}
|
||
|
||
// Ctrl+H / Cmd+H - Replace
|
||
if ((e.ctrlKey || e.metaKey) && e.key === 'h') {
|
||
e.preventDefault();
|
||
toggleSearch();
|
||
$('replaceInput').focus();
|
||
}
|
||
|
||
// ESC - Close search bar if open
|
||
if (e.key === 'Escape') {
|
||
const searchBar = $('searchBar');
|
||
if (searchBar && !searchBar.classList.contains('hidden')) {
|
||
searchBar.classList.add('hidden');
|
||
}
|
||
}
|
||
});
|
||
/* ========================================
|
||
UPDATER FUNKTIONEN (Optimiert & Synchronisiert)
|
||
======================================== */
|
||
|
||
async function initUpdater() {
|
||
try {
|
||
const versionRes = await window.electronAPI.getAppVersion();
|
||
if (versionRes && versionRes.ok && $('appVersion')) {
|
||
$('appVersion').value = versionRes.version;
|
||
}
|
||
} catch (error) {
|
||
console.error('[Renderer] Fehler beim Laden der Version:', error);
|
||
}
|
||
|
||
// Manueller Check Button in Settings
|
||
if ($('btnCheckUpdates')) {
|
||
$('btnCheckUpdates').onclick = async () => {
|
||
const btn = $('btnCheckUpdates');
|
||
const originalHTML = btn.innerHTML;
|
||
btn.innerHTML = '⏳ Suche...';
|
||
btn.disabled = true;
|
||
|
||
try {
|
||
await window.electronAPI.checkForUpdates();
|
||
setStatus('Update-Suche abgeschlossen');
|
||
} catch (error) {
|
||
setStatus('Fehler bei der Update-Prüfung');
|
||
} finally {
|
||
setTimeout(() => {
|
||
btn.innerHTML = originalHTML;
|
||
btn.disabled = false;
|
||
}, 1500);
|
||
}
|
||
};
|
||
}
|
||
}
|
||
|
||
// Event-Listener für das Update-Modal
|
||
if (window.electronAPI.onUpdateAvailable) {
|
||
window.electronAPI.onUpdateAvailable((info) => {
|
||
const modal = $('updateModal');
|
||
const versionInfo = $('updateVersionInfo');
|
||
const changelog = $('updateChangelog');
|
||
|
||
if (versionInfo) versionInfo.innerText = `Version ${info.version} verfügbar!`;
|
||
if (changelog) changelog.innerText = info.body || 'Keine Release-Notes vorhanden.';
|
||
if (modal) modal.classList.remove('hidden');
|
||
|
||
// Button: Jetzt installieren
|
||
const updateBtn = $('btnStartUpdate');
|
||
if (updateBtn) {
|
||
updateBtn.onclick = () => {
|
||
if (modal) modal.classList.add('hidden');
|
||
setStatus('Download gestartet...');
|
||
// Aufruf der korrekten Preload-Funktion
|
||
window.electronAPI.startUpdateDownload(info.asset);
|
||
};
|
||
}
|
||
|
||
// Button: Später
|
||
const ignoreBtn = $('btnIgnoreUpdate');
|
||
if (ignoreBtn) {
|
||
ignoreBtn.onclick = () => { if (modal) modal.classList.add('hidden'); };
|
||
}
|
||
});
|
||
}
|
||
|
||
// AM ENDE DER DATEI: Initialisierung beim Start
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
// 1. Basis-Setup (Settings-Feld füllen etc.)
|
||
initUpdater();
|
||
|
||
// 2. AUTOMATISCHER UPDATE-CHECK BEIM START
|
||
// Wir warten 3 Sekunden, damit die App in Ruhe laden kann
|
||
setTimeout(() => {
|
||
console.log("[Auto-Updater] Suche im Hintergrund nach Updates...");
|
||
window.electronAPI.checkForUpdates();
|
||
}, 3000);
|
||
}); |