Files
Git-Manager-Gui/renderer/renderer.js
2026-03-24 19:18:26 +01:00

5839 lines
206 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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;
let featureAutostart = false;
let repoNameValidationTimer = null;
let batchCloneValidationTimer = null;
let activityHeatmapCollapsed = true;
let activityHeatmapRangeMonths = 20;
// Sidebar-State für die linke Favoriten/Verlauf-Fläche
let sidebarMode = 'favorites';
let currentGiteaRepos = [];
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);
refreshFavHistoryUi();
}
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);
refreshFavHistoryUi();
}
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');
}
function setPlatformSelection(platform) {
const platformInput = $('platform');
if (platformInput) {
platformInput.value = platform;
}
document.querySelectorAll('.platform-option').forEach(button => {
const isActive = button.dataset.platform === platform;
button.classList.toggle('active', isActive);
button.setAttribute('aria-pressed', isActive ? 'true' : 'false');
});
}
function initializePlatformSelection() {
const platformInput = $('platform');
const initialPlatform = platformInput?.value || 'gitea';
setPlatformSelection(initialPlatform);
document.querySelectorAll('.platform-option').forEach(button => {
button.addEventListener('click', () => {
setPlatformSelection(button.dataset.platform || 'gitea');
});
});
}
/* 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 renderFavHistorySidebar(allRepos) {
const sidebar = $('favHistorySidebar');
if (!sidebar) return;
const hasFavFeature = featureFavorites;
const hasRecFeature = featureRecent;
const canShowSidebar = hasFavFeature || hasRecFeature;
// Sidebar nur einblenden wenn Feature aktiv — nichts am main/explorerGrid ändern
sidebar.classList.toggle('visible', canShowSidebar);
if (!canShowSidebar) {
sidebar.innerHTML = '';
return;
}
if (!hasFavFeature && sidebarMode === 'favorites') sidebarMode = 'recent';
if (!hasRecFeature && sidebarMode === 'recent') sidebarMode = 'favorites';
const activeType = sidebarMode;
const items = activeType === 'favorites' ? favorites : recentRepos;
const inner = document.createElement('div');
inner.className = 'fav-history-sidebar-inner';
const tabs = document.createElement('div');
tabs.className = 'fav-history-switch';
if (hasFavFeature) {
const btnFav = document.createElement('button');
btnFav.className = 'fav-history-tab' + (activeType === 'favorites' ? ' active' : '');
btnFav.textContent = `⭐ Favoriten (${favorites.length})`;
btnFav.onclick = () => {
sidebarMode = 'favorites';
renderFavHistorySidebar(allRepos);
};
tabs.appendChild(btnFav);
}
if (hasRecFeature) {
const btnRec = document.createElement('button');
btnRec.className = 'fav-history-tab' + (activeType === 'recent' ? ' active' : '');
btnRec.textContent = `🕐 Verlauf (${recentRepos.length})`;
btnRec.onclick = () => {
sidebarMode = 'recent';
renderFavHistorySidebar(allRepos);
};
tabs.appendChild(btnRec);
}
const list = document.createElement('div');
list.className = 'fav-history-list';
const visibleItems = activeType === 'favorites' ? items : items.slice(0, 30);
if (visibleItems.length === 0) {
const empty = document.createElement('div');
empty.className = 'fav-history-empty';
empty.textContent = activeType === 'favorites'
? 'Noch keine Favoriten markiert.'
: 'Noch kein Verlauf vorhanden.';
list.appendChild(empty);
} else {
visibleItems.forEach((entry) => {
const itemBtn = document.createElement('button');
itemBtn.className = 'fav-history-item';
itemBtn.type = 'button';
const name = document.createElement('span');
name.className = 'fav-history-item-name';
name.textContent = entry.repo || '-';
const meta = document.createElement('span');
meta.className = 'fav-history-item-meta';
if (activeType === 'recent' && entry.openedAt) {
meta.textContent = `${entry.owner || '-'}${formatRelDate(entry.openedAt)}`;
} else {
meta.textContent = entry.owner || '-';
}
itemBtn.appendChild(name);
itemBtn.appendChild(meta);
itemBtn.onclick = () => {
addToRecent(entry.owner, entry.repo, entry.cloneUrl);
loadRepoContents(entry.owner, entry.repo, '');
};
itemBtn.oncontextmenu = (ev) => {
ev.preventDefault();
ev.stopPropagation();
showChipContextMenu(ev, entry, activeType === 'favorites' ? 'favorite' : 'recent');
};
list.appendChild(itemBtn);
});
}
inner.appendChild(tabs);
inner.appendChild(list);
sidebar.innerHTML = '';
sidebar.appendChild(inner);
}
function refreshFavHistoryUi() {
renderFavHistorySidebar(currentGiteaRepos);
// Stern-Buttons im Grid aktualisieren
document.querySelectorAll('.fav-star-btn').forEach(btn => {
const card = btn.closest('.item-card');
if (!card) return;
const owner = card.dataset.owner;
const repo = card.dataset.repo;
if (!owner || !repo) return;
const active = isFavorite(owner, repo);
btn.classList.toggle('active', active);
btn.textContent = active ? '⭐' : '☆';
btn.title = active ? 'Aus Favoriten entfernen' : 'Zu Favoriten hinzufügen';
});
}
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: ''
};
const MAX_ACTIVITY_ITEMS = 300;
let activityEntries = [];
let retryQueueCount = 0;
const HEATMAP_CACHE_MS = 5 * 60 * 1000;
let remoteHeatmapFetchState = 'idle'; // idle | ok | error
let remoteHeatmapFetchedAt = 0;
let remoteHeatmapCounts = new Map();
let remoteHeatmapUsername = '';
function logActivity(level, message) {
const entry = {
id: `${Date.now()}-${Math.random().toString(16).slice(2)}`,
level: level || 'info',
message: String(message || ''),
ts: new Date().toISOString()
};
activityEntries.unshift(entry);
if (activityEntries.length > MAX_ACTIVITY_ITEMS) {
activityEntries = activityEntries.slice(0, MAX_ACTIVITY_ITEMS);
}
renderActivityLog();
refreshActivityHeatmapIfVisible();
}
function refreshActivityHeatmapIfVisible() {
const host = $('repoActivityHeatmapHost');
if (!host) return;
renderActivityHeatmap(host);
}
function normalizeHeatmapEntryDate(value) {
if (value == null) return null;
if (typeof value === 'string') {
const m = value.match(/^(\d{4}-\d{2}-\d{2})/);
if (m) return m[1];
const d = new Date(value);
if (!Number.isNaN(d.getTime())) return formatDateKey(d);
return null;
}
if (typeof value === 'number' && Number.isFinite(value)) {
const ms = value < 1e12 ? value * 1000 : value;
const d = new Date(ms);
if (Number.isNaN(d.getTime())) return null;
return formatDateKey(d);
}
if (value instanceof Date && !Number.isNaN(value.getTime())) {
return formatDateKey(value);
}
return null;
}
function setRemoteHeatmapEntries(entries) {
const next = new Map();
if (Array.isArray(entries)) {
for (const item of entries) {
if (!item) continue;
const date = normalizeHeatmapEntryDate(item.date || item.day || item.timestamp || item.ts);
if (!date) continue;
const countNum = Number(item.count ?? item.value ?? item.contributions ?? 0);
const count = Number.isFinite(countNum) ? Math.max(0, Math.floor(countNum)) : 0;
next.set(date, (next.get(date) || 0) + count);
}
}
remoteHeatmapCounts = next;
}
async function loadRemoteHeatmapData(force = false) {
const now = Date.now();
if (!force && remoteHeatmapFetchState === 'ok' && (now - remoteHeatmapFetchedAt) < HEATMAP_CACHE_MS) {
return;
}
if (!window.electronAPI || typeof window.electronAPI.getGiteaUserHeatmap !== 'function') {
remoteHeatmapFetchState = 'error';
return;
}
try {
const res = await window.electronAPI.getGiteaUserHeatmap();
if (res && res.ok) {
remoteHeatmapUsername = res.username || '';
setRemoteHeatmapEntries(res.entries || []);
remoteHeatmapFetchedAt = now;
remoteHeatmapFetchState = 'ok';
return;
}
remoteHeatmapFetchState = 'error';
} catch (_) {
remoteHeatmapFetchState = 'error';
}
}
function formatDateKey(date) {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
const d = String(date.getDate()).padStart(2, '0');
return `${y}-${m}-${d}`;
}
function startOfDay(date) {
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
}
function addDays(date, days) {
const n = new Date(date);
n.setDate(n.getDate() + days);
return n;
}
function shiftMonths(date, deltaMonths) {
const d = new Date(date.getFullYear(), date.getMonth(), date.getDate());
d.setMonth(d.getMonth() + deltaMonths);
return d;
}
function buildHeatmapData(monthsBack = activityHeatmapRangeMonths) {
const today = startOfDay(new Date());
const from = startOfDay(shiftMonths(today, -(Math.max(1, Number(monthsBack) || 6))));
const fromDow = (from.getDay() + 6) % 7; // Monday=0
const gridStart = addDays(from, -fromDow);
const counts = new Map();
let source = 'local';
if (remoteHeatmapFetchState === 'ok') {
source = 'remote';
for (const [key, count] of remoteHeatmapCounts.entries()) {
const day = startOfDay(new Date(key));
if (Number.isNaN(day.getTime()) || day < from || day > today) continue;
counts.set(key, Math.max(0, Number(count) || 0));
}
} else {
for (const entry of activityEntries) {
const ts = entry && entry.ts ? new Date(entry.ts) : null;
if (!ts || Number.isNaN(ts.getTime())) continue;
const day = startOfDay(ts);
if (day < from || day > today) continue;
const key = formatDateKey(day);
counts.set(key, (counts.get(key) || 0) + 1);
}
}
const totalDays = Math.floor((today - gridStart) / 86400000) + 1;
const weekCount = Math.ceil(totalDays / 7);
const weeks = [];
let maxCount = 0;
let total = 0;
for (let w = 0; w < weekCount; w++) {
const week = [];
for (let d = 0; d < 7; d++) {
const date = addDays(gridStart, w * 7 + d);
const inRange = date >= from && date <= today;
const key = formatDateKey(date);
const count = inRange ? (counts.get(key) || 0) : 0;
if (count > maxCount) maxCount = count;
total += count;
week.push({ date, inRange, count });
}
weeks.push(week);
}
return { weeks, total, maxCount, source };
}
function heatmapLevel(count, max) {
if (!count) return 0;
const q1 = Math.max(1, Math.ceil(max * 0.25));
const q2 = Math.max(q1 + 1, Math.ceil(max * 0.5));
const q3 = Math.max(q2 + 1, Math.ceil(max * 0.75));
if (count <= q1) return 1;
if (count <= q2) return 2;
if (count <= q3) return 3;
return 4;
}
function renderActivityHeatmap(host) {
if (!host) return;
host.innerHTML = '';
const { weeks, total, maxCount, source } = buildHeatmapData(activityHeatmapRangeMonths);
const card = document.createElement('section');
card.className = 'activity-heatmap-card' + (activityHeatmapCollapsed ? ' collapsed' : '');
const header = document.createElement('div');
header.className = 'activity-heatmap-header';
const title = document.createElement('strong');
title.textContent = 'Aktivitäts-Heatmap';
const toggleBtn = document.createElement('button');
toggleBtn.className = 'activity-heatmap-toggle';
toggleBtn.textContent = activityHeatmapCollapsed ? '▸ Ausklappen' : '▾ Einklappen';
toggleBtn.onclick = () => {
activityHeatmapCollapsed = !activityHeatmapCollapsed;
renderActivityHeatmap(host);
};
const controls = document.createElement('div');
controls.className = 'activity-heatmap-controls';
controls.appendChild(toggleBtn);
header.appendChild(title);
header.appendChild(controls);
card.appendChild(header);
const body = document.createElement('div');
body.className = 'activity-heatmap-body';
const months = ['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'];
const monthsRow = document.createElement('div');
monthsRow.className = 'activity-heatmap-months';
monthsRow.style.setProperty('--hm-weeks', String(weeks.length));
let prevMonth = -1;
weeks.forEach((week, idx) => {
const firstInRange = week.find(day => day.inRange);
if (!firstInRange) return;
const month = firstInRange.date.getMonth();
if (month !== prevMonth) {
const label = document.createElement('span');
label.textContent = months[month];
label.style.gridColumn = `${idx + 1} / span 2`;
monthsRow.appendChild(label);
prevMonth = month;
}
});
const gridWrap = document.createElement('div');
gridWrap.className = 'activity-heatmap-grid-wrap';
const weekdays = document.createElement('div');
weekdays.className = 'activity-heatmap-weekdays';
['Mo', '', 'Mi', '', 'Fr', '', ''].forEach(txt => {
const el = document.createElement('span');
el.textContent = txt;
weekdays.appendChild(el);
});
const grid = document.createElement('div');
grid.className = 'activity-heatmap-grid';
grid.style.setProperty('--hm-weeks', String(weeks.length));
weeks.forEach(week => {
const col = document.createElement('div');
col.className = 'activity-heatmap-week';
week.forEach(day => {
const cell = document.createElement('div');
const level = day.inRange ? heatmapLevel(day.count, maxCount || 1) : 0;
cell.className = `activity-heatmap-cell lv${level}` + (day.inRange ? '' : ' out');
cell.title = day.inRange
? `${day.count} Aktivitäten am ${day.date.toLocaleDateString('de-DE')}`
: '';
col.appendChild(cell);
});
grid.appendChild(col);
});
gridWrap.appendChild(weekdays);
gridWrap.appendChild(grid);
const footer = document.createElement('div');
footer.className = 'activity-heatmap-footer';
const summary = document.createElement('span');
if (source === 'remote') {
summary.textContent = `${total.toLocaleString('de-DE')} Beiträge vom Git-Profil in den letzten ${activityHeatmapRangeMonths} Monaten${remoteHeatmapUsername ? ` (${remoteHeatmapUsername})` : ''}`;
} else {
summary.textContent = `${total.toLocaleString('de-DE')} lokale Einträge in den letzten ${activityHeatmapRangeMonths} Monaten`;
}
const legend = document.createElement('div');
legend.className = 'activity-heatmap-legend';
legend.innerHTML = `
<span>Weniger</span>
<i class="activity-heatmap-cell lv0"></i>
<i class="activity-heatmap-cell lv1"></i>
<i class="activity-heatmap-cell lv2"></i>
<i class="activity-heatmap-cell lv3"></i>
<i class="activity-heatmap-cell lv4"></i>
<span>Mehr</span>
`;
footer.appendChild(summary);
footer.appendChild(legend);
body.appendChild(monthsRow);
body.appendChild(gridWrap);
body.appendChild(footer);
card.appendChild(body);
host.appendChild(card);
}
function formatActivityTimestamp(iso) {
try {
return new Date(iso).toLocaleTimeString('de-DE', { hour12: false });
} catch (_) {
return '--:--:--';
}
}
function renderActivityLog() {
const list = $('activityLogList');
if (!list) return;
const filter = ($('activityFilterLevel') && $('activityFilterLevel').value) || 'all';
const visible = activityEntries.filter(e => filter === 'all' || e.level === filter);
if (visible.length === 0) {
list.innerHTML = '<div class="activity-log-item info"><span class="activity-log-message">Noch keine Einträge.</span></div>';
return;
}
list.innerHTML = visible.map(e => {
const lvl = (e.level || 'info').toUpperCase();
return `
<div class="activity-log-item ${e.level}">
<span class="activity-log-time">${formatActivityTimestamp(e.ts)}</span>
<span class="activity-log-level">${lvl}</span>
<span class="activity-log-message">${escapeHtml(e.message)}</span>
</div>
`;
}).join('');
}
function updateRetryQueueBadge(count) {
retryQueueCount = Math.max(0, Number(count || 0));
const btn = $('btnRetryQueueNow');
if (btn) btn.textContent = `🔁 Queue (${retryQueueCount})`;
const info = $('activityQueueInfo');
if (info) info.textContent = `Retry-Queue: ${retryQueueCount}`;
}
function parseBatchRepoInput(raw) {
return String(raw || '')
.split(/\r?\n/)
.map(line => line.trim())
.filter(Boolean)
.filter(line => line.includes('/'));
}
function renderInlineHint(id, text, tone = 'muted') {
const el = $(id);
if (!el) return;
el.textContent = text || '';
el.classList.remove('error', 'success', 'warn');
if (tone === 'error') el.classList.add('error');
if (tone === 'success') el.classList.add('success');
if (tone === 'warn') el.classList.add('warn');
}
function isRepoNameFormatValid(name) {
return /^[a-zA-Z0-9](?:[a-zA-Z0-9._-]{0,98}[a-zA-Z0-9])?$/.test(String(name || '').trim());
}
function findSimilarRepoNamesLocally(name) {
const target = String(name || '').toLowerCase();
const normalizedTarget = target.replace(/[\s._-]+/g, '');
const names = (currentGiteaRepos || [])
.map(r => String(r && r.name || '').trim())
.filter(Boolean);
const exact = names.some(n => n.toLowerCase() === target);
const similar = names
.filter(n => {
const lower = n.toLowerCase();
const normalized = lower.replace(/[\s._-]+/g, '');
return lower.includes(target) || target.includes(lower) || normalized.includes(normalizedTarget) || normalizedTarget.includes(normalized);
})
.filter(n => n.toLowerCase() !== target)
.slice(0, 8);
return { exact, similar };
}
async function validateRepoNameLive(name) {
const value = String(name || '').trim();
if (!value) {
renderInlineHint('repoNameValidationHint', 'Name prüfen: Duplikate, ähnliche Namen und ungültige Zeichen werden erkannt.', 'muted');
return { ok: true, blocking: false, existsExact: false, similar: [] };
}
if (!isRepoNameFormatValid(value)) {
renderInlineHint('repoNameValidationHint', 'Ungültiger Name. Erlaubt: Buchstaben, Zahlen, Punkt, Unterstrich, Bindestrich (1-100 Zeichen).', 'error');
return { ok: true, blocking: true, existsExact: false, similar: [] };
}
let checkedRemotely = false;
let existsExact = false;
let similar = [];
if (window.electronAPI.validateRepoName) {
try {
const platform = $('platform')?.value || 'gitea';
const res = await window.electronAPI.validateRepoName({ name: value, platform });
if (res && res.ok) {
checkedRemotely = !!res.checked;
existsExact = !!res.existsExact;
similar = Array.isArray(res.similar) ? res.similar : [];
}
} catch (_) {}
}
if (!checkedRemotely) {
const local = findSimilarRepoNamesLocally(value);
existsExact = local.exact;
similar = local.similar;
}
if (existsExact) {
renderInlineHint('repoNameValidationHint', 'Dieses Repository existiert bereits. Bitte einen anderen Namen wählen.', 'error');
return { ok: true, blocking: true, existsExact, similar };
}
if (similar.length > 0) {
renderInlineHint('repoNameValidationHint', `Ähnliche Namen gefunden: ${similar.slice(0, 3).join(', ')}`, 'warn');
return { ok: true, blocking: false, existsExact, similar };
}
renderInlineHint('repoNameValidationHint', 'Name ist frei und valide.', 'success');
return { ok: true, blocking: false, existsExact, similar };
}
function scheduleRepoNameValidation() {
if (repoNameValidationTimer) clearTimeout(repoNameValidationTimer);
repoNameValidationTimer = setTimeout(() => {
validateRepoNameLive($('repoName')?.value || '');
}, 180);
}
async function validateBatchCloneCollisions(strict = false) {
const action = $('batchActionType')?.value || 'refresh';
if (action !== 'clone') {
renderInlineHint('batchCloneValidationHint', 'Kollisionsprüfung aktiv: vorhandene Zielordner und Namenskonflikte werden angezeigt.', 'muted');
return true;
}
const repos = parseBatchRepoInput($('batchRepoList')?.value || '');
const targetDir = $('batchCloneTarget')?.value || '';
if (repos.length === 0) {
renderInlineHint('batchCloneValidationHint', 'Keine Repositories eingetragen (Format: owner/repo).', strict ? 'error' : 'warn');
return !strict;
}
if (!targetDir) {
renderInlineHint('batchCloneValidationHint', 'Bitte Zielordner wählen, um Kollisionen zu prüfen.', strict ? 'error' : 'warn');
return !strict;
}
if (!window.electronAPI.checkCloneTargetCollisions) {
renderInlineHint('batchCloneValidationHint', 'Kollisionsprüfung nicht verfügbar.', strict ? 'error' : 'warn');
return !strict;
}
try {
const res = await window.electronAPI.checkCloneTargetCollisions({ targetDir, repos });
if (!res || !res.ok) {
renderInlineHint('batchCloneValidationHint', 'Kollisionsprüfung fehlgeschlagen.', strict ? 'error' : 'warn');
return !strict;
}
const dupes = Array.isArray(res.duplicateRepoNames) ? res.duplicateRepoNames : [];
const existing = Array.isArray(res.existingTargets) ? res.existingTargets : [];
if (dupes.length > 0 || existing.length > 0) {
const duplicateText = dupes.length > 0 ? `Doppelte Repo-Namen: ${dupes.slice(0, 3).join(', ')}` : '';
const existingText = existing.length > 0 ? `Vorhandene Zielordner: ${existing.slice(0, 2).join(' | ')}` : '';
const joined = [duplicateText, existingText].filter(Boolean).join(' • ');
renderInlineHint('batchCloneValidationHint', joined || 'Kollision erkannt.', 'error');
return false;
}
renderInlineHint('batchCloneValidationHint', 'Keine Kollisionen gefunden. Clone-Ziel ist sauber.', 'success');
return true;
} catch (_) {
renderInlineHint('batchCloneValidationHint', 'Kollisionsprüfung fehlgeschlagen.', strict ? 'error' : 'warn');
return !strict;
}
}
function scheduleBatchCloneValidation() {
if (batchCloneValidationTimer) clearTimeout(batchCloneValidationTimer);
batchCloneValidationTimer = setTimeout(() => {
validateBatchCloneCollisions(false);
}, 220);
}
function updateBatchActionFields() {
const action = $('batchActionType')?.value || 'refresh';
const cloneGroup = $('batchCloneGroup');
const tagGroup = $('batchTagGroup');
const releaseNameGroup = $('batchReleaseNameGroup');
const releaseBodyGroup = $('batchReleaseBodyGroup');
if (cloneGroup) cloneGroup.classList.toggle('hidden', action !== 'clone');
if (tagGroup) tagGroup.classList.toggle('hidden', !(action === 'create-tag' || action === 'create-release'));
if (releaseNameGroup) releaseNameGroup.classList.toggle('hidden', action !== 'create-release');
if (releaseBodyGroup) releaseBodyGroup.classList.toggle('hidden', !(action === 'create-tag' || action === 'create-release'));
}
// 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;
let repoSearchHotkeyBound = false;
let settingsHealth = {
url: 'Unbekannt',
api: 'Unbekannt',
auth: 'Unbekannt',
latency: '-',
version: '-',
lastError: '-'
};
function setHealthField(id, value) {
const el = $(id);
if (!el) return;
el.textContent = value;
el.classList.remove('health-ok', 'health-warn', 'health-error');
const v = (value || '').toLowerCase();
if (v === 'ok' || v === 'erreichbar' || v === 'gueltig' || v === 'gültig') {
el.classList.add('health-ok');
} else if (v === 'fehler' || v === 'ungueltig' || v === 'ungültig') {
el.classList.add('health-error');
} else if (v === 'unbekannt' || v === 'kein token' || v === 'token vorhanden' || v === 'nicht konfiguriert') {
el.classList.add('health-warn');
}
}
function renderSettingsHealth() {
setHealthField('healthUrl', settingsHealth.url);
setHealthField('healthApi', settingsHealth.api);
setHealthField('healthAuth', settingsHealth.auth);
setHealthField('healthLatency', settingsHealth.latency);
setHealthField('healthVersion', settingsHealth.version);
setHealthField('healthLastError', settingsHealth.lastError);
}
function syncSettingsPanelHeights() {
const credentialsPanel = document.querySelector('#settingsModal .settings-panel--credentials');
const healthPanel = document.querySelector('#settingsModal .settings-panel--health');
if (!credentialsPanel || !healthPanel) return;
healthPanel.style.minHeight = '';
// In der einspaltigen Ansicht sollen die Karten natuerlich fliessen.
if (window.matchMedia('(max-width: 1120px)').matches) return;
const targetHeight = Math.ceil(credentialsPanel.getBoundingClientRect().height);
if (targetHeight > 0) {
healthPanel.style.minHeight = `${targetHeight}px`;
}
}
function updateSettingsHealth(patch) {
settingsHealth = { ...settingsHealth, ...patch };
renderSettingsHealth();
syncSettingsPanelHeights();
}
function normalizeAndValidateGiteaUrl(rawUrl) {
const value = (rawUrl || '').trim();
if (!value) return { ok: true, value: '' };
let parsed;
try {
parsed = new URL(value);
} catch (_) {
return {
ok: false,
error: 'Ungültige URL. Beispiel für IPv6: http://[2001:db8::1]:3000'
};
}
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
return {
ok: false,
error: 'URL muss mit http:// oder https:// beginnen.'
};
}
return { ok: true, value: value.replace(/\/$/, '') };
}
function renderGiteaUrlHint(rawValue) {
const hint = $('giteaUrlHint');
if (!hint) return;
const result = normalizeAndValidateGiteaUrl(rawValue);
if (!rawValue || !rawValue.trim()) {
hint.className = 'settings-inline-hint';
hint.textContent = 'Hinweis: IPv6 mit Klammern eingeben, z.B. http://[2001:db8::1]:3000';
return;
}
if (!result.ok) {
hint.className = 'settings-inline-hint error';
hint.textContent = result.error;
return;
}
hint.className = 'settings-inline-hint success';
hint.textContent = `Gültige URL: ${result.value}`;
}
function mapErrorMessage(message) {
const raw = String(message || '').toLowerCase();
if (!raw) return 'Unbekannter Fehler';
if (raw.includes('401') || raw.includes('unauthorized') || raw.includes('authentifizierung')) {
return 'Authentifizierung fehlgeschlagen. Bitte Token prüfen.';
}
if (raw.includes('403') || raw.includes('forbidden') || raw.includes('zugriff verweigert')) {
return 'Zugriff verweigert. Bitte Token-Berechtigungen prüfen.';
}
if (raw.includes('404') || raw.includes('not found') || raw.includes('nicht gefunden')) {
return 'Server oder Ressource nicht gefunden. URL/Repo prüfen.';
}
if (raw.includes('econnrefused') || raw.includes('enotfound') || raw.includes('eai_again') || raw.includes('getaddrinfo')) {
return 'Server nicht erreichbar. DNS, IPv4/IPv6 und Port prüfen.';
}
if (raw.includes('timeout') || raw.includes('econnaborted') || raw.includes('zeitueberschreitung') || raw.includes('zeitüberschreitung')) {
return 'Zeitüberschreitung bei der Verbindung. Bitte erneut versuchen.';
}
if (raw.includes('ungueltige') || raw.includes('ungültige') || raw.includes('invalid') || raw.includes('url')) {
return 'Ungültige URL. Beispiel für IPv6: http://[2001:db8::1]:3000';
}
return String(message);
}
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) {
const friendly = mapErrorMessage(msg);
updateSettingsHealth({ lastError: friendly });
setStatus(friendly);
showToast(friendly, 'error');
logActivity('error', friendly);
}
function showSuccess(msg) { setStatus(msg); showToast(msg, 'success', 3000); logActivity('info', msg); }
function showWarning(msg) { setStatus(msg); showToast(msg, 'warning'); logActivity('warning', msg); }
function showInfo(msg) { setStatus(msg); showToast(msg, 'info', 2500); }
function normalizeSearchText(value) {
return String(value || '')
.toLowerCase()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '');
}
function fuzzyScoreToken(token, text) {
if (!token || !text) return 0;
const t = normalizeSearchText(text);
const q = normalizeSearchText(token);
if (!q) return 0;
// Direkter Treffer bekommt starken Score
const idx = t.indexOf(q);
if (idx >= 0) {
let score = 120 - Math.min(idx, 60);
if (t.startsWith(q)) score += 20;
return score;
}
// Fuzzy: alle Zeichen in Reihenfolge vorhanden
let ti = 0;
let gaps = 0;
for (let qi = 0; qi < q.length; qi++) {
const ch = q[qi];
const found = t.indexOf(ch, ti);
if (found === -1) return 0;
if (found > ti) gaps += (found - ti);
ti = found + 1;
}
return Math.max(8, 80 - gaps * 2);
}
function getRepoCardSearchScore(card, query) {
const raw = normalizeSearchText(query).trim();
if (!raw) return 1;
const tokens = raw.split(/\s+/).filter(Boolean);
const fields = [
card.dataset.searchName || '',
card.dataset.searchOwner || '',
card.dataset.searchFull || '',
card.dataset.searchLanguage || '',
card.dataset.searchTopics || '',
card.dataset.searchDescription || ''
];
let total = 0;
for (const token of tokens) {
let best = 0;
for (const field of fields) {
best = Math.max(best, fuzzyScoreToken(token, field));
}
if (best === 0) return 0;
total += best;
}
return total;
}
function applyRepoFuzzyFilter(grid, searchInput, searchMetaEl) {
if (!grid) return;
const cards = Array.from(grid.querySelectorAll('.item-card'));
const query = (searchInput?.value || '').trim();
if (!query) {
cards.forEach(card => {
card.style.display = 'flex';
card.style.order = '';
});
if (searchMetaEl) {
searchMetaEl.textContent = `${cards.length} Repositories`;
}
return;
}
const ranked = cards
.map(card => ({ card, score: getRepoCardSearchScore(card, query) }))
.filter(entry => entry.score > 0)
.sort((a, b) => b.score - a.score || a.card.dataset.searchName.localeCompare(b.card.dataset.searchName));
const visibleSet = new Set(ranked.map(entry => entry.card));
cards.forEach(card => {
card.style.display = visibleSet.has(card) ? 'flex' : 'none';
});
ranked.forEach(entry => grid.appendChild(entry.card));
if (searchMetaEl) {
const label = ranked.length === 1 ? 'Treffer' : 'Treffer';
searchMetaEl.textContent = `${ranked.length} ${label} für "${query}"`;
}
}
function buildRepoWebUrl(owner, repoName) {
const urlInput = $('giteaURL');
const normalized = normalizeAndValidateGiteaUrl(urlInput?.value || '');
if (!normalized.ok || !normalized.value) return null;
return `${normalized.value}/${encodeURIComponent(owner)}/${encodeURIComponent(repoName)}`;
}
// 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 && !response.queued) {
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 if (response.ok && response.queued) {
tab.originalContent = content;
tab.dirty = false;
pushToHistory(content);
renderTabs();
showWarning(response.message || 'Änderung in Retry-Queue gelegt und wird später hochgeladen.');
updateRetryQueueBadge(retryQueueCount + 1);
} 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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
// 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...');
updateSettingsHealth({ lastError: '-' });
try {
const res = await window.electronAPI.listGiteaRepos();
if (!res.ok) {
showError('Failed to load repos: ' + (res.error || 'Unknown error'));
updateSettingsHealth({ api: 'Fehler', auth: 'Fehler' });
return;
}
updateSettingsHealth({ api: 'Erreichbar', auth: 'OK', lastError: '-' });
currentGiteaRepos = Array.isArray(res.repos) ? res.repos : [];
const grid = $('explorerGrid');
if (!grid) return;
grid.innerHTML = '';
if (!res.repos || res.repos.length === 0) {
renderFavHistorySidebar([]);
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;
}
// --- Fuzzy-Suchfeld für Repositories ---
const searchContainer = document.createElement('div');
searchContainer.className = 'repo-search-wrap';
searchContainer.style.cssText = 'grid-column: 1/-1; margin-bottom: 20px;';
const searchTop = document.createElement('div');
searchTop.className = 'repo-search-top';
const searchInput = document.createElement('input');
searchInput.type = 'text';
searchInput.placeholder = '🔍 Fuzzy-Suche: Name, Owner, Sprache, Topics...';
searchInput.className = 'repo-search-input';
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;
`;
const searchClearBtn = document.createElement('button');
searchClearBtn.className = 'secondary repo-search-clear';
searchClearBtn.textContent = '✕';
searchClearBtn.title = 'Suche leeren';
searchClearBtn.onclick = () => {
searchInput.value = '';
applyRepoFuzzyFilter(grid, searchInput, null);
searchInput.focus();
};
// Search Focus Effekt
searchInput.addEventListener('focus', () => {
searchInput.style.borderColor = 'var(--accent-primary)';
});
searchInput.addEventListener('blur', () => {
searchInput.style.borderColor = 'rgba(255, 255, 255, 0.1)';
});
searchTop.appendChild(searchInput);
searchTop.appendChild(searchClearBtn);
searchContainer.appendChild(searchTop);
grid.appendChild(searchContainer);
const heatmapHost = document.createElement('div');
heatmapHost.id = 'repoActivityHeatmapHost';
heatmapHost.style.cssText = 'grid-column: 1/-1; margin-bottom: 18px;';
grid.appendChild(heatmapHost);
await loadRemoteHeatmapData();
renderActivityHeatmap(heatmapHost);
// Fuzzy Search Logic
searchInput.addEventListener('input', (e) => {
applyRepoFuzzyFilter(grid, e.target, null);
});
searchInput.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
searchInput.value = '';
applyRepoFuzzyFilter(grid, searchInput, null);
}
});
if (!repoSearchHotkeyBound) {
window.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'k') {
if (currentState.view !== 'gitea-list') return;
const input = document.querySelector('.repo-search-input');
if (!input) return;
e.preventDefault();
input.focus();
input.select();
}
});
repoSearchHotkeyBound = true;
}
// ── Sidebar links einblenden ──
renderFavHistorySidebar(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;
card.dataset.owner = owner;
card.dataset.repo = repoName;
card.dataset.searchName = repoName || '';
card.dataset.searchOwner = owner || '';
card.dataset.searchFull = `${owner || ''}/${repoName || ''}`;
card.dataset.searchLanguage = repo.language || '';
card.dataset.searchTopics = Array.isArray(repo.topics) ? repo.topics.join(' ') : '';
card.dataset.searchDescription = repo.description || '';
// 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);
});
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);
});
applyRepoFuzzyFilter(grid, searchInput, null);
setStatus(`Loaded ${res.repos.length} repos`);
} catch (error) {
console.error('Error loading repos:', error);
showError('Error loading repositories');
updateSettingsHealth({ api: 'Fehler', auth: 'Unbekannt' });
}
}
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;
};
const addSep = () => {
const sep = document.createElement('div');
sep.style.cssText = 'height:1px;background:rgba(255,255,255,0.08);margin:4px 0;';
menu.appendChild(sep);
};
menu.appendChild(createMenuItem('📂', 'Repository öffnen', () => {
menu.remove();
addToRecent(owner, repoName, cloneUrl);
loadRepoContents(owner, repoName, '');
}));
addSep();
menu.appendChild(createMenuItem('🔗', 'Clone-URL kopieren', async () => {
menu.remove();
const res = await window.electronAPI.copyToClipboard(cloneUrl || '');
if (res?.ok) showInfo('Clone-URL kopiert');
else showError('Clone-URL konnte nicht kopiert werden');
}));
menu.appendChild(createMenuItem('📋', 'owner/repo kopieren', async () => {
menu.remove();
const res = await window.electronAPI.copyToClipboard(`${owner}/${repoName}`);
if (res?.ok) showInfo('owner/repo kopiert');
else showError('owner/repo konnte nicht kopiert werden');
}));
menu.appendChild(createMenuItem('🌐 Im Browser öffnen', async () => {
menu.remove();
const url = buildRepoWebUrl(owner, repoName);
if (!url) {
showWarning('Gitea-URL fehlt oder ist ungültig. Bitte in den Settings prüfen.');
return;
}
const res = await window.electronAPI.openExternalUrl(url);
if (res?.ok) showInfo('Repository im Browser geöffnet');
else showError('Browser konnte nicht geöffnet werden');
}));
addSep();
// ── 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);
addSep();
}
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) {
showWarning('Repository-Name ist erforderlich.');
return;
}
const check = await validateRepoNameLive(name);
if (!check.ok || check.blocking) {
showError('Bitte den Repository-Namen korrigieren.');
return;
}
if (check.similar && check.similar.length > 0) {
const proceed = confirm(`Ähnliche Repository-Namen gefunden: ${check.similar.slice(0, 3).join(', ')}\n\nTrotzdem erstellen?`);
if (!proceed) 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 () => {
initializePlatformSelection();
// Favoriten & Verlauf vorladen
await loadFavoritesAndRecent();
renderSettingsHealth();
// 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 || '';
renderGiteaUrlHint(creds.giteaURL || '');
const checkedUrl = normalizeAndValidateGiteaUrl(creds.giteaURL || '');
updateSettingsHealth({
url: checkedUrl.ok && checkedUrl.value ? 'Gültig' : (checkedUrl.ok ? 'Leer' : 'Ungültig'),
api: creds.giteaURL ? 'Unbekannt' : 'Nicht konfiguriert',
auth: creds.giteaToken ? 'Token vorhanden' : 'Kein Token',
latency: '-',
version: '-'
});
// 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);
// Autostart-Status vom System lesen (Quelle der Wahrheit)
try {
const autostartRes = await window.electronAPI.getAutostart();
if (autostartRes && typeof autostartRes.enabled === 'boolean') {
featureAutostart = autostartRes.enabled;
}
} catch (_) {}
// 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;
const cbAutostart = $('settingAutostart');
if (cbAutostart) cbAutostart.checked = featureAutostart;
// 🆕 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');
renderGiteaUrlHint('');
updateSettingsHealth({
url: 'Nicht konfiguriert',
api: 'Nicht konfiguriert',
auth: 'Kein Token',
latency: '-',
version: '-',
lastError: '-'
});
}
} 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 ($('btnWinMinimize')) $('btnWinMinimize').onclick = () => window.electronAPI.windowMinimize();
if ($('btnWinMaximize')) $('btnWinMaximize').onclick = () => window.electronAPI.windowMaximize();
if ($('btnWinClose')) $('btnWinClose').onclick = () => window.electronAPI.windowClose();
if ($('btnSettings')) {
$('btnSettings').onclick = () => {
$('settingsModal').classList.remove('hidden');
$('settingsWatermarkCard')?.classList.add('hidden');
renderSettingsHealth();
requestAnimationFrame(syncSettingsPanelHeights);
};
}
if ($('btnSettingsWatermark') && $('settingsWatermarkCard')) {
$('btnSettingsWatermark').onclick = (e) => {
e.stopPropagation();
$('settingsWatermarkCard').classList.toggle('hidden');
};
$('settingsWatermarkCard').addEventListener('click', (e) => {
e.stopPropagation();
});
document.addEventListener('click', (e) => {
if ($('settingsModal')?.classList.contains('hidden')) return;
if ($('settingsWatermarkCard')?.classList.contains('hidden')) return;
const target = e.target;
if ($('btnSettingsWatermark')?.contains(target)) return;
if ($('settingsWatermarkCard')?.contains(target)) return;
$('settingsWatermarkCard')?.classList.add('hidden');
});
}
window.addEventListener('resize', syncSettingsPanelHeights);
if ($('btnBatchActions')) {
$('btnBatchActions').onclick = () => {
$('batchActionModal')?.classList.remove('hidden');
updateBatchActionFields();
scheduleBatchCloneValidation();
};
}
if ($('btnOpenActivityLog')) {
$('btnOpenActivityLog').onclick = () => {
$('activityLogModal')?.classList.remove('hidden');
renderActivityLog();
};
}
if ($('btnCloseActivityLog')) {
$('btnCloseActivityLog').onclick = () => $('activityLogModal')?.classList.add('hidden');
}
if ($('activityFilterLevel')) {
$('activityFilterLevel').addEventListener('change', renderActivityLog);
}
if ($('btnClearActivityLog')) {
$('btnClearActivityLog').onclick = () => {
activityEntries = [];
renderActivityLog();
refreshActivityHeatmapIfVisible();
};
}
if ($('btnCloseBatchAction')) {
$('btnCloseBatchAction').onclick = () => $('batchActionModal')?.classList.add('hidden');
}
if ($('batchActionType')) {
$('batchActionType').addEventListener('change', updateBatchActionFields);
$('batchActionType').addEventListener('change', scheduleBatchCloneValidation);
updateBatchActionFields();
}
if ($('repoName')) {
$('repoName').addEventListener('input', scheduleRepoNameValidation);
$('repoName').addEventListener('blur', () => validateRepoNameLive($('repoName')?.value || ''));
}
if ($('batchRepoList')) {
$('batchRepoList').addEventListener('input', scheduleBatchCloneValidation);
}
if ($('batchCloneTarget')) {
$('batchCloneTarget').addEventListener('input', scheduleBatchCloneValidation);
}
if ($('btnSelectBatchCloneTarget')) {
$('btnSelectBatchCloneTarget').onclick = async () => {
const folder = await window.electronAPI.selectFolder();
if (folder && $('batchCloneTarget')) {
$('batchCloneTarget').value = folder;
scheduleBatchCloneValidation();
}
};
}
if ($('btnRunBatchAction')) {
$('btnRunBatchAction').onclick = async () => {
const action = $('batchActionType')?.value || 'refresh';
const repos = parseBatchRepoInput($('batchRepoList')?.value || '');
if (repos.length === 0) {
showWarning('Bitte mindestens ein Repository im Format owner/repo eintragen.');
return;
}
const options = {
cloneTargetDir: $('batchCloneTarget')?.value || '',
tag: $('batchTagName')?.value || '',
name: $('batchReleaseName')?.value || '',
body: $('batchReleaseBody')?.value || ''
};
if (action === 'clone' && !options.cloneTargetDir) {
showWarning('Bitte zuerst einen Zielordner für Clone wählen.');
return;
}
if (action === 'clone') {
const safeToClone = await validateBatchCloneCollisions(true);
if (!safeToClone) {
showError('Clone abgebrochen: Zielordner-Konflikt erkannt.');
return;
}
}
if ((action === 'create-tag' || action === 'create-release') && !String(options.tag).trim()) {
showWarning('Bitte einen Tag eintragen.');
return;
}
const btn = $('btnRunBatchAction');
const old = btn.textContent;
btn.disabled = true;
btn.textContent = 'Läuft...';
logActivity('info', `Batch gestartet: ${action} (${repos.length} Repos)`);
try {
const res = await window.electronAPI.runBatchRepoAction({ action, repos, options });
if (!res.ok) {
showError(res.error || 'Batch-Aktion fehlgeschlagen');
return;
}
const summary = res.summary || { total: repos.length, success: 0, failed: 0 };
if (summary.failed > 0) {
showWarning(`Batch beendet: ${summary.success}/${summary.total} erfolgreich, ${summary.failed} fehlgeschlagen.`);
} else {
showSuccess(`Batch erfolgreich: ${summary.success}/${summary.total}`);
}
(res.results || []).forEach(r => {
if (r.ok) logActivity('info', `${r.repo}: ${r.message || 'OK'}`);
else logActivity('error', `${r.repo}: ${r.error || 'Fehler'}`);
});
} catch (error) {
showError(error && error.message ? error.message : String(error));
} finally {
btn.disabled = false;
btn.textContent = old;
}
};
}
if ($('btnRetryQueueNow')) {
$('btnRetryQueueNow').onclick = async () => {
try {
const res = await window.electronAPI.processRetryQueueNow();
if (res.ok) {
showSuccess(`Queue verarbeitet: ${res.succeeded || 0} erfolgreich, ${res.failed || 0} verworfen.`);
} else {
showWarning(res.error || 'Queue konnte nicht verarbeitet werden');
}
} catch (e) {
showError(e && e.message ? e.message : String(e));
}
};
}
if ($('btnRetryQueueRefresh')) {
$('btnRetryQueueRefresh').onclick = async () => {
try {
const res = await window.electronAPI.processRetryQueueNow();
if (res.ok) {
showSuccess(`Queue verarbeitet: ${res.succeeded || 0} erfolgreich, ${res.failed || 0} verworfen.`);
} else {
showWarning(res.error || 'Queue konnte nicht verarbeitet werden');
}
} catch (e) {
showError(e && e.message ? e.message : String(e));
}
};
}
if ($('giteaURL')) {
$('giteaURL').addEventListener('input', (e) => {
const raw = e.target.value;
renderGiteaUrlHint(raw);
const checked = normalizeAndValidateGiteaUrl(raw);
updateSettingsHealth({
url: checked.ok && checked.value ? 'Gültig' : (checked.ok ? 'Leer' : 'Ungültig')
});
});
}
if ($('btnTestGiteaConnection')) {
$('btnTestGiteaConnection').onclick = async () => {
const token = $('giteaToken')?.value || '';
const rawUrl = $('giteaURL')?.value || '';
const checked = normalizeAndValidateGiteaUrl(rawUrl);
if (!checked.ok) {
showError(checked.error);
updateSettingsHealth({ url: 'Ungültig', api: 'Unbekannt', auth: token ? 'Token vorhanden' : 'Kein Token' });
return;
}
if (!checked.value) {
showWarning('Bitte zuerst eine Gitea URL eintragen.');
return;
}
setStatus('Teste Gitea-Verbindung...');
const btn = $('btnTestGiteaConnection');
const oldText = btn.textContent;
btn.disabled = true;
btn.textContent = 'Teste...';
try {
const res = await window.electronAPI.testGiteaConnection({
token,
url: checked.value,
timeout: 8000
});
if (!res.ok) {
showError(res.error || 'Verbindungstest fehlgeschlagen');
updateSettingsHealth({
url: 'Gültig',
api: 'Fehler',
auth: token ? 'Fehler' : 'Kein Token',
latency: '-',
version: '-'
});
return;
}
const result = res.result || {};
const checks = result.checks || {};
const metrics = result.metrics || {};
const server = result.server || {};
updateSettingsHealth({
url: 'Gültig',
api: checks.apiReachable ? 'Erreichbar' : 'Fehler',
auth: checks.authProvided ? (checks.authOk ? 'OK' : 'Fehler') : 'Kein Token',
latency: metrics.latencyMs ? `${metrics.latencyMs} ms` : '-',
version: server.version || '-',
lastError: '-'
});
if (result.ok) showSuccess('Verbindung erfolgreich getestet');
else showWarning('Server erreichbar, aber Auth/Teilcheck fehlgeschlagen');
} catch (error) {
console.error('test-gitea-connection error:', error);
showError(error && error.message ? error.message : String(error));
} finally {
btn.disabled = false;
btn.textContent = oldText;
}
};
}
if ($('btnCloseSettings')) {
$('btnCloseSettings').onclick = () => {
$('settingsWatermarkCard')?.classList.add('hidden');
$('settingsModal').classList.add('hidden');
};
}
if ($('btnOpenRepoActions')) {
$('btnOpenRepoActions').onclick = () => {
$('repoActionModal').classList.remove('hidden');
scheduleRepoNameValidation();
};
}
if ($('btnCloseRepoActions')) {
$('btnCloseRepoActions').onclick = () => {
$('repoActionModal').classList.add('hidden');
};
}
try {
const queue = await window.electronAPI.getRetryQueue();
if (queue && queue.ok) {
updateRetryQueueBadge(queue.size || 0);
logActivity('info', `Retry-Queue geladen (${queue.size || 0} Einträge)`);
}
} catch (_) {}
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 cbAutostart2 = $('settingAutostart');
const newAutostart = cbAutostart2 ? cbAutostart2.checked : false;
if (newAutostart !== featureAutostart) {
featureAutostart = newAutostart;
await window.electronAPI.setAutostart(featureAutostart);
}
const checkedUrl = normalizeAndValidateGiteaUrl($('giteaURL').value);
if (!checkedUrl.ok) {
showError(checkedUrl.error);
return;
}
const data = {
githubToken: $('githubToken').value,
giteaToken: $('giteaToken').value,
giteaURL: checkedUrl.value,
featureFavorites,
featureRecent,
compactMode,
featureColoredIcons,
favCollapsedFavorites: favSectionCollapsed.favorites,
favCollapsedRecent: favSectionCollapsed.recent
};
await window.electronAPI.saveCredentials(data);
$('settingsModal').classList.add('hidden');
showSuccess('Settings saved');
renderGiteaUrlHint(checkedUrl.value);
updateSettingsHealth({
url: checkedUrl.value ? 'Gültig' : 'Leer',
auth: data.giteaToken ? 'Token vorhanden' : 'Kein Token',
lastError: '-'
});
// 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}`);
});
if (window.electronAPI.onRetryQueueUpdated) {
window.electronAPI.onRetryQueueUpdated((payload) => {
const size = payload && typeof payload.size === 'number' ? payload.size : 0;
updateRetryQueueBadge(size);
if (payload && payload.event === 'queued' && payload.item) {
const p = payload.item.payload || {};
logActivity('warning', `Queue: ${p.owner}/${p.repo}/${p.path} wurde eingeplant`);
} else if (payload && payload.event === 'processed') {
logActivity('info', `Queue-Retry: ${payload.succeeded || 0} erfolgreich, ${payload.failed || 0} verworfen`);
}
});
}
if (window.electronAPI.onBatchActionProgress) {
window.electronAPI.onBatchActionProgress((payload) => {
if (!payload) return;
if (payload.status === 'running') {
logActivity('info', `Batch ${payload.action}: ${payload.repo} (${payload.index}/${payload.total})`);
} else if (payload.status === 'error') {
logActivity('error', `Batch ${payload.action}: ${payload.repo} - ${payload.error || 'Fehler'}`);
}
});
}
// 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 ? `&lt;${escapeHtml(email)}&gt;` : ''}</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;
}
if (versionRes && versionRes.ok && $('watermarkVersion')) {
$('watermarkVersion').textContent = versionRes.version;
}
} catch (error) {
console.error('[Renderer] Fehler beim Laden der Version:', error);
}
if ($('watermarkCopyright')) {
$('watermarkCopyright').textContent = `© ${new Date().getFullYear()} M_Viper`;
}
// 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);
});