59 Commits
2.0.5 ... 2.0.6

Author SHA1 Message Date
b613e3bd73 assets/Thumbs.db gelöscht 2026-03-24 21:17:48 +00:00
62e4f9b85f Upload updater.js via GUI 2026-03-24 21:08:26 +00:00
7b68e92a95 Upload preload.js via GUI 2026-03-24 21:08:25 +00:00
83782d547f Upload package-lock.json via GUI 2026-03-24 21:08:25 +00:00
f1acea14fa Upload package.json via GUI 2026-03-24 21:08:24 +00:00
5513bdcef4 Upload main.js via GUI 2026-03-24 21:08:24 +00:00
f1f896ba65 Upload updater.js via GUI 2026-03-24 21:06:34 +00:00
ca55bbdcae Upload preload.js via GUI 2026-03-24 21:06:34 +00:00
e97e714826 Upload package-lock.json via GUI 2026-03-24 21:06:33 +00:00
cb185d7714 Upload package.json via GUI 2026-03-24 21:06:33 +00:00
4215429114 Upload main.js via GUI 2026-03-24 21:06:32 +00:00
1195dd4c0e Update from Git Manager GUI 2026-03-24 22:02:56 +01:00
a6585a856b Upload updater.js via GUI 2026-03-24 21:02:54 +00:00
5988d98d9a Upload preload.js via GUI 2026-03-24 21:02:53 +00:00
89dcd9f311 Upload package-lock.json via GUI 2026-03-24 21:02:53 +00:00
0366253134 Upload package.json via GUI 2026-03-24 21:02:52 +00:00
853a93e142 Upload main.js via GUI 2026-03-24 21:02:52 +00:00
24dd12c360 Update from Git Manager GUI 2026-03-24 21:59:25 +01:00
f52b99f192 Update from Git Manager GUI 2026-03-24 21:59:23 +01:00
6ba774cb26 Upload updater.js via GUI 2026-03-24 20:59:21 +00:00
f5542a6114 Upload preload.js via GUI 2026-03-24 20:59:19 +00:00
3c0c1e78dc Upload package-lock.json via GUI 2026-03-24 20:59:17 +00:00
f041b3bc32 Upload package.json via GUI 2026-03-24 20:59:15 +00:00
3ecb3125d3 Upload main.js via GUI 2026-03-24 20:59:13 +00:00
272bd00c80 Upload updater.js via GUI 2026-03-24 20:46:42 +00:00
0e9c14b144 Upload preload.js via GUI 2026-03-24 20:46:41 +00:00
a9c0c97287 Upload package-lock.json via GUI 2026-03-24 20:46:41 +00:00
cdd7dc430f Upload package.json via GUI 2026-03-24 20:46:40 +00:00
433c70d389 Upload main.js via GUI 2026-03-24 20:46:40 +00:00
d61c2b39e3 Upload package.json via GUI 2026-03-24 20:44:59 +00:00
96c3a063cc Upload main.js via GUI 2026-03-24 20:44:59 +00:00
9dd3696a09 Update from Git Manager GUI 2026-03-24 21:44:55 +01:00
4bef46201b Upload updater.js via GUI 2026-03-24 20:44:53 +00:00
fd80283328 Upload preload.js via GUI 2026-03-24 20:44:53 +00:00
2dba14ca58 Upload package-lock.json via GUI 2026-03-24 20:44:52 +00:00
1391f092d8 Upload package.json via GUI 2026-03-24 20:38:27 +00:00
88e2535e62 Upload main.js via GUI 2026-03-24 20:38:27 +00:00
de581d878f Update from Git Manager GUI 2026-03-24 21:38:24 +01:00
a1f7c66f67 Update from Git Manager GUI 2026-03-24 21:38:23 +01:00
12dbdbb28e Upload updater.js via GUI 2026-03-24 20:38:20 +00:00
687a80924a Upload preload.js via GUI 2026-03-24 20:38:19 +00:00
b22ea34032 Upload package-lock.json via GUI 2026-03-24 20:38:19 +00:00
c7fb8ce0bf Upload updater.js via GUI 2026-03-24 18:18:33 +00:00
b5f744db12 Upload README.md via GUI 2026-03-24 18:18:33 +00:00
553333c623 Upload preload.js via GUI 2026-03-24 18:18:32 +00:00
13ee1b0339 Upload package-lock.json via GUI 2026-03-24 18:18:32 +00:00
3755796ef6 Upload package.json via GUI 2026-03-24 18:18:31 +00:00
e3dbc6c663 Upload main.js via GUI 2026-03-24 18:18:31 +00:00
8613720a0b Upload CREATE_COMPLETE_FILES.sh via GUI 2026-03-24 18:18:30 +00:00
0f758616cf Update from Git Manager GUI 2026-03-24 19:18:28 +01:00
b2ba2a09e2 Update from Git Manager GUI 2026-03-24 19:18:26 +01:00
2ef1e0df61 Upload preload.js via GUI 2026-03-24 15:34:45 +00:00
fe0e7b7794 Upload package-lock.json via GUI 2026-03-24 15:34:44 +00:00
c8e06bf576 Upload package.json via GUI 2026-03-24 15:34:44 +00:00
7b480acd10 Upload main.js via GUI 2026-03-24 15:34:44 +00:00
658b29368b Update from Git Manager GUI 2026-03-24 16:34:42 +01:00
c64d40fbda Update from Git Manager GUI 2026-03-24 16:34:40 +01:00
f6598cfb19 Update from Git Manager GUI 2026-03-24 16:34:38 +01:00
5787c4ca22 Upload updater.js via GUI 2026-03-24 15:34:36 +00:00
17 changed files with 7288 additions and 419 deletions

View File

@@ -56,6 +56,6 @@ Bei Problemen oder Fragen kannst du ein Issue auf GitHub erstellen.
---
**Copyright © 2026 - M_Viper - Alle Rechte vorbehalten**
## Lizenz
Die unbefugte Vervielfältigung, Verbreitung oder Weitergabe dieses Plugins ist strafbar und wird rechtlich verfolgt.
Dieses Projekt ist Open-Source unter der [GPL Lizenz](LICENSE).

Binary file not shown.

BIN
data/credentials.json Normal file

Binary file not shown.

1141
main.js

File diff suppressed because it is too large Load Diff

512
package-lock.json generated
View File

