Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
BIN
assets/Thumbs.db
Normal file
BIN
assets/Thumbs.db
Normal file
Binary file not shown.
Binary file not shown.
236
package-lock.json
generated
236
package-lock.json
generated
@@ -9,13 +9,13 @@
|
||||
"version": "2.0.5",
|
||||
"dependencies": {
|
||||
"archiver": "^6.0.0",
|
||||
"axios": "^1.13.4",
|
||||
"axios": "^1.13.5",
|
||||
"form-data": "^4.0.5",
|
||||
"simple-git": "^3.19.1",
|
||||
"simple-git": "^3.32.3",
|
||||
"unzipper": "^0.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "^26.6.10",
|
||||
"electron": "^41.0.4",
|
||||
"electron-builder": "^26.8.1"
|
||||
}
|
||||
},
|
||||
@@ -344,16 +344,6 @@
|
||||
"node": ">=16.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron/universal/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==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron/universal/node_modules/fs-extra": {
|
||||
"version": "11.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz",
|
||||
@@ -383,19 +373,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@electron/universal/node_modules/minimatch": {
|
||||
"version": "9.0.9",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
|
||||
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.2"
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron/universal/node_modules/universalify": {
|
||||
@@ -828,13 +815,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.19.130",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz",
|
||||
"integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==",
|
||||
"version": "24.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz",
|
||||
"integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/plist": {
|
||||
@@ -916,9 +903,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
|
||||
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1138,6 +1125,19 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/app-builder-lib/node_modules/minimatch": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/app-builder-lib/node_modules/semver": {
|
||||
"version": "7.7.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
||||
@@ -1202,15 +1202,6 @@
|
||||
"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",
|
||||
@@ -1231,16 +1222,16 @@
|
||||
"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==",
|
||||
"node_modules/archiver-utils/node_modules/glob/node_modules/minimatch": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
@@ -1305,13 +1296,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz",
|
||||
"integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==",
|
||||
"version": "1.13.6",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
|
||||
"integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.4",
|
||||
"follow-redirects": "^1.15.11",
|
||||
"form-data": "^4.0.5",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
@@ -1631,16 +1622,6 @@
|
||||
"node": "^18.17.0 || >=20.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cacache/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==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cacache/node_modules/glob": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
|
||||
@@ -1663,6 +1644,19 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/cacache/node_modules/glob/node_modules/minimatch": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/cacache/node_modules/lru-cache": {
|
||||
"version": "10.4.3",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||
@@ -1670,22 +1664,6 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/cacache/node_modules/minimatch": {
|
||||
"version": "9.0.9",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
|
||||
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/cacheable-lookup": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
|
||||
@@ -2329,15 +2307,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/electron": {
|
||||
"version": "26.6.10",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-26.6.10.tgz",
|
||||
"integrity": "sha512-pV2SD0RXzAiNRb/2yZrsVmVkBOMrf+DVsPulIgRjlL0+My9BL5spFuhHVMQO9yHl9tFpWtuRpQv0ofM/i9P8xg==",
|
||||
"version": "41.0.4",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-41.0.4.tgz",
|
||||
"integrity": "sha512-rO08CxnAsAkKPFj3OZnxFkKrlnpSL3OCOewMDj5kaohVo++7e8hIT5Sl+tNl9WkNKiLvfZSW180ueA9s5zh9dg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@electron/get": "^2.0.0",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/node": "^24.9.0",
|
||||
"extract-zip": "^2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
@@ -2743,27 +2721,17 @@
|
||||
"minimatch": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/filelist/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==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/filelist/node_modules/minimatch": {
|
||||
"version": "5.1.9",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz",
|
||||
"integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
@@ -2976,9 +2944,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/glob/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
@@ -3662,45 +3630,6 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "10.2.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
|
||||
"integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^5.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch/node_modules/balanced-match": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch/node_modules/brace-expansion": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
|
||||
"integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
@@ -4354,25 +4283,16 @@
|
||||
"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==",
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
@@ -4592,9 +4512,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/simple-git": {
|
||||
"version": "3.30.0",
|
||||
"resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.30.0.tgz",
|
||||
"integrity": "sha512-q6lxyDsCmEal/MEGhP1aVyQ3oxnagGlBDOVSIB4XUVLl1iZh0Pah6ebC9V4xBap/RfgP2WlI8EKs0WS0rMEJHg==",
|
||||
"version": "3.33.0",
|
||||
"resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.33.0.tgz",
|
||||
"integrity": "sha512-D4V/tGC2sjsoNhoMybKyGoE+v8A60hRawKQ1iFRA1zwuDgGZCBJ4ByOzZ5J8joBbi4Oam0qiPH+GhzmSBwbJng==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@kwsites/file-exists": "^1.1.1",
|
||||
@@ -4846,9 +4766,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "7.5.9",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.9.tgz",
|
||||
"integrity": "sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==",
|
||||
"version": "7.5.13",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz",
|
||||
"integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
@@ -5094,9 +5014,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
|
||||
23
package.json
23
package.json
@@ -1,23 +1,27 @@
|
||||
{
|
||||
"name": "git-manager-gui",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.9",
|
||||
"description": "Git Manager GUI - Verwaltung von Git Repositories",
|
||||
"author": "M_Viper",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"start": "electron .",
|
||||
"build": "electron-builder",
|
||||
"test": "node --test __tests__/*.test.js"
|
||||
"test": "node --test __tests__/*.test.js",
|
||||
"pdf": "electron generate-pdf.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.13.4",
|
||||
"axios": "^1.13.5",
|
||||
"form-data": "^4.0.5",
|
||||
"simple-git": "^3.19.1",
|
||||
"simple-git": "^3.32.3",
|
||||
"archiver": "^6.0.0",
|
||||
"unzipper": "^0.11.0"
|
||||
},
|
||||
"overrides": {
|
||||
"minimatch": "^3.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "^26.6.10",
|
||||
"electron": "^41.0.4",
|
||||
"electron-builder": "^26.8.1"
|
||||
},
|
||||
"build": {
|
||||
@@ -39,7 +43,14 @@
|
||||
],
|
||||
"extraResources": [
|
||||
"repos/",
|
||||
"data/"
|
||||
{
|
||||
"from": "data",
|
||||
"to": "data",
|
||||
"filter": [
|
||||
"**/*",
|
||||
"!credentials.json"
|
||||
]
|
||||
}
|
||||
],
|
||||
"directories": {
|
||||
"buildResources": "assets"
|
||||
|
||||
62
preload.js
62
preload.js
@@ -1,5 +1,5 @@
|
||||
// preload.js — expose IPC to renderer
|
||||
const { contextBridge, ipcRenderer } = require('electron');
|
||||
const { contextBridge, ipcRenderer, webUtils } = require('electron');
|
||||
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
// Lokale Datei-Operationen
|
||||
@@ -20,7 +20,13 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
|
||||
// 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),
|
||||
getGiteaFileContent: (data) => ipcRenderer.invoke('get-gitea-file-content', data),
|
||||
readGiteaFile: (data) => ipcRenderer.invoke('read-gitea-file', data),
|
||||
@@ -33,13 +39,22 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
// Repository & Git Management
|
||||
saveCredentials: (data) => ipcRenderer.invoke('save-credentials', data),
|
||||
loadCredentials: () => ipcRenderer.invoke('load-credentials'),
|
||||
getCredentialsStatus: () => ipcRenderer.invoke('get-credentials-status'),
|
||||
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),
|
||||
updateGiteaRepoTopics: (data) => ipcRenderer.invoke('update-gitea-repo-topics', data),
|
||||
getGiteaTopicsCatalog: () => ipcRenderer.invoke('get-gitea-topics-catalog'),
|
||||
migrateRepoToGitea: (data) => ipcRenderer.invoke('migrate-repo-to-gitea', data),
|
||||
createRepo: (data) => ipcRenderer.invoke('create-repo', data),
|
||||
pushProject: (data) => ipcRenderer.invoke('push-project', data),
|
||||
getBranches: (data) => ipcRenderer.invoke('getBranches', data),
|
||||
getCommitLogs: (data) => ipcRenderer.invoke('getCommitLogs', data),
|
||||
uploadAndPush: (data) => ipcRenderer.invoke('upload-and-push', data),
|
||||
deleteGiteaRepo: (data) => ipcRenderer.invoke('delete-gitea-repo', data),
|
||||
syncRepoToGitHub: (data) => ipcRenderer.invoke('sync-repo-to-github', data),
|
||||
runBatchRepoAction: (data) => ipcRenderer.invoke('run-batch-repo-action', data),
|
||||
validateRepoName: (data) => ipcRenderer.invoke('validate-repo-name', data),
|
||||
checkCloneTargetCollisions: (data) => ipcRenderer.invoke('check-clone-target-collisions', data),
|
||||
@@ -52,6 +67,14 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
// 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),
|
||||
@@ -84,7 +107,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
saveRecent: (data) => ipcRenderer.invoke('save-recent', data),
|
||||
|
||||
// === UPDATER APIs ===
|
||||
checkForUpdates: () => ipcRenderer.invoke('check-for-updates'),
|
||||
checkForUpdates: (options) => ipcRenderer.invoke('check-for-updates', options || {}),
|
||||
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
|
||||
|
||||
// Triggert den tatsächlichen Download des Assets
|
||||
@@ -98,6 +121,12 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
return () => ipcRenderer.removeListener('update-available', listener);
|
||||
},
|
||||
|
||||
onUpdateNotAvailable: (cb) => {
|
||||
const listener = (event, info) => cb(info);
|
||||
ipcRenderer.on('update-not-available', listener);
|
||||
return () => ipcRenderer.removeListener('update-not-available', listener);
|
||||
},
|
||||
|
||||
onUpdateProgress: (cb) => {
|
||||
const listener = (event, percent) => cb(percent);
|
||||
ipcRenderer.on('update-progress', listener);
|
||||
@@ -110,12 +139,6 @@ 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);
|
||||
@@ -140,22 +163,6 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
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'),
|
||||
@@ -167,5 +174,10 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
|
||||
// Utility
|
||||
copyToClipboard: (text) => ipcRenderer.invoke('copy-to-clipboard', text),
|
||||
openExternalUrl: (url) => ipcRenderer.invoke('open-external-url', url)
|
||||
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')
|
||||
});
|
||||
@@ -1,10 +1,13 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
|
||||
export default function Settings({ onClose }) {
|
||||
const [githubToken, setGithubToken] = useState('');
|
||||
const [giteaToken, setGiteaToken] = useState('');
|
||||
const [giteaURL, setGiteaURL] = useState('');
|
||||
const [avatarB64, setAvatarB64] = useState(null);
|
||||
const [savedOk, setSavedOk] = useState(false);
|
||||
const [avatarUploading, setAvatarUploading] = useState(false);
|
||||
const fileInputRef = useRef(null);
|
||||
|
||||
function normalizeAndValidateGiteaUrl(rawUrl) {
|
||||
const value = (rawUrl || '').trim();
|
||||
@@ -36,17 +39,46 @@ export default function Settings({ onClose }) {
|
||||
setGithubToken(data.githubToken || '');
|
||||
setGiteaToken(data.giteaToken || '');
|
||||
setGiteaURL(data.giteaURL || '');
|
||||
setAvatarB64(data.avatarB64 || null);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
function save() {
|
||||
function handleAvatarFileChange(e) {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = (ev) => setAvatarB64(ev.target.result);
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
async function save() {
|
||||
const checkedUrl = normalizeAndValidateGiteaUrl(giteaURL);
|
||||
if (!checkedUrl.ok) {
|
||||
alert(checkedUrl.error);
|
||||
return;
|
||||
}
|
||||
window.electronAPI.saveCredentials({ githubToken, giteaToken, giteaURL: checkedUrl.value });
|
||||
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);
|
||||
}
|
||||
@@ -55,6 +87,21 @@ export default function Settings({ onClose }) {
|
||||
<div className="modalContent card settings-modal-content">
|
||||
|
||||
<div className="settings-header">
|
||||
<div className="settings-header-inner">
|
||||
<div className="settings-avatar-wrap" onClick={() => fileInputRef.current?.click()} title="Profilbild ändern">
|
||||
{avatarB64
|
||||
? <img src={avatarB64} alt="Avatar" className="settings-avatar-img" />
|
||||
: <div className="settings-avatar-placeholder">👤</div>
|
||||
}
|
||||
<div className="settings-avatar-overlay">✏️</div>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
style={{ display: 'none' }}
|
||||
onChange={handleAvatarFileChange}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="settings-eyebrow">Konfiguration</div>
|
||||
<h2>⚙️ Einstellungen</h2>
|
||||
@@ -63,6 +110,7 @@ export default function Settings({ onClose }) {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-layout">
|
||||
<div className="settings-column settings-column--left">
|
||||
@@ -121,9 +169,10 @@ export default function Settings({ onClose }) {
|
||||
<button
|
||||
className="accent-btn"
|
||||
onClick={save}
|
||||
disabled={avatarUploading}
|
||||
style={savedOk ? { background: 'var(--success)', borderColor: 'var(--success)' } : {}}
|
||||
>
|
||||
{savedOk ? '✅ Gespeichert' : 'Speichern'}
|
||||
{avatarUploading ? '⏳ Bild wird hochgeladen…' : savedOk ? '✅ Gespeichert' : 'Speichern'}
|
||||
</button>
|
||||
{onClose && (
|
||||
<button className="secondary" onClick={onClose}>Abbrechen</button>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<meta charset="utf-8" />
|
||||
<title>Git Manager Explorer Pro</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https: http:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none';">
|
||||
<link rel="icon" type="image/png" href="./icon.png">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
@@ -38,11 +39,13 @@
|
||||
<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 class="toolbar-top-actions">
|
||||
<div class="tool-group tool-group--quick-actions">
|
||||
<button id="btnOpenRepoActions" title="Neues Repository erstellen">🚀 New Repo</button>
|
||||
<button id="btnOpenMigration" title="Repository von GitHub/GitLab zu Gitea migrieren">📥 Migrieren</button>
|
||||
<button id="btnPush" title="Projekt pushen">⬆️ Push</button>
|
||||
</div>
|
||||
|
||||
@@ -66,7 +69,7 @@
|
||||
<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>
|
||||
<button id="btnLoadGiteaRepos" class="accent-btn" title="Projekte der gewählten Plattform laden">🌐 Load Projekte</button>
|
||||
</div>
|
||||
|
||||
<div class="tool-group tool-group--repo">
|
||||
@@ -82,6 +85,8 @@
|
||||
</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">
|
||||
@@ -104,12 +109,21 @@
|
||||
</div>
|
||||
|
||||
<div class="settings-header">
|
||||
<div class="settings-header-inner">
|
||||
<div class="settings-avatar-wrap" id="settingsAvatarWrap" title="Profilbild ändern">
|
||||
<img id="settingsAvatarImg" class="settings-avatar-img" src="" alt="Avatar" style="display:none;">
|
||||
<div id="settingsAvatarPlaceholder" class="settings-avatar-placeholder">👤</div>
|
||||
<div class="settings-avatar-overlay">✏️</div>
|
||||
<input id="settingsAvatarInput" type="file" accept="image/*" style="display:none;">
|
||||
</div>
|
||||
<button id="btnUploadAvatar" class="settings-avatar-upload-btn" title="Gespeichertes Bild jetzt auf Gitea hochladen">📤 Auf Gitea aktualisieren</button>
|
||||
<div>
|
||||
<div class="settings-eyebrow">Konfiguration</div>
|
||||
<h2>⚙️ Einstellungen</h2>
|
||||
<p class="settings-subtitle">Alle wichtigen Optionen auf einer Seite: Zugangsdaten, Verbindungscheck, Darstellung und Updates.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-layout">
|
||||
<div class="settings-column settings-column--left">
|
||||
@@ -121,25 +135,35 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-fields-grid">
|
||||
<div class="input-group">
|
||||
<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>
|
||||
|
||||
<div class="input-group">
|
||||
<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>
|
||||
|
||||
<div class="input-group input-group--wide">
|
||||
<div class="input-group settings-auth-input">
|
||||
<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 id="giteaUrlHint" class="settings-inline-hint">Hinweis: IPv6 mit Klammern eingeben, z.B. http://[2001:db8::1]:3000</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -195,43 +219,8 @@
|
||||
<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>
|
||||
@@ -350,6 +339,60 @@
|
||||
</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>
|
||||
@@ -502,72 +545,6 @@
|
||||
</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>
|
||||
3310
renderer/renderer.js
3310
renderer/renderer.js
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,44 @@
|
||||
.project-gravur-bar {
|
||||
width: 100%;
|
||||
min-height: 28px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
background: none;
|
||||
margin-bottom: 0;
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
.project-gravur-title {
|
||||
font-size: 17px;
|
||||
font-weight: 500;
|
||||
color: rgba(174, 189, 216, 0.18);
|
||||
letter-spacing: 0.04em;
|
||||
font-style: italic;
|
||||
user-select: none;
|
||||
text-shadow: 0 1px 0 rgba(255,255,255,0.04), 0 1.5px 0 rgba(0,0,0,0.13);
|
||||
transition: color 0.2s;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.project-gravur-separator {
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, rgba(88,213,255,0.10) 0%, rgba(92,135,255,0.10) 100%);
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
.toolbar-project-title {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: rgba(174, 189, 216, 0.22);
|
||||
letter-spacing: 0.04em;
|
||||
margin-left: 32px;
|
||||
font-style: italic;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
text-shadow: 0 1px 0 rgba(255,255,255,0.04), 0 1.5px 0 rgba(0,0,0,0.13);
|
||||
transition: color 0.2s;
|
||||
}
|
||||
:root {
|
||||
/* Moderne Farbpalette */
|
||||
--bg-primary: #07111f;
|
||||
@@ -96,12 +137,15 @@
|
||||
|
||||
.titlebar-strip-title {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: rgba(174, 189, 216, 0.68);
|
||||
font-weight: 600;
|
||||
color: rgba(174, 189, 216, 0.32); /* viel dezenter */
|
||||
letter-spacing: 0.045em;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-shadow: 0 1px 0 rgba(255,255,255,0.04), 0 1.5px 0 rgba(0,0,0,0.13);
|
||||
font-style: italic;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
#titlebar-strip .win-controls {
|
||||
@@ -789,9 +833,11 @@ body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
max-height: calc(100vh - 270px);
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding-right: 2px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.fav-history-list::-webkit-scrollbar {
|
||||
@@ -857,10 +903,6 @@ body {
|
||||
width: 180px;
|
||||
margin: 12px 0 12px 12px;
|
||||
}
|
||||
|
||||
.fav-history-list {
|
||||
max-height: 180px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Global Drop Zone Indicator */
|
||||
@@ -1047,6 +1089,14 @@ body.compact-mode .item-card:hover {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.repo-avatar-img {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 10px;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.item-card:hover .item-icon {
|
||||
transform: translateY(-2px) scale(1.08) rotate(-4deg);
|
||||
}
|
||||
@@ -1065,6 +1115,16 @@ body.compact-mode .item-card:hover {
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.item-submeta {
|
||||
margin-top: 4px;
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ===========================
|
||||
MODALS
|
||||
=========================== */
|
||||
@@ -1310,8 +1370,9 @@ input[type="checkbox"] {
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
#btnTestGiteaConnection {
|
||||
margin-top: 10px;
|
||||
#btnTestGiteaConnection,
|
||||
#btnTestGithubConnection {
|
||||
margin-top: 0;
|
||||
min-height: 40px;
|
||||
border-radius: var(--radius-md);
|
||||
padding: 0 14px;
|
||||
@@ -1325,18 +1386,21 @@ input[type="checkbox"] {
|
||||
transition: transform var(--transition-normal), box-shadow var(--transition-normal), border-color var(--transition-normal), background var(--transition-normal);
|
||||
}
|
||||
|
||||
#btnTestGiteaConnection:hover {
|
||||
#btnTestGiteaConnection:hover,
|
||||
#btnTestGithubConnection:hover {
|
||||
transform: translateY(-1px);
|
||||
border-color: var(--accent-primary);
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(0, 212, 255, 0.14) 100%);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
#btnTestGiteaConnection:active {
|
||||
#btnTestGiteaConnection:active,
|
||||
#btnTestGithubConnection:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
#btnTestGiteaConnection:focus-visible {
|
||||
#btnTestGiteaConnection:focus-visible,
|
||||
#btnTestGithubConnection:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px rgba(0, 212, 255, 0.2);
|
||||
}
|
||||
@@ -1432,6 +1496,81 @@ input[type="checkbox"] {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.settings-header-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* Avatar-Upload-Button */
|
||||
.settings-avatar-upload-btn {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
padding: 4px 10px;
|
||||
font-size: 11px;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid rgba(88, 213, 255, 0.3);
|
||||
background: rgba(88, 213, 255, 0.08);
|
||||
color: var(--accent-primary);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: background 0.15s, border-color 0.15s;
|
||||
}
|
||||
.settings-avatar-upload-btn:hover:not(:disabled) {
|
||||
background: rgba(88, 213, 255, 0.18);
|
||||
border-color: rgba(88, 213, 255, 0.6);
|
||||
}
|
||||
.settings-avatar-upload-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
/* Avatar-Picker */
|
||||
.settings-avatar-wrap {
|
||||
position: relative;
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
border: 2px solid rgba(88, 213, 255, 0.35);
|
||||
background: rgba(88, 213, 255, 0.08);
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
.settings-avatar-wrap:hover {
|
||||
border-color: rgba(88, 213, 255, 0.7);
|
||||
}
|
||||
.settings-avatar-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
.settings-avatar-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32px;
|
||||
line-height: 1;
|
||||
}
|
||||
.settings-avatar-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
.settings-avatar-wrap:hover .settings-avatar-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.settings-eyebrow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -1530,6 +1669,66 @@ input[type="checkbox"] {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.settings-credentials-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.settings-auth-card {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
min-height: 252px;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background: linear-gradient(180deg, rgba(255,255,255,0.03), rgba(255,255,255,0.01));
|
||||
}
|
||||
|
||||
.settings-auth-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.settings-auth-card-header button {
|
||||
min-width: 170px;
|
||||
}
|
||||
|
||||
.settings-auth-card-header h4 {
|
||||
margin: 0;
|
||||
color: #c8d8f2;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.settings-auth-input {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.settings-auth-card .settings-inline-hint {
|
||||
min-height: 34px;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.settings-auth-spacer {
|
||||
min-height: 78px;
|
||||
border: 1px dashed rgba(255, 255, 255, 0.1);
|
||||
border-radius: var(--radius-md);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
padding: 8px;
|
||||
background: rgba(255, 255, 255, 0.01);
|
||||
}
|
||||
|
||||
.settings-connection-tools {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
@@ -1776,6 +1975,56 @@ input[type="checkbox"] {
|
||||
transition: border-color 140ms ease, background 140ms ease, color 140ms ease, transform 140ms ease;
|
||||
}
|
||||
|
||||
.repo-owner-tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.repo-owner-tab {
|
||||
appearance: none;
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
color: var(--text-secondary);
|
||||
border-radius: 999px;
|
||||
min-height: 32px;
|
||||
padding: 0 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.repo-owner-tab span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.repo-owner-tab:hover {
|
||||
border-color: rgba(88, 213, 255, 0.45);
|
||||
color: #d9f7ff;
|
||||
}
|
||||
|
||||
.repo-owner-tab.active {
|
||||
border-color: rgba(88, 213, 255, 0.65);
|
||||
background: linear-gradient(135deg, rgba(88, 213, 255, 0.2), rgba(92, 135, 255, 0.18));
|
||||
color: #ecf8ff;
|
||||
}
|
||||
|
||||
.repo-owner-tab.active span {
|
||||
background: rgba(88, 213, 255, 0.22);
|
||||
color: #dff6ff;
|
||||
}
|
||||
|
||||
.repo-search-clear:hover {
|
||||
border-color: rgba(88, 213, 255, 0.45);
|
||||
background: linear-gradient(180deg, rgba(88, 213, 255, 0.18) 0%, rgba(88, 213, 255, 0.10) 100%);
|
||||
@@ -1959,6 +2208,97 @@ input[type="checkbox"] {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.tags-editor-selected {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
min-height: 38px;
|
||||
padding: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 10px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
.tags-editor-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tags-editor-input {
|
||||
width: 100%;
|
||||
min-height: 38px;
|
||||
padding: 0 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.14);
|
||||
background: rgba(10, 18, 30, 0.95);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.tags-editor-add-btn {
|
||||
min-height: 38px;
|
||||
white-space: nowrap;
|
||||
padding: 0 14px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(88, 213, 255, 0.36);
|
||||
background: linear-gradient(135deg, rgba(88, 213, 255, 0.24), rgba(92, 135, 255, 0.22));
|
||||
color: #c8f5ff;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tags-editor-add-btn:hover {
|
||||
border-color: rgba(88, 213, 255, 0.62);
|
||||
background: linear-gradient(135deg, rgba(88, 213, 255, 0.34), rgba(92, 135, 255, 0.31));
|
||||
}
|
||||
|
||||
.tags-editor-suggestions {
|
||||
margin-top: 8px;
|
||||
max-height: 170px;
|
||||
overflow: auto;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 10px;
|
||||
background: rgba(10, 18, 30, 0.95);
|
||||
}
|
||||
|
||||
.tags-editor-suggestion-item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding: 9px 12px;
|
||||
border: 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background: transparent;
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.tags-editor-suggestion-item:hover {
|
||||
background: rgba(255, 255, 255, 0.07);
|
||||
}
|
||||
|
||||
.tags-editor-suggestions::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
.tags-editor-suggestions::-webkit-scrollbar-track {
|
||||
background: rgba(6, 12, 22, 0.85);
|
||||
border-left: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.tags-editor-suggestions::-webkit-scrollbar-thumb {
|
||||
background: linear-gradient(180deg, rgba(88, 213, 255, 0.45), rgba(92, 135, 255, 0.45));
|
||||
border-radius: 999px;
|
||||
border: 2px solid rgba(6, 12, 22, 0.9);
|
||||
}
|
||||
|
||||
.tags-editor-suggestions::-webkit-scrollbar-thumb:hover {
|
||||
background: linear-gradient(180deg, rgba(88, 213, 255, 0.68), rgba(92, 135, 255, 0.68));
|
||||
}
|
||||
|
||||
.activity-heatmap-months {
|
||||
margin-left: calc(var(--hm-weekday-col) + var(--hm-grid-gap));
|
||||
width: max-content;
|
||||
@@ -3165,6 +3505,14 @@ progress::-moz-progress-bar {
|
||||
.settings-column {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.settings-credentials-grid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.settings-auth-card {
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
@@ -3179,6 +3527,7 @@ progress::-moz-progress-bar {
|
||||
}
|
||||
|
||||
.settings-fields-grid,
|
||||
.settings-credentials-grid,
|
||||
.settings-connection-tools,
|
||||
.settings-version-card,
|
||||
.modal-buttons {
|
||||
@@ -3186,7 +3535,8 @@ progress::-moz-progress-bar {
|
||||
}
|
||||
|
||||
.settings-update-btn,
|
||||
#btnTestGiteaConnection {
|
||||
#btnTestGiteaConnection,
|
||||
#btnTestGithubConnection {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -3293,412 +3643,3 @@ body.compact-mode .file-type-badge {
|
||||
.fav-chip[draggable="true"] {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════
|
||||
✅ BACKUP MANAGEMENT MODAL - STYLES
|
||||
═══════════════════════════════════════════════════════════ */
|
||||
|
||||
#backupManagementModal {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
z-index: 99999;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
#backupManagementModal.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.backup-management-card {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid rgba(88, 213, 255, 0.2);
|
||||
border-radius: var(--radius-lg);
|
||||
width: 90%;
|
||||
max-width: 700px;
|
||||
max-height: 85vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
|
||||
animation: slideUp 0.3s ease-out;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.backup-modal-header {
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.backup-modal-header h2 {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.backup-modal-close {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 6px;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.backup-modal-close:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.backup-modal-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.backup-modal-body::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
.backup-modal-body::-webkit-scrollbar-track {
|
||||
background: rgba(9, 20, 39, 0.75);
|
||||
border-left: 1px solid rgba(88, 213, 255, 0.12);
|
||||
}
|
||||
|
||||
.backup-modal-body::-webkit-scrollbar-thumb {
|
||||
background: linear-gradient(180deg, rgba(88, 213, 255, 0.55), rgba(92, 135, 255, 0.55));
|
||||
border-radius: 10px;
|
||||
border: 2px solid rgba(9, 20, 39, 0.9);
|
||||
}
|
||||
|
||||
.backup-modal-body::-webkit-scrollbar-thumb:hover {
|
||||
background: linear-gradient(180deg, rgba(88, 213, 255, 0.8), rgba(92, 135, 255, 0.8));
|
||||
}
|
||||
|
||||
/* Provider Selection */
|
||||
.backup-provider-select {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.backup-provider-select label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
#backupProviderSelect {
|
||||
padding: 10px 12px;
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid rgba(88, 213, 255, 0.25);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--text-primary);
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
#backupProviderSelect:hover {
|
||||
border-color: rgba(88, 213, 255, 0.4);
|
||||
background: rgba(19, 33, 58, 0.8);
|
||||
}
|
||||
|
||||
#backupProviderSelect:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-primary);
|
||||
box-shadow: 0 0 0 3px rgba(88, 213, 255, 0.15);
|
||||
background: rgba(19, 33, 58, 0.9);
|
||||
}
|
||||
|
||||
#backupProviderSelect option {
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
/* Credentials Sections */
|
||||
.backup-credentials-section {
|
||||
display: none;
|
||||
padding: 12px;
|
||||
background: linear-gradient(135deg, rgba(88, 213, 255, 0.08) 0%, rgba(122, 81, 255, 0.05) 100%);
|
||||
border: 1px solid rgba(88, 213, 255, 0.2);
|
||||
border-radius: var(--radius-md);
|
||||
gap: 12px;
|
||||
flex-direction: column;
|
||||
animation: slideDown 0.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.backup-credentials-section.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.backup-credentials-section input,
|
||||
.backup-credentials-section textarea,
|
||||
.backup-credentials-section select {
|
||||
padding: 9px 10px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 6px;
|
||||
color: var(--text-primary);
|
||||
font-size: 12px;
|
||||
font-family: inherit;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.backup-credentials-section input::placeholder {
|
||||
color: rgba(174, 189, 216, 0.35);
|
||||
}
|
||||
|
||||
.backup-credentials-section input:focus,
|
||||
.backup-credentials-section textarea:focus,
|
||||
.backup-credentials-section select:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-primary);
|
||||
box-shadow: 0 0 0 3px rgba(88, 213, 255, 0.12);
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.backup-credentials-section select option {
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.backup-input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.backup-input-group label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.backup-modal-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.backup-btn {
|
||||
padding: 10px 16px;
|
||||
border-radius: var(--radius-md);
|
||||
border: none;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
min-width: 120px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.backup-btn-primary {
|
||||
background: var(--accent-gradient);
|
||||
color: #000;
|
||||
box-shadow: 0 4px 12px rgba(88, 213, 255, 0.25);
|
||||
}
|
||||
|
||||
.backup-btn-primary:hover {
|
||||
box-shadow: 0 8px 20px rgba(88, 213, 255, 0.35);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.backup-btn-primary:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 8px rgba(88, 213, 255, 0.25);
|
||||
}
|
||||
|
||||
.backup-btn-secondary {
|
||||
background: transparent;
|
||||
border: 1.5px solid rgba(88, 213, 255, 0.35);
|
||||
color: var(--text-primary);
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.backup-btn-secondary:hover {
|
||||
background: rgba(88, 213, 255, 0.12);
|
||||
border-color: var(--accent-primary);
|
||||
color: var(--accent-primary);
|
||||
box-shadow: 0 4px 12px rgba(88, 213, 255, 0.15);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.backup-btn-secondary:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.backup-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
/* Backup List */
|
||||
#backupListContainer {
|
||||
background: linear-gradient(135deg, rgba(0, 0, 0, 0.3) 0%, rgba(0, 0, 0, 0.15) 100%);
|
||||
border: 1px solid rgba(88, 213, 255, 0.15);
|
||||
border-radius: var(--radius-md);
|
||||
max-height: 250px;
|
||||
overflow-y: auto;
|
||||
min-height: 100px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.backup-list-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
transition: background var(--transition-fast);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.backup-list-info {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.backup-list-name {
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.backup-list-meta {
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.backup-list-actions {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.backup-list-action {
|
||||
appearance: none;
|
||||
border-radius: 8px;
|
||||
padding: 7px 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
border: 1px solid rgba(88, 213, 255, 0.28);
|
||||
background: linear-gradient(180deg, rgba(20, 34, 58, 0.92), rgba(12, 24, 44, 0.92));
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.backup-list-action:hover {
|
||||
border-color: rgba(88, 213, 255, 0.6);
|
||||
background: linear-gradient(180deg, rgba(30, 52, 86, 0.94), rgba(15, 30, 56, 0.94));
|
||||
}
|
||||
|
||||
.backup-list-action--delete {
|
||||
color: #ff9f9f;
|
||||
border-color: rgba(239, 68, 68, 0.35);
|
||||
}
|
||||
|
||||
.backup-list-action--delete:hover {
|
||||
color: #ffd6d6;
|
||||
border-color: rgba(239, 68, 68, 0.65);
|
||||
background: linear-gradient(180deg, rgba(72, 24, 24, 0.88), rgba(48, 14, 14, 0.9));
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.backup-list-item {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.backup-list-actions {
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.backup-list-action {
|
||||
flex: 1 1 auto;
|
||||
text-align: center;
|
||||
min-width: 130px;
|
||||
}
|
||||
}
|
||||
|
||||
.backup-list-item:hover {
|
||||
background: rgba(88, 213, 255, 0.1);
|
||||
}
|
||||
|
||||
.backup-list-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* Scrollbar Styling */
|
||||
#backupListContainer::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
#backupListContainer::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
#backupListContainer::-webkit-scrollbar-thumb {
|
||||
background: rgba(88, 213, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
#backupListContainer::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(88, 213, 255, 0.4);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
293
src/utils/helpers.js
Normal file
293
src/utils/helpers.js
Normal file
@@ -0,0 +1,293 @@
|
||||
/**
|
||||
* Gemeinsame Utility-Funktionen für Git Manager GUI
|
||||
* - Branch Handling
|
||||
* - API Error Handling
|
||||
* - Standardisiertes Logging
|
||||
* - Caching
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const ppath = require('path');
|
||||
|
||||
// ===== LOGGING SYSTEM =====
|
||||
const LOG_LEVELS = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3 };
|
||||
let currentLogLevel = process.env.NODE_ENV === 'production' ? LOG_LEVELS.INFO : LOG_LEVELS.DEBUG;
|
||||
let logQueue = [];
|
||||
const MAX_LOG_BUFFER = 100;
|
||||
|
||||
function formatLog(level, context, message, details = null) {
|
||||
const timestamp = new Date().toISOString();
|
||||
const levelStr = Object.keys(LOG_LEVELS).find(k => LOG_LEVELS[k] === level);
|
||||
return {
|
||||
timestamp,
|
||||
level: levelStr,
|
||||
context,
|
||||
message,
|
||||
details,
|
||||
pid: process.pid
|
||||
};
|
||||
}
|
||||
|
||||
function writeLog(logEntry) {
|
||||
logQueue.push(logEntry);
|
||||
if (logQueue.length > MAX_LOG_BUFFER) {
|
||||
logQueue.shift();
|
||||
}
|
||||
|
||||
// Auch in Console schreiben
|
||||
const { level, timestamp, context, message, details } = logEntry;
|
||||
const prefix = `[${timestamp}] [${level}] [${context}]`;
|
||||
|
||||
if (level === 'ERROR' && details?.error) {
|
||||
console.error(prefix, message, details.error);
|
||||
} else if (level === 'WARN') {
|
||||
console.warn(prefix, message, details ? JSON.stringify(details) : '');
|
||||
} else if (level !== 'DEBUG' || process.env.DEBUG) {
|
||||
console.log(prefix, message, details ? JSON.stringify(details) : '');
|
||||
}
|
||||
}
|
||||
|
||||
const logger = {
|
||||
debug: (context, message, details) => writeLog(formatLog(LOG_LEVELS.DEBUG, context, message, details)),
|
||||
info: (context, message, details) => writeLog(formatLog(LOG_LEVELS.INFO, context, message, details)),
|
||||
warn: (context, message, details) => writeLog(formatLog(LOG_LEVELS.WARN, context, message, details)),
|
||||
error: (context, message, details) => writeLog(formatLog(LOG_LEVELS.ERROR, context, message, details)),
|
||||
getRecent: (count = 20) => logQueue.slice(-count),
|
||||
setLevel: (level) => { currentLogLevel = LOG_LEVELS[level] || LOG_LEVELS.INFO; }
|
||||
};
|
||||
|
||||
// ===== BRANCH HANDLING =====
|
||||
const BRANCH_DEFAULTS = {
|
||||
gitea: 'main',
|
||||
github: 'main'
|
||||
};
|
||||
|
||||
function normalizeBranch(branch = 'HEAD', platform = 'gitea') {
|
||||
const value = String(branch || '').trim();
|
||||
|
||||
// HEAD sollte immer zu Standard konvertiert werden
|
||||
if (value.toLowerCase() === 'head') {
|
||||
return BRANCH_DEFAULTS[platform] || 'main';
|
||||
}
|
||||
|
||||
// Validierung: nur sichere Git-Referenzen
|
||||
if (/^[a-zA-Z0-9._\-/]+$/.test(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return BRANCH_DEFAULTS[platform] || 'main';
|
||||
}
|
||||
|
||||
function isSafeBranch(branch) {
|
||||
return /^[a-zA-Z0-9._\-/]+$/.test(String(branch || ''));
|
||||
}
|
||||
|
||||
// ===== ERROR HANDLING =====
|
||||
const ERROR_CODES = {
|
||||
NETWORK: 'NETWORK_ERROR',
|
||||
AUTH_FAILED: 'AUTH_FAILED',
|
||||
NOT_FOUND: 'NOT_FOUND',
|
||||
VALIDATION: 'VALIDATION_ERROR',
|
||||
RATE_LIMIT: 'RATE_LIMIT',
|
||||
SERVER_ERROR: 'SERVER_ERROR',
|
||||
UNKNOWN: 'UNKNOWN_ERROR'
|
||||
};
|
||||
|
||||
function parseApiError(error, defaultCode = ERROR_CODES.UNKNOWN) {
|
||||
if (!error) {
|
||||
return { code: defaultCode, message: 'Unknown error', statusCode: null };
|
||||
}
|
||||
|
||||
// Axios-style error
|
||||
if (error.response) {
|
||||
const status = error.response.status;
|
||||
const data = error.response.data;
|
||||
let code = defaultCode;
|
||||
|
||||
if (status === 401 || status === 403) {
|
||||
code = ERROR_CODES.AUTH_FAILED;
|
||||
} else if (status === 404) {
|
||||
code = ERROR_CODES.NOT_FOUND;
|
||||
} else if (status === 429) {
|
||||
code = ERROR_CODES.RATE_LIMIT;
|
||||
} else if (status >= 500) {
|
||||
code = ERROR_CODES.SERVER_ERROR;
|
||||
}
|
||||
|
||||
return {
|
||||
code,
|
||||
message: data?.message || error.message || `HTTP ${status}`,
|
||||
statusCode: status,
|
||||
rawMessage: data?.message
|
||||
};
|
||||
}
|
||||
|
||||
// Network error
|
||||
if (error.message?.includes('timeout') || error.code?.includes('TIMEOUT')) {
|
||||
return { code: ERROR_CODES.NETWORK, message: 'Request timeout', statusCode: null };
|
||||
}
|
||||
|
||||
if (error.code?.includes('ECONNREFUSED') || error.message?.includes('ECONNREFUSED')) {
|
||||
return { code: ERROR_CODES.NETWORK, message: 'Connection refused', statusCode: null };
|
||||
}
|
||||
|
||||
return {
|
||||
code: ERROR_CODES.UNKNOWN,
|
||||
message: error.message || String(error),
|
||||
statusCode: null
|
||||
};
|
||||
}
|
||||
|
||||
function formatErrorForUser(error, context = 'Operation') {
|
||||
const parsed = parseApiError(error);
|
||||
const messages = {
|
||||
[ERROR_CODES.AUTH_FAILED]: `Authentifizierung fehlgeschlagen. Bitte Token überprüfen.`,
|
||||
[ERROR_CODES.NOT_FOUND]: `Ressource nicht gefunden.`,
|
||||
[ERROR_CODES.NETWORK]: `Netzwerkfehler. Bitte Verbindung überprüfen.`,
|
||||
[ERROR_CODES.RATE_LIMIT]: `Zu viele Anfragen. Bitte später versuchen.`,
|
||||
[ERROR_CODES.SERVER_ERROR]: `Server-Fehler. Bitte später versuchen.`,
|
||||
[ERROR_CODES.UNKNOWN]: `${context} fehlgeschlagen.`
|
||||
};
|
||||
|
||||
return {
|
||||
userMessage: messages[parsed.code],
|
||||
technicalMessage: parsed.message,
|
||||
code: parsed.code,
|
||||
details: parsed
|
||||
};
|
||||
}
|
||||
|
||||
// ===== CACHING SYSTEM =====
|
||||
class Cache {
|
||||
constructor(ttl = 300000) { // 5 min default
|
||||
this.store = new Map();
|
||||
this.ttl = ttl;
|
||||
}
|
||||
|
||||
set(key, value, customTtl = null) {
|
||||
const expiry = Date.now() + (customTtl || this.ttl);
|
||||
this.store.set(key, { value, expiry });
|
||||
}
|
||||
|
||||
get(key) {
|
||||
const item = this.store.get(key);
|
||||
if (!item) return null;
|
||||
if (Date.now() > item.expiry) {
|
||||
this.store.delete(key);
|
||||
return null;
|
||||
}
|
||||
return item.value;
|
||||
}
|
||||
|
||||
invalidate(keyPattern) {
|
||||
for (const [key] of this.store) {
|
||||
if (key.includes(keyPattern)) {
|
||||
this.store.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.store.clear();
|
||||
}
|
||||
|
||||
size() {
|
||||
return this.store.size;
|
||||
}
|
||||
}
|
||||
|
||||
// Standard Caches
|
||||
const caches = {
|
||||
repos: new Cache(600000), // 10 min
|
||||
fileTree: new Cache(300000), // 5 min
|
||||
api: new Cache(120000) // 2 min
|
||||
};
|
||||
|
||||
// ===== PARALLEL OPERATIONS =====
|
||||
async function runParallel(operations, concurrency = 4, onProgress = null) {
|
||||
const results = new Array(operations.length);
|
||||
let completed = 0;
|
||||
let index = 0;
|
||||
|
||||
async function worker() {
|
||||
while (index < operations.length) {
|
||||
const i = index++;
|
||||
try {
|
||||
results[i] = { ok: true, result: await operations[i]() };
|
||||
} catch (e) {
|
||||
results[i] = { ok: false, error: e };
|
||||
}
|
||||
completed++;
|
||||
if (onProgress) {
|
||||
try { onProgress(completed, operations.length); } catch (_) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const workers = Array.from({ length: Math.min(concurrency, operations.length) }, () => worker());
|
||||
await Promise.all(workers);
|
||||
return results;
|
||||
}
|
||||
|
||||
// ===== RETRY LOGIC =====
|
||||
async function retryWithBackoff(fn, maxAttempts = 3, baseDelay = 1000) {
|
||||
let lastError;
|
||||
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (e) {
|
||||
lastError = e;
|
||||
if (attempt < maxAttempts - 1) {
|
||||
const delay = baseDelay * Math.pow(2, attempt);
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
}
|
||||
}
|
||||
}
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
// ===== FILE OPERATIONS =====
|
||||
function ensureDirectory(dirPath) {
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
function safeReadFile(filePath, defaultValue = null) {
|
||||
try {
|
||||
if (!fs.existsSync(filePath)) return defaultValue;
|
||||
return fs.readFileSync(filePath, 'utf8');
|
||||
} catch (e) {
|
||||
logger.warn('safeReadFile', `Failed to read ${filePath}`, { error: e.message });
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
function safeWriteFile(filePath, content) {
|
||||
try {
|
||||
ensureDirectory(ppath.dirname(filePath));
|
||||
fs.writeFileSync(filePath, content, 'utf8');
|
||||
return true;
|
||||
} catch (e) {
|
||||
logger.error('safeWriteFile', `Failed to write ${filePath}`, { error: e.message });
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== EXPORTS =====
|
||||
module.exports = {
|
||||
logger,
|
||||
normalizeBranch,
|
||||
isSafeBranch,
|
||||
parseApiError,
|
||||
formatErrorForUser,
|
||||
ERROR_CODES,
|
||||
Cache,
|
||||
caches,
|
||||
runParallel,
|
||||
retryWithBackoff,
|
||||
ensureDirectory,
|
||||
safeReadFile,
|
||||
safeWriteFile,
|
||||
LOG_LEVELS
|
||||
};
|
||||
182
updater.js
182
updater.js
@@ -3,9 +3,11 @@ const { app, shell } = require('electron');
|
||||
const https = require('https');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const { spawn } = require('child_process');
|
||||
|
||||
const GITEA_API_URL = 'https://git.viper.ipv64.net/api/v1/repos/M_Viper/Git-Manager-Gui/releases';
|
||||
const TRUSTED_UPDATE_HOST = 'git.viper.ipv64.net';
|
||||
|
||||
class Updater {
|
||||
constructor(mainWindow) {
|
||||
@@ -31,15 +33,28 @@ class Updater {
|
||||
console.log(`[Updater] Version-Check: Server(${serverVer}) vs Lokal(${localVer})`);
|
||||
|
||||
if (this.compareVersions(serverVer, localVer) > 0) {
|
||||
const asset = this.findAsset(latestRelease.assets);
|
||||
const checksumAsset = this.findChecksumAsset(latestRelease.assets, asset);
|
||||
const expectedSha256 = this.extractChecksumFromReleaseBody(latestRelease.body, asset?.name);
|
||||
|
||||
console.log("[Updater] Update verfügbar. Sende Daten an Renderer...");
|
||||
this.mainWindow.webContents.send('update-available', {
|
||||
version: serverVer,
|
||||
body: latestRelease.body,
|
||||
url: latestRelease.html_url,
|
||||
asset: this.findAsset(latestRelease.assets)
|
||||
asset: asset ? {
|
||||
...asset,
|
||||
checksumUrl: checksumAsset ? checksumAsset.browser_download_url : null,
|
||||
expectedSha256
|
||||
} : null
|
||||
});
|
||||
} else {
|
||||
console.log("[Updater] Anwendung ist auf dem neuesten Stand.");
|
||||
if (!silent) {
|
||||
this.mainWindow.webContents.send('update-not-available', {
|
||||
version: localVer
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Updater] Fehler beim Update-Check:', error);
|
||||
@@ -77,7 +92,118 @@ class Updater {
|
||||
findAsset(assets) {
|
||||
if (!assets) return null;
|
||||
const ext = process.platform === 'win32' ? '.exe' : '.AppImage';
|
||||
return assets.find(a => a.name.toLowerCase().endsWith(ext));
|
||||
return assets.find(a => {
|
||||
const name = String(a?.name || '').toLowerCase();
|
||||
// Leerzeichen im Namen erlauben!
|
||||
const validName = /^[a-z0-9._\- ]+$/i.test(name);
|
||||
return validName && name.endsWith(ext);
|
||||
});
|
||||
}
|
||||
|
||||
findChecksumAsset(assets, targetAsset) {
|
||||
if (!Array.isArray(assets) || !targetAsset?.name) return null;
|
||||
const targetLower = String(targetAsset.name).toLowerCase();
|
||||
|
||||
const exactCandidates = [
|
||||
`${targetLower}.sha256`,
|
||||
`${targetLower}.sha256sum`,
|
||||
`${targetLower}.sha512`,
|
||||
`${targetLower}.sha512sum`
|
||||
];
|
||||
|
||||
const exact = assets.find(a => exactCandidates.includes(String(a?.name || '').toLowerCase()));
|
||||
if (exact) return exact;
|
||||
|
||||
return assets.find(a => {
|
||||
const name = String(a?.name || '').toLowerCase();
|
||||
return name.includes('checksum') || name.includes('checksums') || name.endsWith('.sha256') || name.endsWith('.sha256sum');
|
||||
}) || null;
|
||||
}
|
||||
|
||||
extractChecksumFromReleaseBody(body, fileName) {
|
||||
const text = String(body || '');
|
||||
const target = String(fileName || '').trim();
|
||||
if (!text || !target) return null;
|
||||
|
||||
const lines = text.split(/\r?\n/);
|
||||
const targetLower = target.toLowerCase();
|
||||
for (const line of lines) {
|
||||
const normalized = String(line || '').trim();
|
||||
if (!normalized) continue;
|
||||
const match = normalized.match(/\b([a-fA-F0-9]{64})\b/);
|
||||
if (!match) continue;
|
||||
if (normalized.toLowerCase().includes(targetLower)) {
|
||||
return match[1].toLowerCase();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
downloadText(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.isTrustedDownloadUrl(url)) {
|
||||
reject(new Error('Unsichere Checksum-URL blockiert.'));
|
||||
return;
|
||||
}
|
||||
|
||||
https.get(url, { headers: { 'User-Agent': 'Electron-Updater' } }, (res) => {
|
||||
if (res.statusCode === 301 || res.statusCode === 302) {
|
||||
return resolve(this.downloadText(res.headers.location || ''));
|
||||
}
|
||||
if (res.statusCode !== 200) {
|
||||
reject(new Error(`Checksum-Download fehlgeschlagen: HTTP ${res.statusCode}`));
|
||||
return;
|
||||
}
|
||||
let data = '';
|
||||
res.on('data', chunk => data += chunk.toString('utf8'));
|
||||
res.on('end', () => resolve(data));
|
||||
}).on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
extractChecksumFromText(text, fileName) {
|
||||
const lines = String(text || '').split(/\r?\n/);
|
||||
const targetLower = String(fileName || '').toLowerCase();
|
||||
for (const line of lines) {
|
||||
const normalized = String(line || '').trim();
|
||||
if (!normalized) continue;
|
||||
const hashMatch = normalized.match(/\b([a-fA-F0-9]{64})\b/);
|
||||
if (!hashMatch) continue;
|
||||
if (!targetLower || normalized.toLowerCase().includes(targetLower)) {
|
||||
return hashMatch[1].toLowerCase();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
computeFileSha256(filePath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const hash = crypto.createHash('sha256');
|
||||
const stream = fs.createReadStream(filePath);
|
||||
stream.on('error', reject);
|
||||
stream.on('data', chunk => hash.update(chunk));
|
||||
stream.on('end', () => resolve(hash.digest('hex').toLowerCase()));
|
||||
});
|
||||
}
|
||||
|
||||
async resolveExpectedSha256(asset) {
|
||||
const expectedFromAsset = String(asset?.expectedSha256 || '').trim().toLowerCase();
|
||||
if (/^[a-f0-9]{64}$/.test(expectedFromAsset)) return expectedFromAsset;
|
||||
|
||||
const checksumUrl = String(asset?.checksumUrl || '').trim();
|
||||
if (!checksumUrl) return null;
|
||||
|
||||
const checksumText = await this.downloadText(checksumUrl);
|
||||
return this.extractChecksumFromText(checksumText, asset?.name);
|
||||
}
|
||||
|
||||
isTrustedDownloadUrl(rawUrl) {
|
||||
try {
|
||||
const parsed = new URL(String(rawUrl || ''));
|
||||
return parsed.protocol === 'https:' && parsed.hostname === TRUSTED_UPDATE_HOST;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,16 +215,40 @@ class Updater {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isTrustedDownloadUrl(asset.browser_download_url)) {
|
||||
console.error('[Updater] Unsichere Download-URL blockiert.');
|
||||
return;
|
||||
}
|
||||
|
||||
const tempPath = path.join(app.getPath('temp'), asset.name);
|
||||
console.log(`[Updater] Download gestartet: ${asset.name} -> ${tempPath}`);
|
||||
|
||||
let expectedSha256 = null;
|
||||
try {
|
||||
expectedSha256 = await this.resolveExpectedSha256(asset);
|
||||
} catch (e) {
|
||||
console.error('[Updater] Konnte erwartete Checksumme nicht laden:', e?.message || e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!expectedSha256) {
|
||||
console.error('[Updater] Kein SHA-256-Checksum-Wert gefunden. Update wurde aus Sicherheitsgruenden blockiert.');
|
||||
return;
|
||||
}
|
||||
|
||||
const file = fs.createWriteStream(tempPath);
|
||||
|
||||
const download = (url) => {
|
||||
if (!this.isTrustedDownloadUrl(url)) {
|
||||
console.error('[Updater] Unsicherer Redirect/Download blockiert.');
|
||||
fs.unlink(tempPath, () => {});
|
||||
return;
|
||||
}
|
||||
|
||||
https.get(url, { headers: { 'User-Agent': 'Electron-Updater' } }, (res) => {
|
||||
// Handle Redirects
|
||||
if (res.statusCode === 301 || res.statusCode === 302) {
|
||||
return download(res.headers.location);
|
||||
return download(res.headers.location || '');
|
||||
}
|
||||
|
||||
if (res.statusCode !== 200) {
|
||||
@@ -108,9 +258,22 @@ class Updater {
|
||||
|
||||
res.pipe(file);
|
||||
|
||||
file.on('finish', () => {
|
||||
file.on('finish', async () => {
|
||||
file.close();
|
||||
console.log("[Updater] Download abgeschlossen. Initialisiere entkoppelten Installer...");
|
||||
try {
|
||||
const actualSha256 = await this.computeFileSha256(tempPath);
|
||||
if (actualSha256 !== expectedSha256) {
|
||||
console.error('[Updater] Checksum-Validierung fehlgeschlagen. Installation wurde blockiert.');
|
||||
fs.unlink(tempPath, () => {});
|
||||
return;
|
||||
}
|
||||
} catch (verifyErr) {
|
||||
console.error('[Updater] Checksum-Validierung konnte nicht ausgeführt werden:', verifyErr?.message || verifyErr);
|
||||
fs.unlink(tempPath, () => {});
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("[Updater] Download und Checksum-Validierung abgeschlossen. Initialisiere entkoppelten Installer...");
|
||||
this.installAndQuit(tempPath);
|
||||
});
|
||||
}).on('error', (err) => {
|
||||
@@ -129,15 +292,14 @@ class Updater {
|
||||
console.log(`[Updater] Bereite Installation vor: ${filePath}`);
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
// Wir nutzen spawn mit detached: true, damit der Installer weiterläuft,
|
||||
// wenn der Hauptprozess (Electron) beendet wird.
|
||||
try {
|
||||
const child = spawn('cmd.exe', ['/c', 'start', '""', filePath], {
|
||||
const child = spawn(filePath, [], {
|
||||
detached: true,
|
||||
stdio: 'ignore'
|
||||
stdio: 'ignore',
|
||||
shell: false
|
||||
});
|
||||
|
||||
child.unref(); // Trennt die Referenz zum Installer
|
||||
child.unref();
|
||||
|
||||
console.log("[Updater] Installer-Prozess entkoppelt gestartet. App schließt in 2 Sek...");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user