Dieser Commit ist enthalten in:
Claude Project Manager
2025-07-12 22:29:41 +02:00
Commit d9c9942197
40 geänderte Dateien mit 3361 neuen und 0 gelöschten Zeilen

12
.claude/settings.local.json Normale Datei
Datei anzeigen

@ -0,0 +1,12 @@
{
"permissions": {
"allow": [
"Bash(mkdir:*)",
"Bash(cp:*)",
"Bash(python:*)",
"Bash(find:*)",
"Bash(rm:*)"
],
"deny": []
}
}

78
CLAUDE_PROJECT_README.md Normale Datei
Datei anzeigen

@ -0,0 +1,78 @@
# Toolbox-Webseiten-Crawler
*This README was automatically generated by Claude Project Manager*
## Project Overview
- **Path**: `C:/Users/hendr/Desktop/IntelSight/Projektablage/Toolbox-Webseiten-Crawler`
- **Files**: 23 files
- **Size**: 98.9 KB
- **Last Modified**: 2025-07-12 20:11
## Technology Stack
### Languages
- Batch
- Python
## Project Structure
```
CLAUDE_PROJECT_README.md
install_dependencies.bat
main.py
requirements.txt
start.bat
src/
├── __init__.py
├── core/
│ ├── web_crawler.py
│ └── __init__.py
├── resources/
│ ├── icons/
│ │ ├── check.svg
│ │ ├── download.svg
│ │ ├── folder.svg
│ │ ├── gear.svg
│ │ ├── globe.svg
│ │ ├── moon.svg
│ │ └── sun.svg
│ └── styles/
│ ├── dark_theme.py
│ └── light_theme.py
├── ui/
│ ├── custom_widgets.py
│ ├── main_window.py
│ └── __init__.py
└── utils/
├── local_server.py
├── pdf_report.py
└── __init__.py
```
## Key Files
- `requirements.txt`
## Claude Integration
This project is managed with Claude Project Manager. To work with this project:
1. Open Claude Project Manager
2. Click on this project's tile
3. Claude will open in the project directory
## Notes
*Add your project-specific notes here*
---
## Development Log
- README generated on 2025-07-11 21:27:29
- README updated on 2025-07-11 21:27:36
- README updated on 2025-07-12 12:18:47
- README updated on 2025-07-12 20:10:48
- README updated on 2025-07-12 20:11:07
- README updated on 2025-07-12 20:11:21

6
install_dependencies.bat Normale Datei
Datei anzeigen

@ -0,0 +1,6 @@
@echo off
echo Installing dependencies for IntelSight Webseiten-Crawler...
pip install -r requirements.txt
echo.
echo Installation complete!
pause

37
main.py Normale Datei
Datei anzeigen

@ -0,0 +1,37 @@
import sys
import os
from PyQt6.QtWidgets import QApplication
from PyQt6.QtGui import QIcon, QFontDatabase
from PyQt6.QtCore import Qt
# Füge src zum Python-Pfad hinzu
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
from ui.main_window import WebsiteCrawlerWindow
def main():
# High DPI Support
if hasattr(Qt.HighDpiScaleFactorRoundingPolicy, 'PassThrough'):
QApplication.setHighDpiScaleFactorRoundingPolicy(
Qt.HighDpiScaleFactorRoundingPolicy.PassThrough
)
app = QApplication(sys.argv)
app.setApplicationName("IntelSight Webseiten-Crawler")
app.setOrganizationName("IntelSight")
# Setze App-Icon wenn vorhanden
icon_path = os.path.join(os.path.dirname(__file__), 'src', 'resources', 'icons', 'globe.svg')
if os.path.exists(icon_path):
app.setWindowIcon(QIcon(icon_path))
# Hauptfenster erstellen und anzeigen
window = WebsiteCrawlerWindow()
window.show()
sys.exit(app.exec())
if __name__ == '__main__':
main()

12
requirements.txt Normale Datei
Datei anzeigen

@ -0,0 +1,12 @@
PyQt6==6.6.1
PyQt6-Qt6==6.6.1
PyQt6-sip==13.6.0
beautifulsoup4==4.12.3
requests==2.31.0
lxml==5.1.0
selenium==4.18.1
wget==3.2
urllib3==2.2.1
pywebcopy==7.0.2
reportlab==4.0.9
chardet==5.2.0

0
src/__init__.py Normale Datei
Datei anzeigen

0
src/core/__init__.py Normale Datei
Datei anzeigen

Binäre Datei nicht angezeigt.

Binäre Datei nicht angezeigt.

1027
src/core/web_crawler.py Normale Datei

Datei-Diff unterdrückt, da er zu groß ist Diff laden

Datei anzeigen

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>

Nachher

Breite:  |  Höhe:  |  Größe: 213 B

Datei anzeigen

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17 17H17.01M17.4 14H18C18.9319 14 19.3978 14 19.7654 14.1522C20.2554 14.3552 20.6448 14.7446 20.8478 15.2346C21 15.6022 21 16.0681 21 17C21 17.9319 21 18.3978 20.8478 18.7654C20.6448 19.2554 20.2554 19.6448 19.7654 19.8478C19.3978 20 18.9319 20 18 20H6C5.06812 20 4.60218 20 4.23463 19.8478C3.74458 19.6448 3.35523 19.2554 3.15224 18.7654C3 18.3978 3 17.9319 3 17C3 16.0681 3 15.6022 3.15224 15.2346C3.35523 14.7446 3.74458 14.3552 4.23463 14.1522C4.60218 14 5.06812 14 6 14H6.6M12 15V4M12 15L9 12M12 15L15 12" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Nachher

Breite:  |  Höhe:  |  Größe: 831 B

Datei anzeigen

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 8.2C3 7.07989 3 6.51984 3.21799 6.09202C3.40973 5.71569 3.71569 5.40973 4.09202 5.21799C4.51984 5 5.0799 5 6.2 5H9.67452C10.1637 5 10.4083 5 10.6385 5.05526C10.8425 5.10425 11.0376 5.18506 11.2166 5.29472C11.4184 5.4184 11.5914 5.59135 11.9373 5.93726L12.0627 6.06274C12.4086 6.40865 12.5816 6.5816 12.7834 6.70528C12.9624 6.81494 13.1575 6.89575 13.3615 6.94474C13.5917 7 13.8363 7 14.3255 7H17.8C18.9201 7 19.4802 7 19.908 7.21799C20.2843 7.40973 20.5903 7.71569 20.782 8.09202C21 8.51984 21 9.0799 21 10.2V15.8C21 16.9201 21 17.4802 20.782 17.908C20.5903 18.2843 20.2843 18.5903 19.908 18.782C19.4802 19 18.9201 19 17.8 19H6.2C5.07989 19 4.51984 19 4.09202 18.782C3.71569 18.5903 3.40973 18.2843 3.21799 17.908C3 17.4802 3 16.9201 3 15.8V8.2Z" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Nachher

Breite:  |  Höhe:  |  Größe: 1.0 KiB

Datei anzeigen

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15 12C15 13.6569 13.6569 15 12 15C10.3431 15 9 13.6569 9 12C9 10.3431 10.3431 9 12 9C13.6569 9 15 10.3431 15 12Z" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.9046 3.06005C12.6988 3 12.4659 3 12 3C11.5341 3 11.3012 3 11.0954 3.06005C10.7942 3.14794 10.5281 3.32808 10.3346 3.57511C10.2024 3.74388 10.1159 3.96016 9.94291 4.39272C9.69419 5.01452 9.00393 5.33471 8.36857 5.123L7.79779 4.93281C7.3929 4.79785 7.19045 4.73036 6.99196 4.7188C6.70039 4.70181 6.4102 4.77032 6.15701 4.9159C5.98465 5.01501 5.83376 5.16591 5.53197 5.4677C5.21122 5.78845 5.05084 5.94882 4.94896 6.13189C4.79927 6.40084 4.73595 6.70934 4.76759 7.01551C4.78912 7.2239 4.87335 7.43449 5.04182 7.85566C5.30565 8.51523 5.05184 9.26878 4.44272 9.63433L4.16521 9.80087C3.74031 10.0558 3.52786 10.1833 3.37354 10.3588C3.23698 10.5141 3.13401 10.696 3.07109 10.893C3 11.1156 3 11.3658 3 11.8663C3 12.4589 3 12.7551 3.09462 13.0088C3.17823 13.2329 3.31422 13.4337 3.49124 13.5946C3.69158 13.7766 3.96395 13.8856 4.50866 14.1035C5.06534 14.3261 5.35196 14.9441 5.16236 15.5129L4.94721 16.1584C4.79819 16.6054 4.72367 16.829 4.7169 17.0486C4.70875 17.3127 4.77049 17.5742 4.89587 17.8067C5.00015 18.0002 5.16678 18.1668 5.5 18.5C5.83323 18.8332 5.99985 18.9998 6.19325 19.1041C6.4258 19.2295 6.68733 19.2913 6.9514 19.2831C7.17102 19.2763 7.39456 19.2018 7.84164 19.0528L8.36862 18.8771C9.00393 18.6654 9.6942 18.9855 9.94291 19.6073C10.1159 20.0398 10.2024 20.2561 10.3346 20.4249C10.5281 20.6719 10.7942 20.8521 11.0954 20.94C11.3012 21 11.5341 21 12 21C12.4659 21 12.6988 21 12.9046 20.94C13.2058 20.8521 13.4719 20.6719 13.6654 20.4249C13.7976 20.2561 13.8841 20.0398 14.0571 19.6073C14.3058 18.9855 14.9961 18.6654 15.6313 18.8773L16.1579 19.0529C16.605 19.2019 16.8286 19.2764 17.0482 19.2832C17.3123 19.2913 17.5738 19.2296 17.8063 19.1042C17.9997 18.9999 18.1664 18.8333 18.4996 18.5001C18.8328 18.1669 18.9994 18.0002 19.1037 17.8068C19.2291 17.5743 19.2908 17.3127 19.2827 17.0487C19.2759 16.8291 19.2014 16.6055 19.0524 16.1584L18.8374 15.5134C18.6477 14.9444 18.9344 14.3262 19.4913 14.1035C20.036 13.8856 20.3084 13.7766 20.5088 13.5946C20.6858 13.4337 20.8218 13.2329 20.9054 13.0088C21 12.7551 21 12.4589 21 11.8663C21 11.3658 21 11.1156 20.9289 10.893C20.866 10.696 20.763 10.5141 20.6265 10.3588C20.4721 10.1833 20.2597 10.0558 19.8348 9.80087L19.5569 9.63416C18.9478 9.26867 18.6939 8.51514 18.9578 7.85558C19.1262 7.43443 19.2105 7.22383 19.232 7.01543C19.2636 6.70926 19.2003 6.40077 19.0506 6.13181C18.9487 5.94875 18.7884 5.78837 18.4676 5.46762C18.1658 5.16584 18.0149 5.01494 17.8426 4.91583C17.5894 4.77024 17.2992 4.70174 17.0076 4.71872C16.8091 4.73029 16.6067 4.79777 16.2018 4.93273L15.6314 5.12287C14.9961 5.33464 14.3058 5.0145 14.0571 4.39272C13.8841 3.96016 13.7976 3.74388 13.6654 3.57511C13.4719 3.32808 13.2058 3.14794 12.9046 3.06005Z" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Nachher

