Compare commits
141 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
691c85531c | ||
|
|
a2310fe313 | ||
|
|
1206c18b0b | ||
|
|
3c112ea75f | ||
|
|
c2273b5e44 | ||
|
|
878ae070aa | ||
|
|
5ae6fe5313 | ||
| ab97899d3b | |||
|
|
8bf7f47186 | ||
|
|
781820d5a1 | ||
|
|
fb80838a1c | ||
|
|
e88e798eb4 | ||
|
|
32eeba859e | ||
|
|
60a685fdda | ||
|
|
013a03424d | ||
|
|
46f0c0c56b | ||
|
|
54b87bec66 | ||
|
|
069f1480b9 | ||
| 6f11730fc7 | |||
| e0827faf42 | |||
| 1041f39ced | |||
| 7a43d24a32 | |||
| d6968a4954 | |||
| 464d15464a | |||
| da47343b2e | |||
| d41865608f | |||
| e4b1215aa7 | |||
| 1d7b5e8d6e | |||
| e79c0f411d | |||
| 9da186e5d2 | |||
| a9f0728232 | |||
| edeb05f088 | |||
| 4b4d1520b9 | |||
| b8644b248f | |||
| 71ed9b7c67 | |||
| 78a94f2263 | |||
| b153431543 | |||
| 93ebe5aea9 | |||
| 1ca5856fe3 | |||
| e2dbacc77c | |||
| 925c214a1f | |||
| aafd5c3e66 | |||
| 68002a31df | |||
| ec72cd2a11 | |||
| cd739363ae | |||
| f064ffc8a2 | |||
| 6965503bb3 | |||
| 2a9812575c | |||
| 89272fb899 | |||
| 14d15dc355 | |||
| 2b2d6a8303 | |||
| b613e3bd73 | |||
| 62e4f9b85f | |||
| 7b68e92a95 | |||
| 83782d547f | |||
| f1acea14fa | |||
| 5513bdcef4 | |||
| f1f896ba65 | |||
| ca55bbdcae | |||
| e97e714826 | |||
| cb185d7714 | |||
| 4215429114 | |||
| 1195dd4c0e | |||
| a6585a856b | |||
| 5988d98d9a | |||
| 89dcd9f311 | |||
| 0366253134 | |||
| 853a93e142 | |||
| 24dd12c360 | |||
| f52b99f192 | |||
| 6ba774cb26 | |||
| f5542a6114 | |||
| 3c0c1e78dc | |||
| f041b3bc32 | |||
| 3ecb3125d3 | |||
| 272bd00c80 | |||
| 0e9c14b144 | |||
| a9c0c97287 | |||
| cdd7dc430f | |||
| 433c70d389 | |||
| d61c2b39e3 | |||
| 96c3a063cc | |||
| 9dd3696a09 | |||
| 4bef46201b | |||
| fd80283328 | |||
| 2dba14ca58 | |||
| 1391f092d8 | |||
| 88e2535e62 | |||
| de581d878f | |||
| a1f7c66f67 | |||
| 12dbdbb28e | |||
| 687a80924a | |||
| b22ea34032 | |||
| c7fb8ce0bf | |||
| b5f744db12 | |||
| 553333c623 | |||
| 13ee1b0339 | |||
| 3755796ef6 | |||
| e3dbc6c663 | |||
| 8613720a0b | |||
| 0f758616cf | |||
| b2ba2a09e2 | |||
| 2ef1e0df61 | |||
| fe0e7b7794 | |||
| c8e06bf576 | |||
| 7b480acd10 | |||
| 658b29368b | |||
| c64d40fbda | |||
| f6598cfb19 | |||
| 5787c4ca22 | |||
| e3dfc3de2f | |||
| 51f29a6f69 | |||
| 9157e4a97c | |||
| f82c9ca974 | |||
| 76a0f67121 | |||
| 223714d5e3 | |||
| 43750e2d8a | |||
| 1669412115 | |||
| 2ae24f47e3 | |||
| ea4aa06e31 | |||
| f6e73250f0 | |||
| 57bdd3a735 | |||
| 0b56564074 | |||
| 7659be9adc | |||
| 09bfb602e3 | |||
| 5e0f741bc3 | |||
| 7eec6d19e5 | |||
| d8ce73439e | |||
| d8798ac32f | |||
| f2f5823fbb | |||
| 0a6703b87e | |||
| eece161e58 | |||
| a35d6cf0cf | |||
| 594f12e927 | |||
| 8d70b71d66 | |||
| 2c39ae4651 | |||
| 9c14a1a7e0 | |||
| 677b983706 | |||
| 20a9d601ff | |||
| df9790177d | |||
| 1621a100af |
535
DEV_GUIDE.md
Normal file
535
DEV_GUIDE.md
Normal file
@@ -0,0 +1,535 @@
|
||||
# Entwicklerhandbuch — Git Manager Explorer Pro
|
||||
|
||||
Dieses Dokument richtet sich an Entwickler, die an **Git Manager Explorer Pro** beitragen oder das Projekt erweitern möchten.
|
||||
|
||||
---
|
||||
|
||||
## Inhaltsverzeichnis
|
||||
|
||||
1. [Projektstruktur](#projektstruktur)
|
||||
2. [Setup & Abhängigkeiten](#setup--abhängigkeiten)
|
||||
3. [Entwicklung starten](#entwicklung-starten)
|
||||
4. [Wichtige Module](#wichtige-module)
|
||||
5. [Neue Features hinzufügen](#neue-features-hinzufügen)
|
||||
6. [Build & Release](#build--release)
|
||||
7. [Debugging & Testing](#debugging--testing)
|
||||
8. [Code-Konventionen](#code-konventionen)
|
||||
9. [Häufige Aufgaben](#häufige-aufgaben)
|
||||
|
||||
---
|
||||
|
||||
## Projektstruktur
|
||||
|
||||
```
|
||||
git-manager-gui/
|
||||
├── main.js # Electron Main Process — Fenster, Menü, IPC
|
||||
├── preload.js # Preload Script — sichere Electron API für Renderer
|
||||
├── renderer/ # Frontend (React/Vanilla JS)
|
||||
│ ├── App.jsx # React-Komponente (optional, optional genutzt)
|
||||
│ ├── Settings.jsx # Settings-Modal
|
||||
│ ├── index.html # HTML Entry Point
|
||||
│ ├── index.js # Renderer Init
|
||||
│ ├── renderer.js # Hauptlogik: UI, Grid, Editor, API-Calls
|
||||
│ ├── style.css # Styling
|
||||
│ └── modules/ # Feature-Module
|
||||
│ ├── editor.js # Editor-Funktion (Syntax Highlighting, etc.)
|
||||
│ ├── gitea.js # Gitea API Wrapper
|
||||
│ ├── github.js # GitHub API Wrapper
|
||||
│ ├── progress.js # Progress-Bar & Ladeindikatoren
|
||||
│ ├── state.js # State Management (Favoriten, Verlauf, etc.)
|
||||
│ └── ui.js # UI Helpers (Dialoge, Toast, etc.)
|
||||
├── src/ # Backend (Node.js Main Process)
|
||||
│ ├── git/
|
||||
│ │ ├── gitHandler.js # Git CLI Wrapper (Commits, Tags, etc.)
|
||||
│ │ └── apiHandler.js # Gitea/GitHub API HTTP Client
|
||||
│ ├── backup/
|
||||
│ │ ├── BackupManager.js # Backup/Restore Logik
|
||||
│ │ ├── BackupProvider.js # Provider Interface
|
||||
│ │ └── LocalProvider.js # Lokale Datei-Provider
|
||||
│ └── utils/
|
||||
│ └── helpers.js # Allgemeine Hilfsfunktionen
|
||||
├── __tests__/
|
||||
│ └── gitHandler.test.js # Jest Unit Tests
|
||||
├── updater.js # App-Update-Logik
|
||||
├── package.json # Dependencies & Scripts
|
||||
├── HANDBUCH.html # Benutzerhandbuch (HTML)
|
||||
└── README.md # Projekt-Übersicht
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Setup & Abhängigkeiten
|
||||
|
||||
### Voraussetzungen
|
||||
- **Node.js** 16+ (empfohlen: LTS)
|
||||
- **npm** 7+
|
||||
- **Git** (für Git-Operationen)
|
||||
- **Visual Studio Code** (optional, empfohlen)
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
# Repository klonen
|
||||
git clone https://github.com/dein-username/git-manager-gui.git
|
||||
cd git-manager-gui
|
||||
|
||||
# Dependencies installieren
|
||||
npm install
|
||||
|
||||
# App starten
|
||||
npm start
|
||||
```
|
||||
|
||||
### Wichtige Dependencies
|
||||
|
||||
| Package | Verwendung |
|
||||
|---------|-----------|
|
||||
| `electron` | Desktop-Framework |
|
||||
| `sqlite3` | Lokale Datenspeicherung (Favoriten, Verlauf) |
|
||||
| `axios` | HTTP-Requests zu Gitea/GitHub APIs |
|
||||
| `crypto` | Verschlüsselung von Credentials |
|
||||
| `jest` | Testing Framework |
|
||||
|
||||
---
|
||||
|
||||
## Entwicklung starten
|
||||
|
||||
### Dev-Server / Hot Reload
|
||||
|
||||
Es gibt aktuell keinen automatischen Hot Reload. Nach Code-Änderungen:
|
||||
|
||||
```bash
|
||||
# App neu starten
|
||||
npm start
|
||||
|
||||
# Oder im laufenden Fenster: DevTools öffnen
|
||||
# Strg+Shift+I → Reload (Strg+R)
|
||||
```
|
||||
|
||||
### DevTools debuggen
|
||||
|
||||
```javascript
|
||||
// In main.js, beim Erstellen des Fensters:
|
||||
mainWindow.webContents.openDevTools();
|
||||
|
||||
// Oder per Shortcut im laufenden Fenster:
|
||||
// Strg+Shift+I
|
||||
```
|
||||
|
||||
### Logs anschauen
|
||||
|
||||
```bash
|
||||
# Logs der letzten Session anschauen:
|
||||
# Windows: %APPDATA%\git-manager-gui\logs\
|
||||
# oder direkt im app data folder
|
||||
|
||||
# Mit Winston logger (falls implementiert):
|
||||
npm run logs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Wichtige Module
|
||||
|
||||
### `renderer/renderer.js` — Hauptlogik
|
||||
|
||||
Das Herzstück der App. Enthält:
|
||||
|
||||
- **Globale State-Variablen**: `favorites`, `recentRepos`, `currentState`, `pinnedRepos`
|
||||
- **Grid-Rendering**: `loadRepos()`, `loadGiteaRepos()`, `loadGithubRepos()`
|
||||
- **Kontext-Menü**: `showRepoContextMenu()`
|
||||
- **Datei-Explorer**: `loadRepoContents()`, `renderExplorer()`
|
||||
- **Editor**: `openFileEditor()`, mit Speichern & Syntax-Highlighting
|
||||
- **Favoriten/Verlauf**: `addToFavorites()`, `addToRecent()`
|
||||
|
||||
**Wichtige Funktionen:**
|
||||
|
||||
```javascript
|
||||
// Repository-Liste laden
|
||||
loadRepos() → loadGiteaRepos() oder loadGithubRepos()
|
||||
|
||||
// Repository öffnen (Datei-Explorer)
|
||||
loadRepoContents(owner, repo, path)
|
||||
|
||||
// Datei bearbeiten
|
||||
openFileEditor(owner, repo, path, content)
|
||||
|
||||
// Favoriten hinzufügen
|
||||
async addToFavorites(owner, repo, cloneUrl, platform)
|
||||
|
||||
// Settings speichern
|
||||
async saveSettings(settings)
|
||||
```
|
||||
|
||||
### `src/git/gitHandler.js` — Git-Operationen
|
||||
|
||||
Wrapper um Git CLI. Beispiele:
|
||||
|
||||
```javascript
|
||||
// Commits einsehen
|
||||
const commits = await getCommitLog(repoPath, branch);
|
||||
|
||||
// Tags auflisten
|
||||
const tags = await getTagList(repoPath);
|
||||
|
||||
// Branch wechseln
|
||||
await checkoutBranch(repoPath, branchName);
|
||||
|
||||
// Lokales Repo klonen
|
||||
await cloneRepository(cloneUrl, targetPath);
|
||||
```
|
||||
|
||||
### `src/git/apiHandler.js` — API-Calls
|
||||
|
||||
HTTP-Wrapper für Gitea/GitHub APIs.
|
||||
|
||||
```javascript
|
||||
// Gitea Repositories auflisten
|
||||
const repos = await getGiteaRepos(url, token, username);
|
||||
|
||||
// GitHub Repositories
|
||||
const repos = await getGithubRepos(token);
|
||||
|
||||
// Repository erstellen
|
||||
await createGiteaRepo(url, token, { name, description, private });
|
||||
|
||||
// Datei speichern
|
||||
await updateGiteaFile(url, token, owner, repo, path, content, message);
|
||||
```
|
||||
|
||||
### `renderer/modules/state.js` — State Management
|
||||
|
||||
Persistiert Favoriten, Verlauf und Settings.
|
||||
|
||||
```javascript
|
||||
// State laden/speichern
|
||||
loadState() → returns { favorites, recent, pinnedRepos }
|
||||
saveState(state) → speichert lokal
|
||||
|
||||
// Helpers
|
||||
addFavorite(repo)
|
||||
removeFavorite(repo)
|
||||
addRecentRepo(repo)
|
||||
clearRecent()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Neue Features hinzufügen
|
||||
|
||||
### Beispiel: Neuer API-Endpoint integrieren
|
||||
|
||||
#### Schritt 1: API-Wrapper in `src/git/apiHandler.js` ergänzen
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Gitea Webhook auflisten
|
||||
*/
|
||||
async function getGiteaWebhooks(url, token, owner, repo) {
|
||||
const path = `/api/v1/repos/${owner}/${repo}/hooks`;
|
||||
return axios.get(`${url}${path}`, {
|
||||
headers: { 'Authorization': `token ${token}` }
|
||||
}).then(r => r.data);
|
||||
}
|
||||
|
||||
module.exports = { getGiteaWebhooks, /* ... other functions */ };
|
||||
```
|
||||
|
||||
#### Schritt 2: IPC-Handler in `main.js` ergänzen
|
||||
|
||||
```javascript
|
||||
// In ipcMain.handle() Bereich:
|
||||
ipcMain.handle('getGiteaWebhooks', async (event, { url, token, owner, repo }) => {
|
||||
try {
|
||||
const webhooks = await apiHandler.getGiteaWebhooks(url, token, owner, repo);
|
||||
return { ok: true, webhooks };
|
||||
} catch (err) {
|
||||
return { ok: false, error: err.message };
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### Schritt 3: UI in `renderer/renderer.js` hinzufügen
|
||||
|
||||
```javascript
|
||||
// Button im Repo-Kontextmenü:
|
||||
if (option === 'webhooks') {
|
||||
const webhooks = await window.electronAPI.getGiteaWebhooks({
|
||||
url: currentState.giteaUrl,
|
||||
token: credentials.giteaToken,
|
||||
owner,
|
||||
repo
|
||||
});
|
||||
if (webhooks.ok) {
|
||||
showWebhookList(webhooks.webhooks);
|
||||
} else {
|
||||
showError('Webhooks laden fehlgeschlagen: ' + webhooks.error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Build & Release
|
||||
|
||||
### Production Build
|
||||
|
||||
```bash
|
||||
# Electron App als .exe packagen
|
||||
npm run build
|
||||
|
||||
# Output: dist/git-manager-gui-Setup-2.x.x.exe
|
||||
```
|
||||
|
||||
### Version erhöhen
|
||||
|
||||
1. Version in `package.json` updaten: `"version": "2.1.4"`
|
||||
2. Changelog aktualisieren (optional)
|
||||
3. Build ausführen
|
||||
4. GitHub Release erstellen mit der .exe
|
||||
|
||||
---
|
||||
|
||||
## Debugging & Testing
|
||||
|
||||
### Unit Tests schreiben
|
||||
|
||||
Jest ist konfiguriert. Test-Datei Vorlage:
|
||||
|
||||
```javascript
|
||||
// __tests__/myFeature.test.js
|
||||
const { myFunction } = require('../src/git/gitHandler');
|
||||
|
||||
describe('Git Handler', () => {
|
||||
it('should parse commit log', () => {
|
||||
const result = myFunction(testData);
|
||||
expect(result.length).toBe(3);
|
||||
expect(result[0].sha).toMatch(/^[a-f0-9]{7}/);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Tests starten:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
|
||||
# Mit Coverage:
|
||||
npm test -- --coverage
|
||||
|
||||
# Watch Mode:
|
||||
npm test -- --watch
|
||||
```
|
||||
|
||||
### Debugging im DevTools
|
||||
|
||||
```javascript
|
||||
// Im renderer.js oder irgendwo im Renderer:
|
||||
console.log('Debug Info:', data);
|
||||
|
||||
// Öffne DevTools: Strg+Shift+I
|
||||
// Console Tab → alle Logs sichtbar
|
||||
```
|
||||
|
||||
### Main Process debuggen
|
||||
|
||||
```javascript
|
||||
// In main.js:
|
||||
console.log('Main Process Log:', data);
|
||||
|
||||
// Logs erscheinen in der Terminal-Console
|
||||
// wenn die App mit npm start gestartet wurde
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Code-Konventionen
|
||||
|
||||
### Variablen & Funktionen
|
||||
|
||||
```javascript
|
||||
// camelCase für Variablen und Funktionen
|
||||
const repoName = "mein-projekt";
|
||||
function loadRepositories() { }
|
||||
|
||||
// SCREAMING_SNAKE_CASE für Konstanten
|
||||
const MAX_RETRIES = 5;
|
||||
const DEFAULT_TIMEOUT = 3000;
|
||||
|
||||
// Präfixe für Boolean-Variablen
|
||||
const isPrivate = true;
|
||||
const hasCredentials = false;
|
||||
const shouldRetry = true;
|
||||
```
|
||||
|
||||
### Async/Await
|
||||
|
||||
```javascript
|
||||
// Immer try-catch für async Funktionen
|
||||
async function loadData() {
|
||||
try {
|
||||
const data = await fetchFromAPI();
|
||||
return { ok: true, data };
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
return { ok: false, error: error.message };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Kommentare
|
||||
|
||||
```javascript
|
||||
// Kurze Inline-Kommentare über Code
|
||||
const delay = 1000; // Millisekunden
|
||||
|
||||
/**
|
||||
* Längere Beschreibung vor Funktion
|
||||
* @param {string} repoName - Name des Repositories
|
||||
* @returns {Promise<Object>} Result mit { ok, data, error }
|
||||
*/
|
||||
async function loadRepository(repoName) {
|
||||
// Implementierung
|
||||
}
|
||||
```
|
||||
|
||||
### Fehlerbehandlung
|
||||
|
||||
```javascript
|
||||
// Immer aussagekräftige Fehlermeldungen
|
||||
if (!token) {
|
||||
return { ok: false, error: 'missing-token' };
|
||||
}
|
||||
|
||||
// Oder sprechend für Benutzer:
|
||||
if (!token) {
|
||||
return { ok: false, error: 'Gitea Token nicht gespeichert. Bitte Settings öffnen.' };
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Häufige Aufgaben
|
||||
|
||||
### Neue Einstellung hinzufügen
|
||||
|
||||
1. **In `Settings.jsx` UI-Control hinzufügen**
|
||||
```jsx
|
||||
<label>Neue Option</label>
|
||||
<input type="checkbox" onChange={(e) => setSetting('newOption', e.target.checked)} />
|
||||
```
|
||||
|
||||
2. **In `renderer.js` speichern**
|
||||
```javascript
|
||||
async function saveSettings(settings) {
|
||||
const result = await window.electronAPI.saveSettings(settings);
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
3. **In `main.js` Handler**
|
||||
```javascript
|
||||
ipcMain.handle('saveSettings', async (event, settings) => {
|
||||
// Validation und Speicherung
|
||||
return { ok: true };
|
||||
});
|
||||
```
|
||||
|
||||
### Neuer Button in der Toolbar
|
||||
|
||||
1. **In `index.html` Button hinzufügen**
|
||||
```html
|
||||
<button id="btnMyFeature" title="Mein Feature">🎯 Feature</button>
|
||||
```
|
||||
|
||||
2. **In `renderer.js` Event-Handler**
|
||||
```javascript
|
||||
if ($('btnMyFeature')) {
|
||||
$('btnMyFeature').onclick = () => {
|
||||
myFeatureHandler();
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Neue Kontextmenü-Option
|
||||
|
||||
1. **In `showRepoContextMenu()` Item hinzufügen**
|
||||
```javascript
|
||||
items.push({
|
||||
label: '🎯 Meine Aktion',
|
||||
click: () => myActionHandler(owner, repo)
|
||||
});
|
||||
```
|
||||
|
||||
2. **Handler implementieren**
|
||||
```javascript
|
||||
function myActionHandler(owner, repo) {
|
||||
// Logik...
|
||||
}
|
||||
```
|
||||
|
||||
### Neue API-Methode aufrufen
|
||||
|
||||
1. **In `src/git/apiHandler.js` ergänzen**
|
||||
2. **In `main.js` IPC-Handler anlegen**
|
||||
3. **In `renderer.js` aufrufen:**
|
||||
```javascript
|
||||
const result = await window.electronAPI.myNewMethod({ param1, param2 });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting für Entwickler
|
||||
|
||||
### App startet nicht
|
||||
```bash
|
||||
# Node/npm Version prüfen
|
||||
node --version # sollte >= 16
|
||||
npm --version # sollte >= 7
|
||||
|
||||
# Dependencies neu installieren
|
||||
rm -r node_modules
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
### Fehler: "Cannot find module"
|
||||
```bash
|
||||
# Dependencies aktualisieren
|
||||
npm install
|
||||
|
||||
# Cache löschen
|
||||
npm cache clean --force
|
||||
```
|
||||
|
||||
### Axios/API-Calls schlagen fehl
|
||||
- URL/Token in Settings prüfen
|
||||
- Gitea-Server online? `curl https://git.example.com/api/v1/user`
|
||||
- Token noch gültig? Im Gitea-Interface regenerieren
|
||||
- Firewall/VPN blockiert?
|
||||
|
||||
### Datei-Upload funktioniert nicht
|
||||
- Lokale Temp-Pfade prüfen (in `main.js`)
|
||||
- Git-Auth konfiguriert?
|
||||
- Ausreichend Festplatte?
|
||||
|
||||
---
|
||||
|
||||
## Ressourcen
|
||||
|
||||
- [Electron Dokumentation](https://www.electronjs.org/docs)
|
||||
- [Gitea API](https://docs.gitea.io/en-us/api-usage/)
|
||||
- [GitHub API v3](https://docs.github.com/en/rest)
|
||||
- [Jest Testing](https://jestjs.io/docs/getting-started)
|
||||
|
||||
---
|
||||
|
||||
## Lizenz & Beitragen
|
||||
|
||||
Dieses Projekt ist unter der **MIT-Lizenz** lizenziert.
|
||||
Für Beiträge bitte einen **Pull Request** einreichen mit aussagekräftiger Beschreibung.
|
||||
|
||||
---
|
||||
|
||||
**Letzte Aktualisierung:** 13. Mai 2026 | **Ersteller:** M_Viper
|
||||
1394
HANDBUCH.html
Normal file
1394
HANDBUCH.html
Normal file
File diff suppressed because it is too large
Load Diff
233
README.md
233
README.md
@@ -1,61 +1,212 @@
|
||||
# Git Manager GUI
|
||||
# 🛠 Git Manager Explorer Pro
|
||||
|
||||
**Ein einfacher Desktop-Git-Manager für GitHub & Gitea mit Drag & Drop Funktionalität.**
|
||||
[](https://github.com/M-Viper/git-manager-gui/releases)
|
||||
[](LICENSE)
|
||||
[](https://github.com/M-Viper/git-manager-gui)
|
||||
|
||||
## Übersicht
|
||||
|
||||
Git Manager GUI ermöglicht es dir, deine lokalen Projekte und Gitea/GitHub-Repositories bequem zu verwalten:
|
||||
|
||||
- **Projekte durchsuchen** wie in einem Datei-Explorer
|
||||
- **Repositories laden** und direkt anzeigen
|
||||
- **Drag & Drop Upload/Download** von ganzen Projekten
|
||||
- **Repo erstellen**, aktualisieren und Pushen
|
||||
- **Dateien löschen, bearbeiten und previewen**
|
||||
|
||||
Alles in einer **intuitiven Benutzeroberfläche**.
|
||||
**Eine moderne Desktop-Anwendung für vollständige Git-Repository-Verwaltung mit Gitea & GitHub.**
|
||||
Verwalte deine Repositories intuitiv ohne Kommandozeile — mit Favoriten, Verlauf, Editor und Batch-Aktionen.
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
## ✨ Features
|
||||
|
||||
1. Lade die **Windows-Installer-EXE** von der Release-Seite herunter.
|
||||
2. Führe die EXE aus und folge den Installationsschritten.
|
||||
3. Optional: Desktop-Shortcut erstellen lassen.
|
||||
### 🎯 Repository-Verwaltung
|
||||
- 📂 **Intelligente Repo-Übersicht** mit Tabs nach Besitzer und Fuzzy-Suche
|
||||
- 🌐 **Gitea & GitHub Support** — seamless integration mit beiden Plattformen
|
||||
- 🎯 **Favoriten & Verlauf** — schnelle Navigation zu liebsten Projekten
|
||||
- 📥 **Ein-Klick Migration** — repositories zwischen Services verschieben
|
||||
- 🚀 **Batch-Aktionen** — dieselbe Operation auf mehreren Repos gleichzeitig
|
||||
|
||||
> Hinweis: Die App läuft **nur unter Windows**.
|
||||
### 💾 Datei-Verwaltung
|
||||
- ✏️ **Integrierter Editor** mit Syntax-Highlighting und Auto-Save
|
||||
- 📁 **Datei-Explorer** — navigiere durch Ordnerstruktur wie im Windows Explorer
|
||||
- 📤 **Drag & Drop Upload** — ziehe Dateien ins App-Fenster zum hochladen
|
||||
- 📥 **Drag & Drop Download** — ziehe Repos raus zum herunterladen
|
||||
- 🔄 **Rename & Move** — Dateien verwalten direkt im Editor
|
||||
|
||||
### 📊 Versionscontrol
|
||||
- 📊 **Commit-History** mit Filter nach Autor und Nachricht
|
||||
- 🏷️ **Tag-Management** — Tags erstellen, bearbeiten, löschen
|
||||
- 📦 **Release-Verwaltung** — Releases und Assets verwalten
|
||||
- 📈 **Aktivitäts-Heatmap** — Commit-Historie visualisiert wie auf GitHub
|
||||
- 🔁 **Retry-Queue** — fehlgeschlagene Ops automatisch erneut versuchen
|
||||
|
||||
### ⚙️ Erweiterte Funktionen
|
||||
- 🖼️ **Repo-Avatare hochladen** — Custom Projektbilder
|
||||
- 🏷️ **Auto-Topics** — Repositories mit Tags kategorisieren
|
||||
- 🔒 **Verschlüsselte Credentials** — alle Tokens AES-256 verschlüsselt
|
||||
- 🚀 **Auto-Startup** — App optional mit Windows starten
|
||||
- 🔄 **Auto-Update** — aktuelle Version automatisch verfügbar machen
|
||||
|
||||
---
|
||||
|
||||
## Nutzung
|
||||
## 📋 Systemvoraussetzungen
|
||||
|
||||
1. **Einstellungen**
|
||||
Öffne die Einstellungen und trage ein:
|
||||
- GitHub Token (für GitHub Repos)
|
||||
- Gitea Token & URL (für Gitea Repos)
|
||||
|
||||
2. **Projekte laden**
|
||||
- Klicke auf **Load Gitea Repos**, um deine Repositories zu sehen
|
||||
- Wähle einen lokalen Ordner über **Select Folder** aus
|
||||
|
||||
3. **Drag & Drop**
|
||||
- **Download**: Ziehe ein Repository aus der Liste auf deinen Desktop → komplettes Projekt wird heruntergeladen
|
||||
- **Upload**: Ziehe einen lokalen Projektordner auf ein Repository → Inhalte werden hochgeladen und in Git aktualisiert
|
||||
|
||||
4. **Rechtsklick-Menü**
|
||||
- Auf Repositories klicken → Optionen wie **Download**, **Upload**, **Löschen**
|
||||
|
||||
5. **Branches & Commits**
|
||||
- Branches auswählen und Commit-Logs einsehen
|
||||
- Push-Button zum Aktualisieren des Repos verwenden
|
||||
| Anforderung | Mindest | Empfohlen |
|
||||
|-------------|--------|----------|
|
||||
| **OS** | Windows 10 64-bit | Windows 11 |
|
||||
| **RAM** | 4 GB | 8 GB |
|
||||
| **Festplatte** | 200 MB | 1 GB (für Repos) |
|
||||
| **.NET** | Nicht erforderlich | — |
|
||||
| **Node.js** | Nicht erforderlich | — |
|
||||
|
||||
---
|
||||
|
||||
## Support
|
||||
## 📦 Installation
|
||||
|
||||
Bei Problemen oder Fragen kannst du ein Issue auf GitHub erstellen.
|
||||
### Über Installer (empfohlen)
|
||||
|
||||
1. Gehe zu [Releases](https://github.com/M-Viper/git-manager-gui/releases)
|
||||
2. Lade die neueste `.exe`-Datei herunter
|
||||
3. Starte die EXE und folge dem Installations-Assistenten
|
||||
4. App startet automatisch nach Installation
|
||||
|
||||
### Manuell (Entwickler)
|
||||
|
||||
```bash
|
||||
git clone https://github.com/M-Viper/git-manager-gui.git
|
||||
cd git-manager-gui
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Lizenz
|
||||
## 🚀 Quick Start
|
||||
|
||||
### 1️⃣ Credentials konfigurieren
|
||||
|
||||
1. Starte die App
|
||||
2. Klicke auf **⚙️ Settings**
|
||||
3. Trage ein:
|
||||
- **Gitea URL** (z. B. `https://git.example.com`)
|
||||
- **Gitea Token** (von Gitea → Einstellungen → Anwendungen)
|
||||
- **GitHub Token** (optional, für GitHub Repos)
|
||||
4. Klicke **Verbindung testen** → grüne Haken = erfolgreich ✓
|
||||
|
||||
### 2️⃣ Repositories laden
|
||||
|
||||
- Klicke **🌐 Load Gitea** um alle deine Repos zu sehen
|
||||
- Nutze die **Owner-Tabs** um nach Besitzer zu filtern
|
||||
- Nutze die **Fuzzy-Suche** um schnell ein Repo zu finden
|
||||
|
||||
### 3️⃣ Repository öffnen
|
||||
|
||||
- **Klick** auf eine Repository-Karte → Datei-Explorer
|
||||
- **Rechtsklick** → Kontextmenü mit Optionen (Favorit, Clone-URL, Tags, etc.)
|
||||
- **Drag & Drop** zum Download/Upload
|
||||
|
||||
### 4️⃣ Dateien bearbeiten
|
||||
|
||||
- Klick auf eine Textdatei → Editor öffnet sich
|
||||
- **Ctrl+S** zum Speichern (gepusht automatisch auf Gitea)
|
||||
- **Ctrl+F** zum Suchen & Ersetzen
|
||||
|
||||
---
|
||||
|
||||
## 📚 Dokumentation
|
||||
|
||||
### Für Benutzer
|
||||
👉 **[HANDBUCH.html](HANDBUCH.html)** — Vollständiges Benutzerhandbuch
|
||||
- Alle Features erklärt
|
||||
- Schritt-für-Schritt Anleitungen
|
||||
- Screenshots & Mockups
|
||||
- Häufige Fehler & Lösungen
|
||||
|
||||
### Für Entwickler
|
||||
👉 **[DEV_GUIDE.md](DEV_GUIDE.md)** — Entwicklerhandbuch
|
||||
- Projektstruktur & Architektur
|
||||
- Setup für lokale Entwicklung
|
||||
- API-Integration & neue Features
|
||||
- Code-Konventionen
|
||||
- Build & Release-Prozess
|
||||
|
||||
---
|
||||
|
||||
## 🎮 Tastenkürzel
|
||||
|
||||
| Shortcut | Aktion |
|
||||
|----------|--------|
|
||||
| <kbd>Ctrl</kbd>+<kbd>K</kbd> | Repo-Suche fokussieren |
|
||||
| <kbd>Ctrl</kbd>+<kbd>S</kbd> | Datei im Editor speichern |
|
||||
| <kbd>Ctrl</kbd>+<kbd>F</kbd> | Im Editor suchen |
|
||||
| <kbd>Escape</kbd> | Suche schließen |
|
||||
| <kbd>Rechtsklick</kbd> | Kontextmenü (Repos & Dateien) |
|
||||
| <kbd>Drag</kbd> raus | Datei/Repo herunterladen |
|
||||
| <kbd>Drag</kbd> rein | Datei hochladen |
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Häufige Probleme
|
||||
|
||||
### "401 Unauthorized"
|
||||
- **Ursache**: Token ungültig oder abgelaufen
|
||||
- **Lösung**: Neues Token auf Gitea generieren (Einstellungen → Anwendungen) und in Settings eintragen
|
||||
|
||||
### "ECONNREFUSED" / Server nicht erreichbar
|
||||
- **Ursache**: Gitea-URL nicht korrekt oder Server down
|
||||
- **Lösung**: URL prüfen; bei IPv6: Klammern verwenden `http://[::1]:3000`
|
||||
|
||||
### Repositories werden nicht angezeigt
|
||||
- **Ursache**: Owner-Tab "Meine" ist vorausgewählt (filtert fremde Repos)
|
||||
- **Lösung**: Tab "Alle" wählen oder nach Name suchen
|
||||
|
||||
### Editor speichert nicht
|
||||
- **Ursache**: Token hat keine Schreibrechte
|
||||
- **Lösung**: Token-Berechtigungen prüfen (mindestens `repository:write` nötig)
|
||||
|
||||
Weitere Lösungen im [HANDBUCH.html](HANDBUCH.html#häufige-fehler--lösungen).
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Beitragen
|
||||
|
||||
Fehler gefunden oder Idee für neue Features?
|
||||
|
||||
1. [Erstelle ein Issue](https://github.com/M-Viper/git-manager-gui/issues/new) mit Beschreibung
|
||||
2. Fork das Repo und erstelle einen Branch: `git checkout -b feature/meine-idee`
|
||||
3. Commit deine Änderungen: `git commit -m "Add: meine neue Funktion"`
|
||||
4. Push zum Branch: `git push origin feature/meine-idee`
|
||||
5. Öffne einen Pull Request
|
||||
|
||||
👉 Siehe [DEV_GUIDE.md](DEV_GUIDE.md) für Entwickler-Setup.
|
||||
|
||||
---
|
||||
|
||||
## 📝 Changelog
|
||||
|
||||
### v2.1.3 (13. Mai 2026)
|
||||
- ✨ Projektname wird korrekt ausgeblendet bei Wechsel zur Übersicht
|
||||
- 🐛 Bug-Fix: Zurück-Button setzt Gravur korrekt zurück
|
||||
- 📚 Erweiterte Dokumentation (Benutzer- & Entwicklerhandbuch)
|
||||
|
||||
### v2.1.0
|
||||
- 🎯 Neue Batch-Aktionen
|
||||
- 📊 Aktivitäts-Heatmap
|
||||
- 🔄 Verbesserte Fehlerbehandlung
|
||||
|
||||
[Vollständiger Changelog →](https://github.com/M-Viper/git-manager-gui/releases)
|
||||
|
||||
---
|
||||
|
||||
## 📜 Lizenz
|
||||
|
||||
Dieses Projekt ist unter der **MIT-Lizenz** lizenziert — siehe [LICENSE](LICENSE).
|
||||
|
||||
---
|
||||
|
||||
## 👤 Autor
|
||||
|
||||
**M_Viper** — [GitHub](https://github.com/M-Viper) | [Website](https://m-viper.de)
|
||||
|
||||
---
|
||||
|
||||
## 💬 Support & Kontakt
|
||||
|
||||
- 📧 **Email**: admin@m-viper.de
|
||||
- 🐛 **Issues**: [GitHub Issues](https://github.com/M-Viper/git-manager-gui/issues)
|
||||
- 📖 **Handbuch**: [HANDBUCH.html](HANDBUCH.html)
|
||||
- 👨💻 **Entwicklung**: [DEV_GUIDE.md](DEV_GUIDE.md)
|
||||
|
||||
Dieses Projekt ist Open-Source unter der [GPL Lizenz](LICENSE).
|
||||
|
||||
0
__tests__/gitHandler.test.js
Normal file
0
__tests__/gitHandler.test.js
Normal file
BIN
assets/Thumbs.db
Normal file
BIN
assets/Thumbs.db
Normal file
Binary file not shown.
665
landing.html
Normal file
665
landing.html
Normal file
@@ -0,0 +1,665 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Git Manager Explorer Pro — Desktop Git Manager für Gitea & GitHub</title>
|
||||
<meta name="description" content="Verwalte deine Gitea & GitHub Repositories auf dem Desktop — mit Editor, Favoriten, Batch-Aktionen und mehr.">
|
||||
<meta name="keywords" content="Git, Gitea, GitHub, Desktop, Manager, Repository, Electron, Windows">
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--accent: #58d5ff;
|
||||
--accent2: #67e8f9;
|
||||
--blue: #3b82f6;
|
||||
--green: #22c55e;
|
||||
--red: #ef4444;
|
||||
--purple: #a855f7;
|
||||
--bg-dark: #0d1520;
|
||||
--bg-mid: #0f1e30;
|
||||
--bg-card: #1a2b3c;
|
||||
--text: #e2eaf5;
|
||||
--text-muted: #7a8fab;
|
||||
--border: rgba(88, 213, 255, 0.18);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', system-ui, sans-serif;
|
||||
background: linear-gradient(135deg, #0d1520 0%, #0a1b30 50%, #0f1e35 100%);
|
||||
color: var(--text);
|
||||
line-height: 1.6;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Header / Navigation */
|
||||
header {
|
||||
padding: 20px 40px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--border);
|
||||
backdrop-filter: blur(10px);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
background: linear-gradient(135deg, var(--accent2) 0%, var(--blue) 100%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: rgba(88, 213, 255, 0.15);
|
||||
border: 1px solid rgba(88, 213, 255, 0.3);
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
nav a {
|
||||
color: var(--text-muted);
|
||||
text-decoration: none;
|
||||
transition: color 0.3s;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
nav a:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
transition: all 0.3s;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--accent) 0%, var(--blue) 100%);
|
||||
color: #0a1625;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(88, 213, 255, 0.3);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
/* Hero Section */
|
||||
.hero {
|
||||
padding: 100px 40px;
|
||||
text-align: center;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-size: 54px;
|
||||
font-weight: 900;
|
||||
margin-bottom: 16px;
|
||||
background: linear-gradient(135deg, var(--accent2) 0%, var(--purple) 100%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.hero p {
|
||||
font-size: 18px;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 32px;
|
||||
max-width: 600px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.hero-cta {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
justify-content: center;
|
||||
margin-bottom: 64px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.hero-buttons {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.hero-buttons .btn {
|
||||
padding: 14px 32px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* Features Grid */
|
||||
.section {
|
||||
padding: 80px 40px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 36px;
|
||||
font-weight: 800;
|
||||
text-align: center;
|
||||
margin-bottom: 16px;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.section-desc {
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 48px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 24px;
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 32px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
background: rgba(88, 213, 255, 0.05);
|
||||
border-color: rgba(88, 213, 255, 0.3);
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
font-size: 40px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.feature-card h3 {
|
||||
font-size: 18px;
|
||||
margin-bottom: 12px;
|
||||
color: var(--accent2);
|
||||
}
|
||||
|
||||
.feature-card p {
|
||||
color: var(--text-muted);
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Screenshots / Demo */
|
||||
.demo-section {
|
||||
text-align: center;
|
||||
padding-top: 40px;
|
||||
}
|
||||
|
||||
.demo-mockup {
|
||||
background: linear-gradient(135deg, rgba(88, 213, 255, 0.1) 0%, rgba(92, 135, 255, 0.1) 100%);
|
||||
border: 2px solid rgba(88, 213, 255, 0.2);
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
margin-top: 32px;
|
||||
max-width: 900px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
min-height: 400px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* System Requirements */
|
||||
.requirements {
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 32px;
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
.requirements h3 {
|
||||
font-size: 18px;
|
||||
margin-bottom: 24px;
|
||||
color: var(--accent2);
|
||||
}
|
||||
|
||||
.req-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.req-item {
|
||||
padding: 16px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border-radius: 8px;
|
||||
border-left: 3px solid var(--accent);
|
||||
}
|
||||
|
||||
.req-item strong {
|
||||
color: var(--accent2);
|
||||
}
|
||||
|
||||
.req-item span {
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* Download Section */
|
||||
.download-section {
|
||||
text-align: center;
|
||||
padding: 80px 40px;
|
||||
background: linear-gradient(135deg, rgba(88, 213, 255, 0.05) 0%, rgba(92, 135, 255, 0.05) 100%);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
margin-top: 80px;
|
||||
}
|
||||
|
||||
.download-section h2 {
|
||||
font-size: 32px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.download-section p {
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.download-buttons {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* Stats */
|
||||
.stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 32px;
|
||||
margin-bottom: 80px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-item h3 {
|
||||
font-size: 32px;
|
||||
font-weight: 800;
|
||||
color: var(--accent2);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-item p {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Links / Resources */
|
||||
.links-section {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 24px;
|
||||
margin-top: 48px;
|
||||
}
|
||||
|
||||
.link-card {
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.link-card h3 {
|
||||
margin-bottom: 12px;
|
||||
color: var(--accent2);
|
||||
}
|
||||
|
||||
.link-card p {
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.link-card a {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.link-card a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
footer {
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
border-top: 1px solid var(--border);
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
margin-top: 80px;
|
||||
}
|
||||
|
||||
footer a {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
footer a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
header {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
nav {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.hero {
|
||||
padding: 60px 20px;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.hero p {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: 60px 20px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.hero-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.hero-buttons .btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Header -->
|
||||
<header>
|
||||
<div class="logo">
|
||||
<div class="logo-icon">🛠</div>
|
||||
<span>Git Manager Pro</span>
|
||||
</div>
|
||||
<nav>
|
||||
<a href="#features">Features</a>
|
||||
<a href="#download">Download</a>
|
||||
<a href="HANDBUCH.html" target="_blank">Dokumentation</a>
|
||||
<a href="https://git.viper.ipv64.net/M_Viper/Git-Manager-Gui" target="_blank">Gitea</a>
|
||||
<a href="#contact">Kontakt</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<!-- Hero -->
|
||||
<section class="hero">
|
||||
<h1>Git Manager Explorer Pro</h1>
|
||||
<p>
|
||||
Verwalte deine Gitea & GitHub Repositories auf dem Desktop.<br>
|
||||
Mit Editor, Favoriten, Batch-Aktionen und mehr — alles ohne Kommandozeile.
|
||||
</p>
|
||||
<div class="hero-buttons">
|
||||
<a href="#download" class="btn btn-primary">⬇️ Jetzt Download (v2.1.3)</a>
|
||||
<a href="HANDBUCH.html" target="_blank" class="btn btn-secondary">📖 Handbuch lesen</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Stats -->
|
||||
<section class="section">
|
||||
<div class="stats">
|
||||
<div class="stat-item">
|
||||
<h3>100+</h3>
|
||||
<p>Repos gleichzeitig verwalten</p>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<h3>2.1.3</h3>
|
||||
<p>Aktuelle Version</p>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<h3>⚡</h3>
|
||||
<p>Schnell & zuverlässig</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Features -->
|
||||
<section class="section" id="features">
|
||||
<h2 class="section-title">Warum Git Manager Pro?</h2>
|
||||
<p class="section-desc">Alles was du brauchst um deine Repositories effizient zu verwalten.</p>
|
||||
|
||||
<div class="features-grid">
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">🌐</div>
|
||||
<h3>Gitea & GitHub</h3>
|
||||
<p>Unterstützt beide Plattformen seamless — alle Repositories an einem Ort.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">📂</div>
|
||||
<h3>Datei-Explorer</h3>
|
||||
<p>Navigiere durch Ordnerstruktur wie im Windows Explorer. Mit integriertem Editor.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">⭐</div>
|
||||
<h3>Favoriten & Verlauf</h3>
|
||||
<p>Schnelle Navigation zu Lieblingsprojekten. Automatisch gespeichert.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">📤</div>
|
||||
<h3>Drag & Drop</h3>
|
||||
<p>Ziehe Dateien rein zum Upload, raus zum Download — direkt im App-Fenster.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">🧩</div>
|
||||
<h3>Batch-Aktionen</h3>
|
||||
<p>Wende dieselbe Operation auf mehrere Repositories gleichzeitig an.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">📊</div>
|
||||
<h3>Commit-History</h3>
|
||||
<p>Sieh alle Commits, Tags und Releases. Mit Aktivitäts-Heatmap wie auf GitHub.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">✏️</div>
|
||||
<h3>Code-Editor</h3>
|
||||
<p>Bearbeite Dateien direkt in der App. Mit Syntax-Highlighting und Auto-Save.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">🔒</div>
|
||||
<h3>Sichere Credentials</h3>
|
||||
<p>Alle Tokens verschlüsselt (AES-256) — deine Daten sind sicher.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">📥</div>
|
||||
<h3>Ein-Klick Migration</h3>
|
||||
<p>Migriere Repos zwischen Services — mit kompletter History.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3 style="color: var(--accent2); margin-bottom: 16px;">🎥 Demo (baldiger Update)</h3>
|
||||
<div class="demo-mockup">
|
||||
Screenshot-Bereich — Demo und UI-Preview kommen bald!<br><br>
|
||||
<em>Starte die App mit npm start um einen Live-Demo zu sehen</em>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- System Requirements -->
|
||||
<section class="section">
|
||||
<div class="requirements">
|
||||
<h3>💻 Systemvoraussetzungen</h3>
|
||||
<div class="req-grid">
|
||||
<div class="req-item">
|
||||
<strong>Windows</strong>
|
||||
<span>10 oder 11 (64-bit)</span>
|
||||
</div>
|
||||
<div class="req-item">
|
||||
<strong>RAM</strong>
|
||||
<span>Mindestens 4 GB</span>
|
||||
</div>
|
||||
<div class="req-item">
|
||||
<strong>Festplatte</strong>
|
||||
<span>200 MB für App + Repos</span>
|
||||
</div>
|
||||
<div class="req-item">
|
||||
<strong>Internet</strong>
|
||||
<span>Für Gitea/GitHub Zugriff</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Download -->
|
||||
<section class="section" id="download">
|
||||
<div class="download-section">
|
||||
<h2>⬇️ Git Manager Pro herunterladen</h2>
|
||||
<p>Wähle eine Option um zu starten:</p>
|
||||
<div class="download-buttons">
|
||||
<a href="https://git.viper.ipv64.net/M_Viper/Git-Manager-Gui/releases/download/v2.1.3/git-manager-gui-Setup-2.1.3.exe" class="btn btn-primary">
|
||||
📦 Installer (v2.1.3)
|
||||
</a>
|
||||
<a href="https://git.viper.ipv64.net/M_Viper/Git-Manager-Gui/releases" target="_blank" class="btn btn-secondary">
|
||||
📋 Alle Versionen
|
||||
</a>
|
||||
<a href="https://git.viper.ipv64.net/M_Viper/Git-Manager-Gui" target="_blank" class="btn btn-secondary">
|
||||
💻 Source Code
|
||||
</a>
|
||||
</div>
|
||||
<p style="color: var(--text-muted); margin-top: 24px; font-size: 13px;">
|
||||
✨ Kostenlos, Open Source, MIT Lizenz
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Resources / Links -->
|
||||
<section class="section">
|
||||
<h2 class="section-title">Ressourcen & Support</h2>
|
||||
|
||||
<div class="links-section">
|
||||
<div class="link-card">
|
||||
<h3>📖 Benutzerhandbuch</h3>
|
||||
<p>Vollständiges Handbuch mit Screenshots und Schritt-für-Schritt Anleitungen.</p>
|
||||
<a href="HANDBUCH.html" target="_blank">Zum Handbuch →</a>
|
||||
</div>
|
||||
<div class="link-card">
|
||||
<h3>👨💻 Entwicklerguide</h3>
|
||||
<p>Für Entwickler — Projektstruktur, API-Integration, Build-Prozess.</p>
|
||||
<a href="DEV_GUIDE.md" target="_blank">Zum Dev Guide →</a>
|
||||
</div>
|
||||
<div class="link-card">
|
||||
<h3>🐛 Issues & Support</h3>
|
||||
<p>Fehler gefunden? Idee für neues Feature? Schreib ein Issue auf Gitea.</p>
|
||||
<a href="https://git.viper.ipv64.net/M_Viper/Git-Manager-Gui/issues" target="_blank">Zu Issues →</a>
|
||||
</div>
|
||||
<div class="link-card">
|
||||
<h3>⭐ Gitea Repository</h3>
|
||||
<p>Source Code, Roadmap und Releases — vollständig open source.</p>
|
||||
<a href="https://git.viper.ipv64.net/M_Viper/Git-Manager-Gui" target="_blank">Zum Repo →</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- FAQ Mini -->
|
||||
<section class="section">
|
||||
<h2 class="section-title">❓ Häufige Fragen</h2>
|
||||
|
||||
<div style="max-width: 800px; margin: 0 auto;">
|
||||
<div style="background: rgba(255,255,255,0.02); border: 1px solid var(--border); border-radius: 12px; padding: 24px; margin-bottom: 16px;">
|
||||
<h4 style="color: var(--accent2); margin-bottom: 8px;">Kostet die App etwas?</h4>
|
||||
<p style="color: var(--text-muted);">Nein, Git Manager Pro ist kostenlos und Open Source unter der MIT-Lizenz.</p>
|
||||
</div>
|
||||
<div style="background: rgba(255,255,255,0.02); border: 1px solid var(--border); border-radius: 12px; padding: 24px; margin-bottom: 16px;">
|
||||
<h4 style="color: var(--accent2); margin-bottom: 8px;">Funktioniert es nur mit Gitea?</h4>
|
||||
<p style="color: var(--text-muted);">Nein, die App unterstützt sowohl Gitea als auch GitHub. Du kannst zwischen den Plattformen wechseln.</p>
|
||||
</div>
|
||||
<div style="background: rgba(255,255,255,0.02); border: 1px solid var(--border); border-radius: 12px; padding: 24px; margin-bottom: 16px;">
|
||||
<h4 style="color: var(--accent2); margin-bottom: 8px;">Sind meine Credentials sicher?</h4>
|
||||
<p style="color: var(--text-muted);">Ja, alle Tokens und Passwörter werden lokal mit AES-256 verschlüsselt. Nie auf Servern gespeichert.</p>
|
||||
</div>
|
||||
<div style="background: rgba(255,255,255,0.02); border: 1px solid var(--border); border-radius: 12px; padding: 24px;">
|
||||
<h4 style="color: var(--accent2); margin-bottom: 8px;">Kann ich beitragen?</h4>
|
||||
<p style="color: var(--text-muted);">Gerne! Siehe den DEV_GUIDE.md für Setup-Anleitung und sieh dir unsere <a href="https://git.viper.ipv64.net/M_Viper/Git-Manager-Gui/issues" target="_blank" style="color: var(--accent); text-decoration: none;">offenen Issues</a> an.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer>
|
||||
<p>
|
||||
Git Manager Explorer Pro © 2026 —
|
||||
<a href="https://m-viper.de" target="_blank">m-viper.de</a> |
|
||||
<a href="https://git.viper.ipv64.net/M_Viper/Git-Manager-Gui" target="_blank">Gitea Repository</a> |
|
||||
<a href="HANDBUCH.html" target="_blank">Handbuch</a> |
|
||||
<a href="DEV_GUIDE.md" target="_blank">Dev Guide</a>
|
||||
</p>
|
||||
<p style="margin-top: 16px; opacity: 0.7;">
|
||||
Made with ❤️ von M_Viper — <a href="mailto:admin@m-viper.de">Kontakt</a>
|
||||
</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
1969
package-lock.json
generated
1969
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
43
package.json
43
package.json
@@ -1,27 +1,56 @@
|
||||
{
|
||||
"name": "git-manager-gui",
|
||||
"version": "2.0.0",
|
||||
"version": "2.1.3",
|
||||
"description": "Git Manager GUI - Verwaltung von Git Repositories",
|
||||
"author": "M_Viper",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"start": "electron .",
|
||||
"build": "electron-builder"
|
||||
"build": "electron-builder",
|
||||
"test": "node --test __tests__/*.test.js",
|
||||
"pdf": "electron generate-pdf.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.13.4",
|
||||
"axios": "^1.13.5",
|
||||
"form-data": "^4.0.5",
|
||||
"simple-git": "^3.19.1"
|
||||
"simple-git": "^3.32.3",
|
||||
"archiver": "^6.0.0",
|
||||
"unzipper": "^0.11.0"
|
||||
},
|
||||
"overrides": {
|
||||
"minimatch": "^3.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "^26.2.0",
|
||||
"electron-builder": "^26.0.12"
|
||||
"electron": "^41.0.4",
|
||||
"electron-builder": "^26.8.1"
|
||||
},
|
||||
"build": {
|
||||
"asar": true,
|
||||
"appId": "com.viper.gitmanager",
|
||||
"productName": "Git Manager GUI",
|
||||
"files": [
|
||||
"**/*",
|
||||
"!node_modules/.cache",
|
||||
"!**/*.map"
|
||||
"!**/*.map",
|
||||
"!**/.git",
|
||||
"!**/.git/**",
|
||||
"!repos/**",
|
||||
"!data/**",
|
||||
"!backup/**",
|
||||
"!*.zip",
|
||||
"!*.rar",
|
||||
"!*.log"
|
||||
],
|
||||
"extraResources": [
|
||||
"repos/",
|
||||
{
|
||||
"from": "data",
|
||||
"to": "data",
|
||||
"filter": [
|
||||
"**/*",
|
||||
"!credentials.json"
|
||||
]
|
||||
}
|
||||
],
|
||||
"directories": {
|
||||
"buildResources": "assets"
|
||||
|
||||
102
preload.js
102
preload.js
@@ -1,5 +1,5 @@
|
||||
// preload.js — expose IPC to renderer
|
||||
const { contextBridge, ipcRenderer } = require('electron');
|
||||
const { contextBridge, ipcRenderer, webUtils } = require('electron');
|
||||
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
// Lokale Datei-Operationen
|
||||
@@ -10,9 +10,27 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
writeFile: (data) => ipcRenderer.invoke('writeFile', data),
|
||||
deleteFile: (data) => ipcRenderer.invoke('deleteFile', data),
|
||||
|
||||
// Umbenennen, Erstellen, Verschieben, Kopieren
|
||||
renameGiteaItem: (data) => ipcRenderer.invoke('rename-gitea-item', data),
|
||||
createGiteaItem: (data) => ipcRenderer.invoke('create-gitea-item', data),
|
||||
renameLocalItem: (data) => ipcRenderer.invoke('rename-local-item', data),
|
||||
createLocalItem: (data) => ipcRenderer.invoke('create-local-item', data),
|
||||
moveLocalItem: (data) => ipcRenderer.invoke('move-local-item', data),
|
||||
copyLocalItem: (data) => ipcRenderer.invoke('copy-local-item', data),
|
||||
|
||||
// Gitea Datei-Operationen
|
||||
listGiteaRepos: (data) => ipcRenderer.invoke('list-gitea-repos', data),
|
||||
getGiteaCurrentUser: () => ipcRenderer.invoke('get-gitea-current-user'),
|
||||
getGiteaUserHeatmap: (data) => ipcRenderer.invoke('get-gitea-user-heatmap', data),
|
||||
|
||||
// GitHub Datei-Operationen
|
||||
listGithubRepos: (data) => ipcRenderer.invoke('list-github-repos', data),
|
||||
getGithubCurrentUser: () => ipcRenderer.invoke('get-github-current-user'),
|
||||
getGithubUserHeatmap: (data) => ipcRenderer.invoke('get-github-user-heatmap', data),
|
||||
getGiteaRepoContents: (data) => ipcRenderer.invoke('get-gitea-repo-contents', data),
|
||||
listGiteaTrash: () => ({ ok: true, items: [] }),
|
||||
purgeGiteaTrash: () => ({ ok: true }),
|
||||
restoreGiteaTrashItem: () => ({ ok: false, error: 'Papierkorb deaktiviert' }),
|
||||
getGiteaFileContent: (data) => ipcRenderer.invoke('get-gitea-file-content', data),
|
||||
readGiteaFile: (data) => ipcRenderer.invoke('read-gitea-file', data),
|
||||
writeGiteaFile: (data) => ipcRenderer.invoke('write-gitea-file', data),
|
||||
@@ -24,16 +42,46 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
// Repository & Git Management
|
||||
saveCredentials: (data) => ipcRenderer.invoke('save-credentials', data),
|
||||
loadCredentials: () => ipcRenderer.invoke('load-credentials'),
|
||||
getCredentialsStatus: () => ipcRenderer.invoke('get-credentials-status'),
|
||||
exportSettingsBundle: () => ipcRenderer.invoke('export-settings-bundle'),
|
||||
importSettingsBundle: () => ipcRenderer.invoke('import-settings-bundle'),
|
||||
createDiagnosticsPackage: () => ipcRenderer.invoke('create-diagnostics-package'),
|
||||
testGiteaConnection: (data) => ipcRenderer.invoke('test-gitea-connection', data),
|
||||
testGithubConnection: (data) => ipcRenderer.invoke('test-github-connection', data),
|
||||
updateGiteaAvatar: (data) => ipcRenderer.invoke('update-gitea-avatar', data),
|
||||
updateGiteaRepoAvatar: (data) => ipcRenderer.invoke('update-gitea-repo-avatar', data),
|
||||
updateGiteaRepoVisibility: (data) => ipcRenderer.invoke('update-gitea-repo-visibility', data),
|
||||
updateRepoArchived: (data) => ipcRenderer.invoke('update-repo-archived', data),
|
||||
updateGiteaRepoTopics: (data) => ipcRenderer.invoke('update-gitea-repo-topics', data),
|
||||
getGiteaTopicsCatalog: () => ipcRenderer.invoke('get-gitea-topics-catalog'),
|
||||
migrateRepoToGitea: (data) => ipcRenderer.invoke('migrate-repo-to-gitea', data),
|
||||
createRepo: (data) => ipcRenderer.invoke('create-repo', data),
|
||||
pushProject: (data) => ipcRenderer.invoke('push-project', data),
|
||||
getBranches: (data) => ipcRenderer.invoke('getBranches', data),
|
||||
getCommitLogs: (data) => ipcRenderer.invoke('getCommitLogs', data),
|
||||
uploadAndPush: (data) => ipcRenderer.invoke('upload-and-push', data),
|
||||
deleteGiteaRepo: (data) => ipcRenderer.invoke('delete-gitea-repo', data),
|
||||
syncRepoToGitHub: (data) => ipcRenderer.invoke('sync-repo-to-github', data),
|
||||
runBatchRepoAction: (data) => ipcRenderer.invoke('run-batch-repo-action', data),
|
||||
validateRepoName: (data) => ipcRenderer.invoke('validate-repo-name', data),
|
||||
checkCloneTargetCollisions: (data) => ipcRenderer.invoke('check-clone-target-collisions', data),
|
||||
|
||||
// Offline/Retry Queue
|
||||
getRetryQueue: () => ipcRenderer.invoke('get-retry-queue'),
|
||||
processRetryQueueNow: () => ipcRenderer.invoke('process-retry-queue-now'),
|
||||
removeRetryQueueItem: (data) => ipcRenderer.invoke('remove-retry-queue-item', data),
|
||||
|
||||
// Drag & Drop
|
||||
prepareDownloadDrag: (data) => ipcRenderer.invoke('prepare-download-drag', data),
|
||||
startNativeDrag: (filePath) => ipcRenderer.send('ondragstart', filePath),
|
||||
getPathType: (filePath) => ipcRenderer.invoke('get-path-type', filePath),
|
||||
getPathForFile: (file) => {
|
||||
try {
|
||||
return webUtils.getPathForFile(file) || '';
|
||||
} catch (_) {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
|
||||
// Release Management
|
||||
listReleases: (data) => ipcRenderer.invoke('list-releases', data),
|
||||
@@ -59,8 +107,14 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
getLocalCommitDetails: (data) => ipcRenderer.invoke('get-local-commit-details', data),
|
||||
searchLocalCommits: (data) => ipcRenderer.invoke('search-local-commits', data),
|
||||
|
||||
// === FAVORITEN & ZULETZT GEÖFFNET ===
|
||||
loadFavorites: () => ipcRenderer.invoke('load-favorites'),
|
||||
saveFavorites: (data) => ipcRenderer.invoke('save-favorites', data),
|
||||
loadRecent: () => ipcRenderer.invoke('load-recent'),
|
||||
saveRecent: (data) => ipcRenderer.invoke('save-recent', data),
|
||||
|
||||
// === UPDATER APIs ===
|
||||
checkForUpdates: () => ipcRenderer.invoke('check-for-updates'),
|
||||
checkForUpdates: (options) => ipcRenderer.invoke('check-for-updates', options || {}),
|
||||
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
|
||||
|
||||
// Triggert den tatsächlichen Download des Assets
|
||||
@@ -74,12 +128,24 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
return () => ipcRenderer.removeListener('update-available', listener);
|
||||
},
|
||||
|
||||
onUpdateNotAvailable: (cb) => {
|
||||
const listener = (event, info) => cb(info);
|
||||
ipcRenderer.on('update-not-available', listener);
|
||||
return () => ipcRenderer.removeListener('update-not-available', listener);
|
||||
},
|
||||
|
||||
onUpdateProgress: (cb) => {
|
||||
const listener = (event, percent) => cb(percent);
|
||||
ipcRenderer.on('update-progress', listener);
|
||||
return () => ipcRenderer.removeListener('update-progress', listener);
|
||||
},
|
||||
|
||||
onUpdateError: (cb) => {
|
||||
const listener = (event, payload) => cb(payload);
|
||||
ipcRenderer.on('update-error', listener);
|
||||
return () => ipcRenderer.removeListener('update-error', listener);
|
||||
},
|
||||
|
||||
onPushProgress: (cb) => {
|
||||
const listener = (event, percent) => { try { cb(percent); } catch (_) {} };
|
||||
ipcRenderer.on('push-progress', listener);
|
||||
@@ -96,5 +162,35 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
const listener = (event, payload) => { try { cb(payload); } catch (_) {} };
|
||||
ipcRenderer.on('folder-download-progress', listener);
|
||||
return () => ipcRenderer.removeListener('folder-download-progress', listener);
|
||||
}
|
||||
},
|
||||
|
||||
onRetryQueueUpdated: (cb) => {
|
||||
const listener = (event, payload) => { try { cb(payload); } catch (_) {} };
|
||||
ipcRenderer.on('retry-queue-updated', listener);
|
||||
return () => ipcRenderer.removeListener('retry-queue-updated', listener);
|
||||
},
|
||||
|
||||
onBatchActionProgress: (cb) => {
|
||||
const listener = (event, payload) => { try { cb(payload); } catch (_) {} };
|
||||
ipcRenderer.on('batch-action-progress', listener);
|
||||
return () => ipcRenderer.removeListener('batch-action-progress', listener);
|
||||
},
|
||||
|
||||
// Window Controls
|
||||
windowMinimize: () => ipcRenderer.send('window-minimize'),
|
||||
windowMaximize: () => ipcRenderer.send('window-maximize'),
|
||||
windowClose: () => ipcRenderer.send('window-close'),
|
||||
|
||||
// Autostart
|
||||
setAutostart: (enable) => ipcRenderer.invoke('set-autostart', enable),
|
||||
getAutostart: () => ipcRenderer.invoke('get-autostart'),
|
||||
|
||||
// Utility
|
||||
copyToClipboard: (text) => ipcRenderer.invoke('copy-to-clipboard', text),
|
||||
openExternalUrl: (url) => ipcRenderer.invoke('open-external-url', url),
|
||||
debugToMain: (level, message, payload) => ipcRenderer.send('renderer-debug-log', { level, message, payload }),
|
||||
|
||||
// Debugging & Diagnostics
|
||||
getDebugInfo: () => ipcRenderer.invoke('get-debug-info'),
|
||||
clearCache: (type) => ipcRenderer.invoke('clear-cache', type || 'all')
|
||||
});
|
||||
238
renderer/App.jsx
238
renderer/App.jsx
@@ -4,39 +4,42 @@ import Settings from './Settings.jsx';
|
||||
export default function App() {
|
||||
const [folder, setFolder] = useState('');
|
||||
const [repoName, setRepoName] = useState('');
|
||||
const [platform, setPlatform] = useState('github');
|
||||
const [status, setStatus] = useState('');
|
||||
const [platform, setPlatform] = useState('gitea');
|
||||
const [status, setStatus] = useState('Bereit');
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
const [branches, setBranches] = useState([]);
|
||||
const [selectedBranch, setSelectedBranch] = useState('master');
|
||||
const [selectedBranch, setSelectedBranch] = useState('main');
|
||||
const [logs, setLogs] = useState([]);
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
async function selectFolder() {
|
||||
const selected = await window.electronAPI.selectFolder();
|
||||
if (selected) setFolder(selected);
|
||||
// Branches laden
|
||||
if (selected) {
|
||||
const branchList = await window.electronAPI.getBranches({ folder: selected });
|
||||
setBranches(branchList);
|
||||
if (branchList.includes('master')) setSelectedBranch('master');
|
||||
}
|
||||
if (!selected) return;
|
||||
setFolder(selected);
|
||||
const branchList = await window.electronAPI.getBranches({ folder: selected });
|
||||
setBranches(branchList);
|
||||
if (branchList.includes('main')) setSelectedBranch('main');
|
||||
else if (branchList.includes('master')) setSelectedBranch('master');
|
||||
else if (branchList.length > 0) setSelectedBranch(branchList[0]);
|
||||
}
|
||||
|
||||
async function createRepoHandler() {
|
||||
if (!repoName) return alert('Repo Name required!');
|
||||
setStatus('Creating repository...');
|
||||
if (!repoName) return alert('Repo-Name erforderlich!');
|
||||
setStatus('Repository wird erstellt…');
|
||||
const result = await window.electronAPI.createRepo({ name: repoName, platform });
|
||||
setStatus(result ? 'Repository created!' : 'Failed to create repository.');
|
||||
setStatus(result ? 'Repository erstellt!' : 'Fehler beim Erstellen des Repositories.');
|
||||
}
|
||||
|
||||
async function pushProjectHandler() {
|
||||
if (!folder) return alert('Select a project folder first!');
|
||||
setStatus('Pushing project...');
|
||||
if (!folder) return alert('Bitte zuerst einen Projektordner auswählen!');
|
||||
setStatus('Projekt wird gepusht…');
|
||||
setProgress(0);
|
||||
const onProgress = (p) => setProgress(p); // Callback für Fortschritt
|
||||
const result = await window.electronAPI.pushProject({ folder, branch: selectedBranch, onProgress });
|
||||
setStatus(result ? 'Project pushed!' : 'Failed to push project.');
|
||||
const result = await window.electronAPI.pushProject({
|
||||
folder,
|
||||
branch: selectedBranch,
|
||||
onProgress: (p) => setProgress(p),
|
||||
});
|
||||
setStatus(result ? 'Projekt gepusht!' : 'Push fehlgeschlagen.');
|
||||
if (result) {
|
||||
const logList = await window.electronAPI.getCommitLogs({ folder });
|
||||
setLogs(logList);
|
||||
@@ -44,61 +47,168 @@ export default function App() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: 20 }}>
|
||||
<h1>Git Manager GUI - High-End</h1>
|
||||
<button onClick={() => setShowSettings(!showSettings)}>Settings</button>
|
||||
{showSettings && <Settings />}
|
||||
<div id="app" style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
|
||||
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<label>Platform:</label>
|
||||
<select value={platform} onChange={e => setPlatform(e.target.value)}>
|
||||
<option value="github">GitHub</option>
|
||||
<option value="gitea">Gitea</option>
|
||||
</select>
|
||||
{/* ── Toolbar ── */}
|
||||
<div id="toolbar">
|
||||
<div className="toolbar-row toolbar-row--top">
|
||||
<div className="toolbar-brand">
|
||||
<div className="toolbar-brand-mark">
|
||||
<img src="./icon.png" alt="Git Manager Logo" className="toolbar-brand-logo" />
|
||||
</div>
|
||||
<div className="toolbar-brand-copy">
|
||||
<span className="toolbar-kicker">Workspace Control</span>
|
||||
<strong>Git Manager Explorer Pro</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="toolbar-top-actions">
|
||||
<div className="tool-group tool-group--quick-actions">
|
||||
<button onClick={createRepoHandler} title="Neues Repository erstellen">🚀 New Repo</button>
|
||||
<button onClick={pushProjectHandler} title="Projekt pushen">⬆️ Push</button>
|
||||
</div>
|
||||
|
||||
<div className="tool-group tool-group--utility">
|
||||
<button onClick={() => setShowSettings(true)} title="Einstellungen">⚙️ Settings</button>
|
||||
</div>
|
||||
|
||||
<div className="toolbar-status-wrap">
|
||||
<span className="status-dot" aria-hidden="true" />
|
||||
<span className="status">{status}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="toolbar-row toolbar-row--bottom">
|
||||
<div className="tool-group tool-group--workspace">
|
||||
<button className="accent-btn" onClick={selectFolder} title="Lokalen Ordner öffnen">
|
||||
📂 Open Local
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="tool-group tool-group--repo">
|
||||
<div className="platform-switch" role="tablist" aria-label="Plattform auswählen">
|
||||
{['gitea', 'github'].map(p => (
|
||||
<button
|
||||
key={p}
|
||||
type="button"
|
||||
className={`platform-option${platform === p ? ' active' : ''}`}
|
||||
data-platform={p}
|
||||
aria-pressed={platform === p}
|
||||
onClick={() => setPlatform(p)}
|
||||
>
|
||||
{p === 'gitea' ? 'Gitea' : 'GitHub'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{branches.length > 0 && (
|
||||
<select
|
||||
value={selectedBranch}
|
||||
onChange={e => setSelectedBranch(e.target.value)}
|
||||
title="Branch auswählen"
|
||||
>
|
||||
{branches.map(b => <option key={b} value={b}>{b}</option>)}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<button onClick={selectFolder}>Select Project Folder</button>
|
||||
<span style={{ marginLeft: 10 }}>{folder}</span>
|
||||
</div>
|
||||
{/* ── Main ── */}
|
||||
<main id="main">
|
||||
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<label>Branch:</label>
|
||||
<select value={selectedBranch} onChange={e => setSelectedBranch(e.target.value)}>
|
||||
{branches.map(b => <option key={b} value={b}>{b}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
{/* Fortschrittsbalken */}
|
||||
{progress > 0 && progress < 100 && (
|
||||
<div className="input-group" style={{ marginBottom: 20 }}>
|
||||
<label>Fortschritt</label>
|
||||
<progress value={progress} max="100" style={{ width: '100%', height: 8, borderRadius: 4 }} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Repository Name"
|
||||
value={repoName}
|
||||
onChange={e => setRepoName(e.target.value)} />
|
||||
<button onClick={createRepoHandler} style={{ marginLeft: 10 }}>Create Repo</button>
|
||||
</div>
|
||||
{/* Repo erstellen / Ordner */}
|
||||
<div className="card" style={{ marginBottom: 20 }}>
|
||||
<h2>📁 Projekt & Repository</h2>
|
||||
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<button onClick={pushProjectHandler}>Push / Update Project</button>
|
||||
</div>
|
||||
<div className="input-group">
|
||||
<label>Lokaler Projektordner</label>
|
||||
<div style={{ display: 'flex', gap: 10, alignItems: 'center' }}>
|
||||
<input
|
||||
type="text"
|
||||
readOnly
|
||||
value={folder}
|
||||
placeholder="Noch kein Ordner ausgewählt…"
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<button onClick={selectFolder} style={{ flex: '0 0 auto' }}>📂 Auswählen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<label>Progress:</label>
|
||||
<progress value={progress} max="100" style={{ width: '100%' }} />
|
||||
</div>
|
||||
<div className="input-group">
|
||||
<label>Repository Name</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="mein-projekt"
|
||||
value={repoName}
|
||||
onChange={e => setRepoName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<strong>Status: </strong>{status}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 10, marginTop: 8 }}>
|
||||
<button className="accent-btn" onClick={createRepoHandler}>🚀 Repo erstellen</button>
|
||||
<button onClick={pushProjectHandler}>⬆️ Push / Aktualisieren</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<h3>Commit Logs:</h3>
|
||||
<ul>
|
||||
{logs.map((log, i) => (
|
||||
<li key={i}>{log}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
{/* Commit-Logs */}
|
||||
{logs.length > 0 && (
|
||||
<div className="card">
|
||||
<h2>📊 Commit-Verlauf</h2>
|
||||
<ul style={{ listStyle: 'none', padding: 0, margin: 0, display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
{logs.map((log, i) => (
|
||||
<li
|
||||
key={i}
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
borderRadius: 8,
|
||||
background: 'rgba(255,255,255,0.04)',
|
||||
border: '1px solid rgba(140,173,255,0.08)',
|
||||
fontSize: 13,
|
||||
color: 'var(--text-secondary)',
|
||||
fontFamily: 'monospace',
|
||||
}}
|
||||
>
|
||||
{log}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{logs.length === 0 && !folder && (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingTop: 60,
|
||||
gap: 16,
|
||||
opacity: 0.55,
|
||||
}}>
|
||||
<div style={{ fontSize: 64, filter: 'drop-shadow(0 8px 16px rgba(88,213,255,0.2))' }}>📂</div>
|
||||
<p style={{ color: 'var(--text-muted)', fontSize: 15, textAlign: 'center' }}>
|
||||
Öffne einen lokalen Ordner oder lade Repos über die Toolbar.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
|
||||
{/* ── Settings-Modal ── */}
|
||||
{showSettings && (
|
||||
<div id="settingsModal" className="modal" onClick={e => { if (e.target === e.currentTarget) setShowSettings(false); }}>
|
||||
<Settings onClose={() => setShowSettings(false)} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,37 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
|
||||
export default function Settings() {
|
||||
export default function Settings({ onClose }) {
|
||||
const [githubToken, setGithubToken] = useState('');
|
||||
const [giteaToken, setGiteaToken] = useState('');
|
||||
const [giteaURL, setGiteaURL] = useState('');
|
||||
const [avatarB64, setAvatarB64] = useState(null);
|
||||
const [savedOk, setSavedOk] = useState(false);
|
||||
const [avatarUploading, setAvatarUploading] = useState(false);
|
||||
const fileInputRef = useRef(null);
|
||||
|
||||
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 Gitea-URL. Beispiel für IPv6: http://[2001:db8::1]:3000',
|
||||
};
|
||||
}
|
||||
|
||||
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
||||
return {
|
||||
ok: false,
|
||||
error: 'Die Gitea-URL muss mit http:// oder https:// beginnen.',
|
||||
};
|
||||
}
|
||||
|
||||
return { ok: true, value: value.replace(/\/$/, '') };
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
window.electronAPI.loadCredentials().then(data => {
|
||||
@@ -11,31 +39,145 @@ export default function Settings() {
|
||||
setGithubToken(data.githubToken || '');
|
||||
setGiteaToken(data.giteaToken || '');
|
||||
setGiteaURL(data.giteaURL || '');
|
||||
setAvatarB64(data.avatarB64 || null);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const save = () => {
|
||||
window.electronAPI.saveCredentials({ githubToken, giteaToken, giteaURL });
|
||||
alert('Settings saved securely!');
|
||||
function handleAvatarFileChange(e) {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = (ev) => setAvatarB64(ev.target.result);
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
async function save() {
|
||||
const checkedUrl = normalizeAndValidateGiteaUrl(giteaURL);
|
||||
if (!checkedUrl.ok) {
|
||||
alert(checkedUrl.error);
|
||||
return;
|
||||
}
|
||||
window.electronAPI.saveCredentials({
|
||||
githubToken,
|
||||
giteaToken,
|
||||
giteaURL: checkedUrl.value,
|
||||
avatarB64: avatarB64 || null
|
||||
});
|
||||
|
||||
// Avatar automatisch zu Gitea pushen, wenn Token + URL vorhanden
|
||||
if (avatarB64 && giteaToken && checkedUrl.value) {
|
||||
setAvatarUploading(true);
|
||||
const result = await window.electronAPI.updateGiteaAvatar({
|
||||
token: giteaToken,
|
||||
url: checkedUrl.value,
|
||||
imageBase64: avatarB64
|
||||
});
|
||||
setAvatarUploading(false);
|
||||
if (!result.ok) {
|
||||
console.warn('Avatar-Upload fehlgeschlagen:', result.error);
|
||||
}
|
||||
}
|
||||
|
||||
setSavedOk(true);
|
||||
setTimeout(() => setSavedOk(false), 2500);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: 20 }}>
|
||||
<h2>Settings</h2>
|
||||
<div>
|
||||
<label>GitHub Token:</label>
|
||||
<input type="password" value={githubToken} onChange={e => setGithubToken(e.target.value)} />
|
||||
<div className="modalContent card settings-modal-content">
|
||||
|
||||
<div className="settings-header">
|
||||
<div className="settings-header-inner">
|
||||
<div className="settings-avatar-wrap" onClick={() => fileInputRef.current?.click()} title="Profilbild ändern">
|
||||
{avatarB64
|
||||
? <img src={avatarB64} alt="Avatar" className="settings-avatar-img" />
|
||||
: <div className="settings-avatar-placeholder">👤</div>
|
||||
}
|
||||
<div className="settings-avatar-overlay">✏️</div>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
style={{ display: 'none' }}
|
||||
onChange={handleAvatarFileChange}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="settings-eyebrow">Konfiguration</div>
|
||||
<h2>⚙️ Einstellungen</h2>
|
||||
<p className="settings-subtitle">
|
||||
Zugangsdaten für GitHub und Gitea hinterlegen.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label>Gitea Token:</label>
|
||||
<input type="password" value={giteaToken} onChange={e => setGiteaToken(e.target.value)} />
|
||||
|
||||
<div className="settings-layout">
|
||||
<div className="settings-column settings-column--left">
|
||||
<section className="settings-panel settings-panel--credentials">
|
||||
<div className="settings-panel-header">
|
||||
<div>
|
||||
<h3>Zugangsdaten</h3>
|
||||
<p>API-Zugriffe für GitHub und Gitea konfigurieren.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-fields-grid">
|
||||
<div className="input-group">
|
||||
<label htmlFor="react-githubToken">GitHub Token</label>
|
||||
<input
|
||||
id="react-githubToken"
|
||||
type="password"
|
||||
placeholder="ghp_…"
|
||||
value={githubToken}
|
||||
onChange={e => setGithubToken(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="input-group">
|
||||
<label htmlFor="react-giteaToken">Gitea Token</label>
|
||||
<input
|
||||
id="react-giteaToken"
|
||||
type="password"
|
||||
placeholder="Token hier einfügen"
|
||||
value={giteaToken}
|
||||
onChange={e => setGiteaToken(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="input-group input-group--wide">
|
||||
<label htmlFor="react-giteaURL">Gitea URL</label>
|
||||
<input
|
||||
id="react-giteaURL"
|
||||
type="text"
|
||||
placeholder="https://gitea.example.com"
|
||||
value={giteaURL}
|
||||
onChange={e => setGiteaURL(e.target.value)}
|
||||
/>
|
||||
<div className="settings-connection-tools">
|
||||
<div className="settings-inline-hint">
|
||||
Hinweis: IPv6 mit Klammern eingeben, z.B. http://[2001:db8::1]:3000
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label>Gitea URL:</label>
|
||||
<input type="text" value={giteaURL} onChange={e => setGiteaURL(e.target.value)} />
|
||||
|
||||
<div className="modal-buttons settings-modal-actions">
|
||||
<button
|
||||
className="accent-btn"
|
||||
onClick={save}
|
||||
disabled={avatarUploading}
|
||||
style={savedOk ? { background: 'var(--success)', borderColor: 'var(--success)' } : {}}
|
||||
>
|
||||
{avatarUploading ? '⏳ Bild wird hochgeladen…' : savedOk ? '✅ Gespeichert' : 'Speichern'}
|
||||
</button>
|
||||
{onClose && (
|
||||
<button className="secondary" onClick={onClose}>Abbrechen</button>
|
||||
)}
|
||||
</div>
|
||||
<button onClick={save} style={{ marginTop: 10 }}>Save</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
BIN
renderer/icon.png
Normal file
BIN
renderer/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
@@ -4,75 +4,322 @@
|
||||
<meta charset="utf-8" />
|
||||
<title>Git Manager Explorer Pro</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https: http:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none';">
|
||||
<link rel="icon" type="image/png" href="./icon.png">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div id="toolbar">
|
||||
<div class="tool-group">
|
||||
<button id="btnSettings" title="Einstellungen">⚙️ Settings</button>
|
||||
<button id="btnBack" class="secondary hidden" title="Zurück">⬅️ Zurück</button>
|
||||
<button id="btnSelectFolder" class="accent-btn" title="Lokalen Ordner öffnen">📂 Open Local</button>
|
||||
<button id="btnLoadGiteaRepos" class="accent-btn" title="Gitea Repositories laden">🌐 Load Gitea</button>
|
||||
<!-- Titelbalken-Streifen -->
|
||||
<div id="titlebar-strip">
|
||||
<div class="titlebar-strip-brand">
|
||||
<img src="./icon.png" alt="" class="titlebar-strip-icon">
|
||||
<span class="titlebar-strip-title">Git Manager Explorer Pro</span>
|
||||
</div>
|
||||
|
||||
<div class="tool-group">
|
||||
<select id="platform" title="Plattform auswählen">
|
||||
<option value="gitea" selected>Gitea</option>
|
||||
<option value="github">GitHub</option>
|
||||
</select>
|
||||
<button id="btnOpenRepoActions" title="Neues Repository erstellen">🚀 New Repo</button>
|
||||
<button id="btnPush" title="Projekt pushen">⬆️ Push</button>
|
||||
<button id="btnCommits" class="hidden" title="Commit History anzeigen">📊 Commits</button>
|
||||
<button id="btnReleases" class="hidden" title="Releases anzeigen">📦 Releases</button>
|
||||
<div class="win-controls" aria-label="Fenster-Steuerung">
|
||||
<button class="win-btn win-btn--minimize" id="btnWinMinimize" title="Minimieren">
|
||||
<svg width="10" height="1" viewBox="0 0 10 1"><rect width="10" height="1" fill="currentColor"/></svg>
|
||||
</button>
|
||||
<button class="win-btn win-btn--maximize" id="btnWinMaximize" title="Maximieren">
|
||||
<svg width="10" height="10" viewBox="0 0 10 10"><rect x="0.5" y="0.5" width="9" height="9" rx="1" fill="none" stroke="currentColor"/></svg>
|
||||
</button>
|
||||
<button class="win-btn win-btn--close" id="btnWinClose" title="Schließen">
|
||||
<svg width="10" height="10" viewBox="0 0 10 10"><line x1="0" y1="0" x2="10" y2="10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><line x1="10" y1="0" x2="0" y2="10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<span id="status" class="status">Bereit</span>
|
||||
</div>
|
||||
|
||||
<main id="main">
|
||||
<div id="explorerGrid" class="explorer-grid">
|
||||
<div id="toolbar">
|
||||
<div class="toolbar-row toolbar-row--top">
|
||||
<div class="toolbar-brand" aria-label="App Kopfbereich">
|
||||
<div class="toolbar-brand-mark">
|
||||
<img src="./icon.png" alt="Git Manager Logo" class="toolbar-brand-logo">
|
||||
</div>
|
||||
<div class="toolbar-brand-copy">
|
||||
<span class="toolbar-kicker">Workspace Control</span>
|
||||
<strong>Git Manager Explorer Pro</strong>
|
||||
</div>
|
||||
<span id="project-toolbar-title" class="toolbar-project-title"></span>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div id="settingsModal" class="modal hidden">
|
||||
<div class="modalContent card">
|
||||
<h2>⚙️ Einstellungen</h2>
|
||||
|
||||
<div class="input-group">
|
||||
<label>GitHub Token</label>
|
||||
<input id="githubToken" type="password" placeholder="ghp_...">
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label>Gitea Token</label>
|
||||
<input id="giteaToken" type="password" placeholder="Token hier einfügen">
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label>Gitea URL</label>
|
||||
<input id="giteaURL" type="text" placeholder="https://gitea.example.com">
|
||||
</div>
|
||||
|
||||
<div class="input-group" style="margin-top: 30px; padding-top: 20px; border-top: 1px solid rgba(255,255,255,0.1);">
|
||||
<label>App Version</label>
|
||||
<div style="display: flex; gap: 12px; align-items: center;">
|
||||
<input id="appVersion" type="text" readonly style="flex: 1; background: rgba(255,255,255,0.05); cursor: not-allowed;">
|
||||
<button id="btnCheckUpdates" style="
|
||||
background: linear-gradient(135deg, #00d4ff, #8b5cf6);
|
||||
color: #000;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
">🔄 Nach Updates suchen</button>
|
||||
<div class="toolbar-top-actions">
|
||||
<div class="tool-group tool-group--quick-actions">
|
||||
<button id="btnOpenRepoActions" title="Neues Repository erstellen">🚀 New Repo</button>
|
||||
<button id="btnOpenMigration" title="Repository von GitHub/GitLab zu Gitea migrieren">📥 Migrieren</button>
|
||||
<button id="btnPush" title="Projekt pushen">⬆️ Push</button>
|
||||
<button id="btnGlobalTrash" class="hidden" style="display:none" title=""><!-- removed --></button>
|
||||
</div>
|
||||
|
||||
<div class="tool-group tool-group--utility">
|
||||
<span class="tool-group-title">Steuerung</span>
|
||||
<button id="btnSettings" title="Einstellungen">⚙️ Settings</button>
|
||||
<button id="btnBatchActions" title="Batch-Aktionen">🧩 Batch</button>
|
||||
<button id="btnOpenActivityLog" title="Aktivitätsprotokoll">📝 Activity</button>
|
||||
<button id="btnRetryQueueNow" class="secondary" title="Retry-Queue jetzt verarbeiten">🔁 Queue (0)</button>
|
||||
<button id="btnBack" class="secondary hidden" title="Zurück">⬅️ Zurück</button>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-status-wrap">
|
||||
<span class="status-dot" aria-hidden="true"></span>
|
||||
<span id="status" class="status">Bereit</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-buttons">
|
||||
<button id="btnSaveSettings">Speichern</button>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-row toolbar-row--bottom">
|
||||
<div class="tool-group tool-group--workspace">
|
||||
<span class="tool-group-title">Quelle</span>
|
||||
<button id="btnSelectFolder" class="accent-btn" title="Lokalen Ordner öffnen">📂 Open Local</button>
|
||||
<button id="btnLoadGiteaRepos" class="accent-btn" title="Projekte der gewählten Plattform laden">🌐 Load Projekte</button>
|
||||
</div>
|
||||
|
||||
<div class="tool-group tool-group--repo">
|
||||
<span class="tool-group-title">Repository</span>
|
||||
<input id="platform" type="hidden" value="gitea">
|
||||
<div class="platform-switch" role="tablist" aria-label="Plattform auswählen">
|
||||
<button type="button" class="platform-option active" data-platform="gitea" aria-pressed="true">Gitea</button>
|
||||
<button type="button" class="platform-option" data-platform="github" aria-pressed="false">GitHub</button>
|
||||
</div>
|
||||
<button id="btnCommits" class="hidden" title="Commit History anzeigen">📊 Commits</button>
|
||||
<button id="btnReleases" class="hidden" title="Releases anzeigen">📦 Releases</button>
|
||||
<button id="btnTrash" class="hidden" style="display:none" title=""><!-- removed --></button>
|
||||
<button id="btnPurgeTrash" class="hidden" style="display:none" title=""><!-- removed --></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="project-gravur-bar"><span id="project-gravur-title" class="project-gravur-title"></span></div>
|
||||
<div class="project-gravur-separator"></div>
|
||||
<div id="contentArea" class="content-area">
|
||||
<aside id="favHistorySidebar" class="fav-history-sidebar" aria-label="Favoriten und Verlauf"></aside>
|
||||
<main id="main">
|
||||
<div id="explorerGrid" class="explorer-grid"></div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div id="settingsModal" class="modal hidden">
|
||||
<div class="modalContent card settings-modal-content">
|
||||
<button id="btnSettingsWatermark" class="settings-watermark-btn" title="Projektinformationen anzeigen" aria-label="Projektinformationen anzeigen">ⓘ</button>
|
||||
<div id="settingsWatermarkCard" class="settings-watermark-card hidden" role="dialog" aria-label="Projektinformationen">
|
||||
<h4>Projektinformationen</h4>
|
||||
<div class="settings-watermark-row"><span>Ersteller:</span><strong>M_Viper</strong></div>
|
||||
<div class="settings-watermark-row"><span>Webseite:</span><a href="https://m-viper.de" target="_blank" rel="noopener noreferrer">https://m-viper.de</a></div>
|
||||
<div class="settings-watermark-row"><span>Discord:</span><a id="watermarkDiscord" href="http://discord.viper-network.de" target="_blank" rel="noopener noreferrer">discord.com/invite/FdRs4BRd8D</a></div>
|
||||
<div class="settings-watermark-row"><span>E-Mail:</span><a id="watermarkMail" href="mailto:admin@m-viper.de">admin@m-viper.de</a></div>
|
||||
<div class="settings-watermark-row"><span>Version:</span><strong id="watermarkVersion">-</strong></div>
|
||||
<div class="settings-watermark-row"><span>Copyright:</span><strong id="watermarkCopyright">-</strong></div>
|
||||
<div class="settings-watermark-row"><span>Projekt:</span><strong>Git Manager Explorer Pro</strong></div>
|
||||
</div>
|
||||
|
||||
<div class="settings-header">
|
||||
<div class="settings-header-inner">
|
||||
<div class="settings-avatar-wrap" id="settingsAvatarWrap" title="Profilbild ändern">
|
||||
<img id="settingsAvatarImg" class="settings-avatar-img" src="" alt="Avatar" style="display:none;">
|
||||
<div id="settingsAvatarPlaceholder" class="settings-avatar-placeholder">👤</div>
|
||||
<div class="settings-avatar-overlay">✏️</div>
|
||||
<input id="settingsAvatarInput" type="file" accept="image/*" style="display:none;">
|
||||
</div>
|
||||
<button id="btnUploadAvatar" class="settings-avatar-upload-btn" title="Gespeichertes Bild jetzt auf Gitea hochladen">📤 Auf Gitea aktualisieren</button>
|
||||
<div>
|
||||
<div class="settings-eyebrow">Konfiguration</div>
|
||||
<h2>⚙️ Einstellungen</h2>
|
||||
<p class="settings-subtitle">Alle wichtigen Optionen auf einer Seite: Zugangsdaten, Verbindungscheck, Darstellung und Updates.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-layout">
|
||||
<div class="settings-column settings-column--left">
|
||||
<section class="settings-panel settings-panel--credentials">
|
||||
<div class="settings-panel-header">
|
||||
<div>
|
||||
<h3>Zugangsdaten</h3>
|
||||
<p>API-Zugriffe für GitHub und Gitea konfigurieren.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-credentials-grid">
|
||||
<article class="settings-auth-card settings-auth-card--github">
|
||||
<div class="settings-auth-card-header">
|
||||
<h4>GitHub</h4>
|
||||
<button id="btnTestGithubConnection" class="secondary" type="button">🔌 Verbindung testen</button>
|
||||
</div>
|
||||
<div class="input-group settings-auth-input">
|
||||
<label for="githubToken">GitHub Token</label>
|
||||
<input id="githubToken" type="password" placeholder="ghp_...">
|
||||
</div>
|
||||
<div class="settings-auth-spacer" aria-hidden="true">GitHub benötigt keine Server-URL</div>
|
||||
<div id="githubTokenHint" class="settings-inline-hint">Hinweis: Der Token wird direkt über api.github.com geprüft.</div>
|
||||
</article>
|
||||
|
||||
<article class="settings-auth-card settings-auth-card--gitea">
|
||||
<div class="settings-auth-card-header">
|
||||
<h4>Gitea</h4>
|
||||
<button id="btnTestGiteaConnection" class="secondary" type="button">🔌 Verbindung testen</button>
|
||||
</div>
|
||||
<div class="input-group settings-auth-input">
|
||||
<label for="giteaToken">Gitea Token</label>
|
||||
<input id="giteaToken" type="password" placeholder="Token hier einfügen">
|
||||
</div>
|
||||
<div class="input-group settings-auth-input">
|
||||
<label for="giteaURL">Gitea URL</label>
|
||||
<input id="giteaURL" type="text" placeholder="https://gitea.example.com">
|
||||
</div>
|
||||
<div id="giteaUrlHint" class="settings-inline-hint">Hinweis: IPv6 mit Klammern eingeben, z.B. http://[2001:db8::1]:3000</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settings-panel settings-panel--display">
|
||||
<div class="settings-panel-header">
|
||||
<div>
|
||||
<h3>Darstellung</h3>
|
||||
<p>Übersicht und Explorer an deinen Arbeitsstil anpassen.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-toggle-list">
|
||||
<label class="settings-toggle-row" for="settingFavorites">
|
||||
<span class="settings-toggle-info">
|
||||
<span class="settings-toggle-title">⭐ Favoriten-Bereich anzeigen</span>
|
||||
<span class="settings-toggle-desc">Pinnt wichtige Repositories und Ordner sichtbar im Kopfbereich.</span>
|
||||
</span>
|
||||
<span class="toggle-switch">
|
||||
<input type="checkbox" id="settingFavorites" checked>
|
||||
<span class="toggle-track"></span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label class="settings-toggle-row" for="settingRecent">
|
||||
<span class="settings-toggle-info">
|
||||
<span class="settings-toggle-title">🕐 Zuletzt geöffnet anzeigen</span>
|
||||
<span class="settings-toggle-desc">Zeigt deine letzten Projekte direkt in der Übersicht an.</span>
|
||||
</span>
|
||||
<span class="toggle-switch">
|
||||
<input type="checkbox" id="settingRecent" checked>
|
||||
<span class="toggle-track"></span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label class="settings-toggle-row" for="settingCompact">
|
||||
<span class="settings-toggle-info">
|
||||
<span class="settings-toggle-title">⊞ Kompakt-Modus</span>
|
||||
<span class="settings-toggle-desc">Verdichtet Karten und Abstände für kleinere Fenster.</span>
|
||||
</span>
|
||||
<span class="toggle-switch">
|
||||
<input type="checkbox" id="settingCompact">
|
||||
<span class="toggle-track"></span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label class="settings-toggle-row" for="settingColoredIcons">
|
||||
<span class="settings-toggle-info">
|
||||
<span class="settings-toggle-title">🎨 Farbige Datei-Icons</span>
|
||||
<span class="settings-toggle-desc">Setzt stärkere Dateityp-Farben für schnellere Orientierung.</span>
|
||||
</span>
|
||||
<span class="toggle-switch">
|
||||
<input type="checkbox" id="settingColoredIcons" checked>
|
||||
<span class="toggle-track"></span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settings-panel settings-panel--diagnostics">
|
||||
<div class="settings-panel-header">
|
||||
<div>
|
||||
<h3>Diagnose</h3>
|
||||
<p>Erstellt ein Diagnosepaket mit Logs, Plattform und letzten Fehlern für schnelles Debugging.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-diagnostics-card">
|
||||
<button id="btnCreateDiagnostics" class="settings-action-btn" type="button">🧪 Diagnose erstellen</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="settings-column settings-column--right">
|
||||
<section class="settings-panel settings-panel--health">
|
||||
<div class="settings-panel-header">
|
||||
<div>
|
||||
<h3>Verbindungsstatus</h3>
|
||||
<p>Direkt sehen, ob URL, API und Auth sauber antworten.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-health-box">
|
||||
<div class="settings-health-row"><span>URL</span><strong id="healthUrl">Unbekannt</strong></div>
|
||||
<div class="settings-health-row"><span>API</span><strong id="healthApi">Unbekannt</strong></div>
|
||||
<div class="settings-health-row"><span>Auth</span><strong id="healthAuth">Unbekannt</strong></div>
|
||||
<div class="settings-health-row"><span>Latenz</span><strong id="healthLatency">-</strong></div>
|
||||
<div class="settings-health-row"><span>Server</span><strong id="healthVersion">-</strong></div>
|
||||
<div class="settings-health-row"><span>Letzter Fehler</span><strong id="healthLastError">-</strong></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settings-panel settings-panel--system">
|
||||
<div class="settings-panel-header">
|
||||
<div>
|
||||
<h3>System</h3>
|
||||
<p>Verhalten beim Windows-Start steuern.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-toggle-list">
|
||||
<label class="settings-toggle-row" for="settingAutostart">
|
||||
<span class="settings-toggle-info">
|
||||
<span class="settings-toggle-title">🚀 Mit Windows starten</span>
|
||||
<span class="settings-toggle-desc">Startet die App automatisch beim Anmelden, minimiert im System-Tray.</span>
|
||||
</span>
|
||||
<span class="toggle-switch">
|
||||
<input type="checkbox" id="settingAutostart">
|
||||
<span class="toggle-track"></span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settings-panel settings-panel--app">
|
||||
<div class="settings-panel-header">
|
||||
<div>
|
||||
<h3>App & Updates</h3>
|
||||
<p>Version prüfen und neue Releases direkt anstoßen.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-version-card">
|
||||
<div class="input-group input-group--wide settings-version-field">
|
||||
<label for="appVersion">App Version</label>
|
||||
<input id="appVersion" class="settings-readonly-input" type="text" readonly>
|
||||
</div>
|
||||
<button id="btnCheckUpdates" class="settings-update-btn">🔄 Nach Updates suchen</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settings-panel settings-panel--backup">
|
||||
<div class="settings-panel-header">
|
||||
<div>
|
||||
<h3>Backup & Transfer</h3>
|
||||
<p>Einstellungen, Favoriten und Verlauf exportieren oder auf einem anderen PC importieren.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-backup-card">
|
||||
<div class="settings-tools-grid">
|
||||
<button id="btnExportSettingsBundle" class="settings-action-btn" type="button">📦 Backup exportieren</button>
|
||||
<button id="btnImportSettingsBundle" class="settings-action-btn" type="button">📥 Backup importieren</button>
|
||||
</div>
|
||||
<div class="settings-inline-hint">
|
||||
Export enthält Tokens nur verschlüsselt, nie als Klartext. Der Import ist für andere PCs geeignet.
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-buttons settings-modal-actions">
|
||||
<button id="btnSaveSettings" class="accent-btn">Speichern</button>
|
||||
<button id="btnCloseSettings" class="secondary">Abbrechen</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -85,6 +332,7 @@
|
||||
<div class="input-group">
|
||||
<label>Repository Name</label>
|
||||
<input id="repoName" type="text" placeholder="mein-projekt">
|
||||
<div id="repoNameValidationHint" class="settings-inline-hint">Name prüfen: Duplikate, ähnliche Namen und ungültige Zeichen werden erkannt.</div>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
@@ -125,6 +373,139 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Migration Modal -->
|
||||
<div id="migrationModal" class="modal hidden">
|
||||
<div class="modalContent card migration-modal-content">
|
||||
<h2>📥 Repository migrieren</h2>
|
||||
<p class="settings-subtitle">Ein Repository von GitHub, GitLab oder einer anderen Git-Quelle auf deine Gitea-Instanz kopieren.</p>
|
||||
|
||||
<div class="migration-fields">
|
||||
<div class="input-group">
|
||||
<label for="migrateCloneUrl">Quell-URL (Clone-URL)</label>
|
||||
<input id="migrateCloneUrl" type="text" placeholder="https://github.com/benutzer/repo.git">
|
||||
<div class="settings-inline-hint">Beispiel: https://github.com/M_Viper/NexTrade.git</div>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label for="migrateRepoName">Neuer Repository-Name auf Gitea</label>
|
||||
<input id="migrateRepoName" type="text" placeholder="NexTrade">
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label for="migrateDescription">Beschreibung (optional)</label>
|
||||
<input id="migrateDescription" type="text" placeholder="">
|
||||
</div>
|
||||
|
||||
<div class="migration-row-split">
|
||||
<div class="input-group">
|
||||
<label for="migrateAuthUsername">Auth-Benutzername (bei privaten Repos)</label>
|
||||
<input id="migrateAuthUsername" type="text" placeholder="GitHub-Benutzername">
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label for="migrateAuthToken">Auth-Token (bei privaten Repos)</label>
|
||||
<input id="migrateAuthToken" type="password" placeholder="ghp_…">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="settings-toggle-row" style="margin-top:4px;" for="migratePrivate">
|
||||
<span class="settings-toggle-info">
|
||||
<span class="settings-toggle-title">🔒 Privates Repository</span>
|
||||
</span>
|
||||
<span class="toggle-switch">
|
||||
<input type="checkbox" id="migratePrivate">
|
||||
<span class="toggle-track"></span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="migrationStatus" class="migration-status hidden"></div>
|
||||
|
||||
<div class="modal-buttons">
|
||||
<button id="btnStartMigration" class="accent-btn">📥 Migration starten</button>
|
||||
<button id="btnCloseMigration" class="secondary">Abbrechen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="batchActionModal" class="modal hidden">
|
||||
<div class="modalContent card">
|
||||
<h2>🧩 Batch-Aktionen</h2>
|
||||
|
||||
<div class="input-group">
|
||||
<label>Aktion</label>
|
||||
<select id="batchActionType">
|
||||
<option value="refresh">Repos aktualisieren</option>
|
||||
<option value="clone">Repos klonen</option>
|
||||
<option value="create-tag">Tag erstellen</option>
|
||||
<option value="create-release">Release erstellen</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label>Repositories (pro Zeile: owner/repo)</label>
|
||||
<textarea id="batchRepoList" class="batch-textarea" placeholder="M_Viper/ProjektA M_Viper/ProjektB"></textarea>
|
||||
</div>
|
||||
|
||||
<div id="batchCloneGroup" class="input-group hidden">
|
||||
<label>Zielordner für Clone</label>
|
||||
<div class="batch-inline-row">
|
||||
<input id="batchCloneTarget" type="text" readonly placeholder="Bitte Zielordner auswählen">
|
||||
<button id="btnSelectBatchCloneTarget" class="secondary">📁 Wählen</button>
|
||||
</div>
|
||||
<div id="batchCloneValidationHint" class="settings-inline-hint">Kollisionsprüfung aktiv: vorhandene Zielordner und Namenskonflikte werden angezeigt.</div>
|
||||
</div>
|
||||
|
||||
<div id="batchTagGroup" class="input-group hidden">
|
||||
<label>Tag</label>
|
||||
<input id="batchTagName" type="text" placeholder="v1.0.0">
|
||||
</div>
|
||||
|
||||
<div id="batchReleaseNameGroup" class="input-group hidden">
|
||||
<label>Release-Name</label>
|
||||
<input id="batchReleaseName" type="text" placeholder="Release v1.0.0">
|
||||
</div>
|
||||
|
||||
<div id="batchReleaseBodyGroup" class="input-group hidden">
|
||||
<label>Release-Text</label>
|
||||
<textarea id="batchReleaseBody" class="batch-textarea" placeholder="Changelog..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="modal-buttons">
|
||||
<button id="btnRunBatchAction" class="accent-btn">Ausführen</button>
|
||||
<button id="btnCloseBatchAction" class="secondary">Abbrechen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="activityLogModal" class="modal hidden">
|
||||
<div class="modalContent card">
|
||||
<h2>📝 Aktivitätsprotokoll</h2>
|
||||
|
||||
<div class="activity-toolbar">
|
||||
<select id="activityFilterLevel">
|
||||
<option value="all">Alle</option>
|
||||
<option value="info">Info</option>
|
||||
<option value="warning">Warn</option>
|
||||
<option value="error">Error</option>
|
||||
</select>
|
||||
<select id="activityFilterFix">
|
||||
<option value="all">Alle Einträge</option>
|
||||
<option value="fixable">Nur mit Fix</option>
|
||||
</select>
|
||||
<button id="btnRetryQueueRefresh" class="secondary">🔁 Queue jetzt retry</button>
|
||||
<button id="btnClearActivityLog" class="secondary">🧹 Log leeren</button>
|
||||
</div>
|
||||
|
||||
<div id="activityQueueInfo" class="activity-queue-info">Retry-Queue: 0</div>
|
||||
<div id="activitySummaryInfo" class="activity-summary-info">Fehler mit Fix: 0</div>
|
||||
<div id="activityLogList" class="activity-log-list"></div>
|
||||
|
||||
<div class="modal-buttons">
|
||||
<button id="btnCloseActivityLog" class="secondary">Schließen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="fileEditorModal" class="modal hidden">
|
||||
<div class="file-editor-card">
|
||||
<div class="file-editor-header">
|
||||
@@ -175,7 +556,7 @@
|
||||
<div style="display: flex; align-items: center; gap: 20px; margin-bottom: 20px;">
|
||||
<div style="font-size: 3rem; filter: drop-shadow(0 0 10px var(--accent-primary));">🚀</div>
|
||||
<div>
|
||||
<h2 style="margin: 0; background: var(--accent-gradient); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">Update verfügbar!</h2>
|
||||
<h2 style="margin: 0; background: var(--accent-gradient); background-clip: text; -webkit-background-clip: text; -webkit-text-fill-color: transparent;">Update verfügbar!</h2>
|
||||
<p id="updateVersionInfo" style="color: var(--text-secondary); margin: 5px 0 0 0; font-family: monospace;"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
0
renderer/modules/editor.js
Normal file
0
renderer/modules/editor.js
Normal file
0
renderer/modules/gitea.js
Normal file
0
renderer/modules/gitea.js
Normal file
0
renderer/modules/progress.js
Normal file
0
renderer/modules/progress.js
Normal file
0
renderer/modules/state.js
Normal file
0
renderer/modules/state.js
Normal file
0
renderer/modules/ui.js
Normal file
0
renderer/modules/ui.js
Normal file
5988
renderer/renderer.js
5988
renderer/renderer.js
File diff suppressed because it is too large
Load Diff
2783
renderer/style.css
2783
renderer/style.css
File diff suppressed because it is too large
Load Diff
183
src/backup/BackupManager.js
Normal file
183
src/backup/BackupManager.js
Normal file
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* BackupManager — Orchestrates backup operations
|
||||
*/
|
||||
|
||||
const archiver = require('archiver')
|
||||
const { createReadStream, createWriteStream } = require('fs')
|
||||
const { mkdir } = require('fs').promises
|
||||
const path = require('path')
|
||||
const { Transform } = require('stream')
|
||||
|
||||
class BackupManager {
|
||||
constructor(provider) {
|
||||
this.provider = provider
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a backup from a project folder
|
||||
* @param {string} projectPath - Path to project folder
|
||||
* @param {string} repoName - Repository name (used for filenames)
|
||||
* @returns {Promise<{filename, size, timestamp}>}
|
||||
*/
|
||||
async createBackup(projectPath, repoName) {
|
||||
try {
|
||||
// Create ZIP buffer
|
||||
const buffer = await this._createZip(projectPath)
|
||||
const timestamp = this._getTimestamp()
|
||||
const filename = `${repoName}-backup-${timestamp}.zip`
|
||||
|
||||
// Upload to provider
|
||||
const result = await this.provider.uploadBackup(buffer, filename)
|
||||
|
||||
// Cleanup old backups
|
||||
await this._cleanupOldBackups(repoName)
|
||||
|
||||
return {
|
||||
filename,
|
||||
size: result.size || buffer.length,
|
||||
timestamp
|
||||
}
|
||||
} catch (err) {
|
||||
throw new Error(`Backup creation failed: ${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List all backups for a repository
|
||||
* @param {string} repoName - Repository name
|
||||
* @returns {Promise<Array>}
|
||||
*/
|
||||
async listBackups(repoName) {
|
||||
try {
|
||||
const backups = await this.provider.listBackups()
|
||||
return backups
|
||||
.filter(b => b.name.startsWith(repoName))
|
||||
.sort((a, b) => new Date(b.date || b.name) - new Date(a.date || a.name))
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to list backups: ${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore a backup to a target folder
|
||||
* @param {string} repoName - Repository name
|
||||
* @param {string} filename - Backup filename
|
||||
* @param {string} targetPath - Target folder path
|
||||
*/
|
||||
async restoreBackup(repoName, filename, targetPath) {
|
||||
try {
|
||||
// Download backup
|
||||
const buffer = await this.provider.downloadBackup(filename)
|
||||
|
||||
// Extract ZIP
|
||||
await this._extractZip(buffer, targetPath)
|
||||
|
||||
return { ok: true, restored: filename }
|
||||
} catch (err) {
|
||||
throw new Error(`Restore failed: ${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a backup
|
||||
* @param {string} filename - Backup filename
|
||||
*/
|
||||
async deleteBackup(filename) {
|
||||
try {
|
||||
await this.provider.deleteBackup(filename)
|
||||
return { ok: true }
|
||||
} catch (err) {
|
||||
throw new Error(`Delete failed: ${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== PRIVATE METHODS ====================
|
||||
|
||||
/**
|
||||
* Create ZIP buffer from project folder
|
||||
* Excludes: .git, node_modules, dist, build, .env
|
||||
*/
|
||||
async _createZip(projectPath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const output = []
|
||||
const archive = archiver('zip', { zlib: { level: 5 } })
|
||||
|
||||
archive.on('data', chunk => output.push(chunk))
|
||||
archive.on('end', () => resolve(Buffer.concat(output)))
|
||||
archive.on('error', reject)
|
||||
|
||||
// Add files with exclusions
|
||||
archive.glob('**/*', {
|
||||
cwd: projectPath,
|
||||
ignore: [
|
||||
'.git/**',
|
||||
'.git',
|
||||
'node_modules/**',
|
||||
'node_modules',
|
||||
'dist/**',
|
||||
'dist',
|
||||
'build/**',
|
||||
'build',
|
||||
'.env',
|
||||
'.env.local',
|
||||
'.env.*.local',
|
||||
'*.log',
|
||||
'data/backups/**'
|
||||
],
|
||||
dot: true
|
||||
})
|
||||
|
||||
archive.finalize()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract ZIP buffer to target folder
|
||||
*/
|
||||
async _extractZip(buffer, targetPath) {
|
||||
const unzipper = require('unzipper')
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const { Readable } = require('stream')
|
||||
const stream = Readable.from(buffer)
|
||||
|
||||
stream
|
||||
.pipe(unzipper.Extract({ path: targetPath }))
|
||||
.on('close', resolve)
|
||||
.on('error', reject)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ISO timestamp (YYYYMMDD-HHMMSS format)
|
||||
*/
|
||||
_getTimestamp() {
|
||||
const now = new Date()
|
||||
const year = now.getFullYear()
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(now.getDate()).padStart(2, '0')
|
||||
const hours = String(now.getHours()).padStart(2, '0')
|
||||
const minutes = String(now.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(now.getSeconds()).padStart(2, '0')
|
||||
|
||||
return `${year}${month}${day}-${hours}${minutes}${seconds}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup old backups (keep only last N versions)
|
||||
*/
|
||||
async _cleanupOldBackups(repoName, maxVersions = 5) {
|
||||
try {
|
||||
const backups = await this.listBackups(repoName)
|
||||
|
||||
for (let i = maxVersions; i < backups.length; i++) {
|
||||
await this.provider.deleteBackup(backups[i].name)
|
||||
}
|
||||
} catch (err) {
|
||||
// Silently ignore cleanup errors
|
||||
console.warn(`Cleanup warning: ${err.message}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BackupManager
|
||||
59
src/backup/BackupProvider.js
Normal file
59
src/backup/BackupProvider.js
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* BackupProvider — Abstract base class for backup providers
|
||||
* All providers must implement these methods
|
||||
*/
|
||||
|
||||
class BackupProvider {
|
||||
/**
|
||||
* Authenticate with the backup service
|
||||
* @param {Object} credentials - Provider-specific credentials
|
||||
*/
|
||||
async authenticate(credentials) {
|
||||
throw new Error('authenticate() not implemented in ' + this.constructor.name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a backup file
|
||||
* @param {Buffer} buffer - File content
|
||||
* @param {string} filename - Filename (e.g., 'repo-backup-2025-03-24.zip')
|
||||
* @returns {Promise<{size: number}>}
|
||||
*/
|
||||
async uploadBackup(buffer, filename) {
|
||||
throw new Error('uploadBackup() not implemented in ' + this.constructor.name)
|
||||
}
|
||||
|
||||
/**
|
||||
* List all backups for a repository
|
||||
* @returns {Promise<Array>} Array of {name, size, date}
|
||||
*/
|
||||
async listBackups() {
|
||||
throw new Error('listBackups() not implemented in ' + this.constructor.name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a specific backup
|
||||
* @param {string} filename - Filename to download
|
||||
* @returns {Promise<Buffer>}
|
||||
*/
|
||||
async downloadBackup(filename) {
|
||||
throw new Error('downloadBackup() not implemented in ' + this.constructor.name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a backup file
|
||||
* @param {string} filename - Filename to delete
|
||||
*/
|
||||
async deleteBackup(filename) {
|
||||
throw new Error('deleteBackup() not implemented in ' + this.constructor.name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Test connection to backup service
|
||||
* @returns {Promise<{ok: boolean, error?: string}>}
|
||||
*/
|
||||
async testConnection() {
|
||||
throw new Error('testConnection() not implemented in ' + this.constructor.name)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BackupProvider
|
||||
84
src/backup/LocalProvider.js
Normal file
84
src/backup/LocalProvider.js
Normal file
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* LocalProvider — Backup provider for local folders
|
||||
*/
|
||||
|
||||
const path = require('path')
|
||||
const fs = require('fs').promises
|
||||
const BackupProvider = require('./BackupProvider')
|
||||
|
||||
class LocalProvider extends BackupProvider {
|
||||
constructor() {
|
||||
super()
|
||||
this.basePath = null
|
||||
}
|
||||
|
||||
async authenticate(credentials) {
|
||||
const basePath = String(credentials && credentials.basePath ? credentials.basePath : '').trim()
|
||||
if (!basePath) {
|
||||
throw new Error('Lokaler Backup-Ordner fehlt')
|
||||
}
|
||||
|
||||
this.basePath = path.resolve(basePath)
|
||||
await fs.mkdir(this.basePath, { recursive: true })
|
||||
}
|
||||
|
||||
async testConnection() {
|
||||
if (!this.basePath) return { ok: false, error: 'Not authenticated' }
|
||||
|
||||
try {
|
||||
const stat = await fs.stat(this.basePath)
|
||||
if (!stat.isDirectory()) {
|
||||
return { ok: false, error: 'Pfad ist kein Ordner' }
|
||||
}
|
||||
return { ok: true }
|
||||
} catch (err) {
|
||||
return { ok: false, error: err.message }
|
||||
}
|
||||
}
|
||||
|
||||
async uploadBackup(buffer, filename) {
|
||||
if (!this.basePath) throw new Error('Not authenticated')
|
||||
|
||||
const target = path.join(this.basePath, filename)
|
||||
await fs.mkdir(path.dirname(target), { recursive: true })
|
||||
await fs.writeFile(target, buffer)
|
||||
return { size: buffer.length }
|
||||
}
|
||||
|
||||
async listBackups() {
|
||||
if (!this.basePath) throw new Error('Not authenticated')
|
||||
|
||||
const entries = await fs.readdir(this.basePath, { withFileTypes: true })
|
||||
const files = entries.filter(e => e.isFile() && e.name.endsWith('.zip'))
|
||||
|
||||
const backups = []
|
||||
for (const f of files) {
|
||||
const full = path.join(this.basePath, f.name)
|
||||
const stat = await fs.stat(full)
|
||||
backups.push({
|
||||
name: f.name,
|
||||
size: stat.size,
|
||||
date: stat.mtime.toISOString()
|
||||
})
|
||||
}
|
||||
|
||||
backups.sort((a, b) => new Date(b.date) - new Date(a.date))
|
||||
return backups
|
||||
}
|
||||
|
||||
async downloadBackup(filename) {
|
||||
if (!this.basePath) throw new Error('Not authenticated')
|
||||
|
||||
const file = path.join(this.basePath, filename)
|
||||
return fs.readFile(file)
|
||||
}
|
||||
|
||||
async deleteBackup(filename) {
|
||||
if (!this.basePath) throw new Error('Not authenticated')
|
||||
|
||||
const file = path.join(this.basePath, filename)
|
||||
await fs.unlink(file)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = LocalProvider
|
||||
File diff suppressed because it is too large
Load Diff
@@ -15,9 +15,15 @@ async function initRepo(folderPath) {
|
||||
return true;
|
||||
}
|
||||
|
||||
async function commitAndPush(folderPath, branch = 'master', message = 'Update from Git Manager GUI', progressCb = null) {
|
||||
async function commitAndPush(folderPath, branch = null, message = 'Update from Git Manager GUI', progressCb = null) {
|
||||
const git = gitFor(folderPath);
|
||||
|
||||
// Branch auto-detect: falls nicht angegeben oder 'HEAD', aktuellen Branch ermitteln
|
||||
if (!branch || branch === 'HEAD') {
|
||||
const summary = await git.branchLocal();
|
||||
branch = summary.current || 'main';
|
||||
}
|
||||
|
||||
await git.add('./*');
|
||||
|
||||
try {
|
||||
@@ -44,8 +50,19 @@ async function commitAndPush(folderPath, branch = 'master', message = 'Update fr
|
||||
|
||||
async function getBranches(folderPath) {
|
||||
const git = gitFor(folderPath);
|
||||
const summary = await git.branchLocal();
|
||||
return summary.all;
|
||||
try {
|
||||
// Alle Branches: lokal + remote (origin/main, origin/master usw.)
|
||||
const summary = await git.branch(['-a']);
|
||||
const all = summary.all
|
||||
.map(b => b.replace(/^remotes\/origin\//, '').trim())
|
||||
.filter(b => !b.startsWith('HEAD'))
|
||||
.filter((b, i, arr) => arr.indexOf(b) === i); // deduplizieren
|
||||
return all;
|
||||
} catch (e) {
|
||||
// Fallback: nur lokale Branches
|
||||
const local = await git.branchLocal();
|
||||
return local.all;
|
||||
}
|
||||
}
|
||||
|
||||
async function getCommitLogs(folderPath, count = 50) {
|
||||
|
||||
293
src/utils/helpers.js
Normal file
293
src/utils/helpers.js
Normal file
@@ -0,0 +1,293 @@
|
||||
/**
|
||||
* Gemeinsame Utility-Funktionen für Git Manager GUI
|
||||
* - Branch Handling
|
||||
* - API Error Handling
|
||||
* - Standardisiertes Logging
|
||||
* - Caching
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const ppath = require('path');
|
||||
|
||||
// ===== LOGGING SYSTEM =====
|
||||
const LOG_LEVELS = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3 };
|
||||
let currentLogLevel = process.env.NODE_ENV === 'production' ? LOG_LEVELS.INFO : LOG_LEVELS.DEBUG;
|
||||
let logQueue = [];
|
||||
const MAX_LOG_BUFFER = 2000;
|
||||
|
||||
function formatLog(level, context, message, details = null) {
|
||||
const timestamp = new Date().toISOString();
|
||||
const levelStr = Object.keys(LOG_LEVELS).find(k => LOG_LEVELS[k] === level);
|
||||
return {
|
||||
timestamp,
|
||||
level: levelStr,
|
||||
context,
|
||||
message,
|
||||
details,
|
||||
pid: process.pid
|
||||
};
|
||||
}
|
||||
|
||||
function writeLog(logEntry) {
|
||||
logQueue.push(logEntry);
|
||||
if (logQueue.length > MAX_LOG_BUFFER) {
|
||||
logQueue.shift();
|
||||
}
|
||||
|
||||
// Auch in Console schreiben
|
||||
const { level, timestamp, context, message, details } = logEntry;
|
||||
const prefix = `[${timestamp}] [${level}] [${context}]`;
|
||||
|
||||
if (level === 'ERROR' && details?.error) {
|
||||
console.error(prefix, message, details.error);
|
||||
} else if (level === 'WARN') {
|
||||
console.warn(prefix, message, details ? JSON.stringify(details) : '');
|
||||
} else if (level !== 'DEBUG' || process.env.DEBUG) {
|
||||
console.log(prefix, message, details ? JSON.stringify(details) : '');
|
||||
}
|
||||
}
|
||||
|
||||
const logger = {
|
||||
debug: (context, message, details) => writeLog(formatLog(LOG_LEVELS.DEBUG, context, message, details)),
|
||||
info: (context, message, details) => writeLog(formatLog(LOG_LEVELS.INFO, context, message, details)),
|
||||
warn: (context, message, details) => writeLog(formatLog(LOG_LEVELS.WARN, context, message, details)),
|
||||
error: (context, message, details) => writeLog(formatLog(LOG_LEVELS.ERROR, context, message, details)),
|
||||
getRecent: (count = 20) => logQueue.slice(-count),
|
||||
setLevel: (level) => { currentLogLevel = LOG_LEVELS[level] || LOG_LEVELS.INFO; }
|
||||
};
|
||||
|
||||
// ===== BRANCH HANDLING =====
|
||||
const BRANCH_DEFAULTS = {
|
||||
gitea: 'main',
|
||||
github: 'main'
|
||||
};
|
||||
|
||||
function normalizeBranch(branch = 'HEAD', platform = 'gitea') {
|
||||
const value = String(branch || '').trim();
|
||||
|
||||
// HEAD sollte immer zu Standard konvertiert werden
|
||||
if (value.toLowerCase() === 'head') {
|
||||
return BRANCH_DEFAULTS[platform] || 'main';
|
||||
}
|
||||
|
||||
// Validierung: nur sichere Git-Referenzen
|
||||
if (/^[a-zA-Z0-9._\-/]+$/.test(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return BRANCH_DEFAULTS[platform] || 'main';
|
||||
}
|
||||
|
||||
function isSafeBranch(branch) {
|
||||
return /^[a-zA-Z0-9._\-/]+$/.test(String(branch || ''));
|
||||
}
|
||||
|
||||
// ===== ERROR HANDLING =====
|
||||
const ERROR_CODES = {
|
||||
NETWORK: 'NETWORK_ERROR',
|
||||
AUTH_FAILED: 'AUTH_FAILED',
|
||||
NOT_FOUND: 'NOT_FOUND',
|
||||
VALIDATION: 'VALIDATION_ERROR',
|
||||
RATE_LIMIT: 'RATE_LIMIT',
|
||||
SERVER_ERROR: 'SERVER_ERROR',
|
||||
UNKNOWN: 'UNKNOWN_ERROR'
|
||||
};
|
||||
|
||||
function parseApiError(error, defaultCode = ERROR_CODES.UNKNOWN) {
|
||||
if (!error) {
|
||||
return { code: defaultCode, message: 'Unknown error', statusCode: null };
|
||||
}
|
||||
|
||||
// Axios-style error
|
||||
if (error.response) {
|
||||
const status = error.response.status;
|
||||
const data = error.response.data;
|
||||
let code = defaultCode;
|
||||
|
||||
if (status === 401 || status === 403) {
|
||||
code = ERROR_CODES.AUTH_FAILED;
|
||||
} else if (status === 404) {
|
||||
code = ERROR_CODES.NOT_FOUND;
|
||||
} else if (status === 429) {
|
||||
code = ERROR_CODES.RATE_LIMIT;
|
||||
} else if (status >= 500) {
|
||||
code = ERROR_CODES.SERVER_ERROR;
|
||||
}
|
||||
|
||||
return {
|
||||
code,
|
||||
message: data?.message || error.message || `HTTP ${status}`,
|
||||
statusCode: status,
|
||||
rawMessage: data?.message
|
||||
};
|
||||
}
|
||||
|
||||
// Network error
|
||||
if (error.message?.includes('timeout') || error.code?.includes('TIMEOUT')) {
|
||||
return { code: ERROR_CODES.NETWORK, message: 'Request timeout', statusCode: null };
|
||||
}
|
||||
|
||||
if (error.code?.includes('ECONNREFUSED') || error.message?.includes('ECONNREFUSED')) {
|
||||
return { code: ERROR_CODES.NETWORK, message: 'Connection refused', statusCode: null };
|
||||
}
|
||||
|
||||
return {
|
||||
code: ERROR_CODES.UNKNOWN,
|
||||
message: error.message || String(error),
|
||||
statusCode: null
|
||||
};
|
||||
}
|
||||
|
||||
function formatErrorForUser(error, context = 'Operation') {
|
||||
const parsed = parseApiError(error);
|
||||
const messages = {
|
||||
[ERROR_CODES.AUTH_FAILED]: `Authentifizierung fehlgeschlagen. Bitte Token überprüfen.`,
|
||||
[ERROR_CODES.NOT_FOUND]: `Ressource nicht gefunden.`,
|
||||
[ERROR_CODES.NETWORK]: `Netzwerkfehler. Bitte Verbindung überprüfen.`,
|
||||
[ERROR_CODES.RATE_LIMIT]: `Zu viele Anfragen. Bitte später versuchen.`,
|
||||
[ERROR_CODES.SERVER_ERROR]: `Server-Fehler. Bitte später versuchen.`,
|
||||
[ERROR_CODES.UNKNOWN]: `${context} fehlgeschlagen.`
|
||||
};
|
||||
|
||||
return {
|
||||
userMessage: messages[parsed.code],
|
||||
technicalMessage: parsed.message,
|
||||
code: parsed.code,
|
||||
details: parsed
|
||||
};
|
||||
}
|
||||
|
||||
// ===== CACHING SYSTEM =====
|
||||
class Cache {
|
||||
constructor(ttl = 300000) { // 5 min default
|
||||
this.store = new Map();
|
||||
this.ttl = ttl;
|
||||
}
|
||||
|
||||
set(key, value, customTtl = null) {
|
||||
const expiry = Date.now() + (customTtl || this.ttl);
|
||||
this.store.set(key, { value, expiry });
|
||||
}
|
||||
|
||||
get(key) {
|
||||
const item = this.store.get(key);
|
||||
if (!item) return null;
|
||||
if (Date.now() > item.expiry) {
|
||||
this.store.delete(key);
|
||||
return null;
|
||||
}
|
||||
return item.value;
|
||||
}
|
||||
|
||||
invalidate(keyPattern) {
|
||||
for (const [key] of this.store) {
|
||||
if (key.includes(keyPattern)) {
|
||||
this.store.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.store.clear();
|
||||
}
|
||||
|
||||
size() {
|
||||
return this.store.size;
|
||||
}
|
||||
}
|
||||
|
||||
// Standard Caches
|
||||
const caches = {
|
||||
repos: new Cache(600000), // 10 min
|
||||
fileTree: new Cache(300000), // 5 min
|
||||
api: new Cache(120000) // 2 min
|
||||
};
|
||||
|
||||
// ===== PARALLEL OPERATIONS =====
|
||||
async function runParallel(operations, concurrency = 4, onProgress = null) {
|
||||
const results = new Array(operations.length);
|
||||
let completed = 0;
|
||||
let index = 0;
|
||||
|
||||
async function worker() {
|
||||
while (index < operations.length) {
|
||||
const i = index++;
|
||||
try {
|
||||
results[i] = { ok: true, result: await operations[i]() };
|
||||
} catch (e) {
|
||||
results[i] = { ok: false, error: e };
|
||||
}
|
||||
completed++;
|
||||
if (onProgress) {
|
||||
try { onProgress(completed, operations.length); } catch (_) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const workers = Array.from({ length: Math.min(concurrency, operations.length) }, () => worker());
|
||||
await Promise.all(workers);
|
||||
return results;
|
||||
}
|
||||
|
||||
// ===== RETRY LOGIC =====
|
||||
async function retryWithBackoff(fn, maxAttempts = 3, baseDelay = 1000) {
|
||||
let lastError;
|
||||
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (e) {
|
||||
lastError = e;
|
||||
if (attempt < maxAttempts - 1) {
|
||||
const delay = baseDelay * Math.pow(2, attempt);
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
}
|
||||
}
|
||||
}
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
// ===== FILE OPERATIONS =====
|
||||
function ensureDirectory(dirPath) {
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
function safeReadFile(filePath, defaultValue = null) {
|
||||
try {
|
||||
if (!fs.existsSync(filePath)) return defaultValue;
|
||||
return fs.readFileSync(filePath, 'utf8');
|
||||
} catch (e) {
|
||||
logger.warn('safeReadFile', `Failed to read ${filePath}`, { error: e.message });
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
function safeWriteFile(filePath, content) {
|
||||
try {
|
||||
ensureDirectory(ppath.dirname(filePath));
|
||||
fs.writeFileSync(filePath, content, 'utf8');
|
||||
return true;
|
||||
} catch (e) {
|
||||
logger.error('safeWriteFile', `Failed to write ${filePath}`, { error: e.message });
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== EXPORTS =====
|
||||
module.exports = {
|
||||
logger,
|
||||
normalizeBranch,
|
||||
isSafeBranch,
|
||||
parseApiError,
|
||||
formatErrorForUser,
|
||||
ERROR_CODES,
|
||||
Cache,
|
||||
caches,
|
||||
runParallel,
|
||||
retryWithBackoff,
|
||||
ensureDirectory,
|
||||
safeReadFile,
|
||||
safeWriteFile,
|
||||
LOG_LEVELS
|
||||
};
|
||||
247
updater.js
247
updater.js
@@ -3,9 +3,11 @@ const { app, shell } = require('electron');
|
||||
const https = require('https');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const { spawn } = require('child_process');
|
||||
|
||||
const GITEA_API_URL = 'https://git.viper.ipv64.net/api/v1/repos/M_Viper/Git-Manager-Gui/releases';
|
||||
const TRUSTED_UPDATE_HOST = 'git.viper.ipv64.net';
|
||||
|
||||
class Updater {
|
||||
constructor(mainWindow) {
|
||||
@@ -13,6 +15,22 @@ class Updater {
|
||||
this.checkingForUpdates = false;
|
||||
}
|
||||
|
||||
emitUpdateError(message, details = {}) {
|
||||
const payload = {
|
||||
message: String(message || 'Update-Fehler'),
|
||||
details,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
try {
|
||||
if (this.mainWindow && this.mainWindow.webContents) {
|
||||
this.mainWindow.webContents.send('update-error', payload);
|
||||
}
|
||||
} catch (_) {
|
||||
// no-op: update errors should never crash the app
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hauptfunktion zur Prüfung auf Updates
|
||||
*/
|
||||
@@ -31,18 +49,36 @@ class Updater {
|
||||
console.log(`[Updater] Version-Check: Server(${serverVer}) vs Lokal(${localVer})`);
|
||||
|
||||
if (this.compareVersions(serverVer, localVer) > 0) {
|
||||
const asset = this.findAsset(latestRelease.assets);
|
||||
const checksumAsset = this.findChecksumAsset(latestRelease.assets, asset);
|
||||
const expectedSha256 = this.extractChecksumFromReleaseBody(latestRelease.body, asset?.name);
|
||||
|
||||
console.log("[Updater] Update verfügbar. Sende Daten an Renderer...");
|
||||
this.mainWindow.webContents.send('update-available', {
|
||||
version: serverVer,
|
||||
body: latestRelease.body,
|
||||
url: latestRelease.html_url,
|
||||
asset: this.findAsset(latestRelease.assets)
|
||||
asset: asset ? {
|
||||
...asset,
|
||||
checksumUrl: checksumAsset ? checksumAsset.browser_download_url : null,
|
||||
expectedSha256
|
||||
} : null
|
||||
});
|
||||
} else {
|
||||
console.log("[Updater] Anwendung ist auf dem neuesten Stand.");
|
||||
if (!silent) {
|
||||
this.mainWindow.webContents.send('update-not-available', {
|
||||
version: localVer
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Updater] Fehler beim Update-Check:', error);
|
||||
this.emitUpdateError('Update-Pruefung fehlgeschlagen.', {
|
||||
phase: 'check',
|
||||
silent: !!silent,
|
||||
error: String(error?.message || error)
|
||||
});
|
||||
} finally {
|
||||
this.checkingForUpdates = false;
|
||||
}
|
||||
@@ -52,6 +88,10 @@ class Updater {
|
||||
return new Promise((resolve, reject) => {
|
||||
const options = { headers: { 'User-Agent': 'GitManager-GUI-Updater' } };
|
||||
https.get(GITEA_API_URL, options, (res) => {
|
||||
if (res.statusCode !== 200) {
|
||||
reject(new Error(`Release-API antwortete mit HTTP ${res.statusCode}`));
|
||||
return;
|
||||
}
|
||||
let data = '';
|
||||
res.on('data', chunk => data += chunk);
|
||||
res.on('end', () => {
|
||||
@@ -77,7 +117,118 @@ class Updater {
|
||||
findAsset(assets) {
|
||||
if (!assets) return null;
|
||||
const ext = process.platform === 'win32' ? '.exe' : '.AppImage';
|
||||
return assets.find(a => a.name.toLowerCase().endsWith(ext));
|
||||
return assets.find(a => {
|
||||
const name = String(a?.name || '').toLowerCase();
|
||||
// Leerzeichen im Namen erlauben!
|
||||
const validName = /^[a-z0-9._\- ]+$/i.test(name);
|
||||
return validName && name.endsWith(ext);
|
||||
});
|
||||
}
|
||||
|
||||
findChecksumAsset(assets, targetAsset) {
|
||||
if (!Array.isArray(assets) || !targetAsset?.name) return null;
|
||||
const targetLower = String(targetAsset.name).toLowerCase();
|
||||
|
||||
const exactCandidates = [
|
||||
`${targetLower}.sha256`,
|
||||
`${targetLower}.sha256sum`,
|
||||
`${targetLower}.sha512`,
|
||||
`${targetLower}.sha512sum`
|
||||
];
|
||||
|
||||
const exact = assets.find(a => exactCandidates.includes(String(a?.name || '').toLowerCase()));
|
||||
if (exact) return exact;
|
||||
|
||||
return assets.find(a => {
|
||||
const name = String(a?.name || '').toLowerCase();
|
||||
return name.includes('checksum') || name.includes('checksums') || name.endsWith('.sha256') || name.endsWith('.sha256sum');
|
||||
}) || null;
|
||||
}
|
||||
|
||||
extractChecksumFromReleaseBody(body, fileName) {
|
||||
const text = String(body || '');
|
||||
const target = String(fileName || '').trim();
|
||||
if (!text || !target) return null;
|
||||
|
||||
const lines = text.split(/\r?\n/);
|
||||
const targetLower = target.toLowerCase();
|
||||
for (const line of lines) {
|
||||
const normalized = String(line || '').trim();
|
||||
if (!normalized) continue;
|
||||
const match = normalized.match(/\b([a-fA-F0-9]{64})\b/);
|
||||
if (!match) continue;
|
||||
if (normalized.toLowerCase().includes(targetLower)) {
|
||||
return match[1].toLowerCase();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
downloadText(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.isTrustedDownloadUrl(url)) {
|
||||
reject(new Error('Unsichere Checksum-URL blockiert.'));
|
||||
return;
|
||||
}
|
||||
|
||||
https.get(url, { headers: { 'User-Agent': 'Electron-Updater' } }, (res) => {
|
||||
if (res.statusCode === 301 || res.statusCode === 302) {
|
||||
return resolve(this.downloadText(res.headers.location || ''));
|
||||
}
|
||||
if (res.statusCode !== 200) {
|
||||
reject(new Error(`Checksum-Download fehlgeschlagen: HTTP ${res.statusCode}`));
|
||||
return;
|
||||
}
|
||||
let data = '';
|
||||
res.on('data', chunk => data += chunk.toString('utf8'));
|
||||
res.on('end', () => resolve(data));
|
||||
}).on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
extractChecksumFromText(text, fileName) {
|
||||
const lines = String(text || '').split(/\r?\n/);
|
||||
const targetLower = String(fileName || '').toLowerCase();
|
||||
for (const line of lines) {
|
||||
const normalized = String(line || '').trim();
|
||||
if (!normalized) continue;
|
||||
const hashMatch = normalized.match(/\b([a-fA-F0-9]{64})\b/);
|
||||
if (!hashMatch) continue;
|
||||
if (!targetLower || normalized.toLowerCase().includes(targetLower)) {
|
||||
return hashMatch[1].toLowerCase();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
computeFileSha256(filePath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const hash = crypto.createHash('sha256');
|
||||
const stream = fs.createReadStream(filePath);
|
||||
stream.on('error', reject);
|
||||
stream.on('data', chunk => hash.update(chunk));
|
||||
stream.on('end', () => resolve(hash.digest('hex').toLowerCase()));
|
||||
});
|
||||
}
|
||||
|
||||
async resolveExpectedSha256(asset) {
|
||||
const expectedFromAsset = String(asset?.expectedSha256 || '').trim().toLowerCase();
|
||||
if (/^[a-f0-9]{64}$/.test(expectedFromAsset)) return expectedFromAsset;
|
||||
|
||||
const checksumUrl = String(asset?.checksumUrl || '').trim();
|
||||
if (!checksumUrl) return null;
|
||||
|
||||
const checksumText = await this.downloadText(checksumUrl);
|
||||
return this.extractChecksumFromText(checksumText, asset?.name);
|
||||
}
|
||||
|
||||
isTrustedDownloadUrl(rawUrl) {
|
||||
try {
|
||||
const parsed = new URL(String(rawUrl || ''));
|
||||
return parsed.protocol === 'https:' && parsed.hostname === TRUSTED_UPDATE_HOST;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -86,36 +237,113 @@ class Updater {
|
||||
async startDownload(asset) {
|
||||
if (!asset || !asset.browser_download_url) {
|
||||
console.error("[Updater] Kein gültiges Asset gefunden!");
|
||||
this.emitUpdateError('Update-Download fehlgeschlagen: Kein gueltiges Installer-Asset gefunden.', {
|
||||
phase: 'download',
|
||||
reason: 'invalid-asset'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isTrustedDownloadUrl(asset.browser_download_url)) {
|
||||
console.error('[Updater] Unsichere Download-URL blockiert.');
|
||||
this.emitUpdateError('Update blockiert: Unsichere Download-URL.', {
|
||||
phase: 'download',
|
||||
reason: 'untrusted-download-url'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const tempPath = path.join(app.getPath('temp'), asset.name);
|
||||
console.log(`[Updater] Download gestartet: ${asset.name} -> ${tempPath}`);
|
||||
|
||||
let expectedSha256 = null;
|
||||
try {
|
||||
expectedSha256 = await this.resolveExpectedSha256(asset);
|
||||
} catch (e) {
|
||||
console.error('[Updater] Konnte erwartete Checksumme nicht laden:', e?.message || e);
|
||||
this.emitUpdateError('Update-Download fehlgeschlagen: Checksumme konnte nicht geladen werden.', {
|
||||
phase: 'download',
|
||||
reason: 'checksum-load-failed',
|
||||
error: String(e?.message || e)
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!expectedSha256) {
|
||||
console.error('[Updater] Kein SHA-256-Checksum-Wert gefunden. Update wurde aus Sicherheitsgruenden blockiert.');
|
||||
this.emitUpdateError('Update blockiert: Keine gueltige SHA-256 Checksumme gefunden.', {
|
||||
phase: 'download',
|
||||
reason: 'checksum-missing'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const file = fs.createWriteStream(tempPath);
|
||||
|
||||
const download = (url) => {
|
||||
if (!this.isTrustedDownloadUrl(url)) {
|
||||
console.error('[Updater] Unsicherer Redirect/Download blockiert.');
|
||||
fs.unlink(tempPath, () => {});
|
||||
this.emitUpdateError('Update blockiert: Unsicherer Redirect oder Download-Host.', {
|
||||
phase: 'download',
|
||||
reason: 'untrusted-redirect'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
https.get(url, { headers: { 'User-Agent': 'Electron-Updater' } }, (res) => {
|
||||
// Handle Redirects
|
||||
if (res.statusCode === 301 || res.statusCode === 302) {
|
||||
return download(res.headers.location);
|
||||
return download(res.headers.location || '');
|
||||
}
|
||||
|
||||
if (res.statusCode !== 200) {
|
||||
console.error(`[Updater] Download-Fehler: Status ${res.statusCode}`);
|
||||
this.emitUpdateError(`Update-Download fehlgeschlagen: HTTP ${res.statusCode}.`, {
|
||||
phase: 'download',
|
||||
reason: 'http-error',
|
||||
statusCode: res.statusCode
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.pipe(file);
|
||||
|
||||
file.on('finish', () => {
|
||||
file.on('finish', async () => {
|
||||
file.close();
|
||||
console.log("[Updater] Download abgeschlossen. Initialisiere entkoppelten Installer...");
|
||||
try {
|
||||
const actualSha256 = await this.computeFileSha256(tempPath);
|
||||
if (actualSha256 !== expectedSha256) {
|
||||
console.error('[Updater] Checksum-Validierung fehlgeschlagen. Installation wurde blockiert.');
|
||||
fs.unlink(tempPath, () => {});
|
||||
this.emitUpdateError('Update blockiert: Checksum-Validierung fehlgeschlagen.', {
|
||||
phase: 'download',
|
||||
reason: 'checksum-mismatch'
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (verifyErr) {
|
||||
console.error('[Updater] Checksum-Validierung konnte nicht ausgeführt werden:', verifyErr?.message || verifyErr);
|
||||
fs.unlink(tempPath, () => {});
|
||||
this.emitUpdateError('Update-Download fehlgeschlagen: Checksum-Validierung nicht moeglich.', {
|
||||
phase: 'download',
|
||||
reason: 'checksum-verify-failed',
|
||||
error: String(verifyErr?.message || verifyErr)
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("[Updater] Download und Checksum-Validierung abgeschlossen. Initialisiere entkoppelten Installer...");
|
||||
this.installAndQuit(tempPath);
|
||||
});
|
||||
}).on('error', (err) => {
|
||||
fs.unlink(tempPath, () => {});
|
||||
console.error("[Updater] Netzwerkfehler beim Download:", err);
|
||||
this.emitUpdateError('Update-Download fehlgeschlagen: Netzwerkfehler.', {
|
||||
phase: 'download',
|
||||
reason: 'network-error',
|
||||
error: String(err?.message || err)
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -129,15 +357,14 @@ class Updater {
|
||||
console.log(`[Updater] Bereite Installation vor: ${filePath}`);
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
// Wir nutzen spawn mit detached: true, damit der Installer weiterläuft,
|
||||
// wenn der Hauptprozess (Electron) beendet wird.
|
||||
try {
|
||||
const child = spawn('cmd.exe', ['/c', 'start', '""', filePath], {
|
||||
const child = spawn(filePath, [], {
|
||||
detached: true,
|
||||
stdio: 'ignore'
|
||||
stdio: 'ignore',
|
||||
shell: false
|
||||
});
|
||||
|
||||
child.unref(); // Trennt die Referenz zum Installer
|
||||
child.unref();
|
||||
|
||||
console.log("[Updater] Installer-Prozess entkoppelt gestartet. App schließt in 2 Sek...");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user