diff --git a/renderer/renderer.js b/renderer/renderer.js
index a5afaf2..e6ab1fa 100644
--- a/renderer/renderer.js
+++ b/renderer/renderer.js
@@ -15,6 +15,15 @@ let recentRepos = []; // [{ owner, repo, cloneUrl, openedAt }]
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 {
@@ -40,6 +49,7 @@ async function toggleFavorite(owner, repo, cloneUrl) {
favorites.unshift({ owner, repo, cloneUrl, addedAt: new Date().toISOString() });
}
await window.electronAPI.saveFavorites(favorites);
+ refreshFavHistoryUi();
}
async function addToRecent(owner, repo, cloneUrl) {
@@ -48,6 +58,7 @@ async function addToRecent(owner, repo, cloneUrl) {
recentRepos.unshift({ owner, repo, cloneUrl, openedAt: new Date().toISOString() });
recentRepos = recentRepos.slice(0, 20);
await window.electronAPI.saveRecent(recentRepos);
+ refreshFavHistoryUi();
}
function formatRelDate(iso) {
@@ -168,6 +179,123 @@ function renderFavRecentSection(container, allRepos) {
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');
@@ -327,6 +455,11 @@ let currentState = {
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 = {
@@ -340,6 +473,279 @@ function logActivity(level, message) {
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 = `
+
Weniger
+
+
+
+
+
+
Mehr
+ `;
+
+ footer.appendChild(summary);
+ footer.appendChild(legend);
+
+ body.appendChild(monthsRow);
+ body.appendChild(gridWrap);
+ body.appendChild(footer);
+ card.appendChild(body);
+
+ host.appendChild(card);
}
function formatActivityTimestamp(iso) {
@@ -390,6 +796,150 @@ function parseBatchRepoInput(raw) {
.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');
@@ -418,6 +968,7 @@ let lastSelectedItem = null; // { type:'gitea', item, owner, repo } | { type:'l
// Feature-Flag für farbige Icons
let featureColoredIcons = true;
+let repoSearchHotkeyBound = false;
let settingsHealth = {
url: 'Unbekannt',
@@ -642,6 +1193,110 @@ function showError(msg) {
}
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) {
@@ -1616,24 +2271,32 @@ async function loadGiteaRepos() {
}
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 = '
Keine Repositories gefunden
';
setStatus('No repositories found');
return;
}
- // --- NEU: Suchfeld für Projekte ---
+ // --- 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 = '🔍 Projekt suchen...';
+ searchInput.placeholder = '🔍 Fuzzy-Suche: Name, Owner, Sprache, Topics...';
+ searchInput.className = 'repo-search-input';
searchInput.style.cssText = `
width: 100%;
padding: 12px 16px;
@@ -1645,6 +2308,16 @@ async function loadGiteaRepos() {
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', () => {
@@ -1654,35 +2327,48 @@ async function loadGiteaRepos() {
searchInput.style.borderColor = 'rgba(255, 255, 255, 0.1)';
});
- searchContainer.appendChild(searchInput);
+ searchTop.appendChild(searchInput);
+ searchTop.appendChild(searchClearBtn);
+ searchContainer.appendChild(searchTop);
grid.appendChild(searchContainer);
- // Search Logic
+ 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) => {
- const val = e.target.value.toLowerCase();
- const cards = grid.querySelectorAll('.item-card');
- cards.forEach(card => {
- const name = card.querySelector('.item-name').textContent.toLowerCase();
- if (name.includes(val)) {
- card.style.display = 'flex';
- } else {
- card.style.display = 'none';
+ 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();
}
});
- });
- // -----------------------------------
-
- // ── Favoriten & Zuletzt geöffnet ──
- // Sektion IMMER ins DOM einfügen (auch wenn leer),
- // damit der Stern-Klick später $('favRecentSection') findet
- if (featureFavorites || featureRecent) {
- const favSection = document.createElement('div');
- favSection.id = 'favRecentSection';
- favSection.style.cssText = 'grid-column: 1/-1;';
- grid.appendChild(favSection);
- renderFavRecentSection(favSection, res.repos);
+ 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;
@@ -1696,6 +2382,14 @@ async function loadGiteaRepos() {
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) {
@@ -1706,13 +2400,6 @@ async function loadGiteaRepos() {
starBtn.addEventListener('click', async (e) => {
e.stopPropagation();
await toggleFavorite(owner, repoName, cloneUrl);
- const nowFav = isFavorite(owner, repoName);
- starBtn.textContent = nowFav ? '⭐' : '☆';
- starBtn.title = nowFav ? 'Aus Favoriten entfernen' : 'Zu Favoriten hinzufügen';
- starBtn.classList.toggle('active', nowFav);
- // Favoriten-Sektion live aktualisieren
- const sec = $('favRecentSection');
- if (sec) renderFavRecentSection(sec, res.repos);
});
card.appendChild(starBtn);
}
@@ -1827,6 +2514,8 @@ async function loadGiteaRepos() {
card.oncontextmenu = (ev) => showRepoContextMenu(ev, owner, repoName, cloneUrl, card);
grid.appendChild(card);
});
+
+ applyRepoFuzzyFilter(grid, searchInput, null);
setStatus(`Loaded ${res.repos.length} repos`);
} catch (error) {
@@ -2273,6 +2962,48 @@ function showRepoContextMenu(ev, owner, repoName, cloneUrl, element) {
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);
@@ -2288,9 +3019,7 @@ function showRepoContextMenu(ev, owner, repoName, cloneUrl, element) {
);
menu.appendChild(favItem);
- const sep = document.createElement('div');
- sep.style.cssText = 'height:1px;background:rgba(255,255,255,0.08);margin:4px 0;';
- menu.appendChild(sep);
+ addSep();
}
const uploadItem = createMenuItem('🚀', 'Folder hier hochladen', async () => {
@@ -2798,9 +3527,20 @@ async function previewGiteaFile(owner, repo, filePath) {
async function createRepoHandler() {
const name = $('repoName')?.value?.trim();
if (!name) {
- alert('Name required');
+ 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...');
@@ -3093,6 +3833,14 @@ window.addEventListener('DOMContentLoaded', async () => {
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;
@@ -3106,6 +3854,8 @@ window.addEventListener('DOMContentLoaded', async () => {
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) {
@@ -3172,6 +3922,10 @@ window.addEventListener('DOMContentLoaded', async () => {
}
// 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');
@@ -3207,6 +3961,7 @@ window.addEventListener('DOMContentLoaded', async () => {
$('btnBatchActions').onclick = () => {
$('batchActionModal')?.classList.remove('hidden');
updateBatchActionFields();
+ scheduleBatchCloneValidation();
};
}
@@ -3229,6 +3984,7 @@ window.addEventListener('DOMContentLoaded', async () => {
$('btnClearActivityLog').onclick = () => {
activityEntries = [];
renderActivityLog();
+ refreshActivityHeatmapIfVisible();
};
}
@@ -3238,14 +3994,29 @@ window.addEventListener('DOMContentLoaded', async () => {
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();
}
};
}
@@ -3271,6 +4042,14 @@ window.addEventListener('DOMContentLoaded', async () => {
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;
@@ -3425,6 +4204,7 @@ window.addEventListener('DOMContentLoaded', async () => {
if ($('btnOpenRepoActions')) {
$('btnOpenRepoActions').onclick = () => {
$('repoActionModal').classList.remove('hidden');
+ scheduleRepoNameValidation();
};
}
@@ -3455,6 +4235,12 @@ window.addEventListener('DOMContentLoaded', async () => {
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) {
diff --git a/renderer/style.css b/renderer/style.css
index f3fe28d..f960988 100644
--- a/renderer/style.css
+++ b/renderer/style.css
@@ -59,6 +59,145 @@
padding: 0;
}
+/* ===========================
+ TITELBALKEN-STREIFEN
+ =========================== */
+#titlebar-strip {
+ height: 32px;
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 4px 0 12px;
+ background: linear-gradient(135deg, rgba(8, 14, 26, 0.98) 0%, rgba(10, 19, 34, 0.98) 100%);
+ border-bottom: 1px solid rgba(88, 213, 255, 0.14);
+ -webkit-app-region: drag;
+ user-select: none;
+ position: relative;
+ z-index: 200;
+ box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.24);
+}
+
+.titlebar-strip-brand {
+ display: flex;
+ align-items: center;
+ gap: 7px;
+ min-width: 0;
+}
+
+.titlebar-strip-icon {
+ width: 13px;
+ height: 13px;
+ object-fit: contain;
+ opacity: 0.80;
+ filter: drop-shadow(0 1px 3px rgba(88, 213, 255, 0.25));
+ flex-shrink: 0;
+}
+
+.titlebar-strip-title {
+ font-size: 11px;
+ font-weight: 700;
+ color: rgba(174, 189, 216, 0.68);
+ letter-spacing: 0.045em;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+#titlebar-strip .win-controls {
+ -webkit-app-region: no-drag;
+ margin-left: auto;
+ border-radius: 10px;
+ background: linear-gradient(180deg, rgba(16, 30, 52, 0.96) 0%, rgba(12, 22, 40, 0.98) 100%);
+ border-color: rgba(151, 181, 255, 0.18);
+ box-shadow: 0 6px 14px rgba(2, 8, 24, 0.30), inset 0 1px 0 rgba(255, 255, 255, 0.05);
+}
+
+#titlebar-strip .win-btn {
+ width: 18px;
+ height: 18px;
+ border-radius: 5px;
+}
+
+/* ===========================
+ WINDOW CONTROLS
+ =========================== */
+.win-controls {
+ display: inline-flex;
+ align-items: center;
+ gap: 1px;
+ padding: 2px;
+ border-radius: 8px;
+ background: linear-gradient(180deg, #0f1b30 0%, #0c1628 100%);
+ border: 1px solid rgba(151, 181, 255, 0.12);
+ box-shadow: 0 4px 10px rgba(2, 8, 24, 0.22), inset 0 1px 0 rgba(255, 255, 255, 0.04);
+ flex-shrink: 0;
+ -webkit-app-region: no-drag;
+ user-select: none;
+}
+
+.win-btn {
+ width: 20px;
+ height: 20px;
+ border-radius: 5px;
+ border: 1px solid transparent;
+ background: transparent;
+ color: #aebdd8;
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ transition: background 120ms ease, border-color 120ms ease, color 120ms ease, transform 80ms ease;
+ flex-shrink: 0;
+}
+
+.win-btn svg {
+ display: block;
+ flex-shrink: 0;
+}
+
+.win-btn:hover {
+ color: #f5f8ff;
+ border-color: rgba(151, 181, 255, 0.18);
+ background: rgba(255, 255, 255, 0.08);
+}
+
+.win-btn--minimize:hover {
+ background: rgba(88, 213, 255, 0.14);
+ border-color: rgba(88, 213, 255, 0.28);
+ color: #58d5ff;
+}
+
+.win-btn--maximize:hover {
+ background: rgba(92, 135, 255, 0.14);
+ border-color: rgba(92, 135, 255, 0.28);
+ color: #5c87ff;
+}
+
+.win-btn--close:hover {
+ background: rgba(239, 68, 68, 0.18);
+ border-color: rgba(239, 68, 68, 0.36);
+ color: #ef4444;
+}
+
+.win-btn:active {
+ transform: scale(0.88);
+}
+
+#toolbar {
+ -webkit-app-region: no-drag;
+}
+
+#toolbar button,
+#toolbar select,
+#toolbar input,
+#toolbar a,
+#toolbar .platform-switch,
+#toolbar .toolbar-status-wrap,
+#toolbar .win-controls {
+ -webkit-app-region: no-drag;
+}
+
body {
margin: 0;
background: var(--bg-primary);
@@ -80,6 +219,9 @@ body {
radial-gradient(circle at bottom center, rgba(18, 127, 255, 0.10), transparent 36%),
var(--bg-primary);
position: relative;
+ border: 1px solid rgba(88, 213, 255, 0.14);
+ border-radius: 0;
+ overflow: hidden;
}
#app::before {
@@ -503,6 +645,10 @@ body {
min-height: unset;
}
+ .titlebar-strip-title {
+ max-width: 240px;
+ }
+
.toolbar-row {
flex-direction: column;
align-items: stretch;
@@ -529,12 +675,23 @@ body {
}
}
+@media (max-width: 760px) {
+ #titlebar-strip {
+ padding-left: 8px;
+ }
+
+ .titlebar-strip-title {
+ display: none;
+ }
+}
+
/* ===========================
MAIN CONTENT
=========================== */
#main {
flex: 1;
min-height: 0;
+ min-width: 0;
padding: var(--spacing-xl);
overflow-y: auto;
overflow-x: hidden;
@@ -556,6 +713,156 @@ body {
background: radial-gradient(circle at top right, rgba(88, 213, 255, 0.08), transparent 26%);
}
+.content-area {
+ display: flex;
+ flex-direction: row;
+ flex: 1;
+ min-height: 0;
+ overflow: hidden;
+}
+
+.fav-history-sidebar {
+ width: 0;
+ flex-shrink: 0;
+ overflow: hidden;
+ opacity: 0;
+ transition: width 220ms cubic-bezier(0.4, 0, 0.2, 1),
+ opacity 180ms cubic-bezier(0.4, 0, 0.2, 1),
+ margin 220ms cubic-bezier(0.4, 0, 0.2, 1);
+ margin: 0;
+ align-self: stretch;
+ display: flex;
+ flex-direction: column;
+}
+
+.fav-history-sidebar.visible {
+ width: 220px;
+ opacity: 1;
+ margin: 16px 0 16px 16px;
+}
+
+.fav-history-sidebar-inner {
+ padding: 10px 8px;
+ border-radius: 14px;
+ border: 1px solid rgba(88, 213, 255, 0.18);
+ background: linear-gradient(180deg, rgba(8, 18, 35, 0.9) 0%, rgba(7, 15, 30, 0.94) 100%);
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04), 0 14px 24px rgba(0, 0, 0, 0.22);
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ min-height: 0;
+}
+
+.fav-history-switch {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 6px;
+ margin-bottom: 10px;
+}
+
+.fav-history-tab {
+ height: 28px;
+ border-radius: 8px;
+ border: 1px solid rgba(151, 181, 255, 0.14);
+ background: rgba(255, 255, 255, 0.03);
+ color: rgba(174, 189, 216, 0.88);
+ font-size: 11px;
+ font-weight: 700;
+ letter-spacing: 0.03em;
+ cursor: pointer;
+ transition: background var(--transition-fast), color var(--transition-fast), border-color var(--transition-fast);
+}
+
+.fav-history-tab:hover {
+ border-color: rgba(88, 213, 255, 0.34);
+ color: var(--text-primary);
+}
+
+.fav-history-tab.active {
+ color: #03131a;
+ border-color: rgba(103, 232, 249, 0.62);
+ background: linear-gradient(135deg, #67e8f9 0%, #58a6ff 100%);
+}
+
+.fav-history-list {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ max-height: calc(100vh - 270px);
+ overflow: auto;
+ padding-right: 2px;
+}
+
+.fav-history-list::-webkit-scrollbar {
+ width: 7px;
+}
+
+.fav-history-list::-webkit-scrollbar-thumb {
+ background: rgba(112, 131, 164, 0.4);
+ border-radius: 8px;
+}
+
+.fav-history-item {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ width: 100%;
+ text-align: left;
+ border: 1px solid rgba(151, 181, 255, 0.08);
+ border-radius: 9px;
+ background: rgba(16, 28, 48, 0.72);
+ color: var(--text-primary);
+ padding: 8px 9px;
+ cursor: pointer;
+ transition: border-color var(--transition-fast), transform var(--transition-fast), background var(--transition-fast);
+}
+
+.fav-history-item:hover {
+ border-color: rgba(88, 213, 255, 0.34);
+ background: rgba(19, 34, 58, 0.88);
+ transform: translateY(-1px);
+}
+
+.fav-history-item-name {
+ font-size: 12px;
+ font-weight: 700;
+ line-height: 1.3;
+ color: var(--text-primary);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.fav-history-item-meta {
+ font-size: 10px;
+ color: var(--text-muted);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.fav-history-empty {
+ margin-top: 8px;
+ padding: 10px;
+ border-radius: 9px;
+ border: 1px dashed rgba(151, 181, 255, 0.2);
+ color: var(--text-muted);
+ font-size: 11px;
+ text-align: center;
+}
+
+@media (max-width: 1180px) {
+ .fav-history-sidebar.visible {
+ width: 180px;
+ margin: 12px 0 12px 12px;
+ }
+
+ .fav-history-list {
+ max-height: 180px;
+ }
+}
+
/* Global Drop Zone Indicator */
#main.drop-active {
background: rgba(88, 213, 255, 0.06);
@@ -786,6 +1093,26 @@ body.compact-mode .item-card:hover {
padding: 20px;
}
+/* Custom Scrollbar – Modals */
+.modal::-webkit-scrollbar {
+ width: 8px;
+}
+
+.modal::-webkit-scrollbar-track {
+ background: rgba(7, 17, 31, 0.6);
+ border-radius: 8px;
+}
+
+.modal::-webkit-scrollbar-thumb {
+ background: linear-gradient(180deg, rgba(88, 213, 255, 0.28) 0%, rgba(92, 135, 255, 0.22) 100%);
+ border-radius: 8px;
+ border: 2px solid rgba(7, 17, 31, 0.6);
+}
+
+.modal::-webkit-scrollbar-thumb:hover {
+ background: linear-gradient(180deg, rgba(88, 213, 255, 0.52) 0%, rgba(92, 135, 255, 0.46) 100%);
+}
+
.modal.hidden {
display: none !important;
pointer-events: none;
@@ -898,6 +1225,28 @@ input::placeholder {
color: var(--text-muted);
}
+/* Native Dropdown-Listen in Modals im Dark-Theme halten (Windows/Electron) */
+#repoActionModal select,
+#batchActionModal select,
+#activityLogModal select {
+ appearance: auto;
+ color-scheme: dark;
+}
+
+#repoActionModal select option,
+#batchActionModal select option,
+#activityLogModal select option {
+ background: #1a2233;
+ color: var(--text-primary);
+}
+
+#repoActionModal select option:checked,
+#batchActionModal select option:checked,
+#activityLogModal select option:checked {
+ background: #2d4ea4;
+ color: #ffffff;
+}
+
input[type="checkbox"] {
width: auto;
margin-right: var(--spacing-sm);
@@ -1261,6 +1610,52 @@ input[type="checkbox"] {
.activity-toolbar select {
width: 140px;
+ height: 36px;
+ padding: 0 10px;
+ border-radius: var(--radius-md);
+ border: 1px solid rgba(255, 255, 255, 0.18);
+ background: rgba(255, 255, 255, 0.06);
+ color: var(--text-primary);
+ font-size: 13px;
+ font-weight: 500;
+ cursor: pointer;
+ appearance: auto;
+ color-scheme: dark;
+ transition: background 0.15s, border-color 0.15s;
+}
+
+.activity-toolbar select:hover {
+ background: rgba(255, 255, 255, 0.10);
+ border-color: rgba(255, 255, 255, 0.28);
+}
+
+.activity-toolbar select:focus {
+ outline: none;
+ border-color: rgba(88, 213, 255, 0.5);
+}
+
+.activity-toolbar select option {
+ background: #1a1f2e;
+ color: var(--text-primary);
+}
+
+.activity-toolbar button {
+ height: 36px;
+ padding: 0 14px;
+ border-radius: var(--radius-md);
+ border: 1px solid rgba(255, 255, 255, 0.18);
+ background: rgba(255, 255, 255, 0.06);
+ color: var(--text-primary);
+ font-size: 13px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: background 0.15s, border-color 0.15s;
+ white-space: nowrap;
+}
+
+.activity-toolbar button:hover {
+ background: rgba(255, 255, 255, 0.12);
+ border-color: rgba(255, 255, 255, 0.3);
}
.activity-queue-info {
@@ -1352,6 +1747,286 @@ input[type="checkbox"] {
transform: translateX(4px);
}
+.repo-search-top {
+ display: flex;
+ gap: 10px;
+ align-items: center;
+}
+
+.repo-search-input {
+ flex: 1;
+}
+
+.repo-search-clear {
+ min-width: 34px;
+ height: 34px;
+ padding: 0;
+ appearance: none;
+ border: 1px solid rgba(255, 255, 255, 0.16);
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0.04) 100%);
+ color: var(--text-secondary);
+ font-size: 16px;
+ font-weight: 500;
+ line-height: 1;
+ cursor: pointer;
+ border-radius: var(--radius-md);
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ transition: border-color 140ms ease, background 140ms ease, color 140ms ease, transform 140ms ease;
+}
+
+.repo-search-clear:hover {
+ border-color: rgba(88, 213, 255, 0.45);
+ background: linear-gradient(180deg, rgba(88, 213, 255, 0.18) 0%, rgba(88, 213, 255, 0.10) 100%);
+ color: #d9f7ff;
+ transform: translateY(-1px);
+}
+
+.repo-search-clear:active {
+ transform: translateY(0);
+}
+
+.repo-search-clear:focus {
+ outline: none;
+}
+
+.repo-search-clear:focus-visible {
+ border-color: rgba(88, 213, 255, 0.55);
+ box-shadow: 0 0 0 3px rgba(88, 213, 255, 0.2);
+}
+
+.repo-search-meta {
+ margin-top: 8px;
+ font-size: 12px;
+ color: var(--text-muted);
+}
+
+.activity-heatmap-card {
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ border-radius: var(--radius-lg);
+ background:
+ radial-gradient(120% 180% at 90% -40%, rgba(88, 213, 255, 0.12), transparent 48%),
+ linear-gradient(180deg, rgba(20, 28, 42, 0.92), rgba(13, 18, 30, 0.94));
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.22);
+ overflow: hidden;
+}
+
+.activity-heatmap-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ padding: 12px 14px;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.07);
+}
+
+.activity-heatmap-header strong {
+ font-size: 13px;
+ color: var(--text-primary);
+ letter-spacing: 0.02em;
+}
+
+.activity-heatmap-controls {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.activity-heatmap-range-chip {
+ min-height: 24px;
+ padding: 0 8px;
+ border-radius: 999px;
+ border: 1px solid rgba(130, 162, 218, 0.2);
+ background: rgba(17, 29, 49, 0.78);
+ color: var(--text-secondary);
+ font-size: 11px;
+ cursor: pointer;
+ transition: border-color 140ms ease, background 140ms ease, color 140ms ease;
+}
+
+.activity-heatmap-range-chip:hover {
+ border-color: rgba(88, 213, 255, 0.38);
+ color: #d9ecff;
+}
+
+.activity-heatmap-range-chip.active {
+ border-color: rgba(88, 213, 255, 0.55);
+ background: linear-gradient(180deg, rgba(38, 62, 97, 0.92), rgba(23, 39, 66, 0.98));
+ color: #f2f8ff;
+}
+
+.activity-heatmap-range-chip.static {
+ cursor: default;
+ pointer-events: none;
+}
+
+.activity-heatmap-toggle {
+ min-height: 28px;
+ padding: 0 10px;
+ font-size: 12px;
+ border-radius: 8px;
+ border: 1px solid rgba(130, 162, 218, 0.28);
+ background: linear-gradient(180deg, rgba(28, 41, 66, 0.96), rgba(18, 28, 48, 0.98));
+ color: #d2ddf2;
+ cursor: pointer;
+ transition: border-color 140ms ease, background 140ms ease, color 140ms ease, transform 120ms ease;
+ appearance: none;
+}
+
+.activity-heatmap-toggle:hover {
+ border-color: rgba(88, 213, 255, 0.42);
+ background: linear-gradient(180deg, rgba(34, 52, 82, 0.96), rgba(20, 33, 57, 0.98));
+ color: #f2f7ff;
+ transform: translateY(-1px);
+}
+
+.activity-heatmap-toggle:focus {
+ outline: none;
+}
+
+.activity-heatmap-toggle:focus-visible {
+ border-color: rgba(88, 213, 255, 0.6);
+ box-shadow: 0 0 0 3px rgba(88, 213, 255, 0.2);
+}
+
+.activity-heatmap-body {
+ --hm-weeks: 53;
+ --hm-cell-size: 10px;
+ --hm-week-gap: 2px;
+ --hm-weekday-col: 22px;
+ --hm-grid-gap: 8px;
+ padding: 10px 12px 12px;
+ display: grid;
+ gap: 8px;
+}
+
+.activity-heatmap-card.collapsed .activity-heatmap-body {
+ display: none;
+}
+
+.activity-heatmap-months {
+ margin-left: calc(var(--hm-weekday-col) + var(--hm-grid-gap));
+ width: max-content;
+ display: grid;
+ grid-template-columns: repeat(var(--hm-weeks), var(--hm-cell-size));
+ column-gap: var(--hm-week-gap);
+ color: var(--text-muted);
+ font-size: 10px;
+ line-height: 1;
+ user-select: none;
+}
+
+.activity-heatmap-grid-wrap {
+ display: flex;
+ gap: var(--hm-grid-gap);
+ align-items: flex-start;
+ overflow-x: auto;
+ padding-bottom: 2px;
+}
+
+.activity-heatmap-weekdays {
+ width: var(--hm-weekday-col);
+ display: grid;
+ grid-template-rows: repeat(7, var(--hm-cell-size));
+ gap: var(--hm-week-gap);
+ color: var(--text-muted);
+ font-size: 9px;
+ line-height: 1;
+ user-select: none;
+}
+
+.activity-heatmap-grid {
+ display: grid;
+ grid-template-columns: repeat(var(--hm-weeks), var(--hm-cell-size));
+ column-gap: var(--hm-week-gap);
+ flex: 0 0 auto;
+ min-width: auto;
+}
+
+.activity-heatmap-week {
+ display: grid;
+ grid-template-rows: repeat(7, var(--hm-cell-size));
+ gap: var(--hm-week-gap);
+}
+
+.activity-heatmap-cell {
+ width: var(--hm-cell-size);
+ height: var(--hm-cell-size);
+ border-radius: 2px;
+ display: inline-block;
+ border: 1px solid rgba(255, 255, 255, 0.06);
+ box-sizing: border-box;
+}
+
+.activity-heatmap-cell.lv0 {
+ background: rgba(255, 255, 255, 0.06);
+}
+
+.activity-heatmap-cell.lv1 {
+ background: rgba(66, 192, 122, 0.35);
+}
+
+.activity-heatmap-cell.lv2 {
+ background: rgba(66, 192, 122, 0.58);
+}
+
+.activity-heatmap-cell.lv3 {
+ background: rgba(66, 192, 122, 0.78);
+}
+
+.activity-heatmap-cell.lv4 {
+ background: rgba(66, 192, 122, 0.98);
+}
+
+.activity-heatmap-cell.out {
+ opacity: 0.24;
+}
+
+.activity-heatmap-footer {
+ margin-top: 0;
+ display: flex;
+ justify-content: flex-start;
+ gap: 12px;
+ align-items: center;
+ flex-wrap: wrap;
+ color: var(--text-muted);
+ font-size: 10px;
+}
+
+.activity-heatmap-legend {
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+}
+
+.activity-heatmap-legend .activity-heatmap-cell {
+ width: 10px;
+ height: 10px;
+}
+
+@media (max-width: 768px) {
+ .activity-heatmap-body {
+ --hm-cell-size: 9px;
+ --hm-week-gap: 2px;
+ --hm-grid-gap: 6px;
+ --hm-weekday-col: 20px;
+ padding: 8px;
+ }
+
+ .activity-heatmap-weekdays {
+ font-size: 9px;
+ }
+
+ .activity-heatmap-grid {
+ min-width: auto;
+ }
+
+ .activity-heatmap-cell {
+ border-radius: 1px;
+ }
+}
+
/* ===========================
PROGRESS BAR (falls verwendet)
=========================== */
@@ -2375,6 +3050,10 @@ progress::-moz-progress-bar {
color: #86efac;
}
+.settings-inline-hint.warn {
+ color: #fcd34d;
+}
+
.settings-health-box {
height: auto;
margin-top: 0;