Breite:  |  Höhe:  |  Größe: 3.1 KiB

Datei anzeigen

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 12H21M3 12C3 16.9706 7.02944 21 12 21M3 12C3 7.02944 7.02944 3 12 3M21 12C21 16.9706 16.9706 21 12 21M21 12C21 7.02944 16.9706 3 12 3M12 21C4.75561 13.08 8.98151 5.7 12 3M12 21C19.2444 13.08 15.0185 5.7 12 3" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Nachher

Breite:  |  Höhe:  |  Größe: 531 B

Datei anzeigen

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.0672 11.8568L20.4253 11.469L21.0672 11.8568ZM12.1432 2.93276L11.7553 2.29085V2.29085L12.1432 2.93276ZM21.25 12C21.25 17.1086 17.1086 21.25 12 21.25V22.75C17.9371 22.75 22.75 17.9371 22.75 12H21.25ZM12 21.25C6.89137 21.25 2.75 17.1086 2.75 12H1.25C1.25 17.9371 6.06294 22.75 12 22.75V21.25ZM2.75 12C2.75 6.89137 6.89137 2.75 12 2.75V1.25C6.06294 1.25 1.25 6.06294 1.25 12H2.75ZM15.5 14.25C12.3244 14.25 9.75 11.6756 9.75 8.5H8.25C8.25 12.5041 11.4959 15.75 15.5 15.75V14.25ZM20.4253 11.469C19.4172 13.1373 17.5882 14.25 15.5 14.25V15.75C18.1349 15.75 20.4407 14.3439 21.7092 12.2447L20.4253 11.469ZM9.75 8.5C9.75 6.41182 10.8627 4.5828 12.531 3.57467L11.7553 2.29085C9.65609 3.5593 8.25 5.86509 8.25 8.5H9.75ZM12 2.75C11.9115 2.75 11.8077 2.71008 11.7324 2.63168C11.6686 2.56527 11.6538 2.50244 11.6503 2.47703C11.6461 2.44587 11.6482 2.35557 11.7553 2.29085L12.531 3.57467C13.0342 3.27065 13.196 2.71398 13.1368 2.27627C13.0754 1.82126 12.7166 1.25 12 1.25V2.75ZM21.7092 12.2447C21.6444 12.3518 21.5541 12.3539 21.523 12.3497C21.4976 12.3462 21.4347 12.3314 21.3683 12.2676C21.2899 12.1923 21.25 12.0885 21.25 12H22.75C22.75 11.2834 22.1787 10.9246 21.7237 10.8632C21.286 10.804 20.7293 10.9658 20.4253 11.469L21.7092 12.2447Z" fill="#1C274C"/>
</svg>

Nachher

Breite:  |  Höhe:  |  Größe: 1.5 KiB

7
src/resources/icons/sun.svg Normale Datei
Datei anzeigen

@ -0,0 +1,7 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
<svg fill="#FFFFFF" height="800px" width="800px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 207.628 207.628" xml:space="preserve" stroke="#FFFFFF">
<g id="SVGRepo_bgCarrier" stroke-width="0"/>

Nachher

Breite:  |  Höhe:  |  Größe: 2.4 KiB

Datei anzeigen

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="420" height="90" viewBox="0 0 420 90" xmlns="http://www.w3.org/2000/svg">
<defs>
<style>
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;600&amp;display=swap');
</style>
<!-- Accurate shield matching original -->
<g id="shield-eye-accurate">
<!-- Angular shield shape -->
<path d="M 35 30
L 65 30
L 75 40
L 75 80
L 50 115
L 25 80
L 25 40
L 35 30 Z"
fill="none"
stroke="currentColor"
stroke-width="3.5"
stroke-linejoin="miter"/>
<!-- Eye centered in shield -->
<g transform="translate(50, 65)">
<!-- Almond/football shaped eye -->
<ellipse cx="0" cy="0" rx="24" ry="13"
fill="none"
stroke="currentColor"
stroke-width="3.5"/>
<!-- Circular iris -->
<circle cx="0" cy="0" r="10"
fill="none"
stroke="currentColor"
stroke-width="3.5"/>
<!-- Pupil -->
<circle cx="0" cy="0" r="4" fill="currentColor"/>
</g>
</g>
</defs>
<!-- Dark version for website - NO BACKGROUND -->
<g transform="translate(5, 45)">
<!-- Shield centered vertically with text -->
<g transform="translate(0, -72.5)" color="white">
<use href="#shield-eye-accurate"/>
</g>
<!-- Text aligned with shield center -->
<text x="90" y="5" font-family="'Poppins', sans-serif" font-size="46" font-weight="600" fill="white">IntelSight</text>
<text x="90" y="35" font-family="'Poppins', sans-serif" font-size="12" font-weight="400" fill="white" letter-spacing="1.3">SICHERHEIT MADE IN GERMANY</text>
</g>
</svg>

Nachher

Breite:  |  Höhe:  |  Größe: 1.8 KiB

Datei anzeigen

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="420" height="90" viewBox="0 0 420 90" xmlns="http://www.w3.org/2000/svg">
<defs>
<style>
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;600&amp;display=swap');
</style>
<!-- Accurate shield matching original -->
<g id="shield-eye-accurate">
<!-- Angular shield shape -->
<path d="M 35 30
L 65 30
L 75 40
L 75 80
L 50 115
L 25 80
L 25 40
L 35 30 Z"
fill="none"
stroke="currentColor"
stroke-width="3.5"
stroke-linejoin="miter"/>
<!-- Eye centered in shield -->
<g transform="translate(50, 65)">
<!-- Almond/football shaped eye -->
<ellipse cx="0" cy="0" rx="24" ry="13"
fill="none"
stroke="currentColor"
stroke-width="3.5"/>
<!-- Circular iris -->
<circle cx="0" cy="0" r="10"
fill="none"
stroke="currentColor"
stroke-width="3.5"/>
<!-- Pupil -->
<circle cx="0" cy="0" r="4" fill="currentColor"/>
</g>
</g>
</defs>
<!-- Light version -->
<g transform="translate(5, 45)">
<!-- Shield centered vertically with text -->
<g transform="translate(0, -72.5)" color="#232D53">
<use href="#shield-eye-accurate"/>
</g>
<!-- Text aligned with shield center -->
<text x="90" y="5" font-family="'Poppins', sans-serif" font-size="46" font-weight="600" fill="#232D53">IntelSight</text>
<text x="90" y="35" font-family="'Poppins', sans-serif" font-size="12" font-weight="400" fill="#232D53" letter-spacing="1.3">SICHERHEIT MADE IN GERMANY</text>
</g>
</svg>

Nachher

Breite:  |  Höhe:  |  Größe: 1.8 KiB

Datei anzeigen

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="120" height="120" viewBox="0 0 120 120" xmlns="http://www.w3.org/2000/svg">
<!-- Shield only - dark version (white on transparent) -->
<g transform="translate(60, 60)">
<!-- Center the shield -->
<g transform="translate(-50, -72.5)">
<!-- Angular shield shape -->
<path d="M 35 30
L 65 30
L 75 40
L 75 80
L 50 115
L 25 80
L 25 40
L 35 30 Z"
fill="none"
stroke="white"
stroke-width="3.5"
stroke-linejoin="miter"/>
<!-- Eye centered in shield -->
<g transform="translate(50, 65)">
<!-- Almond/football shaped eye -->
<ellipse cx="0" cy="0" rx="24" ry="13"
fill="none"
stroke="white"
stroke-width="3.5"/>
<!-- Circular iris -->
<circle cx="0" cy="0" r="10"
fill="none"
stroke="white"
stroke-width="3.5"/>
<!-- Pupil -->
<circle cx="0" cy="0" r="4" fill="white"/>
</g>
</g>
</g>
</svg>

Nachher

Breite:  |  Höhe:  |  Größe: 1.2 KiB