@@ -1,19 +1,21 @@
{
"name": "git-manager-gui",
"version": "2.0.4",
"version": "2.0.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "git-manager-gui",
"version": "2.0.4",
"version": "2.0.5",
"dependencies": {
"archiver": "^6.0.0",
"axios": "^1.13.4",
"form-data": "^4.0.5",
"simple-git": "^3.19.1"
"simple-git": "^3.19.1",
"unzipper": "^0.11.0"
},
"devDependencies": {
"electron": "^26.2.0",
"electron": "^26.6.10",
"electron-builder": "^26.8.1"
}
},
@@ -1165,6 +1167,82 @@
"node": "^18.17.0 || >=20.5.0"
}
},
"node_modules/archiver": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/archiver/-/archiver-6.0.2.tgz",
"integrity": "sha512-UQ/2nW7NMl1G+1UnrLypQw1VdT9XZg/ECcKPq7l+STzStrSivFIXIp34D8M5zeNGW5NoOupdYCHv6VySCPNNlw==",
"license": "MIT",
"dependencies": {
"archiver-utils": "^4.0.1",
"async": "^3.2.4",
"buffer-crc32": "^0.2.1",
"readable-stream": "^3.6.0",
"readdir-glob": "^1.1.2",
"tar-stream": "^3.0.0",
"zip-stream": "^5.0.1"
},
"engines": {
"node": ">= 12.0.0"
}
},
"node_modules/archiver-utils": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-4.0.1.tgz",
"integrity": "sha512-Q4Q99idbvzmgCTEAAhi32BkOyq8iVI5EwdO0PmBDSGIzzjYNdcFn7Q7k3OzbLy4kLUPXfJtG6fO2RjftXbobBg==",
"license": "MIT",
"dependencies": {
"glob": "^8.0.0",
"graceful-fs": "^4.2.0",
"lazystream": "^1.0.0",
"lodash": "^4.17.15",
"normalize-path": "^3.0.0",
"readable-stream": "^3.6.0"
},
"engines": {
"node": ">= 12.0.0"
}
},
"node_modules/archiver-utils/node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/archiver-utils/node_modules/glob": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
"integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==",
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^5.0.1",
"once": "^1.3.0"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/archiver-utils/node_modules/minimatch": {
"version": "5.1.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz",
"integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=10"
}
},
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -1198,7 +1276,6 @@
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
"dev": true,
"license": "MIT"
},
"node_modules/async-exit-hook": {
@@ -1238,13 +1315,113 @@
"proxy-from-env": "^1.1.0"
}
},
"node_modules/b4a": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz",
"integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==",
"license": "Apache-2.0",
"peerDependencies": {
"react-native-b4a": "*"
},
"peerDependenciesMeta": {
"react-native-b4a": {
"optional": true
}
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/bare-events": {
"version": "2.8.2",
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
"integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
"license": "Apache-2.0",
"peerDependencies": {
"bare-abort-controller": "*"
},
"peerDependenciesMeta": {
"bare-abort-controller": {
"optional": true
}
}
},
"node_modules/bare-fs": {
"version": "4.5.6",
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.6.tgz",
"integrity": "sha512-1QovqDrR80Pmt5HPAsMsXTCFcDYr+NSUKW6nd6WO5v0JBmnItc/irNRzm2KOQ5oZ69P37y+AMujNyNtG+1Rggw==",
"license": "Apache-2.0",
"dependencies": {
"bare-events": "^2.5.4",
"bare-path": "^3.0.0",
"bare-stream": "^2.6.4",
"bare-url": "^2.2.2",
"fast-fifo": "^1.3.2"
},
"engines": {
"bare": ">=1.16.0"
},
"peerDependencies": {
"bare-buffer": "*"
},
"peerDependenciesMeta": {
"bare-buffer": {
"optional": true
}
}
},
"node_modules/bare-os": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.8.0.tgz",
"integrity": "sha512-Dc9/SlwfxkXIGYhvMQNUtKaXCaGkZYGcd1vuNUUADVqzu4/vQfvnMkYYOUnt2VwQ2AqKr/8qAVFRtwETljgeFg==",
"license": "Apache-2.0",
"engines": {
"bare": ">=1.14.0"
}
},
"node_modules/bare-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
"integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
"license": "Apache-2.0",
"dependencies": {
"bare-os": "^3.0.1"
}
},
"node_modules/bare-stream": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.10.0.tgz",
"integrity": "sha512-DOPZF/DDcDruKDA43cOw6e9Quq5daua7ygcAwJE/pKJsRWhgSSemi7qVNGE5kyDIxIeN1533G/zfbvWX7Wcb9w==",
"license": "Apache-2.0",
"dependencies": {
"streamx": "^2.25.0",
"teex": "^1.0.1"
},
"peerDependencies": {
"bare-buffer": "*",
"bare-events": "*"
},
"peerDependenciesMeta": {
"bare-buffer": {
"optional": true
},
"bare-events": {
"optional": true
}
}
},
"node_modules/bare-url": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.0.tgz",
"integrity": "sha512-NSTU5WN+fy/L0DDenfE8SXQna4voXuW0FHM7wH8i3/q9khUSchfPbPezO4zSFMnDGIf9YE+mt/RWhZgNRKRIXA==",
"license": "Apache-2.0",
"dependencies": {
"bare-path": "^3.0.0"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -1266,6 +1443,15 @@
],
"license": "MIT"
},
"node_modules/big-integer": {
"version": "1.6.52",
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz",
"integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==",
"license": "Unlicense",
"engines": {
"node": ">=0.6"
}
},
"node_modules/bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
@@ -1278,6 +1464,12 @@
"readable-stream": "^3.4.0"
}
},
"node_modules/bluebird": {
"version": "3.4.7",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz",
"integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==",
"license": "MIT"
},
"node_modules/boolean": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz",
@@ -1291,7 +1483,6 @@
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
@@ -1327,7 +1518,6 @@
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "*"
@@ -1706,20 +1896,32 @@
"node": ">=0.10.0"
}
},
"node_modules/compress-commons": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-5.0.3.tgz",
"integrity": "sha512-/UIcLWvwAQyVibgpQDPtfNM3SvqN7G9elAPAV7GM0L53EbNWwWiCsWtK8Fwed/APEbptPHXs5PuW+y8Bq8lFTA==",
"license": "MIT",
"dependencies": {
"crc-32": "^1.2.0",
"crc32-stream": "^5.0.0",
"normalize-path": "^3.0.0",
"readable-stream": "^3.6.0"
},
"engines": {
"node": ">= 12.0.0"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true,
"license": "MIT"
},
"node_modules/core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==",
"dev": true,
"license": "MIT",
"optional": true
"license": "MIT"
},
"node_modules/crc": {
"version": "3.8.0",
@@ -1732,6 +1934,31 @@
"buffer": "^5.1.0"
}
},
"node_modules/crc-32": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
"license": "Apache-2.0",
"bin": {
"crc32": "bin/crc32.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/crc32-stream": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-5.0.1.tgz",
"integrity": "sha512-lO1dFui+CEUh/ztYIpgpKItKW9Bb4NWakCRJrnqAbFIYD+OZAwb2VfD5T5eXMw2FNcsDHkQcNl/Wh3iVXYwU6g==",
"license": "MIT",
"dependencies": {
"crc-32": "^1.2.0",
"readable-stream": "^3.4.0"
},
"engines": {
"node": ">= 12.0.0"
}
},
"node_modules/cross-dirname": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz",
@@ -2039,6 +2266,45 @@
"node": ">= 0.4"
}
},
"node_modules/duplexer2": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
"integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==",
"license": "BSD-3-Clause",
"dependencies": {
"readable-stream": "^2.0.2"
}
},
"node_modules/duplexer2/node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/duplexer2/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/duplexer2/node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@@ -2389,6 +2655,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/events-universal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
"integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
"license": "Apache-2.0",
"dependencies": {
"bare-events": "^2.7.0"
}
},
"node_modules/exponential-backoff": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz",
@@ -2435,6 +2710,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/fast-fifo": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
"integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
"license": "MIT"
},
"node_modules/fast-json-stable-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
@@ -2583,9 +2864,24 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"dev": true,
"license": "ISC"
},
"node_modules/fstream": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz",
"integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==",
"deprecated": "This package is no longer supported.",
"license": "ISC",
"dependencies": {
"graceful-fs": "^4.1.2",
"inherits": "~2.0.0",
"mkdirp": ">=0.5 0",
"rimraf": "2"
},
"engines": {
"node": ">=0.6"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -2663,7 +2959,6 @@
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"dev": true,
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
@@ -2684,7 +2979,6 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
@@ -2786,7 +3080,6 @@
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true,
"license": "ISC"
},
"node_modules/has-flag": {
@@ -2981,7 +3274,6 @@
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
"dev": true,
"license": "ISC",
"dependencies": {
"once": "^1.3.0",
@@ -2992,7 +3284,6 @@
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true,
"license": "ISC"
},
"node_modules/ip-address": {
@@ -3038,6 +3329,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"license": "MIT"
},
"node_modules/isbinaryfile": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz",
@@ -3177,11 +3474,52 @@
"dev": true,
"license": "MIT"
},
"node_modules/lazystream": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz",
"integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==",
"license": "MIT",
"dependencies": {
"readable-stream": "^2.0.5"
},
"engines": {
"node": ">= 0.6.3"
}
},
"node_modules/lazystream/node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/lazystream/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/lazystream/node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/lodash": {
"version": "4.17.23",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
"dev": true,
"license": "MIT"
},
"node_modules/log-symbols": {
@@ -3367,7 +3705,6 @@
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -3509,9 +3846,7 @@
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"minimist": "^1.2.6"
},
@@ -3646,6 +3981,15 @@
"node": "^18.17.0 || >=20.5.0"
}
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/normalize-url": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
@@ -3674,7 +4018,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dev": true,
"license": "ISC",
"dependencies": {
"wrappy": "1"
@@ -3770,7 +4113,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -3894,6 +4236,12 @@
"node": "^18.17.0 || >=20.5.0"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
},
"node_modules/progress": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
@@ -3987,7 +4335,6 @@
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"dev": true,
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
@@ -3998,6 +4345,36 @@
"node": ">= 6"
}
},
"node_modules/readdir-glob": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz",
"integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==",
"license": "Apache-2.0",
"dependencies": {
"minimatch": "^5.1.0"
}
},
"node_modules/readdir-glob/node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/readdir-glob/node_modules/minimatch": {
"version": "5.1.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz",
"integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=10"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -4070,6 +4447,19 @@
"node": ">= 4"
}
},
"node_modules/rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"deprecated": "Rimraf versions prior to v4 are no longer supported",
"license": "ISC",
"dependencies": {
"glob": "^7.1.3"
},
"bin": {
"rimraf": "bin.js"
}
},
"node_modules/roarr": {
"version": "2.15.4",
"resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz",
@@ -4093,7 +4483,6 @@
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"dev": true,
"funding": [
{
"type": "github",
@@ -4352,11 +4741,21 @@
"node": ">= 6"
}
},
"node_modules/streamx": {
"version": "2.25.0",
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz",
"integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==",
"license": "MIT",
"dependencies": {
"events-universal": "^1.0.0",
"fast-fifo": "^1.3.2",
"text-decoder": "^1.1.0"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"dev": true,
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
@@ -4463,6 +4862,18 @@
"node": ">=18"
}
},
"node_modules/tar-stream": {
"version": "3.1.8",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz",
"integrity": "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==",
"license": "MIT",
"dependencies": {
"b4a": "^1.6.4",
"bare-fs": "^4.5.5",
"fast-fifo": "^1.2.0",
"streamx": "^2.15.0"
}
},
"node_modules/tar/node_modules/yallist": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
@@ -4473,6 +4884,15 @@
"node": ">=18"
}
},
"node_modules/teex": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz",
"integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==",
"license": "MIT",
"dependencies": {
"streamx": "^2.12.5"
}
},
"node_modules/temp": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz",
@@ -4552,6 +4972,15 @@
"rimraf": "bin.js"
}
},
"node_modules/text-decoder": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz",
"integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==",
"license": "Apache-2.0",
"dependencies": {
"b4a": "^1.6.4"
}
},
"node_modules/tiny-async-pool": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz",
@@ -4707,6 +5136,19 @@
"node": ">= 4.0.0"
}
},
"node_modules/unzipper": {
"version": "0.11.6",
"resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.11.6.tgz",
"integrity": "sha512-anERl79akvqLbAxfjIFe4hK0wsi0fH4uGLwNEl4QEnG+KKs3QQeApYgOS/f6vH2EdACUlZg35psmd/3xL2duFQ==",
"license": "MIT",
"dependencies": {
"big-integer": "^1.6.17",
"bluebird": "~3.4.1",
"duplexer2": "~0.1.4",
"fstream": "^1.0.12",
"graceful-fs": "^4.2.2"
}
},
"node_modules/uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
@@ -4728,7 +5170,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true,
"license": "MIT"
},
"node_modules/verror": {
@@ -4814,7 +5255,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true,
"license": "ISC"
},
"node_modules/xmlbuilder": {
@@ -4896,6 +5336,20 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zip-stream": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-5.0.2.tgz",
"integrity": "sha512-LfOdrUvPB8ZoXtvOBz6DlNClfvi//b5d56mSWyJi7XbH/HfhOHfUhOqxhT/rUiR7yiktlunqRo+jY6y/cWC/5g==",
"license": "MIT",
"dependencies": {
"archiver-utils": "^4.0.1",
"compress-commons": "^5.0.1",
"readable-stream": "^3.6.0"
},
"engines": {
"node": ">= 12.0.0"
}
}
}
}

