Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e1cee9a59 | ||
|
|
2e26aaa41c | ||
|
|
eb13bd071b | ||
|
|
156b8e3041 | ||
|
|
9d3472dff0 | ||
|
|
0029668033 | ||
|
|
a809efd91a | ||
|
|
a902a0c5d7 | ||
|
|
7fa0db02b5 | ||
|
|
673affe1d1 | ||
|
|
65dd67cef5 | ||
|
|
ef68a305ce | ||
|
|
e154cc507e | ||
|
|
56b5cb17d0 | ||
|
|
e178605833 | ||
|
|
f4c16b173f | ||
|
|
2ad8777311 | ||
|
|
ce5a655758 | ||
|
|
7660b5de1a | ||
|
|
50705d1197 | ||
|
|
1f635d33cc | ||
|
|
38a75bc720 | ||
|
|
ead123c62b | ||
|
|
9b3aed6f0a | ||
|
|
99d9629e25 | ||
|
|
85a222a9fb | ||
|
|
dedb09b9c6 | ||
|
|
c45ad3ba32 | ||
|
|
937c72ce4a | ||
|
|
61d6d11a2f | ||
|
|
ef911afb06 | ||
|
|
4f9494ae7b | ||
|
|
37820af6dd | ||
|
|
9c6a69cc53 | ||
|
|
eb549d3975 | ||
|
|
b2314f6fe6 | ||
|
|
42fb8dfe9b | ||
|
|
54baf9b874 | ||
|
|
8d29040b57 | ||
|
|
8c7b2ddf47 | ||
|
|
03f4fa6fbc | ||
|
|
a57786fe54 | ||
|
|
6f73a87314 | ||
|
|
7ef5522258 | ||
|
|
860c714301 | ||
|
|
68133f4d0f | ||
|
|
75cc88a011 | ||
|
|
bac355eabb | ||
|
|
d7658c873f | ||
|
|
8830aac76c | ||
|
|
336d9e600b | ||
|
|
d6518698b1 | ||
|
|
e4585e50ee | ||
|
|
12a2f5faa9 | ||
|
|
8aede65bd7 | ||
|
|
ae68900363 | ||
|
|
81c541a0b5 | ||
|
|
089257d740 | ||
|
|
2ecc57c761 | ||
|
|
4654eed12c | ||
|
|
434a4e1c7b | ||
|
|
a0546c562d | ||
|
|
264f1b7e45 | ||
|
|
3b31c5893c | ||
|
|
047196c4d0 | ||
|
|
5571f225d5 | ||
|
|
565835418e | ||
|
|
3ed561fe78 | ||
|
|
fe6c31793e | ||
|
|
4bb948d665 | ||
|
|
5c792eb1a8 | ||
|
|
309babf849 | ||
|
|
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 | ||
|
|
3f2e1d1e30 | ||
|
|
e7b3c9c447 | ||
|
|
e8474957e8 | ||
|
|
94e8c21e9b | ||
|
|
7d18161141 | ||
|
|
d88eade5a0 | ||
|
|
6895a042d1 | ||
|
|
e379f6a096 | ||
|
|
fca9d9c66f | ||
|
|
fd868ea238 | ||
|
|
1a76dc4e64 | ||
|
|
a0c964e05e | ||
|
|
e109082d56 | ||
|
|
2c93a139d5 | ||
|
|
7bd8668e79 | ||
|
|
19be614284 | ||
|
|
0272756be7 |
@@ -0,0 +1,5 @@
|
|||||||
|
* text=auto eol=lf
|
||||||
|
*.png binary
|
||||||
|
*.ico binary
|
||||||
|
*.exe binary
|
||||||
|
*.blockmap binary
|
||||||
+58
@@ -0,0 +1,58 @@
|
|||||||
|
# ===== Zugangsdaten / Secrets (NIE hochladen!) =====
|
||||||
|
/data/
|
||||||
|
credentials.json
|
||||||
|
*.env
|
||||||
|
.env*
|
||||||
|
*.pem
|
||||||
|
*.key
|
||||||
|
*.pfx
|
||||||
|
*.p12
|
||||||
|
|
||||||
|
# ===== Abhängigkeiten =====
|
||||||
|
node_modules/
|
||||||
|
.npm/
|
||||||
|
.cache/
|
||||||
|
|
||||||
|
# ===== Build-Ausgaben =====
|
||||||
|
dist/
|
||||||
|
out/
|
||||||
|
release/
|
||||||
|
*.exe
|
||||||
|
*.blockmap
|
||||||
|
*.sha256
|
||||||
|
*.sha256sum
|
||||||
|
|
||||||
|
# ===== .NET Installer (Visual Studio / dotnet) =====
|
||||||
|
installer/bin/
|
||||||
|
installer/obj/
|
||||||
|
**/bin/
|
||||||
|
**/obj/
|
||||||
|
.vs/
|
||||||
|
*.user
|
||||||
|
*.suo
|
||||||
|
|
||||||
|
# ===== Große gebündelte Binaries (MinGit, ca. 93 MB) =====
|
||||||
|
vendor/mingit/
|
||||||
|
|
||||||
|
# ===== Laufzeitdaten / Backups / Repos =====
|
||||||
|
/repos/
|
||||||
|
/backup/
|
||||||
|
*.zip
|
||||||
|
*.rar
|
||||||
|
*.7z
|
||||||
|
|
||||||
|
# ===== Logs =====
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# ===== Editor / OS =====
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
desktop.ini
|
||||||
|
|
||||||
|
# ===== Claude Code (lokale Einstellungen) =====
|
||||||
|
.claude/settings.local.json
|
||||||
+584
@@ -0,0 +1,584 @@
|
|||||||
|
# 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
|
||||||
|
```
|
||||||
|
|
||||||
|
### Abbrechbare Vorgänge
|
||||||
|
|
||||||
|
Uploads, Ordner-Downloads und Batch-Clones laufen über **ein** gemeinsames Abbruch-System:
|
||||||
|
|
||||||
|
- `main.js`: `beginCancellableUpload()` legt einen `AbortController` an, `endCancellableUpload()` räumt auf (immer im `finally`). Der IPC-Kanal `cancel-upload` bricht alle laufenden Vorgänge ab.
|
||||||
|
- Das `signal` wird durchgereicht an `simple-git` (`abort`), `execGit`/`runGitAsync` (`signal`), `runLimited` und `uploadItemsViaGiteaApi` (axios `signal`).
|
||||||
|
- Ein Abbruch ist **kein Fehler**: Er läuft als `UPLOAD_CANCELLED` durch die Kette (`createUploadCancelledError`, `isUploadCancelledError`, `throwIfUploadCancelled` in `src/git/apiHandler.js`), löst keinen API-Ausweichweg aus und kommt als `{ ok: false, cancelled: true }` im Renderer an.
|
||||||
|
- Neuer abbrechbarer Handler: `beginCancellableUpload()` am Anfang, `signal` weiterreichen, im `catch` `isUploadCancelledError(e)` abfangen, im `finally` `endCancellableUpload()`. Im Renderer `showProgress(..., { cancellable: true })`.
|
||||||
|
- Unter Windows beendet `spawn(signal)` nur `git.exe`; `execGit` beendet deshalb den ganzen Prozessbaum (`taskkill /T /F`) und kehrt sofort zurück.
|
||||||
|
|
||||||
|
### Pause, Sperre, Benachrichtigung, Release-Assistent
|
||||||
|
|
||||||
|
- **Pause:** `src/features/pauseGate.js` (`createPauseGate`). In `main.js` gibt es genau ein `transferGate`. Kontrollpunkte rufen `await transferGate.wait(signal)` auf (in `runLimited`, vor dem Staging der Git-Wege, zwischen den Paketen von `uploadItemsViaGiteaApi` über `waitIfPaused`, vor jedem Batch-Clone). Ein Abbruch löst Wartende sofort aus. Neue langlaufende Schritte brauchen einen eigenen Kontrollpunkt.
|
||||||
|
- **Zugangsdaten-Sperre:** `src/features/uploadBlocklist.js` nutzt die Dateinamen-Regeln aus `secretScan.js`. `removeBlockedInPlace()` und `blockedSecretReason()` in `main.js` sind in allen Upload-Handlern eingebaut, die Ergebnisse tragen `blockedFiles`. Neuer Upload-Weg: dort ebenfalls aufrufen. Einstellung `uploadBlockSecrets`.
|
||||||
|
- **Benachrichtigung:** `src/features/transferNotice.js` baut den Text aus dem Verlaufseintrag, `maybeNotifyTransfer()` in `main.js` zeigt ihn nur, wenn das Hauptfenster nicht vorn ist (Einstellung `notifyTransfers`).
|
||||||
|
- **Release-Assistent:** `src/features/releaseAssist.js` (Version aus Dateiname, Versionsvergleich, Tag-Prüfung, Release-Text mit Prüfsummen). IPC `inspect-release-file` und `prepare-release`, Oberfläche in `showCreateReleaseModal()`. Das Prüfsummen-Format in `buildReleaseBody` muss zu `extractChecksumFromReleaseBody` in `updater.js` passen.
|
||||||
|
|
||||||
|
### Startanimation
|
||||||
|
|
||||||
|
Nach dem Vorbild des AeroMC-Launchers (`AeroMC-Launcher/src/startanimation.js`):
|
||||||
|
|
||||||
|
1. `createWindow()` berechnet Position und Größe (`centeredBounds`) und legt über `begin()` zuerst ein randloses, durchsichtiges Fenster mit `renderer/splash.html` an: das Logo dreht sich (CSS `rotateY`), darunter der Schriftzug.
|
||||||
|
2. Das Hauptfenster entsteht danach mit denselben Maßen, verborgen, und lädt.
|
||||||
|
3. `play()` wartet auf `did-finish-load` (höchstens 12 s) und mindestens 1,6 s Drehzeit. Dann zeigt es das Hauptfenster unsichtbar (Deckkraft 0, Mausereignisse aus), wartet auf die gezeichnete Oberfläche (Repo-Liste oder höchstens 1,5 s), fotografiert es mit `capturePage()` und lässt `aufbauen()` in `splash.js` das Bild von der Mitte aus aufziehen.
|
||||||
|
4. Am Ende übernimmt das echte Fenster an derselben Stelle. Fehler und Zeitüberschreitungen zeigen das Hauptfenster immer trotzdem.
|
||||||
|
|
||||||
|
**Stolperstein:** `setIgnoreMouseEvents(true)` am Startfenster darf erst nach dem Zeigen und leicht verzögert aufgerufen werden (`IGNORE_MOUSE_DELAY_MS`). Davor aufgerufen wird das durchsichtige Fenster unter Windows weiß. `scripts/ui-smoke-splash.js` prüft das über eine Bildschirmaufnahme (Farbe an einer leeren Stelle muss der des Desktops entsprechen) und darf deshalb die Hardware-Beschleunigung nicht abschalten.
|
||||||
|
|
||||||
|
Beim Start mit `--hidden` und bei abgeschalteter Einstellung `showSplash` gibt es die Animation nicht. `scripts/ui-smoke-splash.js` prüft sie mit einem Ersatz-Hauptfenster. Im Entwicklungsmodus (`electron .`) startet die App wegen der vielen `node_modules`-Dateien deutlich langsamer als das gepackte Programm; die Animation selbst wird dadurch nicht langsamer.
|
||||||
|
|
||||||
|
### Oberflächen-Testlauf
|
||||||
|
|
||||||
|
`npm run test:ui` (`scripts/ui-smoke.js`) startet die App mit leerem eigenen `APPDATA`, steuert sie über den Debug-Port und prüft Toolbar-Layout bei drei Breiten, Fortschrittsbalken, Pause, Verlauf, Sperre, Release-Prüfung und Vorlagen. `scripts/ui-smoke-tray.js` prüft das Tray-Menüfenster in echtem Electron. Der Lauf berührt weder echte Zugangsdaten noch einen Server und dauert gut eine Minute.
|
||||||
|
|
||||||
|
### Tray-Menü
|
||||||
|
|
||||||
|
Das Tray-Symbol öffnet kein natives Menü, sondern ein eigenes kleines Fenster, denn das native Electron-Menü zeichnet unter Windows keine Hover-Markierung:
|
||||||
|
|
||||||
|
- `src/features/trayMenu.js`: Inhalt (`buildTrayTemplate`, `serializeTemplate`, `runAction`, `trayTooltip`), ohne Electron und damit testbar. Neuer Eintrag: dort mit `id` ergänzen, die Aktion in `trayTemplate()` in `main.js` verdrahten.
|
||||||
|
- `src/features/trayPopup.js`: das Fenster (frameless, transparent, schließt bei Fokusverlust und Esc). Es wird 2 Sekunden nach dem Start vorgeladen, weil der erste Renderer-Start mehrere Sekunden dauert.
|
||||||
|
- `renderer/tray-menu.html`, `renderer/tray-menu.js`, `tray-menu-preload.js`: Darstellung (hell/dunkel nach Windows-Design, Pfeiltasten und Enter) und die vier IPC-Kanäle.
|
||||||
|
- Das Fenster ist von der Zoom-Einstellung ausgenommen und wird beim Beenden zerstört, sonst hielte das versteckte Fenster die App am Leben.
|
||||||
|
|
||||||
|
### Upload-Verlauf und Wiederholen
|
||||||
|
|
||||||
|
- `src/features/uploadHistory.js` (`buildEntry`, `filterEntries`, `createUploadHistory`): speichert die letzten 100 Uploads in `upload-history.json` im userData-Ordner. `main.js` umhüllt dafür `ipcMain.handle` für die Kanäle in `UPLOAD_HISTORY_CHANNELS`, neue Upload-Kanäle dort eintragen.
|
||||||
|
- Teilfehler: `buildPartialUploadError` hängt die fehlgeschlagenen Einträge an den Fehler (`failedItems`). Die Handler geben sie als `retryItems` zurück, der Renderer sendet sie über den Parameter `items` erneut (`retryFailedUploads`).
|
||||||
|
- `.gitignore`-Vorlagen: `src/features/gitignoreTemplates.js`.
|
||||||
|
|
||||||
|
### Version erhöhen
|
||||||
|
|
||||||
|
1. Version in `package.json` updaten: `"version": "2.2.1"`
|
||||||
|
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
|
||||||
+1462
File diff suppressed because it is too large
Load Diff
@@ -1,61 +1,234 @@
|
|||||||
# 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
|
**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.
|
||||||
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**.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Installation
|
## ✨ Features
|
||||||
|
|
||||||
1. Lade die **Windows-Installer-EXE** von der Release-Seite herunter.
|
### 🎯 Repository-Verwaltung
|
||||||
2. Führe die EXE aus und folge den Installationsschritten.
|
- 📂 **Intelligente Repo-Übersicht** mit Tabs nach Besitzer und Fuzzy-Suche
|
||||||
3. Optional: Desktop-Shortcut erstellen lassen.
|
- 🌐 **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
|
||||||
|
- ⏹️ **Abbrechen** — Uploads, Downloads und Batch-Clones lassen sich per Knopf oder <kbd>Esc</kbd> stoppen
|
||||||
|
- ⏱️ **Fortschritt mit MB und Restzeit** — bei großen Uploads siehst du, ob es noch läuft
|
||||||
|
- 🔁 **Nur Fehlgeschlagene wiederholen** — nach einem Teilfehler nicht alles neu hochladen
|
||||||
|
- ⏸️ **Pausieren und Fortsetzen** — Übertragungen anhalten, auch über das Tray-Menü
|
||||||
|
- 🕘 **Upload-Verlauf** — die letzten 100 Uploads, filterbar nach Ergebnis und Repo, Klick öffnet das Repo, Export als CSV
|
||||||
|
- 🛡️ **Zugangsdaten-Sperre** — `credentials.json`, `.env`, Schlüssel und Ähnliches werden auf allen Upload-Wegen nie hochgeladen
|
||||||
|
- 🔔 **Windows-Meldung bei Upload-Ende** — wenn die App im Tray versteckt oder nicht im Vordergrund ist
|
||||||
|
- 📄 **.gitignore anlegen** — Vorlagen für Node.js, .NET, Electron, Python, Java, PHP und Unity
|
||||||
|
- 📦 **Release für den Updater** — Installer wählen, Prüfsumme abgleichen, Tag prüfen, `.sha256` mit hochladen
|
||||||
|
|
||||||
|
### 📊 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**
|
| Anforderung | Mindest | Empfohlen |
|
||||||
Öffne die Einstellungen und trage ein:
|
|-------------|--------|----------|
|
||||||
- GitHub Token (für GitHub Repos)
|
| **OS** | Windows 10 64-bit | Windows 11 |
|
||||||
- Gitea Token & URL (für Gitea Repos)
|
| **RAM** | 4 GB | 8 GB |
|
||||||
|
| **Festplatte** | 200 MB | 1 GB (für Repos) |
|
||||||
2. **Projekte laden**
|
| **.NET** | Nicht erforderlich | — |
|
||||||
- Klicke auf **Load Gitea Repos**, um deine Repositories zu sehen
|
| **Node.js** | Nicht erforderlich | — |
|
||||||
- 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
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 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 · laufenden Upload/Download abbrechen (wenn kein Dialog offen ist) |
|
||||||
|
| <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.2.1
|
||||||
|
- ⏹️ Abbrechen-Knopf (und <kbd>Esc</kbd>) für Uploads, Ordner-Downloads und Batch-Clones
|
||||||
|
- ⏸️ Pausieren und Fortsetzen im Fortschrittsbalken und im Tray-Menü
|
||||||
|
- 🔁 „Nur Fehlgeschlagene wiederholen“ bei Teilfehlern von Drag-&-Drop- und Ordner-Uploads
|
||||||
|
- ⏱️ Fortschrittsanzeige mit übertragenen MB und geschätzter Restzeit
|
||||||
|
- 🕘 Upload-Verlauf mit Filter, Klick zum Öffnen des Repos und CSV-Export
|
||||||
|
- 🛡️ Feste Sperre für Dateien mit Zugangsdaten auf allen Upload-Wegen (abschaltbar)
|
||||||
|
- 🔔 Windows-Meldung, wenn ein Upload endet, während die App nicht vorn ist
|
||||||
|
- 📄 `.gitignore`-Vorlagen im Repo, 📦 Release-Assistent für den Updater
|
||||||
|
- 🎨 Aufgeräumte Toolbar mit eigener Repo-Leiste, neues Tray-Menü mit Hover-Markierung
|
||||||
|
- ✨ Startanimation wie bei AeroMC: das Logo dreht sich, danach baut sich das Fenster von der Mitte aus auf (abschaltbar, beim Autostart im Tray nie)
|
||||||
|
- 🧪 `npm run test:ui`: Oberflächen-Testlauf
|
||||||
|
|
||||||
|
### 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).
|
Dieses Projekt ist Open-Source unter der [GPL Lizenz](LICENSE).
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert');
|
||||||
|
const http = require('node:http');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const os = require('node:os');
|
||||||
|
const path = require('node:path');
|
||||||
|
|
||||||
|
require('../src/git/gitBinary.js').useBundledGit();
|
||||||
|
const { registerFeatureIpc } = require('../src/features/featuresIpc.js');
|
||||||
|
|
||||||
|
// Drei Repos; das ZIP von "langsam" kommt in kleinen Stücken über ~3 s, damit ein Abbruch mitten hineinfällt
|
||||||
|
function fakeGitea() {
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
const u = new URL(req.url, 'http://x');
|
||||||
|
const send = (code, obj) => { res.statusCode = code; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(obj)); };
|
||||||
|
if (u.pathname === '/api/v1/user/repos') {
|
||||||
|
if (u.searchParams.get('page') !== '1') return send(200, []);
|
||||||
|
const port = server.address().port;
|
||||||
|
return send(200, ['eins', 'langsam', 'drei'].map(name => ({
|
||||||
|
name, owner: { login: 'M_Viper' }, default_branch: 'main', empty: false,
|
||||||
|
clone_url: `http://127.0.0.1:${port}/M_Viper/${name}.git`
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
const m = u.pathname.match(/^\/api\/v1\/repos\/M_Viper\/(\w+)\/archive\/main\.zip$/);
|
||||||
|
if (m) {
|
||||||
|
const total = 64 * 1024;
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/zip', 'Content-Length': String(total) });
|
||||||
|
if (m[1] !== 'langsam') return res.end(Buffer.alloc(total, 1));
|
||||||
|
let sent = 0;
|
||||||
|
const t = setInterval(() => {
|
||||||
|
if (res.destroyed) return clearInterval(t);
|
||||||
|
res.write(Buffer.alloc(2048, 1));
|
||||||
|
sent += 2048;
|
||||||
|
if (sent >= total) { clearInterval(t); res.end(); }
|
||||||
|
}, 100);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
send(404, { message: 'not found' });
|
||||||
|
});
|
||||||
|
return new Promise(r => server.listen(0, '127.0.0.1', () => r({ server, url: `http://127.0.0.1:${server.address().port}` })));
|
||||||
|
}
|
||||||
|
|
||||||
|
function setup(url) {
|
||||||
|
const handlers = {};
|
||||||
|
const events = [];
|
||||||
|
registerFeatureIpc({
|
||||||
|
ipcMain: { handle: (name, fn) => { handlers[name] = fn; } },
|
||||||
|
app: { getPath: () => fs.mkdtempSync(path.join(os.tmpdir(), 'gm-ud-')) },
|
||||||
|
readCredentials: () => ({ giteaURL: url, giteaToken: 't' }),
|
||||||
|
buildItemsFromPaths: async () => ({ items: [], skipped: [], ignored: [] }),
|
||||||
|
clearGitTransportBlock: () => false,
|
||||||
|
broadcast: (channel, p) => { if (channel === 'repo-backup-progress') events.push(p); }
|
||||||
|
});
|
||||||
|
return { call: (name, data) => handlers[name]({ sender: { send() {} } }, data), events };
|
||||||
|
}
|
||||||
|
|
||||||
|
test('Backup meldet Fortschritt: Phasen, aktuelles Repo, Bytes und Ergebnis je Repo', async () => {
|
||||||
|
const g = await fakeGitea();
|
||||||
|
try {
|
||||||
|
const { call, events } = setup(g.url);
|
||||||
|
const target = fs.mkdtempSync(path.join(os.tmpdir(), 'gm-bk-'));
|
||||||
|
await call('repo-backup-set-config', { targetDir: target });
|
||||||
|
const r = await call('repo-backup-run-now', {});
|
||||||
|
assert.strictEqual(r.summary.ok, 3, JSON.stringify(r));
|
||||||
|
|
||||||
|
const phases = [...new Set(events.map(e => e.phase))];
|
||||||
|
assert.deepStrictEqual(phases, ['start', 'list', 'running', 'done']);
|
||||||
|
assert.deepStrictEqual(events.find(e => e.phase === 'running').repos, ['M_Viper/eins', 'M_Viper/langsam', 'M_Viper/drei']);
|
||||||
|
assert.ok(events.some(e => e.repo === 'M_Viper/langsam' && e.bytes > 0 && e.bytesTotal === 64 * 1024), 'Download-Fortschritt fehlt');
|
||||||
|
const last = events[events.length - 1];
|
||||||
|
assert.strictEqual(last.results.length, 3);
|
||||||
|
assert.ok(last.results.every(x => x.ok && x.method === 'ZIP'));
|
||||||
|
assert.ok(!events.some(e => 'abort' in e), 'interne Felder dürfen nicht nach außen gehen');
|
||||||
|
|
||||||
|
// Während kein Backup läuft, gibt es auch keinen Zwischenstand
|
||||||
|
const state = await call('repo-backup-get-config', {});
|
||||||
|
assert.strictEqual(state.running, false);
|
||||||
|
assert.strictEqual(state.progress, null);
|
||||||
|
} finally {
|
||||||
|
g.server.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Abbrechen beendet den laufenden ZIP-Download sofort und überspringt den Rest', async () => {
|
||||||
|
const g = await fakeGitea();
|
||||||
|
try {
|
||||||
|
const { call, events } = setup(g.url);
|
||||||
|
const target = fs.mkdtempSync(path.join(os.tmpdir(), 'gm-bk-'));
|
||||||
|
await call('repo-backup-set-config', { targetDir: target });
|
||||||
|
const started = Date.now();
|
||||||
|
const running = call('repo-backup-run-now', {});
|
||||||
|
|
||||||
|
// warten, bis der langsame Download läuft
|
||||||
|
while (!events.some(e => e.repo === 'M_Viper/langsam' && e.bytes > 0)) {
|
||||||
|
if (Date.now() - started > 10000) throw new Error('Download startete nicht');
|
||||||
|
await new Promise(r => setTimeout(r, 50));
|
||||||
|
}
|
||||||
|
const state = await call('repo-backup-get-config', {});
|
||||||
|
assert.strictEqual(state.running, true);
|
||||||
|
assert.strictEqual(state.progress.repo, 'M_Viper/langsam');
|
||||||
|
|
||||||
|
assert.strictEqual((await call('repo-backup-cancel', {})).ok, true);
|
||||||
|
const r = await running;
|
||||||
|
assert.ok(Date.now() - started < 2500, 'Abbruch muss den Download sofort beenden');
|
||||||
|
assert.strictEqual(r.ok, false);
|
||||||
|
assert.strictEqual(r.summary.cancelled, true);
|
||||||
|
assert.strictEqual(r.summary.ok, 1);
|
||||||
|
assert.strictEqual(r.summary.skipped, 2);
|
||||||
|
// keine halbe Datei zurücklassen
|
||||||
|
const archiv = path.join(target, 'M_Viper', 'langsam-archiv');
|
||||||
|
assert.deepStrictEqual(fs.existsSync(archiv) ? fs.readdirSync(archiv) : [], []);
|
||||||
|
assert.strictEqual((await call('repo-backup-cancel', {})).ok, false);
|
||||||
|
} finally {
|
||||||
|
g.server.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert');
|
||||||
|
const http = require('node:http');
|
||||||
|
const { deleteGiteaPathsBatched } = require('../src/git/apiHandler.js');
|
||||||
|
|
||||||
|
// Minimaler Gitea-Nachbau: Repo-Info, Trees-API (seitenweise) und Mehrfach-Commit-Endpunkt.
|
||||||
|
function fakeGitea(paths, { perPage = 1000, failPath = null } = {}) {
|
||||||
|
const files = new Map(paths.map((p, i) => [p, `sha${i}`]));
|
||||||
|
const commits = [];
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
const u = new URL(req.url, 'http://x');
|
||||||
|
const send = (code, obj) => { res.statusCode = code; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(obj)); };
|
||||||
|
if (req.method === 'GET' && u.pathname === '/api/v1/repos/o/r') return send(200, { default_branch: 'main' });
|
||||||
|
if (req.method === 'GET' && u.pathname === '/api/v1/repos/o/r/git/trees/main') {
|
||||||
|
const page = Number(u.searchParams.get('page') || 1);
|
||||||
|
const all = [...files.keys()].map(p => ({ path: p, type: 'blob', sha: files.get(p) }));
|
||||||
|
const slice = all.slice((page - 1) * perPage, page * perPage);
|
||||||
|
return send(200, { tree: slice, truncated: page * perPage < all.length, total_count: all.length, page });
|
||||||
|
}
|
||||||
|
if (req.method === 'POST' && u.pathname === '/api/v1/repos/o/r/contents') {
|
||||||
|
let body = '';
|
||||||
|
req.on('data', c => body += c);
|
||||||
|
req.on('end', () => {
|
||||||
|
const { files: ops } = JSON.parse(body);
|
||||||
|
if (failPath && ops.some(o => o.path === failPath)) return send(422, { message: 'kaputt' });
|
||||||
|
for (const o of ops) {
|
||||||
|
assert.strictEqual(o.operation, 'delete');
|
||||||
|
assert.strictEqual(o.sha, files.get(o.path));
|
||||||
|
files.delete(o.path);
|
||||||
|
}
|
||||||
|
commits.push(ops.length);
|
||||||
|
send(201, {});
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
send(404, { message: 'object does not exist' });
|
||||||
|
});
|
||||||
|
return new Promise(r => server.listen(0, '127.0.0.1', () => r({ server, files, commits, url: `http://127.0.0.1:${server.address().port}` })));
|
||||||
|
}
|
||||||
|
|
||||||
|
test('Ordner wird paketweise gelöscht, Nachbarn bleiben', async () => {
|
||||||
|
const paths = [...Array.from({ length: 2500 }, (_, i) => `mobile/build/f${i}.dex`), 'mobile/build.gradle.kts', 'mobile/src/A.kt'];
|
||||||
|
const g = await fakeGitea(paths, { perPage: 1000 });
|
||||||
|
try {
|
||||||
|
const r = await deleteGiteaPathsBatched({ token: 't', url: g.url, owner: 'o', repo: 'r', prefix: 'mobile/build' });
|
||||||
|
assert.strictEqual(r.deleted, 2500);
|
||||||
|
assert.deepStrictEqual(g.commits, Array(25).fill(100));
|
||||||
|
assert.deepStrictEqual([...g.files.keys()].sort(), ['mobile/build.gradle.kts', 'mobile/src/A.kt']);
|
||||||
|
} finally { g.server.close(); }
|
||||||
|
});
|
||||||
|
|
||||||
|
test('eine fehlerhafte Datei blockiert nicht das ganze Paket', async () => {
|
||||||
|
const paths = Array.from({ length: 10 }, (_, i) => `x/f${i}`);
|
||||||
|
const g = await fakeGitea(paths, { failPath: 'x/f3' });
|
||||||
|
try {
|
||||||
|
const r = await deleteGiteaPathsBatched({ token: 't', url: g.url, owner: 'o', repo: 'r', paths });
|
||||||
|
assert.strictEqual(r.deleted, 9);
|
||||||
|
assert.deepStrictEqual(r.failed.map(f => f.path), ['x/f3']);
|
||||||
|
} finally { g.server.close(); }
|
||||||
|
});
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert');
|
||||||
|
const http = require('node:http');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const os = require('node:os');
|
||||||
|
const path = require('node:path');
|
||||||
|
const { spawnSync } = require('node:child_process');
|
||||||
|
|
||||||
|
require('../src/git/gitBinary.js').useBundledGit();
|
||||||
|
const { registerFeatureIpc, parsePorcelainZ } = require('../src/features/featuresIpc.js');
|
||||||
|
const { scanForSecrets, checkName } = require('../src/features/secretScan.js');
|
||||||
|
|
||||||
|
// Minimaler Gitea-Nachbau für Diagnose, PRs/Issues und Backup
|
||||||
|
function fakeGitea() {
|
||||||
|
const calls = [];
|
||||||
|
const comments = [];
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
const u = new URL(req.url, 'http://x');
|
||||||
|
calls.push(`${req.method} ${u.pathname}`);
|
||||||
|
const send = (code, obj) => { res.statusCode = code; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(obj)); };
|
||||||
|
let body = '';
|
||||||
|
req.on('data', c => body += c);
|
||||||
|
req.on('end', () => {
|
||||||
|
const p = u.pathname;
|
||||||
|
if (p === '/api/v1/version') return send(200, { version: '1.24.0' });
|
||||||
|
if (p === '/api/v1/user') return send(200, { login: 'M_Viper' });
|
||||||
|
if (p === '/api/v1/user/repos') {
|
||||||
|
if (u.searchParams.get('page') && u.searchParams.get('page') !== '1') return send(200, []);
|
||||||
|
return send(200, [{ name: 'deskhub', owner: { login: 'M_Viper' }, default_branch: 'main', empty: false,
|
||||||
|
clone_url: `http://127.0.0.1:${server.address().port}/M_Viper/deskhub.git` }]);
|
||||||
|
}
|
||||||
|
if (p === '/api/v1/repos/M_Viper/deskhub/pulls') return send(200, [{ number: 3, title: 'Fix', state: 'open', user: { login: 'a' }, labels: [], head: { label: 'fix' }, base: { label: 'main' }, mergeable: true }]);
|
||||||
|
if (p === '/api/v1/repos/M_Viper/deskhub/issues' && req.method === 'GET') return send(200, [{ number: 5, title: 'Bug', state: 'open', labels: [{ name: 'bug', color: 'ff0000' }] }]);
|
||||||
|
if (p === '/api/v1/repos/M_Viper/deskhub/issues' && req.method === 'POST') return send(201, { number: 6, ...JSON.parse(body), state: 'open', labels: [] });
|
||||||
|
if (p === '/api/v1/repos/M_Viper/deskhub/issues/5/comments' && req.method === 'POST') { comments.push(JSON.parse(body).body); return send(201, {}); }
|
||||||
|
if (p === '/api/v1/repos/M_Viper/deskhub/issues/5/comments') return send(200, comments.map((b, i) => ({ id: i, body: b, user: { login: 'M_Viper' } })));
|
||||||
|
if (p === '/api/v1/repos/M_Viper/deskhub/issues/5') return send(200, { number: 5, title: 'Bug', state: 'open', labels: [] });
|
||||||
|
if (p === '/api/v1/repos/M_Viper/deskhub/pulls/3/merge') return send(200, {});
|
||||||
|
if (p === '/api/v1/repos/M_Viper/deskhub/archive/main.zip') {
|
||||||
|
res.statusCode = 200; res.setHeader('Content-Type', 'application/zip'); return res.end(Buffer.from('PK\u0003\u0004fake'));
|
||||||
|
}
|
||||||
|
send(404, { message: 'not found' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return new Promise(r => server.listen(0, '127.0.0.1', () => r({ server, calls, comments, url: `http://127.0.0.1:${server.address().port}` })));
|
||||||
|
}
|
||||||
|
|
||||||
|
function setup(url) {
|
||||||
|
const handlers = {};
|
||||||
|
const userData = fs.mkdtempSync(path.join(os.tmpdir(), 'gm-ud-'));
|
||||||
|
registerFeatureIpc({
|
||||||
|
ipcMain: { handle: (name, fn) => { handlers[name] = fn; } },
|
||||||
|
app: { getPath: () => userData },
|
||||||
|
readCredentials: () => ({ giteaURL: url, giteaToken: 'geheim123' }),
|
||||||
|
buildItemsFromPaths: async (paths) => ({
|
||||||
|
items: paths.map(p => ({ localFile: p, targetPath: path.basename(p) })), skipped: [], ignored: []
|
||||||
|
}),
|
||||||
|
clearGitTransportBlock: () => false
|
||||||
|
});
|
||||||
|
const call = (name, data) => handlers[name]({ sender: { send() {} } }, data);
|
||||||
|
return { call, userData };
|
||||||
|
}
|
||||||
|
|
||||||
|
test('Geheimnisse werden an Name und Inhalt erkannt, Vorlagen nicht', async () => {
|
||||||
|
assert.ok(checkName('google-services.json'));
|
||||||
|
assert.ok(checkName('.env'));
|
||||||
|
assert.ok(checkName('release.keystore'));
|
||||||
|
assert.strictEqual(checkName('.env.example'), null);
|
||||||
|
assert.strictEqual(checkName('README.md'), null);
|
||||||
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gm-sec-'));
|
||||||
|
const bot = path.join(dir, 'bot.js');
|
||||||
|
fs.writeFileSync(bot, 'const TOKEN = "123456789:AA' + 'b'.repeat(33) + '";');
|
||||||
|
const clean = path.join(dir, 'app.js');
|
||||||
|
fs.writeFileSync(clean, 'console.log("hallo")');
|
||||||
|
const found = await scanForSecrets([{ localFile: bot, size: fs.statSync(bot).size }, { localFile: clean, size: fs.statSync(clean).size }]);
|
||||||
|
assert.match(found.get(bot), /Telegram/);
|
||||||
|
assert.strictEqual(found.has(clean), false);
|
||||||
|
fs.rmSync(dir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('git status -z wird korrekt gelesen (inkl. Umbenennung)', () => {
|
||||||
|
const files = parsePorcelainZ(' M src/a.js\0?? neu.txt\0R neu/name.js\0alt/name.js\0D weg.txt\0');
|
||||||
|
assert.deepStrictEqual(files.map(f => [f.code, f.path, f.from]), [['M', 'src/a.js', null], ['?', 'neu.txt', null], ['R', 'neu/name.js', 'alt/name.js'], ['D', 'weg.txt', null]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Lokale Änderungen: Status, Diff und Commit nur der ausgewählten Dateien', async () => {
|
||||||
|
const { call } = setup('http://127.0.0.1:1');
|
||||||
|
const repo = fs.mkdtempSync(path.join(os.tmpdir(), 'gm-local-'));
|
||||||
|
const git = (...a) => spawnSync('git', ['-C', repo, '-c', 'user.name=t', '-c', 'user.email=t@t', ...a], { encoding: 'utf8' });
|
||||||
|
git('init', '-q');
|
||||||
|
fs.writeFileSync(path.join(repo, 'a.txt'), 'eins\n');
|
||||||
|
git('add', '.'); git('commit', '-qm', 'start');
|
||||||
|
fs.writeFileSync(path.join(repo, 'a.txt'), 'zwei\n');
|
||||||
|
fs.writeFileSync(path.join(repo, 'b.txt'), 'neu\n');
|
||||||
|
|
||||||
|
const st = await call('local-git', { action: 'status', folder: repo });
|
||||||
|
assert.strictEqual(st.ok, true);
|
||||||
|
assert.deepStrictEqual(st.files.map(f => f.path).sort(), ['a.txt', 'b.txt']);
|
||||||
|
|
||||||
|
const diff = await call('local-git', { action: 'diff', folder: repo, file: 'a.txt', code: 'M' });
|
||||||
|
assert.match(diff.diff, /-eins/);
|
||||||
|
assert.match(diff.diff, /\+zwei/);
|
||||||
|
|
||||||
|
const c = await call('local-git', { action: 'commit', folder: repo, files: ['a.txt'], message: 'nur a' });
|
||||||
|
assert.strictEqual(c.ok, true, c.error);
|
||||||
|
const after = await call('local-git', { action: 'status', folder: repo });
|
||||||
|
assert.deepStrictEqual(after.files.map(f => f.path), ['b.txt']);
|
||||||
|
|
||||||
|
const notRepo = await call('local-git', { action: 'status', folder: os.tmpdir() + path.sep + 'gm-gibtsnicht-' + Date.now() });
|
||||||
|
assert.strictEqual(notRepo.ok, false);
|
||||||
|
fs.rmSync(repo, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Upload-Vorschau markiert Geheimnisse', async () => {
|
||||||
|
const { call } = setup('http://127.0.0.1:1');
|
||||||
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gm-prev-'));
|
||||||
|
const a = path.join(dir, 'main.kt'); fs.writeFileSync(a, 'fun main() {}');
|
||||||
|
const g = path.join(dir, 'google-services.json'); fs.writeFileSync(g, '{}');
|
||||||
|
const r = await call('preview-upload-paths', { paths: [a, g], destPath: '' });
|
||||||
|
assert.strictEqual(r.ok, true);
|
||||||
|
assert.strictEqual(r.secretCount, 1);
|
||||||
|
assert.ok(r.files.find(f => f.localFile === g).secret);
|
||||||
|
fs.rmSync(dir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Diagnose, PRs/Issues und Backup gegen einen nachgebauten Server', async () => {
|
||||||
|
const g = await fakeGitea();
|
||||||
|
const { call } = setup(g.url);
|
||||||
|
try {
|
||||||
|
const d = await call('run-server-diagnosis', {});
|
||||||
|
const byLabel = Object.fromEntries(d.checks.map(c => [c.label, c.status]));
|
||||||
|
assert.strictEqual(byLabel['Gitea-API'], 'ok');
|
||||||
|
assert.strictEqual(byLabel['Token'], 'ok');
|
||||||
|
assert.strictEqual(byLabel['Erreichbar über IPv4'], 'ok');
|
||||||
|
assert.ok(!JSON.stringify(d).includes('geheim123'), 'Token darf nicht in der Diagnose stehen');
|
||||||
|
|
||||||
|
const pulls = await call('gitea-collab', { action: 'listPulls', owner: 'M_Viper', repo: 'deskhub' });
|
||||||
|
assert.strictEqual(pulls.items[0].head, 'fix');
|
||||||
|
const issues = await call('gitea-collab', { action: 'listIssues', owner: 'M_Viper', repo: 'deskhub' });
|
||||||
|
assert.strictEqual(issues.items[0].labels[0].name, 'bug');
|
||||||
|
assert.strictEqual((await call('gitea-collab', { action: 'comment', owner: 'M_Viper', repo: 'deskhub', index: 5, body: 'Danke' })).ok, true);
|
||||||
|
const detail = await call('gitea-collab', { action: 'get', owner: 'M_Viper', repo: 'deskhub', index: 5 });
|
||||||
|
assert.strictEqual(detail.comments[0].body, 'Danke');
|
||||||
|
assert.strictEqual((await call('gitea-collab', { action: 'merge', owner: 'M_Viper', repo: 'deskhub', index: 3, style: 'squash' })).ok, true);
|
||||||
|
assert.strictEqual((await call('gitea-collab', { action: 'createIssue', owner: 'M_Viper', repo: 'deskhub', title: 'Neu' })).item.number, 6);
|
||||||
|
|
||||||
|
const target = fs.mkdtempSync(path.join(os.tmpdir(), 'gm-bk-'));
|
||||||
|
await call('repo-backup-set-config', { targetDir: target, interval: 'daily', keep: 2 });
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
const r = await call('repo-backup-run-now', {});
|
||||||
|
assert.strictEqual(r.summary.ok, 1, JSON.stringify(r));
|
||||||
|
await new Promise(res => setTimeout(res, 61000 / 60)); // eindeutige Zeitstempel
|
||||||
|
}
|
||||||
|
const zips = fs.readdirSync(path.join(target, 'M_Viper', 'deskhub-archiv'));
|
||||||
|
assert.ok(zips.length <= 3 && zips.length >= 1);
|
||||||
|
const state = await call('repo-backup-get-config', {});
|
||||||
|
assert.strictEqual(state.config.lastResult.total, 1);
|
||||||
|
fs.rmSync(target, { recursive: true, force: true });
|
||||||
|
} finally {
|
||||||
|
g.server.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert');
|
||||||
|
const { listTemplates, buildGitignore } = require('../src/features/gitignoreTemplates.js');
|
||||||
|
|
||||||
|
test('listTemplates nennt alle Vorlagen mit Beschriftung', () => {
|
||||||
|
const list = listTemplates();
|
||||||
|
assert.deepStrictEqual(list.map(t => t.key), ['node', 'dotnet', 'electron', 'python', 'java', 'php', 'unity']);
|
||||||
|
assert.ok(list.every(t => t.label));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildGitignore: Vorlagen für Python, Java, PHP und Unity enthalten ihre Kerneinträge', () => {
|
||||||
|
assert.match(buildGitignore(['python']), /__pycache__\//);
|
||||||
|
assert.match(buildGitignore(['java']), /target\//);
|
||||||
|
assert.match(buildGitignore(['php']), /vendor\//);
|
||||||
|
assert.match(buildGitignore(['unity']), /\[Ll\]ibrary\//);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildGitignore enthält immer den allgemeinen Teil', () => {
|
||||||
|
const out = buildGitignore([]);
|
||||||
|
assert.match(out, /\.env/);
|
||||||
|
assert.doesNotMatch(out, /node_modules/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildGitignore fügt gewählte Vorlagen genau einmal hinzu', () => {
|
||||||
|
const out = buildGitignore(['node', 'node', 'electron']);
|
||||||
|
assert.strictEqual(out.split('node_modules/').length - 1, 1);
|
||||||
|
assert.match(out, /dist\//);
|
||||||
|
assert.doesNotMatch(out, /\.vs\//);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildGitignore ignoriert unbekannte Schlüssel', () => {
|
||||||
|
assert.doesNotThrow(() => buildGitignore(['gibt-es-nicht', '__proto__', null]));
|
||||||
|
});
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert');
|
||||||
|
const http = require('node:http');
|
||||||
|
const { bareHostname } = require('../src/utils/helpers.js');
|
||||||
|
const { checkGiteaConnection } = require('../src/git/apiHandler.js');
|
||||||
|
|
||||||
|
function listen(host) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
res.setHeader('Content-Type', 'application/json');
|
||||||
|
res.end(JSON.stringify({ version: '1.22.0' }));
|
||||||
|
});
|
||||||
|
server.once('error', reject);
|
||||||
|
server.listen(0, host, () => resolve(server));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test('bareHostname entfernt Klammern nur bei IPv6-Literalen', () => {
|
||||||
|
assert.strictEqual(bareHostname(new URL('http://[2001:db8::1]:3000').hostname), '2001:db8::1');
|
||||||
|
assert.strictEqual(bareHostname('[::1]'), '::1');
|
||||||
|
assert.strictEqual(bareHostname('127.0.0.1'), '127.0.0.1');
|
||||||
|
assert.strictEqual(bareHostname('git.example.com'), 'git.example.com');
|
||||||
|
assert.strictEqual(bareHostname(undefined), '');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Gitea-Verbindung über IPv4', async () => {
|
||||||
|
const server = await listen('127.0.0.1');
|
||||||
|
try {
|
||||||
|
const r = await checkGiteaConnection({ url: `http://127.0.0.1:${server.address().port}` });
|
||||||
|
assert.strictEqual(r.ok, true);
|
||||||
|
} finally { server.close(); }
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Gitea-Verbindung über IPv6', async (t) => {
|
||||||
|
let server;
|
||||||
|
try { server = await listen('::1'); } catch { return t.skip('IPv6-Loopback nicht verfügbar'); }
|
||||||
|
try {
|
||||||
|
const r = await checkGiteaConnection({ url: `http://[::1]:${server.address().port}` });
|
||||||
|
assert.strictEqual(r.ok, true);
|
||||||
|
} finally { server.close(); }
|
||||||
|
});
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert');
|
||||||
|
const { createPauseGate } = require('../src/features/pauseGate.js');
|
||||||
|
|
||||||
|
const tick = () => new Promise((r) => setTimeout(r, 20));
|
||||||
|
|
||||||
|
test('Ohne Pause läuft wait() sofort durch', async () => {
|
||||||
|
const gate = createPauseGate();
|
||||||
|
await gate.wait();
|
||||||
|
await gate.wait(new AbortController().signal);
|
||||||
|
assert.strictEqual(gate.isPaused(), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Während der Pause bleibt wait() stehen und läuft nach resume() weiter', async () => {
|
||||||
|
const gate = createPauseGate();
|
||||||
|
gate.pause();
|
||||||
|
let done = false;
|
||||||
|
const p = gate.wait().then(() => { done = true; });
|
||||||
|
await tick();
|
||||||
|
assert.strictEqual(done, false);
|
||||||
|
assert.strictEqual(gate.waitingCount(), 1);
|
||||||
|
gate.resume();
|
||||||
|
await p;
|
||||||
|
assert.strictEqual(done, true);
|
||||||
|
assert.strictEqual(gate.waitingCount(), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resume() gibt alle Wartenden frei', async () => {
|
||||||
|
const gate = createPauseGate();
|
||||||
|
gate.pause();
|
||||||
|
const all = Promise.all([gate.wait(), gate.wait(), gate.wait()]);
|
||||||
|
await tick();
|
||||||
|
assert.strictEqual(gate.waitingCount(), 3);
|
||||||
|
gate.resume();
|
||||||
|
await all;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Abbruch während der Pause löst wait() mit dem Abbruchfehler aus', async () => {
|
||||||
|
const gate = createPauseGate();
|
||||||
|
gate.pause();
|
||||||
|
const c = new AbortController();
|
||||||
|
const p = gate.wait(c.signal);
|
||||||
|
await tick();
|
||||||
|
c.abort();
|
||||||
|
await assert.rejects(p, (e) => e.code === 'UPLOAD_CANCELLED');
|
||||||
|
assert.strictEqual(gate.waitingCount(), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Bereits abgebrochenes Signal wirft sofort, auch ohne Pause', async () => {
|
||||||
|
const gate = createPauseGate();
|
||||||
|
const c = new AbortController();
|
||||||
|
c.abort();
|
||||||
|
await assert.rejects(gate.wait(c.signal), (e) => e.code === 'UPLOAD_CANCELLED');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('onChange meldet nur echte Wechsel', () => {
|
||||||
|
const states = [];
|
||||||
|
const gate = createPauseGate({ onChange: (p) => states.push(p) });
|
||||||
|
assert.strictEqual(gate.pause(), true);
|
||||||
|
assert.strictEqual(gate.pause(), false);
|
||||||
|
assert.strictEqual(gate.resume(), true);
|
||||||
|
assert.strictEqual(gate.resume(), false);
|
||||||
|
assert.deepStrictEqual(states, [true, false]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Ein abgebrochener Wartender blockiert die übrigen nicht', async () => {
|
||||||
|
const gate = createPauseGate();
|
||||||
|
gate.pause();
|
||||||
|
const c = new AbortController();
|
||||||
|
const a = gate.wait(c.signal).catch((e) => e.code);
|
||||||
|
const b = gate.wait();
|
||||||
|
await tick();
|
||||||
|
c.abort();
|
||||||
|
assert.strictEqual(await a, 'UPLOAD_CANCELLED');
|
||||||
|
gate.resume();
|
||||||
|
await b;
|
||||||
|
});
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const os = require('node:os');
|
||||||
|
const path = require('node:path');
|
||||||
|
const { spawnSync } = require('node:child_process');
|
||||||
|
|
||||||
|
const { createPrefs, DEFAULTS } = require('../src/features/appPrefs.js');
|
||||||
|
const { useBundledGit, applyGitIdentity, commitIdentity } = require('../src/git/gitBinary.js');
|
||||||
|
const { createRepoBackup, excludeMatcher } = require('../src/backup/GiteaRepoBackup.js');
|
||||||
|
|
||||||
|
const tmp = (p) => fs.mkdtempSync(path.join(os.tmpdir(), p));
|
||||||
|
|
||||||
|
test('Einstellungen: Standardwerte, Bereinigung und ungültige E-Mail', () => {
|
||||||
|
const file = path.join(tmp('gm-prefs-'), 'app-settings.json');
|
||||||
|
const prefs = createPrefs(file);
|
||||||
|
assert.deepStrictEqual(prefs.get(), DEFAULTS);
|
||||||
|
|
||||||
|
const r = prefs.set({ zoom: 400, theme: 'pink', closeAction: 'tray', gitName: 'Max <x>\n', unbekannt: 1, uploadSecretScan: 0 });
|
||||||
|
assert.strictEqual(r.prefs.zoom, 150);
|
||||||
|
assert.strictEqual(r.prefs.theme, 'dark');
|
||||||
|
assert.strictEqual(r.prefs.closeAction, 'tray');
|
||||||
|
assert.strictEqual(r.prefs.gitName, 'Max x');
|
||||||
|
assert.strictEqual(r.prefs.uploadSecretScan, false);
|
||||||
|
assert.ok(!('unbekannt' in r.prefs));
|
||||||
|
|
||||||
|
const bad = prefs.set({ gitEmail: 'keine-mail' });
|
||||||
|
assert.deepStrictEqual(bad.rejected, ['gitEmail']);
|
||||||
|
assert.strictEqual(bad.prefs.gitEmail, '');
|
||||||
|
|
||||||
|
// nach dem Neuladen noch da
|
||||||
|
assert.strictEqual(createPrefs(file).get().closeAction, 'tray');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Git-Identität greift nur als Rückfallwert und verschwindet beim Leeren', () => {
|
||||||
|
useBundledGit();
|
||||||
|
const userData = tmp('gm-ud-');
|
||||||
|
const home = tmp('gm-home-');
|
||||||
|
const env = { ...process.env, HOME: home, USERPROFILE: home };
|
||||||
|
const ident = () => spawnSync('git', ['var', 'GIT_AUTHOR_IDENT'], { encoding: 'utf8', env: { ...env, GIT_CONFIG_SYSTEM: process.env.GIT_CONFIG_SYSTEM || '' } }).stdout;
|
||||||
|
|
||||||
|
assert.strictEqual(applyGitIdentity({ name: 'Test Name', email: 'test@example.com' }, userData), true);
|
||||||
|
assert.match(ident(), /^Test Name <test@example\.com>/);
|
||||||
|
|
||||||
|
// Eine globale Einstellung hat Vorrang
|
||||||
|
fs.writeFileSync(path.join(home, '.gitconfig'), '[user]\n\tname = Global\n\temail = global@example.com\n');
|
||||||
|
assert.match(ident(), /^Global <global@example\.com>/);
|
||||||
|
|
||||||
|
applyGitIdentity({ name: '', email: '' }, userData);
|
||||||
|
assert.strictEqual(process.env.GIT_CONFIG_SYSTEM, undefined);
|
||||||
|
assert.ok(!fs.existsSync(path.join(userData, 'git-system.gitconfig')));
|
||||||
|
assert.deepStrictEqual(commitIdentity({ gitName: '', gitEmail: '' }), { name: 'Git Manager GUI', email: 'gui@gitmanager.local' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Backup: Ausschlussmuster und Zeitpunkt für automatische Backups', () => {
|
||||||
|
const m = excludeMatcher('test-*\nM_Viper/Alt?\n# Kommentar\nmy.repo');
|
||||||
|
assert.strictEqual(m('M_Viper/test-1'), true);
|
||||||
|
assert.strictEqual(m('M_Viper/Alt1'), true);
|
||||||
|
assert.strictEqual(m('M_Viper/Alt12'), false);
|
||||||
|
assert.strictEqual(m('x/my.repo'), true);
|
||||||
|
assert.strictEqual(m('x/myXrepo'), false);
|
||||||
|
|
||||||
|
const configPath = path.join(tmp('gm-bk-'), 'repo-backup.json');
|
||||||
|
const backup = createRepoBackup({ configPath, getContext() {}, giteaCall() {}, runGit() {}, isPackFailure() {}, redact: (s) => s });
|
||||||
|
backup.setConfig({ targetDir: 'X:/irgendwo', interval: 'daily', time: '03:00' });
|
||||||
|
const at = (s) => new Date(s);
|
||||||
|
// noch nie gelaufen: vor 03:00 nicht, danach schon
|
||||||
|
assert.strictEqual(backup.isDue(at('2026-10-22T02:59:00')), true, 'Slot von gestern 03:00 ist offen');
|
||||||
|
fs.writeFileSync(configPath, JSON.stringify({ ...JSON.parse(fs.readFileSync(configPath, 'utf8')), lastRun: '2026-10-22T03:05:00' }));
|
||||||
|
assert.strictEqual(backup.isDue(at('2026-10-22T20:00:00')), false, 'heute schon gelaufen');
|
||||||
|
assert.strictEqual(backup.isDue(at('2026-10-23T02:59:00')), false, 'vor 03:00 noch nicht');
|
||||||
|
assert.strictEqual(backup.isDue(at('2026-10-23T03:00:00')), true, 'ab 03:00 wieder fällig');
|
||||||
|
|
||||||
|
backup.setConfig({ time: '25:99' });
|
||||||
|
assert.strictEqual(backup.getState().config.time, '');
|
||||||
|
});
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert');
|
||||||
|
const {
|
||||||
|
parseVersionFromFileName, compareVersions, parseSidecar, checkNewTag, buildReleaseBody
|
||||||
|
} = require('../src/features/releaseAssist.js');
|
||||||
|
|
||||||
|
const HASH = '5ca7f0c680b6d73a92eadb5e90f8164f7f3fa52071ffaac54803b0e5ec95f35d';
|
||||||
|
|
||||||
|
test('Version aus dem Dateinamen', () => {
|
||||||
|
assert.strictEqual(parseVersionFromFileName('Git-Manager-GUI-Setup-2.2.1.exe'), '2.2.1');
|
||||||
|
assert.strictEqual(parseVersionFromFileName('Git Manager GUI Setup 2.1.10.exe'), '2.1.10');
|
||||||
|
assert.strictEqual(parseVersionFromFileName('setup.exe'), null);
|
||||||
|
assert.strictEqual(parseVersionFromFileName(null), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Versionen werden numerisch verglichen, nicht als Text', () => {
|
||||||
|
assert.strictEqual(compareVersions('2.2.1', '2.2.0'), 1);
|
||||||
|
assert.strictEqual(compareVersions('v2.10.0', 'v2.9.9'), 1);
|
||||||
|
assert.strictEqual(compareVersions('2.2.1', 'v2.2.1'), 0);
|
||||||
|
assert.strictEqual(compareVersions('2.1.9', '2.2.0'), -1);
|
||||||
|
assert.deepStrictEqual(['v2.10.0', 'v2.9.0', 'v2.2.1'].sort(compareVersions), ['v2.2.1', 'v2.9.0', 'v2.10.0']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Sidecar-Datei: Hash wird gefunden, auch mit Großbuchstaben', () => {
|
||||||
|
assert.strictEqual(parseSidecar(`${HASH} Git-Manager-GUI-Setup-2.2.1.exe\n`), HASH);
|
||||||
|
assert.strictEqual(parseSidecar(HASH.toUpperCase()), HASH);
|
||||||
|
assert.strictEqual(parseSidecar('kein Hash'), null);
|
||||||
|
assert.strictEqual(parseSidecar('abc123'), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Neuer Tag: Format, Duplikat und Reihenfolge werden geprüft', () => {
|
||||||
|
const releases = [{ tag_name: 'v2.2.0' }, { tag_name: 'v2.1.9' }];
|
||||||
|
assert.strictEqual(checkNewTag('v2.2.1', releases).ok, true);
|
||||||
|
assert.strictEqual(checkNewTag('2.2.1', releases).ok, true);
|
||||||
|
assert.strictEqual(checkNewTag('v2.10.0', releases).ok, true);
|
||||||
|
|
||||||
|
assert.match(checkNewTag('release-2', releases).reason, /Version wie v2\.2\.1/);
|
||||||
|
assert.match(checkNewTag('v2.2.0', releases).reason, /gibt es schon/);
|
||||||
|
assert.match(checkNewTag('V2.2.0', releases).reason, /gibt es schon/);
|
||||||
|
assert.match(checkNewTag('v2.1.5', releases).reason, /nicht neuer/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Neuer Tag: ohne vorhandene Releases ist jede Version erlaubt', () => {
|
||||||
|
assert.strictEqual(checkNewTag('v1.0.0', []).ok, true);
|
||||||
|
assert.strictEqual(checkNewTag('v1.0.0', null).ok, true);
|
||||||
|
assert.strictEqual(checkNewTag('v1.0.0', [{ tag_name: 'nightly' }]).ok, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Release-Text: Prüfsummen im Format, das der Updater liest', () => {
|
||||||
|
const body = buildReleaseBody('## Neu\n\n- Abbrechen\n', [{ name: 'Git-Manager-GUI-Setup-2.2.1.exe', sha256: HASH.toUpperCase() }]);
|
||||||
|
assert.match(body, /^## Neu/);
|
||||||
|
assert.ok(body.includes(`- Git-Manager-GUI-Setup-2.2.1.exe: ${HASH}`));
|
||||||
|
|
||||||
|
// genau so liest updater.js die Zeile
|
||||||
|
const line = body.split('\n').find((l) => l.toLowerCase().includes('git-manager-gui-setup-2.2.1.exe') && /\b[a-f0-9]{64}\b/i.test(l));
|
||||||
|
assert.ok(line);
|
||||||
|
assert.strictEqual(line.match(/\b([a-fA-F0-9]{64})\b/)[1], HASH);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Release-Text: ungültige Prüfsummen und leere Notizen', () => {
|
||||||
|
assert.strictEqual(buildReleaseBody('Nur Text', [{ name: 'a.exe', sha256: 'zu kurz' }]), 'Nur Text');
|
||||||
|
assert.strictEqual(buildReleaseBody('', []), '');
|
||||||
|
assert.ok(buildReleaseBody('', [{ name: 'a.exe', sha256: HASH }]).startsWith('### Prüfsummen'));
|
||||||
|
});
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert');
|
||||||
|
const http = require('node:http');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const os = require('node:os');
|
||||||
|
const path = require('node:path');
|
||||||
|
const { spawnSync } = require('node:child_process');
|
||||||
|
const archiver = require('archiver');
|
||||||
|
|
||||||
|
require('../src/git/gitBinary.js').useBundledGit();
|
||||||
|
const { registerFeatureIpc } = require('../src/features/featuresIpc.js');
|
||||||
|
|
||||||
|
const git = (cwd, ...a) => spawnSync('git', ['-c', 'user.name=t', '-c', 'user.email=t@t', ...a], { cwd, encoding: 'utf8' });
|
||||||
|
const tmp = (p) => fs.mkdtempSync(path.join(os.tmpdir(), p));
|
||||||
|
|
||||||
|
function makeZip(file, entries) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const out = fs.createWriteStream(file);
|
||||||
|
const zip = archiver('zip');
|
||||||
|
out.on('close', resolve);
|
||||||
|
zip.on('error', reject);
|
||||||
|
zip.pipe(out);
|
||||||
|
for (const [name, content] of entries) zip.append(content, { name });
|
||||||
|
zip.finalize();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backup-Ordner wie von GiteaRepoBackup angelegt: Spiegel + ZIP-Stände
|
||||||
|
async function makeBackupDir() {
|
||||||
|
const work = tmp('gm-src-');
|
||||||
|
git(work, 'init', '-q', '-b', 'main');
|
||||||
|
fs.writeFileSync(path.join(work, 'a.txt'), 'hallo');
|
||||||
|
git(work, 'add', '.'); git(work, 'commit', '-qm', 'eins');
|
||||||
|
git(work, 'branch', 'feature'); git(work, 'tag', 'v1.0');
|
||||||
|
const dir = tmp('gm-bk-');
|
||||||
|
fs.mkdirSync(path.join(dir, 'M_Viper'));
|
||||||
|
git(dir, 'clone', '-q', '--mirror', work, path.join(dir, 'M_Viper', 'deskhub.git'));
|
||||||
|
const zipDir = path.join(dir, 'M_Viper', 'deskhub-archiv');
|
||||||
|
fs.mkdirSync(zipDir);
|
||||||
|
await makeZip(path.join(zipDir, 'deskhub-2026-10-01_10-00.zip'), [['deskhub/a.txt', 'alt'], ['deskhub/src/b.kt', 'fun b() {}']]);
|
||||||
|
await makeZip(path.join(zipDir, 'deskhub-2026-10-20_10-00.zip'), [['deskhub/a.txt', 'neu'], ['deskhub/src/b.kt', 'fun b() {}']]);
|
||||||
|
fs.utimesSync(path.join(zipDir, 'deskhub-2026-10-01_10-00.zip'), new Date('2026-10-01'), new Date('2026-10-01'));
|
||||||
|
fs.writeFileSync(path.join(dir, 'backup-protokoll.json'), '{}');
|
||||||
|
return { dir, work };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gitea-Nachbau: neue Repos zeigen auf echte lokale Bare-Repos, damit der Push wirklich läuft
|
||||||
|
function fakeGitea() {
|
||||||
|
const created = [];
|
||||||
|
const patches = [];
|
||||||
|
const repoRoot = tmp('gm-remote-');
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
const u = new URL(req.url, 'http://x');
|
||||||
|
const send = (code, obj) => { res.statusCode = code; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(obj)); };
|
||||||
|
let body = '';
|
||||||
|
req.on('data', c => body += c);
|
||||||
|
req.on('end', () => {
|
||||||
|
if (u.pathname === '/api/v1/user') return send(200, { login: 'M_Viper' });
|
||||||
|
if (u.pathname === '/api/v1/user/repos' && req.method === 'POST') {
|
||||||
|
const b = JSON.parse(body);
|
||||||
|
if (b.name === 'deskhub') return send(409, { message: 'repository already exists' });
|
||||||
|
const bare = path.join(repoRoot, `${b.name}.git`);
|
||||||
|
spawnSync('git', ['init', '-q', '--bare', '-b', 'main', bare]);
|
||||||
|
created.push(b);
|
||||||
|
return send(201, { name: b.name, full_name: `M_Viper/${b.name}`, owner: { login: 'M_Viper' }, clone_url: bare, html_url: `http://gitea/M_Viper/${b.name}`, default_branch: 'main' });
|
||||||
|
}
|
||||||
|
if (u.pathname.startsWith('/api/v1/repos/M_Viper/') && req.method === 'PATCH') { patches.push(JSON.parse(body)); return send(200, {}); }
|
||||||
|
send(404, { message: 'not found' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return new Promise(r => server.listen(0, '127.0.0.1', () => r({ server, created, patches, repoRoot, url: `http://127.0.0.1:${server.address().port}` })));
|
||||||
|
}
|
||||||
|
|
||||||
|
function setup(url) {
|
||||||
|
const handlers = {};
|
||||||
|
const userData = tmp('gm-ud-');
|
||||||
|
registerFeatureIpc({
|
||||||
|
ipcMain: { handle: (name, fn) => { handlers[name] = fn; } },
|
||||||
|
app: { getPath: () => userData },
|
||||||
|
readCredentials: () => ({ giteaURL: url, giteaToken: 'geheim123' }),
|
||||||
|
buildItemsFromPaths: async () => ({ items: [], skipped: [], ignored: [] }),
|
||||||
|
clearGitTransportBlock: () => false
|
||||||
|
});
|
||||||
|
return (name, data) => handlers[name]({ sender: { send() {} } }, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('Sicherungen werden gefunden: Spiegel und ZIP-Stände, neuester zuerst', async () => {
|
||||||
|
const { dir } = await makeBackupDir();
|
||||||
|
const call = setup('http://127.0.0.1:1');
|
||||||
|
const r = await call('repo-backup-list', { dir });
|
||||||
|
assert.strictEqual(r.ok, true, r.error);
|
||||||
|
assert.strictEqual(r.backups.length, 1);
|
||||||
|
const b = r.backups[0];
|
||||||
|
assert.deepStrictEqual([b.owner, b.repo], ['M_Viper', 'deskhub']);
|
||||||
|
assert.ok(b.mirror);
|
||||||
|
assert.deepStrictEqual(b.zips.map(z => z.file), ['deskhub-2026-10-20_10-00.zip', 'deskhub-2026-10-01_10-00.zip']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Lokal: Spiegel wird zur Arbeitskopie mit allen Branches, ZIP ohne Oberordner entpackt', async () => {
|
||||||
|
const { dir } = await makeBackupDir();
|
||||||
|
const call = setup('http://127.0.0.1:1');
|
||||||
|
const { backups: [b] } = await call('repo-backup-list', { dir });
|
||||||
|
|
||||||
|
const dest1 = tmp('gm-dst-');
|
||||||
|
const m = await call('repo-restore-local', { kind: 'mirror', source: b.mirror.path, destParent: dest1, repo: 'deskhub' });
|
||||||
|
assert.strictEqual(m.ok, true, m.error);
|
||||||
|
assert.strictEqual(fs.readFileSync(path.join(dest1, 'deskhub', 'a.txt'), 'utf8'), 'hallo');
|
||||||
|
assert.match(git(path.join(dest1, 'deskhub'), 'branch', '-r').stdout, /feature/);
|
||||||
|
|
||||||
|
// Zweiter Versuch in denselben Ordner darf nichts überschreiben
|
||||||
|
const again = await call('repo-restore-local', { kind: 'mirror', source: b.mirror.path, destParent: dest1, repo: 'deskhub' });
|
||||||
|
assert.strictEqual(again.ok, false);
|
||||||
|
assert.match(again.error, /existiert bereits/);
|
||||||
|
|
||||||
|
const dest2 = tmp('gm-dst-');
|
||||||
|
const z = await call('repo-restore-local', { kind: 'zip', source: b.zips[0].path, destParent: dest2, repo: 'deskhub' });
|
||||||
|
assert.strictEqual(z.ok, true, z.error);
|
||||||
|
assert.strictEqual(z.files, 2);
|
||||||
|
assert.strictEqual(fs.readFileSync(path.join(dest2, 'deskhub', 'a.txt'), 'utf8'), 'neu');
|
||||||
|
assert.ok(fs.existsSync(path.join(dest2, 'deskhub', 'src', 'b.kt')));
|
||||||
|
});
|
||||||
|
|
||||||
|
// ZIP von Hand bauen: archiver entschärft "../" schon beim Erzeugen, ein echter Angreifer nicht.
|
||||||
|
function rawZip(file, entries) {
|
||||||
|
const zlib = require('node:zlib');
|
||||||
|
const locals = [];
|
||||||
|
const centrals = [];
|
||||||
|
let offset = 0;
|
||||||
|
for (const [name, content] of entries) {
|
||||||
|
const n = Buffer.from(name);
|
||||||
|
const data = Buffer.from(content);
|
||||||
|
const crc = zlib.crc32(data);
|
||||||
|
const lh = Buffer.alloc(30);
|
||||||
|
lh.writeUInt32LE(0x04034b50, 0); lh.writeUInt16LE(20, 4); lh.writeUInt32LE(crc, 14);
|
||||||
|
lh.writeUInt32LE(data.length, 18); lh.writeUInt32LE(data.length, 22); lh.writeUInt16LE(n.length, 26);
|
||||||
|
const ch = Buffer.alloc(46);
|
||||||
|
ch.writeUInt32LE(0x02014b50, 0); ch.writeUInt16LE(20, 4); ch.writeUInt16LE(20, 6); ch.writeUInt32LE(crc, 16);
|
||||||
|
ch.writeUInt32LE(data.length, 20); ch.writeUInt32LE(data.length, 24); ch.writeUInt16LE(n.length, 28); ch.writeUInt32LE(offset, 42);
|
||||||
|
locals.push(lh, n, data);
|
||||||
|
centrals.push(ch, n);
|
||||||
|
offset += lh.length + n.length + data.length;
|
||||||
|
}
|
||||||
|
const cd = Buffer.concat(centrals);
|
||||||
|
const end = Buffer.alloc(22);
|
||||||
|
end.writeUInt32LE(0x06054b50, 0); end.writeUInt16LE(entries.length, 8); end.writeUInt16LE(entries.length, 10);
|
||||||
|
end.writeUInt32LE(cd.length, 12); end.writeUInt32LE(offset, 16);
|
||||||
|
fs.writeFileSync(file, Buffer.concat([...locals, cd, end]));
|
||||||
|
}
|
||||||
|
|
||||||
|
test('Archive mit Pfaden außerhalb des Ziels werden abgewiesen', async () => {
|
||||||
|
const d = tmp('gm-evil-');
|
||||||
|
const zip = path.join(d, 'evil.zip');
|
||||||
|
rawZip(zip, [['deskhub/ok.txt', 'ok'], ['deskhub/../../boese.txt', 'x']]);
|
||||||
|
const unzipper = require('unzipper');
|
||||||
|
const names = (await unzipper.Open.file(zip)).files.map(f => f.path);
|
||||||
|
assert.ok(names.includes('deskhub/../../boese.txt'), 'Testarchiv muss den gefährlichen Pfad wirklich enthalten');
|
||||||
|
const call = setup('http://127.0.0.1:1');
|
||||||
|
const destParent = tmp('gm-dst-');
|
||||||
|
const r = await call('repo-restore-local', { kind: 'zip', source: zip, destParent, repo: 'x' });
|
||||||
|
assert.strictEqual(r.ok, false);
|
||||||
|
assert.match(r.error, /Unsicherer Pfad/);
|
||||||
|
assert.ok(!fs.existsSync(path.join(destParent, 'x', 'ok.txt')), 'auch harmlose Dateien dürfen dann nicht entpackt werden');
|
||||||
|
assert.ok(!fs.existsSync(path.join(destParent, 'boese.txt')));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Gitea: Spiegel und ZIP als neues Repo, Namenskonflikt und ZIP-Überschreiben werden abgefangen', async () => {
|
||||||
|
const { dir } = await makeBackupDir();
|
||||||
|
const g = await fakeGitea();
|
||||||
|
try {
|
||||||
|
const call = setup(g.url);
|
||||||
|
const { backups: [b] } = await call('repo-backup-list', { dir });
|
||||||
|
|
||||||
|
const m = await call('repo-restore-gitea', { kind: 'mirror', source: b.mirror.path, owner: 'M_Viper', repo: 'deskhub', mode: 'new', newName: 'deskhub-wieder', isPrivate: true });
|
||||||
|
assert.strictEqual(m.ok, true, m.error);
|
||||||
|
const bare = path.join(g.repoRoot, 'deskhub-wieder.git');
|
||||||
|
const refs = git(bare, 'for-each-ref', '--format=%(refname)').stdout;
|
||||||
|
assert.match(refs, /refs\/heads\/main/);
|
||||||
|
assert.match(refs, /refs\/heads\/feature/);
|
||||||
|
assert.match(refs, /refs\/tags\/v1\.0/);
|
||||||
|
assert.strictEqual(g.created[0].private, true);
|
||||||
|
|
||||||
|
const z = await call('repo-restore-gitea', { kind: 'zip', source: b.zips[0].path, owner: 'M_Viper', repo: 'deskhub', mode: 'new', newName: 'deskhub-zip' });
|
||||||
|
assert.strictEqual(z.ok, true, z.error);
|
||||||
|
assert.strictEqual(z.method, 'Git');
|
||||||
|
const files = git(path.join(g.repoRoot, 'deskhub-zip.git'), 'ls-tree', '-r', '--name-only', 'main').stdout.trim().split('\n');
|
||||||
|
assert.deepStrictEqual(files.sort(), ['a.txt', 'src/b.kt']);
|
||||||
|
|
||||||
|
const conflict = await call('repo-restore-gitea', { kind: 'mirror', source: b.mirror.path, owner: 'M_Viper', repo: 'deskhub', mode: 'new', newName: 'deskhub' });
|
||||||
|
assert.strictEqual(conflict.ok, false);
|
||||||
|
assert.match(conflict.error, /existiert bereits/);
|
||||||
|
|
||||||
|
const zipOverwrite = await call('repo-restore-gitea', { kind: 'zip', source: b.zips[0].path, owner: 'M_Viper', repo: 'deskhub', mode: 'overwrite' });
|
||||||
|
assert.strictEqual(zipOverwrite.ok, false);
|
||||||
|
assert.match(zipOverwrite.error, /nur mit einem Git-Spiegel/);
|
||||||
|
|
||||||
|
const badName = await call('repo-restore-gitea', { kind: 'zip', source: b.zips[0].path, owner: 'M_Viper', repo: 'deskhub', mode: 'new', newName: 'mit leerzeichen' });
|
||||||
|
assert.strictEqual(badName.ok, false);
|
||||||
|
} finally {
|
||||||
|
g.server.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert');
|
||||||
|
const { remainingMs, centeredBounds, MIN_SPIN_MS } = require('../src/features/splash.js');
|
||||||
|
|
||||||
|
test('Startanimation: wurde das Logo nie gezeigt, gibt es keine Wartezeit', () => {
|
||||||
|
assert.strictEqual(remainingMs(0, 5000, 1600), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Startanimation: das Logo dreht sich mindestens minMs', () => {
|
||||||
|
assert.strictEqual(remainingMs(1000, 1300, 1600), 1300);
|
||||||
|
assert.strictEqual(remainingMs(1000, 1000, 1600), 1600);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Startanimation: nach Ablauf von minMs wird nicht mehr gewartet', () => {
|
||||||
|
assert.strictEqual(remainingMs(1000, 2600, 1600), 0);
|
||||||
|
assert.strictEqual(remainingMs(1000, 9000, 1600), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Startanimation: Mindestdauer ist wie bei AeroMC 1,6 s', () => {
|
||||||
|
assert.strictEqual(MIN_SPIN_MS, 1600);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Startanimation: Fenster wird im Arbeitsbereich zentriert', () => {
|
||||||
|
const b = centeredBounds({ x: 0, y: 0, width: 1920, height: 1040 }, 1200, 820);
|
||||||
|
assert.deepStrictEqual(b, { x: 360, y: 110, width: 1200, height: 820 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Startanimation: Arbeitsbereich mit Versatz und mehrere Monitore', () => {
|
||||||
|
const b = centeredBounds({ x: -1920, y: 40, width: 1920, height: 1000 }, 1200, 820);
|
||||||
|
assert.deepStrictEqual(b, { x: -1560, y: 130, width: 1200, height: 820 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Startanimation: auf kleinen Bildschirmen wird das Fenster begrenzt', () => {
|
||||||
|
const b = centeredBounds({ x: 0, y: 0, width: 1024, height: 700 }, 1200, 820);
|
||||||
|
assert.deepStrictEqual(b, { x: 0, y: 0, width: 1024, height: 700 });
|
||||||
|
});
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert');
|
||||||
|
const { describeTransfer } = require('../src/features/transferNotice.js');
|
||||||
|
|
||||||
|
test('Erfolg nennt Repo und Dateizahl', () => {
|
||||||
|
assert.deepStrictEqual(describeTransfer({ status: 'ok', repo: 'a/b', files: 3 }), { title: 'Upload fertig', body: 'a/b: 3 Dateien hochgeladen.' });
|
||||||
|
assert.match(describeTransfer({ status: 'ok', repo: 'a/b', files: 1 }).body, /1 Datei hochgeladen/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Erfolg ohne bekannte Dateizahl bleibt lesbar', () => {
|
||||||
|
assert.strictEqual(describeTransfer({ status: 'ok', repo: 'a/b', files: null }).body, 'a/b: Upload hochgeladen.');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Abgebrochene Uploads melden nichts', () => {
|
||||||
|
assert.strictEqual(describeTransfer({ status: 'abgebrochen', repo: 'a/b' }), null);
|
||||||
|
assert.strictEqual(describeTransfer(null), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Fehler zeigt fehlgeschlagene Dateien oder den Fehlertext', () => {
|
||||||
|
assert.strictEqual(describeTransfer({ status: 'fehler', repo: 'a/b', failed: 2 }).title, 'Upload fehlgeschlagen');
|
||||||
|
assert.match(describeTransfer({ status: 'fehler', repo: 'a/b', failed: 2 }).body, /2 Dateien fehlgeschlagen/);
|
||||||
|
assert.match(describeTransfer({ status: 'fehler', repo: 'a/b', error: 'Server nicht erreichbar' }).body, /Server nicht erreichbar/);
|
||||||
|
assert.match(describeTransfer({ status: 'fehler', repo: '' }).body, /Unbekannter Fehler/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Langer Fehlertext wird gekürzt', () => {
|
||||||
|
assert.ok(describeTransfer({ status: 'fehler', repo: 'a/b', error: 'x'.repeat(500) }).body.length < 200);
|
||||||
|
});
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert');
|
||||||
|
const { buildTrayTemplate, trayTooltip } = require('../src/features/trayMenu.js');
|
||||||
|
|
||||||
|
const noop = () => {};
|
||||||
|
const base = { version: '2.2.1', onOpen: noop, onHistory: noop, onCancel: noop, onAutostart: noop, onQuit: noop };
|
||||||
|
const labels = (tpl) => tpl.filter(i => i.label).map(i => i.label);
|
||||||
|
const find = (tpl, label) => tpl.find(i => i.label === label);
|
||||||
|
|
||||||
|
test('Tooltip nennt Version und zeigt eine laufende Übertragung', () => {
|
||||||
|
assert.strictEqual(trayTooltip('2.2.1', 0), 'Git Manager Explorer Pro v2.2.1');
|
||||||
|
assert.match(trayTooltip('2.2.1', 2), /Übertragung läuft/);
|
||||||
|
assert.match(trayTooltip('2.2.1', 1, true), /Übertragung pausiert/);
|
||||||
|
assert.strictEqual(trayTooltip('2.2.1', 0, true), 'Git Manager Explorer Pro v2.2.1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Menü im Ruhezustand: Kopfzeile, Status, Aktionen; Abbrechen gesperrt', () => {
|
||||||
|
const tpl = buildTrayTemplate(base);
|
||||||
|
assert.deepStrictEqual(labels(tpl), [
|
||||||
|
'Git Manager Explorer Pro v2.2.1',
|
||||||
|
'Keine laufende Übertragung',
|
||||||
|
'Fenster öffnen',
|
||||||
|
'Upload-Verlauf',
|
||||||
|
'Übertragung pausieren',
|
||||||
|
'Laufende Übertragung abbrechen',
|
||||||
|
'Mit Windows starten',
|
||||||
|
'Beenden'
|
||||||
|
]);
|
||||||
|
assert.strictEqual(find(tpl, 'Laufende Übertragung abbrechen').enabled, false);
|
||||||
|
assert.strictEqual(tpl[0].enabled, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Menü bei laufender Übertragung: Zähler, Abbrechen freigeschaltet', () => {
|
||||||
|
const one = buildTrayTemplate({ ...base, active: 1 });
|
||||||
|
assert.ok(labels(one).includes('1 Übertragung läuft'));
|
||||||
|
assert.strictEqual(find(one, 'Laufende Übertragung abbrechen').enabled, true);
|
||||||
|
assert.ok(labels(buildTrayTemplate({ ...base, active: 3 })).includes('3 Übertragungen laufen'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Retry-Queue erscheint nur, wenn etwas wartet', () => {
|
||||||
|
assert.ok(!labels(buildTrayTemplate(base)).some(l => l.startsWith('Retry-Queue')));
|
||||||
|
assert.ok(labels(buildTrayTemplate({ ...base, queued: 4 })).includes('Retry-Queue: 4 wartend'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Autostart ist ein Häkchen und spiegelt den Zustand', () => {
|
||||||
|
assert.strictEqual(find(buildTrayTemplate({ ...base, autostart: true }), 'Mit Windows starten').checked, true);
|
||||||
|
const off = find(buildTrayTemplate(base), 'Mit Windows starten');
|
||||||
|
assert.strictEqual(off.type, 'checkbox');
|
||||||
|
assert.strictEqual(off.checked, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Einträge rufen die übergebenen Aktionen auf', () => {
|
||||||
|
const calls = [];
|
||||||
|
const tpl = buildTrayTemplate({
|
||||||
|
...base,
|
||||||
|
active: 1,
|
||||||
|
onOpen: () => calls.push('open'),
|
||||||
|
onHistory: () => calls.push('history'),
|
||||||
|
onCancel: () => calls.push('cancel'),
|
||||||
|
onQuit: () => calls.push('quit')
|
||||||
|
});
|
||||||
|
for (const l of ['Fenster öffnen', 'Upload-Verlauf', 'Laufende Übertragung abbrechen', 'Beenden']) find(tpl, l).click();
|
||||||
|
assert.deepStrictEqual(calls, ['open', 'history', 'cancel', 'quit']);
|
||||||
|
});
|
||||||
|
|
||||||
|
const { serializeTemplate, runAction } = require('../src/features/trayMenu.js');
|
||||||
|
const { computePosition } = require('../src/features/trayPopup.js');
|
||||||
|
|
||||||
|
test('serializeTemplate: nur Daten, keine Funktionen, Trenner bleiben erhalten', () => {
|
||||||
|
const plain = serializeTemplate(buildTrayTemplate({ ...base, active: 1, autostart: true }));
|
||||||
|
assert.ok(plain.every(i => Object.values(i).every(v => typeof v !== 'function')));
|
||||||
|
assert.strictEqual(plain.filter(i => i.type === 'separator').length, 3);
|
||||||
|
const cancel = plain.find(i => i.id === 'cancel');
|
||||||
|
assert.deepStrictEqual(cancel, { id: 'cancel', label: 'Laufende Übertragung abbrechen', type: 'normal', enabled: true, checked: false });
|
||||||
|
assert.strictEqual(plain.find(i => i.id === 'autostart').checked, true);
|
||||||
|
assert.doesNotThrow(() => JSON.stringify(plain));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('runAction: führt Aktionen per id aus, Häkchen wird umgeschaltet', () => {
|
||||||
|
const calls = [];
|
||||||
|
const tpl = buildTrayTemplate({
|
||||||
|
...base,
|
||||||
|
active: 1,
|
||||||
|
autostart: false,
|
||||||
|
onOpen: () => calls.push('open'),
|
||||||
|
onAutostart: (item) => calls.push(`autostart:${item.checked}`)
|
||||||
|
});
|
||||||
|
assert.strictEqual(runAction(tpl, 'open'), true);
|
||||||
|
assert.strictEqual(runAction(tpl, 'autostart'), true);
|
||||||
|
assert.deepStrictEqual(calls, ['open', 'autostart:true']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('runAction: gesperrte oder unbekannte Einträge tun nichts', () => {
|
||||||
|
let cancelled = false;
|
||||||
|
const tpl = buildTrayTemplate({ ...base, active: 0, onCancel: () => { cancelled = true; } });
|
||||||
|
assert.strictEqual(runAction(tpl, 'cancel'), false);
|
||||||
|
assert.strictEqual(runAction(tpl, 'gibt-es-nicht'), false);
|
||||||
|
assert.strictEqual(runAction(tpl, 'header'), false);
|
||||||
|
assert.strictEqual(cancelled, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('computePosition: Menü bleibt im sichtbaren Bereich', () => {
|
||||||
|
const wa = { x: 0, y: 0, width: 1920, height: 1040 };
|
||||||
|
// Zeiger in der Taskleiste unten rechts: Menü sitzt direkt über der Taskleiste, nicht außerhalb rechts
|
||||||
|
const bottomRight = computePosition({ x: 1900, y: 1060 }, wa, 286, 300);
|
||||||
|
assert.strictEqual(bottomRight.y, 1040 - 300 - 8);
|
||||||
|
assert.ok(bottomRight.x + 286 <= 1920 - 8);
|
||||||
|
// Zeiger weit links: nicht links aus dem Bildschirm
|
||||||
|
assert.ok(computePosition({ x: 2, y: 1060 }, wa, 286, 300).x >= 8);
|
||||||
|
// Taskleiste oben
|
||||||
|
assert.strictEqual(computePosition({ x: 500, y: 10 }, { ...wa, y: 40, height: 1000 }, 286, 300).y, 48);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Pause: Eintrag wechselt zwischen pausieren und fortsetzen, nur bei laufender Übertragung wählbar', () => {
|
||||||
|
const idle = buildTrayTemplate(base);
|
||||||
|
assert.strictEqual(find(idle, 'Übertragung pausieren').enabled, false);
|
||||||
|
|
||||||
|
const running = buildTrayTemplate({ ...base, active: 1 });
|
||||||
|
assert.strictEqual(find(running, 'Übertragung pausieren').enabled, true);
|
||||||
|
|
||||||
|
const paused = buildTrayTemplate({ ...base, active: 2, paused: true });
|
||||||
|
assert.ok(labels(paused).includes('Übertragung fortsetzen'));
|
||||||
|
assert.ok(labels(paused).includes('2 Übertragungen pausiert'));
|
||||||
|
assert.ok(!labels(paused).includes('Übertragung pausieren'));
|
||||||
|
assert.ok(labels(buildTrayTemplate({ ...base, active: 1, paused: true })).includes('1 Übertragung pausiert'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Pause-Eintrag ruft onPause auf', () => {
|
||||||
|
const calls = [];
|
||||||
|
const tpl = buildTrayTemplate({ ...base, active: 1, onPause: () => calls.push('pause') });
|
||||||
|
assert.strictEqual(runAction(tpl, 'pause'), true);
|
||||||
|
assert.deepStrictEqual(calls, ['pause']);
|
||||||
|
});
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert');
|
||||||
|
const { blockedReason, partitionBlocked } = require('../src/features/uploadBlocklist.js');
|
||||||
|
|
||||||
|
test('Sperre: Dateien mit Zugangsdaten werden erkannt', () => {
|
||||||
|
for (const name of ['credentials.json', 'data/credentials.json', 'C:\\proj\\.env', '.env.production', 'id_rsa', 'release.keystore', 'server.pem', 'secrets.json', '.npmrc']) {
|
||||||
|
assert.ok(blockedReason(name, {}), `${name} sollte gesperrt sein`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Sperre: normale Dateien und Vorlagen bleiben erlaubt', () => {
|
||||||
|
for (const name of ['main.js', 'package.json', 'README.md', '.env.example', 'config.sample.json', 'id_rsa.pub', 'credentials.md', 'src/credentials/index.js']) {
|
||||||
|
assert.strictEqual(blockedReason(name, {}), null, `${name} darf hochgeladen werden`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Sperre: Einstellung kann sie abschalten, Standard ist an', () => {
|
||||||
|
assert.ok(blockedReason('credentials.json', undefined));
|
||||||
|
assert.ok(blockedReason('credentials.json', { uploadBlockSecrets: true }));
|
||||||
|
assert.strictEqual(blockedReason('credentials.json', { uploadBlockSecrets: false }), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('partitionBlocked: trennt Einträge und behält die Reihenfolge', () => {
|
||||||
|
const items = [
|
||||||
|
{ localFile: 'a/main.js', targetPath: 'a/main.js' },
|
||||||
|
{ localFile: 'a/data/credentials.json', targetPath: 'a/data/credentials.json' },
|
||||||
|
{ localFile: 'a/.env', targetPath: 'a/.env' },
|
||||||
|
{ localFile: 'a/README.md', targetPath: 'a/README.md' }
|
||||||
|
];
|
||||||
|
const { allowed, blocked } = partitionBlocked(items, {});
|
||||||
|
assert.deepStrictEqual(allowed.map(i => i.localFile), ['a/main.js', 'a/README.md']);
|
||||||
|
assert.deepStrictEqual(blocked.map(b => b.path), ['a/data/credentials.json', 'a/.env']);
|
||||||
|
assert.ok(blocked.every(b => b.reason));
|
||||||
|
assert.strictEqual(partitionBlocked(items, { uploadBlockSecrets: false }).blocked.length, 0);
|
||||||
|
});
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert');
|
||||||
|
const http = require('node:http');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const os = require('node:os');
|
||||||
|
const path = require('node:path');
|
||||||
|
const {
|
||||||
|
uploadItemsViaGiteaApi,
|
||||||
|
createUploadCancelledError,
|
||||||
|
isUploadCancelledError,
|
||||||
|
throwIfUploadCancelled
|
||||||
|
} = require('../src/git/apiHandler.js');
|
||||||
|
|
||||||
|
test('Abbruchfehler: Erkennung und throwIfUploadCancelled', () => {
|
||||||
|
assert.strictEqual(isUploadCancelledError(createUploadCancelledError()), true);
|
||||||
|
assert.strictEqual(isUploadCancelledError(new Error('anderer Fehler')), false);
|
||||||
|
assert.strictEqual(isUploadCancelledError(null), false);
|
||||||
|
|
||||||
|
assert.doesNotThrow(() => throwIfUploadCancelled(undefined));
|
||||||
|
assert.doesNotThrow(() => throwIfUploadCancelled(new AbortController().signal));
|
||||||
|
const c = new AbortController();
|
||||||
|
c.abort();
|
||||||
|
assert.throws(() => throwIfUploadCancelled(c.signal), (e) => isUploadCancelledError(e));
|
||||||
|
});
|
||||||
|
|
||||||
|
function makeTempFile() {
|
||||||
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gm-cancel-'));
|
||||||
|
const file = path.join(dir, 'a.txt');
|
||||||
|
fs.writeFileSync(file, 'hallo');
|
||||||
|
return { dir, file };
|
||||||
|
}
|
||||||
|
|
||||||
|
test('API-Upload: bereits abgebrochenes Signal sendet nichts', async () => {
|
||||||
|
const { dir, file } = makeTempFile();
|
||||||
|
try {
|
||||||
|
const c = new AbortController();
|
||||||
|
c.abort();
|
||||||
|
await assert.rejects(
|
||||||
|
uploadItemsViaGiteaApi({
|
||||||
|
token: 't', url: 'http://127.0.0.1:1', owner: 'o', repo: 'r', branch: 'main',
|
||||||
|
items: [{ localFile: file, targetPath: 'a.txt' }], message: 'm', signal: c.signal
|
||||||
|
}),
|
||||||
|
(e) => isUploadCancelledError(e)
|
||||||
|
);
|
||||||
|
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
|
||||||
|
});
|
||||||
|
|
||||||
|
test('API-Upload: Abbruch während der Übertragung wirft Abbruchfehler und landet nicht in "failed"', async () => {
|
||||||
|
const { dir, file } = makeTempFile();
|
||||||
|
let postSeen = false;
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
if (req.method === 'POST') {
|
||||||
|
postSeen = true;
|
||||||
|
setTimeout(() => { res.writeHead(201, { 'content-type': 'application/json' }); res.end('{}'); }, 3000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.writeHead(200, { 'content-type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ default_branch: 'main', tree: [], entries: [] }));
|
||||||
|
});
|
||||||
|
await new Promise(r => server.listen(0, '127.0.0.1', r));
|
||||||
|
const { port } = server.address();
|
||||||
|
try {
|
||||||
|
const c = new AbortController();
|
||||||
|
const started = Date.now();
|
||||||
|
setTimeout(() => c.abort(), 400);
|
||||||
|
await assert.rejects(
|
||||||
|
uploadItemsViaGiteaApi({
|
||||||
|
token: 't', url: `http://127.0.0.1:${port}`, owner: 'o', repo: 'r', branch: 'main',
|
||||||
|
items: [{ localFile: file, targetPath: 'a.txt' }], message: 'm', signal: c.signal
|
||||||
|
}),
|
||||||
|
(e) => isUploadCancelledError(e)
|
||||||
|
);
|
||||||
|
assert.ok(postSeen, 'Der POST muss gestartet sein, sonst prüft der Test nichts');
|
||||||
|
assert.ok(Date.now() - started < 2500, 'Abbruch muss sofort greifen, nicht erst nach der Serverantwort');
|
||||||
|
} finally {
|
||||||
|
server.closeAllConnections?.();
|
||||||
|
await new Promise(r => server.close(r));
|
||||||
|
fs.rmSync(dir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { createPauseGate } = require('../src/features/pauseGate.js');
|
||||||
|
|
||||||
|
function gitea(onPost) {
|
||||||
|
return http.createServer((req, res) => {
|
||||||
|
if (req.method === 'POST') {
|
||||||
|
onPost();
|
||||||
|
res.writeHead(201, { 'content-type': 'application/json' });
|
||||||
|
res.end('{}');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.writeHead(200, { 'content-type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ default_branch: 'main', tree: [], entries: [] }));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test('API-Upload: während der Pause wird nichts gesendet, nach resume() schon', async () => {
|
||||||
|
const { dir, file } = makeTempFile();
|
||||||
|
let posts = 0;
|
||||||
|
const server = gitea(() => { posts++; });
|
||||||
|
await new Promise(r => server.listen(0, '127.0.0.1', r));
|
||||||
|
const { port } = server.address();
|
||||||
|
const gate = createPauseGate();
|
||||||
|
try {
|
||||||
|
gate.pause();
|
||||||
|
const upload = uploadItemsViaGiteaApi({
|
||||||
|
token: 't', url: `http://127.0.0.1:${port}`, owner: 'o', repo: 'r', branch: 'main',
|
||||||
|
items: [{ localFile: file, targetPath: 'a.txt' }], message: 'm',
|
||||||
|
waitIfPaused: (s) => gate.wait(s)
|
||||||
|
});
|
||||||
|
await new Promise(r => setTimeout(r, 700));
|
||||||
|
assert.strictEqual(posts, 0, 'Während der Pause darf kein Paket gesendet werden');
|
||||||
|
gate.resume();
|
||||||
|
const result = await upload;
|
||||||
|
assert.strictEqual(posts, 1);
|
||||||
|
assert.deepStrictEqual(result.uploaded, ['a.txt']);
|
||||||
|
} finally {
|
||||||
|
await new Promise(r => server.close(r));
|
||||||
|
fs.rmSync(dir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('API-Upload: Abbruch während der Pause beendet den Upload sofort', async () => {
|
||||||
|
const { dir, file } = makeTempFile();
|
||||||
|
let posts = 0;
|
||||||
|
const server = gitea(() => { posts++; });
|
||||||
|
await new Promise(r => server.listen(0, '127.0.0.1', r));
|
||||||
|
const { port } = server.address();
|
||||||
|
const gate = createPauseGate();
|
||||||
|
try {
|
||||||
|
gate.pause();
|
||||||
|
const c = new AbortController();
|
||||||
|
const upload = uploadItemsViaGiteaApi({
|
||||||
|
token: 't', url: `http://127.0.0.1:${port}`, owner: 'o', repo: 'r', branch: 'main',
|
||||||
|
items: [{ localFile: file, targetPath: 'a.txt' }], message: 'm',
|
||||||
|
signal: c.signal, waitIfPaused: (s) => gate.wait(s)
|
||||||
|
});
|
||||||
|
setTimeout(() => c.abort(), 400);
|
||||||
|
await assert.rejects(upload, (e) => isUploadCancelledError(e));
|
||||||
|
assert.strictEqual(posts, 0);
|
||||||
|
assert.strictEqual(gate.waitingCount(), 0);
|
||||||
|
} finally {
|
||||||
|
await new Promise(r => server.close(r));
|
||||||
|
fs.rmSync(dir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert');
|
||||||
|
const { buildEntry, filterEntries, createUploadHistory } = require('../src/features/uploadHistory.js');
|
||||||
|
|
||||||
|
function memoryFs(initial) {
|
||||||
|
const files = new Map(initial ? [['h.json', initial]] : []);
|
||||||
|
return {
|
||||||
|
files,
|
||||||
|
readFileSync: (f) => { if (!files.has(f)) throw new Error('ENOENT'); return files.get(f); },
|
||||||
|
writeFileSync: (f, c) => { files.set(f, c); }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test('buildEntry: erfolgreicher Upload', () => {
|
||||||
|
const e = buildEntry('Drag & Drop', { owner: 'a', repo: 'b' }, { ok: true, results: [{}, {}, {}] }, 1000, null, 4000);
|
||||||
|
assert.strictEqual(e.status, 'ok');
|
||||||
|
assert.strictEqual(e.repo, 'a/b');
|
||||||
|
assert.strictEqual(e.files, 3);
|
||||||
|
assert.strictEqual(e.durationMs, 3000);
|
||||||
|
assert.strictEqual(e.error, '');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildEntry: abgebrochen ist kein Fehler und trägt keinen Fehlertext', () => {
|
||||||
|
const e = buildEntry('Ordner', { owner: 'a', repo: 'b' }, { ok: false, cancelled: true, error: 'Upload abgebrochen' }, 0, null, 10);
|
||||||
|
assert.strictEqual(e.status, 'abgebrochen');
|
||||||
|
assert.strictEqual(e.error, '');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildEntry: Fehler und geworfene Ausnahmen', () => {
|
||||||
|
const failed = buildEntry('Dateien', { owner: 'a', repo: 'b' }, { ok: false, error: 'kaputt', failedCount: 2 }, 0, null, 10);
|
||||||
|
assert.strictEqual(failed.status, 'fehler');
|
||||||
|
assert.strictEqual(failed.error, 'kaputt');
|
||||||
|
assert.strictEqual(failed.failed, 2);
|
||||||
|
|
||||||
|
const thrown = buildEntry('Dateien', {}, null, 0, new Error('boom'), 10);
|
||||||
|
assert.strictEqual(thrown.status, 'fehler');
|
||||||
|
assert.strictEqual(thrown.error, 'boom');
|
||||||
|
assert.strictEqual(thrown.repo, '');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildEntry: Einzeldatei zählt als eine Datei, lange Fehler werden gekürzt', () => {
|
||||||
|
assert.strictEqual(buildEntry('x', {}, { ok: true, singleFile: true }, 0, null, 1).files, 1);
|
||||||
|
assert.strictEqual(buildEntry('x', {}, { ok: false, error: 'e'.repeat(1000) }, 0, null, 1).error.length, 300);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('filterEntries: nach Status und Repo-Suche', () => {
|
||||||
|
const list = [
|
||||||
|
{ repo: 'a/alpha', status: 'ok' },
|
||||||
|
{ repo: 'a/beta', status: 'fehler' },
|
||||||
|
{ repo: 'b/Alpha-2', status: 'abgebrochen' }
|
||||||
|
];
|
||||||
|
assert.strictEqual(filterEntries(list).length, 3);
|
||||||
|
assert.deepStrictEqual(filterEntries(list, { status: 'fehler' }).map(e => e.repo), ['a/beta']);
|
||||||
|
assert.deepStrictEqual(filterEntries(list, { query: 'ALPHA' }).map(e => e.repo), ['a/alpha', 'b/Alpha-2']);
|
||||||
|
assert.strictEqual(filterEntries(list, { status: 'ok', query: 'beta' }).length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Verlauf: neueste zuerst, auf max Einträge begrenzt', () => {
|
||||||
|
const history = createUploadHistory({ file: 'h.json', max: 3, fs: memoryFs() });
|
||||||
|
for (let i = 1; i <= 5; i++) history.record({ n: i });
|
||||||
|
assert.deepStrictEqual(history.read().map(e => e.n), [5, 4, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Verlauf: kaputte oder fehlende Datei ergibt eine leere Liste', () => {
|
||||||
|
assert.deepStrictEqual(createUploadHistory({ file: 'h.json', fs: memoryFs() }).read(), []);
|
||||||
|
assert.deepStrictEqual(createUploadHistory({ file: 'h.json', fs: memoryFs('{kaputt') }).read(), []);
|
||||||
|
assert.deepStrictEqual(createUploadHistory({ file: 'h.json', fs: memoryFs('{"a":1}') }).read(), []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Verlauf: leeren', () => {
|
||||||
|
const history = createUploadHistory({ file: 'h.json', fs: memoryFs() });
|
||||||
|
history.record({ n: 1 });
|
||||||
|
history.clear();
|
||||||
|
assert.deepStrictEqual(history.read(), []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Verlauf: Schreibfehler werfen nicht', () => {
|
||||||
|
const broken = { readFileSync: () => '[]', writeFileSync: () => { throw new Error('Platte voll'); } };
|
||||||
|
assert.strictEqual(createUploadHistory({ file: 'h.json', fs: broken }).record({ n: 1 }), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
const { toCsv, formatDate } = require('../src/features/uploadHistory.js');
|
||||||
|
|
||||||
|
test('formatDate: TT.MM.JJJJ HH:MM:SS, ungültige Eingabe ergibt leeren Text', () => {
|
||||||
|
assert.match(formatDate('2026-10-22T07:11:53.080Z'), /^\d{2}\.\d{2}\.2026 \d{2}:\d{2}:\d{2}$/);
|
||||||
|
assert.strictEqual(formatDate('kein Datum'), '');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('toCsv: BOM, Semikolon, Windows-Zeilenenden, Kopfzeile', () => {
|
||||||
|
const csv = toCsv([{ at: '2026-10-22T07:11:53.080Z', repo: 'a/b', kind: 'Drag & Drop', files: 3, failed: 0, status: 'ok', durationMs: 2500, error: '' }]);
|
||||||
|
assert.strictEqual(csv.charCodeAt(0), 0xFEFF);
|
||||||
|
const lines = csv.slice(1).split('\r\n');
|
||||||
|
assert.strictEqual(lines[0], 'Datum;Repo;Art;Dateien;Fehlgeschlagen;Ergebnis;Dauer (s);Fehler');
|
||||||
|
assert.match(lines[1], /;a\/b;Drag & Drop;3;0;Erfolgreich;2\.5;$/);
|
||||||
|
assert.strictEqual(lines[2], '');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('toCsv: Anführungszeichen, Semikolon und Zeilenumbrüche werden maskiert', () => {
|
||||||
|
const csv = toCsv([{ at: '2026-10-22T07:11:53Z', repo: 'a/b', kind: 'x', files: null, failed: 1, status: 'fehler', durationMs: 100, error: 'sagt "nein"; und\nmehr' }]);
|
||||||
|
assert.ok(csv.includes('"sagt ""nein""; und\nmehr"'));
|
||||||
|
assert.match(csv, /;Fehler;0\.1;/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('toCsv: Excel-Formeln werden entschärft', () => {
|
||||||
|
const csv = toCsv([{ at: '2026-10-22T07:11:53Z', repo: '=cmd|calc', kind: '+x', files: 1, failed: 0, status: 'ok', durationMs: 0, error: '@SUMME(A1)' }]);
|
||||||
|
assert.ok(csv.includes(";'=cmd|calc;"));
|
||||||
|
assert.ok(csv.includes(";'+x;"));
|
||||||
|
assert.ok(csv.includes(";'@SUMME(A1)"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('toCsv: leere Liste liefert nur die Kopfzeile', () => {
|
||||||
|
assert.strictEqual(toCsv([]).slice(1).split('\r\n').filter(Boolean).length, 1);
|
||||||
|
});
|
||||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 55 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
@@ -0,0 +1,19 @@
|
|||||||
|
<Application x:Class="GitManagerSetup.App"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
<Application.Resources>
|
||||||
|
<ResourceDictionary>
|
||||||
|
<ResourceDictionary.MergedDictionaries>
|
||||||
|
<!-- [0] wird je nach Hell/Dunkel ersetzt -->
|
||||||
|
<ResourceDictionary Source="Themes/Colors.Light.xaml"/>
|
||||||
|
<ResourceDictionary Source="Themes/Controls.xaml"/>
|
||||||
|
</ResourceDictionary.MergedDictionaries>
|
||||||
|
<!-- Akzent wie in der App (Cyan → Blau) -->
|
||||||
|
<SolidColorBrush x:Key="AccentBrush" Color="#2E7FE0"/>
|
||||||
|
<SolidColorBrush x:Key="AccentHoverBrush" Color="#4590EA"/>
|
||||||
|
<SolidColorBrush x:Key="OnAccentBrush" Color="White"/>
|
||||||
|
<SolidColorBrush x:Key="AccentSubtleBrush" Color="#222E7FE0"/>
|
||||||
|
<SolidColorBrush x:Key="AccentSubtleStrokeBrush" Color="#552E7FE0"/>
|
||||||
|
</ResourceDictionary>
|
||||||
|
</Application.Resources>
|
||||||
|
</Application>
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using Microsoft.Win32;
|
||||||
|
|
||||||
|
namespace GitManagerSetup;
|
||||||
|
|
||||||
|
public partial class App : Application
|
||||||
|
{
|
||||||
|
private static Mutex? _single;
|
||||||
|
|
||||||
|
protected override void OnStartup(StartupEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnStartup(e);
|
||||||
|
bool has(string arg) => e.Args.Any(a => a.Equals(arg, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
// Das Deinstallationsprogramm ist das Setup ohne angehängte App → ohne Anhang immer Deinstallation
|
||||||
|
bool uninstall = has("--uninstall") || !Payload.Exists;
|
||||||
|
bool silent = has("/S");
|
||||||
|
bool update = has("--update");
|
||||||
|
bool launch = has("/launch");
|
||||||
|
var dirArg = e.Args.FirstOrDefault(a => a.StartsWith("/D=", StringComparison.OrdinalIgnoreCase))?.Substring(3);
|
||||||
|
|
||||||
|
_single = new Mutex(true, "GitManagerGUI-Setup", out bool first);
|
||||||
|
if (!first)
|
||||||
|
{
|
||||||
|
Shutdown(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stille Installation/Deinstallation für Skripte:
|
||||||
|
// Git-Manager-GUI-Setup.exe /S [/D=Ordner] [/launch] bzw. --uninstall /S
|
||||||
|
if (silent)
|
||||||
|
{
|
||||||
|
var progress = new Progress<(double, string)>(_ => { });
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (uninstall) Installer.Uninstall(removeData: false, progress);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var dir = dirArg ?? Installer.InstalledDir ?? Installer.DefaultDir;
|
||||||
|
Installer.Install(dir, desktopLink: Installer.InstalledDir is null || Installer.DesktopLinkExists,
|
||||||
|
progress, waitForExit: true);
|
||||||
|
if (launch) System.Diagnostics.Process.Start(Path.Combine(dir, Installer.ExeName));
|
||||||
|
}
|
||||||
|
Shutdown(0);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// ohne Oberfläche: Fehler für Skripte nachvollziehbar ablegen
|
||||||
|
try { File.WriteAllText(Path.Combine(Path.GetTempPath(), "Git-Manager-GUI-Setup.log"), $"[{DateTime.Now}] {ex}"); } catch { }
|
||||||
|
Shutdown(1);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplyTheme();
|
||||||
|
var mode = uninstall ? SetupMode.Uninstall : update ? SetupMode.Update : SetupMode.Install;
|
||||||
|
new MainWindow(mode).Show();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnExit(ExitEventArgs e)
|
||||||
|
{
|
||||||
|
try { _single?.ReleaseMutex(); } catch { }
|
||||||
|
base.OnExit(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Hell/Dunkel wie in Windows eingestellt.</summary>
|
||||||
|
private void ApplyTheme()
|
||||||
|
{
|
||||||
|
bool light = ReadDword(@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize", "AppsUseLightTheme") != 0;
|
||||||
|
Resources.MergedDictionaries[0] = new ResourceDictionary
|
||||||
|
{
|
||||||
|
Source = new Uri($"pack://application:,,,/Themes/Colors.{(light ? "Light" : "Dark")}.xaml"),
|
||||||
|
};
|
||||||
|
if (!light)
|
||||||
|
{
|
||||||
|
// im Dunkeln das Cyan der App, damit Text und Fortschrittsbalken gut lesbar bleiben
|
||||||
|
var a = Color.FromRgb(0x4C, 0xB8, 0xF5);
|
||||||
|
Resources["AccentBrush"] = new SolidColorBrush(a);
|
||||||
|
Resources["AccentHoverBrush"] = new SolidColorBrush(Color.FromRgb(0x67, 0xC6, 0xF8));
|
||||||
|
Resources["OnAccentBrush"] = new SolidColorBrush(Color.FromRgb(0x04, 0x10, 0x1F));
|
||||||
|
Resources["AccentSubtleBrush"] = new SolidColorBrush(Color.FromArgb(0x30, a.R, a.G, a.B));
|
||||||
|
Resources["AccentSubtleStrokeBrush"] = new SolidColorBrush(Color.FromArgb(0x60, a.R, a.G, a.B));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ReadDword(string key, string name)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var k = Registry.CurrentUser.OpenSubKey(key);
|
||||||
|
return k?.GetValue(name) is int i ? i : -1;
|
||||||
|
}
|
||||||
|
catch { return -1; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum SetupMode { Install, Update, Uninstall }
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<!-- .NET Framework 4.8 ist in jedem Windows 10/11 enthalten → der Installer läuft ohne Zusatzinstallation -->
|
||||||
|
<TargetFramework>net48</TargetFramework>
|
||||||
|
<UseWPF>true</UseWPF>
|
||||||
|
<UseWindowsForms>true</UseWindowsForms>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||||
|
<Prefer32Bit>false</Prefer32Bit>
|
||||||
|
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||||
|
<AssemblyName>Git-Manager-GUI-Setup</AssemblyName>
|
||||||
|
<RootNamespace>GitManagerSetup</RootNamespace>
|
||||||
|
<ApplicationIcon>..\assets\icon.ico</ApplicationIcon>
|
||||||
|
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||||
|
<Product>Git Manager GUI Setup</Product>
|
||||||
|
<AssemblyTitle>Git Manager GUI Setup</AssemblyTitle>
|
||||||
|
<Description>Installiert Git Manager GUI</Description>
|
||||||
|
<Company>M_Viper</Company>
|
||||||
|
<Authors>M_Viper</Authors>
|
||||||
|
<Copyright>Copyright (c) M_Viper</Copyright>
|
||||||
|
<NeutralLanguage>de-DE</NeutralLanguage>
|
||||||
|
<!-- Version kommt beim Bauen aus package.json (scripts/build-setup.js) -->
|
||||||
|
<Version Condition="'$(Version)' == ''">0.0.0</Version>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<!-- WinForms nur für den Ordner-Dialog; nicht global, sonst Namenskonflikte mit WPF -->
|
||||||
|
<Using Remove="System.Windows.Forms" />
|
||||||
|
<Using Remove="System.Drawing" />
|
||||||
|
<Reference Include="Microsoft.VisualBasic" />
|
||||||
|
<Reference Include="System.IO.Compression" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<!-- Themes\*.xaml bindet das SDK automatisch als Page ein -->
|
||||||
|
<Resource Include="..\assets\icon.ico" Link="app.ico" />
|
||||||
|
<Resource Include="..\assets\icon.png" Link="app.png" />
|
||||||
|
</ItemGroup>
|
||||||
|
<!--
|
||||||
|
Die eigentliche App (entpackter Electron-Ordner als ZIP) wird NICHT hier eingebettet,
|
||||||
|
sondern nach dem Bauen hinten an die .exe angehängt (scripts/build-setup.js, siehe Payload.cs).
|
||||||
|
So muss der Compiler keine ~150 MB als Ressource verarbeiten, und das Deinstallationsprogramm
|
||||||
|
ist einfach der vordere Teil der Datei ohne Anhang.
|
||||||
|
-->
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,501 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.IO.Compression;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Microsoft.Win32;
|
||||||
|
|
||||||
|
namespace GitManagerSetup;
|
||||||
|
|
||||||
|
/// <summary>Installation/Deinstallation – nur für den aktuellen Benutzer, ohne Administratorrechte.</summary>
|
||||||
|
public static class Installer
|
||||||
|
{
|
||||||
|
public const string AppName = "Git Manager GUI";
|
||||||
|
public const string Publisher = "M_Viper";
|
||||||
|
// productName aus package.json → so heißt die EXE im entpackten Electron-Ordner
|
||||||
|
public const string ExeName = "Git Manager GUI.exe";
|
||||||
|
private const string ProcessName = "Git Manager GUI";
|
||||||
|
// Gleicher Name wie beim früheren NSIS-Setup (electron-builder), damit eine alte Installation sauber ersetzt wird
|
||||||
|
public const string UninstallerName = "Uninstall Git Manager GUI.exe";
|
||||||
|
// wie "appId" in package.json – gleiche Taskleisten-/Benachrichtigungs-Zuordnung wie bisher
|
||||||
|
public const string AppUserModelId = "com.viper.gitmanager";
|
||||||
|
|
||||||
|
// Schlüssel vom alten electron-builder-Setup (Version 2.1.7) übernommen → in "Apps" bleibt es ein einziger Eintrag
|
||||||
|
private const string KeyId = "4156cc7f-a3e7-5b1d-9238-0af3e0f54ecd";
|
||||||
|
private const string UninstallKey = @"Software\Microsoft\Windows\CurrentVersion\Uninstall\" + KeyId;
|
||||||
|
private const string LegacyAppKey = @"Software\" + KeyId;
|
||||||
|
private const string RunKey = @"Software\Microsoft\Windows\CurrentVersion\Run";
|
||||||
|
private const string FileListName = "install-files.txt";
|
||||||
|
|
||||||
|
public static string Version
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var info = typeof(Installer).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
|
||||||
|
if (!string.IsNullOrEmpty(info)) return info!.Split('+')[0];
|
||||||
|
var v = typeof(Installer).Assembly.GetName().Version!;
|
||||||
|
return $"{v.Major}.{v.Minor}.{v.Build}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Pfad dieses Setups (bzw. des Deinstallationsprogramms).</summary>
|
||||||
|
public static string SelfPath => Assembly.GetExecutingAssembly().Location;
|
||||||
|
|
||||||
|
public static string DefaultDir =>
|
||||||
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Programs", AppName);
|
||||||
|
|
||||||
|
/// <summary>App-Daten: Zugangsdaten (verschlüsselt), Einstellungen, Favoriten, Verlauf, Backup-Einstellung.</summary>
|
||||||
|
public static string DataDir =>
|
||||||
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "git-manager-gui");
|
||||||
|
|
||||||
|
private static string StartMenuLink =>
|
||||||
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Programs), AppName + ".lnk");
|
||||||
|
|
||||||
|
private static string DesktopLink =>
|
||||||
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), AppName + ".lnk");
|
||||||
|
|
||||||
|
// ---------- vorhandene Installation ----------
|
||||||
|
|
||||||
|
private static string? RegValue(string name)
|
||||||
|
{
|
||||||
|
using var k = Registry.CurrentUser.OpenSubKey(UninstallKey);
|
||||||
|
return k?.GetValue(name) as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Ordner einer vorhandenen Installation (dieses Setups oder des alten NSIS-Setups), oder null.</summary>
|
||||||
|
public static string? InstalledDir
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var dir = RegValue("InstallLocation");
|
||||||
|
if (string.IsNullOrWhiteSpace(dir))
|
||||||
|
{
|
||||||
|
// altes NSIS-Setup trägt keinen InstallLocation ein → aus dem Deinstallationsbefehl ablesen
|
||||||
|
var m = Regex.Match(RegValue("UninstallString") ?? "", "^\"([^\"]+)\"");
|
||||||
|
dir = m.Success ? Path.GetDirectoryName(m.Groups[1].Value) : null;
|
||||||
|
}
|
||||||
|
return dir is not null && File.Exists(Path.Combine(dir, ExeName)) ? dir : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Stammt die vorhandene Installation noch vom alten NSIS-Setup?</summary>
|
||||||
|
public static bool IsLegacyInstall =>
|
||||||
|
InstalledDir is not null && (RegValue("UninstallString") ?? "").IndexOf("--uninstall", StringComparison.OrdinalIgnoreCase) < 0;
|
||||||
|
|
||||||
|
public static string? InstalledVersion => RegValue("DisplayVersion");
|
||||||
|
|
||||||
|
public static bool DesktopLinkExists => File.Exists(DesktopLink);
|
||||||
|
|
||||||
|
public static bool IsRunning(string? dir) => AppProcesses(dir, InstalledDir).Any();
|
||||||
|
|
||||||
|
// ---------- Installieren ----------
|
||||||
|
|
||||||
|
/// <param name="waitForExit">nach einem Update aus der App: ihr erst Zeit geben, sich selbst zu beenden</param>
|
||||||
|
public static void Install(string dir, bool desktopLink, IProgress<(double, string)> progress, bool waitForExit = false)
|
||||||
|
{
|
||||||
|
if (!Environment.Is64BitOperatingSystem)
|
||||||
|
throw new InvalidOperationException("Git Manager GUI benötigt ein 64-Bit-Windows (Windows 10 oder 11).");
|
||||||
|
|
||||||
|
var oldDir = InstalledDir;
|
||||||
|
progress.Report((0.02, "Git Manager GUI wird beendet …"));
|
||||||
|
CloseRunningApp(waitForExit ? 20000 : 0, dir, oldDir);
|
||||||
|
|
||||||
|
using var zip = Payload.OpenArchive();
|
||||||
|
var topLevel = TopLevelNames(zip);
|
||||||
|
|
||||||
|
progress.Report((0.05, "Alte Version wird entfernt …"));
|
||||||
|
Directory.CreateDirectory(dir);
|
||||||
|
RemoveAppFiles(dir, topLevel);
|
||||||
|
if (oldDir is not null && !SamePath(oldDir, dir))
|
||||||
|
{
|
||||||
|
// Installation wurde in einen anderen Ordner verlegt → alten Ordner aufräumen
|
||||||
|
var oldExe = Path.Combine(oldDir, ExeName);
|
||||||
|
DeleteIfPointsTo(StartMenuLink, oldExe);
|
||||||
|
DeleteIfPointsTo(DesktopLink, oldExe);
|
||||||
|
RemoveAppFiles(oldDir, topLevel);
|
||||||
|
try { Directory.Delete(oldDir); } catch { } // nur wenn leer
|
||||||
|
}
|
||||||
|
|
||||||
|
Extract(zip, dir, p => progress.Report((0.08 + p * 0.8, "Dateien werden kopiert …")));
|
||||||
|
|
||||||
|
progress.Report((0.9, "Deinstallationsprogramm wird angelegt …"));
|
||||||
|
var exe = Path.Combine(dir, ExeName);
|
||||||
|
var uninstaller = Path.Combine(dir, UninstallerName);
|
||||||
|
if (!SamePath(SelfPath, uninstaller))
|
||||||
|
{
|
||||||
|
MoveAside(uninstaller);
|
||||||
|
CopyStub(uninstaller);
|
||||||
|
}
|
||||||
|
File.WriteAllLines(Path.Combine(dir, FileListName), topLevel.Concat([UninstallerName, FileListName]));
|
||||||
|
|
||||||
|
progress.Report((0.94, "Verknüpfungen werden angelegt …"));
|
||||||
|
Shortcut.Create(StartMenuLink, exe, "Repositories auf Gitea und GitHub verwalten", AppUserModelId);
|
||||||
|
if (desktopLink) Shortcut.Create(DesktopLink, exe, "Repositories auf Gitea und GitHub verwalten", AppUserModelId);
|
||||||
|
else DeleteIfPointsTo(DesktopLink, exe);
|
||||||
|
|
||||||
|
progress.Report((0.97, "Windows wird eingerichtet …"));
|
||||||
|
Registry.CurrentUser.DeleteSubKeyTree(UninstallKey, throwOnMissingSubKey: false); // Reste des alten Setups
|
||||||
|
Registry.CurrentUser.DeleteSubKeyTree(LegacyAppKey, throwOnMissingSubKey: false);
|
||||||
|
using (var k = Registry.CurrentUser.CreateSubKey(UninstallKey))
|
||||||
|
{
|
||||||
|
long size = DirSize(dir) / 1024;
|
||||||
|
k.SetValue("DisplayName", AppName);
|
||||||
|
k.SetValue("DisplayVersion", Version);
|
||||||
|
k.SetValue("Publisher", Publisher);
|
||||||
|
k.SetValue("DisplayIcon", $"{exe},0");
|
||||||
|
k.SetValue("InstallLocation", dir);
|
||||||
|
k.SetValue("UninstallString", $"\"{uninstaller}\" --uninstall");
|
||||||
|
k.SetValue("QuietUninstallString", $"\"{uninstaller}\" --uninstall /S");
|
||||||
|
k.SetValue("URLInfoAbout", "https://git.viper.ipv64.net/M_Viper/Git-Manager-Gui");
|
||||||
|
k.SetValue("InstallDate", DateTime.Now.ToString("yyyyMMdd"));
|
||||||
|
k.SetValue("EstimatedSize", (int)Math.Min(int.MaxValue, size), RegistryValueKind.DWord);
|
||||||
|
k.SetValue("NoModify", 1, RegistryValueKind.DWord);
|
||||||
|
k.SetValue("NoRepair", 1, RegistryValueKind.DWord);
|
||||||
|
}
|
||||||
|
|
||||||
|
progress.Report((0.98, "Installation wird abgeschlossen …"));
|
||||||
|
Vorwaermen(exe);
|
||||||
|
|
||||||
|
progress.Report((1.0, "Fertig"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Startet die App einmal kurz mit "--gm-vorwaermen" – sie beendet sich
|
||||||
|
/// damit sofort wieder (siehe main.js). Windows Defender prüft eine neue,
|
||||||
|
/// noch unbekannte EXE beim ersten Start mehrere Sekunden lang (gemessen: ~8 s);
|
||||||
|
/// das passiert so während des Fortschrittsbalkens und nicht erst beim Klick
|
||||||
|
/// auf "Fertigstellen". Das Ergebnis merkt sich Windows, der echte Start ist
|
||||||
|
/// danach sofort da.
|
||||||
|
/// </summary>
|
||||||
|
private static void Vorwaermen(string exe)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var p = Process.Start(new ProcessStartInfo(exe, "--gm-vorwaermen")
|
||||||
|
{
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true,
|
||||||
|
WorkingDirectory = Path.GetDirectoryName(exe)!,
|
||||||
|
});
|
||||||
|
if (p != null && !p.WaitForExit(30000)) { try { p.Kill(); } catch { } }
|
||||||
|
}
|
||||||
|
catch { /* nur eine Beschleunigung – Fehler sind egal */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<string> TopLevelNames(ZipArchive zip) =>
|
||||||
|
zip.Entries
|
||||||
|
.Select(e => e.FullName.Replace('\\', '/').Split('/')[0])
|
||||||
|
.Where(n => n.Length > 0)
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
private static void Extract(ZipArchive zip, string dir, Action<double> report)
|
||||||
|
{
|
||||||
|
var root = Path.GetFullPath(dir).TrimEnd('\\') + "\\";
|
||||||
|
long total = Math.Max(1, zip.Entries.Sum(e => e.Length)), done = 0;
|
||||||
|
var buffer = new byte[1 << 20];
|
||||||
|
int lastPercent = -1;
|
||||||
|
|
||||||
|
foreach (var entry in zip.Entries)
|
||||||
|
{
|
||||||
|
var target = Path.GetFullPath(Path.Combine(dir, entry.FullName));
|
||||||
|
if (!target.StartsWith(root, StringComparison.OrdinalIgnoreCase)) continue; // nie außerhalb des Ordners schreiben
|
||||||
|
if (entry.Name.Length == 0)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(target);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(target)!);
|
||||||
|
long before = done;
|
||||||
|
Retry(target, () =>
|
||||||
|
{
|
||||||
|
done = before;
|
||||||
|
MoveAside(target);
|
||||||
|
using var src = entry.Open();
|
||||||
|
using var dst = File.Create(target);
|
||||||
|
int n;
|
||||||
|
while ((n = src.Read(buffer, 0, buffer.Length)) > 0)
|
||||||
|
{
|
||||||
|
dst.Write(buffer, 0, n);
|
||||||
|
done += n;
|
||||||
|
int percent = (int)(done * 100 / total);
|
||||||
|
if (percent != lastPercent)
|
||||||
|
{
|
||||||
|
lastPercent = percent;
|
||||||
|
report(Math.Min(1.0, (double)done / total));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
report(1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Setup ohne angehängte App als Deinstallationsprogramm ablegen (nur ein paar hundert KB).</summary>
|
||||||
|
private static void CopyStub(string path)
|
||||||
|
{
|
||||||
|
long length = Payload.StubLength;
|
||||||
|
using var src = File.OpenRead(SelfPath);
|
||||||
|
using var dst = File.Create(path);
|
||||||
|
var buffer = new byte[81920];
|
||||||
|
long left = length;
|
||||||
|
while (left > 0)
|
||||||
|
{
|
||||||
|
int n = src.Read(buffer, 0, (int)Math.Min(buffer.Length, left));
|
||||||
|
if (n <= 0) break;
|
||||||
|
dst.Write(buffer, 0, n);
|
||||||
|
left -= n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Deinstallieren ----------
|
||||||
|
|
||||||
|
public static void Uninstall(bool removeData, IProgress<(double, string)> progress)
|
||||||
|
{
|
||||||
|
// Nur im eingetragenen Installationsordner löschen – nie dort, wo das Setup zufällig liegt (z. B. Downloads)
|
||||||
|
var dir = InstalledDir;
|
||||||
|
var exe = dir is null ? "" : Path.Combine(dir, ExeName);
|
||||||
|
|
||||||
|
progress.Report((0.1, "Git Manager GUI wird beendet …"));
|
||||||
|
CloseRunningApp(0, dir);
|
||||||
|
|
||||||
|
progress.Report((0.3, "Verknüpfungen werden entfernt …"));
|
||||||
|
DeleteIfPointsTo(StartMenuLink, exe);
|
||||||
|
DeleteIfPointsTo(DesktopLink, exe);
|
||||||
|
|
||||||
|
progress.Report((0.45, "Windows-Einträge werden entfernt …"));
|
||||||
|
RemoveAutostart(exe);
|
||||||
|
Registry.CurrentUser.DeleteSubKeyTree(UninstallKey, throwOnMissingSubKey: false);
|
||||||
|
Registry.CurrentUser.DeleteSubKeyTree(LegacyAppKey, throwOnMissingSubKey: false);
|
||||||
|
// Name/Logo für Windows-Benachrichtigungen (legt Electron selbst an)
|
||||||
|
Registry.CurrentUser.DeleteSubKeyTree(@"Software\Classes\AppUserModelId\" + AppUserModelId, throwOnMissingSubKey: false);
|
||||||
|
|
||||||
|
if (removeData && Directory.Exists(DataDir))
|
||||||
|
{
|
||||||
|
progress.Report((0.6, "Zugangsdaten und Einstellungen werden in den Papierkorb gelegt …"));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Microsoft.VisualBasic.FileIO.FileSystem.DeleteDirectory(DataDir,
|
||||||
|
Microsoft.VisualBasic.FileIO.UIOption.OnlyErrorDialogs,
|
||||||
|
Microsoft.VisualBasic.FileIO.RecycleOption.SendToRecycleBin);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
progress.Report((0.8, "Dateien werden entfernt …"));
|
||||||
|
if (dir is null) { progress.Report((1.0, "Fertig")); return; }
|
||||||
|
RemoveAppFiles(dir, null);
|
||||||
|
|
||||||
|
// Das laufende Deinstallationsprogramm kann sich nicht selbst löschen → kurz nach dem Beenden per cmd.
|
||||||
|
// rmdir ohne /s entfernt den Ordner nur, wenn er danach leer ist.
|
||||||
|
var self = SelfPath;
|
||||||
|
if (IsUnder(self, dir))
|
||||||
|
{
|
||||||
|
Process.Start(new ProcessStartInfo("cmd.exe",
|
||||||
|
$"/c ping 127.0.0.1 -n 3 > nul & del /f /q \"{self}\" & del /f /q \"{Path.Combine(dir, "*.old")}\" & rmdir \"{dir}\"")
|
||||||
|
{
|
||||||
|
CreateNoWindow = true, UseShellExecute = false, WindowStyle = ProcessWindowStyle.Hidden,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TryDelete(Path.Combine(dir, UninstallerName));
|
||||||
|
try { Directory.Delete(dir); } catch { } // nur wenn leer
|
||||||
|
}
|
||||||
|
progress.Report((1.0, "Fertig"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Autostart-Einträge entfernen, die die App selbst gesetzt hat (Name vergibt Electron).</summary>
|
||||||
|
private static void RemoveAutostart(string exe)
|
||||||
|
{
|
||||||
|
if (exe.Length == 0) return;
|
||||||
|
using var run = Registry.CurrentUser.OpenSubKey(RunKey, writable: true);
|
||||||
|
if (run is null) return;
|
||||||
|
foreach (var name in run.GetValueNames())
|
||||||
|
{
|
||||||
|
if (run.GetValue(name) is string cmd && cmd.IndexOf(exe, StringComparison.OrdinalIgnoreCase) >= 0)
|
||||||
|
run.DeleteValue(name, throwOnMissingValue: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Dateien ----------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Entfernt die App-Dateien aus einem Installationsordner. Gelöscht wird nur, was nachweislich zur App gehört:
|
||||||
|
/// Einträge aus der Dateiliste der letzten Installation bzw. die Namen aus dem neuen Paket –
|
||||||
|
/// Ordner nur, wenn sie eindeutig von Electron stammen. Fremde Dateien im Ordner bleiben unangetastet.
|
||||||
|
/// </summary>
|
||||||
|
private static void RemoveAppFiles(string dir, IEnumerable<string>? extraNames)
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(dir)) return;
|
||||||
|
CleanupOldFiles(dir);
|
||||||
|
var listFile = Path.Combine(dir, FileListName);
|
||||||
|
var fromList = File.Exists(listFile) ? File.ReadAllLines(listFile).Where(l => l.Trim().Length > 0).ToList() : [];
|
||||||
|
var names = fromList.Concat(extraNames ?? []).Concat([UninstallerName, FileListName])
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (var name in names)
|
||||||
|
{
|
||||||
|
if (name.IndexOfAny(['\\', '/', ':']) >= 0 || name is "." or "..") continue;
|
||||||
|
var path = Path.Combine(dir, name);
|
||||||
|
if (SamePath(path, SelfPath)) continue; // das laufende Deinstallationsprogramm räumt cmd weg
|
||||||
|
|
||||||
|
if (Directory.Exists(path))
|
||||||
|
{
|
||||||
|
bool ours = fromList.Contains(name, StringComparer.OrdinalIgnoreCase)
|
||||||
|
|| (name.Equals("resources", StringComparison.OrdinalIgnoreCase) && File.Exists(Path.Combine(path, "app.asar")))
|
||||||
|
|| (name.Equals("locales", StringComparison.OrdinalIgnoreCase) && File.Exists(Path.Combine(path, "en-US.pak")));
|
||||||
|
if (ours) Retry(path, () => Directory.Delete(path, recursive: true));
|
||||||
|
}
|
||||||
|
else if (File.Exists(path))
|
||||||
|
{
|
||||||
|
Retry(path, () => MoveAside(path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Längster Pfad im Paket. Windows erlaubt (ohne Sonderfreigabe) nur 259 Zeichen pro Pfad –
|
||||||
|
/// der Installationsordner darf deshalb höchstens <see cref="MaxDirLength"/> Zeichen lang sein.
|
||||||
|
/// </summary>
|
||||||
|
private static int? _longestEntry;
|
||||||
|
public static int MaxDirLength
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_longestEntry is null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var zip = Payload.OpenArchive();
|
||||||
|
_longestEntry = zip.Entries.Count == 0 ? 0 : zip.Entries.Max(e => e.FullName.Length);
|
||||||
|
}
|
||||||
|
catch { _longestEntry = 0; }
|
||||||
|
}
|
||||||
|
return 259 - 1 - _longestEntry.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Retry(string path, Action action)
|
||||||
|
{
|
||||||
|
for (int attempt = 1; ; attempt++)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
action();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (PathTooLongException ex)
|
||||||
|
{
|
||||||
|
throw new IOException($"Der Installationsordner ist zu lang. Bitte einen kürzeren Ordner wählen (höchstens {MaxDirLength} Zeichen).", ex);
|
||||||
|
}
|
||||||
|
catch (Exception ex) when ((ex is IOException || ex is UnauthorizedAccessException) && attempt < 10)
|
||||||
|
{
|
||||||
|
Thread.Sleep(500);
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
throw new IOException($"„{Path.GetFileName(path)}“ wird noch verwendet. Bitte Git Manager GUI beenden " +
|
||||||
|
"(auch das Symbol im Infobereich neben der Uhr) und erneut versuchen.", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Vorhandene Datei löschen oder – falls sie noch geöffnet ist (Explorer, Virenscanner) – beiseite umbenennen.
|
||||||
|
/// Umbenennen klappt unter Windows auch bei geöffneten Dateien, überschreiben nicht.
|
||||||
|
/// </summary>
|
||||||
|
private static void MoveAside(string path)
|
||||||
|
{
|
||||||
|
if (!File.Exists(path)) return;
|
||||||
|
try { File.Delete(path); return; } catch { }
|
||||||
|
File.Move(path, $"{path}.{Guid.NewGuid():N}.old");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CleanupOldFiles(string dir)
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(dir)) return;
|
||||||
|
foreach (var f in Directory.GetFiles(dir, "*.old", SearchOption.AllDirectories))
|
||||||
|
try { File.Delete(f); } catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TryDelete(string path)
|
||||||
|
{
|
||||||
|
try { if (File.Exists(path)) File.Delete(path); } catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DeleteIfPointsTo(string link, string target)
|
||||||
|
{
|
||||||
|
if (!File.Exists(link) || target.Length == 0) return;
|
||||||
|
if (!SamePath(Shortcut.Target(link) ?? "", target)) return;
|
||||||
|
// Explorer/Indexdienst halten Verknüpfungen manchmal kurz fest → mehrmals versuchen
|
||||||
|
for (int i = 0; i < 10 && File.Exists(link); i++)
|
||||||
|
{
|
||||||
|
try { File.Delete(link); }
|
||||||
|
catch { Thread.Sleep(300); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Prozesse ----------
|
||||||
|
|
||||||
|
private static IEnumerable<Process> AppProcesses(params string?[] dirs)
|
||||||
|
{
|
||||||
|
var roots = dirs.Where(d => !string.IsNullOrEmpty(d)).Select(d => d!).ToList();
|
||||||
|
if (roots.Count == 0) yield break;
|
||||||
|
// die App (mehrere Electron-Prozesse) und das mitgelieferte Git, falls gerade ein Upload läuft
|
||||||
|
foreach (var p in Process.GetProcessesByName(ProcessName).Concat(Process.GetProcessesByName("git")))
|
||||||
|
{
|
||||||
|
string? file = null;
|
||||||
|
try { file = p.MainModule?.FileName; } catch { }
|
||||||
|
if (file is not null && roots.Any(r => IsUnder(file, r))) yield return p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CloseRunningApp(int waitMs, params string?[] dirs)
|
||||||
|
{
|
||||||
|
var until = DateTime.Now.AddMilliseconds(waitMs);
|
||||||
|
while (waitMs > 0 && DateTime.Now < until && AppProcesses(dirs).Any())
|
||||||
|
Thread.Sleep(300);
|
||||||
|
|
||||||
|
foreach (var p in AppProcesses(dirs).ToList())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
p.Kill();
|
||||||
|
p.WaitForExit(10000);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Helfer ----------
|
||||||
|
|
||||||
|
private static bool SamePath(string a, string b)
|
||||||
|
{
|
||||||
|
try { return string.Equals(Path.GetFullPath(a).TrimEnd('\\'), Path.GetFullPath(b).TrimEnd('\\'), StringComparison.OrdinalIgnoreCase); }
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsUnder(string path, string dir) =>
|
||||||
|
Path.GetFullPath(path).StartsWith(Path.GetFullPath(dir).TrimEnd('\\') + "\\", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
private static long DirSize(string dir)
|
||||||
|
{
|
||||||
|
long sum = 0;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var f in Directory.EnumerateFiles(dir, "*", SearchOption.AllDirectories))
|
||||||
|
try { sum += new FileInfo(f).Length; } catch { }
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long DataSize() => Directory.Exists(DataDir) ? DirSize(DataDir) : 0;
|
||||||
|
|
||||||
|
public static string FormatSize(long bytes) => bytes switch
|
||||||
|
{
|
||||||
|
< 1024 * 1024 => $"{Math.Max(1, bytes / 1024)} KB",
|
||||||
|
< 1024L * 1024 * 1024 => $"{bytes / 1024.0 / 1024:0.#} MB",
|
||||||
|
_ => $"{bytes / 1024.0 / 1024 / 1024:0.##} GB",
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,352 @@
|
|||||||
|
<Window x:Class="GitManagerSetup.MainWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
Title="Git Manager GUI Setup" Icon="app.ico" Width="780" Height="520" ResizeMode="CanMinimize"
|
||||||
|
Background="{DynamicResource LayerBrush}" Foreground="{DynamicResource TextBrush}"
|
||||||
|
FontFamily="{DynamicResource UiFont}" FontSize="14"
|
||||||
|
TextOptions.TextFormattingMode="Display" UseLayoutRounding="True"
|
||||||
|
WindowStartupLocation="CenterScreen">
|
||||||
|
|
||||||
|
<WindowChrome.WindowChrome>
|
||||||
|
<WindowChrome CaptionHeight="36" ResizeBorderThickness="0" GlassFrameThickness="0,0,0,1"
|
||||||
|
CornerRadius="0" UseAeroCaptionButtons="False"/>
|
||||||
|
</WindowChrome.WindowChrome>
|
||||||
|
|
||||||
|
<Window.Resources>
|
||||||
|
<!-- Markenfarben der Seitenleiste (in Hell und Dunkel gleich, wie die App) -->
|
||||||
|
<SolidColorBrush x:Key="SideText" Color="#F5F8FF"/>
|
||||||
|
<SolidColorBrush x:Key="SideTextSecondary" Color="#AEBDD8"/>
|
||||||
|
<SolidColorBrush x:Key="SideTextMuted" Color="#7083A4"/>
|
||||||
|
<LinearGradientBrush x:Key="BrandGradient" StartPoint="0,0" EndPoint="1,0">
|
||||||
|
<GradientStop Color="#58D5FF" Offset="0"/>
|
||||||
|
<GradientStop Color="#5C87FF" Offset="1"/>
|
||||||
|
</LinearGradientBrush>
|
||||||
|
|
||||||
|
<Style x:Key="PageTitle" TargetType="TextBlock">
|
||||||
|
<Setter Property="FontFamily" Value="{DynamicResource DisplayFont}"/>
|
||||||
|
<Setter Property="FontSize" Value="26"/>
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
<Setter Property="Margin" Value="0,0,0,8"/>
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="Body" TargetType="TextBlock">
|
||||||
|
<Setter Property="TextWrapping" Value="Wrap"/>
|
||||||
|
<Setter Property="LineHeight" Value="21"/>
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TextSecondaryBrush}"/>
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="StepDot" TargetType="Border">
|
||||||
|
<Setter Property="Width" Value="26"/>
|
||||||
|
<Setter Property="Height" Value="26"/>
|
||||||
|
<Setter Property="CornerRadius" Value="13"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1.5"/>
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="StepNum" TargetType="TextBlock">
|
||||||
|
<Setter Property="FontSize" Value="12"/>
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Center"/>
|
||||||
|
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="StepLabel" TargetType="TextBlock">
|
||||||
|
<Setter Property="Margin" Value="12,0,0,0"/>
|
||||||
|
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||||
|
<Setter Property="FontSize" Value="13.5"/>
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="StepLine" TargetType="Border">
|
||||||
|
<Setter Property="Width" Value="2"/>
|
||||||
|
<Setter Property="Height" Value="18"/>
|
||||||
|
<Setter Property="Margin" Value="12,3,0,3"/>
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Left"/>
|
||||||
|
<Setter Property="CornerRadius" Value="1"/>
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="FeatureIcon" TargetType="Border">
|
||||||
|
<Setter Property="Width" Value="38"/>
|
||||||
|
<Setter Property="Height" Value="38"/>
|
||||||
|
<Setter Property="CornerRadius" Value="10"/>
|
||||||
|
<Setter Property="Background" Value="{DynamicResource AccentSubtleBrush}"/>
|
||||||
|
<Setter Property="VerticalAlignment" Value="Top"/>
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="FeatureGlyph" TargetType="TextBlock">
|
||||||
|
<Setter Property="FontFamily" Value="{DynamicResource IconFont}"/>
|
||||||
|
<Setter Property="FontSize" Value="17"/>
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource AccentBrush}"/>
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Center"/>
|
||||||
|
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="FeatureTitle" TargetType="TextBlock">
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
<Setter Property="FontSize" Value="13.5"/>
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="FeatureText" TargetType="TextBlock">
|
||||||
|
<Setter Property="FontSize" Value="12"/>
|
||||||
|
<Setter Property="TextWrapping" Value="Wrap"/>
|
||||||
|
<Setter Property="Margin" Value="0,2,0,0"/>
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TextSecondaryBrush}"/>
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="Card" TargetType="Border">
|
||||||
|
<Setter Property="Padding" Value="16,14"/>
|
||||||
|
<Setter Property="CornerRadius" Value="8"/>
|
||||||
|
<Setter Property="Background" Value="{DynamicResource ControlFillBrush}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource ControlStrokeBrush}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
</Style>
|
||||||
|
</Window.Resources>
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="36"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="250"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- ===== Seitenleiste im Look der App ===== -->
|
||||||
|
<Border Grid.RowSpan="3" ClipToBounds="True">
|
||||||
|
<Border.Background>
|
||||||
|
<LinearGradientBrush StartPoint="0,0" EndPoint="0.6,1">
|
||||||
|
<GradientStop Color="#07111F" Offset="0"/>
|
||||||
|
<GradientStop Color="#0D1729" Offset="0.5"/>
|
||||||
|
<GradientStop Color="#13305C" Offset="1"/>
|
||||||
|
</LinearGradientBrush>
|
||||||
|
</Border.Background>
|
||||||
|
<Grid>
|
||||||
|
<!-- Lichtschein hinter dem Logo und unten -->
|
||||||
|
<Ellipse Width="320" Height="320" Margin="-120,-110,0,0" HorizontalAlignment="Left" VerticalAlignment="Top" IsHitTestVisible="False">
|
||||||
|
<Ellipse.Fill>
|
||||||
|
<RadialGradientBrush>
|
||||||
|
<GradientStop Color="#5558D5FF" Offset="0"/>
|
||||||
|
<GradientStop Color="#0058D5FF" Offset="1"/>
|
||||||
|
</RadialGradientBrush>
|
||||||
|
</Ellipse.Fill>
|
||||||
|
</Ellipse>
|
||||||
|
<Ellipse Width="300" Height="300" Margin="0,0,-150,-150" HorizontalAlignment="Right" VerticalAlignment="Bottom" IsHitTestVisible="False">
|
||||||
|
<Ellipse.Fill>
|
||||||
|
<RadialGradientBrush>
|
||||||
|
<GradientStop Color="#445C87FF" Offset="0"/>
|
||||||
|
<GradientStop Color="#005C87FF" Offset="1"/>
|
||||||
|
</RadialGradientBrush>
|
||||||
|
</Ellipse.Fill>
|
||||||
|
</Ellipse>
|
||||||
|
|
||||||
|
<StackPanel Margin="30,64,24,0">
|
||||||
|
<Image Source="app.png" Width="68" Height="68" HorizontalAlignment="Left" RenderOptions.BitmapScalingMode="HighQuality">
|
||||||
|
<Image.Effect>
|
||||||
|
<DropShadowEffect Color="#58D5FF" BlurRadius="28" ShadowDepth="0" Opacity="0.6"/>
|
||||||
|
</Image.Effect>
|
||||||
|
</Image>
|
||||||
|
<TextBlock Text="Git Manager" FontFamily="{DynamicResource DisplayFont}" FontSize="26" FontWeight="SemiBold"
|
||||||
|
Foreground="{StaticResource SideText}" Margin="0,16,0,0"/>
|
||||||
|
<TextBlock x:Name="VersionText" Foreground="{StaticResource SideTextSecondary}" FontSize="12" Margin="0,2,0,0"/>
|
||||||
|
|
||||||
|
<StackPanel x:Name="Steps" Margin="0,38,0,0">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Border x:Name="Dot0" Style="{StaticResource StepDot}"><TextBlock x:Name="Num0" Text="1" Style="{StaticResource StepNum}"/></Border>
|
||||||
|
<TextBlock x:Name="Label0" Text="Willkommen" Style="{StaticResource StepLabel}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<Border x:Name="Line0" Style="{StaticResource StepLine}"/>
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Border x:Name="Dot1" Style="{StaticResource StepDot}"><TextBlock x:Name="Num1" Text="2" Style="{StaticResource StepNum}"/></Border>
|
||||||
|
<TextBlock x:Name="Label1" Text="Optionen" Style="{StaticResource StepLabel}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<Border x:Name="Line1" Style="{StaticResource StepLine}"/>
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Border x:Name="Dot2" Style="{StaticResource StepDot}"><TextBlock x:Name="Num2" Text="3" Style="{StaticResource StepNum}"/></Border>
|
||||||
|
<TextBlock x:Name="Label2" Text="Installation" Style="{StaticResource StepLabel}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<TextBlock Text="© M_Viper" FontSize="11" Foreground="{StaticResource SideTextMuted}"
|
||||||
|
VerticalAlignment="Bottom" Margin="30,0,0,20"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Titelleiste -->
|
||||||
|
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="14,0,0,0">
|
||||||
|
<Image Source="app.ico" Width="16" Height="16" RenderOptions.BitmapScalingMode="HighQuality"/>
|
||||||
|
<TextBlock x:Name="WindowTitle" Text="Git Manager GUI Setup" FontSize="12" Margin="10,0,0,0" VerticalAlignment="Center"
|
||||||
|
Foreground="{StaticResource SideTextSecondary}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Top">
|
||||||
|
<Button Style="{StaticResource CaptionButton}" Content="" Click="Minimize_Click"
|
||||||
|
WindowChrome.IsHitTestVisibleInChrome="True"/>
|
||||||
|
<Button x:Name="CloseButton" Style="{StaticResource CloseCaptionButton}" Content="" Click="Cancel_Click"
|
||||||
|
WindowChrome.IsHitTestVisibleInChrome="True"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- ===== Inhalt ===== -->
|
||||||
|
<Grid Grid.Row="1" Grid.Column="1" Margin="40,18,40,0">
|
||||||
|
|
||||||
|
<!-- 0: Willkommen -->
|
||||||
|
<StackPanel x:Name="PageWelcome">
|
||||||
|
<TextBlock Text="Willkommen beim Git Manager" Style="{StaticResource PageTitle}"/>
|
||||||
|
<TextBlock Style="{StaticResource Body}"
|
||||||
|
Text="Der Assistent richtet Git Manager GUI in wenigen Schritten ein – nur für dein Benutzerkonto und ohne Administratorrechte."/>
|
||||||
|
|
||||||
|
<Grid Margin="0,24,0,0">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="20"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="18"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<DockPanel>
|
||||||
|
<Border Style="{StaticResource FeatureIcon}" DockPanel.Dock="Left"><TextBlock Text="" Style="{StaticResource FeatureGlyph}"/></Border>
|
||||||
|
<StackPanel Margin="12,0,0,0">
|
||||||
|
<TextBlock Text="Gitea & GitHub" Style="{StaticResource FeatureTitle}"/>
|
||||||
|
<TextBlock Text="Repos, Dateien, Releases und Commits an einem Ort" Style="{StaticResource FeatureText}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</DockPanel>
|
||||||
|
<DockPanel Grid.Column="2">
|
||||||
|
<Border Style="{StaticResource FeatureIcon}" DockPanel.Dock="Left"><TextBlock Text="" Style="{StaticResource FeatureGlyph}"/></Border>
|
||||||
|
<StackPanel Margin="12,0,0,0">
|
||||||
|
<TextBlock Text="Git eingebaut" Style="{StaticResource FeatureTitle}"/>
|
||||||
|
<TextBlock Text="Kein zusätzliches Programm nötig" Style="{StaticResource FeatureText}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</DockPanel>
|
||||||
|
<DockPanel Grid.Row="2">
|
||||||
|
<Border Style="{StaticResource FeatureIcon}" DockPanel.Dock="Left"><TextBlock Text="" Style="{StaticResource FeatureGlyph}"/></Border>
|
||||||
|
<StackPanel Margin="12,0,0,0">
|
||||||
|
<TextBlock Text="PRs & Issues" Style="{StaticResource FeatureTitle}"/>
|
||||||
|
<TextBlock Text="Ansehen, kommentieren und mergen" Style="{StaticResource FeatureText}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</DockPanel>
|
||||||
|
<DockPanel Grid.Row="2" Grid.Column="2">
|
||||||
|
<Border Style="{StaticResource FeatureIcon}" DockPanel.Dock="Left"><TextBlock Text="" Style="{StaticResource FeatureGlyph}"/></Border>
|
||||||
|
<StackPanel Margin="12,0,0,0">
|
||||||
|
<TextBlock Text="Backup" Style="{StaticResource FeatureTitle}"/>
|
||||||
|
<TextBlock Text="Alle Repos sichern und wiederherstellen" Style="{StaticResource FeatureText}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</DockPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Border x:Name="UpdateHint" Margin="0,26,0,0" Padding="14,10" CornerRadius="8" Visibility="Collapsed"
|
||||||
|
Background="{DynamicResource AccentSubtleBrush}" BorderBrush="{DynamicResource AccentSubtleStrokeBrush}" BorderThickness="1">
|
||||||
|
<DockPanel>
|
||||||
|
<TextBlock DockPanel.Dock="Left" Text="" FontFamily="{DynamicResource IconFont}" FontSize="16"
|
||||||
|
Foreground="{DynamicResource AccentBrush}" Margin="0,2,12,0" VerticalAlignment="Top"/>
|
||||||
|
<TextBlock x:Name="UpdateHintText" TextWrapping="Wrap" LineHeight="20" FontSize="13"/>
|
||||||
|
</DockPanel>
|
||||||
|
</Border>
|
||||||
|
<Border x:Name="RunningHint" Margin="0,10,0,0" Padding="14,10" CornerRadius="8" Visibility="Collapsed"
|
||||||
|
Background="{DynamicResource ControlFillBrush}" BorderBrush="{DynamicResource ControlStrokeBrush}" BorderThickness="1">
|
||||||
|
<DockPanel>
|
||||||
|
<TextBlock DockPanel.Dock="Left" Text="" FontFamily="{DynamicResource IconFont}" FontSize="16"
|
||||||
|
Foreground="{DynamicResource AccentBrush}" Margin="0,2,12,0" VerticalAlignment="Top"/>
|
||||||
|
<TextBlock TextWrapping="Wrap" LineHeight="20" FontSize="13"
|
||||||
|
Text="Git Manager GUI läuft gerade und wird für die Installation automatisch beendet. Laufende Uploads gehen dabei verloren."/>
|
||||||
|
</DockPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- 1: Optionen -->
|
||||||
|
<StackPanel x:Name="PageOptions" Visibility="Collapsed">
|
||||||
|
<TextBlock Text="Optionen" Style="{StaticResource PageTitle}"/>
|
||||||
|
<TextBlock Style="{StaticResource Body}" Text="Wohin soll Git Manager GUI installiert werden?"/>
|
||||||
|
|
||||||
|
<Border Style="{StaticResource Card}" Margin="0,18,0,0">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="Installationsordner" FontWeight="SemiBold" FontSize="13" Margin="0,0,0,10"/>
|
||||||
|
<DockPanel>
|
||||||
|
<Button DockPanel.Dock="Right" Content="Ändern …" Margin="8,0,0,0" Click="ChangeDir_Click"/>
|
||||||
|
<TextBox x:Name="DirBox"/>
|
||||||
|
</DockPanel>
|
||||||
|
<TextBlock Style="{StaticResource Body}" FontSize="12" LineHeight="18" Margin="0,10,0,0"
|
||||||
|
Text="Benötigt ca. 450 MB. Zugangsdaten, Einstellungen und Favoriten liegen getrennt davon und bleiben bei Updates erhalten."/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Style="{StaticResource Card}" Margin="0,12,0,0">
|
||||||
|
<StackPanel>
|
||||||
|
<CheckBox x:Name="DesktopLinkBox" Content="Verknüpfung auf dem Desktop" IsChecked="True"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- 2: Fortschritt / Fertig -->
|
||||||
|
<StackPanel x:Name="PageProgress" Visibility="Collapsed">
|
||||||
|
<Border x:Name="SuccessIcon" Width="64" Height="64" CornerRadius="32" HorizontalAlignment="Left" Margin="0,4,0,20"
|
||||||
|
Background="{StaticResource BrandGradient}" RenderTransformOrigin="0.5,0.5" Visibility="Collapsed">
|
||||||
|
<Border.RenderTransform><ScaleTransform x:Name="SuccessScale" ScaleX="1" ScaleY="1"/></Border.RenderTransform>
|
||||||
|
<Border.Effect><DropShadowEffect Color="#58D5FF" BlurRadius="24" ShadowDepth="0" Opacity="0.5"/></Border.Effect>
|
||||||
|
<TextBlock Text="" FontFamily="{DynamicResource IconFont}" FontSize="26" Foreground="#04101F"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<TextBlock x:Name="ProgressTitle" Text="Git Manager GUI wird installiert …" Style="{StaticResource PageTitle}"/>
|
||||||
|
|
||||||
|
<StackPanel x:Name="ProgressBarPanel" Margin="0,18,0,0">
|
||||||
|
<TextBlock x:Name="PercentText" Text="0 %" FontFamily="{DynamicResource DisplayFont}" FontSize="44" FontWeight="SemiBold"
|
||||||
|
Foreground="{DynamicResource AccentBrush}"/>
|
||||||
|
<Border x:Name="ProgressTrack" SizeChanged="ProgressTrack_SizeChanged" Height="8" CornerRadius="4" Margin="0,10,0,0" Background="{DynamicResource SubtleSelectedBrush}">
|
||||||
|
<Border x:Name="ProgressFill" HorizontalAlignment="Left" Width="0" CornerRadius="4" Background="{StaticResource BrandGradient}"/>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock x:Name="ProgressText" Style="{StaticResource Body}" Margin="0,12,0,0" Text="Einen Moment bitte."/>
|
||||||
|
|
||||||
|
<StackPanel x:Name="DonePanel" Visibility="Collapsed">
|
||||||
|
<CheckBox x:Name="LaunchBox" Content="Git Manager GUI jetzt starten" IsChecked="True" Margin="0,24,0,0"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Deinstallation -->
|
||||||
|
<StackPanel x:Name="PageUninstall" Visibility="Collapsed">
|
||||||
|
<TextBlock Text="Git Manager GUI deinstallieren" Style="{StaticResource PageTitle}"/>
|
||||||
|
<TextBlock Style="{StaticResource Body}"
|
||||||
|
Text="Git Manager GUI wird von deinem PC entfernt: Programm, Verknüpfungen, Autostart und der Eintrag in den Windows-Einstellungen."/>
|
||||||
|
<Border Style="{StaticResource Card}" Margin="0,22,0,0">
|
||||||
|
<StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Text="" FontFamily="{DynamicResource IconFont}" FontSize="15" Foreground="{DynamicResource AccentBrush}"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Text="Deine Zugangsdaten und Einstellungen bleiben erhalten" FontWeight="SemiBold" Margin="10,0,0,0" VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock x:Name="DataInfo" Style="{StaticResource Body}" FontSize="12.5" Margin="0,6,0,14"/>
|
||||||
|
<CheckBox x:Name="RemoveDataBox" Content="Auch Zugangsdaten, Einstellungen, Favoriten und Verlauf löschen"
|
||||||
|
Checked="RemoveDataBox_Click" Unchecked="RemoveDataBox_Click"/>
|
||||||
|
<TextBlock x:Name="RemoveDataWarning" Visibility="Collapsed" Margin="30,8,0,0" TextWrapping="Wrap" FontSize="12.5"
|
||||||
|
Foreground="{DynamicResource DangerBrush}"
|
||||||
|
Text="Die gespeicherten Tokens für Gitea und GitHub werden entfernt (sie landen im Papierkorb). Deine Repos auf dem Server, lokale Projektordner und Repo-Backups bleiben unangetastet."/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Fußleiste -->
|
||||||
|
<StackPanel Grid.Row="2" Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,14,40,24">
|
||||||
|
<Button x:Name="BackButton" Content="Zurück" Width="110" Margin="0,0,8,0" Click="Back_Click"/>
|
||||||
|
<Button x:Name="NextButton" Content="Weiter" Width="140" Margin="0,0,8,0" Style="{StaticResource AccentButton}" IsDefault="True" Click="Next_Click"/>
|
||||||
|
<Button x:Name="CancelButton" Content="Abbrechen" Width="110" Click="Cancel_Click"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Eigene Rückfrage im Setup-Look statt Windows-Meldungsfenster (siehe Frage()) -->
|
||||||
|
<Grid x:Name="DialogOverlay" Grid.RowSpan="3" Grid.ColumnSpan="2" Visibility="Collapsed" Background="#99000000">
|
||||||
|
<Border x:Name="DialogCard" Width="420" HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||||
|
Background="{DynamicResource LayerBrush}" BorderBrush="{DynamicResource ControlStrokeBrush}" BorderThickness="1"
|
||||||
|
CornerRadius="10" Padding="26,22,22,18" Effect="{StaticResource DialogShadow}" RenderTransformOrigin="0.5,0.5">
|
||||||
|
<Border.RenderTransform><ScaleTransform x:Name="DialogScale" ScaleX="1" ScaleY="1"/></Border.RenderTransform>
|
||||||
|
<StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Border Width="34" Height="34" CornerRadius="17" Background="{DynamicResource AccentSubtleBrush}" VerticalAlignment="Top">
|
||||||
|
<TextBlock x:Name="DialogIcon" Text="" FontFamily="{DynamicResource IconFont}" FontSize="16"
|
||||||
|
Foreground="{DynamicResource AccentBrush}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<TextBlock x:Name="DialogTitle" FontFamily="{DynamicResource DisplayFont}" FontSize="18" FontWeight="SemiBold"
|
||||||
|
VerticalAlignment="Center" Margin="12,0,0,0" TextWrapping="Wrap" MaxWidth="320"/>
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock x:Name="DialogText" TextWrapping="Wrap" LineHeight="20" Margin="46,8,0,0"
|
||||||
|
Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||||
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,22,0,0">
|
||||||
|
<Button x:Name="DialogSecondary" MinWidth="120" Margin="0,0,8,0" Click="DialogSecondary_Click"/>
|
||||||
|
<Button x:Name="DialogPrimary" MinWidth="130" Style="{StaticResource AccentButton}" Click="DialogPrimary_Click"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,454 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Media.Animation;
|
||||||
|
using System.Windows.Threading;
|
||||||
|
|
||||||
|
namespace GitManagerSetup;
|
||||||
|
|
||||||
|
public partial class MainWindow : Window
|
||||||
|
{
|
||||||
|
private readonly SetupMode _mode;
|
||||||
|
private int _page;
|
||||||
|
private bool _busy;
|
||||||
|
private bool _done;
|
||||||
|
private string? _installedExe;
|
||||||
|
|
||||||
|
private const int PageNumOptions = 1;
|
||||||
|
private const int PageNumProgress = 2;
|
||||||
|
|
||||||
|
public MainWindow(SetupMode mode)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
_mode = mode;
|
||||||
|
VersionText.Text = $"Version {Installer.Version}";
|
||||||
|
|
||||||
|
var installed = Installer.InstalledDir;
|
||||||
|
DirBox.Text = installed ?? Installer.DefaultDir;
|
||||||
|
DesktopLinkBox.IsChecked = installed is null || Installer.DesktopLinkExists;
|
||||||
|
|
||||||
|
if (installed is not null)
|
||||||
|
{
|
||||||
|
UpdateHint.Visibility = Visibility.Visible;
|
||||||
|
UpdateHintText.Text = Installer.InstalledVersion == Installer.Version
|
||||||
|
? $"Git Manager GUI {Installer.Version} ist bereits installiert und wird neu installiert. Zugangsdaten und Einstellungen bleiben erhalten."
|
||||||
|
: $"Die vorhandene Installation{(Installer.InstalledVersion is { } v ? $" (Version {v})" : "")} wird durch Version {Installer.Version} ersetzt. Zugangsdaten und Einstellungen bleiben erhalten.";
|
||||||
|
}
|
||||||
|
if (Installer.IsRunning(installed)) RunningHint.Visibility = Visibility.Visible;
|
||||||
|
|
||||||
|
switch (mode)
|
||||||
|
{
|
||||||
|
case SetupMode.Uninstall:
|
||||||
|
Title = WindowTitle.Text = "Git Manager GUI deinstallieren";
|
||||||
|
Steps.Visibility = Visibility.Collapsed;
|
||||||
|
ShowOnly(PageUninstall);
|
||||||
|
NextButton.Content = "Deinstallieren";
|
||||||
|
BackButton.Visibility = Visibility.Collapsed;
|
||||||
|
LoadDataInfo();
|
||||||
|
break;
|
||||||
|
case SetupMode.Update:
|
||||||
|
// aus der App heraus: ohne Rückfragen direkt aktualisieren und danach wieder starten
|
||||||
|
Title = WindowTitle.Text = "Git Manager GUI wird aktualisiert";
|
||||||
|
Steps.Visibility = Visibility.Collapsed;
|
||||||
|
Loaded += async (_, _) => await RunInstall(DirBox.Text, DesktopLinkBox.IsChecked == true, update: true);
|
||||||
|
ShowOnly(PageProgress);
|
||||||
|
ProgressTitle.Text = "Git Manager GUI wird aktualisiert …";
|
||||||
|
BackButton.Visibility = Visibility.Hidden;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ShowPage(0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Seiten ----------
|
||||||
|
|
||||||
|
private UIElement? _shownPage;
|
||||||
|
|
||||||
|
/// <summary>Seite wechseln – mit kurzem Einblenden von rechts.</summary>
|
||||||
|
private void ShowOnly(UIElement page)
|
||||||
|
{
|
||||||
|
foreach (var p in new UIElement[] { PageWelcome, PageOptions, PageProgress, PageUninstall })
|
||||||
|
p.Visibility = p == page ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
if (page == _shownPage) return;
|
||||||
|
_shownPage = page;
|
||||||
|
|
||||||
|
var ease = new CubicEase { EasingMode = EasingMode.EaseOut };
|
||||||
|
var slide = new TranslateTransform(18, 0);
|
||||||
|
page.RenderTransform = slide;
|
||||||
|
slide.BeginAnimation(TranslateTransform.XProperty, new DoubleAnimation(18, 0, TimeSpan.FromMilliseconds(260)) { EasingFunction = ease });
|
||||||
|
page.BeginAnimation(OpacityProperty, new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(220)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Farben der Schritt-Anzeige in der (immer dunklen) Seitenleiste – Cyan wie in der App
|
||||||
|
private static readonly Brush StepDoneFill = Frozen(Color.FromRgb(0x58, 0xD5, 0xFF));
|
||||||
|
private static readonly Brush StepDoneGlyph = Frozen(Color.FromRgb(0x04, 0x10, 0x1F));
|
||||||
|
private static readonly Brush StepCurrentFill = Brushes.White;
|
||||||
|
private static readonly Brush StepCurrentText = Frozen(Color.FromRgb(0x13, 0x30, 0x5C));
|
||||||
|
private static readonly Brush StepUpcoming = Frozen(Color.FromRgb(0x70, 0x83, 0xA4));
|
||||||
|
private static readonly Brush StepUpcomingStroke = Frozen(Color.FromRgb(0x3A, 0x4E, 0x72));
|
||||||
|
private static readonly Brush StepDoneText = Frozen(Color.FromRgb(0xAE, 0xBD, 0xD8));
|
||||||
|
private static readonly Brush StepLineIdle = Frozen(Color.FromRgb(0x24, 0x36, 0x58));
|
||||||
|
|
||||||
|
private static Brush Frozen(Color c)
|
||||||
|
{
|
||||||
|
var b = new SolidColorBrush(c);
|
||||||
|
b.Freeze();
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateSteps(int page)
|
||||||
|
{
|
||||||
|
var dots = new[] { Dot0, Dot1, Dot2 };
|
||||||
|
var nums = new[] { Num0, Num1, Num2 };
|
||||||
|
var labels = new[] { Label0, Label1, Label2 };
|
||||||
|
var lines = new[] { Line0, Line1 };
|
||||||
|
var iconFont = (FontFamily)FindResource("IconFont");
|
||||||
|
var uiFont = (FontFamily)FindResource("UiFont");
|
||||||
|
|
||||||
|
for (int i = 0; i < dots.Length; i++)
|
||||||
|
{
|
||||||
|
bool done = i < page || (_done && i == page);
|
||||||
|
bool current = i == page && !done;
|
||||||
|
dots[i].Background = done ? StepDoneFill : current ? StepCurrentFill : Brushes.Transparent;
|
||||||
|
dots[i].BorderBrush = done ? StepDoneFill : current ? StepCurrentFill : StepUpcomingStroke;
|
||||||
|
nums[i].Text = done ? "" : (i + 1).ToString();
|
||||||
|
nums[i].FontFamily = done ? iconFont : uiFont;
|
||||||
|
nums[i].FontSize = done ? 11 : 12;
|
||||||
|
nums[i].Foreground = done ? StepDoneGlyph : current ? StepCurrentText : StepUpcoming;
|
||||||
|
labels[i].Foreground = current ? Brushes.White : done ? StepDoneText : StepUpcoming;
|
||||||
|
labels[i].FontWeight = current ? FontWeights.SemiBold : FontWeights.Normal;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < lines.Length; i++)
|
||||||
|
lines[i].Background = i < page ? StepDoneFill : StepLineIdle;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowPage(int page)
|
||||||
|
{
|
||||||
|
_page = page;
|
||||||
|
ShowOnly(page switch { 0 => PageWelcome, PageNumOptions => PageOptions, _ => PageProgress });
|
||||||
|
UpdateSteps(page);
|
||||||
|
|
||||||
|
BackButton.Visibility = page == PageNumOptions ? Visibility.Visible : Visibility.Hidden;
|
||||||
|
NextButton.Content = page switch
|
||||||
|
{
|
||||||
|
PageNumOptions => "Installieren",
|
||||||
|
PageNumProgress => "Fertigstellen",
|
||||||
|
_ => "Weiter",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Back_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (!_busy && _page > 0) ShowPage(_page - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void Next_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_busy) return;
|
||||||
|
|
||||||
|
if (_mode == SetupMode.Uninstall)
|
||||||
|
{
|
||||||
|
if (_done) { Close(); return; }
|
||||||
|
await RunUninstall();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_mode == SetupMode.Update)
|
||||||
|
{
|
||||||
|
Close(); // nur nach einem Fehler erreichbar
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (_page)
|
||||||
|
{
|
||||||
|
case 0: ShowPage(PageNumOptions); break;
|
||||||
|
case PageNumOptions:
|
||||||
|
var dir = DirBox.Text.Trim();
|
||||||
|
if (!IsFullPath(dir) || dir.IndexOfAny(Path.GetInvalidPathChars()) >= 0)
|
||||||
|
{
|
||||||
|
await Frage("Ungültiger Ordner", "Bitte einen vollständigen Ordner angeben, z. B. C:\\Programme\\Git Manager GUI.", "OK");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (dir.TrimEnd('\\', '/').Length > Installer.MaxDirLength)
|
||||||
|
{
|
||||||
|
await Frage("Ordnerpfad zu lang",
|
||||||
|
$"Der Pfad hat {dir.TrimEnd('\\', '/').Length} Zeichen, Windows erlaubt hier höchstens {Installer.MaxDirLength}. " +
|
||||||
|
"Bitte einen kürzeren Ordner wählen.", "OK");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await RunInstall(dir, DesktopLinkBox.IsChecked == true, update: false);
|
||||||
|
break;
|
||||||
|
case PageNumProgress:
|
||||||
|
// Start im Hintergrund: Process.Start kann blockieren, solange Windows
|
||||||
|
// die EXE prüft – das Fenster darf dabei nie einfrieren
|
||||||
|
if (LaunchBox.IsChecked == true)
|
||||||
|
{
|
||||||
|
_busy = true;
|
||||||
|
NextButton.IsEnabled = false;
|
||||||
|
NextButton.Content = "Wird gestartet …";
|
||||||
|
await Task.Run(StartApp);
|
||||||
|
_busy = false;
|
||||||
|
}
|
||||||
|
Close();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ChangeDir_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
using var dlg = new System.Windows.Forms.FolderBrowserDialog
|
||||||
|
{
|
||||||
|
Description = "Installationsordner wählen",
|
||||||
|
SelectedPath = Directory.Exists(DirBox.Text) ? DirBox.Text : Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||||
|
ShowNewFolderButton = true,
|
||||||
|
};
|
||||||
|
if (dlg.ShowDialog() != System.Windows.Forms.DialogResult.OK) return;
|
||||||
|
var chosen = dlg.SelectedPath;
|
||||||
|
// wie bei üblichen Installern: Unterordner "Git Manager GUI" anhängen
|
||||||
|
DirBox.Text = Path.GetFileName(chosen.TrimEnd('\\')).Equals(Installer.AppName, StringComparison.OrdinalIgnoreCase)
|
||||||
|
? chosen : Path.Combine(chosen, Installer.AppName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Installation ----------
|
||||||
|
|
||||||
|
private async Task RunInstall(string dir, bool desktop, bool update)
|
||||||
|
{
|
||||||
|
_busy = true;
|
||||||
|
if (!update) ShowPage(PageNumProgress);
|
||||||
|
SetButtons(false);
|
||||||
|
var progress = new Progress<(double Value, string Text)>(p => SetProgress(p.Value, p.Text));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Run(() => Installer.Install(dir, desktop, progress, waitForExit: update));
|
||||||
|
_installedExe = Path.Combine(dir, Installer.ExeName);
|
||||||
|
_done = true;
|
||||||
|
|
||||||
|
if (update)
|
||||||
|
{
|
||||||
|
ShowSuccess("Update abgeschlossen", $"Git Manager GUI {Installer.Version} wird gestartet …");
|
||||||
|
StartApp();
|
||||||
|
_busy = false;
|
||||||
|
var timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1.5) };
|
||||||
|
timer.Tick += (_, _) => { timer.Stop(); Close(); };
|
||||||
|
timer.Start();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShowSuccess("Alles bereit!", "Git Manager GUI ist installiert. Du findest es im Startmenü" + (desktop ? " und auf dem Desktop." : "."));
|
||||||
|
DonePanel.Visibility = Visibility.Visible;
|
||||||
|
ShowPage(PageNumProgress);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ProgressTitle.Text = update ? "Update fehlgeschlagen" : "Installation fehlgeschlagen";
|
||||||
|
ProgressBarPanel.Visibility = Visibility.Collapsed;
|
||||||
|
ProgressText.Text = ex.Message;
|
||||||
|
ProgressText.Foreground = (Brush)FindResource("DangerBrush");
|
||||||
|
NextButton.Content = "Schließen";
|
||||||
|
_installedExe = null;
|
||||||
|
}
|
||||||
|
_busy = false;
|
||||||
|
NextButton.IsEnabled = true;
|
||||||
|
CloseButton.IsEnabled = true;
|
||||||
|
BackButton.Visibility = Visibility.Hidden;
|
||||||
|
CancelButton.Visibility = Visibility.Collapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Fortschritt ausblenden und den Haken mit kurzem "Aufploppen" zeigen.</summary>
|
||||||
|
private void ShowSuccess(string title, string text)
|
||||||
|
{
|
||||||
|
ProgressBarPanel.Visibility = Visibility.Collapsed;
|
||||||
|
ProgressTitle.Text = title;
|
||||||
|
ProgressText.Text = text;
|
||||||
|
SuccessIcon.Visibility = Visibility.Visible;
|
||||||
|
var pop = new DoubleAnimation(0.4, 1, TimeSpan.FromMilliseconds(420)) { EasingFunction = new BackEase { EasingMode = EasingMode.EaseOut, Amplitude = 0.5 } };
|
||||||
|
SuccessScale.BeginAnimation(ScaleTransform.ScaleXProperty, pop);
|
||||||
|
SuccessScale.BeginAnimation(ScaleTransform.ScaleYProperty, pop);
|
||||||
|
SuccessIcon.BeginAnimation(OpacityProperty, new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(200)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StartApp()
|
||||||
|
{
|
||||||
|
if (_installedExe is null || !File.Exists(_installedExe)) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Process.Start(new ProcessStartInfo(_installedExe)
|
||||||
|
{
|
||||||
|
UseShellExecute = true,
|
||||||
|
WorkingDirectory = Path.GetDirectoryName(_installedExe)!,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Deinstallation ----------
|
||||||
|
|
||||||
|
private async void LoadDataInfo()
|
||||||
|
{
|
||||||
|
var root = Installer.DataDir;
|
||||||
|
if (!Directory.Exists(root))
|
||||||
|
{
|
||||||
|
DataInfo.Text = "Es wurden keine App-Daten gefunden.";
|
||||||
|
RemoveDataBox.IsEnabled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DataInfo.Text = $"Sie liegen in {root}. Größe wird berechnet …";
|
||||||
|
var size = await Task.Run(Installer.DataSize);
|
||||||
|
DataInfo.Text = $"Sie liegen in {root} ({Installer.FormatSize(size)}) und stehen wieder bereit, falls du Git Manager GUI später neu installierst.";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveDataBox_Click(object sender, RoutedEventArgs e) =>
|
||||||
|
RemoveDataWarning.Visibility = RemoveDataBox.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
|
||||||
|
private async Task RunUninstall()
|
||||||
|
{
|
||||||
|
_busy = true;
|
||||||
|
bool removeData = RemoveDataBox.IsChecked == true;
|
||||||
|
ShowOnly(PageProgress);
|
||||||
|
ProgressTitle.Text = "Git Manager GUI wird entfernt …";
|
||||||
|
LaunchBox.Visibility = Visibility.Collapsed;
|
||||||
|
SetButtons(false);
|
||||||
|
var progress = new Progress<(double Value, string Text)>(p => SetProgress(p.Value, p.Text));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Run(() => Installer.Uninstall(removeData, progress));
|
||||||
|
ShowSuccess("Git Manager GUI wurde entfernt", removeData
|
||||||
|
? "Programm, Zugangsdaten und Einstellungen wurden entfernt (die Daten liegen im Papierkorb)."
|
||||||
|
: "Deine Zugangsdaten und Einstellungen liegen weiterhin in " + Installer.DataDir + " – falls du Git Manager GUI später wieder installierst.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ProgressTitle.Text = "Deinstallation fehlgeschlagen";
|
||||||
|
ProgressBarPanel.Visibility = Visibility.Collapsed;
|
||||||
|
ProgressText.Text = ex.Message;
|
||||||
|
}
|
||||||
|
_done = true;
|
||||||
|
_busy = false;
|
||||||
|
CloseButton.IsEnabled = true;
|
||||||
|
NextButton.Content = "Schließen";
|
||||||
|
NextButton.IsEnabled = true;
|
||||||
|
CancelButton.Visibility = Visibility.Collapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Helfer ----------
|
||||||
|
|
||||||
|
private void SetButtons(bool enabled)
|
||||||
|
{
|
||||||
|
NextButton.IsEnabled = enabled;
|
||||||
|
BackButton.IsEnabled = enabled;
|
||||||
|
CancelButton.IsEnabled = enabled;
|
||||||
|
CloseButton.IsEnabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetProgress(double value, string text)
|
||||||
|
{
|
||||||
|
if (_done) return;
|
||||||
|
ProgressText.Text = text;
|
||||||
|
PercentText.Text = $"{(int)Math.Round(value * 100)} %";
|
||||||
|
_progress = value;
|
||||||
|
AnimateProgress();
|
||||||
|
}
|
||||||
|
|
||||||
|
private double _progress;
|
||||||
|
|
||||||
|
private void AnimateProgress()
|
||||||
|
{
|
||||||
|
var target = Math.Max(0, ProgressTrack.ActualWidth * _progress);
|
||||||
|
ProgressFill.BeginAnimation(WidthProperty,
|
||||||
|
new DoubleAnimation(target, TimeSpan.FromMilliseconds(250)) { EasingFunction = new CubicEase() });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kommt der erste Wert, bevor die Seite fertig angeordnet ist (Breite 0), nachträglich richtig setzen
|
||||||
|
private void ProgressTrack_SizeChanged(object sender, SizeChangedEventArgs e) => AnimateProgress();
|
||||||
|
|
||||||
|
private static bool IsFullPath(string p) =>
|
||||||
|
p.Length >= 3 && char.IsLetter(p[0]) && p[1] == ':' && (p[2] == '\\' || p[2] == '/');
|
||||||
|
|
||||||
|
private void Minimize_Click(object sender, RoutedEventArgs e) => WindowState = WindowState.Minimized;
|
||||||
|
|
||||||
|
private async void Cancel_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_busy || _dialog is not null) return;
|
||||||
|
if (!_done && _mode == SetupMode.Install && _page > 0)
|
||||||
|
{
|
||||||
|
bool abbrechen = await Frage("Installation abbrechen?",
|
||||||
|
"Git Manager GUI wird nicht installiert. Du kannst das Setup später jederzeit erneut starten.",
|
||||||
|
"Ja, abbrechen", "Weiter installieren", "\uE897");
|
||||||
|
if (!abbrechen) return;
|
||||||
|
}
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- eigene Rückfragen im Setup-Look ----------
|
||||||
|
|
||||||
|
private TaskCompletionSource<bool>? _dialog;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Zeigt eine Rückfrage über dem Fenster. Mit <paramref name="zurueck"/>: zwei Knöpfe –
|
||||||
|
/// der hervorgehobene ist der sichere Weg zurück (liefert false), der andere bestätigt (true).
|
||||||
|
/// Ohne: nur ein Hinweis mit einem Knopf (liefert true).
|
||||||
|
/// </summary>
|
||||||
|
private Task<bool> Frage(string titel, string text, string bestaetigen, string? zurueck = null, string symbol = "\uE7BA")
|
||||||
|
{
|
||||||
|
_dialog?.TrySetResult(false);
|
||||||
|
_dialog = new TaskCompletionSource<bool>();
|
||||||
|
DialogTitle.Text = titel;
|
||||||
|
DialogText.Text = text;
|
||||||
|
DialogIcon.Text = symbol;
|
||||||
|
if (zurueck is null)
|
||||||
|
{
|
||||||
|
DialogSecondary.Visibility = Visibility.Collapsed;
|
||||||
|
DialogPrimary.Content = bestaetigen;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DialogSecondary.Visibility = Visibility.Visible;
|
||||||
|
DialogSecondary.Content = bestaetigen;
|
||||||
|
DialogPrimary.Content = zurueck;
|
||||||
|
}
|
||||||
|
|
||||||
|
DialogOverlay.Visibility = Visibility.Visible;
|
||||||
|
var ein = new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(160));
|
||||||
|
DialogOverlay.BeginAnimation(OpacityProperty, ein);
|
||||||
|
var groesse = new DoubleAnimation(0.94, 1, TimeSpan.FromMilliseconds(220)) { EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut } };
|
||||||
|
DialogScale.BeginAnimation(ScaleTransform.ScaleXProperty, groesse);
|
||||||
|
DialogScale.BeginAnimation(ScaleTransform.ScaleYProperty, groesse);
|
||||||
|
DialogPrimary.Focus();
|
||||||
|
return _dialog.Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SchliesseFrage(bool ergebnis)
|
||||||
|
{
|
||||||
|
if (_dialog is null) return;
|
||||||
|
var tcs = _dialog;
|
||||||
|
_dialog = null;
|
||||||
|
DialogOverlay.Visibility = Visibility.Collapsed;
|
||||||
|
tcs.TrySetResult(ergebnis);
|
||||||
|
}
|
||||||
|
|
||||||
|
// hervorgehobener Knopf: bei Rückfragen "zurück" (false), beim reinen Hinweis "OK" (true)
|
||||||
|
private void DialogPrimary_Click(object sender, RoutedEventArgs e) =>
|
||||||
|
SchliesseFrage(DialogSecondary.Visibility != Visibility.Visible);
|
||||||
|
|
||||||
|
private void DialogSecondary_Click(object sender, RoutedEventArgs e) => SchliesseFrage(true);
|
||||||
|
|
||||||
|
protected override void OnPreviewKeyDown(System.Windows.Input.KeyEventArgs e)
|
||||||
|
{
|
||||||
|
// solange die Rückfrage offen ist: Enter = hervorgehobener Knopf, Esc = zurück
|
||||||
|
if (_dialog is not null)
|
||||||
|
{
|
||||||
|
if (e.Key == System.Windows.Input.Key.Escape) { SchliesseFrage(DialogSecondary.Visibility != Visibility.Visible); e.Handled = true; return; }
|
||||||
|
if (e.Key == System.Windows.Input.Key.Enter) { DialogPrimary_Click(this, new RoutedEventArgs()); e.Handled = true; return; }
|
||||||
|
}
|
||||||
|
base.OnPreviewKeyDown(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnClosing(System.ComponentModel.CancelEventArgs e)
|
||||||
|
{
|
||||||
|
if (_busy) e.Cancel = true; // während der Installation nicht schließen
|
||||||
|
_dialog?.TrySetResult(false);
|
||||||
|
base.OnClosing(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.IO.Compression;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace GitManagerSetup;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Die App liegt als ZIP hinten an der Setup-.exe angehängt:
|
||||||
|
/// [Setup.exe][ZIP][8 Byte: ZIP-Länge][8 Byte: "GITMGRPK"]
|
||||||
|
/// Windows ignoriert angehängte Daten beim Starten einer .exe.
|
||||||
|
/// </summary>
|
||||||
|
public static class Payload
|
||||||
|
{
|
||||||
|
private static readonly byte[] Magic = Encoding.ASCII.GetBytes("GITMGRPK");
|
||||||
|
private const int FooterSize = 16;
|
||||||
|
|
||||||
|
private static (long Offset, long Length)? _info;
|
||||||
|
private static bool _checked;
|
||||||
|
|
||||||
|
/// <summary>Position und Länge des ZIPs in dieser .exe, oder null (z. B. beim Deinstallationsprogramm).</summary>
|
||||||
|
public static (long Offset, long Length)? Info
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_checked) return _info;
|
||||||
|
_checked = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var f = File.OpenRead(Installer.SelfPath);
|
||||||
|
if (f.Length < FooterSize) return null;
|
||||||
|
f.Seek(-FooterSize, SeekOrigin.End);
|
||||||
|
var footer = new byte[FooterSize];
|
||||||
|
if (f.Read(footer, 0, FooterSize) != FooterSize) return null;
|
||||||
|
for (int i = 0; i < Magic.Length; i++)
|
||||||
|
if (footer[8 + i] != Magic[i]) return null;
|
||||||
|
long length = BitConverter.ToInt64(footer, 0);
|
||||||
|
long offset = f.Length - FooterSize - length;
|
||||||
|
if (length <= 0 || offset <= 0) return null;
|
||||||
|
_info = (offset, length);
|
||||||
|
}
|
||||||
|
catch { _info = null; }
|
||||||
|
return _info;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool Exists => Info is not null;
|
||||||
|
|
||||||
|
/// <summary>Länge des eigentlichen Setup-Programms ohne Anhang.</summary>
|
||||||
|
public static long StubLength => Info?.Offset ?? new FileInfo(Installer.SelfPath).Length;
|
||||||
|
|
||||||
|
public static ZipArchive OpenArchive()
|
||||||
|
{
|
||||||
|
var info = Info ?? throw new InvalidOperationException("Die Installationsdaten fehlen in dieser Datei. Bitte das Setup neu herunterladen.");
|
||||||
|
var file = File.OpenRead(Installer.SelfPath);
|
||||||
|
return new ZipArchive(new SubStream(file, info.Offset, info.Length), ZipArchiveMode.Read, leaveOpen: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Nur-Lese-Ausschnitt einer Datei, damit ZipArchive den Anhang wie eine eigene Datei sieht.</summary>
|
||||||
|
private sealed class SubStream(Stream inner, long start, long length) : Stream
|
||||||
|
{
|
||||||
|
private long _pos;
|
||||||
|
|
||||||
|
public override bool CanRead => true;
|
||||||
|
public override bool CanSeek => true;
|
||||||
|
public override bool CanWrite => false;
|
||||||
|
public override long Length => length;
|
||||||
|
|
||||||
|
public override long Position
|
||||||
|
{
|
||||||
|
get => _pos;
|
||||||
|
set => _pos = Math.Max(0, Math.Min(value, length));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int Read(byte[] buffer, int offset, int count)
|
||||||
|
{
|
||||||
|
long left = length - _pos;
|
||||||
|
if (left <= 0) return 0;
|
||||||
|
if (count > left) count = (int)left;
|
||||||
|
inner.Position = start + _pos;
|
||||||
|
int n = inner.Read(buffer, offset, count);
|
||||||
|
_pos += n;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override long Seek(long offset, SeekOrigin origin)
|
||||||
|
{
|
||||||
|
Position = origin switch
|
||||||
|
{
|
||||||
|
SeekOrigin.Begin => offset,
|
||||||
|
SeekOrigin.Current => _pos + offset,
|
||||||
|
_ => length + offset,
|
||||||
|
};
|
||||||
|
return _pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Flush() { }
|
||||||
|
public override void SetLength(long value) => throw new NotSupportedException();
|
||||||
|
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (disposing) inner.Dispose();
|
||||||
|
base.Dispose(disposing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Runtime.InteropServices.ComTypes;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace GitManagerSetup;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verknüpfungen (.lnk) über die Shell-Schnittstellen – inklusive AppUserModelID, damit
|
||||||
|
/// Taskleiste und Windows-Benachrichtigungen dem Git Manager wie bisher zuordnen (wie beim alten NSIS-Setup).
|
||||||
|
/// </summary>
|
||||||
|
public static class Shortcut
|
||||||
|
{
|
||||||
|
public static void Create(string link, string target, string description, string appUserModelId)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(link)!);
|
||||||
|
var shellLink = (IShellLinkW)new CShellLink();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
shellLink.SetPath(target);
|
||||||
|
shellLink.SetWorkingDirectory(Path.GetDirectoryName(target)!);
|
||||||
|
shellLink.SetIconLocation(target, 0);
|
||||||
|
shellLink.SetDescription(description);
|
||||||
|
|
||||||
|
var store = (IPropertyStore)shellLink;
|
||||||
|
var key = new PropertyKey(new Guid("9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3"), 5); // PKEY_AppUserModel_ID
|
||||||
|
var value = new PropVariant { vt = 31 /* VT_LPWSTR */, p = Marshal.StringToCoTaskMemUni(appUserModelId) };
|
||||||
|
try
|
||||||
|
{
|
||||||
|
store.SetValue(ref key, ref value);
|
||||||
|
store.Commit();
|
||||||
|
}
|
||||||
|
finally { Marshal.FreeCoTaskMem(value.p); }
|
||||||
|
|
||||||
|
((IPersistFile)shellLink).Save(link, true);
|
||||||
|
}
|
||||||
|
finally { Marshal.ReleaseComObject(shellLink); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Ziel einer vorhandenen Verknüpfung, oder null.</summary>
|
||||||
|
public static string? Target(string link)
|
||||||
|
{
|
||||||
|
if (!File.Exists(link)) return null;
|
||||||
|
var shellLink = (IShellLinkW)new CShellLink();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
((IPersistFile)shellLink).Load(link, 0);
|
||||||
|
var sb = new StringBuilder(1024);
|
||||||
|
shellLink.GetPath(sb, sb.Capacity, IntPtr.Zero, 0);
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
catch { return null; }
|
||||||
|
finally { Marshal.ReleaseComObject(shellLink); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- COM ----------
|
||||||
|
|
||||||
|
[ComImport, Guid("00021401-0000-0000-C000-000000000046")]
|
||||||
|
private class CShellLink { }
|
||||||
|
|
||||||
|
[ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("000214F9-0000-0000-C000-000000000046")]
|
||||||
|
private interface IShellLinkW
|
||||||
|
{
|
||||||
|
void GetPath([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszFile, int cchMaxPath, IntPtr pfd, uint fFlags);
|
||||||
|
void GetIDList(out IntPtr ppidl);
|
||||||
|
void SetIDList(IntPtr pidl);
|
||||||
|
void GetDescription([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszName, int cchMaxName);
|
||||||
|
void SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName);
|
||||||
|
void GetWorkingDirectory([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszDir, int cchMaxPath);
|
||||||
|
void SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir);
|
||||||
|
void GetArguments([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszArgs, int cchMaxPath);
|
||||||
|
void SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs);
|
||||||
|
void GetHotkey(out short pwHotkey);
|
||||||
|
void SetHotkey(short wHotkey);
|
||||||
|
void GetShowCmd(out int piShowCmd);
|
||||||
|
void SetShowCmd(int iShowCmd);
|
||||||
|
void GetIconLocation([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszIconPath, int cchIconPath, out int piIcon);
|
||||||
|
void SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon);
|
||||||
|
void SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel, uint dwReserved);
|
||||||
|
void Resolve(IntPtr hwnd, uint fFlags);
|
||||||
|
void SetPath([MarshalAs(UnmanagedType.LPWStr)] string pszFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
[ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99")]
|
||||||
|
private interface IPropertyStore
|
||||||
|
{
|
||||||
|
void GetCount(out uint cProps);
|
||||||
|
void GetAt(uint iProp, out PropertyKey pkey);
|
||||||
|
void GetValue(ref PropertyKey key, out PropVariant pv);
|
||||||
|
void SetValue(ref PropertyKey key, ref PropVariant pv);
|
||||||
|
void Commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential, Pack = 4)]
|
||||||
|
private struct PropertyKey(Guid fmtid, uint pid)
|
||||||
|
{
|
||||||
|
public Guid fmtid = fmtid;
|
||||||
|
public uint pid = pid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PROPVARIANT: 16 Byte (32 Bit) bzw. 24 Byte (64 Bit)
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct PropVariant
|
||||||
|
{
|
||||||
|
public ushort vt;
|
||||||
|
public ushort r1, r2, r3;
|
||||||
|
public IntPtr p;
|
||||||
|
public IntPtr p2;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
<SolidColorBrush x:Key="BaseBrush" Color="#202020"/>
|
||||||
|
<SolidColorBrush x:Key="LayerBrush" Color="#282828"/>
|
||||||
|
<SolidColorBrush x:Key="LayerStrokeBrush" Color="#1A1A1A"/>
|
||||||
|
<SolidColorBrush x:Key="SubtleHoverBrush" Color="#0FFFFFFF"/>
|
||||||
|
<SolidColorBrush x:Key="SubtlePressedBrush" Color="#0AFFFFFF"/>
|
||||||
|
<SolidColorBrush x:Key="SubtleSelectedBrush" Color="#14FFFFFF"/>
|
||||||
|
<SolidColorBrush x:Key="ControlFillBrush" Color="#2D2D2D"/>
|
||||||
|
<SolidColorBrush x:Key="ControlHoverBrush" Color="#323232"/>
|
||||||
|
<SolidColorBrush x:Key="ControlPressedBrush" Color="#272727"/>
|
||||||
|
<SolidColorBrush x:Key="ControlStrokeBrush" Color="#353535"/>
|
||||||
|
<SolidColorBrush x:Key="ControlStrongStrokeBrush" Color="#8A8A8A"/>
|
||||||
|
<SolidColorBrush x:Key="InputFocusBrush" Color="#1F1F1F"/>
|
||||||
|
<SolidColorBrush x:Key="TextBrush" Color="#FFFFFF"/>
|
||||||
|
<SolidColorBrush x:Key="TextSecondaryBrush" Color="#C8C8C8"/>
|
||||||
|
<SolidColorBrush x:Key="TextTertiaryBrush" Color="#9A9A9A"/>
|
||||||
|
<SolidColorBrush x:Key="FlyoutBrush" Color="#2C2C2C"/>
|
||||||
|
<SolidColorBrush x:Key="FlyoutStrokeBrush" Color="#404040"/>
|
||||||
|
<SolidColorBrush x:Key="DialogFooterBrush" Color="#202020"/>
|
||||||
|
<SolidColorBrush x:Key="ScrollThumbBrush" Color="#9F9F9F"/>
|
||||||
|
<SolidColorBrush x:Key="OverlayBrush" Color="#66000000"/>
|
||||||
|
<SolidColorBrush x:Key="SuccessBrush" Color="#4CAF50"/>
|
||||||
|
<SolidColorBrush x:Key="DangerBrush" Color="#FF99A4"/>
|
||||||
|
<SolidColorBrush x:Key="FenceBrush" Color="#F21C1C1C"/>
|
||||||
|
<Color x:Key="ShadowColor">#000000</Color>
|
||||||
|
</ResourceDictionary>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
<SolidColorBrush x:Key="BaseBrush" Color="#F3F3F3"/>
|
||||||
|
<SolidColorBrush x:Key="LayerBrush" Color="#FAFAFA"/>
|
||||||
|
<SolidColorBrush x:Key="LayerStrokeBrush" Color="#E5E5E5"/>
|
||||||
|
<SolidColorBrush x:Key="SubtleHoverBrush" Color="#0A000000"/>
|
||||||
|
<SolidColorBrush x:Key="SubtlePressedBrush" Color="#06000000"/>
|
||||||
|
<SolidColorBrush x:Key="SubtleSelectedBrush" Color="#0F000000"/>
|
||||||
|
<SolidColorBrush x:Key="ControlFillBrush" Color="#FDFDFD"/>
|
||||||
|
<SolidColorBrush x:Key="ControlHoverBrush" Color="#F6F6F6"/>
|
||||||
|
<SolidColorBrush x:Key="ControlPressedBrush" Color="#F2F2F2"/>
|
||||||
|
<SolidColorBrush x:Key="ControlStrokeBrush" Color="#E3E3E3"/>
|
||||||
|
<SolidColorBrush x:Key="ControlStrongStrokeBrush" Color="#C4C4C4"/>
|
||||||
|
<SolidColorBrush x:Key="InputFocusBrush" Color="#FFFFFF"/>
|
||||||
|
<SolidColorBrush x:Key="TextBrush" Color="#1B1B1B"/>
|
||||||
|
<SolidColorBrush x:Key="TextSecondaryBrush" Color="#5D5D5D"/>
|
||||||
|
<SolidColorBrush x:Key="TextTertiaryBrush" Color="#8B8B8B"/>
|
||||||
|
<SolidColorBrush x:Key="FlyoutBrush" Color="#F9F9F9"/>
|
||||||
|
<SolidColorBrush x:Key="FlyoutStrokeBrush" Color="#DCDCDC"/>
|
||||||
|
<SolidColorBrush x:Key="DialogFooterBrush" Color="#F3F3F3"/>
|
||||||
|
<SolidColorBrush x:Key="ScrollThumbBrush" Color="#8A8A8A"/>
|
||||||
|
<SolidColorBrush x:Key="OverlayBrush" Color="#4D000000"/>
|
||||||
|
<SolidColorBrush x:Key="SuccessBrush" Color="#0F7B0F"/>
|
||||||
|
<SolidColorBrush x:Key="DangerBrush" Color="#C42B1C"/>
|
||||||
|
<SolidColorBrush x:Key="FenceBrush" Color="#F5F7F7F7"/>
|
||||||
|
<Color x:Key="ShadowColor">#000000</Color>
|
||||||
|
</ResourceDictionary>
|
||||||
@@ -0,0 +1,651 @@
|
|||||||
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
|
||||||
|
<!-- Schriften: Windows 11 nutzt "Segoe UI Variable" / "Segoe Fluent Icons", Windows 10 fällt auf Segoe UI / MDL2 zurück -->
|
||||||
|
<FontFamily x:Key="UiFont">Segoe UI Variable Text, Segoe UI</FontFamily>
|
||||||
|
<FontFamily x:Key="DisplayFont">Segoe UI Variable Display, Segoe UI</FontFamily>
|
||||||
|
<FontFamily x:Key="IconFont">Segoe Fluent Icons, Segoe MDL2 Assets</FontFamily>
|
||||||
|
|
||||||
|
<DropShadowEffect x:Key="FlyoutShadow" BlurRadius="24" ShadowDepth="6" Direction="270" Opacity="0.18"
|
||||||
|
Color="Black"/>
|
||||||
|
<DropShadowEffect x:Key="DialogShadow" BlurRadius="40" ShadowDepth="12" Direction="270" Opacity="0.28"
|
||||||
|
Color="Black"/>
|
||||||
|
|
||||||
|
<!-- ===== Buttons ===== -->
|
||||||
|
<Style TargetType="Button">
|
||||||
|
<Setter Property="FontFamily" Value="{DynamicResource UiFont}"/>
|
||||||
|
<Setter Property="FontSize" Value="14"/>
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||||
|
<Setter Property="Background" Value="{DynamicResource ControlFillBrush}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource ControlStrokeBrush}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="Padding" Value="14,5"/>
|
||||||
|
<Setter Property="MinHeight" Value="32"/>
|
||||||
|
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Button">
|
||||||
|
<Border x:Name="Bd" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}"
|
||||||
|
BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="4" Padding="{TemplateBinding Padding}"
|
||||||
|
SnapsToDevicePixels="True">
|
||||||
|
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||||
|
VerticalAlignment="Center" RecognizesAccessKey="True"/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource ControlHoverBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsPressed" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource ControlPressedBrush}"/>
|
||||||
|
<Setter Property="Opacity" Value="0.85"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsKeyboardFocused" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource TextBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter Property="Opacity" Value="0.4"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="AccentButton" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource OnAccentBrush}"/>
|
||||||
|
<Setter Property="Background" Value="{DynamicResource AccentBrush}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource AccentBrush}"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Button">
|
||||||
|
<Border x:Name="Bd" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}"
|
||||||
|
BorderThickness="1" CornerRadius="4" Padding="{TemplateBinding Padding}" SnapsToDevicePixels="True">
|
||||||
|
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||||
|
VerticalAlignment="Center" RecognizesAccessKey="True"/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource AccentHoverBrush}"/>
|
||||||
|
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource AccentHoverBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsPressed" Value="True">
|
||||||
|
<Setter Property="Opacity" Value="0.8"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsKeyboardFocused" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource TextBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter Property="Opacity" Value="0.4"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Transparenter Button (Symbolleisten, Sidebar) -->
|
||||||
|
<Style x:Key="SubtleButton" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
<Setter Property="BorderThickness" Value="0"/>
|
||||||
|
<Setter Property="Padding" Value="9,6"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Button">
|
||||||
|
<Border x:Name="Bd" Background="{TemplateBinding Background}" CornerRadius="4" Padding="{TemplateBinding Padding}">
|
||||||
|
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtleHoverBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsPressed" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtlePressedBrush}"/>
|
||||||
|
<Setter Property="Opacity" Value="0.75"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter Property="Opacity" Value="0.35"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Titelleisten-Buttons -->
|
||||||
|
<Style x:Key="CaptionButton" TargetType="Button">
|
||||||
|
<Setter Property="Width" Value="46"/>
|
||||||
|
<Setter Property="Height" Value="32"/>
|
||||||
|
<Setter Property="FontFamily" Value="{DynamicResource IconFont}"/>
|
||||||
|
<Setter Property="FontSize" Value="10"/>
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||||
|
<Setter Property="IsTabStop" Value="False"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Button">
|
||||||
|
<Border x:Name="Bd" Background="{TemplateBinding Background}">
|
||||||
|
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtleHoverBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsPressed" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtlePressedBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="CloseCaptionButton" TargetType="Button" BasedOn="{StaticResource CaptionButton}">
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Button">
|
||||||
|
<Border x:Name="Bd" Background="Transparent">
|
||||||
|
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="#C42B1C"/>
|
||||||
|
<Setter Property="Foreground" Value="White"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsPressed" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="#C83C30"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ===== TextBox ===== -->
|
||||||
|
<Style TargetType="TextBox">
|
||||||
|
<Setter Property="FontFamily" Value="{DynamicResource UiFont}"/>
|
||||||
|
<Setter Property="FontSize" Value="14"/>
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||||
|
<Setter Property="CaretBrush" Value="{DynamicResource TextBrush}"/>
|
||||||
|
<Setter Property="SelectionBrush" Value="{DynamicResource AccentBrush}"/>
|
||||||
|
<Setter Property="Background" Value="{DynamicResource ControlFillBrush}"/>
|
||||||
|
<Setter Property="Padding" Value="10,6"/>
|
||||||
|
<Setter Property="MinHeight" Value="32"/>
|
||||||
|
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="TextBox">
|
||||||
|
<Grid>
|
||||||
|
<Border x:Name="Bd" Background="{TemplateBinding Background}" CornerRadius="4"
|
||||||
|
BorderBrush="{DynamicResource ControlStrokeBrush}" BorderThickness="1"/>
|
||||||
|
<!-- Unterstrich wie bei Windows 11 -->
|
||||||
|
<Border x:Name="Line" Height="1" VerticalAlignment="Bottom" Margin="3,0" CornerRadius="0,0,4,4"
|
||||||
|
Background="{DynamicResource ControlStrongStrokeBrush}"/>
|
||||||
|
<ScrollViewer x:Name="PART_ContentHost" Margin="{TemplateBinding Padding}" VerticalAlignment="Center"
|
||||||
|
Focusable="False" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden"/>
|
||||||
|
</Grid>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource ControlHoverBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsKeyboardFocused" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource InputFocusBrush}"/>
|
||||||
|
<Setter TargetName="Line" Property="Height" Value="2"/>
|
||||||
|
<Setter TargetName="Line" Property="Margin" Value="1,0"/>
|
||||||
|
<Setter TargetName="Line" Property="Background" Value="{DynamicResource AccentBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ===== Scrollbar (schmal, wird beim Überfahren breiter) ===== -->
|
||||||
|
<Style x:Key="ScrollThumb" TargetType="Thumb">
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Thumb">
|
||||||
|
<Border Background="{DynamicResource ScrollThumbBrush}" CornerRadius="3"/>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style TargetType="ScrollBar">
|
||||||
|
<Setter Property="Width" Value="12"/>
|
||||||
|
<Setter Property="MinWidth" Value="12"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ScrollBar">
|
||||||
|
<Grid Background="Transparent">
|
||||||
|
<Track x:Name="PART_Track" IsDirectionReversed="True">
|
||||||
|
<Track.Thumb>
|
||||||
|
<Thumb x:Name="Th" Style="{StaticResource ScrollThumb}" Width="3" HorizontalAlignment="Center" Opacity="0.7"/>
|
||||||
|
</Track.Thumb>
|
||||||
|
</Track>
|
||||||
|
</Grid>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Th" Property="Width" Value="6"/>
|
||||||
|
<Setter TargetName="Th" Property="Opacity" Value="1"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
<Style.Triggers>
|
||||||
|
<Trigger Property="Orientation" Value="Horizontal">
|
||||||
|
<Setter Property="Width" Value="Auto"/>
|
||||||
|
<Setter Property="MinWidth" Value="0"/>
|
||||||
|
<Setter Property="Height" Value="12"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ScrollBar">
|
||||||
|
<Grid Background="Transparent">
|
||||||
|
<Track x:Name="PART_Track">
|
||||||
|
<Track.Thumb>
|
||||||
|
<Thumb x:Name="Th" Style="{StaticResource ScrollThumb}" Height="3" VerticalAlignment="Center" Opacity="0.7"/>
|
||||||
|
</Track.Thumb>
|
||||||
|
</Track>
|
||||||
|
</Grid>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Th" Property="Height" Value="6"/>
|
||||||
|
<Setter TargetName="Th" Property="Opacity" Value="1"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Trigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ===== ToolTip ===== -->
|
||||||
|
<Style TargetType="ToolTip">
|
||||||
|
<Setter Property="FontFamily" Value="{DynamicResource UiFont}"/>
|
||||||
|
<Setter Property="FontSize" Value="12"/>
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||||
|
<Setter Property="HasDropShadow" Value="False"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ToolTip">
|
||||||
|
<Border Margin="8,4,8,12" Padding="9,6" CornerRadius="4" MaxWidth="320"
|
||||||
|
Background="{DynamicResource FlyoutBrush}" BorderBrush="{DynamicResource FlyoutStrokeBrush}" BorderThickness="1"
|
||||||
|
Effect="{StaticResource FlyoutShadow}">
|
||||||
|
<ContentPresenter>
|
||||||
|
<ContentPresenter.Resources>
|
||||||
|
<Style TargetType="TextBlock">
|
||||||
|
<Setter Property="TextWrapping" Value="Wrap"/>
|
||||||
|
</Style>
|
||||||
|
</ContentPresenter.Resources>
|
||||||
|
</ContentPresenter>
|
||||||
|
</Border>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ===== Kontextmenü ===== -->
|
||||||
|
<Style TargetType="ContextMenu">
|
||||||
|
<Setter Property="FontFamily" Value="{DynamicResource UiFont}"/>
|
||||||
|
<Setter Property="FontSize" Value="14"/>
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||||
|
<Setter Property="HasDropShadow" Value="False"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ContextMenu">
|
||||||
|
<Border Margin="12,4,12,16" Padding="0,4" CornerRadius="8" MinWidth="200"
|
||||||
|
Background="{DynamicResource FlyoutBrush}" BorderBrush="{DynamicResource FlyoutStrokeBrush}" BorderThickness="1"
|
||||||
|
Effect="{StaticResource FlyoutShadow}">
|
||||||
|
<ItemsPresenter KeyboardNavigation.DirectionalNavigation="Cycle"/>
|
||||||
|
</Border>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="{x:Static MenuItem.SeparatorStyleKey}" TargetType="Separator">
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Separator">
|
||||||
|
<Border Height="1" Margin="0,4" Background="{DynamicResource FlyoutStrokeBrush}"/>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style TargetType="MenuItem">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||||
|
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="MenuItem">
|
||||||
|
<Border x:Name="Bd" Background="Transparent" CornerRadius="4" Margin="4,1" Padding="11,0" Height="34">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="28"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<ContentPresenter ContentSource="Icon" VerticalAlignment="Center" HorizontalAlignment="Left"
|
||||||
|
TextElement.FontFamily="{DynamicResource IconFont}" TextElement.FontSize="16"/>
|
||||||
|
<ContentPresenter Grid.Column="1" ContentSource="Header" VerticalAlignment="Center" RecognizesAccessKey="True"/>
|
||||||
|
<TextBlock x:Name="Gesture" Grid.Column="2" Text="{TemplateBinding InputGestureText}" FontSize="12"
|
||||||
|
VerticalAlignment="Center" Margin="24,0,0,0" Foreground="{DynamicResource TextTertiaryBrush}"/>
|
||||||
|
<TextBlock x:Name="Arrow" Grid.Column="2" Text="" FontFamily="{DynamicResource IconFont}" FontSize="11"
|
||||||
|
VerticalAlignment="Center" Margin="24,0,0,0" Visibility="Collapsed"
|
||||||
|
Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||||
|
<Popup x:Name="PART_Popup" Placement="Right" HorizontalOffset="-4" VerticalOffset="-12"
|
||||||
|
IsOpen="{Binding IsSubmenuOpen, RelativeSource={RelativeSource TemplatedParent}}"
|
||||||
|
AllowsTransparency="True" Focusable="False" PopupAnimation="Fade">
|
||||||
|
<Border Margin="12,4,12,16" Padding="0,4" CornerRadius="8" MinWidth="180"
|
||||||
|
Background="{DynamicResource FlyoutBrush}" BorderBrush="{DynamicResource FlyoutStrokeBrush}"
|
||||||
|
BorderThickness="1" Effect="{StaticResource FlyoutShadow}">
|
||||||
|
<ScrollViewer MaxHeight="420" VerticalScrollBarVisibility="Auto">
|
||||||
|
<ItemsPresenter KeyboardNavigation.DirectionalNavigation="Cycle"/>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Border>
|
||||||
|
</Popup>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsHighlighted" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtleHoverBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="Role" Value="SubmenuHeader">
|
||||||
|
<Setter TargetName="Arrow" Property="Visibility" Value="Visible"/>
|
||||||
|
<Setter TargetName="Gesture" Property="Visibility" Value="Collapsed"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter Property="Opacity" Value="0.4"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ===== Listen ===== -->
|
||||||
|
<Style TargetType="ListBox">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
<Setter Property="BorderThickness" Value="0"/>
|
||||||
|
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled"/>
|
||||||
|
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ListBox">
|
||||||
|
<ScrollViewer Focusable="False" Padding="{TemplateBinding Padding}">
|
||||||
|
<ItemsPresenter/>
|
||||||
|
</ScrollViewer>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Navigationseintrag in der Seitenleiste -->
|
||||||
|
<Style x:Key="NavItem" TargetType="ListBoxItem">
|
||||||
|
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||||
|
<Setter Property="Height" Value="38"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ListBoxItem">
|
||||||
|
<Grid Margin="4,1" Background="Transparent">
|
||||||
|
<Border x:Name="Bd" CornerRadius="4" BorderThickness="1" BorderBrush="Transparent" Background="Transparent"/>
|
||||||
|
<Rectangle x:Name="Pill" Width="3" Height="16" RadiusX="1.5" RadiusY="1.5" HorizontalAlignment="Left"
|
||||||
|
Fill="{DynamicResource AccentBrush}" Visibility="Collapsed"/>
|
||||||
|
<ContentPresenter Margin="14,0,12,0" VerticalAlignment="Center"/>
|
||||||
|
<!-- Einfügelinie beim Umsortieren -->
|
||||||
|
<Border x:Name="InsertTop" Height="2" CornerRadius="1" VerticalAlignment="Top" Margin="6,-2,6,0"
|
||||||
|
Background="{DynamicResource AccentBrush}" Visibility="Collapsed"/>
|
||||||
|
<Border x:Name="InsertBottom" Height="2" CornerRadius="1" VerticalAlignment="Bottom" Margin="6,0,6,-2"
|
||||||
|
Background="{DynamicResource AccentBrush}" Visibility="Collapsed"/>
|
||||||
|
</Grid>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtleHoverBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsSelected" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtleSelectedBrush}"/>
|
||||||
|
<Setter TargetName="Pill" Property="Visibility" Value="Visible"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="Tag" Value="before">
|
||||||
|
<Setter TargetName="InsertTop" Property="Visibility" Value="Visible"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="Tag" Value="after">
|
||||||
|
<Setter TargetName="InsertBottom" Property="Visibility" Value="Visible"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="Tag" Value="drop">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource AccentSubtleBrush}"/>
|
||||||
|
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource AccentBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Icon-Kachel -->
|
||||||
|
<Style x:Key="TileItem" TargetType="ListBoxItem">
|
||||||
|
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ListBoxItem">
|
||||||
|
<Border x:Name="Bd" Margin="2" CornerRadius="6" BorderThickness="1" BorderBrush="Transparent" Background="Transparent">
|
||||||
|
<ContentPresenter/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtleHoverBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsSelected" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource AccentSubtleBrush}"/>
|
||||||
|
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource AccentSubtleStrokeBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ===== Listenansicht-Zeile ===== -->
|
||||||
|
<Style x:Key="RowItem" TargetType="ListBoxItem">
|
||||||
|
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||||
|
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ListBoxItem">
|
||||||
|
<Grid Margin="0,1">
|
||||||
|
<Border x:Name="Bd" CornerRadius="4" Background="Transparent"/>
|
||||||
|
<Rectangle x:Name="Pill" Width="3" Height="16" RadiusX="1.5" RadiusY="1.5" HorizontalAlignment="Left"
|
||||||
|
Fill="{DynamicResource AccentBrush}" Visibility="Collapsed"/>
|
||||||
|
<ContentPresenter/>
|
||||||
|
</Grid>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtleHoverBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsSelected" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource AccentSubtleBrush}"/>
|
||||||
|
<Setter TargetName="Pill" Property="Visibility" Value="Visible"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ===== Karte (Einstellungen) ===== -->
|
||||||
|
<Style x:Key="Card" TargetType="Border">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource ControlFillBrush}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource ControlStrokeBrush}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="CornerRadius" Value="6"/>
|
||||||
|
<Setter Property="Padding" Value="18,14"/>
|
||||||
|
<Setter Property="Margin" Value="0,0,0,4"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="SectionTitle" TargetType="TextBlock">
|
||||||
|
<Setter Property="FontSize" Value="14"/>
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
<Setter Property="Margin" Value="2,24,0,8"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ===== Kippschalter (Windows 11) ===== -->
|
||||||
|
<Style x:Key="ToggleSwitch" TargetType="CheckBox">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||||
|
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||||
|
<Setter Property="Cursor" Value="Hand"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="CheckBox">
|
||||||
|
<StackPanel Orientation="Horizontal" Background="Transparent">
|
||||||
|
<TextBlock x:Name="State" Text="Aus" VerticalAlignment="Center" Margin="0,0,12,0" MinWidth="24" TextAlignment="Right"/>
|
||||||
|
<Grid Width="40" Height="20">
|
||||||
|
<Border x:Name="Track" CornerRadius="10" BorderThickness="1"
|
||||||
|
Background="{DynamicResource SubtleSelectedBrush}" BorderBrush="{DynamicResource ControlStrongStrokeBrush}"/>
|
||||||
|
<Ellipse x:Name="Knob" Width="12" Height="12" HorizontalAlignment="Left" Margin="4,0,0,0"
|
||||||
|
Fill="{DynamicResource TextSecondaryBrush}"/>
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsChecked" Value="True">
|
||||||
|
<Setter TargetName="Track" Property="Background" Value="{DynamicResource AccentBrush}"/>
|
||||||
|
<Setter TargetName="Track" Property="BorderBrush" Value="{DynamicResource AccentBrush}"/>
|
||||||
|
<Setter TargetName="Knob" Property="Fill" Value="{DynamicResource OnAccentBrush}"/>
|
||||||
|
<Setter TargetName="Knob" Property="HorizontalAlignment" Value="Right"/>
|
||||||
|
<Setter TargetName="Knob" Property="Margin" Value="0,0,4,0"/>
|
||||||
|
<Setter TargetName="State" Property="Text" Value="Ein"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Knob" Property="Width" Value="14"/>
|
||||||
|
<Setter TargetName="Knob" Property="Height" Value="14"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter Property="Opacity" Value="0.4"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ===== Kontrollkästchen ===== -->
|
||||||
|
<Style TargetType="CheckBox">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||||
|
<Setter Property="FontFamily" Value="{DynamicResource UiFont}"/>
|
||||||
|
<Setter Property="FontSize" Value="14"/>
|
||||||
|
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="CheckBox">
|
||||||
|
<StackPanel Orientation="Horizontal" Background="Transparent">
|
||||||
|
<Border x:Name="Box" Width="20" Height="20" CornerRadius="4" BorderThickness="1" VerticalAlignment="Center"
|
||||||
|
Background="{DynamicResource ControlFillBrush}" BorderBrush="{DynamicResource ControlStrongStrokeBrush}">
|
||||||
|
<TextBlock x:Name="Check" Text="" FontFamily="{DynamicResource IconFont}" FontSize="12"
|
||||||
|
Foreground="{DynamicResource OnAccentBrush}" HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||||
|
Visibility="Collapsed"/>
|
||||||
|
</Border>
|
||||||
|
<ContentPresenter Margin="10,0,0,0" VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsChecked" Value="True">
|
||||||
|
<Setter TargetName="Box" Property="Background" Value="{DynamicResource AccentBrush}"/>
|
||||||
|
<Setter TargetName="Box" Property="BorderBrush" Value="{DynamicResource AccentBrush}"/>
|
||||||
|
<Setter TargetName="Check" Property="Visibility" Value="Visible"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Box" Property="Opacity" Value="0.85"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter Property="Opacity" Value="0.4"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ===== Segment-Auswahl (z. B. Hell | Dunkel | System) ===== -->
|
||||||
|
<Style x:Key="SegmentRadio" TargetType="RadioButton">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||||
|
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||||
|
<Setter Property="Padding" Value="14,6"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="RadioButton">
|
||||||
|
<Border x:Name="Bd" CornerRadius="4" Padding="{TemplateBinding Padding}" Margin="2" Background="Transparent">
|
||||||
|
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtleHoverBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsChecked" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource AccentBrush}"/>
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource OnAccentBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="SegmentGroup" TargetType="Border">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource SubtlePressedBrush}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource ControlStrokeBrush}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="CornerRadius" Value="6"/>
|
||||||
|
<Setter Property="Padding" Value="1"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ===== Farb- und Symbolauswahl ===== -->
|
||||||
|
<Style x:Key="SwatchRadio" TargetType="RadioButton">
|
||||||
|
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||||
|
<Setter Property="Cursor" Value="Hand"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="RadioButton">
|
||||||
|
<Grid Width="34" Height="34" Margin="0,0,6,6" Background="Transparent">
|
||||||
|
<Ellipse x:Name="Ring" Stroke="{DynamicResource TextBrush}" StrokeThickness="2" Visibility="Collapsed"/>
|
||||||
|
<Ellipse Margin="5" Fill="{TemplateBinding Background}" Stroke="{DynamicResource ControlStrokeBrush}"/>
|
||||||
|
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Grid>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsChecked" Value="True">
|
||||||
|
<Setter TargetName="Ring" Property="Visibility" Value="Visible"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="GlyphRadio" TargetType="RadioButton">
|
||||||
|
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||||
|
<Setter Property="FontFamily" Value="{DynamicResource IconFont}"/>
|
||||||
|
<Setter Property="FontSize" Value="16"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="RadioButton">
|
||||||
|
<Border x:Name="Bd" Width="36" Height="36" Margin="0,0,4,4" CornerRadius="4" BorderThickness="1"
|
||||||
|
BorderBrush="Transparent" Background="Transparent">
|
||||||
|
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtleHoverBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsChecked" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource AccentSubtleBrush}"/>
|
||||||
|
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource AccentBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
</ResourceDictionary>
|
||||||
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<assemblyIdentity version="1.0.0.0" name="GitManager.Setup"/>
|
||||||
|
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||||
|
<security>
|
||||||
|
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<!-- Installation nur für den aktuellen Benutzer → keine Administratorrechte -->
|
||||||
|
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
|
||||||
|
</requestedPrivileges>
|
||||||
|
</security>
|
||||||
|
</trustInfo>
|
||||||
|
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||||
|
<application>
|
||||||
|
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||||
|
</application>
|
||||||
|
</compatibility>
|
||||||
|
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<windowsSettings>
|
||||||
|
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
|
||||||
|
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||||
|
</windowsSettings>
|
||||||
|
</application>
|
||||||
|
</assembly>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
// <autogenerated />
|
||||||
|
using System;
|
||||||
|
using System.Reflection;
|
||||||
|
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
|
||||||
Binary file not shown.
@@ -0,0 +1,77 @@
|
|||||||
|
#pragma checksum "..\..\..\App.xaml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "D722851FC43B542A95C365A4D89BC7E17AC3C32D"
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// <auto-generated>
|
||||||
|
// This code was generated by a tool.
|
||||||
|
//
|
||||||
|
// Changes to this file may cause incorrect behavior and will be lost if
|
||||||
|
// the code is regenerated.
|
||||||
|
// </auto-generated>
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Automation;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Controls.Primitives;
|
||||||
|
using System.Windows.Controls.Ribbon;
|
||||||
|
using System.Windows.Data;
|
||||||
|
using System.Windows.Documents;
|
||||||
|
using System.Windows.Forms.Integration;
|
||||||
|
using System.Windows.Ink;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Markup;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Media.Animation;
|
||||||
|
using System.Windows.Media.Effects;
|
||||||
|
using System.Windows.Media.Imaging;
|
||||||
|
using System.Windows.Media.Media3D;
|
||||||
|
using System.Windows.Media.TextFormatting;
|
||||||
|
using System.Windows.Navigation;
|
||||||
|
using System.Windows.Shapes;
|
||||||
|
using System.Windows.Shell;
|
||||||
|
|
||||||
|
|
||||||
|
namespace GitManagerSetup {
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// App
|
||||||
|
/// </summary>
|
||||||
|
public partial class App : System.Windows.Application {
|
||||||
|
|
||||||
|
private bool _contentLoaded;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// InitializeComponent
|
||||||
|
/// </summary>
|
||||||
|
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||||
|
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "10.0.13.0")]
|
||||||
|
public void InitializeComponent() {
|
||||||
|
if (_contentLoaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_contentLoaded = true;
|
||||||
|
System.Uri resourceLocater = new System.Uri("/Git-Manager-GUI-Setup;V0.0.0.0;component/app.xaml", System.UriKind.Relative);
|
||||||
|
|
||||||
|
#line 1 "..\..\..\App.xaml"
|
||||||
|
System.Windows.Application.LoadComponent(this, resourceLocater);
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Application Entry Point.
|
||||||
|
/// </summary>
|
||||||
|
[System.STAThreadAttribute()]
|
||||||
|
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||||
|
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "10.0.13.0")]
|
||||||
|
public static void Main() {
|
||||||
|
GitManagerSetup.App app = new GitManagerSetup.App();
|
||||||
|
app.InitializeComponent();
|
||||||
|
app.Run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<configuration>
|
||||||
|
<startup>
|
||||||
|
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
|
||||||
|
</startup>
|
||||||
|
</configuration>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
Git-Manager-GUI-Setup
|
||||||
|
0.0.0.0
|
||||||
|
|
||||||
|
winexe
|
||||||
|
C#
|
||||||
|
.cs
|
||||||
|
C:\Users\Viper\Desktop\git-manager-gui\installer\obj\Debug\net48\
|
||||||
|
GitManagerSetup
|
||||||
|
none
|
||||||
|
false
|
||||||
|
TRACE;DEBUG;NETFRAMEWORK;NET48;NET30_OR_GREATER;NET35_OR_GREATER;NET40_OR_GREATER;NET45_OR_GREATER;NET451_OR_GREATER;NET452_OR_GREATER;NET46_OR_GREATER;NET461_OR_GREATER;NET462_OR_GREATER;NET47_OR_GREATER;NET471_OR_GREATER;NET472_OR_GREATER;NET48_OR_GREATER
|
||||||
|
C:\Users\Viper\Desktop\git-manager-gui\installer\App.xaml
|
||||||
|
41549299691
|
||||||
|
|
||||||
|
8-482891414
|
||||||
|
23-602304815
|
||||||
|
MainWindow.xaml;Themes\Colors.Dark.xaml;Themes\Colors.Light.xaml;Themes\Controls.xaml;
|
||||||
|
|
||||||
|
False
|
||||||
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// <auto-generated>
|
||||||
|
// This code was generated by a tool.
|
||||||
|
//
|
||||||
|
// Changes to this file may cause incorrect behavior and will be lost if
|
||||||
|
// the code is regenerated.
|
||||||
|
// </auto-generated>
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
[assembly: System.Reflection.AssemblyCompanyAttribute("M_Viper")]
|
||||||
|
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||||
|
[assembly: System.Reflection.AssemblyCopyrightAttribute("Copyright (c) M_Viper")]
|
||||||
|
[assembly: System.Reflection.AssemblyDescriptionAttribute("Installiert Git Manager GUI")]
|
||||||
|
[assembly: System.Reflection.AssemblyFileVersionAttribute("0.0.0.0")]
|
||||||
|
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("0.0.0")]
|
||||||
|
[assembly: System.Reflection.AssemblyProductAttribute("Git Manager GUI Setup")]
|
||||||
|
[assembly: System.Reflection.AssemblyTitleAttribute("Git Manager GUI Setup")]
|
||||||
|
[assembly: System.Reflection.AssemblyVersionAttribute("0.0.0.0")]
|
||||||
|
[assembly: System.Resources.NeutralResourcesLanguageAttribute("de-DE")]
|
||||||
|
|
||||||
|
// Von der MSBuild WriteCodeFragment-Klasse generiert.
|
||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
9c3aaae1d47c807c810117538379b6f2752577cee8cc71a0a6017da12833e788
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
is_global = true
|
||||||
|
build_property.ApplicationManifest = app.manifest
|
||||||
|
build_property.StartupObject =
|
||||||
|
build_property.ApplicationDefaultFont =
|
||||||
|
build_property.ApplicationHighDpiMode =
|
||||||
|
build_property.ApplicationUseCompatibleTextRendering =
|
||||||
|
build_property.ApplicationVisualStyles =
|
||||||
|
build_property.RootNamespace = GitManagerSetup
|
||||||
|
build_property.ProjectDir = C:\Users\Viper\Desktop\git-manager-gui\installer\
|
||||||
|
build_property.EnableComHosting =
|
||||||
|
build_property.EnableGeneratedComInterfaceComImportInterop =
|
||||||
|
build_property.CsWinRTUseWindowsUIXamlProjections = false
|
||||||
|
build_property.EffectiveAnalysisLevelStyle =
|
||||||
|
build_property.EnableCodeStyleSeverity =
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
// <auto-generated/>
|
||||||
|
global using System;
|
||||||
|
global using System.Collections.Generic;
|
||||||
|
global using System.Linq;
|
||||||
|
global using System.Threading;
|
||||||
|
global using System.Threading.Tasks;
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,676 @@
|
|||||||
|
#pragma checksum "..\..\..\MainWindow.xaml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "248163D303017091F6C4A571FD0B5DF4D5DBF414"
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// <auto-generated>
|
||||||
|
// This code was generated by a tool.
|
||||||
|
//
|
||||||
|
// Changes to this file may cause incorrect behavior and will be lost if
|
||||||
|
// the code is regenerated.
|
||||||
|
// </auto-generated>
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Automation;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Controls.Primitives;
|
||||||
|
using System.Windows.Controls.Ribbon;
|
||||||
|
using System.Windows.Data;
|
||||||
|
using System.Windows.Documents;
|
||||||
|
using System.Windows.Forms.Integration;
|
||||||
|
using System.Windows.Ink;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Markup;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Media.Animation;
|
||||||
|
using System.Windows.Media.Effects;
|
||||||
|
using System.Windows.Media.Imaging;
|
||||||
|
using System.Windows.Media.Media3D;
|
||||||
|
using System.Windows.Media.TextFormatting;
|
||||||
|
using System.Windows.Navigation;
|
||||||
|
using System.Windows.Shapes;
|
||||||
|
using System.Windows.Shell;
|
||||||
|
|
||||||
|
|
||||||
|
namespace GitManagerSetup {
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MainWindow
|
||||||
|
/// </summary>
|
||||||
|
public partial class MainWindow : System.Windows.Window, System.Windows.Markup.IComponentConnector {
|
||||||
|
|
||||||
|
|
||||||
|
#line 140 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock VersionText;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 142 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.StackPanel Steps;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 144 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Border Dot0;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 144 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock Num0;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 145 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock Label0;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 147 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Border Line0;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 149 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Border Dot1;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 149 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock Num1;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 150 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock Label1;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 152 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Border Line1;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 154 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Border Dot2;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 154 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock Num2;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 155 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock Label2;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 168 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock WindowTitle;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 174 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Button CloseButton;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 182 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.StackPanel PageWelcome;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 229 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Border UpdateHint;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 234 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock UpdateHintText;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 237 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Border RunningHint;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 249 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.StackPanel PageOptions;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 258 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBox DirBox;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 267 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.CheckBox DesktopLinkBox;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 273 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.StackPanel PageProgress;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 274 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Border SuccessIcon;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 276 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Media.ScaleTransform SuccessScale;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 281 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock ProgressTitle;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 283 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.StackPanel ProgressBarPanel;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 284 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock PercentText;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 286 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Border ProgressTrack;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 287 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Border ProgressFill;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 290 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock ProgressText;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 292 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.StackPanel DonePanel;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 293 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.CheckBox LaunchBox;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 298 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.StackPanel PageUninstall;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 309 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock DataInfo;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 310 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.CheckBox RemoveDataBox;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 312 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock RemoveDataWarning;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 322 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Button BackButton;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 323 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Button NextButton;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 324 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Button CancelButton;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 328 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Grid DialogOverlay;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 329 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Border DialogCard;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 332 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Media.ScaleTransform DialogScale;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 336 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock DialogIcon;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 339 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock DialogTitle;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 342 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock DialogText;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 345 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Button DialogSecondary;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 346 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Button DialogPrimary;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
private bool _contentLoaded;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// InitializeComponent
|
||||||
|
/// </summary>
|
||||||
|
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||||
|
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "10.0.13.0")]
|
||||||
|
public void InitializeComponent() {
|
||||||
|
if (_contentLoaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_contentLoaded = true;
|
||||||
|
System.Uri resourceLocater = new System.Uri("/Git-Manager-GUI-Setup;V0.0.0.0;component/mainwindow.xaml", System.UriKind.Relative);
|
||||||
|
|
||||||
|
#line 1 "..\..\..\MainWindow.xaml"
|
||||||
|
System.Windows.Application.LoadComponent(this, resourceLocater);
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
}
|
||||||
|
|
||||||
|
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||||
|
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "10.0.13.0")]
|
||||||
|
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes")]
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")]
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily")]
|
||||||
|
void System.Windows.Markup.IComponentConnector.Connect(int connectionId, object target) {
|
||||||
|
switch (connectionId)
|
||||||
|
{
|
||||||
|
case 1:
|
||||||
|
this.VersionText = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 2:
|
||||||
|
this.Steps = ((System.Windows.Controls.StackPanel)(target));
|
||||||
|
return;
|
||||||
|
case 3:
|
||||||
|
this.Dot0 = ((System.Windows.Controls.Border)(target));
|
||||||
|
return;
|
||||||
|
case 4:
|
||||||
|
this.Num0 = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 5:
|
||||||
|
this.Label0 = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 6:
|
||||||
|
this.Line0 = ((System.Windows.Controls.Border)(target));
|
||||||
|
return;
|
||||||
|
case 7:
|
||||||
|
this.Dot1 = ((System.Windows.Controls.Border)(target));
|
||||||
|
return;
|
||||||
|
case 8:
|
||||||
|
this.Num1 = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 9:
|
||||||
|
this.Label1 = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 10:
|
||||||
|
this.Line1 = ((System.Windows.Controls.Border)(target));
|
||||||
|
return;
|
||||||
|
case 11:
|
||||||
|
this.Dot2 = ((System.Windows.Controls.Border)(target));
|
||||||
|
return;
|
||||||
|
case 12:
|
||||||
|
this.Num2 = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 13:
|
||||||
|
this.Label2 = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 14:
|
||||||
|
this.WindowTitle = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 15:
|
||||||
|
|
||||||
|
#line 172 "..\..\..\MainWindow.xaml"
|
||||||
|
((System.Windows.Controls.Button)(target)).Click += new System.Windows.RoutedEventHandler(this.Minimize_Click);
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
return;
|
||||||
|
case 16:
|
||||||
|
this.CloseButton = ((System.Windows.Controls.Button)(target));
|
||||||
|
|
||||||
|
#line 174 "..\..\..\MainWindow.xaml"
|
||||||
|
this.CloseButton.Click += new System.Windows.RoutedEventHandler(this.Cancel_Click);
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
return;
|
||||||
|
case 17:
|
||||||
|
this.PageWelcome = ((System.Windows.Controls.StackPanel)(target));
|
||||||
|
return;
|
||||||
|
case 18:
|
||||||
|
this.UpdateHint = ((System.Windows.Controls.Border)(target));
|
||||||
|
return;
|
||||||
|
case 19:
|
||||||
|
this.UpdateHintText = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 20:
|
||||||
|
this.RunningHint = ((System.Windows.Controls.Border)(target));
|
||||||
|
return;
|
||||||
|
case 21:
|
||||||
|
this.PageOptions = ((System.Windows.Controls.StackPanel)(target));
|
||||||
|
return;
|
||||||
|
case 22:
|
||||||
|
|
||||||
|
#line 257 "..\..\..\MainWindow.xaml"
|
||||||
|
((System.Windows.Controls.Button)(target)).Click += new System.Windows.RoutedEventHandler(this.ChangeDir_Click);
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
return;
|
||||||
|
case 23:
|
||||||
|
this.DirBox = ((System.Windows.Controls.TextBox)(target));
|
||||||
|
return;
|
||||||
|
case 24:
|
||||||
|
this.DesktopLinkBox = ((System.Windows.Controls.CheckBox)(target));
|
||||||
|
return;
|
||||||
|
case 25:
|
||||||
|
this.PageProgress = ((System.Windows.Controls.StackPanel)(target));
|
||||||
|
return;
|
||||||
|
case 26:
|
||||||
|
this.SuccessIcon = ((System.Windows.Controls.Border)(target));
|
||||||
|
return;
|
||||||
|
case 27:
|
||||||
|
this.SuccessScale = ((System.Windows.Media.ScaleTransform)(target));
|
||||||
|
return;
|
||||||
|
case 28:
|
||||||
|
this.ProgressTitle = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 29:
|
||||||
|
this.ProgressBarPanel = ((System.Windows.Controls.StackPanel)(target));
|
||||||
|
return;
|
||||||
|
case 30:
|
||||||
|
this.PercentText = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 31:
|
||||||
|
this.ProgressTrack = ((System.Windows.Controls.Border)(target));
|
||||||
|
|
||||||
|
#line 286 "..\..\..\MainWindow.xaml"
|
||||||
|
this.ProgressTrack.SizeChanged += new System.Windows.SizeChangedEventHandler(this.ProgressTrack_SizeChanged);
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
return;
|
||||||
|
case 32:
|
||||||
|
this.ProgressFill = ((System.Windows.Controls.Border)(target));
|
||||||
|
return;
|
||||||
|
case 33:
|
||||||
|
this.ProgressText = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 34:
|
||||||
|
this.DonePanel = ((System.Windows.Controls.StackPanel)(target));
|
||||||
|
return;
|
||||||
|
case 35:
|
||||||
|
this.LaunchBox = ((System.Windows.Controls.CheckBox)(target));
|
||||||
|
return;
|
||||||
|
case 36:
|
||||||
|
this.PageUninstall = ((System.Windows.Controls.StackPanel)(target));
|
||||||
|
return;
|
||||||
|
case 37:
|
||||||
|
this.DataInfo = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 38:
|
||||||
|
this.RemoveDataBox = ((System.Windows.Controls.CheckBox)(target));
|
||||||
|
|
||||||
|
#line 311 "..\..\..\MainWindow.xaml"
|
||||||
|
this.RemoveDataBox.Checked += new System.Windows.RoutedEventHandler(this.RemoveDataBox_Click);
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
#line 311 "..\..\..\MainWindow.xaml"
|
||||||
|
this.RemoveDataBox.Unchecked += new System.Windows.RoutedEventHandler(this.RemoveDataBox_Click);
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
return;
|
||||||
|
case 39:
|
||||||
|
this.RemoveDataWarning = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 40:
|
||||||
|
this.BackButton = ((System.Windows.Controls.Button)(target));
|
||||||
|
|
||||||
|
#line 322 "..\..\..\MainWindow.xaml"
|
||||||
|
this.BackButton.Click += new System.Windows.RoutedEventHandler(this.Back_Click);
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
return;
|
||||||
|
case 41:
|
||||||
|
this.NextButton = ((System.Windows.Controls.Button)(target));
|
||||||
|
|
||||||
|
#line 323 "..\..\..\MainWindow.xaml"
|
||||||
|
this.NextButton.Click += new System.Windows.RoutedEventHandler(this.Next_Click);
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
return;
|
||||||
|
case 42:
|
||||||
|
this.CancelButton = ((System.Windows.Controls.Button)(target));
|
||||||
|
|
||||||
|
#line 324 "..\..\..\MainWindow.xaml"
|
||||||
|
this.CancelButton.Click += new System.Windows.RoutedEventHandler(this.Cancel_Click);
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
return;
|
||||||
|
case 43:
|
||||||
|
this.DialogOverlay = ((System.Windows.Controls.Grid)(target));
|
||||||
|
return;
|
||||||
|
case 44:
|
||||||
|
this.DialogCard = ((System.Windows.Controls.Border)(target));
|
||||||
|
return;
|
||||||
|
case 45:
|
||||||
|
this.DialogScale = ((System.Windows.Media.ScaleTransform)(target));
|
||||||
|
return;
|
||||||
|
case 46:
|
||||||
|
this.DialogIcon = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 47:
|
||||||
|
this.DialogTitle = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 48:
|
||||||
|
this.DialogText = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 49:
|
||||||
|
this.DialogSecondary = ((System.Windows.Controls.Button)(target));
|
||||||
|
|
||||||
|
#line 345 "..\..\..\MainWindow.xaml"
|
||||||
|
this.DialogSecondary.Click += new System.Windows.RoutedEventHandler(this.DialogSecondary_Click);
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
return;
|
||||||
|
case 50:
|
||||||
|
this.DialogPrimary = ((System.Windows.Controls.Button)(target));
|
||||||
|
|
||||||
|
#line 346 "..\..\..\MainWindow.xaml"
|
||||||
|
this.DialogPrimary.Click += new System.Windows.RoutedEventHandler(this.DialogPrimary_Click);
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._contentLoaded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"format": 1,
|
||||||
|
"restore": {
|
||||||
|
"C:\\Users\\Viper\\Desktop\\git-manager-gui\\installer\\GitManagerSetup.csproj": {}
|
||||||
|
},
|
||||||
|
"projects": {
|
||||||
|
"C:\\Users\\Viper\\Desktop\\git-manager-gui\\installer\\GitManagerSetup.csproj": {
|
||||||
|
"version": "0.0.0",
|
||||||
|
"restore": {
|
||||||
|
"projectUniqueName": "C:\\Users\\Viper\\Desktop\\git-manager-gui\\installer\\GitManagerSetup.csproj",
|
||||||
|
"projectName": "Git-Manager-GUI-Setup",
|
||||||
|
"projectPath": "C:\\Users\\Viper\\Desktop\\git-manager-gui\\installer\\GitManagerSetup.csproj",
|
||||||
|
"packagesPath": "C:\\Users\\Viper\\.nuget\\packages\\",
|
||||||
|
"outputPath": "C:\\Users\\Viper\\Desktop\\git-manager-gui\\installer\\obj\\",
|
||||||
|
"projectStyle": "PackageReference",
|
||||||
|
"configFilePaths": [
|
||||||
|
"C:\\Users\\Viper\\AppData\\Roaming\\NuGet\\NuGet.Config"
|
||||||
|
],
|
||||||
|
"originalTargetFrameworks": [
|
||||||
|
"net48"
|
||||||
|
],
|
||||||
|
"sources": {
|
||||||
|
"https://api.nuget.org/v3/index.json": {}
|
||||||
|
},
|
||||||
|
"frameworks": {
|
||||||
|
"net48": {
|
||||||
|
"targetAlias": "net48",
|
||||||
|
"projectReferences": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"warningProperties": {
|
||||||
|
"warnAsError": [
|
||||||
|
"NU1605"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"restoreAuditProperties": {
|
||||||
|
"enableAudit": "true",
|
||||||
|
"auditLevel": "low",
|
||||||
|
"auditMode": "direct"
|
||||||
|
},
|
||||||
|
"SdkAnalysisLevel": "10.0.100"
|
||||||
|
},
|
||||||
|
"frameworks": {
|
||||||
|
"net48": {
|
||||||
|
"targetAlias": "net48",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.NETFramework.ReferenceAssemblies": {
|
||||||
|
"suppressParent": "All",
|
||||||
|
"target": "Package",
|
||||||
|
"version": "[1.0.3, )",
|
||||||
|
"autoReferenced": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\10.0.113\\RuntimeIdentifierGraph.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||||
|
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<PropertyGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
||||||
|
<RestoreSuccess Condition=" '$(RestoreSuccess)' == '' ">True</RestoreSuccess>
|
||||||
|
<RestoreTool Condition=" '$(RestoreTool)' == '' ">NuGet</RestoreTool>
|
||||||
|
<ProjectAssetsFile Condition=" '$(ProjectAssetsFile)' == '' ">$(MSBuildThisFileDirectory)project.assets.json</ProjectAssetsFile>
|
||||||
|
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">$(UserProfile)\.nuget\packages\</NuGetPackageRoot>
|
||||||
|
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\Viper\.nuget\packages\</NuGetPackageFolders>
|
||||||
|
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
|
||||||
|
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">7.0.0</NuGetToolVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
||||||
|
<SourceRoot Include="C:\Users\Viper\.nuget\packages\" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||||
|
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<ImportGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
||||||
|
<Import Project="$(NuGetPackageRoot)microsoft.netframework.referenceassemblies.net48\1.0.3\build\Microsoft.NETFramework.ReferenceAssemblies.net48.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.netframework.referenceassemblies.net48\1.0.3\build\Microsoft.NETFramework.ReferenceAssemblies.net48.targets')" />
|
||||||
|
</ImportGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
// <autogenerated />
|
||||||
|
using System;
|
||||||
|
using System.Reflection;
|
||||||
|
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
|
||||||
Binary file not shown.
@@ -0,0 +1,77 @@
|
|||||||
|
#pragma checksum "..\..\..\App.xaml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "D722851FC43B542A95C365A4D89BC7E17AC3C32D"
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// <auto-generated>
|
||||||
|
// This code was generated by a tool.
|
||||||
|
//
|
||||||
|
// Changes to this file may cause incorrect behavior and will be lost if
|
||||||
|
// the code is regenerated.
|
||||||
|
// </auto-generated>
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Automation;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Controls.Primitives;
|
||||||
|
using System.Windows.Controls.Ribbon;
|
||||||
|
using System.Windows.Data;
|
||||||
|
using System.Windows.Documents;
|
||||||
|
using System.Windows.Forms.Integration;
|
||||||
|
using System.Windows.Ink;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Markup;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Media.Animation;
|
||||||
|
using System.Windows.Media.Effects;
|
||||||
|
using System.Windows.Media.Imaging;
|
||||||
|
using System.Windows.Media.Media3D;
|
||||||
|
using System.Windows.Media.TextFormatting;
|
||||||
|
using System.Windows.Navigation;
|
||||||
|
using System.Windows.Shapes;
|
||||||
|
using System.Windows.Shell;
|
||||||
|
|
||||||
|
|
||||||
|
namespace GitManagerSetup {
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// App
|
||||||
|
/// </summary>
|
||||||
|
public partial class App : System.Windows.Application {
|
||||||
|
|
||||||
|
private bool _contentLoaded;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// InitializeComponent
|
||||||
|
/// </summary>
|
||||||
|
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||||
|
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "10.0.13.0")]
|
||||||
|
public void InitializeComponent() {
|
||||||
|
if (_contentLoaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_contentLoaded = true;
|
||||||
|
System.Uri resourceLocater = new System.Uri("/Git-Manager-GUI-Setup;component/app.xaml", System.UriKind.Relative);
|
||||||
|
|
||||||
|
#line 1 "..\..\..\App.xaml"
|
||||||
|
System.Windows.Application.LoadComponent(this, resourceLocater);
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Application Entry Point.
|
||||||
|
/// </summary>
|
||||||
|
[System.STAThreadAttribute()]
|
||||||
|
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||||
|
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "10.0.13.0")]
|
||||||
|
public static void Main() {
|
||||||
|
GitManagerSetup.App app = new GitManagerSetup.App();
|
||||||
|
app.InitializeComponent();
|
||||||
|
app.Run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Binary file not shown.
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<configuration>
|
||||||
|
<startup>
|
||||||
|
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
|
||||||
|
</startup>
|
||||||
|
</configuration>
|
||||||
Binary file not shown.
@@ -0,0 +1,20 @@
|
|||||||
|
Git-Manager-GUI-Setup
|
||||||
|
|
||||||
|
|
||||||
|
winexe
|
||||||
|
C#
|
||||||
|
.cs
|
||||||
|
C:\Users\Viper\Desktop\git-manager-gui\installer\obj\Release\net48\
|
||||||
|
GitManagerSetup
|
||||||
|
none
|
||||||
|
false
|
||||||
|
TRACE;RELEASE;NETFRAMEWORK;NET48;NET30_OR_GREATER;NET35_OR_GREATER;NET40_OR_GREATER;NET45_OR_GREATER;NET451_OR_GREATER;NET452_OR_GREATER;NET46_OR_GREATER;NET461_OR_GREATER;NET462_OR_GREATER;NET47_OR_GREATER;NET471_OR_GREATER;NET472_OR_GREATER;NET48_OR_GREATER
|
||||||
|
C:\Users\Viper\Desktop\git-manager-gui\installer\App.xaml
|
||||||
|
41549299691
|
||||||
|
|
||||||
|
6-1705775420
|
||||||
|
23-602304815
|
||||||
|
MainWindow.xaml;Themes\Colors.Dark.xaml;Themes\Colors.Light.xaml;Themes\Controls.xaml;
|
||||||
|
|
||||||
|
False
|
||||||
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// <auto-generated>
|
||||||
|
// This code was generated by a tool.
|
||||||
|
//
|
||||||
|
// Changes to this file may cause incorrect behavior and will be lost if
|
||||||
|
// the code is regenerated.
|
||||||
|
// </auto-generated>
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
[assembly: System.Reflection.AssemblyCompanyAttribute("M_Viper")]
|
||||||
|
[assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
|
||||||
|
[assembly: System.Reflection.AssemblyCopyrightAttribute("Copyright (c) M_Viper")]
|
||||||
|
[assembly: System.Reflection.AssemblyDescriptionAttribute("Installiert Git Manager GUI")]
|
||||||
|
[assembly: System.Reflection.AssemblyFileVersionAttribute("2.2.0.0")]
|
||||||
|
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("2.2.0")]
|
||||||
|
[assembly: System.Reflection.AssemblyProductAttribute("Git Manager GUI Setup")]
|
||||||
|
[assembly: System.Reflection.AssemblyTitleAttribute("Git Manager GUI Setup")]
|
||||||
|
[assembly: System.Reflection.AssemblyVersionAttribute("2.2.0.0")]
|
||||||
|
[assembly: System.Resources.NeutralResourcesLanguageAttribute("de-DE")]
|
||||||
|
|
||||||
|
// Von der MSBuild WriteCodeFragment-Klasse generiert.
|
||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
62f4b9ac7b1430e3a52ef9dfe2e8422ccd7cc5a7e41145550122532f56db4abe
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
is_global = true
|
||||||
|
build_property.ApplicationManifest = app.manifest
|
||||||
|
build_property.StartupObject =
|
||||||
|
build_property.ApplicationDefaultFont =
|
||||||
|
build_property.ApplicationHighDpiMode =
|
||||||
|
build_property.ApplicationUseCompatibleTextRendering =
|
||||||
|
build_property.ApplicationVisualStyles =
|
||||||
|
build_property.RootNamespace = GitManagerSetup
|
||||||
|
build_property.ProjectDir = C:\Users\Viper\Desktop\git-manager-gui\installer\
|
||||||
|
build_property.EnableComHosting =
|
||||||
|
build_property.EnableGeneratedComInterfaceComImportInterop =
|
||||||
|
build_property.CsWinRTUseWindowsUIXamlProjections = false
|
||||||
|
build_property.EffectiveAnalysisLevelStyle =
|
||||||
|
build_property.EnableCodeStyleSeverity =
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
// <auto-generated/>
|
||||||
|
global using System;
|
||||||
|
global using System.Collections.Generic;
|
||||||
|
global using System.Linq;
|
||||||
|
global using System.Threading;
|
||||||
|
global using System.Threading.Tasks;
|
||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
4e41dc5e237f26559d3c4bd8208c813ba09bdba804615d78813773d6b0be3d1d
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
C:\Users\Viper\AppData\Local\Temp\claude\G--\bba0d154-b599-43ca-80e6-538e01f4f870\scratchpad\setupbuild\Git-Manager-GUI-Setup.exe.config
|
||||||
|
C:\Users\Viper\AppData\Local\Temp\claude\G--\bba0d154-b599-43ca-80e6-538e01f4f870\scratchpad\setupbuild\Git-Manager-GUI-Setup.exe
|
||||||
|
C:\Users\Viper\Desktop\git-manager-gui\installer\obj\Release\net48\GitManagerSetup.csproj.AssemblyReference.cache
|
||||||
|
C:\Users\Viper\Desktop\git-manager-gui\installer\obj\Release\net48\MainWindow.baml
|
||||||
|
C:\Users\Viper\Desktop\git-manager-gui\installer\obj\Release\net48\Themes\Colors.Dark.baml
|
||||||
|
C:\Users\Viper\Desktop\git-manager-gui\installer\obj\Release\net48\Themes\Colors.Light.baml
|
||||||
|
C:\Users\Viper\Desktop\git-manager-gui\installer\obj\Release\net48\Themes\Controls.baml
|
||||||
|
C:\Users\Viper\Desktop\git-manager-gui\installer\obj\Release\net48\App.baml
|
||||||
|
C:\Users\Viper\Desktop\git-manager-gui\installer\obj\Release\net48\MainWindow.g.cs
|
||||||
|
C:\Users\Viper\Desktop\git-manager-gui\installer\obj\Release\net48\App.g.cs
|
||||||
|
C:\Users\Viper\Desktop\git-manager-gui\installer\obj\Release\net48\Git-Manager-GUI-Setup_MarkupCompile.cache
|
||||||
|
C:\Users\Viper\Desktop\git-manager-gui\installer\obj\Release\net48\Git-Manager-GUI-Setup.g.resources
|
||||||
|
C:\Users\Viper\Desktop\git-manager-gui\installer\obj\Release\net48\GitManagerSetup.GeneratedMSBuildEditorConfig.editorconfig
|
||||||
|
C:\Users\Viper\Desktop\git-manager-gui\installer\obj\Release\net48\GitManagerSetup.AssemblyInfoInputs.cache
|
||||||
|
C:\Users\Viper\Desktop\git-manager-gui\installer\obj\Release\net48\GitManagerSetup.AssemblyInfo.cs
|
||||||
|
C:\Users\Viper\Desktop\git-manager-gui\installer\obj\Release\net48\GitManagerSetup.csproj.CoreCompileInputs.cache
|
||||||
|
C:\Users\Viper\Desktop\git-manager-gui\installer\obj\Release\net48\Git-Manager-GUI-Setup.exe
|
||||||
|
C:\Users\Viper\Desktop\git-manager-gui\dist\.setup\stub\Git-Manager-GUI-Setup.exe.config
|
||||||
|
C:\Users\Viper\Desktop\git-manager-gui\dist\.setup\stub\Git-Manager-GUI-Setup.exe
|
||||||
Binary file not shown.
@@ -0,0 +1,676 @@
|
|||||||
|
#pragma checksum "..\..\..\MainWindow.xaml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "248163D303017091F6C4A571FD0B5DF4D5DBF414"
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// <auto-generated>
|
||||||
|
// This code was generated by a tool.
|
||||||
|
//
|
||||||
|
// Changes to this file may cause incorrect behavior and will be lost if
|
||||||
|
// the code is regenerated.
|
||||||
|
// </auto-generated>
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Automation;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Controls.Primitives;
|
||||||
|
using System.Windows.Controls.Ribbon;
|
||||||
|
using System.Windows.Data;
|
||||||
|
using System.Windows.Documents;
|
||||||
|
using System.Windows.Forms.Integration;
|
||||||
|
using System.Windows.Ink;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Markup;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Media.Animation;
|
||||||
|
using System.Windows.Media.Effects;
|
||||||
|
using System.Windows.Media.Imaging;
|
||||||
|
using System.Windows.Media.Media3D;
|
||||||
|
using System.Windows.Media.TextFormatting;
|
||||||
|
using System.Windows.Navigation;
|
||||||
|
using System.Windows.Shapes;
|
||||||
|
using System.Windows.Shell;
|
||||||
|
|
||||||
|
|
||||||
|
namespace GitManagerSetup {
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MainWindow
|
||||||
|
/// </summary>
|
||||||
|
public partial class MainWindow : System.Windows.Window, System.Windows.Markup.IComponentConnector {
|
||||||
|
|
||||||
|
|
||||||
|
#line 140 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock VersionText;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 142 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.StackPanel Steps;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 144 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Border Dot0;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 144 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock Num0;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 145 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock Label0;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 147 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Border Line0;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 149 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Border Dot1;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 149 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock Num1;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 150 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock Label1;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 152 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Border Line1;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 154 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Border Dot2;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 154 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock Num2;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 155 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock Label2;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 168 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock WindowTitle;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 174 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Button CloseButton;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 182 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.StackPanel PageWelcome;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 229 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Border UpdateHint;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 234 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock UpdateHintText;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 237 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Border RunningHint;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 249 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.StackPanel PageOptions;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 258 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBox DirBox;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 267 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.CheckBox DesktopLinkBox;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 273 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.StackPanel PageProgress;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 274 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Border SuccessIcon;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 276 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Media.ScaleTransform SuccessScale;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 281 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock ProgressTitle;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 283 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.StackPanel ProgressBarPanel;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 284 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock PercentText;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 286 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Border ProgressTrack;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 287 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Border ProgressFill;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 290 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock ProgressText;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 292 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.StackPanel DonePanel;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 293 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.CheckBox LaunchBox;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 298 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.StackPanel PageUninstall;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 309 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock DataInfo;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 310 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.CheckBox RemoveDataBox;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 312 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock RemoveDataWarning;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 322 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Button BackButton;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 323 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Button NextButton;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 324 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Button CancelButton;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 328 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Grid DialogOverlay;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 329 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Border DialogCard;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 332 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Media.ScaleTransform DialogScale;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 336 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock DialogIcon;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 339 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock DialogTitle;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 342 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.TextBlock DialogText;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 345 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Button DialogSecondary;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
|
||||||
|
#line 346 "..\..\..\MainWindow.xaml"
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||||
|
internal System.Windows.Controls.Button DialogPrimary;
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
private bool _contentLoaded;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// InitializeComponent
|
||||||
|
/// </summary>
|
||||||
|
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||||
|
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "10.0.13.0")]
|
||||||
|
public void InitializeComponent() {
|
||||||
|
if (_contentLoaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_contentLoaded = true;
|
||||||
|
System.Uri resourceLocater = new System.Uri("/Git-Manager-GUI-Setup;component/mainwindow.xaml", System.UriKind.Relative);
|
||||||
|
|
||||||
|
#line 1 "..\..\..\MainWindow.xaml"
|
||||||
|
System.Windows.Application.LoadComponent(this, resourceLocater);
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
}
|
||||||
|
|
||||||
|
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||||
|
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "10.0.13.0")]
|
||||||
|
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes")]
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")]
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily")]
|
||||||
|
void System.Windows.Markup.IComponentConnector.Connect(int connectionId, object target) {
|
||||||
|
switch (connectionId)
|
||||||
|
{
|
||||||
|
case 1:
|
||||||
|
this.VersionText = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 2:
|
||||||
|
this.Steps = ((System.Windows.Controls.StackPanel)(target));
|
||||||
|
return;
|
||||||
|
case 3:
|
||||||
|
this.Dot0 = ((System.Windows.Controls.Border)(target));
|
||||||
|
return;
|
||||||
|
case 4:
|
||||||
|
this.Num0 = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 5:
|
||||||
|
this.Label0 = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 6:
|
||||||
|
this.Line0 = ((System.Windows.Controls.Border)(target));
|
||||||
|
return;
|
||||||
|
case 7:
|
||||||
|
this.Dot1 = ((System.Windows.Controls.Border)(target));
|
||||||
|
return;
|
||||||
|
case 8:
|
||||||
|
this.Num1 = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 9:
|
||||||
|
this.Label1 = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 10:
|
||||||
|
this.Line1 = ((System.Windows.Controls.Border)(target));
|
||||||
|
return;
|
||||||
|
case 11:
|
||||||
|
this.Dot2 = ((System.Windows.Controls.Border)(target));
|
||||||
|
return;
|
||||||
|
case 12:
|
||||||
|
this.Num2 = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 13:
|
||||||
|
this.Label2 = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 14:
|
||||||
|
this.WindowTitle = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 15:
|
||||||
|
|
||||||
|
#line 172 "..\..\..\MainWindow.xaml"
|
||||||
|
((System.Windows.Controls.Button)(target)).Click += new System.Windows.RoutedEventHandler(this.Minimize_Click);
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
return;
|
||||||
|
case 16:
|
||||||
|
this.CloseButton = ((System.Windows.Controls.Button)(target));
|
||||||
|
|
||||||
|
#line 174 "..\..\..\MainWindow.xaml"
|
||||||
|
this.CloseButton.Click += new System.Windows.RoutedEventHandler(this.Cancel_Click);
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
return;
|
||||||
|
case 17:
|
||||||
|
this.PageWelcome = ((System.Windows.Controls.StackPanel)(target));
|
||||||
|
return;
|
||||||
|
case 18:
|
||||||
|
this.UpdateHint = ((System.Windows.Controls.Border)(target));
|
||||||
|
return;
|
||||||
|
case 19:
|
||||||
|
this.UpdateHintText = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 20:
|
||||||
|
this.RunningHint = ((System.Windows.Controls.Border)(target));
|
||||||
|
return;
|
||||||
|
case 21:
|
||||||
|
this.PageOptions = ((System.Windows.Controls.StackPanel)(target));
|
||||||
|
return;
|
||||||
|
case 22:
|
||||||
|
|
||||||
|
#line 257 "..\..\..\MainWindow.xaml"
|
||||||
|
((System.Windows.Controls.Button)(target)).Click += new System.Windows.RoutedEventHandler(this.ChangeDir_Click);
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
return;
|
||||||
|
case 23:
|
||||||
|
this.DirBox = ((System.Windows.Controls.TextBox)(target));
|
||||||
|
return;
|
||||||
|
case 24:
|
||||||
|
this.DesktopLinkBox = ((System.Windows.Controls.CheckBox)(target));
|
||||||
|
return;
|
||||||
|
case 25:
|
||||||
|
this.PageProgress = ((System.Windows.Controls.StackPanel)(target));
|
||||||
|
return;
|
||||||
|
case 26:
|
||||||
|
this.SuccessIcon = ((System.Windows.Controls.Border)(target));
|
||||||
|
return;
|
||||||
|
case 27:
|
||||||
|
this.SuccessScale = ((System.Windows.Media.ScaleTransform)(target));
|
||||||
|
return;
|
||||||
|
case 28:
|
||||||
|
this.ProgressTitle = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 29:
|
||||||
|
this.ProgressBarPanel = ((System.Windows.Controls.StackPanel)(target));
|
||||||
|
return;
|
||||||
|
case 30:
|
||||||
|
this.PercentText = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 31:
|
||||||
|
this.ProgressTrack = ((System.Windows.Controls.Border)(target));
|
||||||
|
|
||||||
|
#line 286 "..\..\..\MainWindow.xaml"
|
||||||
|
this.ProgressTrack.SizeChanged += new System.Windows.SizeChangedEventHandler(this.ProgressTrack_SizeChanged);
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
return;
|
||||||
|
case 32:
|
||||||
|
this.ProgressFill = ((System.Windows.Controls.Border)(target));
|
||||||
|
return;
|
||||||
|
case 33:
|
||||||
|
this.ProgressText = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 34:
|
||||||
|
this.DonePanel = ((System.Windows.Controls.StackPanel)(target));
|
||||||
|
return;
|
||||||
|
case 35:
|
||||||
|
this.LaunchBox = ((System.Windows.Controls.CheckBox)(target));
|
||||||
|
return;
|
||||||
|
case 36:
|
||||||
|
this.PageUninstall = ((System.Windows.Controls.StackPanel)(target));
|
||||||
|
return;
|
||||||
|
case 37:
|
||||||
|
this.DataInfo = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 38:
|
||||||
|
this.RemoveDataBox = ((System.Windows.Controls.CheckBox)(target));
|
||||||
|
|
||||||
|
#line 311 "..\..\..\MainWindow.xaml"
|
||||||
|
this.RemoveDataBox.Checked += new System.Windows.RoutedEventHandler(this.RemoveDataBox_Click);
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
|
||||||
|
#line 311 "..\..\..\MainWindow.xaml"
|
||||||
|
this.RemoveDataBox.Unchecked += new System.Windows.RoutedEventHandler(this.RemoveDataBox_Click);
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
return;
|
||||||
|
case 39:
|
||||||
|
this.RemoveDataWarning = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 40:
|
||||||
|
this.BackButton = ((System.Windows.Controls.Button)(target));
|
||||||
|
|
||||||
|
#line 322 "..\..\..\MainWindow.xaml"
|
||||||
|
this.BackButton.Click += new System.Windows.RoutedEventHandler(this.Back_Click);
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
return;
|
||||||
|
case 41:
|
||||||
|
this.NextButton = ((System.Windows.Controls.Button)(target));
|
||||||
|
|
||||||
|
#line 323 "..\..\..\MainWindow.xaml"
|
||||||
|
this.NextButton.Click += new System.Windows.RoutedEventHandler(this.Next_Click);
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
return;
|
||||||
|
case 42:
|
||||||
|
this.CancelButton = ((System.Windows.Controls.Button)(target));
|
||||||
|
|
||||||
|
#line 324 "..\..\..\MainWindow.xaml"
|
||||||
|
this.CancelButton.Click += new System.Windows.RoutedEventHandler(this.Cancel_Click);
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
return;
|
||||||
|
case 43:
|
||||||
|
this.DialogOverlay = ((System.Windows.Controls.Grid)(target));
|
||||||
|
return;
|
||||||
|
case 44:
|
||||||
|
this.DialogCard = ((System.Windows.Controls.Border)(target));
|
||||||
|
return;
|
||||||
|
case 45:
|
||||||
|
this.DialogScale = ((System.Windows.Media.ScaleTransform)(target));
|
||||||
|
return;
|
||||||
|
case 46:
|
||||||
|
this.DialogIcon = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 47:
|
||||||
|
this.DialogTitle = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 48:
|
||||||
|
this.DialogText = ((System.Windows.Controls.TextBlock)(target));
|
||||||
|
return;
|
||||||
|
case 49:
|
||||||
|
this.DialogSecondary = ((System.Windows.Controls.Button)(target));
|
||||||
|
|
||||||
|
#line 345 "..\..\..\MainWindow.xaml"
|
||||||
|
this.DialogSecondary.Click += new System.Windows.RoutedEventHandler(this.DialogSecondary_Click);
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
return;
|
||||||
|
case 50:
|
||||||
|
this.DialogPrimary = ((System.Windows.Controls.Button)(target));
|
||||||
|
|
||||||
|
#line 346 "..\..\..\MainWindow.xaml"
|
||||||
|
this.DialogPrimary.Click += new System.Windows.RoutedEventHandler(this.DialogPrimary_Click);
|
||||||
|
|
||||||
|
#line default
|
||||||
|
#line hidden
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._contentLoaded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,471 @@
|
|||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"targets": {
|
||||||
|
".NETFramework,Version=v4.8": {
|
||||||
|
"Microsoft.NETFramework.ReferenceAssemblies/1.0.3": {
|
||||||
|
"type": "package",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.NETFramework.ReferenceAssemblies.net48": "1.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.NETFramework.ReferenceAssemblies.net48/1.0.3": {
|
||||||
|
"type": "package",
|
||||||
|
"build": {
|
||||||
|
"build/Microsoft.NETFramework.ReferenceAssemblies.net48.targets": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"libraries": {
|
||||||
|
"Microsoft.NETFramework.ReferenceAssemblies/1.0.3": {
|
||||||
|
"sha512": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==",
|
||||||
|
"type": "package",
|
||||||
|
"path": "microsoft.netframework.referenceassemblies/1.0.3",
|
||||||
|
"files": [
|
||||||
|
".nupkg.metadata",
|
||||||
|
".signature.p7s",
|
||||||
|
"microsoft.netframework.referenceassemblies.1.0.3.nupkg.sha512",
|
||||||
|
"microsoft.netframework.referenceassemblies.nuspec"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Microsoft.NETFramework.ReferenceAssemblies.net48/1.0.3": {
|
||||||
|
"sha512": "zMk4D+9zyiEWByyQ7oPImPN/Jhpj166Ky0Nlla4eXlNL8hI/BtSJsgR8Inldd4NNpIAH3oh8yym0W2DrhXdSLQ==",
|
||||||
|
"type": "package",
|
||||||
|
"path": "microsoft.netframework.referenceassemblies.net48/1.0.3",
|
||||||
|
"files": [
|
||||||
|
".nupkg.metadata",
|
||||||
|
".signature.p7s",
|
||||||
|
"build/.NETFramework/v4.8/Accessibility.dll",
|
||||||
|
"build/.NETFramework/v4.8/Accessibility.xml",
|
||||||
|
"build/.NETFramework/v4.8/CustomMarshalers.dll",
|
||||||
|
"build/.NETFramework/v4.8/CustomMarshalers.xml",
|
||||||
|
"build/.NETFramework/v4.8/Facades/Microsoft.Win32.Primitives.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.AppContext.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Collections.Concurrent.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Collections.NonGeneric.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Collections.Specialized.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Collections.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.ComponentModel.Annotations.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.ComponentModel.EventBasedAsync.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.ComponentModel.Primitives.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.ComponentModel.TypeConverter.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.ComponentModel.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Console.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Data.Common.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Diagnostics.Contracts.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Diagnostics.Debug.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Diagnostics.FileVersionInfo.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Diagnostics.Process.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Diagnostics.StackTrace.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Diagnostics.TextWriterTraceListener.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Diagnostics.Tools.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Diagnostics.TraceSource.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Drawing.Primitives.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Dynamic.Runtime.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Globalization.Calendars.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Globalization.Extensions.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Globalization.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.IO.Compression.ZipFile.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.IO.FileSystem.DriveInfo.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.IO.FileSystem.Primitives.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.IO.FileSystem.Watcher.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.IO.FileSystem.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.IO.IsolatedStorage.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.IO.MemoryMappedFiles.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.IO.Pipes.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.IO.UnmanagedMemoryStream.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.IO.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Linq.Expressions.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Linq.Parallel.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Linq.Queryable.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Linq.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Net.Http.Rtc.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Net.NameResolution.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Net.NetworkInformation.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Net.Ping.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Net.Primitives.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Net.Requests.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Net.Security.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Net.Sockets.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Net.WebHeaderCollection.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Net.WebSockets.Client.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Net.WebSockets.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.ObjectModel.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Reflection.Emit.ILGeneration.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Reflection.Emit.Lightweight.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Reflection.Emit.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Reflection.Extensions.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Reflection.Primitives.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Reflection.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Resources.Reader.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Resources.ResourceManager.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Resources.Writer.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Runtime.CompilerServices.VisualC.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Runtime.Extensions.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Runtime.Handles.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Runtime.InteropServices.RuntimeInformation.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Runtime.InteropServices.WindowsRuntime.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Runtime.InteropServices.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Runtime.Numerics.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Runtime.Serialization.Formatters.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Runtime.Serialization.Json.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Runtime.Serialization.Primitives.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Runtime.Serialization.Xml.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Runtime.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Security.Claims.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Security.Cryptography.Algorithms.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Security.Cryptography.Csp.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Security.Cryptography.Encoding.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Security.Cryptography.Primitives.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Security.Cryptography.X509Certificates.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Security.Principal.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Security.SecureString.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.ServiceModel.Duplex.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.ServiceModel.Http.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.ServiceModel.NetTcp.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.ServiceModel.Primitives.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.ServiceModel.Security.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Text.Encoding.Extensions.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Text.Encoding.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Text.RegularExpressions.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Threading.Overlapped.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Threading.Tasks.Parallel.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Threading.Tasks.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Threading.Thread.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Threading.ThreadPool.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Threading.Timer.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Threading.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.ValueTuple.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Xml.ReaderWriter.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Xml.XDocument.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Xml.XPath.XDocument.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Xml.XPath.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Xml.XmlDocument.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/System.Xml.XmlSerializer.dll",
|
||||||
|
"build/.NETFramework/v4.8/Facades/netstandard.dll",
|
||||||
|
"build/.NETFramework/v4.8/ISymWrapper.dll",
|
||||||
|
"build/.NETFramework/v4.8/ISymWrapper.xml",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.Activities.Build.dll",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.Activities.Build.xml",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.Build.Conversion.v4.0.dll",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.Build.Conversion.v4.0.xml",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.Build.Engine.dll",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.Build.Engine.xml",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.Build.Framework.dll",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.Build.Framework.xml",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.Build.Tasks.v4.0.dll",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.Build.Tasks.v4.0.xml",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.Build.Utilities.v4.0.dll",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.Build.Utilities.v4.0.xml",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.Build.dll",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.Build.xml",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.CSharp.dll",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.CSharp.xml",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.JScript.dll",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.JScript.xml",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.VisualBasic.Compatibility.Data.dll",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.VisualBasic.Compatibility.Data.xml",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.VisualBasic.Compatibility.dll",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.VisualBasic.Compatibility.xml",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.VisualBasic.dll",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.VisualBasic.xml",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.VisualC.STLCLR.dll",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.VisualC.STLCLR.xml",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.VisualC.dll",
|
||||||
|
"build/.NETFramework/v4.8/Microsoft.VisualC.xml",
|
||||||
|
"build/.NETFramework/v4.8/PermissionSets/FullTrust.xml",
|
||||||
|
"build/.NETFramework/v4.8/PermissionSets/Internet.xml",
|
||||||
|
"build/.NETFramework/v4.8/PermissionSets/LocalIntranet.xml",
|
||||||
|
"build/.NETFramework/v4.8/PresentationBuildTasks.dll",
|
||||||
|
"build/.NETFramework/v4.8/PresentationBuildTasks.xml",
|
||||||
|
"build/.NETFramework/v4.8/PresentationCore.dll",
|
||||||
|
"build/.NETFramework/v4.8/PresentationCore.xml",
|
||||||
|
"build/.NETFramework/v4.8/PresentationFramework.Aero.dll",
|
||||||
|
"build/.NETFramework/v4.8/PresentationFramework.Aero.xml",
|
||||||
|
"build/.NETFramework/v4.8/PresentationFramework.Aero2.dll",
|
||||||
|
"build/.NETFramework/v4.8/PresentationFramework.Aero2.xml",
|
||||||
|
"build/.NETFramework/v4.8/PresentationFramework.AeroLite.dll",
|
||||||
|
"build/.NETFramework/v4.8/PresentationFramework.AeroLite.xml",
|
||||||
|
"build/.NETFramework/v4.8/PresentationFramework.Classic.dll",
|
||||||
|
"build/.NETFramework/v4.8/PresentationFramework.Classic.xml",
|
||||||
|
"build/.NETFramework/v4.8/PresentationFramework.Luna.dll",
|
||||||
|
"build/.NETFramework/v4.8/PresentationFramework.Luna.xml",
|
||||||
|
"build/.NETFramework/v4.8/PresentationFramework.Royale.dll",
|
||||||
|
"build/.NETFramework/v4.8/PresentationFramework.Royale.xml",
|
||||||
|
"build/.NETFramework/v4.8/PresentationFramework.dll",
|
||||||
|
"build/.NETFramework/v4.8/PresentationFramework.xml",
|
||||||
|
"build/.NETFramework/v4.8/ReachFramework.dll",
|
||||||
|
"build/.NETFramework/v4.8/ReachFramework.xml",
|
||||||
|
"build/.NETFramework/v4.8/RedistList/FrameworkList.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Activities.Core.Presentation.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Activities.Core.Presentation.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Activities.DurableInstancing.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Activities.DurableInstancing.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Activities.Presentation.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Activities.Presentation.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Activities.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Activities.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.AddIn.Contract.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.AddIn.Contract.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.AddIn.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.AddIn.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.ComponentModel.Composition.Registration.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.ComponentModel.Composition.Registration.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.ComponentModel.Composition.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.ComponentModel.Composition.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.ComponentModel.DataAnnotations.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.ComponentModel.DataAnnotations.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Configuration.Install.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Configuration.Install.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Configuration.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Configuration.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Core.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Core.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Data.DataSetExtensions.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Data.DataSetExtensions.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Data.Entity.Design.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Data.Entity.Design.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Data.Entity.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Data.Entity.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Data.Linq.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Data.Linq.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Data.OracleClient.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Data.OracleClient.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Data.Services.Client.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Data.Services.Client.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Data.Services.Design.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Data.Services.Design.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Data.Services.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Data.Services.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Data.SqlXml.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Data.SqlXml.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Data.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Data.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Deployment.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Deployment.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Design.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Design.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Device.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Device.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Diagnostics.Tracing.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Diagnostics.Tracing.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.DirectoryServices.AccountManagement.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.DirectoryServices.AccountManagement.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.DirectoryServices.Protocols.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.DirectoryServices.Protocols.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.DirectoryServices.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.DirectoryServices.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Drawing.Design.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Drawing.Design.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Drawing.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Drawing.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Dynamic.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.EnterpriseServices.Thunk.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.EnterpriseServices.Wrapper.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.EnterpriseServices.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.EnterpriseServices.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.IO.Compression.FileSystem.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.IO.Compression.FileSystem.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.IO.Compression.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.IO.Compression.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.IO.Log.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.IO.Log.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.IdentityModel.Selectors.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.IdentityModel.Selectors.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.IdentityModel.Services.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.IdentityModel.Services.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.IdentityModel.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.IdentityModel.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Linq.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Management.Instrumentation.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Management.Instrumentation.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Management.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Management.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Messaging.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Messaging.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Net.Http.WebRequest.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Net.Http.WebRequest.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Net.Http.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Net.Http.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Net.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Net.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Numerics.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Numerics.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Printing.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Printing.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Reflection.Context.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Reflection.Context.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Runtime.Caching.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Runtime.Caching.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Runtime.DurableInstancing.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Runtime.DurableInstancing.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Runtime.Remoting.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Runtime.Remoting.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Runtime.Serialization.Formatters.Soap.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Runtime.Serialization.Formatters.Soap.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Runtime.Serialization.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Runtime.Serialization.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Security.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Security.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.ServiceModel.Activation.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.ServiceModel.Activation.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.ServiceModel.Activities.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.ServiceModel.Activities.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.ServiceModel.Channels.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.ServiceModel.Channels.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.ServiceModel.Discovery.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.ServiceModel.Discovery.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.ServiceModel.Routing.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.ServiceModel.Routing.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.ServiceModel.Web.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.ServiceModel.Web.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.ServiceModel.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.ServiceModel.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.ServiceProcess.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.ServiceProcess.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Speech.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Speech.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Threading.Tasks.Dataflow.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Transactions.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Transactions.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.Abstractions.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.ApplicationServices.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.ApplicationServices.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.DataVisualization.Design.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.DataVisualization.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.DataVisualization.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.DynamicData.Design.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.DynamicData.Design.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.DynamicData.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.DynamicData.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.Entity.Design.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.Entity.Design.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.Entity.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.Entity.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.Extensions.Design.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.Extensions.Design.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.Extensions.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.Extensions.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.Mobile.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.Mobile.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.RegularExpressions.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.RegularExpressions.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.Routing.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.Services.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.Services.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Web.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Windows.Controls.Ribbon.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Windows.Controls.Ribbon.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Windows.Forms.DataVisualization.Design.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Windows.Forms.DataVisualization.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Windows.Forms.DataVisualization.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Windows.Forms.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Windows.Forms.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Windows.Input.Manipulations.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Windows.Input.Manipulations.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Windows.Presentation.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Windows.Presentation.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Windows.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Workflow.Activities.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Workflow.Activities.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Workflow.ComponentModel.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Workflow.ComponentModel.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Workflow.Runtime.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Workflow.Runtime.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.WorkflowServices.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.WorkflowServices.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Xaml.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Xaml.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Xml.Linq.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Xml.Linq.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.Xml.Serialization.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Xml.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.Xml.xml",
|
||||||
|
"build/.NETFramework/v4.8/System.dll",
|
||||||
|
"build/.NETFramework/v4.8/System.xml",
|
||||||
|
"build/.NETFramework/v4.8/UIAutomationClient.dll",
|
||||||
|
"build/.NETFramework/v4.8/UIAutomationClient.xml",
|
||||||
|
"build/.NETFramework/v4.8/UIAutomationClientsideProviders.dll",
|
||||||
|
"build/.NETFramework/v4.8/UIAutomationClientsideProviders.xml",
|
||||||
|
"build/.NETFramework/v4.8/UIAutomationProvider.dll",
|
||||||
|
"build/.NETFramework/v4.8/UIAutomationProvider.xml",
|
||||||
|
"build/.NETFramework/v4.8/UIAutomationTypes.dll",
|
||||||
|
"build/.NETFramework/v4.8/UIAutomationTypes.xml",
|
||||||
|
"build/.NETFramework/v4.8/WindowsBase.dll",
|
||||||
|
"build/.NETFramework/v4.8/WindowsBase.xml",
|
||||||
|
"build/.NETFramework/v4.8/WindowsFormsIntegration.dll",
|
||||||
|
"build/.NETFramework/v4.8/WindowsFormsIntegration.xml",
|
||||||
|
"build/.NETFramework/v4.8/XamlBuildTask.dll",
|
||||||
|
"build/.NETFramework/v4.8/XamlBuildTask.xml",
|
||||||
|
"build/.NETFramework/v4.8/mscorlib.dll",
|
||||||
|
"build/.NETFramework/v4.8/mscorlib.xml",
|
||||||
|
"build/.NETFramework/v4.8/namespaces.xml",
|
||||||
|
"build/.NETFramework/v4.8/sysglobl.dll",
|
||||||
|
"build/.NETFramework/v4.8/sysglobl.xml",
|
||||||
|
"build/Microsoft.NETFramework.ReferenceAssemblies.net48.targets",
|
||||||
|
"microsoft.netframework.referenceassemblies.net48.1.0.3.nupkg.sha512",
|
||||||
|
"microsoft.netframework.referenceassemblies.net48.nuspec"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"projectFileDependencyGroups": {
|
||||||
|
".NETFramework,Version=v4.8": [
|
||||||
|
"Microsoft.NETFramework.ReferenceAssemblies >= 1.0.3"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"packageFolders": {
|
||||||
|
"C:\\Users\\Viper\\.nuget\\packages\\": {}
|
||||||
|
},
|
||||||
|
"project": {
|
||||||
|
"version": "0.0.0",
|
||||||
|
"restore": {
|
||||||
|
"projectUniqueName": "C:\\Users\\Viper\\Desktop\\git-manager-gui\\installer\\GitManagerSetup.csproj",
|
||||||
|
"projectName": "Git-Manager-GUI-Setup",
|
||||||
|
"projectPath": "C:\\Users\\Viper\\Desktop\\git-manager-gui\\installer\\GitManagerSetup.csproj",
|
||||||
|
"packagesPath": "C:\\Users\\Viper\\.nuget\\packages\\",
|
||||||
|
"outputPath": "C:\\Users\\Viper\\Desktop\\git-manager-gui\\installer\\obj\\",
|
||||||
|
"projectStyle": "PackageReference",
|
||||||
|
"configFilePaths": [
|
||||||
|
"C:\\Users\\Viper\\AppData\\Roaming\\NuGet\\NuGet.Config"
|
||||||
|
],
|
||||||
|
"originalTargetFrameworks": [
|
||||||
|
"net48"
|
||||||
|
],
|
||||||
|
"sources": {
|
||||||
|
"https://api.nuget.org/v3/index.json": {}
|
||||||
|
},
|
||||||
|
"frameworks": {
|
||||||
|
"net48": {
|
||||||
|
"targetAlias": "net48",
|
||||||
|
"projectReferences": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"warningProperties": {
|
||||||
|
"warnAsError": [
|
||||||
|
"NU1605"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"restoreAuditProperties": {
|
||||||
|
"enableAudit": "true",
|
||||||
|
"auditLevel": "low",
|
||||||
|
"auditMode": "direct"
|
||||||
|
},
|
||||||
|
"SdkAnalysisLevel": "10.0.100"
|
||||||
|
},
|
||||||
|
"frameworks": {
|
||||||
|
"net48": {
|
||||||
|
"targetAlias": "net48",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.NETFramework.ReferenceAssemblies": {
|
||||||
|
"suppressParent": "All",
|
||||||
|
"target": "Package",
|
||||||
|
"version": "[1.0.3, )",
|
||||||
|
"autoReferenced": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\10.0.113\\RuntimeIdentifierGraph.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"version": 2,
|
||||||
|
"dgSpecHash": "5on68f3wDDM=",
|
||||||
|
"success": true,
|
||||||
|
"projectFilePath": "C:\\Users\\Viper\\Desktop\\git-manager-gui\\installer\\GitManagerSetup.csproj",
|
||||||
|
"expectedPackageFiles": [
|
||||||
|
"C:\\Users\\Viper\\.nuget\\packages\\microsoft.netframework.referenceassemblies\\1.0.3\\microsoft.netframework.referenceassemblies.1.0.3.nupkg.sha512",
|
||||||
|
"C:\\Users\\Viper\\.nuget\\packages\\microsoft.netframework.referenceassemblies.net48\\1.0.3\\microsoft.netframework.referenceassemblies.net48.1.0.3.nupkg.sha512"
|
||||||
|
],
|
||||||
|
"logs": []
|
||||||
|
}
|
||||||
+665
@@ -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 nahtlos — 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 (baldiges Update)</h3>
|
||||||
|
<div class="demo-mockup">
|
||||||
|
Screenshot-Bereich — Demo und UI-Vorschau kommen bald!<br><br>
|
||||||
|
<em>Starte die App mit npm start, um eine 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>
|
||||||
Generated
+1170
-2342
File diff suppressed because it is too large
Load Diff
+48
-16
@@ -1,26 +1,66 @@
|
|||||||
{
|
{
|
||||||
"name": "git-manager-gui",
|
"name": "git-manager-gui",
|
||||||
"version": "1.2.0",
|
"version": "2.2.1",
|
||||||
|
"description": "Git Manager GUI - Verwaltung von Git Repositories",
|
||||||
|
"author": "M_Viper",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "electron .",
|
"start": "electron .",
|
||||||
"build": "electron-builder"
|
"build": "electron-builder --win dir && node scripts/build-setup.js",
|
||||||
|
"test": "node --test __tests__/*.test.js",
|
||||||
|
"pdf": "electron generate-pdf.js",
|
||||||
|
"test:ui": "node scripts/ui-smoke.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.5.0",
|
"archiver": "^6.0.0",
|
||||||
"simple-git": "^3.19.1"
|
"axios": "^1.13.5",
|
||||||
|
"form-data": "^4.0.5",
|
||||||
|
"simple-git": "^3.32.3",
|
||||||
|
"unzipper": "^0.11.0"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"minimatch": "^3.1.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"electron": "^26.2.0",
|
"electron": "^44.6.0",
|
||||||
"electron-builder": "^26.0.12"
|
"electron-builder": "^26.17.0"
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
|
"asar": true,
|
||||||
"appId": "com.viper.gitmanager",
|
"appId": "com.viper.gitmanager",
|
||||||
"productName": "Git Manager GUI",
|
"productName": "Git Manager GUI",
|
||||||
"files": [
|
"files": [
|
||||||
"**/*",
|
"**/*",
|
||||||
"!node_modules/.cache",
|
"!node_modules/.cache",
|
||||||
"!**/*.map"
|
"!**/*.map",
|
||||||
|
"!**/.git",
|
||||||
|
"!**/.git/**",
|
||||||
|
"!repos/**",
|
||||||
|
"!data/**",
|
||||||
|
"!backup/**",
|
||||||
|
"!*.zip",
|
||||||
|
"!*.rar",
|
||||||
|
"!*.log",
|
||||||
|
"!vendor/**",
|
||||||
|
"!installer/**",
|
||||||
|
"!scripts/**",
|
||||||
|
"!__tests__/**",
|
||||||
|
"!assets/icon.ico"
|
||||||
|
],
|
||||||
|
"extraResources": [
|
||||||
|
"repos/",
|
||||||
|
{
|
||||||
|
"from": "data",
|
||||||
|
"to": "data",
|
||||||
|
"filter": [
|
||||||
|
"**/*",
|
||||||
|
"!credentials.json"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "vendor/mingit",
|
||||||
|
"to": "mingit"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"directories": {
|
"directories": {
|
||||||
"buildResources": "assets"
|
"buildResources": "assets"
|
||||||
@@ -28,16 +68,8 @@
|
|||||||
"win": {
|
"win": {
|
||||||
"icon": "assets/icon.png",
|
"icon": "assets/icon.png",
|
||||||
"target": [
|
"target": [
|
||||||
"nsis"
|
"dir"
|
||||||
]
|
]
|
||||||
},
|
|
||||||
"nsis": {
|
|
||||||
"oneClick": false,
|
|
||||||
"perMachine": false,
|
|
||||||
"allowToChangeInstallationDirectory": true,
|
|
||||||
"createDesktopShortcut": true,
|
|
||||||
"createStartMenuShortcut": true,
|
|
||||||
"shortcutName": "Git Manager GUI"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+227
-24
@@ -1,44 +1,247 @@
|
|||||||
// preload.js — expose IPC to renderer
|
// preload.js — expose IPC to renderer
|
||||||
const { contextBridge, ipcRenderer } = require('electron');
|
const { contextBridge, ipcRenderer, webUtils } = require('electron');
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('electronAPI', {
|
contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
|
// Lokale Datei-Operationen
|
||||||
selectFolder: () => ipcRenderer.invoke('select-folder'),
|
selectFolder: () => ipcRenderer.invoke('select-folder'),
|
||||||
selectFile: () => ipcRenderer.invoke('select-file'),
|
selectFile: () => ipcRenderer.invoke('select-file'),
|
||||||
|
getFileTree: (data) => ipcRenderer.invoke('getFileTree', data),
|
||||||
|
readFile: (data) => ipcRenderer.invoke('readFile', data),
|
||||||
|
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),
|
||||||
|
uploadGiteaFile: (data) => ipcRenderer.invoke('upload-gitea-file', data),
|
||||||
|
downloadGiteaFolder: (data) => ipcRenderer.invoke('download-gitea-folder', data),
|
||||||
|
downloadGiteaFile: (data) => ipcRenderer.invoke('download-gitea-file', data),
|
||||||
|
uploadLocalFolderToGitea: (data) => ipcRenderer.invoke('upload-local-folder-to-gitea', data),
|
||||||
|
uploadPathsToGitea: (data) => ipcRenderer.invoke('upload-paths-to-gitea', data),
|
||||||
|
cancelUpload: () => ipcRenderer.invoke('cancel-upload'),
|
||||||
|
pauseUpload: () => ipcRenderer.invoke('pause-upload'),
|
||||||
|
resumeUpload: () => ipcRenderer.invoke('resume-upload'),
|
||||||
|
getPauseState: () => ipcRenderer.invoke('get-pause-state'),
|
||||||
|
onTransferPauseState: (cb) => {
|
||||||
|
const listener = (_event, payload) => cb(payload || {});
|
||||||
|
ipcRenderer.on('transfer-pause-state', listener);
|
||||||
|
return () => ipcRenderer.removeListener('transfer-pause-state', listener);
|
||||||
|
},
|
||||||
|
getUploadHistory: (data) => ipcRenderer.invoke('get-upload-history', data),
|
||||||
|
clearUploadHistory: () => ipcRenderer.invoke('clear-upload-history'),
|
||||||
|
exportUploadHistoryCsv: (data) => ipcRenderer.invoke('export-upload-history-csv', data),
|
||||||
|
getGitignoreTemplates: () => ipcRenderer.invoke('get-gitignore-templates'),
|
||||||
|
buildGitignore: (data) => ipcRenderer.invoke('build-gitignore', data),
|
||||||
|
|
||||||
|
// Repository & Git Management
|
||||||
saveCredentials: (data) => ipcRenderer.invoke('save-credentials', data),
|
saveCredentials: (data) => ipcRenderer.invoke('save-credentials', data),
|
||||||
loadCredentials: () => ipcRenderer.invoke('load-credentials'),
|
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),
|
createRepo: (data) => ipcRenderer.invoke('create-repo', data),
|
||||||
pushProject: (data) => ipcRenderer.invoke('push-project', data),
|
pushProject: (data) => ipcRenderer.invoke('push-project', data),
|
||||||
getBranches: (data) => ipcRenderer.invoke('getBranches', data),
|
getBranches: (data) => ipcRenderer.invoke('getBranches', data),
|
||||||
getCommitLogs: (data) => ipcRenderer.invoke('getCommitLogs', data),
|
getCommitLogs: (data) => ipcRenderer.invoke('getCommitLogs', data),
|
||||||
getFileTree: (data) => ipcRenderer.invoke('getFileTree', data),
|
|
||||||
readFile: (data) => ipcRenderer.invoke('readFile', data),
|
|
||||||
deleteFile: (data) => ipcRenderer.invoke('deleteFile', data),
|
|
||||||
listGiteaRepos: (data) => ipcRenderer.invoke('list-gitea-repos', data),
|
|
||||||
getGiteaRepoContents: (data) => ipcRenderer.invoke('get-gitea-repo-contents', data),
|
|
||||||
getGiteaFileContent: (data) => ipcRenderer.invoke('get-gitea-file-content', data),
|
|
||||||
uploadGiteaFile: (data) => ipcRenderer.invoke('upload-gitea-file', data),
|
|
||||||
uploadLocalFolderToGitea: (data) => ipcRenderer.invoke('upload-local-folder-to-gitea', data),
|
|
||||||
downloadGiteaFolder: (data) => ipcRenderer.invoke('download-gitea-folder', data),
|
|
||||||
downloadGiteaFile: (data) => ipcRenderer.invoke('download-gitea-file', data),
|
|
||||||
prepareDownloadDrag: (data) => ipcRenderer.invoke('prepare-download-drag', data),
|
|
||||||
startNativeDrag: (filePath) => ipcRenderer.send('ondragstart', filePath),
|
|
||||||
uploadAndPush: (data) => ipcRenderer.invoke('upload-and-push', data),
|
uploadAndPush: (data) => ipcRenderer.invoke('upload-and-push', data),
|
||||||
deleteGiteaRepo: (data) => ipcRenderer.invoke('delete-gitea-repo', 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),
|
||||||
|
getRelease: (data) => ipcRenderer.invoke('get-release', data),
|
||||||
|
createRelease: (data) => ipcRenderer.invoke('create-release', data),
|
||||||
|
editRelease: (data) => ipcRenderer.invoke('edit-release', data),
|
||||||
|
deleteRelease: (data) => ipcRenderer.invoke('delete-release', data),
|
||||||
|
uploadReleaseAsset: (data) => ipcRenderer.invoke('upload-release-asset', data),
|
||||||
|
inspectReleaseFile: (data) => ipcRenderer.invoke('inspect-release-file', data),
|
||||||
|
prepareRelease: (data) => ipcRenderer.invoke('prepare-release', data),
|
||||||
|
deleteReleaseAsset: (data) => ipcRenderer.invoke('delete-release-asset', data),
|
||||||
|
downloadReleaseArchive: (data) => ipcRenderer.invoke('download-release-archive', data),
|
||||||
|
|
||||||
|
// Commit History & Visualization
|
||||||
|
getCommits: (data) => ipcRenderer.invoke('get-commits', data),
|
||||||
|
getCommitDiff: (data) => ipcRenderer.invoke('get-commit-diff', data),
|
||||||
|
getCommitFiles: (data) => ipcRenderer.invoke('get-commit-files', data),
|
||||||
|
searchCommits: (data) => ipcRenderer.invoke('search-commits', data),
|
||||||
|
getRepoBranches: (data) => ipcRenderer.invoke('get-repo-branches', data),
|
||||||
|
|
||||||
|
// Local Repository Commits
|
||||||
|
getLocalCommits: (data) => ipcRenderer.invoke('get-local-commits', data),
|
||||||
|
getLocalCommitDiff: (data) => ipcRenderer.invoke('get-local-commit-diff', data),
|
||||||
|
findGitignoredInRepo: (data) => ipcRenderer.invoke('find-gitignored-in-repo', data),
|
||||||
|
deleteGiteaPaths: (data) => ipcRenderer.invoke('delete-gitea-paths', data),
|
||||||
|
getLocalCommitFiles: (data) => ipcRenderer.invoke('get-local-commit-files', data),
|
||||||
|
getLocalCommitDetails: (data) => ipcRenderer.invoke('get-local-commit-details', 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: (options) => ipcRenderer.invoke('check-for-updates', options || {}),
|
||||||
|
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
|
||||||
|
|
||||||
|
// Triggert den tatsächlichen Download des Assets
|
||||||
|
// WICHTIG: Muss in renderer.js als window.electronAPI.startUpdateDownload(asset) aufgerufen werden
|
||||||
|
startUpdateDownload: (asset) => ipcRenderer.invoke('start-update-download', asset),
|
||||||
|
|
||||||
|
// === Progress & Update Subscriptions ===
|
||||||
|
onUpdateAvailable: (cb) => {
|
||||||
|
const listener = (event, info) => cb(info);
|
||||||
|
ipcRenderer.on('update-available', listener);
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
|
||||||
// progress subscriptions
|
|
||||||
onPushProgress: (cb) => {
|
onPushProgress: (cb) => {
|
||||||
ipcRenderer.on('push-progress', (event, percent) => {
|
const listener = (event, percent) => { try { cb(percent); } catch (_) {} };
|
||||||
try { cb(percent); } catch (_) {}
|
ipcRenderer.on('push-progress', listener);
|
||||||
});
|
return () => ipcRenderer.removeListener('push-progress', listener);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Neue Funktionen
|
||||||
|
prefsGet: () => ipcRenderer.invoke('prefs-get'),
|
||||||
|
prefsSet: (data) => ipcRenderer.invoke('prefs-set', data),
|
||||||
|
openKnownFolder: (which) => ipcRenderer.invoke('open-known-folder', { which }),
|
||||||
|
gitTransportState: () => ipcRenderer.invoke('git-transport-state'),
|
||||||
|
gitTransportClear: (host) => ipcRenderer.invoke('git-transport-clear', { host }),
|
||||||
|
previewUploadPaths: (data) => ipcRenderer.invoke('preview-upload-paths', data),
|
||||||
|
runServerDiagnosis: (data) => ipcRenderer.invoke('run-server-diagnosis', data),
|
||||||
|
localGit: (data) => ipcRenderer.invoke('local-git', data),
|
||||||
|
giteaCollab: (data) => ipcRenderer.invoke('gitea-collab', data),
|
||||||
|
repoBackupGetConfig: () => ipcRenderer.invoke('repo-backup-get-config'),
|
||||||
|
repoBackupSetConfig: (data) => ipcRenderer.invoke('repo-backup-set-config', data),
|
||||||
|
repoBackupRunNow: () => ipcRenderer.invoke('repo-backup-run-now'),
|
||||||
|
repoBackupCancel: () => ipcRenderer.invoke('repo-backup-cancel'),
|
||||||
|
repoBackupList: (data) => ipcRenderer.invoke('repo-backup-list', data),
|
||||||
|
repoRestoreLocal: (data) => ipcRenderer.invoke('repo-restore-local', data),
|
||||||
|
repoRestoreGitea: (data) => ipcRenderer.invoke('repo-restore-gitea', data),
|
||||||
|
onRepoRestoreProgress: (cb) => {
|
||||||
|
const listener = (event, payload) => { try { cb(payload); } catch (_) {} };
|
||||||
|
ipcRenderer.on('repo-restore-progress', listener);
|
||||||
|
return () => ipcRenderer.removeListener('repo-restore-progress', listener);
|
||||||
|
},
|
||||||
|
onRepoBackupProgress: (cb) => {
|
||||||
|
const listener = (event, payload) => { try { cb(payload); } catch (_) {} };
|
||||||
|
ipcRenderer.on('repo-backup-progress', listener);
|
||||||
|
return () => ipcRenderer.removeListener('repo-backup-progress', listener);
|
||||||
|
},
|
||||||
|
// Tray-Menü → "Upload-Verlauf": Renderer soll den Verlauf-Dialog öffnen.
|
||||||
|
onOpenUploadHistory: (cb) => {
|
||||||
|
const listener = () => cb();
|
||||||
|
ipcRenderer.on('open-upload-history', listener);
|
||||||
|
return () => ipcRenderer.removeListener('open-upload-history', listener);
|
||||||
},
|
},
|
||||||
onFolderUploadProgress: (cb) => {
|
onFolderUploadProgress: (cb) => {
|
||||||
ipcRenderer.on('folder-upload-progress', (event, payload) => {
|
const listener = (event, payload) => { try { cb(payload); } catch (_) {} };
|
||||||
try { cb(payload); } catch (_) {}
|
ipcRenderer.on('folder-upload-progress', listener);
|
||||||
});
|
return () => ipcRenderer.removeListener('folder-upload-progress', listener);
|
||||||
},
|
},
|
||||||
|
|
||||||
onFolderDownloadProgress: (cb) => {
|
onFolderDownloadProgress: (cb) => {
|
||||||
ipcRenderer.on('folder-download-progress', (event, payload) => {
|
const listener = (event, payload) => { try { cb(payload); } catch (_) {} };
|
||||||
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')
|
||||||
});
|
});
|
||||||
+161
-51
@@ -4,39 +4,42 @@ import Settings from './Settings.jsx';
|
|||||||
export default function App() {
|
export default function App() {
|
||||||
const [folder, setFolder] = useState('');
|
const [folder, setFolder] = useState('');
|
||||||
const [repoName, setRepoName] = useState('');
|
const [repoName, setRepoName] = useState('');
|
||||||
const [platform, setPlatform] = useState('github');
|
const [platform, setPlatform] = useState('gitea');
|
||||||
const [status, setStatus] = useState('');
|
const [status, setStatus] = useState('Bereit');
|
||||||
const [showSettings, setShowSettings] = useState(false);
|
const [showSettings, setShowSettings] = useState(false);
|
||||||
const [branches, setBranches] = useState([]);
|
const [branches, setBranches] = useState([]);
|
||||||
const [selectedBranch, setSelectedBranch] = useState('master');
|
const [selectedBranch, setSelectedBranch] = useState('main');
|
||||||
const [logs, setLogs] = useState([]);
|
const [logs, setLogs] = useState([]);
|
||||||
const [progress, setProgress] = useState(0);
|
const [progress, setProgress] = useState(0);
|
||||||
|
|
||||||
async function selectFolder() {
|
async function selectFolder() {
|
||||||
const selected = await window.electronAPI.selectFolder();
|
const selected = await window.electronAPI.selectFolder();
|
||||||
if (selected) setFolder(selected);
|
if (!selected) return;
|
||||||
// Branches laden
|
setFolder(selected);
|
||||||
if (selected) {
|
|
||||||
const branchList = await window.electronAPI.getBranches({ folder: selected });
|
const branchList = await window.electronAPI.getBranches({ folder: selected });
|
||||||
setBranches(branchList);
|
setBranches(branchList);
|
||||||
if (branchList.includes('master')) setSelectedBranch('master');
|
if (branchList.includes('main')) setSelectedBranch('main');
|
||||||
}
|
else if (branchList.includes('master')) setSelectedBranch('master');
|
||||||
|
else if (branchList.length > 0) setSelectedBranch(branchList[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createRepoHandler() {
|
async function createRepoHandler() {
|
||||||
if (!repoName) return alert('Repo Name required!');
|
if (!repoName) return alert('Repo-Name erforderlich!');
|
||||||
setStatus('Creating repository...');
|
setStatus('Repository wird erstellt…');
|
||||||
const result = await window.electronAPI.createRepo({ name: repoName, platform });
|
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() {
|
async function pushProjectHandler() {
|
||||||
if (!folder) return alert('Select a project folder first!');
|
if (!folder) return alert('Bitte zuerst einen Projektordner auswählen!');
|
||||||
setStatus('Pushing project...');
|
setStatus('Projekt wird gepusht…');
|
||||||
setProgress(0);
|
setProgress(0);
|
||||||
const onProgress = (p) => setProgress(p); // Callback für Fortschritt
|
const result = await window.electronAPI.pushProject({
|
||||||
const result = await window.electronAPI.pushProject({ folder, branch: selectedBranch, onProgress });
|
folder,
|
||||||
setStatus(result ? 'Project pushed!' : 'Failed to push project.');
|
branch: selectedBranch,
|
||||||
|
onProgress: (p) => setProgress(p),
|
||||||
|
});
|
||||||
|
setStatus(result ? 'Projekt gepusht!' : 'Push fehlgeschlagen.');
|
||||||
if (result) {
|
if (result) {
|
||||||
const logList = await window.electronAPI.getCommitLogs({ folder });
|
const logList = await window.electronAPI.getCommitLogs({ folder });
|
||||||
setLogs(logList);
|
setLogs(logList);
|
||||||
@@ -44,61 +47,168 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: 20 }}>
|
<div id="app" style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
|
||||||
<h1>Git Manager GUI - High-End</h1>
|
|
||||||
<button onClick={() => setShowSettings(!showSettings)}>Settings</button>
|
|
||||||
{showSettings && <Settings />}
|
|
||||||
|
|
||||||
<div style={{ marginTop: 20 }}>
|
{/* ── Toolbar ── */}
|
||||||
<label>Platform:</label>
|
<div id="toolbar">
|
||||||
<select value={platform} onChange={e => setPlatform(e.target.value)}>
|
<div className="toolbar-row toolbar-row--top">
|
||||||
<option value="github">GitHub</option>
|
<div className="toolbar-brand">
|
||||||
<option value="gitea">Gitea</option>
|
<div className="toolbar-brand-mark">
|
||||||
</select>
|
<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>
|
||||||
|
|
||||||
<div style={{ marginTop: 20 }}>
|
<div className="toolbar-top-actions">
|
||||||
<button onClick={selectFolder}>Select Project Folder</button>
|
<div className="tool-group tool-group--quick-actions">
|
||||||
<span style={{ marginLeft: 10 }}>{folder}</span>
|
<button onClick={createRepoHandler} title="Neues Repository erstellen">🚀 New Repo</button>
|
||||||
|
<button onClick={pushProjectHandler} title="Projekt pushen">⬆️ Push</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ marginTop: 20 }}>
|
<div className="tool-group tool-group--utility">
|
||||||
<label>Branch:</label>
|
<button onClick={() => setShowSettings(true)} title="Einstellungen">⚙️ Settings</button>
|
||||||
<select value={selectedBranch} onChange={e => setSelectedBranch(e.target.value)}>
|
</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>)}
|
{branches.map(b => <option key={b} value={b}>{b}</option>)}
|
||||||
</select>
|
</select>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ marginTop: 20 }}>
|
{/* ── Main ── */}
|
||||||
|
<main id="main">
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Repo erstellen / Ordner */}
|
||||||
|
<div className="card" style={{ marginBottom: 20 }}>
|
||||||
|
<h2>📁 Projekt & Repository</h2>
|
||||||
|
|
||||||
|
<div className="input-group">
|
||||||
|
<label>Lokaler Projektordner</label>
|
||||||
|
<div style={{ display: 'flex', gap: 10, alignItems: 'center' }}>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Repository Name"
|
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 className="input-group">
|
||||||
|
<label>Repository Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="mein-projekt"
|
||||||
value={repoName}
|
value={repoName}
|
||||||
onChange={e => setRepoName(e.target.value)} />
|
onChange={e => setRepoName(e.target.value)}
|
||||||
<button onClick={createRepoHandler} style={{ marginLeft: 10 }}>Create Repo</button>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ marginTop: 20 }}>
|
<div style={{ display: 'flex', gap: 10, marginTop: 8 }}>
|
||||||
<button onClick={pushProjectHandler}>Push / Update Project</button>
|
<button className="accent-btn" onClick={createRepoHandler}>🚀 Repo erstellen</button>
|
||||||
|
<button onClick={pushProjectHandler}>⬆️ Push / Aktualisieren</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ marginTop: 10 }}>
|
{/* Commit-Logs */}
|
||||||
<label>Progress:</label>
|
{logs.length > 0 && (
|
||||||
<progress value={progress} max="100" style={{ width: '100%' }} />
|
<div className="card">
|
||||||
</div>
|
<h2>📊 Commit-Verlauf</h2>
|
||||||
|
<ul style={{ listStyle: 'none', padding: 0, margin: 0, display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||||
<div style={{ marginTop: 20 }}>
|
|
||||||
<strong>Status: </strong>{status}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ marginTop: 20 }}>
|
|
||||||
<h3>Commit Logs:</h3>
|
|
||||||
<ul>
|
|
||||||
{logs.map((log, i) => (
|
{logs.map((log, i) => (
|
||||||
<li key={i}>{log}</li>
|
<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>
|
</ul>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+157
-15
@@ -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 [githubToken, setGithubToken] = useState('');
|
||||||
const [giteaToken, setGiteaToken] = useState('');
|
const [giteaToken, setGiteaToken] = useState('');
|
||||||
const [giteaURL, setGiteaURL] = 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(() => {
|
useEffect(() => {
|
||||||
window.electronAPI.loadCredentials().then(data => {
|
window.electronAPI.loadCredentials().then(data => {
|
||||||
@@ -11,31 +39,145 @@ export default function Settings() {
|
|||||||
setGithubToken(data.githubToken || '');
|
setGithubToken(data.githubToken || '');
|
||||||
setGiteaToken(data.giteaToken || '');
|
setGiteaToken(data.giteaToken || '');
|
||||||
setGiteaURL(data.giteaURL || '');
|
setGiteaURL(data.giteaURL || '');
|
||||||
|
setAvatarB64(data.avatarB64 || null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const save = () => {
|
function handleAvatarFileChange(e) {
|
||||||
window.electronAPI.saveCredentials({ githubToken, giteaToken, giteaURL });
|
const file = e.target.files[0];
|
||||||
alert('Settings saved securely!');
|
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 (
|
return (
|
||||||
<div style={{ padding: 20 }}>
|
<div className="modalContent card settings-modal-content">
|
||||||
<h2>Settings</h2>
|
|
||||||
<div>
|
<div className="settings-header">
|
||||||
<label>GitHub Token:</label>
|
<div className="settings-header-inner">
|
||||||
<input type="password" value={githubToken} onChange={e => setGithubToken(e.target.value)} />
|
<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>
|
<div>
|
||||||
<label>Gitea Token:</label>
|
<div className="settings-eyebrow">Konfiguration</div>
|
||||||
<input type="password" value={giteaToken} onChange={e => setGiteaToken(e.target.value)} />
|
<h2>⚙️ Einstellungen</h2>
|
||||||
|
<p className="settings-subtitle">
|
||||||
|
Zugangsdaten für GitHub und Gitea hinterlegen.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="settings-layout">
|
||||||
|
<div className="settings-column settings-column--left">
|
||||||
|
<section className="settings-panel settings-panel--credentials">
|
||||||
|
<div className="settings-panel-header">
|
||||||
<div>
|
<div>
|
||||||
<label>Gitea URL:</label>
|
<h3>Zugangsdaten</h3>
|
||||||
<input type="text" value={giteaURL} onChange={e => setGiteaURL(e.target.value)} />
|
<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 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>
|
</div>
|
||||||
<button onClick={save} style={{ marginTop: 10 }}>Save</button>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,918 @@
|
|||||||
|
// renderer/features.js
|
||||||
|
// Oberfläche für: Upload-Vorschau mit Geheimnis-Warnung, lokale Änderungen, Pull Requests &
|
||||||
|
// Issues, Server-Diagnose und Repo-Backup. Wird nach renderer.js geladen und nutzt dessen
|
||||||
|
// globale Helfer ($, currentState, selectedFolder, showProgress, showSuccess, ...).
|
||||||
|
// Inhalte vom Server werden ausschließlich per textContent eingesetzt, nie als HTML.
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/* ---------- kleine Helfer ---------- */
|
||||||
|
function el(tag, props = {}, children = []) {
|
||||||
|
const node = document.createElement(tag);
|
||||||
|
for (const [k, v] of Object.entries(props)) {
|
||||||
|
if (v == null) continue;
|
||||||
|
if (k === 'class') node.className = v;
|
||||||
|
else if (k === 'text') node.textContent = v;
|
||||||
|
else if (k.startsWith('on')) node.addEventListener(k.slice(2), v);
|
||||||
|
else if (k in node) node[k] = v;
|
||||||
|
else node.setAttribute(k, v);
|
||||||
|
}
|
||||||
|
for (const c of [].concat(children)) if (c != null) node.append(c);
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtBytes(n) {
|
||||||
|
if (n < 1024) return `${n} B`;
|
||||||
|
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
||||||
|
return `${(n / 1024 / 1024).toFixed(1)} MB`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtDate(iso) {
|
||||||
|
if (!iso) return '';
|
||||||
|
try { return new Date(iso).toLocaleString('de-DE', { dateStyle: 'medium', timeStyle: 'short' }); } catch (_) { return iso; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function fxModal({ title, wide = false }) {
|
||||||
|
const titleEl = el('h2', { class: 'fx-title', text: title });
|
||||||
|
const closeBtn = el('button', { class: 'fx-close', type: 'button', title: 'Schließen', text: '✕' });
|
||||||
|
const body = el('div', { class: 'fx-body' });
|
||||||
|
const footer = el('div', { class: 'fx-footer' });
|
||||||
|
const card = el('div', { class: `modalContent card fx-card${wide ? ' fx-card--wide' : ''}` }, [
|
||||||
|
el('div', { class: 'fx-header' }, [titleEl, closeBtn]), body, footer
|
||||||
|
]);
|
||||||
|
const root = el('div', { class: 'modal fx-modal' }, [card]);
|
||||||
|
let onClose = null;
|
||||||
|
const close = (result) => {
|
||||||
|
root.remove();
|
||||||
|
document.removeEventListener('keydown', onKey);
|
||||||
|
if (onClose) onClose(result);
|
||||||
|
};
|
||||||
|
const onKey = (e) => {
|
||||||
|
if (e.key !== 'Escape') return;
|
||||||
|
// Nur den obersten Dialog schließen (z. B. eine Rückfrage über diesem Fenster)
|
||||||
|
if (document.querySelector('.confirm-modal') || root.nextElementSibling && root.nextElementSibling.classList.contains('fx-modal')) return;
|
||||||
|
close(undefined);
|
||||||
|
};
|
||||||
|
closeBtn.onclick = () => close(undefined);
|
||||||
|
root.addEventListener('mousedown', (e) => { if (e.target === root) close(undefined); });
|
||||||
|
document.addEventListener('keydown', onKey);
|
||||||
|
document.body.appendChild(root);
|
||||||
|
return {
|
||||||
|
root, body, footer, close,
|
||||||
|
setTitle: (t) => { titleEl.textContent = t; },
|
||||||
|
onClose: (fn) => { onClose = fn; }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function button(text, onclick, variant = 'secondary') {
|
||||||
|
return el('button', { type: 'button', class: `fx-btn fx-btn--${variant}`, text, onclick });
|
||||||
|
}
|
||||||
|
|
||||||
|
function busy(container, text) {
|
||||||
|
container.textContent = '';
|
||||||
|
container.append(el('div', { class: 'fx-busy', text }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Knöpfe je nach Ansicht ---------- */
|
||||||
|
window.updateFeatureButtons = function (mode, owner, repo) {
|
||||||
|
const isGitea = currentState.platform !== 'github';
|
||||||
|
const btnPulls = $('btnPulls');
|
||||||
|
const btnIssues = $('btnIssues');
|
||||||
|
const btnLocal = $('btnLocalChanges');
|
||||||
|
const showRepo = mode === 'repo' && isGitea;
|
||||||
|
if (btnPulls) {
|
||||||
|
btnPulls.classList.toggle('hidden', !showRepo);
|
||||||
|
btnPulls.onclick = () => openCollab(owner, repo, 'pulls');
|
||||||
|
}
|
||||||
|
if (btnIssues) {
|
||||||
|
btnIssues.classList.toggle('hidden', !showRepo);
|
||||||
|
btnIssues.onclick = () => openCollab(owner, repo, 'issues');
|
||||||
|
}
|
||||||
|
if (btnLocal) {
|
||||||
|
btnLocal.classList.toggle('hidden', mode !== 'local');
|
||||||
|
btnLocal.onclick = () => openLocalChanges(selectedFolder);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
1. Upload-Vorschau mit Geheimnis-Warnung
|
||||||
|
================================================================ */
|
||||||
|
window.uploadPreviewGate = async function ({ paths, destPath, repo }) {
|
||||||
|
showProgress(5, 'Prüfe Dateien vor dem Upload ...');
|
||||||
|
let res;
|
||||||
|
try {
|
||||||
|
res = await window.electronAPI.previewUploadPaths({ paths, destPath });
|
||||||
|
} catch (e) {
|
||||||
|
res = { ok: false, error: String(e) };
|
||||||
|
} finally {
|
||||||
|
hideProgress();
|
||||||
|
}
|
||||||
|
if (!res || !res.ok) {
|
||||||
|
// Die Vorschau darf einen Upload nie verhindern.
|
||||||
|
showWarning('Upload-Vorschau nicht möglich: ' + (res && res.error ? res.error : 'unbekannter Fehler'));
|
||||||
|
return { proceed: true, excludeFiles: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = res.files || [];
|
||||||
|
const secrets = files.filter(f => f.secret);
|
||||||
|
const ignored = res.ignored || [];
|
||||||
|
const baseDir = paths.length ? paths[0].replace(/[\\/][^\\/]+$/, '') : '';
|
||||||
|
const relLocal = (p) => (baseDir && p.startsWith(baseDir) ? p.slice(baseDir.length + 1) : p).split('\\').join('/');
|
||||||
|
|
||||||
|
// Einstellung "Upload-Vorschau: nur bei Auffälligkeiten" – ohne Zugangsdaten und ohne Dateiflut direkt hochladen
|
||||||
|
const blocked = res.blocked || [];
|
||||||
|
if (res.previewMode === 'warnings' && secrets.length === 0 && blocked.length === 0 && files.length > 0 && files.length <= 1000) {
|
||||||
|
return { proceed: true, excludeFiles: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const m = fxModal({ title: '📤 Upload-Vorschau', wide: true });
|
||||||
|
let settled = false;
|
||||||
|
const finish = (result) => { if (!settled) { settled = true; m.close(); resolve(result); } };
|
||||||
|
m.onClose(() => { if (!settled) { settled = true; resolve({ proceed: false }); } });
|
||||||
|
|
||||||
|
const target = destPath ? `${repo}/${destPath}` : repo;
|
||||||
|
m.body.append(el('p', { class: 'fx-lead', text: `${files.length} Dateien (${fmtBytes(res.totalBytes || 0)}) werden nach „${target}“ hochgeladen.` }));
|
||||||
|
|
||||||
|
if (files.length > 1000) {
|
||||||
|
m.body.append(el('div', { class: 'fx-alert fx-alert--warn', text:
|
||||||
|
`Das sind sehr viele Dateien. Prüfe, ob Build-Ordner (z. B. build/, dist/, node_modules/) in der .gitignore stehen.` }));
|
||||||
|
}
|
||||||
|
if (files.length === 0) {
|
||||||
|
m.body.append(el('div', { class: 'fx-alert fx-alert--warn', text: 'Es bleibt keine Datei zum Hochladen übrig.' }));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blocked.length) {
|
||||||
|
const list = el('div', { class: 'fx-list' });
|
||||||
|
for (const b of blocked.slice(0, 25)) {
|
||||||
|
list.append(el('div', { class: 'fx-list-row' }, [
|
||||||
|
el('span', { class: 'fx-mono', text: b.targetPath || b.path }),
|
||||||
|
el('span', { class: 'fx-muted', text: b.reason })
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
m.body.append(el('div', { class: 'fx-alert fx-alert--danger' }, [
|
||||||
|
el('strong', { text: `🛡️ ${blocked.length} Datei(en) gesperrt und werden nicht hochgeladen` }),
|
||||||
|
el('div', { text: 'Dateien mit Zugangsdaten werden nie hochgeladen. Die Sperre lässt sich unter Einstellungen → Uploads abschalten.' }),
|
||||||
|
list
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
const secretChecks = [];
|
||||||
|
if (secrets.length) {
|
||||||
|
const list = el('div', { class: 'fx-list' });
|
||||||
|
for (const f of secrets) {
|
||||||
|
const cb = el('input', { type: 'checkbox', checked: true });
|
||||||
|
secretChecks.push({ cb, file: f.localFile });
|
||||||
|
list.append(el('label', { class: 'fx-list-row fx-list-row--check' }, [
|
||||||
|
cb,
|
||||||
|
el('span', { class: 'fx-mono', text: f.targetPath }),
|
||||||
|
el('span', { class: 'fx-muted', text: f.secret })
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
m.body.append(el('div', { class: 'fx-alert fx-alert--danger' }, [
|
||||||
|
el('strong', { text: `⚠️ ${secrets.length} Datei(en) enthalten vermutlich Zugangsdaten` }),
|
||||||
|
el('div', { text: 'Angehakte Dateien werden NICHT hochgeladen. Haken nur entfernen, wenn die Datei wirklich ins Repo gehört (z. B. bei einem privaten Repo bewusst gewollt).' }),
|
||||||
|
list,
|
||||||
|
el('div', { class: 'fx-muted', text: 'Hinweis: Dateien, die schon im Repo liegen, bleiben dort, bis du sie löschst. Ein geleakter Schlüssel sollte zusätzlich erneuert werden.' })
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ignored.length) {
|
||||||
|
const list = el('div', { class: 'fx-list fx-list--compact' });
|
||||||
|
ignored.slice(0, 25).forEach(i => list.append(el('div', { class: 'fx-list-row fx-mono', text: relLocal(i.path) + (i.isDir ? '/' : '') })));
|
||||||
|
if (ignored.length > 25) list.append(el('div', { class: 'fx-list-row fx-muted', text: `… und ${ignored.length - 25} weitere` }));
|
||||||
|
m.body.append(el('div', { class: 'fx-section' }, [
|
||||||
|
el('h3', { text: `🚫 Von .gitignore aussortiert (${ignored.length})` }),
|
||||||
|
list
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (files.length) {
|
||||||
|
const list = el('div', { class: 'fx-list fx-list--compact' });
|
||||||
|
files.slice(0, 500).forEach(f => list.append(el('div', { class: 'fx-list-row' }, [
|
||||||
|
el('span', { class: 'fx-mono', text: f.targetPath }),
|
||||||
|
el('span', { class: 'fx-muted', text: fmtBytes(f.size) })
|
||||||
|
])));
|
||||||
|
if (files.length > 500) list.append(el('div', { class: 'fx-list-row fx-muted', text: `… und ${files.length - 500} weitere` }));
|
||||||
|
m.body.append(el('details', { class: 'fx-section' }, [
|
||||||
|
el('summary', { text: `Alle ${files.length} Dateien anzeigen` }),
|
||||||
|
list
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
m.footer.append(
|
||||||
|
button('Abbrechen', () => finish({ proceed: false })),
|
||||||
|
button(files.length ? 'Hochladen' : 'Schließen', () => {
|
||||||
|
if (!files.length) return finish({ proceed: false });
|
||||||
|
const excludeFiles = secretChecks.filter(s => s.cb.checked).map(s => s.file);
|
||||||
|
if (excludeFiles.length >= files.length) return finish({ proceed: false });
|
||||||
|
finish({ proceed: true, excludeFiles });
|
||||||
|
}, 'primary')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
2. Lokale Änderungen: Status, Diff, Commit, Push
|
||||||
|
================================================================ */
|
||||||
|
async function openLocalChanges(folder) {
|
||||||
|
if (!folder) { showWarning('Bitte zuerst einen lokalen Ordner öffnen.'); return; }
|
||||||
|
const m = fxModal({ title: '📝 Lokale Änderungen', wide: true });
|
||||||
|
const info = el('div', { class: 'fx-lead' });
|
||||||
|
const listPane = el('div', { class: 'fx-pane fx-pane--list' });
|
||||||
|
const diffPane = el('pre', { class: 'fx-pane fx-diff', text: 'Datei anklicken, um die Änderungen zu sehen.' });
|
||||||
|
m.body.append(info, el('div', { class: 'fx-split' }, [listPane, diffPane]));
|
||||||
|
|
||||||
|
const msg = el('input', { type: 'text', class: 'fx-input', placeholder: 'Commit-Nachricht, z. B. „Login-Fehler behoben“' });
|
||||||
|
const btnCommit = button('Commit', () => commit(false), 'primary');
|
||||||
|
const btnCommitPush = button('Commit & Push', () => commit(true), 'primary');
|
||||||
|
const btnRefresh = button('↻ Aktualisieren', () => load());
|
||||||
|
const btnPushOnly = button('⬆ Nur pushen', () => push());
|
||||||
|
m.footer.append(msg, btnRefresh, btnPushOnly, btnCommit, btnCommitPush);
|
||||||
|
|
||||||
|
let files = [];
|
||||||
|
const checks = new Map();
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
busy(listPane, 'Lade Status ...');
|
||||||
|
const res = await window.electronAPI.localGit({ action: 'status', folder });
|
||||||
|
if (!res.ok) {
|
||||||
|
listPane.textContent = '';
|
||||||
|
listPane.append(el('div', { class: 'fx-alert fx-alert--warn', text: res.notRepo
|
||||||
|
? 'Dieser Ordner ist noch kein Git-Repository. Über „Push“ in der Werkzeugleiste kannst du ihn als Repo anlegen und hochladen.'
|
||||||
|
: (res.error || 'Status nicht lesbar') }));
|
||||||
|
[btnCommit, btnCommitPush, btnPushOnly].forEach(b => { b.disabled = true; });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
files = res.files;
|
||||||
|
info.textContent = `Branch: ${res.branch}` + (res.ahead ? ` · ${res.ahead} Commit(s) noch nicht gepusht` : '') + ` · ${files.length} geänderte Datei(en)`;
|
||||||
|
btnPushOnly.disabled = !res.ahead;
|
||||||
|
listPane.textContent = '';
|
||||||
|
checks.clear();
|
||||||
|
if (!files.length) {
|
||||||
|
listPane.append(el('div', { class: 'fx-empty', text: '✅ Keine Änderungen. Alles ist committet.' }));
|
||||||
|
diffPane.textContent = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const all = el('input', { type: 'checkbox', checked: true, onchange: () => { for (const cb of checks.values()) cb.checked = all.checked; } });
|
||||||
|
listPane.append(el('label', { class: 'fx-list-row fx-list-row--check fx-list-head' }, [all, el('span', { text: 'Alle auswählen' })]));
|
||||||
|
for (const f of files) {
|
||||||
|
const cb = el('input', { type: 'checkbox', checked: true });
|
||||||
|
checks.set(f.path, cb);
|
||||||
|
const name = el('button', { type: 'button', class: 'fx-linkish fx-mono', text: f.from ? `${f.from} → ${f.path}` : f.path, onclick: () => showDiff(f, row) });
|
||||||
|
const discard = el('button', { type: 'button', class: 'fx-icon-btn', title: 'Änderung verwerfen', text: '↺', onclick: () => discardFile(f) });
|
||||||
|
const row = el('div', { class: 'fx-list-row fx-list-row--check' }, [
|
||||||
|
cb, el('span', { class: `fx-badge fx-badge--${f.code === '?' ? 'new' : f.code}`, text: f.label }), name, discard
|
||||||
|
]);
|
||||||
|
listPane.append(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showDiff(f, row) {
|
||||||
|
listPane.querySelectorAll('.fx-list-row.is-active').forEach(r => r.classList.remove('is-active'));
|
||||||
|
row.classList.add('is-active');
|
||||||
|
diffPane.textContent = 'Lade Unterschiede ...';
|
||||||
|
const res = await window.electronAPI.localGit({ action: 'diff', folder, file: f.path, code: f.code });
|
||||||
|
diffPane.textContent = '';
|
||||||
|
if (!res.ok) { diffPane.textContent = res.error || 'Diff nicht verfügbar'; return; }
|
||||||
|
for (const line of String(res.diff).split('\n')) {
|
||||||
|
let cls = '';
|
||||||
|
if (line.startsWith('+') && !line.startsWith('+++')) cls = 'fx-diff-add';
|
||||||
|
else if (line.startsWith('-') && !line.startsWith('---')) cls = 'fx-diff-del';
|
||||||
|
else if (line.startsWith('@@')) cls = 'fx-diff-hunk';
|
||||||
|
diffPane.append(el('span', { class: cls, text: line + '\n' }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function discardFile(f) {
|
||||||
|
const ok = await showActionConfirmModal({
|
||||||
|
title: 'Änderung verwerfen',
|
||||||
|
message: f.code === '?'
|
||||||
|
? `Die neue Datei "${f.path}" wird gelöscht. Das lässt sich nicht rückgängig machen.`
|
||||||
|
: `Alle nicht committeten Änderungen an "${f.path}" gehen verloren. Das lässt sich nicht rückgängig machen.`,
|
||||||
|
confirmText: 'Verwerfen',
|
||||||
|
danger: true
|
||||||
|
});
|
||||||
|
if (!ok) return;
|
||||||
|
const res = await window.electronAPI.localGit({ action: 'discard', folder, file: f.path, code: f.code });
|
||||||
|
if (!res.ok) showError('Verwerfen fehlgeschlagen: ' + res.error);
|
||||||
|
await load();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function commit(andPush) {
|
||||||
|
const selected = files.filter(f => checks.get(f.path) && checks.get(f.path).checked).map(f => f.path);
|
||||||
|
if (!selected.length) { showWarning('Keine Datei ausgewählt.'); return; }
|
||||||
|
if (!msg.value.trim()) { showWarning('Bitte eine Commit-Nachricht eingeben.'); msg.focus(); return; }
|
||||||
|
[btnCommit, btnCommitPush].forEach(b => { b.disabled = true; });
|
||||||
|
try {
|
||||||
|
const res = await window.electronAPI.localGit({ action: 'commit', folder, files: selected, message: msg.value });
|
||||||
|
if (!res.ok) { showError('Commit fehlgeschlagen: ' + res.error); return; }
|
||||||
|
showSuccess(`${selected.length} Datei(en) committet`);
|
||||||
|
msg.value = '';
|
||||||
|
if (andPush) await push();
|
||||||
|
} finally {
|
||||||
|
[btnCommit, btnCommitPush].forEach(b => { b.disabled = false; });
|
||||||
|
await load();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function push() {
|
||||||
|
showProgress(30, 'Pushe ...');
|
||||||
|
const res = await window.electronAPI.localGit({ action: 'push', folder });
|
||||||
|
hideProgress();
|
||||||
|
if (res.ok) showSuccess('Push erfolgreich');
|
||||||
|
else await showInfoModal('Push fehlgeschlagen', (res.error || '') + (res.hint ? '\n\n' + res.hint : ''), true);
|
||||||
|
await load();
|
||||||
|
}
|
||||||
|
|
||||||
|
await load();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
3. Pull Requests & Issues
|
||||||
|
================================================================ */
|
||||||
|
async function openCollab(owner, repo, kind) {
|
||||||
|
const isPulls = kind === 'pulls';
|
||||||
|
const m = fxModal({ title: `${isPulls ? '🔀 Pull Requests' : '🐞 Issues'} · ${repo}`, wide: true });
|
||||||
|
let state = 'open';
|
||||||
|
let page = 1;
|
||||||
|
|
||||||
|
function renderList() {
|
||||||
|
m.setTitle(`${isPulls ? '🔀 Pull Requests' : '🐞 Issues'} · ${repo}`);
|
||||||
|
m.body.textContent = '';
|
||||||
|
m.footer.textContent = '';
|
||||||
|
const tabs = el('div', { class: 'fx-tabs' }, [
|
||||||
|
['open', 'Offen'], ['closed', 'Geschlossen']
|
||||||
|
].map(([s, label]) => el('button', {
|
||||||
|
type: 'button', class: `fx-tab${s === state ? ' is-active' : ''}`, text: label,
|
||||||
|
onclick: () => { state = s; page = 1; renderList(); }
|
||||||
|
})));
|
||||||
|
const list = el('div', { class: 'fx-list fx-list--cards' });
|
||||||
|
m.body.append(tabs, list);
|
||||||
|
if (!isPulls) m.footer.append(button('+ Neues Issue', () => renderNewIssue(), 'primary'));
|
||||||
|
loadPage(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadPage(list) {
|
||||||
|
const loading = el('div', { class: 'fx-busy', text: 'Lade ...' });
|
||||||
|
list.append(loading);
|
||||||
|
const res = await window.electronAPI.giteaCollab({ action: isPulls ? 'listPulls' : 'listIssues', owner, repo, state, page });
|
||||||
|
loading.remove();
|
||||||
|
if (!res.ok) { list.append(el('div', { class: 'fx-alert fx-alert--danger', text: res.error })); return; }
|
||||||
|
if (!res.items.length && page === 1) {
|
||||||
|
list.append(el('div', { class: 'fx-empty', text: state === 'open'
|
||||||
|
? (isPulls ? 'Keine offenen Pull Requests.' : 'Keine offenen Issues.')
|
||||||
|
: 'Nichts gefunden.' }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const it of res.items) {
|
||||||
|
const meta = [`#${it.number}`, it.user ? `von ${it.user.login}` : null, `aktualisiert ${fmtDate(it.updated)}`, it.comments ? `💬 ${it.comments}` : null]
|
||||||
|
.filter(Boolean).join(' · ');
|
||||||
|
const card = el('button', { type: 'button', class: 'fx-item', onclick: () => renderDetail(it.number) }, [
|
||||||
|
el('div', { class: 'fx-item-title', text: (it.merged ? '🟣 ' : it.state === 'open' ? '🟢 ' : '🔴 ') + it.title }),
|
||||||
|
el('div', { class: 'fx-muted', text: meta + (isPulls && it.head ? ` · ${it.head} → ${it.base}` : '') }),
|
||||||
|
it.labels.length ? el('div', { class: 'fx-labels' }, it.labels.map(l => {
|
||||||
|
const lab = el('span', { class: 'fx-label', text: l.name });
|
||||||
|
if (/^[0-9a-f]{6}$/i.test(String(l.color).replace('#', ''))) lab.style.borderColor = '#' + String(l.color).replace('#', '');
|
||||||
|
return lab;
|
||||||
|
})) : null
|
||||||
|
]);
|
||||||
|
list.append(card);
|
||||||
|
}
|
||||||
|
if (res.items.length === 30) {
|
||||||
|
const more = button('Mehr laden', () => { more.remove(); page++; loadPage(list); });
|
||||||
|
list.append(more);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderDetail(number) {
|
||||||
|
m.body.textContent = '';
|
||||||
|
m.footer.textContent = '';
|
||||||
|
busy(m.body, 'Lade ...');
|
||||||
|
const res = await window.electronAPI.giteaCollab({ action: 'get', owner, repo, index: number, isPull: isPulls });
|
||||||
|
m.body.textContent = '';
|
||||||
|
if (!res.ok) { m.body.append(el('div', { class: 'fx-alert fx-alert--danger', text: res.error })); m.footer.append(button('← Zurück', renderList)); return; }
|
||||||
|
const it = res.item;
|
||||||
|
m.setTitle(`#${it.number} ${it.title}`);
|
||||||
|
const meta = [it.state === 'open' ? 'offen' : (it.merged ? 'gemergt' : 'geschlossen'), it.user ? `von ${it.user.login}` : null, `erstellt ${fmtDate(it.created)}`]
|
||||||
|
.filter(Boolean).join(' · ');
|
||||||
|
m.body.append(el('div', { class: 'fx-muted', text: meta + (isPulls && it.head ? ` · ${it.head} → ${it.base}` : '') }));
|
||||||
|
m.body.append(el('div', { class: 'fx-text', text: it.body || '(keine Beschreibung)' }));
|
||||||
|
|
||||||
|
const comments = el('div', { class: 'fx-comments' });
|
||||||
|
for (const c of res.comments) {
|
||||||
|
comments.append(el('div', { class: 'fx-comment' }, [
|
||||||
|
el('div', { class: 'fx-muted', text: `${c.user ? c.user.login : '?'} · ${fmtDate(c.created)}` }),
|
||||||
|
el('div', { class: 'fx-text', text: c.body })
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
m.body.append(el('h3', { text: `Kommentare (${res.comments.length})` }), comments);
|
||||||
|
|
||||||
|
const input = el('textarea', { class: 'fx-input fx-textarea', placeholder: 'Kommentar schreiben ...' });
|
||||||
|
const send = button('Kommentieren', async () => {
|
||||||
|
if (!input.value.trim()) return;
|
||||||
|
send.disabled = true;
|
||||||
|
const r = await window.electronAPI.giteaCollab({ action: 'comment', owner, repo, index: number, body: input.value });
|
||||||
|
send.disabled = false;
|
||||||
|
if (r.ok) { showSuccess('Kommentar gespeichert'); renderDetail(number); } else showError('Kommentar fehlgeschlagen: ' + r.error);
|
||||||
|
}, 'primary');
|
||||||
|
m.body.append(input, el('div', { class: 'fx-row-end' }, [send]));
|
||||||
|
|
||||||
|
m.footer.append(button('← Zurück', renderList));
|
||||||
|
if (it.url) m.footer.append(button('Im Browser öffnen', () => window.electronAPI.openExternalUrl(it.url)));
|
||||||
|
|
||||||
|
if (isPulls && it.state === 'open' && !it.merged) {
|
||||||
|
const style = el('select', { class: 'fx-input fx-select' }, [
|
||||||
|
['merge', 'Merge-Commit'], ['squash', 'Squash'], ['rebase', 'Rebase'], ['rebase-merge', 'Rebase + Merge-Commit']
|
||||||
|
].map(([v, t]) => el('option', { value: v, text: t })));
|
||||||
|
const delBranch = el('input', { type: 'checkbox' });
|
||||||
|
const mergeBtn = button('Mergen', async () => {
|
||||||
|
const ok = await showActionConfirmModal({
|
||||||
|
title: 'Pull Request mergen',
|
||||||
|
message: `#${it.number} „${it.title}“ wird per ${style.options[style.selectedIndex].text} in ${it.base} übernommen.` + (delBranch.checked ? `\nDer Branch ${it.head} wird danach gelöscht.` : ''),
|
||||||
|
confirmText: 'Mergen'
|
||||||
|
});
|
||||||
|
if (!ok) return;
|
||||||
|
mergeBtn.disabled = true;
|
||||||
|
const r = await window.electronAPI.giteaCollab({ action: 'merge', owner, repo, index: number, style: style.value, deleteBranch: delBranch.checked });
|
||||||
|
mergeBtn.disabled = false;
|
||||||
|
if (r.ok) { showSuccess(`#${number} gemergt`); renderDetail(number); }
|
||||||
|
else await showInfoModal('Mergen fehlgeschlagen', r.error + (it.mergeable === false ? '\n\nDer Pull Request hat Konflikte und muss erst angepasst werden.' : ''), true);
|
||||||
|
}, 'primary');
|
||||||
|
if (it.mergeable === false) m.footer.append(el('span', { class: 'fx-muted', text: '⚠️ Konflikte' }));
|
||||||
|
m.footer.append(style, el('label', { class: 'fx-inline-check' }, [delBranch, el('span', { text: 'Branch löschen' })]), mergeBtn);
|
||||||
|
}
|
||||||
|
if (!it.merged) {
|
||||||
|
const toState = it.state === 'open' ? 'closed' : 'open';
|
||||||
|
m.footer.append(button(toState === 'closed' ? 'Schließen' : 'Wieder öffnen', async () => {
|
||||||
|
const r = await window.electronAPI.giteaCollab({ action: 'setState', owner, repo, index: number, state: toState });
|
||||||
|
if (r.ok) renderDetail(number); else showError(r.error);
|
||||||
|
}, toState === 'closed' ? 'danger' : 'secondary'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderNewIssue() {
|
||||||
|
m.setTitle(`🐞 Neues Issue · ${repo}`);
|
||||||
|
m.body.textContent = '';
|
||||||
|
m.footer.textContent = '';
|
||||||
|
const title = el('input', { type: 'text', class: 'fx-input', placeholder: 'Titel' });
|
||||||
|
const body = el('textarea', { class: 'fx-input fx-textarea fx-textarea--tall', placeholder: 'Beschreibung (optional)' });
|
||||||
|
m.body.append(title, body);
|
||||||
|
m.footer.append(button('← Zurück', renderList), button('Issue anlegen', async () => {
|
||||||
|
if (!title.value.trim()) { title.focus(); return; }
|
||||||
|
const r = await window.electronAPI.giteaCollab({ action: 'createIssue', owner, repo, title: title.value, body: body.value });
|
||||||
|
if (r.ok) { showSuccess(`Issue #${r.item.number} angelegt`); renderDetail(r.item.number); } else showError(r.error);
|
||||||
|
}, 'primary'));
|
||||||
|
title.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
4. Server-Diagnose
|
||||||
|
================================================================ */
|
||||||
|
async function openDiagnosis() {
|
||||||
|
const m = fxModal({ title: '🩺 Server-Diagnose', wide: true });
|
||||||
|
busy(m.body, 'Prüfe Server, Verbindung und Git-Transport ... (bis zu einer Minute)');
|
||||||
|
m.footer.append(button('Schließen', () => m.close()));
|
||||||
|
const inRepo = currentState.view === 'gitea-repo' && currentState.owner && currentState.repo;
|
||||||
|
const res = await window.electronAPI.runServerDiagnosis(inRepo ? { owner: currentState.owner, repo: currentState.repo } : {});
|
||||||
|
m.body.textContent = '';
|
||||||
|
if (!res || !res.ok) { m.body.append(el('div', { class: 'fx-alert fx-alert--danger', text: (res && res.error) || 'Diagnose fehlgeschlagen' })); return; }
|
||||||
|
m.body.append(el('div', { class: 'fx-muted', text: 'Verwendet die gespeicherten Zugangsdaten.' }));
|
||||||
|
const icons = { ok: '✅', warn: '⚠️', fail: '❌', info: 'ℹ️' };
|
||||||
|
for (const c of res.checks) {
|
||||||
|
m.body.append(el('div', { class: `fx-check fx-check--${c.status}` }, [
|
||||||
|
el('div', { class: 'fx-check-head' }, [el('span', { text: icons[c.status] || '•' }), el('strong', { text: c.label })]),
|
||||||
|
el('div', { class: 'fx-text', text: c.detail || '' }),
|
||||||
|
c.hint ? el('pre', { class: 'fx-hint', text: c.hint }) : null
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
const bad = res.checks.filter(c => c.status === 'fail').length;
|
||||||
|
m.setTitle(bad ? `🩺 Server-Diagnose: ${bad} Problem(e)` : '🩺 Server-Diagnose: alles in Ordnung');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
5. Repo-Backup (Einstellungen)
|
||||||
|
================================================================ */
|
||||||
|
/* ---------- Fortschrittsfenster für das Repo-Backup ---------- */
|
||||||
|
let backupView = null;
|
||||||
|
|
||||||
|
function fmtDuration(ms) {
|
||||||
|
const s = Math.max(0, Math.round(ms / 1000));
|
||||||
|
if (s < 60) return `${s} s`;
|
||||||
|
const m = Math.floor(s / 60);
|
||||||
|
return m < 60 ? `${m} min ${s % 60} s` : `${Math.floor(m / 60)} h ${m % 60} min`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function backupPercent(p) {
|
||||||
|
if (!p || !p.total) return 0;
|
||||||
|
if (p.phase === 'done') return 100;
|
||||||
|
// Laufendes Repo anteilig mitzählen, wenn die Größe des Downloads bekannt ist
|
||||||
|
const partial = p.bytesTotal ? Math.min(0.99, p.bytes / p.bytesTotal) : 0;
|
||||||
|
return Math.min(99, Math.floor(((p.done || 0) + partial) / p.total * 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
function openBackupProgress(initial) {
|
||||||
|
if (backupView) { backupView.update(initial); return; }
|
||||||
|
const m = fxModal({ title: '⇣ Backup läuft', wide: true });
|
||||||
|
const percentEl = el('div', { class: 'fx-progress-percent', text: '0 %' });
|
||||||
|
const fill = el('div', { class: 'fx-progress-fill' });
|
||||||
|
const bar = el('div', { class: 'fx-progress-track' }, [fill]);
|
||||||
|
const currentEl = el('div', { class: 'fx-lead', text: 'Startet …' });
|
||||||
|
const stepEl = el('div', { class: 'fx-muted' });
|
||||||
|
const metaEl = el('div', { class: 'fx-progress-meta' });
|
||||||
|
const list = el('div', { class: 'fx-list fx-backup-list' });
|
||||||
|
const warn = el('div', { class: 'fx-alert fx-alert--warn hidden', text:
|
||||||
|
'Der Server liefert keine Git-Pakete. Die Repos werden deshalb als ZIP gesichert: nur der Standard-Branch, ohne Historie.' });
|
||||||
|
m.body.append(el('div', { class: 'fx-progress-head' }, [percentEl, el('div', { class: 'fx-progress-now' }, [currentEl, stepEl])]),
|
||||||
|
bar, metaEl, warn, list);
|
||||||
|
|
||||||
|
const cancelBtn = button('Abbrechen', async () => {
|
||||||
|
const ok = await showActionConfirmModal({
|
||||||
|
title: 'Backup abbrechen?',
|
||||||
|
message: 'Bereits gesicherte Repos bleiben erhalten. Ein laufender ZIP-Download wird sofort beendet, ein laufender Git-Spiegel erst nach dem aktuellen Repo.',
|
||||||
|
confirmText: 'Abbrechen',
|
||||||
|
danger: true
|
||||||
|
});
|
||||||
|
if (ok) { cancelBtn.disabled = true; await window.electronAPI.repoBackupCancel(); }
|
||||||
|
}, 'danger');
|
||||||
|
const closeBtn = button('Im Hintergrund weiterlaufen lassen', () => m.close());
|
||||||
|
m.footer.append(cancelBtn, closeBtn);
|
||||||
|
m.onClose(() => { backupView = null; clearInterval(timer); });
|
||||||
|
|
||||||
|
let last = null;
|
||||||
|
let rowsFor = '';
|
||||||
|
const rows = new Map();
|
||||||
|
// Verstrichene Zeit auch ohne neue Meldung weiterzählen
|
||||||
|
const timer = setInterval(() => { if (last && last.phase !== 'done') renderMeta(last); }, 1000);
|
||||||
|
|
||||||
|
function renderMeta(p) {
|
||||||
|
const elapsed = Date.now() - (p.startedAt || Date.now());
|
||||||
|
const parts = [`${p.done || 0} von ${p.total || '?'} Repos`, `Dauer ${fmtDuration(p.summary ? p.summary.durationMs : elapsed)}`];
|
||||||
|
if (p.phase === 'running' && p.done >= 1 && p.total > p.done) {
|
||||||
|
parts.push(`noch ca. ${fmtDuration(elapsed / p.done * (p.total - p.done))}`);
|
||||||
|
}
|
||||||
|
metaEl.textContent = parts.join(' · ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function update(p) {
|
||||||
|
if (!p) return;
|
||||||
|
last = p;
|
||||||
|
const pct = backupPercent(p);
|
||||||
|
percentEl.textContent = `${pct} %`;
|
||||||
|
fill.style.width = `${pct}%`;
|
||||||
|
warn.classList.toggle('hidden', !p.mirrorBroken);
|
||||||
|
renderMeta(p);
|
||||||
|
|
||||||
|
if (p.phase === 'list' || p.phase === 'start') {
|
||||||
|
currentEl.textContent = p.step || 'Startet …';
|
||||||
|
stepEl.textContent = p.targetDir ? `Ziel: ${p.targetDir}` : '';
|
||||||
|
} else if (p.phase === 'running') {
|
||||||
|
currentEl.textContent = p.repo ? `${p.current}/${p.total}: ${p.repo}` : 'Läuft …';
|
||||||
|
let step = p.step || '';
|
||||||
|
if (p.bytes) step += ` ${fmtBytes(p.bytes)}${p.bytesTotal ? ` von ${fmtBytes(p.bytesTotal)}` : ''}`;
|
||||||
|
stepEl.textContent = step;
|
||||||
|
} else if (p.phase === 'done') {
|
||||||
|
const s = p.summary || {};
|
||||||
|
m.setTitle(s.cancelled ? '⇣ Backup abgebrochen' : s.failed ? '⇣ Backup mit Fehlern beendet' : '⇣ Backup fertig');
|
||||||
|
currentEl.textContent = `${s.ok} von ${s.total} Repos gesichert` + (s.failed ? `, ${s.failed} Fehler` : '') + (s.skipped ? `, ${s.skipped} übersprungen` : '');
|
||||||
|
stepEl.textContent = `${s.mirror} als Git-Spiegel, ${s.zip} als ZIP · Protokoll: backup-protokoll.json im Backup-Ordner`;
|
||||||
|
percentEl.textContent = s.cancelled ? '–' : '100 %';
|
||||||
|
fill.style.width = s.cancelled ? fill.style.width : '100%';
|
||||||
|
cancelBtn.classList.add('hidden');
|
||||||
|
closeBtn.textContent = 'Schließen';
|
||||||
|
} else if (p.phase === 'error') {
|
||||||
|
m.setTitle('⇣ Backup fehlgeschlagen');
|
||||||
|
currentEl.textContent = p.error || 'Unbekannter Fehler';
|
||||||
|
stepEl.textContent = '';
|
||||||
|
cancelBtn.classList.add('hidden');
|
||||||
|
closeBtn.textContent = 'Schließen';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Liste aller Repos mit Zustand
|
||||||
|
const repos = p.repos || [];
|
||||||
|
if (repos.join('|') !== rowsFor) {
|
||||||
|
rowsFor = repos.join('|');
|
||||||
|
list.textContent = '';
|
||||||
|
rows.clear();
|
||||||
|
for (const name of repos) {
|
||||||
|
const icon = el('span', { class: 'fx-backup-icon', text: '○' });
|
||||||
|
const info = el('span', { class: 'fx-muted', text: 'wartet' });
|
||||||
|
rows.set(name, { row: el('div', { class: 'fx-list-row' }, [icon, el('span', { class: 'fx-mono', text: name }), info]), icon, info });
|
||||||
|
list.append(rows.get(name).row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const byRepo = new Map((p.results || []).map(r => [r.repo, r]));
|
||||||
|
for (const [name, r] of rows) {
|
||||||
|
const res = byRepo.get(name);
|
||||||
|
r.row.classList.toggle('is-active', !res && name === p.repo && p.phase === 'running');
|
||||||
|
if (res) {
|
||||||
|
r.icon.textContent = res.ok ? '✅' : '❌';
|
||||||
|
r.info.textContent = res.ok
|
||||||
|
? (res.method === 'leer' ? 'leeres Repo' : res.method + (res.bytes ? ` · ${fmtBytes(res.bytes)}` : ''))
|
||||||
|
: res.error;
|
||||||
|
r.info.title = res.error || '';
|
||||||
|
} else if (name === p.repo && p.phase === 'running') {
|
||||||
|
r.icon.textContent = '⏳';
|
||||||
|
r.info.textContent = 'läuft …';
|
||||||
|
} else if (p.phase === 'done') {
|
||||||
|
r.icon.textContent = '–';
|
||||||
|
r.info.textContent = 'übersprungen';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const active = list.querySelector('.fx-list-row.is-active');
|
||||||
|
if (active) active.scrollIntoView({ block: 'nearest' });
|
||||||
|
}
|
||||||
|
|
||||||
|
backupView = { update };
|
||||||
|
update(initial || { phase: 'start', step: 'Startet …', startedAt: Date.now(), results: [], repos: [] });
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderBackupStatus(state) {
|
||||||
|
const statusEl = $('repoBackupStatus');
|
||||||
|
if (!statusEl) return;
|
||||||
|
const runBtn = $('btnRepoBackupRun');
|
||||||
|
if (runBtn) runBtn.textContent = state.running ? '⏳ Fortschritt anzeigen' : '▶️ Jetzt sichern';
|
||||||
|
if (state.running) {
|
||||||
|
const p = state.progress;
|
||||||
|
statusEl.textContent = p && p.total
|
||||||
|
? `⏳ Backup läuft: ${backupPercent(p)} % · ${p.current}/${p.total}: ${p.repo || ''}`
|
||||||
|
: '⏳ Backup läuft …';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const r = state.config && state.config.lastResult;
|
||||||
|
if (!r) { statusEl.textContent = 'Noch kein Backup gelaufen.'; return; }
|
||||||
|
let text = `Letztes Backup: ${fmtDate(r.at)} · ${r.ok}/${r.total} gesichert (${r.mirror} als Git-Spiegel, ${r.zip} als ZIP)`;
|
||||||
|
if (r.cancelled) text += ' · abgebrochen';
|
||||||
|
if (r.durationMs) text += ` · Dauer ${fmtDuration(r.durationMs)}`;
|
||||||
|
if (r.failed) text += ` · ${r.failed} Fehler: ` + r.errors.map(e => `${e.repo} (${e.error})`).join('; ');
|
||||||
|
if (r.mirrorBroken) text += '\nDer Server liefert keine Git-Pakete, daher ZIP-Archive: nur der Standard-Branch, ohne Historie. Server-Diagnose zeigt die Ursache.';
|
||||||
|
statusEl.textContent = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadBackupSettings() {
|
||||||
|
try {
|
||||||
|
const state = await window.electronAPI.repoBackupGetConfig();
|
||||||
|
if (!state || !state.ok) return;
|
||||||
|
const c = state.config;
|
||||||
|
if ($('repoBackupTarget')) $('repoBackupTarget').value = c.targetDir || '';
|
||||||
|
if ($('repoBackupInterval')) $('repoBackupInterval').value = c.interval || 'off';
|
||||||
|
if ($('repoBackupKeep')) $('repoBackupKeep').value = c.keep || 5;
|
||||||
|
if ($('repoBackupTime')) $('repoBackupTime').value = c.time || '';
|
||||||
|
if ($('repoBackupNotify')) $('repoBackupNotify').value = c.notify || 'errors';
|
||||||
|
if ($('repoBackupExclude')) $('repoBackupExclude').value = c.excludeRepos || '';
|
||||||
|
renderBackupStatus(state);
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveBackupSettings(silent) {
|
||||||
|
const res = await window.electronAPI.repoBackupSetConfig({
|
||||||
|
targetDir: $('repoBackupTarget').value,
|
||||||
|
interval: $('repoBackupInterval').value,
|
||||||
|
keep: $('repoBackupKeep').value,
|
||||||
|
time: $('repoBackupTime') ? $('repoBackupTime').value : '',
|
||||||
|
notify: $('repoBackupNotify') ? $('repoBackupNotify').value : 'errors',
|
||||||
|
excludeRepos: $('repoBackupExclude') ? $('repoBackupExclude').value : ''
|
||||||
|
});
|
||||||
|
if (res && res.ok) {
|
||||||
|
renderBackupStatus(res);
|
||||||
|
if (!silent) showSuccess('Backup-Einstellung gespeichert');
|
||||||
|
} else if (!silent) showError('Speichern fehlgeschlagen: ' + (res && res.error));
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
6. Backup wiederherstellen
|
||||||
|
================================================================ */
|
||||||
|
async function openRestore() {
|
||||||
|
const m = fxModal({ title: '↺ Backup wiederherstellen', wide: true });
|
||||||
|
let dir = null;
|
||||||
|
|
||||||
|
async function renderList(customDir) {
|
||||||
|
m.setTitle('↺ Backup wiederherstellen');
|
||||||
|
m.footer.textContent = '';
|
||||||
|
busy(m.body, 'Suche Sicherungen ...');
|
||||||
|
const res = await window.electronAPI.repoBackupList(customDir ? { dir: customDir } : {});
|
||||||
|
m.body.textContent = '';
|
||||||
|
const pick = button('📂 Anderen Ordner wählen', async () => {
|
||||||
|
const folder = await window.electronAPI.selectFolder();
|
||||||
|
if (folder) renderList(folder);
|
||||||
|
});
|
||||||
|
m.footer.append(pick, button('Schließen', () => m.close()));
|
||||||
|
if (!res || !res.ok) {
|
||||||
|
m.body.append(el('div', { class: 'fx-alert fx-alert--warn', text: (res && res.error) || 'Backup-Ordner nicht lesbar' }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dir = res.dir;
|
||||||
|
m.body.append(el('div', { class: 'fx-muted', text: `Quelle: ${dir}` }));
|
||||||
|
if (!res.backups.length) {
|
||||||
|
m.body.append(el('div', { class: 'fx-empty', text: 'In diesem Ordner wurden keine Sicherungen gefunden.' }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const list = el('div', { class: 'fx-list fx-list--cards' });
|
||||||
|
for (const b of res.backups) {
|
||||||
|
const parts = [];
|
||||||
|
if (b.mirror) parts.push(`Git-Spiegel · Stand ${fmtDate(b.mirror.updated)}`);
|
||||||
|
if (b.zips.length) parts.push(`${b.zips.length} ZIP-Stand${b.zips.length > 1 ? 'e' : ''} · neuester ${fmtDate(b.zips[0].date)}`);
|
||||||
|
list.append(el('button', { type: 'button', class: 'fx-item', onclick: () => renderDetail(b) }, [
|
||||||
|
el('div', { class: 'fx-item-title', text: `${b.owner}/${b.repo}` }),
|
||||||
|
el('div', { class: 'fx-muted', text: parts.join(' · ') })
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
m.body.append(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDetail(b) {
|
||||||
|
m.setTitle(`↺ ${b.owner}/${b.repo} wiederherstellen`);
|
||||||
|
m.body.textContent = '';
|
||||||
|
m.footer.textContent = '';
|
||||||
|
|
||||||
|
// Welche Sicherung?
|
||||||
|
const sources = [];
|
||||||
|
if (b.mirror) sources.push({ kind: 'mirror', path: b.mirror.path, label: `Git-Spiegel – volle Historie, alle Branches und Tags (Stand ${fmtDate(b.mirror.updated)})`, date: b.mirror.updated });
|
||||||
|
for (const z of b.zips) sources.push({ kind: 'zip', path: z.path, label: `ZIP vom ${fmtDate(z.date)} – nur Standard-Branch, ohne Historie (${fmtBytes(z.size)})`, date: z.date });
|
||||||
|
let chosen = sources[0];
|
||||||
|
const srcList = el('div', { class: 'fx-list' });
|
||||||
|
sources.forEach((src, i) => {
|
||||||
|
const radio = el('input', { type: 'radio', name: 'fxRestoreSource', checked: i === 0, onchange: () => { chosen = src; updateMode(); } });
|
||||||
|
srcList.append(el('label', { class: 'fx-list-row fx-list-row--check' }, [radio, el('span', { text: src.label })]));
|
||||||
|
});
|
||||||
|
m.body.append(el('h3', { text: '1. Sicherung wählen' }), srcList);
|
||||||
|
|
||||||
|
// Lokal entpacken
|
||||||
|
const localBtn = button('📂 Zielordner wählen und entpacken', async () => {
|
||||||
|
const parent = await window.electronAPI.selectFolder();
|
||||||
|
if (!parent) return;
|
||||||
|
setBusy(true, chosen.kind === 'mirror' ? 'Klone Spiegel ...' : 'Entpacke Archiv ...');
|
||||||
|
const r = await window.electronAPI.repoRestoreLocal({ kind: chosen.kind, source: chosen.path, destParent: parent, repo: b.repo });
|
||||||
|
setBusy(false);
|
||||||
|
if (!r.ok) { await showInfoModal('Entpacken fehlgeschlagen', r.error, true); return; }
|
||||||
|
showSuccess(`Wiederhergestellt nach ${r.path}`);
|
||||||
|
showResult(`✅ Lokal wiederhergestellt: ${r.path}` + (r.branches ? ` (${r.branches} Branches)` : r.files ? ` (${r.files} Dateien)` : ''), [
|
||||||
|
button('In der App öffnen', () => openLocalInApp(r.path, m), 'primary')
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
m.body.append(el('h3', { text: '2a. Lokal entpacken' }),
|
||||||
|
el('div', { class: 'fx-muted', text: `Legt den Ordner „${b.repo}“ im gewählten Verzeichnis an. Ein vorhandener, nicht leerer Ordner wird nie überschrieben.` }),
|
||||||
|
el('div', {}, [localBtn]));
|
||||||
|
|
||||||
|
// Auf Gitea zurückspielen
|
||||||
|
const modeNew = el('input', { type: 'radio', name: 'fxRestoreMode', checked: true, onchange: updateMode });
|
||||||
|
const modeOverwrite = el('input', { type: 'radio', name: 'fxRestoreMode', onchange: updateMode });
|
||||||
|
const nameInput = el('input', { type: 'text', class: 'fx-input', value: `${b.repo}-wiederhergestellt` });
|
||||||
|
const privateCb = el('input', { type: 'checkbox', checked: true });
|
||||||
|
const overwriteNote = el('div', { class: 'fx-muted' });
|
||||||
|
const giteaBtn = button('Auf Gitea zurückspielen', () => restoreGitea(), 'primary');
|
||||||
|
m.body.append(el('h3', { text: '2b. Auf Gitea zurückspielen' }),
|
||||||
|
el('label', { class: 'fx-list-row fx-list-row--check' }, [modeNew, el('span', { text: 'Als neues Repo anlegen' })]),
|
||||||
|
el('div', { class: 'fx-row fx-indent' }, [nameInput, el('label', { class: 'fx-inline-check' }, [privateCb, el('span', { text: 'Privat' })])]),
|
||||||
|
el('label', { class: 'fx-list-row fx-list-row--check' }, [modeOverwrite, el('span', { text: `Bestehendes Repo „${b.owner}/${b.repo}“ überschreiben` })]),
|
||||||
|
overwriteNote,
|
||||||
|
el('div', {}, [giteaBtn]));
|
||||||
|
|
||||||
|
const status = el('div', { class: 'fx-lead fx-restore-status' });
|
||||||
|
m.body.append(status);
|
||||||
|
m.footer.append(button('← Zurück', () => renderList(dir)));
|
||||||
|
|
||||||
|
let offProgress = null;
|
||||||
|
function setBusy(on, text) {
|
||||||
|
[localBtn, giteaBtn].forEach(btn => { btn.disabled = on; });
|
||||||
|
status.textContent = on ? `⏳ ${text}` : '';
|
||||||
|
if (on && !offProgress) offProgress = window.electronAPI.onRepoRestoreProgress(p => { if (p && p.text) status.textContent = `⏳ ${p.text}`; });
|
||||||
|
if (!on && offProgress) { offProgress(); offProgress = null; }
|
||||||
|
}
|
||||||
|
function showResult(text, actions = []) {
|
||||||
|
status.textContent = '';
|
||||||
|
status.append(el('div', { text }), el('div', { class: 'fx-row' }, actions));
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateMode() {
|
||||||
|
const zip = chosen.kind === 'zip';
|
||||||
|
modeOverwrite.disabled = zip;
|
||||||
|
if (zip && modeOverwrite.checked) modeNew.checked = true;
|
||||||
|
nameInput.disabled = !modeNew.checked;
|
||||||
|
privateCb.disabled = !modeNew.checked;
|
||||||
|
overwriteNote.textContent = zip
|
||||||
|
? 'Überschreiben geht nur mit einem Git-Spiegel, weil ein ZIP-Stand keine Historie enthält.'
|
||||||
|
: 'Achtung: Setzt alle Branches und Tags auf den Stand der Sicherung. Neuere Commits auf Gitea gehen verloren.';
|
||||||
|
}
|
||||||
|
updateMode();
|
||||||
|
|
||||||
|
async function restoreGitea() {
|
||||||
|
const overwrite = modeOverwrite.checked;
|
||||||
|
const name = nameInput.value.trim();
|
||||||
|
if (!overwrite && !name) { nameInput.focus(); return; }
|
||||||
|
const ok = await showActionConfirmModal(overwrite ? {
|
||||||
|
title: 'Repo überschreiben',
|
||||||
|
message: `Alle Branches und Tags von „${b.owner}/${b.repo}“ werden auf den Stand vom ${fmtDate(chosen.date)} gesetzt.\n\nCommits, Branches und Tags, die danach auf Gitea entstanden sind, gehen verloren. Das lässt sich nur mit einer neueren Sicherung rückgängig machen.`,
|
||||||
|
confirmText: 'Überschreiben',
|
||||||
|
danger: true
|
||||||
|
} : {
|
||||||
|
title: 'Neues Repo anlegen',
|
||||||
|
message: `Das Repo „${b.owner}/${name}“ wird ${privateCb.checked ? 'privat' : 'öffentlich'} angelegt und mit der Sicherung vom ${fmtDate(chosen.date)} befüllt.`,
|
||||||
|
confirmText: 'Anlegen und befüllen'
|
||||||
|
});
|
||||||
|
if (!ok) return;
|
||||||
|
setBusy(true, 'Starte ...');
|
||||||
|
const r = await window.electronAPI.repoRestoreGitea({
|
||||||
|
kind: chosen.kind, source: chosen.path, owner: b.owner, repo: b.repo,
|
||||||
|
mode: overwrite ? 'overwrite' : 'new', newName: name, isPrivate: privateCb.checked
|
||||||
|
});
|
||||||
|
setBusy(false);
|
||||||
|
if (!r.ok) { await showInfoModal('Wiederherstellen fehlgeschlagen', r.error, true); return; }
|
||||||
|
showSuccess(`${r.repo} wiederhergestellt`);
|
||||||
|
showResult(`✅ Auf Gitea wiederhergestellt: ${r.repo} (über ${r.method}${r.files ? `, ${r.files} Dateien` : ''})`,
|
||||||
|
r.url ? [button('Im Browser öffnen', () => window.electronAPI.openExternalUrl(r.url))] : []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderList(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openLocalInApp(folder, modal) {
|
||||||
|
modal.close();
|
||||||
|
$('settingsModal')?.classList.add('hidden');
|
||||||
|
selectedFolder = folder;
|
||||||
|
currentState.view = 'local';
|
||||||
|
setStatus('Local: ' + folder);
|
||||||
|
if (typeof updateNavigationUI === 'function') updateNavigationUI();
|
||||||
|
window.updateFeatureButtons('local');
|
||||||
|
if (typeof refreshLocalTree === 'function') refreshLocalTree(folder);
|
||||||
|
if (typeof loadBranches === 'function') loadBranches(folder);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bereiche in den Einstellungen (Navigation links). "general" bleibt als alter Name für "konten" gültig.
|
||||||
|
window.showSettingsTab = function (name) {
|
||||||
|
if (name === 'general') name = 'konten';
|
||||||
|
document.querySelectorAll('#settingsModal [data-settings-tab]').forEach(tab => {
|
||||||
|
const active = tab.dataset.settingsTab === name;
|
||||||
|
tab.classList.toggle('is-active', active);
|
||||||
|
tab.setAttribute('aria-selected', String(active));
|
||||||
|
});
|
||||||
|
document.querySelectorAll('#settingsModal [data-settings-panel]').forEach(panel => {
|
||||||
|
panel.classList.toggle('hidden', panel.dataset.settingsPanel !== name);
|
||||||
|
});
|
||||||
|
if (name === 'backup') loadBackupSettings();
|
||||||
|
// Höhenabgleich der Karten braucht sichtbare Elemente
|
||||||
|
if (name === 'konten' && typeof syncSettingsPanelHeights === 'function') requestAnimationFrame(syncSettingsPanelHeights);
|
||||||
|
const pages = document.querySelector('#settingsModal .settings-pages');
|
||||||
|
if (pages) pages.scrollTop = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Für das gemeinsame "Speichern" in settings-extra.js
|
||||||
|
window.gmBackupSettings = { load: loadBackupSettings, save: saveBackupSettings };
|
||||||
|
|
||||||
|
function wireSettings() {
|
||||||
|
document.querySelectorAll('#settingsModal [data-settings-tab]').forEach(tab => {
|
||||||
|
tab.addEventListener('click', () => window.showSettingsTab(tab.dataset.settingsTab));
|
||||||
|
});
|
||||||
|
const restoreBtn = $('btnRepoRestore');
|
||||||
|
if (restoreBtn) restoreBtn.onclick = () => openRestore();
|
||||||
|
const diag = $('btnServerDiagnosis');
|
||||||
|
if (diag) diag.onclick = () => openDiagnosis();
|
||||||
|
const browse = $('btnRepoBackupBrowse');
|
||||||
|
if (browse) browse.onclick = async () => {
|
||||||
|
const folder = await window.electronAPI.selectFolder();
|
||||||
|
if (folder) $('repoBackupTarget').value = folder;
|
||||||
|
};
|
||||||
|
const save = $('btnRepoBackupSave');
|
||||||
|
if (save) save.onclick = () => saveBackupSettings(false);
|
||||||
|
const run = $('btnRepoBackupRun');
|
||||||
|
if (run) run.onclick = async () => {
|
||||||
|
const state = await window.electronAPI.repoBackupGetConfig();
|
||||||
|
// Läuft schon ein Backup (z. B. ein geplantes), nur den Fortschritt zeigen
|
||||||
|
if (state && state.running) { openBackupProgress(state.progress); return; }
|
||||||
|
if (!$('repoBackupTarget').value.trim()) { showWarning('Bitte zuerst ein Backup-Ziel wählen.'); return; }
|
||||||
|
await saveBackupSettings(true);
|
||||||
|
openBackupProgress(null);
|
||||||
|
const res = await window.electronAPI.repoBackupRunNow();
|
||||||
|
if (!res.ok && !res.summary && !res.running) showError('Backup fehlgeschlagen: ' + res.error);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fortschritt kommt für jedes Backup an alle Fenster – auch für geplante Backups
|
||||||
|
window.electronAPI.onRepoBackupProgress((p) => {
|
||||||
|
if (!p) return;
|
||||||
|
const finished = p.phase === 'done' || p.phase === 'error';
|
||||||
|
renderBackupStatus({ running: !finished, progress: p });
|
||||||
|
if (backupView) backupView.update(p);
|
||||||
|
if (finished) {
|
||||||
|
const s = p.summary;
|
||||||
|
if (p.phase === 'error') showError('Backup fehlgeschlagen: ' + p.error);
|
||||||
|
else if (s && s.cancelled) showWarning(`Backup abgebrochen: ${s.ok} von ${s.total} Repos gesichert`);
|
||||||
|
else if (s && s.failed) showWarning(`Backup mit ${s.failed} Fehler(n) beendet`);
|
||||||
|
else if (s) showSuccess(`Backup fertig: ${s.ok}/${s.total} Repos gesichert`);
|
||||||
|
setTimeout(loadBackupSettings, 300);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Beim Öffnen der Einstellungen den aktuellen Stand anzeigen
|
||||||
|
const btnSettings = $('btnSettings');
|
||||||
|
if (btnSettings) btnSettings.addEventListener('click', () => { window.showSettingsTab('konten'); setTimeout(loadBackupSettings, 50); });
|
||||||
|
loadBackupSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', wireSettings);
|
||||||
|
else wireSettings();
|
||||||
|
})();
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
+906
-58
@@ -1,89 +1,937 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<title>Git Manager Explorer</title>
|
<title>Git Manager Explorer Pro</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<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">
|
<link rel="stylesheet" href="style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
|
<!-- 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="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>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="toolbar">
|
<div id="toolbar">
|
||||||
<button id="btnSettings">Settings</button>
|
<div class="toolbar-row toolbar-row--top">
|
||||||
<button id="btnSelectFolder">Select Folder</button>
|
<div class="toolbar-brand" aria-label="App Kopfbereich">
|
||||||
<button id="btnPush">Push / Update</button>
|
<div class="toolbar-brand-mark">
|
||||||
<select id="platform"><option value="github">GitHub</option><option value="gitea">Gitea</option></select>
|
<img src="./icon.png" alt="Git Manager Logo" class="toolbar-brand-logo">
|
||||||
<span id="status" class="status"></span>
|
</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>
|
</div>
|
||||||
|
|
||||||
<div id="main">
|
<div class="toolbar-top-actions">
|
||||||
<aside id="explorerPanel" class="card">
|
<div class="tool-group tool-group--quick-actions">
|
||||||
<h3>Project Files</h3>
|
<button id="btnOpenRepoActions" title="Neues Repository erstellen">🚀 New Repo</button>
|
||||||
<div id="fileTree" class="file-tree"></div>
|
<button id="btnOpenMigration" title="Repository von GitHub/GitLab zu Gitea migrieren">📥 Migrieren</button>
|
||||||
</aside>
|
<button id="btnPush" title="Projekt pushen">⬆️ Push</button>
|
||||||
|
<button id="btnGlobalTrash" class="hidden" style="display:none" title=""><!-- removed --></button>
|
||||||
<section id="detailsPanel">
|
|
||||||
<div id="repoDetails" class="card">
|
|
||||||
<div><label>Branch:</label><select id="branchSelect"></select></div>
|
|
||||||
<div><label>Repository Name:</label><input id="repoName" type="text" placeholder="repo-name"></div>
|
|
||||||
|
|
||||||
<!-- NEU: License Auswahl -->
|
|
||||||
<div style="margin-bottom: 5px;">
|
|
||||||
<label for="licenseSelect">License:</label>
|
|
||||||
<select id="licenseSelect">
|
|
||||||
<option value="">No License</option>
|
|
||||||
<option value="MIT">MIT License</option>
|
|
||||||
<option value="Apache-2.0">Apache License 2.0</option>
|
|
||||||
<option value="GPL-3.0">GNU General Public License v3.0</option>
|
|
||||||
<option value="BSD-3-Clause">BSD 3-Clause "New" or "Revised" License</option>
|
|
||||||
<option value="BSD-2-Clause">BSD 2-Clause "Simplified" License</option>
|
|
||||||
<option value="LGPL-3.0">GNU Lesser General Public License v3.0</option>
|
|
||||||
<option value="MPL-2.0">Mozilla Public License 2.0</option>
|
|
||||||
<option value="Unlicense">The Unlicense</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- NEU: README Checkbox -->
|
<div class="tool-group tool-group--utility">
|
||||||
<div style="margin-bottom: 10px;">
|
<span class="tool-group-title">Steuerung</span>
|
||||||
<label>
|
<button id="btnSettings" title="Einstellungen">⚙️ <span class="btn-label">Settings</span></button>
|
||||||
<input type="checkbox" id="createReadme" checked>
|
<button id="btnBatchActions" title="Batch-Aktionen">🧩 <span class="btn-label">Batch</span></button>
|
||||||
Initialize Repository with a README
|
<button id="btnOpenActivityLog" title="Aktivitätsprotokoll">📝 <span class="btn-label">Activity</span></button>
|
||||||
</label>
|
<button id="btnUploadHistory" title="Verlauf der letzten Uploads">🕘 <span class="btn-label">Verlauf</span></button>
|
||||||
|
<button id="btnRetryQueueNow" class="secondary" title="Retry-Queue jetzt verarbeiten">🔁 Queue (0)</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div><button id="btnCreateRepo">Create Repo</button></div>
|
|
||||||
<div style="margin-top:10px;"><button id="btnLoadGiteaRepos">Load My Gitea Repos</button>
|
|
||||||
<div id="giteaRepoContainer"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="previewArea" class="card">
|
<div class="toolbar-row toolbar-row--bottom">
|
||||||
<h3 id="previewTitle">Preview</h3>
|
<div class="tool-group tool-group--workspace">
|
||||||
<pre id="previewContent" class="preview-content">Select a file to preview</pre>
|
<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>
|
||||||
|
|
||||||
<div id="commitLogs" class="card">
|
<div class="tool-group tool-group--platform">
|
||||||
<h3>Commit Logs</h3>
|
<input id="platform" type="hidden" value="gitea">
|
||||||
<div id="logs">No commits yet.</div>
|
<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>
|
||||||
|
<div class="toolbar-status-wrap">
|
||||||
|
<span class="status-dot" aria-hidden="true"></span>
|
||||||
|
<span id="status" class="status">Bereit</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Nur in der Repo-Ansicht sichtbar: erscheint, sobald mindestens ein Knopf darin eingeblendet ist. -->
|
||||||
|
<div class="toolbar-row toolbar-row--context">
|
||||||
|
<button id="btnBack" class="secondary hidden" title="Zurück">⬅️ Zurück</button>
|
||||||
|
<div class="tool-group tool-group--repo-nav">
|
||||||
|
<span class="tool-group-title">Repository</span>
|
||||||
|
<button id="btnCommits" class="hidden" title="Commit History anzeigen">📊 Commits</button>
|
||||||
|
<button id="btnReleases" class="hidden" title="Releases anzeigen">📦 Releases</button>
|
||||||
|
<button id="btnPulls" class="hidden" title="Pull Requests ansehen, kommentieren und mergen">🔀 PRs</button>
|
||||||
|
<button id="btnIssues" class="hidden" title="Issues ansehen und kommentieren">🐞 Issues</button>
|
||||||
|
<button id="btnLocalChanges" class="hidden" title="Lokale Änderungen ansehen und committen">📝 Änderungen</button>
|
||||||
|
</div>
|
||||||
|
<div class="tool-group tool-group--repo-tools">
|
||||||
|
<span class="tool-group-title">Werkzeuge</span>
|
||||||
|
<button id="btnCleanupIgnored" class="hidden" title="Dateien entfernen, die laut .gitignore nicht ins Repo gehören">🧹 Aufräumen</button>
|
||||||
|
<button id="btnGitignoreTemplate" class="hidden" title="Standard-.gitignore (Node, .NET, Java, Python …) im Repo anlegen">📄 .gitignore</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">Links den Bereich wählen. „Speichern“ übernimmt alle Änderungen aus allen Bereichen.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-shell">
|
||||||
|
<nav class="settings-nav" role="tablist" aria-label="Einstellungsbereiche" aria-orientation="vertical">
|
||||||
|
<button type="button" class="settings-tab is-active" role="tab" data-settings-tab="konten" aria-selected="true">
|
||||||
|
<span class="settings-tab-icon" aria-hidden="true">🔑</span>
|
||||||
|
<span class="settings-tab-text"><strong>Konten</strong><small>Gitea, GitHub, Verbindung</small></span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="settings-tab" role="tab" data-settings-tab="darstellung" aria-selected="false">
|
||||||
|
<span class="settings-tab-icon" aria-hidden="true">🎨</span>
|
||||||
|
<span class="settings-tab-text"><strong>Darstellung</strong><small>Übersicht und Explorer</small></span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="settings-tab" role="tab" data-settings-tab="uploads" aria-selected="false">
|
||||||
|
<span class="settings-tab-icon" aria-hidden="true">📤</span>
|
||||||
|
<span class="settings-tab-text"><strong>Uploads</strong><small>Ausschlüsse, Prüfungen</small></span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="settings-tab" role="tab" data-settings-tab="system" aria-selected="false">
|
||||||
|
<span class="settings-tab-icon" aria-hidden="true">🖥️</span>
|
||||||
|
<span class="settings-tab-text"><strong>System</strong><small>Autostart, Version, Updates</small></span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="settings-tab" role="tab" data-settings-tab="backup" aria-selected="false">
|
||||||
|
<span class="settings-tab-icon" aria-hidden="true">💾</span>
|
||||||
|
<span class="settings-tab-text"><strong>Backup</strong><small>Sichern, wiederherstellen</small></span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="settings-tab" role="tab" data-settings-tab="diagnose" aria-selected="false">
|
||||||
|
<span class="settings-tab-icon" aria-hidden="true">🩺</span>
|
||||||
|
<span class="settings-tab-text"><strong>Diagnose</strong><small>Server prüfen, Fehlersuche</small></span>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="settings-pages">
|
||||||
|
<div class="settings-tabpanel" data-settings-panel="konten" role="tabpanel">
|
||||||
|
<div class="settings-column">
|
||||||
|
<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--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--profiles">
|
||||||
|
<div class="settings-panel-header">
|
||||||
|
<div>
|
||||||
|
<h3>Gitea-Server</h3>
|
||||||
|
<p>Mehrere Server oder Konten speichern und mit einem Klick wechseln. Tokens werden verschlüsselt gespeichert.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-backup-card">
|
||||||
|
<div class="fx-row">
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="giteaProfileSelect">Gespeicherte Server</label>
|
||||||
|
<select id="giteaProfileSelect"></select>
|
||||||
|
</div>
|
||||||
|
<button id="btnGiteaProfileUse" class="settings-action-btn fx-browse-btn" type="button">⇄ Wechseln</button>
|
||||||
|
<button id="btnGiteaProfileDelete" class="settings-action-btn fx-browse-btn" type="button">🗑 Entfernen</button>
|
||||||
|
</div>
|
||||||
|
<div class="fx-row">
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="giteaProfileName">Name für den aktuellen Server</label>
|
||||||
|
<input id="giteaProfileName" type="text" placeholder="z. B. Homelab oder Arbeit">
|
||||||
|
</div>
|
||||||
|
<button id="btnGiteaProfileSave" class="settings-action-btn fx-browse-btn" type="button">💾 Als Profil speichern</button>
|
||||||
|
</div>
|
||||||
|
<div id="giteaProfileHint" class="settings-inline-hint">Speichert URL und Token aus „Zugangsdaten“ unter diesem Namen.</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="settings-panel settings-panel--identity">
|
||||||
|
<div class="settings-panel-header">
|
||||||
|
<div>
|
||||||
|
<h3>Git-Identität</h3>
|
||||||
|
<p>Name und E-Mail für Commits, die die App anlegt (Uploads, lokale Commits, Wiederherstellen).</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-backup-card">
|
||||||
|
<div class="fx-row">
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="prefGitName">Name</label>
|
||||||
|
<input id="prefGitName" type="text" placeholder="z. B. M_Viper" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="prefGitEmail">E-Mail</label>
|
||||||
|
<input id="prefGitEmail" type="email" placeholder="z. B. name@example.com" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-inline-hint">Ist im Repo oder in deiner globalen Git-Konfiguration schon ein Name eingetragen, gilt dieser. Leer lassen = „Git Manager GUI“.</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Settings Modal -->
|
<div class="settings-tabpanel hidden" data-settings-panel="darstellung" role="tabpanel">
|
||||||
<div id="settingsModal" class="modal hidden">
|
<div class="settings-column">
|
||||||
|
<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--look">
|
||||||
|
<div class="settings-panel-header">
|
||||||
|
<div>
|
||||||
|
<h3>Design & Größe</h3>
|
||||||
|
<p>Hell oder dunkel und wie groß die Oberfläche angezeigt wird. Wird sofort angewendet.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-backup-card">
|
||||||
|
<div class="fx-row">
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="prefTheme">Design</label>
|
||||||
|
<select id="prefTheme">
|
||||||
|
<option value="dark">Dunkel</option>
|
||||||
|
<option value="light">Hell</option>
|
||||||
|
<option value="system">Wie Windows</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="prefZoom">Größe</label>
|
||||||
|
<select id="prefZoom">
|
||||||
|
<option value="80">80 %</option>
|
||||||
|
<option value="90">90 %</option>
|
||||||
|
<option value="100">100 % (Standard)</option>
|
||||||
|
<option value="110">110 %</option>
|
||||||
|
<option value="125">125 %</option>
|
||||||
|
<option value="150">150 %</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-tabpanel hidden" data-settings-panel="uploads" role="tabpanel">
|
||||||
|
<div class="settings-column">
|
||||||
|
<section class="settings-panel settings-panel--uploads">
|
||||||
|
<div class="settings-panel-header">
|
||||||
|
<div>
|
||||||
|
<h3>Upload-Regeln</h3>
|
||||||
|
<p>Was beim Hochladen geprüft und weggelassen wird.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-toggle-list">
|
||||||
|
<label class="settings-toggle-row" for="prefSecretScan">
|
||||||
|
<span class="settings-toggle-info">
|
||||||
|
<span class="settings-toggle-title">🔐 Zugangsdaten erkennen</span>
|
||||||
|
<span class="settings-toggle-desc">Dateien wie .env, Keystores oder Bot-Tokens werden erkannt und vor dem Upload abgewählt.</span>
|
||||||
|
</span>
|
||||||
|
<span class="toggle-switch">
|
||||||
|
<input type="checkbox" id="prefSecretScan" checked>
|
||||||
|
<span class="toggle-track"></span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<label class="settings-toggle-row" for="prefBlockSecrets">
|
||||||
|
<span class="settings-toggle-info">
|
||||||
|
<span class="settings-toggle-title">🛡️ Zugangsdaten nie hochladen</span>
|
||||||
|
<span class="settings-toggle-desc">Dateien wie credentials.json, .env oder private Schlüssel werden auf allen Upload-Wegen ausgelassen, auch wenn sie in der Vorschau nicht abgewählt wurden. Empfohlen bei öffentlichen Repos.</span>
|
||||||
|
</span>
|
||||||
|
<span class="toggle-switch">
|
||||||
|
<input type="checkbox" id="prefBlockSecrets" checked>
|
||||||
|
<span class="toggle-track"></span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="settings-backup-card">
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="prefUploadPreview">Upload-Vorschau anzeigen</label>
|
||||||
|
<select id="prefUploadPreview">
|
||||||
|
<option value="always">Immer vor dem Hochladen</option>
|
||||||
|
<option value="warnings">Nur bei Zugangsdaten oder sehr vielen Dateien</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="prefNotifyTransfers">Windows-Meldung bei Upload-Ende</label>
|
||||||
|
<select id="prefNotifyTransfers">
|
||||||
|
<option value="hidden">Nur wenn die App nicht im Vordergrund ist</option>
|
||||||
|
<option value="never">Nie</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="input-group input-group--wide">
|
||||||
|
<label for="prefUploadExcludes">Zusätzlich nie hochladen (zusätzlich zur .gitignore)</label>
|
||||||
|
<textarea id="prefUploadExcludes" class="settings-textarea" rows="6" spellcheck="false" placeholder="Ein Muster pro Zeile, z. B. *.log dist/ .idea/ *.psd"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="settings-inline-hint">Gleiche Schreibweise wie in einer .gitignore. Gilt für alle Projekte. node_modules, .git, .vscode und .idea werden immer weggelassen.</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-tabpanel hidden" data-settings-panel="system" role="tabpanel">
|
||||||
|
<div class="settings-column">
|
||||||
|
<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--behaviour">
|
||||||
|
<div class="settings-panel-header">
|
||||||
|
<div>
|
||||||
|
<h3>Verhalten</h3>
|
||||||
|
<p>Updates, Schließen-Knopf und Standardordner.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-toggle-list">
|
||||||
|
<label class="settings-toggle-row" for="prefAutoUpdate">
|
||||||
|
<span class="settings-toggle-info">
|
||||||
|
<span class="settings-toggle-title">🔄 Beim Start nach Updates suchen</span>
|
||||||
|
<span class="settings-toggle-desc">Prüft kurz nach dem Start, ob eine neue Version bereitsteht.</span>
|
||||||
|
</span>
|
||||||
|
<span class="toggle-switch">
|
||||||
|
<input type="checkbox" id="prefAutoUpdate" checked>
|
||||||
|
<span class="toggle-track"></span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<label class="settings-toggle-row" for="prefShowSplash">
|
||||||
|
<span class="settings-toggle-info">
|
||||||
|
<span class="settings-toggle-title">✨ Startanimation</span>
|
||||||
|
<span class="settings-toggle-desc">Beim Öffnen dreht sich das Logo, danach baut sich das Fenster von der Mitte aus auf. Beim automatischen Start mit Windows (im Tray) gibt es sie nie.</span>
|
||||||
|
</span>
|
||||||
|
<span class="toggle-switch">
|
||||||
|
<input type="checkbox" id="prefShowSplash" checked>
|
||||||
|
<span class="toggle-track"></span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="settings-backup-card">
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="prefCloseAction">Beim Schließen des Fensters</label>
|
||||||
|
<select id="prefCloseAction">
|
||||||
|
<option value="auto">Automatisch: in den Tray, wenn die App mit Windows startet</option>
|
||||||
|
<option value="tray">Immer in den Tray (läuft im Hintergrund weiter)</option>
|
||||||
|
<option value="quit">Immer beenden</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="input-group input-group--wide">
|
||||||
|
<label for="prefCloneDir">Standardordner zum Klonen</label>
|
||||||
|
<div class="fx-row">
|
||||||
|
<input id="prefCloneDir" type="text" placeholder="z. B. D:\Projekte">
|
||||||
|
<button id="btnPrefCloneDir" class="settings-action-btn fx-browse-btn" type="button">📂 Wählen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-inline-hint">Wird beim Klonen mehrerer Repos (Batch) als Ziel vorgeschlagen.</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-tabpanel hidden" data-settings-panel="backup" role="tabpanel">
|
||||||
|
<div class="settings-column">
|
||||||
|
<section class="settings-panel settings-panel--repobackup">
|
||||||
|
<div class="settings-panel-header">
|
||||||
|
<div>
|
||||||
|
<h3>Repo-Backup</h3>
|
||||||
|
<p>Alle Gitea-Repos automatisch in einen Ordner oder auf die NAS sichern.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-backup-card">
|
||||||
|
<div class="input-group input-group--wide">
|
||||||
|
<label for="repoBackupTarget">Backup-Ziel (Ordner oder Netzwerkfreigabe)</label>
|
||||||
|
<div class="fx-row">
|
||||||
|
<input id="repoBackupTarget" type="text" placeholder="z. B. \\NAS\backup\gitea oder D:\Backups\gitea">
|
||||||
|
<button id="btnRepoBackupBrowse" class="settings-action-btn fx-browse-btn" type="button">📂 Wählen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="fx-row">
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="repoBackupInterval">Automatisch sichern</label>
|
||||||
|
<select id="repoBackupInterval">
|
||||||
|
<option value="off">Aus</option>
|
||||||
|
<option value="daily">Täglich</option>
|
||||||
|
<option value="weekly">Wöchentlich</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="repoBackupKeep">ZIP-Stände behalten</label>
|
||||||
|
<input id="repoBackupKeep" type="number" min="1" max="100" value="5">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="fx-row">
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="repoBackupTime">Frühestens um</label>
|
||||||
|
<input id="repoBackupTime" type="time">
|
||||||
|
</div>
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="repoBackupNotify">Benachrichtigen</label>
|
||||||
|
<select id="repoBackupNotify">
|
||||||
|
<option value="errors">Nur bei Fehlern oder Abbruch</option>
|
||||||
|
<option value="always">Nach jedem Backup</option>
|
||||||
|
<option value="never">Nie</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="input-group input-group--wide">
|
||||||
|
<label for="repoBackupExclude">Diese Repos nicht sichern</label>
|
||||||
|
<textarea id="repoBackupExclude" class="settings-textarea" rows="4" spellcheck="false" placeholder="Ein Muster pro Zeile, z. B. test-* M_Viper/alt-* *-archiv"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="settings-tools-grid">
|
||||||
|
<button id="btnRepoBackupRun" class="settings-action-btn" type="button">▶️ Jetzt sichern</button>
|
||||||
|
<button id="btnRepoBackupOpen" class="settings-action-btn" type="button">📂 Backup-Ordner öffnen</button>
|
||||||
|
</div>
|
||||||
|
<div id="repoBackupStatus" class="settings-inline-hint">Noch kein Backup gelaufen.</div>
|
||||||
|
<div class="settings-inline-hint">Automatische Backups laufen, solange die App geöffnet ist (auch im Tray).</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="settings-panel settings-panel--restore">
|
||||||
|
<div class="settings-panel-header">
|
||||||
|
<div>
|
||||||
|
<h3>Wiederherstellen</h3>
|
||||||
|
<p>Ein gesichertes Repo zurück auf Gitea spielen oder lokal entpacken.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-backup-card">
|
||||||
|
<div class="settings-tools-grid">
|
||||||
|
<button id="btnRepoRestore" class="settings-action-btn" type="button">↺ Backup wiederherstellen</button>
|
||||||
|
</div>
|
||||||
|
<div class="settings-inline-hint">Zeigt alle Sicherungen im Backup-Ziel. Git-Spiegel enthalten die volle Historie, ZIP-Stände nur den Standard-Branch.</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="settings-panel settings-panel--backup">
|
||||||
|
<div class="settings-panel-header">
|
||||||
|
<div>
|
||||||
|
<h3>Einstellungen sichern & übertragen</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="settings-tabpanel hidden" data-settings-panel="diagnose" role="tabpanel">
|
||||||
|
<div class="settings-column">
|
||||||
|
<section class="settings-panel settings-panel--diagnostics">
|
||||||
|
<div class="settings-panel-header">
|
||||||
|
<div>
|
||||||
|
<h3>Diagnose</h3>
|
||||||
|
<p>Diagnosepaket für die Fehlersuche erstellen oder die Verbindung zum Gitea-Server Schritt für Schritt prüfen.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-diagnostics-card">
|
||||||
|
<button id="btnCreateDiagnostics" class="settings-action-btn" type="button">🧪 Diagnose erstellen</button>
|
||||||
|
<button id="btnServerDiagnosis" class="settings-action-btn" type="button">🩺 Server-Diagnose starten</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="settings-panel settings-panel--transport">
|
||||||
|
<div class="settings-panel-header">
|
||||||
|
<div>
|
||||||
|
<h3>Schneller Git-Weg</h3>
|
||||||
|
<p>Bricht der Server beim Git-Transport ab, nutzt die App 24 Stunden lang die langsamere API. Hier siehst du den Stand.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-backup-card">
|
||||||
|
<div id="gitTransportState" class="settings-inline-hint">Wird geprüft …</div>
|
||||||
|
<div class="settings-tools-grid">
|
||||||
|
<button id="btnGitTransportClear" class="settings-action-btn" type="button">🔓 Sperre aufheben</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="settings-panel settings-panel--folders">
|
||||||
|
<div class="settings-panel-header">
|
||||||
|
<div>
|
||||||
|
<h3>Ordner</h3>
|
||||||
|
<p>Wo die App ihre Daten und Protokolle ablegt.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-backup-card">
|
||||||
|
<div class="settings-tools-grid">
|
||||||
|
<button id="btnOpenAppData" class="settings-action-btn" type="button">📂 App-Daten öffnen</button>
|
||||||
|
<button id="btnOpenLogs" class="settings-action-btn" type="button">📄 Protokolle öffnen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="repoActionModal" class="modal hidden">
|
||||||
<div class="modalContent card">
|
<div class="modalContent card">
|
||||||
<h2>Settings</h2>
|
<h2>🚀 Neues Repository erstellen</h2>
|
||||||
<label>GitHub Token</label><input id="githubToken" type="password">
|
|
||||||
<label>Gitea Token</label><input id="giteaToken" type="password">
|
<div class="input-group">
|
||||||
<label>Gitea URL</label><input id="giteaURL" type="text" placeholder="https://gitea.example.com">
|
<label>Repository Name</label>
|
||||||
<div style="margin-top:10px;">
|
<input id="repoName" type="text" placeholder="mein-projekt">
|
||||||
<button id="btnSaveSettings">Save</button>
|
<div id="repoNameValidationHint" class="settings-inline-hint">Name prüfen: Duplikate, ähnliche Namen und ungültige Zeichen werden erkannt.</div>
|
||||||
<button id="btnCloseSettings">Close</button>
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<label>Target Branch</label>
|
||||||
|
<input id="targetBranch" type="text" value="main" placeholder="main">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<label>Lizenz</label>
|
||||||
|
<select id="licenseSelect">
|
||||||
|
<option value="">Keine Lizenz</option>
|
||||||
|
<option value="mit">MIT License</option>
|
||||||
|
<option value="apache-2.0">Apache 2.0</option>
|
||||||
|
<option value="gpl-3.0">GPL v3</option>
|
||||||
|
<option value="bsd-3-clause">BSD 3-Clause</option>
|
||||||
|
<option value="agpl-3.0">AGPL v3</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
|
||||||
|
<input type="checkbox" id="createReadme" checked>
|
||||||
|
<span style="text-transform: none; letter-spacing: normal;">README.md initialisieren</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<label>Lokaler Push-Branch</label>
|
||||||
|
<select id="branchSelect">
|
||||||
|
<option value="main">main</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-buttons">
|
||||||
|
<button id="btnCreateRepo" class="accent-btn">Erstellen</button>
|
||||||
|
<button id="btnCloseRepoActions" class="secondary">Abbrechen</button>
|
||||||
|
</div>
|
||||||
|
</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">
|
||||||
|
<div class="file-editor-title">
|
||||||
|
<span id="fileEditorIcon">📄</span>
|
||||||
|
<span id="fileEditorName">file.txt</span>
|
||||||
|
</div>
|
||||||
|
<div class="file-editor-toolbar">
|
||||||
|
<button id="btnEditorSearch" class="editor-tool-btn" title="Suchen (Ctrl+F)">🔍</button>
|
||||||
|
<button id="btnEditorSave" class="editor-tool-btn" title="Speichern">💾</button>
|
||||||
|
<button id="btnCloseEditor" class="editor-tool-btn" title="Schließen">✕</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="fileEditorTabs" class="file-editor-tabs"></div>
|
||||||
|
|
||||||
|
<div id="searchBar" class="search-bar hidden">
|
||||||
|
<input id="searchInput" type="text" class="search-input" placeholder="Suchen...">
|
||||||
|
<input id="replaceInput" type="text" class="search-input" placeholder="Ersetzen...">
|
||||||
|
<button id="btnReplace" class="search-btn">Ersetzen</button>
|
||||||
|
<button id="btnReplaceAll" class="search-btn">Alle</button>
|
||||||
|
<button id="btnCloseSearch" class="search-btn">✕</button>
|
||||||
|
<span id="searchInfo" class="search-info">0/0</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="file-editor-container">
|
||||||
|
<div id="lineNumbers" class="line-numbers"></div>
|
||||||
|
<textarea id="fileEditorContent" class="file-editor-textarea" placeholder="Dateiinhalt..."></textarea>
|
||||||
|
<div id="imagePreview" class="image-preview hidden"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="file-editor-footer">
|
||||||
|
<div class="file-editor-info">
|
||||||
|
<span id="fileEditorPath" class="file-path">Pfad: /path/to/file</span>
|
||||||
|
<span id="fileEditorStats" class="file-stats"></span>
|
||||||
|
<span id="fileEditorCursor" class="file-cursor">Zeile 1, Spalte 1</span>
|
||||||
|
<span id="autoSaveStatus" class="auto-save-status" style="display:none;">✓ Gespeichert</span>
|
||||||
|
</div>
|
||||||
|
<div class="modal-buttons">
|
||||||
|
<button id="btnDiscardEdit" class="secondary">Verwerfen</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="./renderer.js"></script>
|
<div id="updateModal" class="modal hidden">
|
||||||
|
<div class="modalContent card" style="max-width: 450px; border: 1px solid var(--accent-primary); box-shadow: 0 0 30px rgba(0, 212, 255, 0.2);">
|
||||||
|
<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); 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>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<label style="color: var(--accent-primary); font-size: 0.8rem; letter-spacing: 1px;">RELEASE NOTES</label>
|
||||||
|
<div id="updateChangelog" style="
|
||||||
|
background: rgba(0,0,0,0.25);
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
max-height: 180px;
|
||||||
|
overflow-y: auto;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--text-primary);
|
||||||
|
border: 1px solid rgba(255,255,255,0.05);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-buttons" style="margin-top: 25px; gap: 12px;">
|
||||||
|
<button id="btnStartUpdate" class="accent-btn" style="flex: 2; height: 45px; font-weight: bold;">🚀 Jetzt installieren</button>
|
||||||
|
<button id="btnIgnoreUpdate" class="secondary" style="flex: 1; height: 45px;">Später</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div> <script src="renderer.js"></script>
|
||||||
|
<script src="features.js"></script>
|
||||||
|
<script src="settings-extra.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
+8796
-518
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,285 @@
|
|||||||
|
// renderer/settings-extra.js
|
||||||
|
// Ergänzungen der Einstellungen: App-Einstellungen (Identität, Uploads, Design, System),
|
||||||
|
// ein gemeinsames "Speichern" für alle Bereiche, Warnung bei ungespeicherten Änderungen,
|
||||||
|
// mehrere Gitea-Server, Token anzeigen/entfernen und Diagnose-Hilfen.
|
||||||
|
// Wird nach renderer.js und features.js geladen und nutzt deren globale Helfer.
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
let savedPrefs = null;
|
||||||
|
|
||||||
|
/* ---------- Design ---------- */
|
||||||
|
const systemDark = window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null;
|
||||||
|
|
||||||
|
function applyTheme(theme) {
|
||||||
|
const effective = theme === 'system' ? (systemDark && !systemDark.matches ? 'light' : 'dark') : theme;
|
||||||
|
document.documentElement.dataset.theme = effective === 'light' ? 'light' : 'dark';
|
||||||
|
}
|
||||||
|
if (systemDark) {
|
||||||
|
systemDark.addEventListener('change', () => { if (savedPrefs && savedPrefs.theme === 'system') applyTheme('system'); });
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Laden ---------- */
|
||||||
|
async function loadPrefs() {
|
||||||
|
const res = await window.electronAPI.prefsGet();
|
||||||
|
if (!res || !res.ok) return null;
|
||||||
|
savedPrefs = res.prefs;
|
||||||
|
applyTheme(savedPrefs.theme);
|
||||||
|
return savedPrefs;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillPrefFields(p) {
|
||||||
|
if (!p) return;
|
||||||
|
const set = (id, v) => { const el = $(id); if (!el) return; if (el.type === 'checkbox') el.checked = !!v; else el.value = v; };
|
||||||
|
set('prefGitName', p.gitName);
|
||||||
|
set('prefGitEmail', p.gitEmail);
|
||||||
|
set('prefTheme', p.theme);
|
||||||
|
set('prefZoom', String(p.zoom));
|
||||||
|
if ($('prefZoom') && !$('prefZoom').value) $('prefZoom').value = '100';
|
||||||
|
set('prefAutoUpdate', p.autoUpdateCheck);
|
||||||
|
set('prefShowSplash', p.showSplash);
|
||||||
|
set('prefCloseAction', p.closeAction);
|
||||||
|
set('prefCloneDir', p.defaultCloneDir);
|
||||||
|
set('prefSecretScan', p.uploadSecretScan);
|
||||||
|
set('prefBlockSecrets', p.uploadBlockSecrets);
|
||||||
|
set('prefUploadPreview', p.uploadPreview);
|
||||||
|
set('prefNotifyTransfers', p.notifyTransfers);
|
||||||
|
set('prefUploadExcludes', p.uploadExtraExcludes);
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectPrefs() {
|
||||||
|
const val = (id) => ($(id) ? $(id).value : undefined);
|
||||||
|
const chk = (id) => ($(id) ? $(id).checked : undefined);
|
||||||
|
return {
|
||||||
|
gitName: val('prefGitName'),
|
||||||
|
gitEmail: val('prefGitEmail'),
|
||||||
|
theme: val('prefTheme'),
|
||||||
|
zoom: Number(val('prefZoom')),
|
||||||
|
autoUpdateCheck: chk('prefAutoUpdate'),
|
||||||
|
showSplash: chk('prefShowSplash'),
|
||||||
|
closeAction: val('prefCloseAction'),
|
||||||
|
defaultCloneDir: val('prefCloneDir'),
|
||||||
|
uploadSecretScan: chk('prefSecretScan'),
|
||||||
|
uploadBlockSecrets: chk('prefBlockSecrets'),
|
||||||
|
uploadPreview: val('prefUploadPreview'),
|
||||||
|
notifyTransfers: val('prefNotifyTransfers'),
|
||||||
|
uploadExtraExcludes: val('prefUploadExcludes')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Ungespeicherte Änderungen ---------- */
|
||||||
|
let snapshot = null;
|
||||||
|
|
||||||
|
function readFormState() {
|
||||||
|
const state = {};
|
||||||
|
document.querySelectorAll('#settingsModal input, #settingsModal select, #settingsModal textarea').forEach(el => {
|
||||||
|
if (!el.id || el.type === 'file') return;
|
||||||
|
state[el.id] = el.type === 'checkbox' ? el.checked : el.value;
|
||||||
|
});
|
||||||
|
return JSON.stringify(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function takeSnapshot() {
|
||||||
|
// Erst nachdem alle Felder befüllt sind (Zugangsdaten, App-Einstellungen, Backup)
|
||||||
|
fillPrefFields(await loadPrefs());
|
||||||
|
if (window.gmBackupSettings) await window.gmBackupSettings.load();
|
||||||
|
await loadProfiles();
|
||||||
|
await refreshTransportState();
|
||||||
|
snapshot = readFormState();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wird vom "Abbrechen"-Knopf in renderer.js gefragt
|
||||||
|
window.settingsCanClose = async function () {
|
||||||
|
if (!snapshot || readFormState() === snapshot) return true;
|
||||||
|
const discard = await showActionConfirmModal({
|
||||||
|
title: 'Ungespeicherte Änderungen',
|
||||||
|
message: 'Du hast Einstellungen geändert, aber nicht gespeichert. Änderungen verwerfen?',
|
||||||
|
confirmText: 'Verwerfen',
|
||||||
|
danger: true
|
||||||
|
});
|
||||||
|
if (discard && savedPrefs) applyTheme(savedPrefs.theme); // Vorschau zurücknehmen
|
||||||
|
return discard;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ---------- Gemeinsames Speichern ---------- */
|
||||||
|
async function saveExtras() {
|
||||||
|
const problems = [];
|
||||||
|
const res = await window.electronAPI.prefsSet(collectPrefs());
|
||||||
|
if (res && res.prefs) { savedPrefs = res.prefs; applyTheme(savedPrefs.theme); fillPrefFields(savedPrefs); }
|
||||||
|
if (!res || !res.ok) problems.push(res && res.error ? res.error : 'App-Einstellungen nicht gespeichert');
|
||||||
|
if (window.gmBackupSettings) {
|
||||||
|
const b = await window.gmBackupSettings.save(true);
|
||||||
|
if (!b || !b.ok) problems.push('Backup-Einstellung nicht gespeichert' + (b && b.error ? ': ' + b.error : ''));
|
||||||
|
}
|
||||||
|
snapshot = null;
|
||||||
|
if (problems.length) showWarning(problems.join(' · '));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Mehrere Gitea-Server ---------- */
|
||||||
|
async function loadProfiles() {
|
||||||
|
const sel = $('giteaProfileSelect');
|
||||||
|
if (!sel) return;
|
||||||
|
const creds = (await window.electronAPI.loadCredentials()) || {};
|
||||||
|
const profiles = Array.isArray(creds.giteaProfiles) ? creds.giteaProfiles : [];
|
||||||
|
sel.textContent = '';
|
||||||
|
if (!profiles.length) {
|
||||||
|
sel.append(new Option('Noch keine Profile gespeichert', ''));
|
||||||
|
sel.disabled = true;
|
||||||
|
} else {
|
||||||
|
sel.disabled = false;
|
||||||
|
for (const p of profiles) {
|
||||||
|
const active = p.url === creds.giteaURL && p.token === creds.giteaToken;
|
||||||
|
sel.append(new Option(`${p.name} – ${p.url}${active ? ' (aktiv)' : ''}`, p.name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$('btnGiteaProfileUse').disabled = $('btnGiteaProfileDelete').disabled = !profiles.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveProfile() {
|
||||||
|
const name = $('giteaProfileName').value.trim();
|
||||||
|
const url = $('giteaURL').value.trim();
|
||||||
|
const token = $('giteaToken').value.trim();
|
||||||
|
if (!name) { showWarning('Bitte einen Namen für das Profil eingeben.'); $('giteaProfileName').focus(); return; }
|
||||||
|
if (!url || !token) { showWarning('Bitte zuerst Gitea-URL und Token unter „Zugangsdaten“ eintragen.'); return; }
|
||||||
|
const creds = (await window.electronAPI.loadCredentials()) || {};
|
||||||
|
const profiles = (Array.isArray(creds.giteaProfiles) ? creds.giteaProfiles : []).filter(p => p.name !== name);
|
||||||
|
profiles.push({ name, url, token });
|
||||||
|
profiles.sort((a, b) => a.name.localeCompare(b.name, 'de'));
|
||||||
|
await window.electronAPI.saveCredentials({ ...creds, giteaProfiles: profiles });
|
||||||
|
$('giteaProfileName').value = '';
|
||||||
|
await loadProfiles();
|
||||||
|
snapshot = readFormState();
|
||||||
|
showSuccess(`Profil „${name}“ gespeichert`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function useProfile() {
|
||||||
|
const name = $('giteaProfileSelect').value;
|
||||||
|
const creds = (await window.electronAPI.loadCredentials()) || {};
|
||||||
|
const p = (creds.giteaProfiles || []).find(x => x.name === name);
|
||||||
|
if (!p) return;
|
||||||
|
$('giteaURL').value = p.url;
|
||||||
|
$('giteaToken').value = p.token;
|
||||||
|
// Wie ein normales "Speichern": übernimmt den Server und lädt danach die Repos neu
|
||||||
|
$('btnSaveSettings').click();
|
||||||
|
setTimeout(() => { if (typeof loadRepos === 'function') loadRepos(); }, 600);
|
||||||
|
showInfo(`Wechsle zu „${p.name}“ …`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteProfile() {
|
||||||
|
const name = $('giteaProfileSelect').value;
|
||||||
|
if (!name) return;
|
||||||
|
const ok = await showActionConfirmModal({
|
||||||
|
title: 'Profil entfernen',
|
||||||
|
message: `Das gespeicherte Profil „${name}“ wird entfernt. Der aktuell eingetragene Server bleibt unverändert.`,
|
||||||
|
confirmText: 'Entfernen',
|
||||||
|
danger: true
|
||||||
|
});
|
||||||
|
if (!ok) return;
|
||||||
|
const creds = (await window.electronAPI.loadCredentials()) || {};
|
||||||
|
await window.electronAPI.saveCredentials({ ...creds, giteaProfiles: (creds.giteaProfiles || []).filter(p => p.name !== name) });
|
||||||
|
await loadProfiles();
|
||||||
|
snapshot = readFormState();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Token anzeigen / entfernen ---------- */
|
||||||
|
function addTokenTools(inputId) {
|
||||||
|
const input = $(inputId);
|
||||||
|
if (!input || input.dataset.tools) return;
|
||||||
|
input.dataset.tools = '1';
|
||||||
|
const wrap = document.createElement('div');
|
||||||
|
wrap.className = 'token-field';
|
||||||
|
input.parentNode.insertBefore(wrap, input);
|
||||||
|
wrap.appendChild(input);
|
||||||
|
const eye = document.createElement('button');
|
||||||
|
eye.type = 'button';
|
||||||
|
eye.className = 'token-tool';
|
||||||
|
eye.title = 'Token anzeigen';
|
||||||
|
eye.textContent = '👁';
|
||||||
|
eye.onclick = () => {
|
||||||
|
const show = input.type === 'password';
|
||||||
|
input.type = show ? 'text' : 'password';
|
||||||
|
eye.title = show ? 'Token verbergen' : 'Token anzeigen';
|
||||||
|
eye.classList.toggle('is-on', show);
|
||||||
|
};
|
||||||
|
const clear = document.createElement('button');
|
||||||
|
clear.type = 'button';
|
||||||
|
clear.className = 'token-tool';
|
||||||
|
clear.title = 'Token entfernen (wirksam nach „Speichern“)';
|
||||||
|
clear.textContent = '✕';
|
||||||
|
clear.onclick = () => { input.value = ''; input.focus(); };
|
||||||
|
wrap.append(eye, clear);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Diagnose ---------- */
|
||||||
|
async function refreshTransportState() {
|
||||||
|
const el = $('gitTransportState');
|
||||||
|
const btn = $('btnGitTransportClear');
|
||||||
|
if (!el) return;
|
||||||
|
const res = await window.electronAPI.gitTransportState();
|
||||||
|
const blocked = (res && res.blocked) || [];
|
||||||
|
if (!blocked.length) {
|
||||||
|
el.textContent = '✅ Nicht gesperrt: Uploads, Löschen und Backup nutzen den schnellen Git-Weg, sobald der Server ihn anbietet.';
|
||||||
|
if (btn) btn.disabled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
el.textContent = blocked.map(b => `⛔ ${b.host} gesperrt seit ${new Date(b.since).toLocaleString('de-DE')} – die App nutzt dort die langsamere API.`).join('\n');
|
||||||
|
if (btn) { btn.disabled = false; btn.dataset.host = blocked[0].host; }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openFolder(which) {
|
||||||
|
const r = await window.electronAPI.openKnownFolder(which);
|
||||||
|
if (!r || !r.ok) showWarning((r && r.error) || 'Ordner konnte nicht geöffnet werden');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Verdrahtung ---------- */
|
||||||
|
function wire() {
|
||||||
|
addTokenTools('githubToken');
|
||||||
|
addTokenTools('giteaToken');
|
||||||
|
|
||||||
|
const on = (id, fn) => { const el = $(id); if (el) el.addEventListener('click', fn); };
|
||||||
|
on('btnGiteaProfileSave', saveProfile);
|
||||||
|
on('btnGiteaProfileUse', useProfile);
|
||||||
|
on('btnGiteaProfileDelete', deleteProfile);
|
||||||
|
on('btnPrefCloneDir', async () => { const d = await window.electronAPI.selectFolder(); if (d) $('prefCloneDir').value = d; });
|
||||||
|
on('btnRepoBackupOpen', () => openFolder('backup'));
|
||||||
|
on('btnOpenAppData', () => openFolder('appData'));
|
||||||
|
on('btnOpenLogs', () => openFolder('logs'));
|
||||||
|
on('btnGitTransportClear', async (e) => {
|
||||||
|
const r = await window.electronAPI.gitTransportClear(e.currentTarget.dataset.host || '');
|
||||||
|
if (r && r.ok) showSuccess('Sperre aufgehoben. Der nächste Upload probiert wieder den schnellen Git-Weg.');
|
||||||
|
refreshTransportState();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Design als Vorschau sofort zeigen; "Abbrechen" nimmt sie zurück
|
||||||
|
const theme = $('prefTheme');
|
||||||
|
if (theme) theme.addEventListener('change', () => applyTheme(theme.value));
|
||||||
|
|
||||||
|
// Gemeinsames Speichern: läuft zusätzlich zum bisherigen Speichern der Zugangsdaten
|
||||||
|
on('btnSaveSettings', () => { saveExtras(); });
|
||||||
|
|
||||||
|
// Beim Öffnen der Einstellungen Stand merken (für die Warnung beim Abbrechen)
|
||||||
|
on('btnSettings', () => setTimeout(takeSnapshot, 80));
|
||||||
|
|
||||||
|
// Standardordner beim Klonen mehrerer Repos vorschlagen
|
||||||
|
on('btnBatchActions', () => setTimeout(() => {
|
||||||
|
const field = $('batchCloneTarget');
|
||||||
|
if (field && !field.value && savedPrefs && savedPrefs.defaultCloneDir) field.value = savedPrefs.defaultCloneDir;
|
||||||
|
}, 50));
|
||||||
|
|
||||||
|
// Diagnose-Bereich: Sperrstatus aktualisieren, wenn er geöffnet wird
|
||||||
|
const diagTab = document.querySelector('#settingsModal [data-settings-tab="diagnose"]');
|
||||||
|
if (diagTab) diagTab.addEventListener('click', refreshTransportState);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gemeinsamer Zugriff für features.js / renderer.js
|
||||||
|
window.gmPrefs = {
|
||||||
|
get: () => savedPrefs,
|
||||||
|
load: loadPrefs,
|
||||||
|
takeSnapshot
|
||||||
|
};
|
||||||
|
|
||||||
|
loadPrefs().catch(() => {});
|
||||||
|
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', wire);
|
||||||
|
else wire();
|
||||||
|
})();
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src 'self' data:; style-src 'unsafe-inline'; script-src 'self'">
|
||||||
|
<title>Git Manager</title>
|
||||||
|
<style>
|
||||||
|
html, body { margin: 0; height: 100%; overflow: hidden; background: transparent; user-select: none; cursor: default; }
|
||||||
|
body { font-family: "Segoe UI Variable Text", "Segoe UI", system-ui, sans-serif; }
|
||||||
|
|
||||||
|
/* Logo und Schriftzug in der Mitte */
|
||||||
|
.marke {
|
||||||
|
position: fixed; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 14px;
|
||||||
|
transition: opacity .28s ease, transform .32s ease;
|
||||||
|
}
|
||||||
|
.buehne { perspective: 700px; animation: rein .45s cubic-bezier(.2, .9, .3, 1.2) both; }
|
||||||
|
.logo {
|
||||||
|
width: 132px; height: 132px; display: block; object-fit: contain;
|
||||||
|
filter: drop-shadow(0 10px 26px rgba(0, 0, 0, .55)) drop-shadow(0 0 22px rgba(90, 160, 255, .35));
|
||||||
|
animation: drehen 1.3s cubic-bezier(.45, .05, .25, 1) .15s infinite;
|
||||||
|
}
|
||||||
|
.name {
|
||||||
|
font-size: 34px; font-weight: 600; letter-spacing: .06em;
|
||||||
|
background: linear-gradient(90deg, #7fe3ff, #6c9cff); -webkit-background-clip: text; background-clip: text; color: transparent;
|
||||||
|
filter: drop-shadow(0 2px 8px rgba(0, 0, 0, .8));
|
||||||
|
animation: schrift .5s ease .25s both;
|
||||||
|
}
|
||||||
|
@keyframes rein { from { opacity: 0; transform: scale(.6); } to { opacity: 1; transform: none; } }
|
||||||
|
@keyframes drehen { from { transform: rotateY(0deg); } to { transform: rotateY(360deg); } }
|
||||||
|
@keyframes schrift { from { opacity: 0; transform: translateY(8px); letter-spacing: .3em; } to { opacity: 1; transform: none; letter-spacing: .06em; } }
|
||||||
|
|
||||||
|
/* das fertige Fenster als Bild, wächst von der Mitte nach außen */
|
||||||
|
#fenster { position: fixed; inset: 0; width: 100%; height: 100%; visibility: hidden; }
|
||||||
|
body.aufbau .marke { opacity: 0; transform: scale(.82); }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="marke">
|
||||||
|
<div class="buehne"><img class="logo" src="./icon.png" alt=""></div>
|
||||||
|
<div class="name">Git Manager</div>
|
||||||
|
</div>
|
||||||
|
<img id="fenster" alt="">
|
||||||
|
<script src="./splash.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// Vom Hauptprozess aufgerufen: das Bild des fertigen Fensters von der Mitte aus aufziehen.
|
||||||
|
// Liefert true, wenn die Animation durchgelaufen ist, sonst false.
|
||||||
|
window.aufbauen = (bildAdresse) => new Promise((fertig) => {
|
||||||
|
const bild = document.getElementById('fenster');
|
||||||
|
bild.onerror = () => fertig(false);
|
||||||
|
bild.onload = () => {
|
||||||
|
const v = Math.max(0, innerHeight / 2 - 66);
|
||||||
|
const h = Math.max(0, innerWidth / 2 - 66);
|
||||||
|
bild.style.visibility = 'visible';
|
||||||
|
document.body.classList.add('aufbau');
|
||||||
|
bild.animate([
|
||||||
|
{ clipPath: `inset(${v}px ${h}px round 30px)`, opacity: 0.2 },
|
||||||
|
{ opacity: 1, offset: 0.25 },
|
||||||
|
{ clipPath: 'inset(0px 0px round 0px)', opacity: 1 }
|
||||||
|
], { duration: 560, easing: 'cubic-bezier(.2, .8, .2, 1)', fill: 'forwards' })
|
||||||
|
.finished.then(() => fertig(true), () => fertig(false));
|
||||||
|
};
|
||||||
|
bild.src = bildAdresse;
|
||||||
|
});
|
||||||
+4565
-36
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,71 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'self'">
|
||||||
|
<title>Git Manager Menü</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #0f1a2e;
|
||||||
|
--fg: #e8eefc;
|
||||||
|
--muted: #8fa0c0;
|
||||||
|
--hover: rgba(88, 213, 255, 0.16);
|
||||||
|
--border: rgba(88, 213, 255, 0.24);
|
||||||
|
--sep: rgba(138, 164, 214, 0.22);
|
||||||
|
--accent: #67e8f9;
|
||||||
|
}
|
||||||
|
body.light {
|
||||||
|
--bg: #f7f9fd;
|
||||||
|
--fg: #16233d;
|
||||||
|
--muted: #6a7a99;
|
||||||
|
--hover: rgba(40, 110, 220, 0.12);
|
||||||
|
--border: rgba(30, 60, 110, 0.2);
|
||||||
|
--sep: rgba(30, 60, 110, 0.14);
|
||||||
|
--accent: #1d6fe0;
|
||||||
|
}
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--fg);
|
||||||
|
font: 13px "Segoe UI Variable Text", "Segoe UI", Arial, sans-serif;
|
||||||
|
user-select: none;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
body { padding: 8px; }
|
||||||
|
#menu {
|
||||||
|
background: var(--bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 6px;
|
||||||
|
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.45);
|
||||||
|
}
|
||||||
|
.header { font-weight: 700; padding: 8px 12px 2px; }
|
||||||
|
.status { color: var(--muted); font-size: 12px; padding: 2px 12px; }
|
||||||
|
.sep { height: 1px; background: var(--sep); margin: 6px 4px; }
|
||||||
|
.item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
width: 100%;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 12px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
text-align: left;
|
||||||
|
cursor: default;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.item:hover:not(:disabled), .item.active:not(:disabled) { background: var(--hover); }
|
||||||
|
.item:disabled { color: var(--muted); opacity: 0.6; }
|
||||||
|
.check { width: 14px; flex: 0 0 14px; text-align: center; color: var(--accent); font-weight: 700; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="menu" role="menu"></div>
|
||||||
|
<script src="./tray-menu.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
// Zeichnet das Tray-Menü aus dem Zustand, den der Hauptprozess schickt.
|
||||||
|
// Ohne Electron (Seite direkt im Browser geöffnet) greift ein Stub, damit sich die Optik prüfen lässt.
|
||||||
|
(function () {
|
||||||
|
const api = window.trayApi || {
|
||||||
|
onState: () => {},
|
||||||
|
action: (id) => console.log('action', id),
|
||||||
|
resize: () => {},
|
||||||
|
close: () => {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const menu = document.getElementById('menu');
|
||||||
|
let buttons = [];
|
||||||
|
let activeIndex = -1;
|
||||||
|
|
||||||
|
function setActive(index) {
|
||||||
|
activeIndex = index;
|
||||||
|
buttons.forEach((b, i) => b.classList.toggle('active', i === index));
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(state) {
|
||||||
|
document.body.classList.toggle('light', state && state.dark === false);
|
||||||
|
menu.textContent = '';
|
||||||
|
buttons = [];
|
||||||
|
activeIndex = -1;
|
||||||
|
|
||||||
|
for (const item of (state && state.items) || []) {
|
||||||
|
if (item.type === 'separator') {
|
||||||
|
const sep = document.createElement('div');
|
||||||
|
sep.className = 'sep';
|
||||||
|
menu.appendChild(sep);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (item.id === 'header' || item.id === 'status' || item.id === 'queue') {
|
||||||
|
const line = document.createElement('div');
|
||||||
|
line.className = item.id === 'header' ? 'header' : 'status';
|
||||||
|
line.textContent = item.label;
|
||||||
|
menu.appendChild(line);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const button = document.createElement('button');
|
||||||
|
button.className = 'item';
|
||||||
|
button.disabled = item.enabled === false;
|
||||||
|
button.setAttribute('role', item.type === 'checkbox' ? 'menuitemcheckbox' : 'menuitem');
|
||||||
|
if (item.type === 'checkbox') button.setAttribute('aria-checked', String(!!item.checked));
|
||||||
|
|
||||||
|
const check = document.createElement('span');
|
||||||
|
check.className = 'check';
|
||||||
|
check.textContent = item.type === 'checkbox' && item.checked ? '✓' : '';
|
||||||
|
const label = document.createElement('span');
|
||||||
|
label.textContent = item.label;
|
||||||
|
button.append(check, label);
|
||||||
|
|
||||||
|
button.addEventListener('click', () => api.action(item.id));
|
||||||
|
button.addEventListener('mouseenter', () => { if (!button.disabled) setActive(buttons.indexOf(button)); });
|
||||||
|
button.addEventListener('mouseleave', () => setActive(-1));
|
||||||
|
menu.appendChild(button);
|
||||||
|
buttons.push(button);
|
||||||
|
}
|
||||||
|
|
||||||
|
api.resize(Math.ceil(document.body.scrollHeight));
|
||||||
|
}
|
||||||
|
|
||||||
|
function move(step) {
|
||||||
|
const enabled = buttons.map((b, i) => (b.disabled ? -1 : i)).filter((i) => i >= 0);
|
||||||
|
if (enabled.length === 0) return;
|
||||||
|
const pos = enabled.indexOf(activeIndex);
|
||||||
|
const next = pos < 0 ? (step > 0 ? 0 : enabled.length - 1) : (pos + step + enabled.length) % enabled.length;
|
||||||
|
setActive(enabled[next]);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Escape') api.close();
|
||||||
|
else if (e.key === 'ArrowDown') { e.preventDefault(); move(1); }
|
||||||
|
else if (e.key === 'ArrowUp') { e.preventDefault(); move(-1); }
|
||||||
|
else if ((e.key === 'Enter' || e.key === ' ') && buttons[activeIndex]) { e.preventDefault(); buttons[activeIndex].click(); }
|
||||||
|
});
|
||||||
|
|
||||||
|
window.__renderTray = render;
|
||||||
|
api.onState(render);
|
||||||
|
})();
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* build-setup.js – eigenes Setup (installer/) mit der App zusammenbauen
|
||||||
|
* ---------------------------------------------------------------------
|
||||||
|
* Läuft nach "electron-builder --win dir" (siehe "build" in package.json):
|
||||||
|
*
|
||||||
|
* 1. dist/win-unpacked (die fertige App inkl. eingebautem Git) als ZIP packen
|
||||||
|
* 2. installer/ (WPF, .NET Framework 4.8) mit der Version aus package.json bauen
|
||||||
|
* 3. ZIP hinten an die Setup-.exe hängen:
|
||||||
|
* [Setup.exe][ZIP][8 Byte ZIP-Länge][8 Byte "GITMGRPK"]
|
||||||
|
* (das Setup liest den Anhang selbst aus, siehe installer/Payload.cs)
|
||||||
|
* 4. SHA-256-Prüfsumme daneben schreiben (der Updater der App prüft sie)
|
||||||
|
* 5. Zwischenstände wegräumen → übrig bleibt dist/Git-Manager-GUI-Setup-<version>.exe
|
||||||
|
*
|
||||||
|
* Benötigt das .NET SDK (dotnet) – das Setup selbst läuft danach auf jedem
|
||||||
|
* Windows 10/11 ohne Zusatzinstallation.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const { execFileSync } = require('child_process');
|
||||||
|
|
||||||
|
const ROOT = path.join(__dirname, '..');
|
||||||
|
const DIST = path.join(ROOT, 'dist');
|
||||||
|
const APP_DIR = path.join(DIST, 'win-unpacked');
|
||||||
|
const APP_EXE = 'Git Manager GUI.exe';
|
||||||
|
const WORK = path.join(DIST, '.setup');
|
||||||
|
const ZIP = path.join(WORK, 'app.zip');
|
||||||
|
const MAGIC = Buffer.from('GITMGRPK', 'ascii');
|
||||||
|
|
||||||
|
function schritt(text) {
|
||||||
|
console.log(`\n» ${text}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sha256(file) {
|
||||||
|
const hash = crypto.createHash('sha256');
|
||||||
|
const fd = fs.openSync(file, 'r');
|
||||||
|
try {
|
||||||
|
const buf = Buffer.alloc(8 * 1024 * 1024);
|
||||||
|
let n;
|
||||||
|
while ((n = fs.readSync(fd, buf, 0, buf.length, null)) > 0) hash.update(buf.subarray(0, n));
|
||||||
|
} finally { fs.closeSync(fd); }
|
||||||
|
return hash.digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
const version = require(path.join(ROOT, 'package.json')).version;
|
||||||
|
const outName = `Git-Manager-GUI-Setup-${version}.exe`;
|
||||||
|
const OUT = path.join(DIST, outName);
|
||||||
|
|
||||||
|
if (!fs.existsSync(path.join(APP_DIR, APP_EXE))) {
|
||||||
|
throw new Error(`${APP_DIR}\\${APP_EXE} fehlt – vorher "electron-builder --win dir" ausführen.`);
|
||||||
|
}
|
||||||
|
if (!fs.existsSync(path.join(APP_DIR, 'resources', 'mingit', 'cmd', 'git.exe'))) {
|
||||||
|
throw new Error('Eingebautes Git fehlt in resources/mingit – extraResources in package.json prüfen.');
|
||||||
|
}
|
||||||
|
fs.rmSync(WORK, { recursive: true, force: true });
|
||||||
|
fs.mkdirSync(WORK, { recursive: true });
|
||||||
|
|
||||||
|
schritt('App wird gepackt …');
|
||||||
|
// .NET-ZIP über PowerShell: in jedem Windows enthalten, keine zusätzliche npm-Abhängigkeit
|
||||||
|
const ps = [
|
||||||
|
'Add-Type -AssemblyName System.IO.Compression.FileSystem',
|
||||||
|
`[System.IO.Compression.ZipFile]::CreateFromDirectory('${APP_DIR.replace(/'/g, "''")}', '${ZIP.replace(/'/g, "''")}', [System.IO.Compression.CompressionLevel]::Optimal, $false)`,
|
||||||
|
].join('; ');
|
||||||
|
execFileSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', ps], { stdio: 'inherit' });
|
||||||
|
|
||||||
|
schritt(`Setup ${version} wird gebaut …`);
|
||||||
|
const stubDir = path.join(WORK, 'stub');
|
||||||
|
execFileSync('dotnet', [
|
||||||
|
'build', path.join(ROOT, 'installer', 'GitManagerSetup.csproj'),
|
||||||
|
'-c', 'Release', `-p:Version=${version}`, '-p:DebugType=none',
|
||||||
|
'--nologo', '-v', 'q', '-o', stubDir,
|
||||||
|
], { stdio: 'inherit' });
|
||||||
|
const stub = path.join(stubDir, 'Git-Manager-GUI-Setup.exe');
|
||||||
|
if (!fs.existsSync(stub)) throw new Error('Build des Setups fehlgeschlagen.');
|
||||||
|
|
||||||
|
schritt('App wird ans Setup angehängt …');
|
||||||
|
const zipSize = fs.statSync(ZIP).size;
|
||||||
|
const footer = Buffer.alloc(16);
|
||||||
|
footer.writeBigInt64LE(BigInt(zipSize), 0);
|
||||||
|
MAGIC.copy(footer, 8);
|
||||||
|
fs.rmSync(OUT, { force: true });
|
||||||
|
fs.copyFileSync(stub, OUT);
|
||||||
|
// ZIP in Blöcken anhängen statt komplett in den Speicher zu laden
|
||||||
|
const fd = fs.openSync(OUT, 'a');
|
||||||
|
try {
|
||||||
|
const src = fs.openSync(ZIP, 'r');
|
||||||
|
try {
|
||||||
|
const buf = Buffer.alloc(8 * 1024 * 1024);
|
||||||
|
let n;
|
||||||
|
while ((n = fs.readSync(src, buf, 0, buf.length, null)) > 0) fs.writeSync(fd, buf, 0, n);
|
||||||
|
} finally { fs.closeSync(src); }
|
||||||
|
fs.writeSync(fd, footer);
|
||||||
|
} finally { fs.closeSync(fd); }
|
||||||
|
|
||||||
|
schritt('Prüfsumme wird geschrieben …');
|
||||||
|
const sum = sha256(OUT);
|
||||||
|
fs.writeFileSync(`${OUT}.sha256`, `${sum} ${outName}\n`);
|
||||||
|
|
||||||
|
schritt('Aufräumen …');
|
||||||
|
for (const t of [WORK, APP_DIR, path.join(DIST, 'builder-debug.yml'), path.join(DIST, 'builder-effective-config.yaml')]) {
|
||||||
|
fs.rmSync(t, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const mb = (fs.statSync(OUT).size / 1024 / 1024).toFixed(1);
|
||||||
|
console.log(`\nFertig: ${OUT} (${mb} MB)\nSHA-256: ${sum}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
main();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`\nFehler: ${err.message}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user