Datei anzeigen

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="450" height="100" viewBox="0 0 450 100" xmlns="http://www.w3.org/2000/svg">
<defs>
<style>
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;600&amp;display=swap');
</style>
<!-- Accurate shield matching original -->
<g id="shield-eye-accurate">
<!-- Angular shield shape -->
<path d="M 35 30
L 65 30
L 75 40
L 75 80
L 50 115
L 25 80
L 25 40
L 35 30 Z"
fill="none"
stroke="currentColor"
stroke-width="3.5"
stroke-linejoin="miter"/>
<!-- Eye centered in shield -->
<g transform="translate(50, 65)">
<!-- Almond/football shaped eye -->
<ellipse cx="0" cy="0" rx="24" ry="13"
fill="none"
stroke="currentColor"
stroke-width="3.5"/>
<!-- Circular iris -->
<circle cx="0" cy="0" r="10"
fill="none"
stroke="currentColor"
stroke-width="3.5"/>
<!-- Pupil -->
<circle cx="0" cy="0" r="4" fill="currentColor"/>
</g>
</g>
</defs>
<!-- Dark version for website - NO BACKGROUND, NO TAGLINE -->
<g transform="translate(20, 50)">
<!-- Shield centered vertically with text -->
<g transform="translate(0, -72.5)" color="white">
<use href="#shield-eye-accurate"/>
</g>
<!-- Text aligned with shield center - NO TAGLINE -->
<text x="90" y="5" font-family="'Poppins', sans-serif" font-size="46" font-weight="600" fill="white">IntelSight</text>
</g>
</svg>

Nachher

Breite:  |  Höhe:  |  Größe: 1.7 KiB

Datei anzeigen

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="450" height="100" viewBox="0 0 450 100" xmlns="http://www.w3.org/2000/svg">
<defs>
<style>
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;600&amp;display=swap');
</style>
<!-- Accurate shield matching original -->
<g id="shield-eye-accurate">
<!-- Angular shield shape -->
<path d="M 35 30
L 65 30
L 75 40
L 75 80
L 50 115
L 25 80
L 25 40
L 35 30 Z"
fill="none"
stroke="currentColor"
stroke-width="3.5"
stroke-linejoin="miter"/>
<!-- Eye centered in shield -->
<g transform="translate(50, 65)">
<!-- Almond/football shaped eye -->
<ellipse cx="0" cy="0" rx="24" ry="13"
fill="none"
stroke="currentColor"
stroke-width="3.5"/>
<!-- Circular iris -->
<circle cx="0" cy="0" r="10"
fill="none"
stroke="currentColor"
stroke-width="3.5"/>
<!-- Pupil -->
<circle cx="0" cy="0" r="4" fill="currentColor"/>
</g>
</g>
</defs>
<!-- Light version -->
<g transform="translate(20, 50)">
<!-- Shield centered vertically with text -->
<g transform="translate(0, -72.5)" color="#232D53">
<use href="#shield-eye-accurate"/>
</g>
<!-- Text aligned with shield center - NO TAGLINE -->
<text x="90" y="5" font-family="'Poppins', sans-serif" font-size="46" font-weight="600" fill="#232D53">IntelSight</text>
</g>
</svg>

Nachher

Breite:  |  Höhe:  |  Größe: 1.7 KiB

Datei anzeigen

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="450" height="100" viewBox="0 0 450 100" xmlns="http://www.w3.org/2000/svg">
<defs>
<style>
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;600&amp;display=swap');
</style>
<!-- Accurate shield matching original -->
<g id="shield-eye-accurate">
<!-- Angular shield shape -->
<path d="M 35 30
L 65 30
L 75 40
L 75 80
L 50 115
L 25 80
L 25 40
L 35 30 Z"
fill="none"
stroke="currentColor"
stroke-width="3.5"
stroke-linejoin="miter"/>
<!-- Eye centered in shield -->
<g transform="translate(50, 65)">
<!-- Almond/football shaped eye -->
<ellipse cx="0" cy="0" rx="24" ry="13"
fill="none"
stroke="currentColor"
stroke-width="3.5"/>
<!-- Circular iris -->
<circle cx="0" cy="0" r="10"
fill="none"
stroke="currentColor"
stroke-width="3.5"/>
<!-- Pupil -->
<circle cx="0" cy="0" r="4" fill="currentColor"/>
</g>
</g>
</defs>
<!-- Dark version for website - NO BACKGROUND, NO TAGLINE -->
<g transform="translate(20, 50)">
<!-- Shield centered vertically with text -->
<g transform="translate(0, -72.5)" color="white">
<use href="#shield-eye-accurate"/>
</g>
<!-- Text aligned with shield center - NO TAGLINE -->
<text x="90" y="5" font-family="'Poppins', sans-serif" font-size="46" font-weight="600" fill="white">IntelSight</text>
</g>
</svg>

Nachher

Breite:  |  Höhe:  |  Größe: 1.7 KiB

Binäre Datei nicht angezeigt.

Binäre Datei nicht angezeigt.

Datei anzeigen

@ -0,0 +1,337 @@
DARK_THEME = """
/* Globale Variablen und Basis-Styles */
* {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;
}
/* Hauptfenster */
QMainWindow {
background-color: #0a0a0a;
color: #FFFFFF;
}
/* Widget Hintergründe */
QWidget {
background-color: transparent;
color: #FFFFFF;
}
/* App Header */
QWidget#appHeader {
background-color: transparent;
padding-bottom: 20px;
}
/* Überschriften */
QLabel#heading {
font-family: 'Poppins', sans-serif;
font-size: 32px;
font-weight: 700;
color: #FFFFFF;
letter-spacing: -0.5px;
}
QLabel#subheading {
font-size: 16px;
color: rgba(255, 255, 255, 0.6);
margin-top: -5px;
}
QLabel#sectionTitle {
font-size: 18px;
font-weight: 600;
color: #FFFFFF;
margin-bottom: 10px;
}
QLabel#inputLabel {
font-size: 14px;
font-weight: 600;
color: rgba(255, 255, 255, 0.8);
margin-bottom: 4px;
}
/* Content Card - Hauptcontainer */
QWidget#contentCard {
background-color: #1a1a1a;
border-radius: 12px;
padding: 32px;
}
/* Tabellen-Style */
QTableWidget#dataTable {
background-color: transparent;
border: none;
outline: none;
}
QTableWidget#dataTable::item {
background-color: transparent;
color: rgba(255, 255, 255, 0.7);
font-weight: 600;
padding: 12px 16px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
QTableWidget#dataTable::item:selected {
background-color: transparent;
}
/* Eingabefelder */
QLineEdit {
background-color: #232D53;
border: none;
border-radius: 8px;
padding: 12px 16px;
color: #FFFFFF;
font-size: 14px;
min-height: 24px;
}
QLineEdit:focus {
background-color: #2A3560;
outline: none;
}
QLineEdit::placeholder {
color: rgba(255, 255, 255, 0.4);
}
/* Buttons */
QPushButton {
background-color: transparent;
color: #FFFFFF;
border: 1px solid #232D53;
border-radius: 24px;
padding: 0 24px;
min-height: 40px;
font-size: 14px;
font-weight: 600;
}
QPushButton:hover {
background-color: #232D53;
}
QPushButton:pressed {
background-color: #1A1F3A;
}
QPushButton:disabled {
background-color: #1a1a1a;
color: rgba(255, 255, 255, 0.3);
border: 1px solid rgba(255, 255, 255, 0.1);
}
/* Primary Button */
QPushButton#primaryButton {
background-color: #00D4FF;
color: #1A1F3A;
border: none;
}
QPushButton#primaryButton:hover {
background-color: #00B8E6;
color: #FFFFFF;
}
QPushButton#primaryButton:pressed {
background-color: #0099CC;
}
QPushButton#primaryButton:disabled {
background-color: #2a2a2a;
color: rgba(255, 255, 255, 0.3);
}
/* Mode Toggle Button */
QPushButton#modeToggle {
background-color: rgba(255, 255, 255, 0.1);
border: none;
border-radius: 20px;
padding: 8px;
min-width: 40px;
min-height: 40px;
max-width: 40px;
max-height: 40px;
}
QPushButton#modeToggle:hover {
background-color: rgba(255, 255, 255, 0.2);
}
/* CheckBox */
QCheckBox {
spacing: 10px;
color: #FFFFFF;
font-size: 14px;
padding: 4px 0;
}
QCheckBox::indicator {
width: 20px;
height: 20px;
border-radius: 4px;
border: 2px solid rgba(255, 255, 255, 0.3);
background-color: rgba(255, 255, 255, 0.05);
}
QCheckBox::indicator:hover {
border: 2px solid rgba(255, 255, 255, 0.5);
background-color: rgba(255, 255, 255, 0.1);
}
QCheckBox::indicator:checked {
background-color: #00D4FF;
border: 2px solid #00D4FF;
image: url(src/resources/icons/check.svg);
padding: 2px;
}
/* SpinBox */
QSpinBox {
background-color: #232D53;
border: none;
border-radius: 8px;
padding: 8px 12px;
color: #FFFFFF;
font-size: 14px;
min-width: 60px;
}
QSpinBox:focus {
background-color: #2A3560;
}
QSpinBox::up-button, QSpinBox::down-button {
background-color: transparent;
border: none;
width: 20px;
}
QSpinBox::up-button:hover, QSpinBox::down-button:hover {
background-color: rgba(0, 212, 255, 0.1);
}
QSpinBox::up-arrow, QSpinBox::down-arrow {
image: none;
width: 0;
height: 0;
}
/* Progress Bar */
QProgressBar {
background-color: rgba(255, 255, 255, 0.1);
border: none;
border-radius: 6px;
height: 12px;
text-align: center;
font-size: 12px;
}
QProgressBar::chunk {
background: linear-gradient(90deg, #00D4FF 0%, #00B8E6 100%);
border-radius: 6px;
}
/* Text Edit für Status */
QTextEdit#statusLog {
background-color: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 12px;
color: rgba(255, 255, 255, 0.8);
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
font-size: 13px;
}
/* Scrollbar */
QScrollBar:vertical {
background-color: transparent;
width: 10px;
border-radius: 5px;
}
QScrollBar::handle:vertical {
background-color: rgba(255, 255, 255, 0.2);
border-radius: 5px;
min-height: 30px;
}
QScrollBar::handle:vertical:hover {
background-color: rgba(255, 255, 255, 0.3);
}
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
height: 0;
}
/* Frame Separator */
QFrame#separator {
background-color: rgba(255, 255, 255, 0.1);
max-height: 1px;
margin: 20px 0;
}
/* Status Bar */
QStatusBar {
background-color: #0a0a0a;
color: rgba(255, 255, 255, 0.6);
border-top: 1px solid rgba(255, 255, 255, 0.1);
font-size: 13px;
padding: 4px;
}
/* Message Box */
QMessageBox {
background-color: #1a1a1a;
color: #FFFFFF;
}
QMessageBox QPushButton {
min-width: 80px;
}
/* ComboBox */
QComboBox {
background-color: #232D53;
border: none;
border-radius: 8px;
padding: 10px 16px;
color: #FFFFFF;
font-size: 14px;
}
QComboBox:hover {
background-color: #2A3560;
}
QComboBox::drop-down {
border: none;
width: 24px;
}
QComboBox::down-arrow {
width: 0;
height: 0;
border-style: solid;
border-width: 6px 4px 0 4px;
border-color: #00D4FF transparent transparent transparent;
}
QComboBox QAbstractItemView {
background-color: #232D53;
border: none;
selection-background-color: #2A3560;
color: #FFFFFF;
}
/* Tool Tips */
QToolTip {
background-color: #232D53;
color: #FFFFFF;
border: 1px solid rgba(0, 212, 255, 0.2);
border-radius: 6px;
padding: 8px 12px;
font-size: 13px;
}
"""