View File

@@ -1,27 +1,45 @@
{
"name": "git-manager-gui",
"version": "2.0.4",
"version": "2.0.5",
"description": "Git Manager GUI - Verwaltung von Git Repositories",
"author": "M_Viper",
"main": "main.js",
"scripts": {
"start": "electron .",
"build": "electron-builder"
"build": "electron-builder",
"test": "node --test __tests__/*.test.js"
},
"dependencies": {
"axios": "^1.13.4",
"form-data": "^4.0.5",
"simple-git": "^3.19.1"
"simple-git": "^3.19.1",
"archiver": "^6.0.0",
"unzipper": "^0.11.0"
},
"devDependencies": {
"electron": "^26.2.0",
"electron": "^26.6.10",
"electron-builder": "^26.8.1"
},
"build": {
"asar": true,
"appId": "com.viper.gitmanager",
"productName": "Git Manager GUI",
"files": [
"**/*",
"!node_modules/.cache",
"!**/*.map"
"!**/*.map",
"!**/.git",
"!**/.git/**",
"!repos/**",
"!data/**",
"!backup/**",
"!*.zip",
"!*.rar",
"!*.log"
],
"extraResources": [
"repos/",
"data/"
],
"directories": {
"buildResources": "assets"

View File

@@ -20,6 +20,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
// Gitea Datei-Operationen
listGiteaRepos: (data) => ipcRenderer.invoke('list-gitea-repos', data),
getGiteaUserHeatmap: (data) => ipcRenderer.invoke('get-gitea-user-heatmap', data),
getGiteaRepoContents: (data) => ipcRenderer.invoke('get-gitea-repo-contents', data),
getGiteaFileContent: (data) => ipcRenderer.invoke('get-gitea-file-content', data),
readGiteaFile: (data) => ipcRenderer.invoke('read-gitea-file', data),
@@ -32,12 +33,21 @@ contextBridge.exposeInMainWorld('electronAPI', {
// Repository & Git Management
saveCredentials: (data) => ipcRenderer.invoke('save-credentials', data),
loadCredentials: () => ipcRenderer.invoke('load-credentials'),
testGiteaConnection: (data) => ipcRenderer.invoke('test-gitea-connection', data),
createRepo: (data) => ipcRenderer.invoke('create-repo', data),
pushProject: (data) => ipcRenderer.invoke('push-project', data),
getBranches: (data) => ipcRenderer.invoke('getBranches', data),
getCommitLogs: (data) => ipcRenderer.invoke('getCommitLogs', data),
uploadAndPush: (data) => ipcRenderer.invoke('upload-and-push', data),
deleteGiteaRepo: (data) => ipcRenderer.invoke('delete-gitea-repo', data),
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),
@@ -100,6 +110,12 @@ contextBridge.exposeInMainWorld('electronAPI', {
return () => ipcRenderer.removeListener('push-progress', listener);
},
onPrePushBackupStatus: (cb) => {
const listener = (event, payload) => { try { cb(payload); } catch (_) {} };
ipcRenderer.on('pre-push-backup-status', listener);
return () => ipcRenderer.removeListener('pre-push-backup-status', listener);
},
onFolderUploadProgress: (cb) => {
const listener = (event, payload) => { try { cb(payload); } catch (_) {} };
ipcRenderer.on('folder-upload-progress', listener);
@@ -110,5 +126,46 @@ contextBridge.exposeInMainWorld('electronAPI', {
const listener = (event, payload) => { try { cb(payload); } catch (_) {} };
ipcRenderer.on('folder-download-progress', listener);
return () => ipcRenderer.removeListener('folder-download-progress', listener);
}
},
onRetryQueueUpdated: (cb) => {
const listener = (event, payload) => { try { cb(payload); } catch (_) {} };
ipcRenderer.on('retry-queue-updated', listener);
return () => ipcRenderer.removeListener('retry-queue-updated', listener);
},
onBatchActionProgress: (cb) => {
const listener = (event, payload) => { try { cb(payload); } catch (_) {} };
ipcRenderer.on('batch-action-progress', listener);
return () => ipcRenderer.removeListener('batch-action-progress', listener);
},
// Backup Management
exportGiteaProjectsToLocal: (data) => ipcRenderer.invoke('export-gitea-projects-to-local', data),
setupBackupProvider: (data) => ipcRenderer.invoke('setup-backup-provider', data),
testBackupProvider: (data) => ipcRenderer.invoke('test-backup-provider', data),
getBackupAuthStatus: (data) => ipcRenderer.invoke('get-backup-auth-status', data),
createCloudBackup: (data) => ipcRenderer.invoke('create-cloud-backup', data),
listCloudBackups: (data) => ipcRenderer.invoke('list-cloud-backups', data),
restoreCloudBackup: (data) => ipcRenderer.invoke('restore-cloud-backup', data),
deleteCloudBackup: (data) => ipcRenderer.invoke('delete-cloud-backup', data),
onBackupCreated: (cb) => {
const listener = (event, payload) => { try { cb(payload); } catch (_) {} };
ipcRenderer.on('backup-created', listener);
return () => ipcRenderer.removeListener('backup-created', 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)
});

View File

@@ -4,39 +4,42 @@ import Settings from './Settings.jsx';
export default function App() {
const [folder, setFolder] = useState('');
const [repoName, setRepoName] = useState('');
const [platform, setPlatform] = useState('github');
const [status, setStatus] = useState('');
const [platform, setPlatform] = useState('gitea');
const [status, setStatus] = useState('Bereit');
const [showSettings, setShowSettings] = useState(false);
const [branches, setBranches] = useState([]);
const [selectedBranch, setSelectedBranch] = useState('master');
const [selectedBranch, setSelectedBranch] = useState('main');
const [logs, setLogs] = useState([]);
const [progress, setProgress] = useState(0);
async function selectFolder() {
const selected = await window.electronAPI.selectFolder();
if (selected) setFolder(selected);
// Branches laden
if (selected) {
const branchList = await window.electronAPI.getBranches({ folder: selected });
setBranches(branchList);
if (branchList.includes('master')) setSelectedBranch('master');
}
if (!selected) return;
setFolder(selected);
const branchList = await window.electronAPI.getBranches({ folder: selected });
setBranches(branchList);
if (branchList.includes('main')) setSelectedBranch('main');
else if (branchList.includes('master')) setSelectedBranch('master');
else if (branchList.length > 0) setSelectedBranch(branchList[0]);
}
async function createRepoHandler() {
if (!repoName) return alert('Repo Name required!');
setStatus('Creating repository...');
if (!repoName) return alert('Repo-Name erforderlich!');
setStatus('Repository wird erstellt…');
const result = await window.electronAPI.createRepo({ name: repoName, platform });
setStatus(result ? 'Repository created!' : 'Failed to create repository.');
setStatus(result ? 'Repository erstellt!' : 'Fehler beim Erstellen des Repositories.');
}
async function pushProjectHandler() {
if (!folder) return alert('Select a project folder first!');
setStatus('Pushing project...');
if (!folder) return alert('Bitte zuerst einen Projektordner auswählen!');
setStatus('Projekt wird gepusht…');
setProgress(0);
const onProgress = (p) => setProgress(p); // Callback für Fortschritt
const result = await window.electronAPI.pushProject({ folder, branch: selectedBranch, onProgress });
setStatus(result ? 'Project pushed!' : 'Failed to push project.');
const result = await window.electronAPI.pushProject({
folder,
branch: selectedBranch,
onProgress: (p) => setProgress(p),
});
setStatus(result ? 'Projekt gepusht!' : 'Push fehlgeschlagen.');
if (result) {
const logList = await window.electronAPI.getCommitLogs({ folder });
setLogs(logList);
@@ -44,61 +47,168 @@ export default function App() {
}
return (
<div style={{ padding: 20 }}>
<h1>Git Manager GUI - High-End</h1>
<button onClick={() => setShowSettings(!showSettings)}>Settings</button>
{showSettings && <Settings />}
<div id="app" style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
<div style={{ marginTop: 20 }}>
<label>Platform:</label>
<select value={platform} onChange={e => setPlatform(e.target.value)}>
<option value="github">GitHub</option>
<option value="gitea">Gitea</option>
</select>
{/* ── Toolbar ── */}
<div id="toolbar">
<div className="toolbar-row toolbar-row--top">
<div className="toolbar-brand">
<div className="toolbar-brand-mark">
<img src="./icon.png" alt="Git Manager Logo" className="toolbar-brand-logo" />
</div>
<div className="toolbar-brand-copy">
<span className="toolbar-kicker">Workspace Control</span>
<strong>Git Manager Explorer Pro</strong>
</div>
</div>
<div className="toolbar-top-actions">
<div className="tool-group tool-group--quick-actions">
<button onClick={createRepoHandler} title="Neues Repository erstellen">🚀 New Repo</button>
<button onClick={pushProjectHandler} title="Projekt pushen"> Push</button>
</div>
<div className="tool-group tool-group--utility">
<button onClick={() => setShowSettings(true)} title="Einstellungen"> Settings</button>
</div>
<div className="toolbar-status-wrap">
<span className="status-dot" aria-hidden="true" />
<span className="status">{status}</span>
</div>
</div>
</div>
<div className="toolbar-row toolbar-row--bottom">
<div className="tool-group tool-group--workspace">
<button className="accent-btn" onClick={selectFolder} title="Lokalen Ordner öffnen">
📂 Open Local
</button>
</div>
<div className="tool-group tool-group--repo">
<div className="platform-switch" role="tablist" aria-label="Plattform auswählen">
{['gitea', 'github'].map(p => (
<button
key={p}
type="button"
className={`platform-option${platform === p ? ' active' : ''}`}
data-platform={p}
aria-pressed={platform === p}
onClick={() => setPlatform(p)}
>
{p === 'gitea' ? 'Gitea' : 'GitHub'}
</button>
))}
</div>
{branches.length > 0 && (
<select
value={selectedBranch}
onChange={e => setSelectedBranch(e.target.value)}
title="Branch auswählen"
>
{branches.map(b => <option key={b} value={b}>{b}</option>)}
</select>
)}
</div>
</div>
</div>
<div style={{ marginTop: 20 }}>
<button onClick={selectFolder}>Select Project Folder</button>
<span style={{ marginLeft: 10 }}>{folder}</span>
</div>
{/* ── Main ── */}
<main id="main">
<div style={{ marginTop: 20 }}>
<label>Branch:</label>
<select value={selectedBranch} onChange={e => setSelectedBranch(e.target.value)}>
{branches.map(b => <option key={b} value={b}>{b}</option>)}
</select>
</div>
{/* Fortschrittsbalken */}
{progress > 0 && progress < 100 && (
<div className="input-group" style={{ marginBottom: 20 }}>
<label>Fortschritt</label>
<progress value={progress} max="100" style={{ width: '100%', height: 8, borderRadius: 4 }} />
</div>
)}
<div style={{ marginTop: 20 }}>
<input
type="text"
placeholder="Repository Name"
value={repoName}
onChange={e => setRepoName(e.target.value)} />
<button onClick={createRepoHandler} style={{ marginLeft: 10 }}>Create Repo</button>
</div>
{/* Repo erstellen / Ordner */}
<div className="card" style={{ marginBottom: 20 }}>
<h2>📁 Projekt & Repository</h2>
<div style={{ marginTop: 20 }}>
<button onClick={pushProjectHandler}>Push / Update Project</button>
</div>
<div className="input-group">
<label>Lokaler Projektordner</label>
<div style={{ display: 'flex', gap: 10, alignItems: 'center' }}>
<input
type="text"
readOnly
value={folder}
placeholder="Noch kein Ordner ausgewählt…"
style={{ flex: 1 }}
/>
<button onClick={selectFolder} style={{ flex: '0 0 auto' }}>📂 Auswählen</button>
</div>
</div>
<div style={{ marginTop: 10 }}>
<label>Progress:</label>
<progress value={progress} max="100" style={{ width: '100%' }} />
</div>
<div className="input-group">
<label>Repository Name</label>
<input
type="text"
placeholder="mein-projekt"
value={repoName}
onChange={e => setRepoName(e.target.value)}
/>
</div>
<div style={{ marginTop: 20 }}>
<strong>Status: </strong>{status}
</div>
<div style={{ display: 'flex', gap: 10, marginTop: 8 }}>
<button className="accent-btn" onClick={createRepoHandler}>🚀 Repo erstellen</button>
<button onClick={pushProjectHandler}> Push / Aktualisieren</button>
</div>
</div>
<div style={{ marginTop: 20 }}>
<h3>Commit Logs:</h3>
<ul>
{logs.map((log, i) => (
<li key={i}>{log}</li>
))}
</ul>
</div>
{/* Commit-Logs */}
{logs.length > 0 && (
<div className="card">
<h2>📊 Commit-Verlauf</h2>
<ul style={{ listStyle: 'none', padding: 0, margin: 0, display: 'flex', flexDirection: 'column', gap: 6 }}>
{logs.map((log, i) => (
<li
key={i}
style={{
padding: '8px 12px',
borderRadius: 8,
background: 'rgba(255,255,255,0.04)',
border: '1px solid rgba(140,173,255,0.08)',
fontSize: 13,
color: 'var(--text-secondary)',
fontFamily: 'monospace',
}}
>
{log}
</li>
))}
</ul>
</div>
)}
{logs.length === 0 && !folder && (
<div style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
paddingTop: 60,
gap: 16,
opacity: 0.55,
}}>
<div style={{ fontSize: 64, filter: 'drop-shadow(0 8px 16px rgba(88,213,255,0.2))' }}>📂</div>
<p style={{ color: 'var(--text-muted)', fontSize: 15, textAlign: 'center' }}>
Öffne einen lokalen Ordner oder lade Repos über die Toolbar.
</p>
</div>
)}
</main>
{/* ── Settings-Modal ── */}
{showSettings && (
<div id="settingsModal" className="modal" onClick={e => { if (e.target === e.currentTarget) setShowSettings(false); }}>
<Settings onClose={() => setShowSettings(false)} />
</div>
)}
</div>
);
}

View File

@@ -1,9 +1,34 @@
import React, { useState, useEffect } from 'react';
export default function Settings() {
export default function Settings({ onClose }) {
const [githubToken, setGithubToken] = useState('');
const [giteaToken, setGiteaToken] = useState('');
const [giteaURL, setGiteaURL] = useState('');
const [savedOk, setSavedOk] = useState(false);
function normalizeAndValidateGiteaUrl(rawUrl) {
const value = (rawUrl || '').trim();
if (!value) return { ok: true, value: '' };
let parsed;
try {
parsed = new URL(value);
} catch (_) {
return {
ok: false,
error: 'Ungültige Gitea-URL. Beispiel für IPv6: http://[2001:db8::1]:3000',
};
}
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
return {
ok: false,
error: 'Die Gitea-URL muss mit http:// oder https:// beginnen.',
};
}
return { ok: true, value: value.replace(/\/$/, '') };
}
useEffect(() => {
window.electronAPI.loadCredentials().then(data => {
@@ -15,27 +40,95 @@ export default function Settings() {
});
}, []);
const save = () => {
window.electronAPI.saveCredentials({ githubToken, giteaToken, giteaURL });
alert('Settings saved securely!');
function save() {
const checkedUrl = normalizeAndValidateGiteaUrl(giteaURL);
if (!checkedUrl.ok) {
alert(checkedUrl.error);
return;
}
window.electronAPI.saveCredentials({ githubToken, giteaToken, giteaURL: checkedUrl.value });
setSavedOk(true);
setTimeout(() => setSavedOk(false), 2500);
}
return (
<div style={{ padding: 20 }}>
<h2>Settings</h2>
<div>
<label>GitHub Token:</label>
<input type="password" value={githubToken} onChange={e => setGithubToken(e.target.value)} />
<div className="modalContent card settings-modal-content">
<div className="settings-header">
<div>
<div className="settings-eyebrow">Konfiguration</div>
<h2> Einstellungen</h2>
<p className="settings-subtitle">
Zugangsdaten für GitHub und Gitea hinterlegen.
</p>
</div>
</div>
<div>
<label>Gitea Token:</label>
<input type="password" value={giteaToken} onChange={e => setGiteaToken(e.target.value)} />
<div className="settings-layout">
<div className="settings-column settings-column--left">
<section className="settings-panel settings-panel--credentials">
<div className="settings-panel-header">
<div>
<h3>Zugangsdaten</h3>
<p>API-Zugriffe für GitHub und Gitea konfigurieren.</p>
</div>
</div>
<div className="settings-fields-grid">
<div className="input-group">
<label htmlFor="react-githubToken">GitHub Token</label>
<input
id="react-githubToken"
type="password"
placeholder="ghp_…"
value={githubToken}
onChange={e => setGithubToken(e.target.value)}
/>
</div>
<div className="input-group">
<label htmlFor="react-giteaToken">Gitea Token</label>
<input
id="react-giteaToken"
type="password"
placeholder="Token hier einfügen"
value={giteaToken}
onChange={e => setGiteaToken(e.target.value)}
/>
</div>
</div>
<div className="input-group input-group--wide">
<label htmlFor="react-giteaURL">Gitea URL</label>
<input
id="react-giteaURL"
type="text"
placeholder="https://gitea.example.com"
value={giteaURL}
onChange={e => setGiteaURL(e.target.value)}
/>
<div className="settings-connection-tools">
<div className="settings-inline-hint">
Hinweis: IPv6 mit Klammern eingeben, z.B. http://[2001:db8::1]:3000
</div>
</div>
</div>
</section>
</div>
</div>
<div>
<label>Gitea URL:</label>
<input type="text" value={giteaURL} onChange={e => setGiteaURL(e.target.value)} />
<div className="modal-buttons settings-modal-actions">
<button
className="accent-btn"
onClick={save}
style={savedOk ? { background: 'var(--success)', borderColor: 'var(--success)' } : {}}
>
{savedOk ? '✅ Gespeichert' : 'Speichern'}
</button>
{onClose && (
<button className="secondary" onClick={onClose}>Abbrechen</button>
)}
</div>
<button onClick={save} style={{ marginTop: 10 }}>Save</button>
</div>
);
}

BIN
renderer/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

View File

@@ -4,97 +4,299 @@
<meta charset="utf-8" />
<title>Git Manager Explorer Pro</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/png" href="./icon.png">
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="app">
<div id="toolbar">
<div class="tool-group">
<button id="btnSettings" title="Einstellungen">⚙️ Settings</button>
<button id="btnBack" class="secondary hidden" title="Zurück">⬅️ Zurück</button>
<button id="btnSelectFolder" class="accent-btn" title="Lokalen Ordner öffnen">📂 Open Local</button>
<button id="btnLoadGiteaRepos" class="accent-btn" title="Gitea Repositories laden">🌐 Load Gitea</button>
<!-- Titelbalken-Streifen -->
<div id="titlebar-strip">
<div class="titlebar-strip-brand">
<img src="./icon.png" alt="" class="titlebar-strip-icon">
<span class="titlebar-strip-title">Git Manager Explorer Pro</span>
</div>
<div class="tool-group">
<select id="platform" title="Plattform auswählen">
<option value="gitea" selected>Gitea</option>
<option value="github">GitHub</option>
</select>
<button id="btnOpenRepoActions" title="Neues Repository erstellen">🚀 New Repo</button>
<button id="btnPush" title="Projekt pushen">⬆️ Push</button>
<button id="btnCommits" class="hidden" title="Commit History anzeigen">📊 Commits</button>
<button id="btnReleases" class="hidden" title="Releases anzeigen">📦 Releases</button>
<div class="win-controls" aria-label="Fenster-Steuerung">
<button class="win-btn win-btn--minimize" id="btnWinMinimize" title="Minimieren">
<svg width="10" height="1" viewBox="0 0 10 1"><rect width="10" height="1" fill="currentColor"/></svg>
</button>
<button class="win-btn win-btn--maximize" id="btnWinMaximize" title="Maximieren">
<svg width="10" height="10" viewBox="0 0 10 10"><rect x="0.5" y="0.5" width="9" height="9" rx="1" fill="none" stroke="currentColor"/></svg>
</button>
<button class="win-btn win-btn--close" id="btnWinClose" title="Schließen">
<svg width="10" height="10" viewBox="0 0 10 10"><line x1="0" y1="0" x2="10" y2="10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><line x1="10" y1="0" x2="0" y2="10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
</button>
</div>
<span id="status" class="status">Bereit</span>
</div>
<main id="main">
<div id="explorerGrid" class="explorer-grid">
<div id="toolbar">
<div class="toolbar-row toolbar-row--top">
<div class="toolbar-brand" aria-label="App Kopfbereich">
<div class="toolbar-brand-mark">
<img src="./icon.png" alt="Git Manager Logo" class="toolbar-brand-logo">
</div>
<div class="toolbar-brand-copy">
<span class="toolbar-kicker">Workspace Control</span>
<strong>Git Manager Explorer Pro</strong>
</div>
</div>
</main>
<div class="toolbar-top-actions">
<div class="tool-group tool-group--quick-actions">
<button id="btnOpenRepoActions" title="Neues Repository erstellen">🚀 New Repo</button>
<button id="btnPush" title="Projekt pushen">⬆️ Push</button>
</div>
<div class="tool-group tool-group--utility">
<span class="tool-group-title">Steuerung</span>
<button id="btnSettings" title="Einstellungen">⚙️ Settings</button>
<button id="btnBatchActions" title="Batch-Aktionen">🧩 Batch</button>
<button id="btnOpenActivityLog" title="Aktivitätsprotokoll">📝 Activity</button>
<button id="btnRetryQueueNow" class="secondary" title="Retry-Queue jetzt verarbeiten">🔁 Queue (0)</button>
<button id="btnBack" class="secondary hidden" title="Zurück">⬅️ Zurück</button>
</div>
<div class="toolbar-status-wrap">
<span class="status-dot" aria-hidden="true"></span>
<span id="status" class="status">Bereit</span>
</div>
</div>
</div>
<div class="toolbar-row toolbar-row--bottom">
<div class="tool-group tool-group--workspace">
<span class="tool-group-title">Quelle</span>
<button id="btnSelectFolder" class="accent-btn" title="Lokalen Ordner öffnen">📂 Open Local</button>
<button id="btnLoadGiteaRepos" class="accent-btn" title="Gitea Repositories laden">🌐 Load Gitea</button>
</div>
<div class="tool-group tool-group--repo">
<span class="tool-group-title">Repository</span>
<input id="platform" type="hidden" value="gitea">
<div class="platform-switch" role="tablist" aria-label="Plattform auswählen">
<button type="button" class="platform-option active" data-platform="gitea" aria-pressed="true">Gitea</button>
<button type="button" class="platform-option" data-platform="github" aria-pressed="false">GitHub</button>
</div>
<button id="btnCommits" class="hidden" title="Commit History anzeigen">📊 Commits</button>
<button id="btnReleases" class="hidden" title="Releases anzeigen">📦 Releases</button>
</div>
</div>
</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">
<h2>⚙️ Einstellungen</h2>
<div class="input-group">
<label>GitHub Token</label>
<input id="githubToken" type="password" placeholder="ghp_...">
<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="https://discord.com/invite/FdRs4BRd8D" 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="input-group">
<label>Gitea Token</label>
<input id="giteaToken" type="password" placeholder="Token hier einfügen">
</div>
<div class="input-group">
<label>Gitea URL</label>
<input id="giteaURL" type="text" placeholder="https://gitea.example.com">
</div>
<div class="input-group" style="margin-top: 20px; padding-top: 20px; border-top: 1px solid rgba(255,255,255,0.1);">
<label style="margin-bottom: 12px;">Übersicht</label>
<div style="display: flex; flex-direction: column; gap: 10px;">
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer; text-transform: none; letter-spacing: normal; font-weight: normal;">
<input type="checkbox" id="settingFavorites" checked>
<span>⭐ Favoriten-Bereich anzeigen</span>
</label>
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer; text-transform: none; letter-spacing: normal; font-weight: normal;">
<input type="checkbox" id="settingRecent" checked>
<span>🕐 Zuletzt geöffnet anzeigen</span>
</label>
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer; text-transform: none; letter-spacing: normal; font-weight: normal;">
<input type="checkbox" id="settingCompact">
<span>⊞ Kompakt-Modus (kleinere Karten)</span>
</label>
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer; text-transform: none; letter-spacing: normal; font-weight: normal;">
<input type="checkbox" id="settingColoredIcons" checked>
<span>🎨 Farbige Datei-Icons</span>
</label>
<div class="settings-header">
<div>
<div class="settings-eyebrow">Konfiguration</div>
<h2>⚙️ Einstellungen</h2>
<p class="settings-subtitle">Alle wichtigen Optionen auf einer Seite: Zugangsdaten, Verbindungscheck, Darstellung und Updates.</p>
</div>
</div>
<div class="input-group" style="margin-top: 30px; padding-top: 20px; border-top: 1px solid rgba(255,255,255,0.1);">
<label>App Version</label>
<div style="display: flex; gap: 12px; align-items: center;">
<input id="appVersion" type="text" readonly style="flex: 1; background: rgba(255,255,255,0.05); cursor: not-allowed;">
<button id="btnCheckUpdates" style="
background: linear-gradient(135deg, #00d4ff, #8b5cf6);
color: #000;
border: none;
padding: 10px 20px;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
white-space: nowrap;
">🔄 Nach Updates suchen</button>
<div class="settings-layout">
<div class="settings-column settings-column--left">
<section class="settings-panel settings-panel--credentials">
<div class="settings-panel-header">
<div>
<h3>Zugangsdaten</h3>
<p>API-Zugriffe für GitHub und Gitea konfigurieren.</p>
</div>
</div>
<div class="settings-fields-grid">
<div class="input-group">
<label for="githubToken">GitHub Token</label>
<input id="githubToken" type="password" placeholder="ghp_...">
</div>
<div class="input-group">
<label for="giteaToken">Gitea Token</label>
<input id="giteaToken" type="password" placeholder="Token hier einfügen">
</div>
</div>
<div class="input-group input-group--wide">
<label for="giteaURL">Gitea URL</label>
<input id="giteaURL" type="text" placeholder="https://gitea.example.com">
<div class="settings-connection-tools">
<div id="giteaUrlHint" class="settings-inline-hint">Hinweis: IPv6 mit Klammern eingeben, z.B. http://[2001:db8::1]:3000</div>
<button id="btnTestGiteaConnection" class="secondary">🔌 Verbindung testen</button>
</div>
</div>
</section>
<section class="settings-panel settings-panel--display">
<div class="settings-panel-header">
<div>
<h3>Darstellung</h3>
<p>Übersicht und Explorer an deinen Arbeitsstil anpassen.</p>
</div>
</div>
<div class="settings-toggle-list">
<label class="settings-toggle-row" for="settingFavorites">
<span class="settings-toggle-info">
<span class="settings-toggle-title">⭐ Favoriten-Bereich anzeigen</span>
<span class="settings-toggle-desc">Pinnt wichtige Repositories und Ordner sichtbar im Kopfbereich.</span>
</span>
<span class="toggle-switch">
<input type="checkbox" id="settingFavorites" checked>
<span class="toggle-track"></span>
</span>
</label>
<label class="settings-toggle-row" for="settingRecent">
<span class="settings-toggle-info">
<span class="settings-toggle-title">🕐 Zuletzt geöffnet anzeigen</span>
<span class="settings-toggle-desc">Zeigt deine letzten Projekte direkt in der Übersicht an.</span>
</span>
<span class="toggle-switch">
<input type="checkbox" id="settingRecent" checked>
<span class="toggle-track"></span>
</span>
</label>
<label class="settings-toggle-row" for="settingCompact">
<span class="settings-toggle-info">
<span class="settings-toggle-title">⊞ Kompakt-Modus</span>
<span class="settings-toggle-desc">Verdichtet Karten und Abstände für kleinere Fenster.</span>
</span>
<span class="toggle-switch">
<input type="checkbox" id="settingCompact">
<span class="toggle-track"></span>
</span>
</label>
<label class="settings-toggle-row" for="settingColoredIcons">
<span class="settings-toggle-info">
<span class="settings-toggle-title">🎨 Farbige Datei-Icons</span>
<span class="settings-toggle-desc">Setzt stärkere Dateityp-Farben für schnellere Orientierung.</span>
</span>
<span class="toggle-switch">
<input type="checkbox" id="settingColoredIcons" checked>
<span class="toggle-track"></span>
</span>
</label>
</div>
</section>
<section class="settings-panel settings-panel--backups">
<div class="settings-panel-header">
<div>
<h3>💽 Lokale Backups</h3>
<p>Automatische lokale Backups in einen Zielordner.</p>
</div>
</div>
<div class="settings-toggle-list">
<label class="settings-toggle-row" for="settingAutoBackup">
<span class="settings-toggle-info">
<span class="settings-toggle-title">🔄 Auto-Backup nach Push</span>
<span class="settings-toggle-desc">Erstellt automatisch vor jedem Upload ein lokales Backup im gewählten Zielordner.</span>
</span>
<span class="toggle-switch">
<input type="checkbox" id="settingAutoBackup">
<span class="toggle-track"></span>
</span>
</label>
</div>
<button id="btnOpenBackupManagement" style="
width: 100%;
padding: 12px;
background: linear-gradient(135deg, rgba(0,212,255,0.1), rgba(100,200,255,0.05));
border: 1px solid rgba(0,212,255,0.3);
border-radius: 6px;
color: var(--accent-primary);
cursor: pointer;
font-weight: 500;
margin-top: 12px;
">
💾 Backup-Verwaltung öffnen →
</button>
</section>
</div>
<div class="settings-column settings-column--right">
<section class="settings-panel settings-panel--health">
<div class="settings-panel-header">
<div>
<h3>Verbindungsstatus</h3>
<p>Direkt sehen, ob URL, API und Auth sauber antworten.</p>
</div>
</div>
<div class="settings-health-box">
<div class="settings-health-row"><span>URL</span><strong id="healthUrl">Unbekannt</strong></div>
<div class="settings-health-row"><span>API</span><strong id="healthApi">Unbekannt</strong></div>
<div class="settings-health-row"><span>Auth</span><strong id="healthAuth">Unbekannt</strong></div>
<div class="settings-health-row"><span>Latenz</span><strong id="healthLatency">-</strong></div>
<div class="settings-health-row"><span>Server</span><strong id="healthVersion">-</strong></div>
<div class="settings-health-row"><span>Letzter Fehler</span><strong id="healthLastError">-</strong></div>
</div>
</section>
<section class="settings-panel settings-panel--system">
<div class="settings-panel-header">
<div>
<h3>System</h3>
<p>Verhalten beim Windows-Start steuern.</p>
</div>
</div>
<div class="settings-toggle-list">
<label class="settings-toggle-row" for="settingAutostart">
<span class="settings-toggle-info">
<span class="settings-toggle-title">🚀 Mit Windows starten</span>
<span class="settings-toggle-desc">Startet die App automatisch beim Anmelden, minimiert im System-Tray.</span>
</span>
<span class="toggle-switch">
<input type="checkbox" id="settingAutostart">
<span class="toggle-track"></span>
</span>
</label>
</div>
</section>
<section class="settings-panel settings-panel--app">
<div class="settings-panel-header">
<div>
<h3>App & Updates</h3>
<p>Version prüfen und neue Releases direkt anstoßen.</p>
</div>
</div>
<div class="settings-version-card">
<div class="input-group input-group--wide settings-version-field">
<label for="appVersion">App Version</label>
<input id="appVersion" class="settings-readonly-input" type="text" readonly>
</div>
<button id="btnCheckUpdates" class="settings-update-btn">🔄 Nach Updates suchen</button>
</div>
</section>
</div>
</div>
<div class="modal-buttons">
<button id="btnSaveSettings">Speichern</button>
<div class="modal-buttons settings-modal-actions">
<button id="btnSaveSettings" class="accent-btn">Speichern</button>
<button id="btnCloseSettings" class="secondary">Abbrechen</button>
</div>
</div>
@@ -107,6 +309,7 @@
<div class="input-group">
<label>Repository Name</label>
<input id="repoName" type="text" placeholder="mein-projekt">
<div id="repoNameValidationHint" class="settings-inline-hint">Name prüfen: Duplikate, ähnliche Namen und ungültige Zeichen werden erkannt.</div>
</div>
<div class="input-group">
@@ -147,6 +350,80 @@
</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&#10;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>
<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="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">
@@ -197,7 +474,7 @@
<div style="display: flex; align-items: center; gap: 20px; margin-bottom: 20px;">
<div style="font-size: 3rem; filter: drop-shadow(0 0 10px var(--accent-primary));">🚀</div>
<div>
<h2 style="margin: 0; background: var(--accent-gradient); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">Update verfügbar!</h2>
<h2 style="margin: 0; background: var(--accent-gradient); background-clip: text; -webkit-background-clip: text; -webkit-text-fill-color: transparent;">Update verfügbar!</h2>
<p id="updateVersionInfo" style="color: var(--text-secondary); margin: 5px 0 0 0; font-family: monospace;"></p>
</div>
</div>
@@ -225,6 +502,72 @@
</div>
</div>
<!-- Backup Management Modal -->
<div id="backupManagementModal" class="hidden">
<div class="backup-management-card">
<div class="backup-modal-header">
<h2 style="margin: 0; display: flex; align-items: center; gap: 10px; font-size: 18px;">
<span>📦</span> Backup-Verwaltung
</h2>
<button id="btnCloseBackupModal" class="backup-modal-close" title="Schließen"></button>
</div>
<div class="backup-modal-body">
<div class="backup-credentials-section" style="display: flex;">
<div class="backup-input-group">
<label for="backupSourceSelect">Backup-Quelle (aus vorhandenen Projekten)</label>
<select id="backupSourceSelect">
<option value="">-- Wähle aus vorhandenen Projekten --</option>
</select>
<small style="color: var(--text-muted); font-size: 11px;">
Option "Alles komplett sichern" erstellt Backups aller verfügbaren Git-Projekte im gewählten Zielordner.
</small>
</div>
</div>
<!-- Local Credentials -->
<div id="localCredentials" class="backup-credentials-section">
<div class="backup-input-group">
<label for="localBackupFolder">Backup-Zielordner</label>
<div style="display: grid; grid-template-columns: 1fr auto; gap: 8px;">
<input id="localBackupFolder" type="text" placeholder="C:/Backups/GitManager" readonly>
<button id="btnPickLocalBackupFolder" class="backup-btn backup-btn-secondary" type="button" style="min-width: 120px;">📁 Ordner wählen</button>
</div>
</div>
</div>
<!-- Section Divider -->
<div style="height: 1px; background: rgba(88, 213, 255, 0.2); margin: 8px 0;"></div>
<!-- Backup List Section -->
<div style="margin-top: 16px;">
<h3 style="margin: 0 0 12px 0; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; color: var(--accent-primary);">📋 Gespeicherte Backups</h3>
<div id="backupListContainer" style="
background: rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 6px;
max-height: 250px;
overflow-y: auto;
min-height: 100px;
display: flex;
align-items: center;
justify-content: center;
">
<div style="padding: 16px; text-align: center; color: var(--text-muted); font-size: 13px;">
⏳ Lade Liste... oder keine Backups vorhanden
</div>
</div>
<!-- Backup Action Buttons -->
<div class="backup-modal-buttons">
<button id="btnCreateBackupNow" class="backup-btn backup-btn-primary" style="flex: 1;"> Backup erstellen</button>
<button id="btnRefreshBackupsList" class="backup-btn backup-btn-secondary" style="flex: 1;">🔄 Aktualisieren</button>
</div>
</div>
</div>
</div>
</div>
</div> <script src="renderer.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

183
src/backup/BackupManager.js Normal file
View File

@@ -0,0 +1,183 @@
/**
* BackupManager — Orchestrates backup operations
*/
const archiver = require('archiver')
const { createReadStream, createWriteStream } = require('fs')
const { mkdir } = require('fs').promises
const path = require('path')
const { Transform } = require('stream')
class BackupManager {
constructor(provider) {
this.provider = provider
}
/**
* Create a backup from a project folder
* @param {string} projectPath - Path to project folder
* @param {string} repoName - Repository name (used for filenames)
* @returns {Promise<{filename, size, timestamp}>}
*/
async createBackup(projectPath, repoName) {
try {
// Create ZIP buffer
const buffer = await this._createZip(projectPath)
const timestamp = this._getTimestamp()
const filename = `${repoName}-backup-${timestamp}.zip`
// Upload to provider
const result = await this.provider.uploadBackup(buffer, filename)
// Cleanup old backups
await this._cleanupOldBackups(repoName)
return {
filename,
size: result.size || buffer.length,
timestamp
}
} catch (err) {
throw new Error(`Backup creation failed: ${err.message}`)
}
}
/**
* List all backups for a repository
* @param {string} repoName - Repository name
* @returns {Promise<Array>}
*/
async listBackups(repoName) {
try {
const backups = await this.provider.listBackups()
return backups
.filter(b => b.name.startsWith(repoName))
.sort((a, b) => new Date(b.date || b.name) - new Date(a.date || a.name))
} catch (err) {
throw new Error(`Failed to list backups: ${err.message}`)
}
}
/**
* Restore a backup to a target folder
* @param {string} repoName - Repository name
* @param {string} filename - Backup filename
* @param {string} targetPath - Target folder path
*/
async restoreBackup(repoName, filename, targetPath) {
try {
// Download backup
const buffer = await this.provider.downloadBackup(filename)
// Extract ZIP
await this._extractZip(buffer, targetPath)
return { ok: true, restored: filename }
} catch (err) {
throw new Error(`Restore failed: ${err.message}`)
}
}
/**
* Delete a backup
* @param {string} filename - Backup filename
*/
async deleteBackup(filename) {
try {
await this.provider.deleteBackup(filename)
return { ok: true }
} catch (err) {
throw new Error(`Delete failed: ${err.message}`)
}
}
// ==================== PRIVATE METHODS ====================
/**
* Create ZIP buffer from project folder
* Excludes: .git, node_modules, dist, build, .env
*/
async _createZip(projectPath) {
return new Promise((resolve, reject) => {
const output = []
const archive = archiver('zip', { zlib: { level: 5 } })
archive.on('data', chunk => output.push(chunk))
archive.on('end', () => resolve(Buffer.concat(output)))
archive.on('error', reject)
// Add files with exclusions
archive.glob('**/*', {
cwd: projectPath,
ignore: [
'.git/**',
'.git',
'node_modules/**',
'node_modules',
'dist/**',
'dist',
'build/**',
'build',
'.env',
'.env.local',
'.env.*.local',
'*.log',
'data/backups/**'
],
dot: true
})
archive.finalize()
})
}
/**
* Extract ZIP buffer to target folder
*/
async _extractZip(buffer, targetPath) {
const unzipper = require('unzipper')
return new Promise((resolve, reject) => {
const { Readable } = require('stream')
const stream = Readable.from(buffer)
stream
.pipe(unzipper.Extract({ path: targetPath }))
.on('close', resolve)
.on('error', reject)
})
}
/**
* Get ISO timestamp (YYYYMMDD-HHMMSS format)
*/
_getTimestamp() {
const now = new Date()
const year = now.getFullYear()
const month = String(now.getMonth() + 1).padStart(2, '0')
const day = String(now.getDate()).padStart(2, '0')
const hours = String(now.getHours()).padStart(2, '0')
const minutes = String(now.getMinutes()).padStart(2, '0')
const seconds = String(now.getSeconds()).padStart(2, '0')
return `${year}${month}${day}-${hours}${minutes}${seconds}`
}
/**
* Cleanup old backups (keep only last N versions)
*/
async _cleanupOldBackups(repoName, maxVersions = 5) {
try {
const backups = await this.listBackups(repoName)
for (let i = maxVersions; i < backups.length; i++) {
await this.provider.deleteBackup(backups[i].name)
}
} catch (err) {
// Silently ignore cleanup errors
console.warn(`Cleanup warning: ${err.message}`)
}
}
}
module.exports = BackupManager

View File

@@ -0,0 +1,59 @@
/**
* BackupProvider — Abstract base class for backup providers
* All providers must implement these methods
*/
class BackupProvider {
/**
* Authenticate with the backup service
* @param {Object} credentials - Provider-specific credentials
*/
async authenticate(credentials) {
throw new Error('authenticate() not implemented in ' + this.constructor.name)
}
/**
* Upload a backup file
* @param {Buffer} buffer - File content
* @param {string} filename - Filename (e.g., 'repo-backup-2025-03-24.zip')
* @returns {Promise<{size: number}>}
*/
async uploadBackup(buffer, filename) {
throw new Error('uploadBackup() not implemented in ' + this.constructor.name)
}
/**
* List all backups for a repository
* @returns {Promise<Array>} Array of {name, size, date}
*/
async listBackups() {
throw new Error('listBackups() not implemented in ' + this.constructor.name)
}
/**
* Download a specific backup
* @param {string} filename - Filename to download
* @returns {Promise<Buffer>}
*/
async downloadBackup(filename) {
throw new Error('downloadBackup() not implemented in ' + this.constructor.name)
}
/**
* Delete a backup file
* @param {string} filename - Filename to delete
*/
async deleteBackup(filename) {
throw new Error('deleteBackup() not implemented in ' + this.constructor.name)
}
/**
* Test connection to backup service
* @returns {Promise<{ok: boolean, error?: string}>}
*/
async testConnection() {
throw new Error('testConnection() not implemented in ' + this.constructor.name)
}
}
module.exports = BackupProvider

View File

@@ -0,0 +1,84 @@
/**
* LocalProvider — Backup provider for local folders
*/
const path = require('path')
const fs = require('fs').promises
const BackupProvider = require('./BackupProvider')
class LocalProvider extends BackupProvider {
constructor() {
super()
this.basePath = null
}
async authenticate(credentials) {
const basePath = String(credentials && credentials.basePath ? credentials.basePath : '').trim()
if (!basePath) {
throw new Error('Lokaler Backup-Ordner fehlt')
}
this.basePath = path.resolve(basePath)
await fs.mkdir(this.basePath, { recursive: true })
}
async testConnection() {
if (!this.basePath) return { ok: false, error: 'Not authenticated' }
try {
const stat = await fs.stat(this.basePath)
if (!stat.isDirectory()) {
return { ok: false, error: 'Pfad ist kein Ordner' }
}
return { ok: true }
} catch (err) {
return { ok: false, error: err.message }
}
}
async uploadBackup(buffer, filename) {
if (!this.basePath) throw new Error('Not authenticated')
const target = path.join(this.basePath, filename)
await fs.mkdir(path.dirname(target), { recursive: true })
await fs.writeFile(target, buffer)
return { size: buffer.length }
}
async listBackups() {
if (!this.basePath) throw new Error('Not authenticated')
const entries = await fs.readdir(this.basePath, { withFileTypes: true })
const files = entries.filter(e => e.isFile() && e.name.endsWith('.zip'))
const backups = []
for (const f of files) {
const full = path.join(this.basePath, f.name)
const stat = await fs.stat(full)
backups.push({
name: f.name,
size: stat.size,
date: stat.mtime.toISOString()
})
}
backups.sort((a, b) => new Date(b.date) - new Date(a.date))
return backups
}
async downloadBackup(filename) {
if (!this.basePath) throw new Error('Not authenticated')
const file = path.join(this.basePath, filename)
return fs.readFile(file)
}
async deleteBackup(filename) {
if (!this.basePath) throw new Error('Not authenticated')
const file = path.join(this.basePath, filename)
await fs.unlink(file)
}
}
module.exports = LocalProvider

View File

@@ -3,10 +3,96 @@
// getGiteaRepoContents, getGiteaFileContent, uploadGiteaFile
const axios = require('axios');
const http = require('http');
const https = require('https');
// IPv4 bevorzugen verhindert ETIMEDOUT wenn der Hostname nur per IPv6 erreichbar wäre
// oder Node.js fälschlicherweise IPv6 vorranging versucht.
const ipv4HttpAgent = new http.Agent({ family: 4, keepAlive: true });
const ipv4HttpsAgent = new https.Agent({ family: 4, keepAlive: true });
const axiosInstance = axios.create({
httpAgent: ipv4HttpAgent,
httpsAgent: ipv4HttpsAgent,
});
function normalizeAndValidateBaseUrl(rawUrl) {
const value = (rawUrl || '').trim();
if (!value) {
throw new Error('Gitea URL fehlt. Bitte tragen Sie eine URL ein.');
}
let parsed;
try {
parsed = new URL(value);
} catch (_) {
throw new Error('Ungueltige Gitea URL. Beispiel fuer IPv6: http://[2001:db8::1]:3000');
}
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
throw new Error('Gitea URL muss mit http:// oder https:// beginnen.');
}
return value.replace(/\/$/, '');
}
function normalizeBase(url) {
if (!url) return null;
return url.replace(/\/$/, '');
return normalizeAndValidateBaseUrl(url);
}
async function checkGiteaConnection({ token, url, timeout = 8000 }) {
const base = normalizeAndValidateBaseUrl(url);
const started = Date.now();
const versionRes = await axiosInstance.get(`${base}/api/v1/version`, {
timeout,
validateStatus: () => true,
headers: {
'User-Agent': 'Git-Manager-GUI'
}
});
const latencyMs = Math.max(1, Date.now() - started);
const apiReachable = versionRes.status >= 200 && versionRes.status < 500;
let authStatus = null;
let authOk = false;
if (token) {
const userRes = await axiosInstance.get(`${base}/api/v1/user`, {
timeout,
validateStatus: () => true,
headers: {
Authorization: `token ${token}`,
'User-Agent': 'Git-Manager-GUI'
}
});
authStatus = userRes.status;
authOk = userRes.status >= 200 && userRes.status < 300;
}
const serverVersion =
(versionRes.data && (versionRes.data.version || versionRes.data.Version || versionRes.data.tag)) ||
null;
return {
ok: apiReachable && (!!token ? authOk : true),
base,
checks: {
urlValid: true,
apiReachable,
authProvided: !!token,
authOk
},
metrics: {
latencyMs,
versionStatus: versionRes.status,
authStatus
},
server: {
version: serverVersion
}
};
}
function buildContentsUrl(base, owner, repo, p) {
@@ -17,7 +103,7 @@ function buildContentsUrl(base, owner, repo, p) {
async function tryRequest(url, token, opts = {}) {
try {
const res = await axios.get(url, {
const res = await axiosInstance.get(url, {
headers: token ? { Authorization: `token ${token}` } : {},
timeout: opts.timeout || 10000
});
@@ -40,7 +126,7 @@ async function createRepoGitHub({ name, token, auto_init = true, license = '', p
}
try {
const response = await axios.post('https://api.github.com/user/repos', body, {
const response = await axiosInstance.post('https://api.github.com/user/repos', body, {
headers: { Authorization: `token ${token}` }
});
return response.data;
@@ -99,7 +185,7 @@ async function createRepoGitea({ name, token, url, auto_init = true, license = '
console.log('Request body:', JSON.stringify(body, null, 2));
try {
const response = await axios.post(endpoint, body, {
const response = await axiosInstance.post(endpoint, body, {
headers: { Authorization: `token ${token}` },
timeout: 15000 // 15 Sekunden Timeout
});
@@ -122,7 +208,7 @@ async function createRepoGitea({ name, token, url, auto_init = true, license = '
};
try {
const retryResponse = await axios.post(endpoint, bodyWithoutLicense, {
const retryResponse = await axiosInstance.post(endpoint, bodyWithoutLicense, {
headers: { Authorization: `token ${token}` },
timeout: 15000
});
@@ -168,12 +254,150 @@ async function createRepoGitea({ name, token, url, auto_init = true, license = '
async function listGiteaRepos({ token, url }) {
const endpoint = normalizeBase(url) + '/api/v1/user/repos';
const response = await axios.get(endpoint, {
const response = await axiosInstance.get(endpoint, {
headers: { Authorization: `token ${token}` }
});
return response.data;
}
function toDateKey(value) {
if (value == null) return null;
if (typeof value === 'string') {
const s = value.trim();
if (!s) return null;
const match = s.match(/^(\d{4}-\d{2}-\d{2})/);
if (match) return match[1];
if (/^\d+$/.test(s)) {
const n = Number(s);
if (!Number.isFinite(n)) return null;
const ms = n < 1e12 ? n * 1000 : n;
const d = new Date(ms);
if (Number.isNaN(d.getTime())) return null;
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
}
const d = new Date(s);
if (Number.isNaN(d.getTime())) return null;
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
}
if (typeof value === 'number' && Number.isFinite(value)) {
const ms = value < 1e12 ? value * 1000 : value;
const d = new Date(ms);
if (Number.isNaN(d.getTime())) return null;
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
}
if (value instanceof Date && !Number.isNaN(value.getTime())) {
return `${value.getFullYear()}-${String(value.getMonth() + 1).padStart(2, '0')}-${String(value.getDate()).padStart(2, '0')}`;
}
return null;
}
function normalizeHeatmapEntries(payload) {
const acc = new Map();
const addEntry = (dateLike, countLike) => {
const key = toDateKey(dateLike);
if (!key) return;
const n = Number(countLike);
const count = Number.isFinite(n) ? Math.max(0, Math.floor(n)) : 1;
acc.set(key, (acc.get(key) || 0) + count);
};
const walk = (node, depth = 0) => {
if (depth > 6 || node == null) return;
if (Array.isArray(node)) {
node.forEach(item => walk(item, depth + 1));
return;
}
if (typeof node === 'number' || typeof node === 'string' || node instanceof Date) {
addEntry(node, 1);
return;
}
if (typeof node !== 'object') return;
const dateLike =
node.date ?? node.day ?? node.timestamp ?? node.time ?? node.ts ?? node.created_at ?? node.created ?? node.when;
const countLike =
node.count ?? node.contributions ?? node.value ?? node.total ?? node.commits ?? node.frequency;
if (dateLike != null) {
addEntry(dateLike, countLike);
}
if (node.data != null) walk(node.data, depth + 1);
if (node.values != null) walk(node.values, depth + 1);
if (node.heatmap != null) walk(node.heatmap, depth + 1);
if (node.contributions != null) walk(node.contributions, depth + 1);
if (node.days != null) walk(node.days, depth + 1);
const keys = Object.keys(node);
for (const key of keys) {
if (/^\d{4}-\d{2}-\d{2}$/.test(key)) {
addEntry(key, node[key]);
}
}
};
walk(payload, 0);
return Array.from(acc.entries())
.map(([date, count]) => ({ date, count }))
.sort((a, b) => a.date.localeCompare(b.date));
}
async function getGiteaUserHeatmap({ token, url }) {
const base = normalizeBase(url);
if (!base) throw new Error('Invalid Gitea base URL');
const headers = {
Authorization: `token ${token}`,
'User-Agent': 'Git-Manager-GUI'
};
const meRes = await axiosInstance.get(`${base}/api/v1/user`, {
headers,
timeout: 10000
});
const username = meRes?.data?.login || meRes?.data?.username || meRes?.data?.name || null;
const candidates = [
`${base}/api/v1/user/heatmap`,
username ? `${base}/api/v1/users/${encodeURIComponent(username)}/heatmap` : null
].filter(Boolean);
let lastError = null;
for (const endpoint of candidates) {
try {
const response = await axiosInstance.get(endpoint, {
headers,
timeout: 12000,
validateStatus: () => true
});
if (response.status >= 200 && response.status < 300) {
return {
username,
endpoint,
entries: normalizeHeatmapEntries(response.data)
};
}
lastError = new Error(`Heatmap endpoint failed (${response.status}): ${endpoint}`);
} catch (e) {
lastError = e;
}
}
throw lastError || new Error('No usable heatmap endpoint found');
}
/**
* Returns array of items for a directory or single item for file.
* Each item includes name, path, type, size, download_url, sha (if present).
@@ -335,8 +559,9 @@ async function uploadGiteaFile({ token, url, owner, repo, path, contentBase64, m
const fetchSha = async () => {
try {
const existing = await getGiteaRepoContents({ token, url: base, owner, repo, path, ref: branchName });
if (existing && existing.length > 0 && existing[0].sha) {
return existing[0].sha;
const items = existing && existing.items ? existing.items : (Array.isArray(existing) ? existing : []);
if (items.length > 0 && items[0].sha) {
return items[0].sha;
}
return null;
} catch (e) {
@@ -350,11 +575,10 @@ async function uploadGiteaFile({ token, url, owner, repo, path, contentBase64, m
const fileName = pathParts.pop();
const dirPath = pathParts.join('/');
const list = await getGiteaRepoContents({ token, url: base, owner, repo, path: dirPath, ref: branchName });
if (Array.isArray(list)) {
const item = list.find(i => i.name === fileName);
if (item && item.sha) return item.sha;
}
const result = await getGiteaRepoContents({ token, url: base, owner, repo, path: dirPath, ref: branchName });
const list = result && result.items ? result.items : (Array.isArray(result) ? result : []);
const item = list.find(i => i.name === fileName);
if (item && item.sha) return item.sha;
return null;
} catch (e) {
return null;
@@ -385,7 +609,7 @@ async function uploadGiteaFile({ token, url, owner, repo, path, contentBase64, m
console.log(`[Upload Debug] Datei: ${path}, Branch: ${branchName}, SHA: ${sha ? sha.substring(0, 8) : 'keine'}`);
try {
const res = await axios.put(endpoint, body, {
const res = await axiosInstance.put(endpoint, body, {
headers: { Authorization: `token ${token}` },
timeout: 30000 // 30 Sekunden Timeout für größere Dateien
});
@@ -478,7 +702,7 @@ async function getGiteaCommits({ token, url, owner, repo, branch = 'HEAD', page
const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/commits`;
try {
const response = await axios.get(endpoint, {
const response = await axiosInstance.get(endpoint, {
headers: { Authorization: `token ${token}` },
params: {
sha: branch,
@@ -503,7 +727,7 @@ async function getGiteaCommit({ token, url, owner, repo, sha }) {
const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/git/commits/${sha}`;
try {
const response = await axios.get(endpoint, {
const response = await axiosInstance.get(endpoint, {
headers: { Authorization: `token ${token}` }
});
return response.data;
@@ -524,7 +748,7 @@ async function getGiteaCommitDiff({ token, url, owner, repo, sha }) {
const endpoint = `${base}/${owner}/${repo}/commit/${sha}.diff`;
try {
const response = await axios.get(endpoint, {
const response = await axiosInstance.get(endpoint, {
headers: { Authorization: `token ${token}` }
});
return response.data;
@@ -544,7 +768,7 @@ async function getGiteaCommitFiles({ token, url, owner, repo, sha }) {
const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/git/commits/${sha}`;
try {
const response = await axios.get(endpoint, {
const response = await axiosInstance.get(endpoint, {
headers: { Authorization: `token ${token}` }
});
@@ -581,7 +805,7 @@ async function searchGiteaCommits({ token, url, owner, repo, query, branch = 'HE
const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/commits`;
try {
const response = await axios.get(endpoint, {
const response = await axiosInstance.get(endpoint, {
headers: { Authorization: `token ${token}` },
params: {
sha: branch,
@@ -614,7 +838,7 @@ async function getGiteaBranches({ token, url, owner, repo }) {
const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/branches`;
try {
const response = await axios.get(endpoint, {
const response = await axiosInstance.get(endpoint, {
headers: { Authorization: `token ${token}` }
});
return response.data;
@@ -638,7 +862,7 @@ async function listGiteaReleases({ token, url, owner, repo }) {
const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases`;
try {
const response = await axios.get(endpoint, {
const response = await axiosInstance.get(endpoint, {
headers: { Authorization: `token ${token}` }
});
return response.data;
@@ -658,7 +882,7 @@ async function getGiteaRelease({ token, url, owner, repo, tag }) {
const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/tags/${encodeURIComponent(tag)}`;
try {
const response = await axios.get(endpoint, {
const response = await axiosInstance.get(endpoint, {
headers: { Authorization: `token ${token}` }
});
return response.data;
@@ -687,7 +911,7 @@ async function createGiteaRelease({ token, url, owner, repo, data }) {
};
try {
const response = await axios.post(endpoint, body, {
const response = await axiosInstance.post(endpoint, body, {
headers: {
Authorization: `token ${token}`,
'Content-Type': 'application/json'
@@ -747,7 +971,7 @@ async function editGiteaRelease({ token, url, owner, repo, releaseId, data }) {
if (data.tag_name !== undefined) body.tag_name = data.tag_name;
try {
const response = await axios.patch(endpoint, body, {
const response = await axiosInstance.patch(endpoint, body, {
headers: {
Authorization: `token ${token}`,
'Content-Type': 'application/json'
@@ -770,7 +994,7 @@ async function deleteGiteaRelease({ token, url, owner, repo, releaseId }) {
const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/${releaseId}`;
try {
await axios.delete(endpoint, {
await axiosInstance.delete(endpoint, {
headers: { Authorization: `token ${token}` }
});
return { ok: true };
@@ -799,7 +1023,7 @@ async function uploadReleaseAsset({ token, url, owner, repo, releaseId, filePath
});
try {
const response = await axios.post(endpoint, formData, {
const response = await axiosInstance.post(endpoint, formData, {
headers: {
Authorization: `token ${token}`,
...formData.getHeaders()
@@ -824,7 +1048,7 @@ async function deleteReleaseAsset({ token, url, owner, repo, assetId }) {
const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/assets/${assetId}`;
try {
await axios.delete(endpoint, {
await axiosInstance.delete(endpoint, {
headers: { Authorization: `token ${token}` }
});
return { ok: true };
@@ -835,9 +1059,12 @@ async function deleteReleaseAsset({ token, url, owner, repo, assetId }) {
}
module.exports = {
normalizeAndValidateBaseUrl,
createRepoGitHub,
createRepoGitea,
checkGiteaConnection,
listGiteaRepos,
getGiteaUserHeatmap,
getGiteaRepoContents,
getGiteaFileContent,
uploadGiteaFile,