Datei anzeigen

@ -0,0 +1,347 @@
LIGHT_THEME = """
/* Globale Variablen und Basis-Styles */
* {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;
}
/* Hauptfenster */
QMainWindow {
background-color: #f8f9fa;
color: #212529;
}
QMainWindow > QWidget {
background-color: #f8f9fa;
}
/* Widget Hintergründe */
QWidget {
background-color: transparent;
color: #212529;
}
/* App Header */
QWidget#appHeader {
background-color: transparent;
padding-bottom: 20px;
}
/* Überschriften */
QLabel#heading {
font-family: 'Poppins', sans-serif;
font-size: 32px;
font-weight: 700;
color: #212529 !important;
letter-spacing: -0.5px;
background-color: transparent;
}
QLabel#subheading {
font-size: 16px;
color: #6c757d;
margin-top: -5px;
}
QLabel#sectionTitle {
font-size: 18px;
font-weight: 600;
color: #212529;
margin-bottom: 10px;
}
QLabel#inputLabel {
font-size: 14px;
font-weight: 600;
color: #495057;
margin-bottom: 4px;
}
/* Content Card - Hauptcontainer */
QWidget#contentCard {
background-color: #ffffff;
border: 1px solid #e9ecef;
border-radius: 12px;
padding: 32px;
}
/* Tabellen-Style */
QTableWidget#dataTable {
background-color: transparent;
border: none;
outline: none;
}
QTableWidget#dataTable::item {
background-color: transparent;
color: #495057;
font-weight: 600;
padding: 12px 16px;
border-bottom: 1px solid #e9ecef;
}
QTableWidget#dataTable::item:selected {
background-color: transparent;
}
/* Eingabefelder */
QLineEdit {
background-color: #f8f9fa;
border: 1px solid #ced4da;
border-radius: 8px;
padding: 12px 16px;
color: #212529;
font-size: 14px;
min-height: 24px;
}
QLineEdit:focus {
border-color: #00D4FF;
background-color: #ffffff;
outline: none;
}
QLineEdit::placeholder {
color: #adb5bd;
}
/* Buttons */
QPushButton {
background-color: #ffffff;
color: #212529;
border: 1px solid #ced4da;
border-radius: 24px;
padding: 0 24px;
min-height: 40px;
font-size: 14px;
font-weight: 600;
}
QPushButton:hover {
background-color: #f8f9fa;
border-color: #00D4FF;
}
QPushButton:pressed {
background-color: #e9ecef;
}
QPushButton:disabled {
background-color: #f8f9fa;
color: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(0, 0, 0, 0.1);
}
/* Primary Button */
QPushButton#primaryButton {
background-color: #00D4FF;
color: #212529;
border: none;
}
QPushButton#primaryButton:hover {
background-color: #00B8E6;
color: #ffffff;
}
QPushButton#primaryButton:pressed {
background-color: #0099CC;
}
QPushButton#primaryButton:disabled {
background-color: #e9ecef;
color: #adb5bd;
}
/* Mode Toggle Button */
QPushButton#modeToggle {
background-color: rgba(0, 0, 0, 0.05);
border: none;
border-radius: 20px;
padding: 8px;
min-width: 40px;
min-height: 40px;
max-width: 40px;
max-height: 40px;
}
QPushButton#modeToggle:hover {
background-color: rgba(0, 0, 0, 0.1);
}
/* CheckBox */
QCheckBox {
spacing: 10px;
color: #212529;
font-size: 14px;
padding: 4px 0;
}
QCheckBox::indicator {
width: 20px;
height: 20px;
border-radius: 4px;
border: 2px solid #adb5bd;
background-color: #ffffff;
}
QCheckBox::indicator:hover {
border: 2px solid #6c757d;
background-color: #f8f9fa;
}
QCheckBox::indicator:checked {
background-color: #00D4FF;
border: 2px solid #00D4FF;
image: url(src/resources/icons/check.svg);
padding: 2px;
}
/* SpinBox */
QSpinBox {
background-color: #f8f9fa;
border: 1px solid #ced4da;
border-radius: 8px;
padding: 8px 12px;
color: #212529;
font-size: 14px;
min-width: 60px;
}
QSpinBox:focus {
border-color: #00D4FF;
background-color: #ffffff;
}
QSpinBox::up-button, QSpinBox::down-button {
background-color: transparent;
border: none;
width: 20px;
}
QSpinBox::up-button:hover, QSpinBox::down-button:hover {
background-color: rgba(0, 212, 255, 0.1);
}
QSpinBox::up-arrow, QSpinBox::down-arrow {
image: none;
width: 0;
height: 0;
}
/* Progress Bar */
QProgressBar {
background-color: #e9ecef;
border: none;
border-radius: 6px;
height: 12px;
text-align: center;
font-size: 12px;
color: #212529;
}
QProgressBar::chunk {
background: linear-gradient(90deg, #00D4FF 0%, #00B8E6 100%);
border-radius: 6px;
}
/* Text Edit für Status */
QTextEdit#statusLog {
background-color: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 12px;
color: #495057;
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
font-size: 13px;
}
/* Scrollbar */
QScrollBar:vertical {
background-color: transparent;
width: 10px;
border-radius: 5px;
}
QScrollBar::handle:vertical {
background-color: #ced4da;
border-radius: 5px;
min-height: 30px;
}
QScrollBar::handle:vertical:hover {
background-color: #adb5bd;
}
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
height: 0;
}
/* Frame Separator */
QFrame#separator {
background-color: #e9ecef;
max-height: 1px;
margin: 20px 0;
}
/* Status Bar */
QStatusBar {
background-color: #f8f9fa;
color: #6c757d;
border-top: 1px solid #e9ecef;
font-size: 13px;
padding: 4px;
}
/* Message Box */
QMessageBox {
background-color: #ffffff;
color: #212529;
}
QMessageBox QPushButton {
min-width: 80px;
}
/* ComboBox */
QComboBox {
background-color: #f8f9fa;
border: 1px solid #ced4da;
border-radius: 8px;
padding: 10px 16px;
color: #212529;
font-size: 14px;
}
QComboBox:hover {
border-color: #00D4FF;
}
QComboBox::drop-down {
border: none;
width: 24px;
}
QComboBox::down-arrow {
width: 0;
height: 0;
border-style: solid;
border-width: 6px 4px 0 4px;
border-color: #00D4FF transparent transparent transparent;
}
QComboBox QAbstractItemView {
background-color: #ffffff;
border: 1px solid #ced4da;
selection-background-color: #e3f2fd;
color: #212529;
}
/* Tool Tips */
QToolTip {
background-color: #212529;
color: #ffffff;
border: none;
border-radius: 6px;
padding: 8px 12px;
font-size: 13px;
}
"""

1
src/ui/__init__.py Normale Datei
Datei anzeigen

@ -0,0 +1 @@
# UI Module

Binäre Datei nicht angezeigt.

Binäre Datei nicht angezeigt.

Binäre Datei nicht angezeigt.

54
src/ui/custom_widgets.py Normale Datei
Datei anzeigen

@ -0,0 +1,54 @@
from PyQt6.QtWidgets import QSpinBox, QStyleOptionSpinBox, QStyle
from PyQt6.QtCore import Qt, QRect
from PyQt6.QtGui import QPainter, QFont, QPen, QColor
class CustomSpinBox(QSpinBox):
def __init__(self, parent=None):
super().__init__(parent)
def paintEvent(self, event):
# Standard paint event
super().paintEvent(event)
# Zeichne custom + und - Zeichen
painter = QPainter(self)
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
# Button-Bereiche berechnen
option = QStyleOptionSpinBox()
self.initStyleOption(option)
up_rect = self.style().subControlRect(
QStyle.ComplexControl.CC_SpinBox,
option,
QStyle.SubControl.SC_SpinBoxUp,
self
)
down_rect = self.style().subControlRect(
QStyle.ComplexControl.CC_SpinBox,
option,
QStyle.SubControl.SC_SpinBoxDown,
self
)
# Font für Symbole
font = QFont()
font.setPixelSize(14)
font.setBold(True)
painter.setFont(font)
# Farbe
if self.isEnabled():
painter.setPen(QPen(QColor("#00D4FF"), 2))
else:
painter.setPen(QPen(QColor("#666666"), 2))
# + Zeichen zeichnen
painter.drawText(up_rect, Qt.AlignmentFlag.AlignCenter, "+")
# - Zeichen zeichnen
painter.drawText(down_rect, Qt.AlignmentFlag.AlignCenter, "") # Unicode minus
painter.end()

604
src/ui/main_window.py Normale Datei
Datei anzeigen

@ -0,0 +1,604 @@
from PyQt6.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QLineEdit, QPushButton, QGroupBox,
QComboBox, QTextEdit, QProgressBar, QCheckBox,
QFileDialog, QMessageBox, QStatusBar, QSpinBox,
QTabWidget, QListWidget, QListWidgetItem,
QTableWidget, QTableWidgetItem, QHeaderView,
QFrame, QSplitter, QScrollArea)
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QTimer, QSettings
from PyQt6.QtGui import QIcon, QPixmap, QPalette, QColor, QDesktopServices, QPainter
from PyQt6.QtCore import QUrl
import os
import sys
from datetime import datetime
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from resources.styles.dark_theme import DARK_THEME
from resources.styles.light_theme import LIGHT_THEME
from core.web_crawler import WebCrawler
from utils.pdf_report import PDFReport
from ui.custom_widgets import CustomSpinBox
from utils.local_server import LocalWebServer
class CrawlerThread(QThread):
progress = pyqtSignal(int)
status = pyqtSignal(str)
finished = pyqtSignal(bool)
report_ready = pyqtSignal(str) # Signal für PDF-Pfad
def __init__(self, url, save_path, options):
super().__init__()
self.url = url
self.save_path = save_path
self.options = options
self.crawler = None
def run(self):
try:
# Human behavior ist immer aktiviert
self.crawler = WebCrawler(human_behavior=True)
self.crawler.progress_callback = self.progress.emit
self.crawler.status_callback = self.status.emit
success = self.crawler.download_website(
self.url,
self.save_path,
**self.options
)
# Erstelle PDF-Bericht
try:
pdf_report = PDFReport()
report_path = pdf_report.generate_report(
url=self.url,
save_path=self.save_path,
start_time=self.crawler.start_time,
end_time=self.crawler.end_time,
downloaded_resources=self.crawler.downloaded_resources,
skipped_urls=self.crawler.skipped_urls,
options=self.options,
success=success
)
self.status.emit(f"PDF-Bericht erstellt: {report_path}")
self.report_ready.emit(report_path)
except Exception as e:
self.status.emit(f"Fehler beim Erstellen des PDF-Berichts: {str(e)}")
self.finished.emit(success)
except Exception as e:
self.status.emit(f"Fehler: {str(e)}")
# Versuche trotzdem einen Fehlerbericht zu erstellen
try:
if self.crawler and self.crawler.start_time:
pdf_report = PDFReport()
report_path = pdf_report.generate_report(
url=self.url,
save_path=self.save_path,
start_time=self.crawler.start_time,
end_time=self.crawler.end_time or datetime.now(),
downloaded_resources=self.crawler.downloaded_resources,
skipped_urls=self.crawler.skipped_urls,
options=self.options,
success=False
)
self.status.emit(f"Fehler-PDF-Bericht erstellt: {report_path}")
self.report_ready.emit(report_path)
except:
pass
self.finished.emit(False)
class WebsiteCrawlerWindow(QMainWindow):
def __init__(self):
super().__init__()
self.crawler_thread = None
self.last_report_path = None
self.last_save_path = None
self.local_server = None
self.settings = QSettings('IntelSight', 'WebsiteCrawler')
self.dark_mode = self.settings.value('dark_mode', True, type=bool)
self.init_ui()
def init_ui(self):
self.setWindowTitle("IntelSight Webseiten-Crawler")
self.setGeometry(100, 100, 1200, 800)
# Theme anwenden
self.apply_theme()
# Scroll Area als zentrales Widget
scroll_area = QScrollArea()
scroll_area.setWidgetResizable(True)
scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
self.setCentralWidget(scroll_area)
# Container Widget für den Inhalt
central_widget = QWidget()
scroll_area.setWidget(central_widget)
# Hauptlayout
main_layout = QVBoxLayout(central_widget)
main_layout.setContentsMargins(32, 32, 32, 32)
main_layout.setSpacing(20)
# Header mit Logo, Titel und Mode Toggle
header_widget = QWidget()
header_widget.setObjectName("appHeader")
header_layout = QHBoxLayout(header_widget)
header_layout.setContentsMargins(0, 0, 0, 20)
# IntelSight Logo - Neu implementiert
self.logo_label = QLabel()
self.logo_label.setFixedSize(300, 70) # Feste Größe für konsistente Darstellung
self.logo_label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
self.refresh_logo() # Neue Methode verwenden
header_layout.addWidget(self.logo_label)
# Titel (ohne IntelSight, da es im Logo ist)
title_label = QLabel("Webseiten-Crawler")
title_label.setObjectName("heading")
header_layout.addWidget(title_label)
header_layout.addStretch()
# Mode Toggle Button
self.mode_toggle = QPushButton()
self.mode_toggle.setObjectName("modeToggle")
self.mode_toggle.clicked.connect(self.toggle_theme)
self.update_mode_icon()
header_layout.addWidget(self.mode_toggle)
main_layout.addWidget(header_widget)
# Beschreibung
desc_label = QLabel("Sichere Webseiten lokal für Offline-Zugriff")
desc_label.setObjectName("subheading")
main_layout.addWidget(desc_label)
# Container für Formular
content_widget = QWidget()
content_widget.setObjectName("contentCard")
content_layout = QVBoxLayout(content_widget)
content_layout.setSpacing(20)
# URL Eingabe
url_widget = QWidget()
url_layout = QVBoxLayout(url_widget)
url_layout.setSpacing(8)
url_label = QLabel("URL:")
url_label.setObjectName("inputLabel")
self.url_input = QLineEdit()
self.url_input.setPlaceholderText("https://example.com")
url_layout.addWidget(url_label)
url_layout.addWidget(self.url_input)
content_layout.addWidget(url_widget)
# Speicherort
save_widget = QWidget()
save_layout = QVBoxLayout(save_widget)
save_layout.setSpacing(8)
save_label = QLabel("Speicherort:")
save_label.setObjectName("inputLabel")
save_input_layout = QHBoxLayout()
save_input_layout.setContentsMargins(0, 0, 0, 0)
self.save_path_input = QLineEdit()
self.save_path_input.setPlaceholderText("C:\\Downloads\\Webseite")
self.browse_button = QPushButton("Durchsuchen")
self.browse_button.clicked.connect(self.browse_folder)
save_input_layout.addWidget(self.save_path_input)
save_input_layout.addWidget(self.browse_button)
save_layout.addWidget(save_label)
save_layout.addLayout(save_input_layout)
content_layout.addWidget(save_widget)
# Sicherungsart
backup_type_widget = QWidget()
backup_type_layout = QVBoxLayout(backup_type_widget)
backup_type_layout.setSpacing(8)
backup_type_label = QLabel("Sicherungsart:")
backup_type_label.setObjectName("inputLabel")
backup_type_layout.addWidget(backup_type_label)
self.snapshot_mode = QCheckBox("Webseiten-Snapshot erstellen (nur die aktuelle Seite mit Bildern und Formatierung)")
self.snapshot_mode.setChecked(True)
self.full_backup_mode = QCheckBox("Gesamte Webseite sichern (alle verlinkten Unterseiten und Medien)")
backup_type_layout.addWidget(self.snapshot_mode)
backup_type_layout.addWidget(self.full_backup_mode)
content_layout.addWidget(backup_type_widget)
# Checkbox-Verhalten: nur eine Option auswählbar
self.snapshot_mode.toggled.connect(lambda checked: self.full_backup_mode.setChecked(not checked) if checked else None)
self.full_backup_mode.toggled.connect(lambda checked: self.snapshot_mode.setChecked(not checked) if checked else None)
self.full_backup_mode.toggled.connect(self.toggle_resource_options)
# Ressourcen-Optionen (nur sichtbar bei "Gesamte Webseite sichern")
self.resources_widget = QWidget()
resources_layout = QVBoxLayout(self.resources_widget)
resources_layout.setSpacing(8)
resources_label = QLabel("Ressourcen:")
resources_label.setObjectName("inputLabel")
resources_layout.addWidget(resources_label)
self.download_images = QCheckBox("Bilder herunterladen")
self.download_images.setChecked(True)
self.download_videos = QCheckBox("Videos herunterladen")
self.download_videos.setChecked(True)
self.download_css = QCheckBox("CSS-Dateien herunterladen")
self.download_css.setChecked(True)
self.download_js = QCheckBox("JavaScript-Dateien herunterladen")
self.download_js.setChecked(True)
resources_layout.addWidget(self.download_images)
resources_layout.addWidget(self.download_videos)
resources_layout.addWidget(self.download_css)
resources_layout.addWidget(self.download_js)
content_layout.addWidget(self.resources_widget)
self.resources_widget.setVisible(False) # Standardmäßig ausgeblendet
# Separator
separator = QFrame()
separator.setFrameShape(QFrame.Shape.HLine)
separator.setObjectName("separator")
content_layout.addWidget(separator)
# Progress Section
progress_widget = QWidget()
progress_layout = QVBoxLayout(progress_widget)
progress_label = QLabel("Download-Fortschritt")
progress_label.setObjectName("sectionTitle")
progress_layout.addWidget(progress_label)
self.progress_bar = QProgressBar()
self.progress_bar.setTextVisible(True)
progress_layout.addWidget(self.progress_bar)
# Status Log
self.status_text = QTextEdit()
self.status_text.setReadOnly(True)
self.status_text.setMaximumHeight(120)
self.status_text.setObjectName("statusLog")
progress_layout.addWidget(self.status_text)
content_layout.addWidget(progress_widget)
# Buttons
button_widget = QWidget()
button_layout = QHBoxLayout(button_widget)
button_layout.setContentsMargins(0, 20, 0, 0)
button_layout.addStretch()
self.view_button = QPushButton("Webseite lokal anzeigen")
self.view_button.setEnabled(False)
self.view_button.clicked.connect(self.view_website)
self.report_button = QPushButton("Download Bericht (PDF)")
self.report_button.setEnabled(False)
self.report_button.clicked.connect(self.open_last_report)
self.stop_button = QPushButton("Abbrechen")
self.stop_button.setEnabled(False)
self.stop_button.clicked.connect(self.stop_download)
self.start_button = QPushButton("Download starten")
self.start_button.setObjectName("primaryButton")
self.start_button.clicked.connect(self.start_download)
button_layout.addWidget(self.view_button)
button_layout.addWidget(self.report_button)
button_layout.addWidget(self.stop_button)
button_layout.addWidget(self.start_button)
content_layout.addWidget(button_widget)
main_layout.addWidget(content_widget)
main_layout.addStretch()
# Status Bar
self.status_bar = QStatusBar()
self.setStatusBar(self.status_bar)
self.status_bar.showMessage("Bereit")
def toggle_resource_options(self, checked):
"""Zeigt/versteckt die Ressourcen-Optionen basierend auf der Sicherungsart"""
self.resources_widget.setVisible(checked)
def browse_folder(self):
folder = QFileDialog.getExistingDirectory(self, "Speicherort wählen")
if folder:
self.save_path_input.setText(folder)
def start_download(self):
url = self.url_input.text().strip()
save_path = self.save_path_input.text().strip()
if not url:
QMessageBox.warning(self, "Warnung", "Bitte geben Sie eine URL ein.")
return
if not save_path:
QMessageBox.warning(self, "Warnung", "Bitte wählen Sie einen Speicherort.")
return
# Optionen sammeln
if self.full_backup_mode.isChecked():
options = {
'download_images': self.download_images.isChecked(),
'download_videos': self.download_videos.isChecked(),
'download_css': self.download_css.isChecked(),
'download_js': self.download_js.isChecked(),
'follow_links': True,
'max_depth': 999 # Sehr hohe Tiefe für vollständige Sicherung
}
else:
# Snapshot-Modus: nur die aktuelle Seite mit minimalen Ressourcen
options = {
'download_images': True,
'download_videos': False,
'download_css': True,
'download_js': False,
'follow_links': False,
'max_depth': 0
}
options['backup_type'] = 'full' if self.full_backup_mode.isChecked() else 'snapshot'
# Erstelle Verzeichnisstruktur: Datum_Webseitenname_Art
from datetime import datetime
from urllib.parse import urlparse
# Datum im Format YYMMDD
date_str = datetime.now().strftime("%y%m%d")
# Webseitenname aus URL extrahieren
parsed_url = urlparse(url)
website_name = parsed_url.netloc.replace('www.', '').replace(':', '_')
if not website_name:
website_name = 'website'
# Art der Sicherung
backup_type = 'complete' if self.full_backup_mode.isChecked() else 'snapshot'
# Verzeichnisname erstellen
dir_name = f"{date_str}_{website_name}_{backup_type}"
final_save_path = os.path.join(save_path, dir_name)
# UI für Download vorbereiten - Deaktiviere alle Eingabefelder
self.start_button.setEnabled(False)
self.stop_button.setEnabled(True)
self.view_button.setEnabled(False)
self.report_button.setEnabled(False)
# Deaktiviere alle Eingabefelder
self.url_input.setEnabled(False)
self.save_path_input.setEnabled(False)
self.browse_button.setEnabled(False)
# Deaktiviere Sicherungsart
self.snapshot_mode.setEnabled(False)
self.full_backup_mode.setEnabled(False)
# Deaktiviere Ressourcen-Checkboxen
self.download_images.setEnabled(False)
self.download_videos.setEnabled(False)
self.download_css.setEnabled(False)
self.download_js.setEnabled(False)
# Deaktiviere Theme-Toggle
self.mode_toggle.setEnabled(False)
self.progress_bar.setValue(0)
self.status_text.clear()
# Thread starten mit angepasstem Pfad
self.crawler_thread = CrawlerThread(url, final_save_path, options)
self.crawler_thread.progress.connect(self.update_progress)
self.crawler_thread.status.connect(self.update_status)
self.crawler_thread.finished.connect(self.download_finished)
self.crawler_thread.report_ready.connect(self.open_report)
self.crawler_thread.start()
# Speichere den finalen Pfad für die lokale Anzeige
self.last_save_path = final_save_path
def stop_download(self):
if self.crawler_thread and self.crawler_thread.isRunning():
self.update_status("Download wird abgebrochen...")
# Versuche Bericht zu erstellen bevor Thread beendet wird
if self.crawler_thread.crawler:
try:
pdf_report = PDFReport()
report_path = pdf_report.generate_report(
url=self.crawler_thread.url,
save_path=self.crawler_thread.save_path,
start_time=self.crawler_thread.crawler.start_time,
end_time=datetime.now(),
downloaded_resources=self.crawler_thread.crawler.downloaded_resources,
skipped_urls=self.crawler_thread.crawler.skipped_urls,
options=self.crawler_thread.options,
success=False,
error_message="Download manuell abgebrochen"
)
self.last_report_path = report_path
self.report_button.setEnabled(True)
self.update_status(f"Abbruch-Bericht erstellt: {report_path}")
except Exception as e:
self.update_status(f"Fehler beim Erstellen des Abbruch-Berichts: {str(e)}")
self.crawler_thread.terminate()
self.download_finished(False)
def update_progress(self, value):
self.progress_bar.setValue(value)
def update_status(self, message):
self.status_text.append(message)
self.status_bar.showMessage(message)
def download_finished(self, success):
# Aktiviere alle UI-Elemente wieder
self.start_button.setEnabled(True)
self.stop_button.setEnabled(False)
# Aktiviere alle Eingabefelder wieder
self.url_input.setEnabled(True)
self.save_path_input.setEnabled(True)
self.browse_button.setEnabled(True)
# Aktiviere Sicherungsart wieder
self.snapshot_mode.setEnabled(True)
self.full_backup_mode.setEnabled(True)
# Aktiviere Ressourcen-Checkboxen wieder
self.download_images.setEnabled(True)
self.download_videos.setEnabled(True)
self.download_css.setEnabled(True)
self.download_js.setEnabled(True)
# Aktiviere Theme-Toggle wieder
self.mode_toggle.setEnabled(True)
# Speichere den Pfad für die Webseiten-Anzeige
self.last_save_path = self.save_path_input.text()
if success:
self.view_button.setEnabled(True)
self.progress_bar.setValue(100)
QMessageBox.information(self, "Erfolg", "Website wurde erfolgreich heruntergeladen!")
else:
# Bei Fehler trotzdem Report-Button aktivieren, wenn ein Bericht vorhanden ist
if self.last_report_path and os.path.exists(self.last_report_path):
self.report_button.setEnabled(True)
QMessageBox.warning(self, "Fehler", "Der Download wurde unterbrochen oder es ist ein Fehler aufgetreten.\nDetails finden Sie im PDF-Bericht.")
self.status_bar.showMessage("Bereit")
def apply_theme(self):
if self.dark_mode:
self.setStyleSheet(DARK_THEME)
else:
self.setStyleSheet(LIGHT_THEME)
def toggle_theme(self):
self.dark_mode = not self.dark_mode
self.settings.setValue('dark_mode', self.dark_mode)
self.apply_theme()
self.update_mode_icon()
self.refresh_logo() # Neue Methode verwenden
def update_mode_icon(self):
if self.dark_mode:
icon_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
'resources', 'icons', 'sun.svg')
else:
icon_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
'resources', 'icons', 'moon.svg')
if os.path.exists(icon_path):
self.mode_toggle.setIcon(QIcon(icon_path))
def refresh_logo(self):
"""Neue Methode zum Laden des Logos - komplett neu implementiert"""
# Bestimme den Basispfad für die Ressourcen
base_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
resources_path = os.path.join(base_path, 'resources', 'logo')
# Wähle die richtige Logo-Datei basierend auf dem Theme
if self.dark_mode:
logo_filename = 'intelsight-name-dark.svg'
else:
logo_filename = 'intelsight-name-light.svg'
logo_path = os.path.join(resources_path, logo_filename)
# Prüfe ob die Datei existiert
if not os.path.exists(logo_path):
print(f"Logo-Datei nicht gefunden: {logo_path}")
self.logo_label.setText("IntelSight") # Fallback Text
return
# Lade das Logo
pixmap = QPixmap(logo_path)
# Prüfe ob das Pixmap erfolgreich geladen wurde
if pixmap.isNull():
print(f"Fehler beim Laden des Logos: {logo_path}")
self.logo_label.setText("IntelSight") # Fallback Text
return
# Skaliere das Logo auf die gewünschte Größe
scaled_pixmap = pixmap.scaled(
290, 65, # Etwas kleiner als die Label-Größe für Padding
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation
)
# Setze das Logo
self.logo_label.setPixmap(scaled_pixmap)
# Debug-Info
print(f"Logo erfolgreich geladen: {logo_filename} (Dark Mode: {self.dark_mode})")
def open_report(self, report_path: str):
"""Speichert den PDF-Bericht-Pfad ohne Benutzeraufforderung"""
self.last_report_path = report_path
self.report_button.setEnabled(True)
def open_last_report(self):
"""Öffnet den letzten PDF-Bericht"""
if self.last_report_path and os.path.exists(self.last_report_path):
QDesktopServices.openUrl(QUrl.fromLocalFile(self.last_report_path))
else:
QMessageBox.information(self, "Info", "Kein Bericht vorhanden.")
def view_website(self):
"""Startet einen lokalen Webserver und öffnet die Webseite"""
if not self.last_save_path or not os.path.exists(self.last_save_path):
QMessageBox.information(self, "Info", "Keine gesicherte Webseite vorhanden.")
return
try:
# Stoppe vorherigen Server falls vorhanden
if self.local_server:
self.local_server.stop()
# Starte neuen Server
self.local_server = LocalWebServer(self.last_save_path)
url = self.local_server.start()
# Öffne im Browser
self.local_server.open_in_browser()
# Zeige Info
QMessageBox.information(
self,
"Webserver gestartet",
f"Die Webseite wird auf {url} bereitgestellt.\n\n"
"Der Server läuft im Hintergrund und wird beim Beenden der Anwendung gestoppt."
)
except Exception as e:
QMessageBox.critical(self, "Fehler", f"Fehler beim Starten des Webservers:\n{str(e)}")
def closeEvent(self, event):
"""Wird beim Schließen der Anwendung aufgerufen"""
# Stoppe lokalen Server falls vorhanden
if self.local_server:
self.local_server.stop()
# Speichere Einstellungen
self.settings.setValue('dark_mode', self.dark_mode)
# Stoppe laufenden Download
if self.crawler_thread and self.crawler_thread.isRunning():
self.crawler_thread.terminate()
self.crawler_thread.wait()
event.accept()

0
src/utils/__init__.py Normale Datei
Datei anzeigen

Binäre Datei nicht angezeigt.

Binäre Datei nicht angezeigt.

Binäre Datei nicht angezeigt.

86
src/utils/local_server.py Normale Datei
Datei anzeigen

@ -0,0 +1,86 @@
import http.server
import socketserver
import os
import webbrowser
import threading
from pathlib import Path
class LocalWebServer:
"""Einfacher lokaler Webserver für die Anzeige gesicherter Webseiten"""
def __init__(self, directory: str, port: int = 8000):
self.directory = Path(directory).resolve()
self.port = port
self.server = None
self.server_thread = None
def start(self):
"""Startet den lokalen Webserver"""
# Wechsle zum Verzeichnis
os.chdir(self.directory)
# Erstelle Handler
handler = http.server.SimpleHTTPRequestHandler
# Finde einen freien Port
while True:
try:
self.server = socketserver.TCPServer(("", self.port), handler)
break
except OSError:
self.port += 1
if self.port > 9000:
raise Exception("Kein freier Port zwischen 8000 und 9000 gefunden")
# Starte Server in separatem Thread
self.server_thread = threading.Thread(target=self.server.serve_forever)
self.server_thread.daemon = True
self.server_thread.start()
return f"http://localhost:{self.port}"
def stop(self):
"""Stoppt den Webserver"""
if self.server:
self.server.shutdown()
self.server_thread.join()
def open_in_browser(self):
"""Öffnet die Webseite im Browser"""
url = f"http://localhost:{self.port}"
webbrowser.open(url)
return url
def serve_website(directory: str, auto_open: bool = True):
"""
Startet einen lokalen Webserver für die gesicherte Webseite
Args:
directory: Pfad zum Verzeichnis mit der gesicherten Webseite
auto_open: Öffnet automatisch den Browser
"""
server = LocalWebServer(directory)
url = server.start()
print(f"Webserver gestartet auf {url}")
print("Drücken Sie Strg+C zum Beenden")
if auto_open:
server.open_in_browser()
try:
# Server läuft bis Ctrl+C
while True:
pass
except KeyboardInterrupt:
print("\nServer wird beendet...")
server.stop()
if __name__ == "__main__":
import sys
if len(sys.argv) > 1:
serve_website(sys.argv[1])
else:
print("Verwendung: python local_server.py <verzeichnis>")

418
src/utils/pdf_report.py Normale Datei
Datei anzeigen

@ -0,0 +1,418 @@
from reportlab.lib import colors
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import inch, cm
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, PageBreak
from reportlab.platypus.tableofcontents import TableOfContents
from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_RIGHT, TA_JUSTIFY
from datetime import datetime
import os
from typing import Dict, List, Optional
class PDFReport:
def __init__(self):
self.styles = getSampleStyleSheet()
self._create_custom_styles()
def _create_custom_styles(self):
"""Erstellt angepasste Styles für den Bericht"""
# Titel-Style
self.styles.add(ParagraphStyle(
name='CustomTitle',
parent=self.styles['Heading1'],
fontSize=24,
textColor=colors.HexColor('#232D53'),
spaceAfter=30,
alignment=TA_CENTER
))
# Untertitel-Style
self.styles.add(ParagraphStyle(
name='CustomSubtitle',
parent=self.styles['Normal'],
fontSize=14,
textColor=colors.HexColor('#6C757D'),
spaceAfter=20,
alignment=TA_CENTER
))
# Section Header
self.styles.add(ParagraphStyle(
name='SectionHeader',
parent=self.styles['Heading2'],
fontSize=16,
textColor=colors.HexColor('#232D53'),
spaceAfter=12,
spaceBefore=20
))
# Info Text
self.styles.add(ParagraphStyle(
name='InfoText',
parent=self.styles['Normal'],
fontSize=11,
textColor=colors.HexColor('#495057'),
alignment=TA_JUSTIFY
))
# Error Text
self.styles.add(ParagraphStyle(
name='ErrorText',
parent=self.styles['Normal'],
fontSize=10,
textColor=colors.HexColor('#FF4444'),
leftIndent=20
))
# Success Text
self.styles.add(ParagraphStyle(
name='SuccessText',
parent=self.styles['Normal'],
fontSize=10,
textColor=colors.HexColor('#4CAF50'),
leftIndent=20
))
# Table Cell Style für automatischen Textumbruch
self.styles.add(ParagraphStyle(
name='TableCell',
parent=self.styles['Normal'],
fontSize=9,
leading=11,
wordWrap='CJK' # Besserer Umbruch für lange URLs
))
# Small Table Cell Style
self.styles.add(ParagraphStyle(
name='SmallTableCell',
parent=self.styles['Normal'],
fontSize=8,
leading=10,
wordWrap='CJK'
))
def generate_report(self,
url: str,
save_path: str,
start_time: datetime,
end_time: datetime,
downloaded_resources: Dict[str, str],
skipped_urls: Dict[str, str],
options: Dict,
success: bool,
output_path: Optional[str] = None,
error_message: Optional[str] = None) -> str:
"""Generiert einen PDF-Bericht"""
# Bestimme Ausgabepfad
if not output_path:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"crawler_report_{timestamp}.pdf"
output_path = os.path.join(save_path, filename)
# Erstelle PDF
doc = SimpleDocTemplate(
output_path,
pagesize=A4,
rightMargin=2*cm,
leftMargin=2*cm,
topMargin=2*cm,
bottomMargin=2*cm
)
# Story - Container für alle Elemente
story = []
# Titel
story.append(Paragraph("IntelSight Webseiten-Crawler", self.styles['CustomTitle']))
story.append(Paragraph("Sicherungsbericht", self.styles['CustomSubtitle']))
story.append(Spacer(1, 0.5*inch))
# Zusammenfassung
story.append(Paragraph("Zusammenfassung", self.styles['SectionHeader']))
# Erstelle Zusammenfassungstabelle
duration = end_time - start_time
status_text = 'Erfolgreich' if success else 'Fehlgeschlagen'
if error_message:
status_text += f' - {error_message}'
# Verwende Paragraph-Objekte für automatischen Textumbruch
# Erstelle speziellen Style für die linke Spalte
label_style = ParagraphStyle(
name='LabelCell',
parent=self.styles['TableCell'],
alignment=TA_RIGHT,
fontName='Helvetica-Bold'
)
summary_data = [
[Paragraph('Status:', label_style), Paragraph(status_text, self.styles['TableCell'])],
[Paragraph('URL:', label_style), Paragraph(url, self.styles['TableCell'])],
[Paragraph('Speicherort:', label_style), Paragraph(save_path, self.styles['TableCell'])],
[Paragraph('Startzeit:', label_style), Paragraph(start_time.strftime('%d.%m.%Y %H:%M:%S'), self.styles['TableCell'])],
[Paragraph('Endzeit:', label_style), Paragraph(end_time.strftime('%d.%m.%Y %H:%M:%S'), self.styles['TableCell'])],
[Paragraph('Dauer:', label_style), Paragraph(f"{duration.total_seconds():.1f} Sekunden", self.styles['TableCell'])],
[Paragraph('Gesicherte Dateien:', label_style), Paragraph(str(len(downloaded_resources)), self.styles['TableCell'])],
[Paragraph('Übersprungene URLs:', label_style), Paragraph(str(len(skipped_urls)), self.styles['TableCell'])],
]
# Erhöhe die linke Spalte für längere deutsche Texte
summary_table = Table(summary_data, colWidths=[4.5*cm, 11*cm])
summary_table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (0, -1), colors.HexColor('#F8F9FA')),
('VALIGN', (0, 0), (-1, -1), 'TOP'),
('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor('#E9ECEF')),
('ROWBACKGROUNDS', (0, 0), (-1, -1), [colors.white, colors.HexColor('#F8F9FA')]),
('TOPPADDING', (0, 0), (-1, -1), 8),
('BOTTOMPADDING', (0, 0), (-1, -1), 8),
('LEFTPADDING', (0, 0), (0, -1), 5),
('RIGHTPADDING', (0, 0), (0, -1), 10),
]))
story.append(summary_table)
story.append(Spacer(1, 0.3*inch))
# Fehlerinformationen wenn vorhanden
if not success and error_message:
story.append(Paragraph("Fehlerdetails", self.styles['SectionHeader']))
error_style = ParagraphStyle(
name='ErrorText',
parent=self.styles['Normal'],
fontSize=12,
textColor=colors.HexColor('#DC3545'),
leftIndent=20,
spaceBefore=6,
spaceAfter=6
)
story.append(Paragraph(error_message, error_style))
story.append(Spacer(1, 0.3*inch))
# Download-Optionen
story.append(Paragraph("Download-Optionen", self.styles['SectionHeader']))
# Sicherungsart
backup_type = options.get('backup_type', 'snapshot')
backup_type_text = 'Webseiten-Snapshot' if backup_type == 'snapshot' else 'Gesamte Webseite'
options_data = [
['Sicherungsart:', Paragraph(backup_type_text, self.styles['TableCell'])],
]
# Füge weitere Optionen nur bei "Gesamte Webseite" hinzu
if backup_type == 'full':
options_data.extend([
['Bilder herunterladen:', Paragraph('Ja' if options.get('download_images', False) else 'Nein', self.styles['TableCell'])],
['Videos herunterladen:', Paragraph('Ja' if options.get('download_videos', False) else 'Nein', self.styles['TableCell'])],
['CSS-Dateien herunterladen:', Paragraph('Ja' if options.get('download_css', False) else 'Nein', self.styles['TableCell'])],
['JavaScript herunterladen:', Paragraph('Ja' if options.get('download_js', False) else 'Nein', self.styles['TableCell'])],
['Maximale Tiefe:', Paragraph(str(options.get('max_depth', 0)), self.styles['TableCell'])],
])
options_table = Table(options_data, colWidths=[6*cm, 9.5*cm])
options_table.setStyle(TableStyle([
('FONTSIZE', (0, 0), (-1, -1), 10),
('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor('#E9ECEF')),
('TOPPADDING', (0, 0), (-1, -1), 6),
('BOTTOMPADDING', (0, 0), (-1, -1), 6),
]))
story.append(options_table)
story.append(Spacer(1, 0.3*inch))
# Übersprungene URLs
if skipped_urls:
story.append(Paragraph("Übersprungene URLs", self.styles['SectionHeader']))
story.append(Paragraph(
f"Insgesamt wurden {len(skipped_urls)} URLs übersprungen:",
self.styles['InfoText']
))
story.append(Spacer(1, 0.1*inch))
# Gruppiere nach Fehlertyp
error_groups = {}
for url, reason in skipped_urls.items():
if reason not in error_groups:
error_groups[reason] = []
error_groups[reason].append(url)
# Zeige Fehlergruppen
for reason, urls in sorted(error_groups.items()):
story.append(Paragraph(f"<b>{reason}</b> ({len(urls)} URLs):", self.styles['Normal']))
# Zeige maximal 10 URLs pro Gruppe
for url in urls[:10]:
story.append(Paragraph(f"{url}", self.styles['ErrorText']))
if len(urls) > 10:
story.append(Paragraph(
f"... und {len(urls) - 10} weitere",
self.styles['ErrorText']
))
story.append(Spacer(1, 0.1*inch))
# Erfolgreich gesicherte Dateien
if downloaded_resources:
story.append(PageBreak())
story.append(Paragraph("Gesicherte Dateien", self.styles['SectionHeader']))
story.append(Paragraph(
f"Insgesamt wurden {len(downloaded_resources)} Dateien erfolgreich gesichert.",
self.styles['InfoText']
))
# Kategorisiere Dateien nach Typ
categories = {
'HTML/Webseiten': ['.html', '.htm', '.xhtml', '.php', '.asp', '.aspx', '.jsp'],
'Bilder': ['.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp', '.ico', '.bmp', '.tiff'],
'CSS/Stylesheets': ['.css', '.scss', '.sass', '.less'],
'JavaScript': ['.js', '.mjs', '.jsx', '.ts', '.tsx'],
'Videos': ['.mp4', '.webm', '.ogg', '.avi', '.mov', '.flv', '.wmv', '.m4v'],
'Schriften': ['.woff', '.woff2', '.ttf', '.otf', '.eot'],
'Dokumente': ['.pdf', '.doc', '.docx', '.txt', '.rtf'],
'Sonstige': []
}
# Zuordnung der Dateien zu Kategorien
categorized_files = {cat: [] for cat in categories}
unknown_extensions = set()
for url, local_path in downloaded_resources.items():
ext = os.path.splitext(url)[1].lower()
if not ext:
# Prüfe ob es eine Zahl als Extension ist (.0, .1, etc)
parts = url.split('.')
if len(parts) > 1 and parts[-1].isdigit():
ext = '.' + parts[-1]
unknown_extensions.add(ext)
# Finde passende Kategorie
categorized = False
for category, extensions in categories.items():
if category != 'Sonstige' and ext in extensions:
categorized_files[category].append((url, local_path))
categorized = True
break
if not categorized:
categorized_files['Sonstige'].append((url, local_path))
# Zeige Kategorien-Übersicht
story.append(Spacer(1, 0.3*inch))
story.append(Paragraph("Übersicht nach Kategorie:", self.styles['InfoText']))
category_data = []
for category, files in categorized_files.items():
if files: # Nur Kategorien mit Dateien anzeigen
category_data.append([category, str(len(files))])
if category_data:
category_table = Table(category_data, colWidths=[6*cm, 2*cm])
category_table.setStyle(TableStyle([
('FONTSIZE', (0, 0), (-1, -1), 10),
('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor('#E9ECEF')),
('TOPPADDING', (0, 0), (-1, -1), 4),
('BOTTOMPADDING', (0, 0), (-1, -1), 4),
('BACKGROUND', (0, 0), (0, -1), colors.HexColor('#F8F9FA')),
('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'),
]))
story.append(category_table)
# Hinweis zu unbekannten Extensions
if unknown_extensions:
story.append(Spacer(1, 0.2*inch))
story.append(Paragraph(
f"Hinweis: Dateien mit numerischen Endungen ({', '.join(sorted(unknown_extensions))}) " +
"sind vermutlich versionierte Ressourcen oder Chunks von größeren Dateien.",
self.styles['InfoText']
))
# Detaillierte Liste pro Kategorie (begrenzt auf erste 20 pro Kategorie)
for category, files in categorized_files.items():
if files:
story.append(Spacer(1, 0.3*inch))
story.append(Paragraph(f"{category} ({len(files)} Dateien):", self.styles['InfoText']))
# Zeige maximal 20 Einträge pro Kategorie
display_files = files[:20]
if len(files) > 20:
story.append(Paragraph(
f"(Zeige erste 20 von {len(files)} Dateien)",
self.styles['CustomSubtitle']
))
file_data = []
for url, local_path in display_files:
# Verwende SmallTableCell Style für besseren Textumbruch
file_data.append([
Paragraph(url, self.styles['SmallTableCell']),
Paragraph(os.path.basename(local_path), self.styles['SmallTableCell'])
])
# Dynamische Spaltenbreiten: mehr Platz für URLs
file_table = Table(file_data, colWidths=[12*cm, 3.5*cm])
file_table.setStyle(TableStyle([
('FONTSIZE', (0, 0), (-1, -1), 8),
('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor('#E9ECEF')),
('VALIGN', (0, 0), (-1, -1), 'TOP'),
('TOPPADDING', (0, 0), (-1, -1), 2),
('BOTTOMPADDING', (0, 0), (-1, -1), 2),
]))
story.append(file_table)
# Übersprungene URLs
if skipped_urls:
story.append(PageBreak())
story.append(Paragraph("Übersprungene URLs", self.styles['SectionHeader']))
story.append(Paragraph(
f"Es wurden {len(skipped_urls)} URLs übersprungen.",
self.styles['InfoText']
))
story.append(Spacer(1, 0.3*inch))
# Gruppiere nach Fehlergrund
skip_reasons = {}
for url, reason in skipped_urls.items():
if reason not in skip_reasons:
skip_reasons[reason] = []
skip_reasons[reason].append(url)
for reason, urls in skip_reasons.items():
story.append(Paragraph(f"{reason} ({len(urls)} URLs):", self.styles['InfoText']))
# Zeige maximal 10 URLs pro Grund
display_urls = urls[:10]
if len(urls) > 10:
story.append(Paragraph(
f"(Zeige erste 10 von {len(urls)} URLs)",
self.styles['CustomSubtitle']
))
skip_data = []
for url in display_urls:
# Verwende SmallTableCell für automatischen Umbruch
skip_data.append([Paragraph(url, self.styles['SmallTableCell'])])
skip_table = Table(skip_data, colWidths=[15.5*cm])
skip_table.setStyle(TableStyle([
('FONTSIZE', (0, 0), (-1, -1), 8),
('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor('#E9ECEF')),
('VALIGN', (0, 0), (-1, -1), 'TOP'),
('TOPPADDING', (0, 0), (-1, -1), 2),
('BOTTOMPADDING', (0, 0), (-1, -1), 2),
]))
story.append(skip_table)
story.append(Spacer(1, 0.2*inch))
# Footer
story.append(Spacer(1, 0.5*inch))
story.append(Paragraph(
f"Bericht erstellt am {datetime.now().strftime('%d.%m.%Y um %H:%M:%S')}",
self.styles['CustomSubtitle']
))
# Erstelle PDF
doc.build(story)
return output_path

4
start.bat Normale Datei
Datei anzeigen

@ -0,0 +1,4 @@
@echo off
echo IntelSight Webseiten-Crawler wird gestartet...
python main.py
pause