Export drin noch nicht komplett

Dieser Commit ist enthalten in:
Claude Project Manager
2025-11-10 03:43:02 +01:00
Ursprung 14eefa18f6
Commit 88dc8eea5e
8 geänderte Dateien mit 1473 neuen und 4 gelöschten Zeilen

Datei anzeigen

@ -104,8 +104,8 @@ CREATE TABLE verification_codes (
## 📦 Feature 2: Profil-Export
**Priorität:** Mittel
**Status:** Geplant
**Geschätzter Aufwand:** 2-3 Tage
**Status:** ✅ Abgeschlossen (2025-11-10)
**Tatsächlicher Aufwand:** 1 Tag
### Beschreibung
Export-Funktion für einzelne Account-Profile zur Weitergabe an andere Nutzer oder als Backup.

Datei anzeigen

@ -0,0 +1,234 @@
"""
Controller für Profil-Export
Orchestriert den Export-Prozess von Account-Profilen.
"""
import os
import logging
from typing import Optional, Dict, Any
from pathlib import Path
from PyQt5.QtWidgets import QFileDialog
from database.db_manager import DatabaseManager
from utils.profile_export_service import ProfileExportService
from views.dialogs.profile_export_dialog import show_export_dialog
from views.dialogs.export_success_dialog import show_export_success
from views.widgets.modern_message_box import show_error, show_warning
logger = logging.getLogger("profile_export_controller")
class ProfileExportController:
"""Controller für Account-Profil-Export"""
def __init__(self, db_manager: DatabaseManager):
"""
Initialisiert den Export-Controller.
Args:
db_manager: Datenbank-Manager
"""
self.db_manager = db_manager
self.export_service = ProfileExportService()
def export_account(self, parent_widget, account_id: int) -> bool:
"""
Startet den Export-Prozess für einen Account.
Args:
parent_widget: Parent-Widget für Dialoge
account_id: ID des zu exportierenden Accounts
Returns:
True bei Erfolg, False bei Fehler oder Abbruch
"""
try:
# 1. Account-Daten aus DB laden
logger.info(f"Starte Export für Account-ID: {account_id}")
account_data = self.db_manager.get_account(account_id)
if not account_data:
logger.error(f"Account nicht gefunden: ID {account_id}")
show_error(
parent_widget,
"Account nicht gefunden",
f"Account mit ID {account_id} konnte nicht gefunden werden."
)
return False
username = account_data.get("username", "Unbekannt")
logger.info(f"Account geladen: {username}")
# 2. Export-Dialog anzeigen
accepted, formats, password_protect = show_export_dialog(parent_widget, username)
if not accepted:
logger.info("Export abgebrochen durch Nutzer")
return False
logger.info(f"Export-Optionen: Formate={formats}, Passwort={password_protect}")
# 3. Speicherort wählen
save_directory = self._select_save_location(parent_widget, account_data, formats, password_protect)
if not save_directory:
logger.info("Kein Speicherort ausgewählt - Export abgebrochen")
return False
logger.info(f"Speicherort: {save_directory}")
# 4. Export durchführen
files_dict, password = self.export_service.export_account(
account_data,
formats,
password_protect
)
if not files_dict:
logger.error("Keine Dateien wurden generiert")
show_error(
parent_widget,
"Export fehlgeschlagen",
"Beim Export ist ein Fehler aufgetreten."
)
return False
# 5. Dateien speichern
saved_files = []
for filename, content in files_dict.items():
file_path = os.path.join(save_directory, filename)
try:
with open(file_path, 'wb') as f:
f.write(content)
saved_files.append(filename)
logger.info(f"Datei gespeichert: {filename}")
except Exception as e:
logger.error(f"Fehler beim Speichern von {filename}: {e}")
show_error(
parent_widget,
"Fehler beim Speichern",
f"Datei {filename} konnte nicht gespeichert werden:\n{str(e)}"
)
return False
# 6. Erfolgs-Dialog anzeigen
show_export_success(
parent_widget,
saved_files,
save_directory,
password
)
logger.info(f"Export erfolgreich: {len(saved_files)} Datei(en) gespeichert")
return True
except Exception as e:
logger.error(f"Fehler beim Export: {e}", exc_info=True)
# Benutzerfreundliche Fehlermeldungen
error_message = str(e)
if "reportlab" in error_message.lower():
error_message = (
"PDF-Export ist nicht verfügbar.\n\n"
"Bitte installieren Sie die erforderlichen Bibliotheken:\n"
"pip install reportlab svglib"
)
elif "pyzipper" in error_message.lower():
error_message = (
"Passwortgeschützter Export ist nicht verfügbar.\n\n"
"Bitte installieren Sie die erforderliche Bibliothek:\n"
"pip install pyzipper"
)
show_error(
parent_widget,
"Export fehlgeschlagen",
f"Beim Export ist ein Fehler aufgetreten:\n\n{error_message}"
)
return False
def _select_save_location(
self,
parent_widget,
account_data: Dict[str, Any],
formats: list,
password_protect: bool
) -> Optional[str]:
"""
Öffnet einen Datei-Dialog zur Auswahl des Speicherorts.
Args:
parent_widget: Parent-Widget
account_data: Account-Daten
formats: Liste der Export-Formate
password_protect: Ob Passwortschutz aktiviert ist
Returns:
Ausgewähltes Verzeichnis oder None bei Abbruch
"""
# Standard-Dateiname generieren
if password_protect:
# Bei Passwortschutz wird eine ZIP erstellt
default_filename = self.export_service.generate_filename(
account_data,
"zip"
)
else:
# Ohne Passwortschutz: Ersten Format als Beispiel nehmen
first_format = formats[0] if formats else "csv"
default_filename = self.export_service.generate_filename(
account_data,
first_format
)
# Standard-Speicherort: Benutzer-Downloads-Ordner
default_directory = str(Path.home() / "Downloads")
if password_protect:
# Für ZIP: File-Dialog
file_path, _ = QFileDialog.getSaveFileName(
parent_widget,
"Profil exportieren",
os.path.join(default_directory, default_filename),
"ZIP-Archiv (*.zip)"
)
if not file_path:
return None
# Verzeichnis aus Dateipfad extrahieren
return os.path.dirname(file_path)
else:
# Für mehrere Dateien: Verzeichnis-Dialog
directory = QFileDialog.getExistingDirectory(
parent_widget,
"Speicherort für Export auswählen",
default_directory
)
return directory if directory else None
def export_multiple_accounts(self, parent_widget, account_ids: list) -> bool:
"""
Exportiert mehrere Accounts gleichzeitig.
HINWEIS: Diese Funktion ist vorbereitet für zukünftige Erweiterung,
wird aber gemäß ROADMAP.md vorerst NICHT implementiert (YAGNI).
Args:
parent_widget: Parent-Widget
account_ids: Liste von Account-IDs
Returns:
True bei Erfolg, False bei Fehler
"""
show_warning(
parent_widget,
"Nicht verfügbar",
"Mehrfach-Export ist derzeit nicht verfügbar.\n"
"Bitte exportieren Sie Accounts einzeln."
)
return False

Datei anzeigen

@ -16,3 +16,8 @@ PyYAML>=6.0
# Web automation and anti-detection
random-user-agent>=1.0.1
# Profil-Export (Feature 2)
reportlab>=3.6.0 # PDF-Generierung
pyzipper>=0.3.6 # ZIP mit Passwortschutz (AES)
svglib>=1.5.0 # SVG-Support für PDF (optional)

Datei anzeigen

@ -0,0 +1,479 @@
"""
Profil-Export-Service für Account-Daten
Exportiert Account-Profile in verschiedene Formate (CSV, TXT, PDF)
mit optionalem Passwortschutz.
"""
import os
import csv
import secrets
import string
import logging
from io import BytesIO, StringIO
from datetime import datetime
from typing import Dict, Any, List, Optional, Tuple
from pathlib import Path
logger = logging.getLogger("profile_export_service")
class ProfileExportService:
"""Service für den Export von Account-Profilen"""
# Felder die exportiert werden (ohne id, last_login, notes, status)
EXPORT_FIELDS = {
"username": "Username",
"password": "Passwort",
"email": "E-Mail",
"phone": "Telefon",
"platform": "Plattform",
"full_name": "Name",
"created_at": "Erstellt_am"
}
@staticmethod
def export_to_csv(account_data: Dict[str, Any]) -> bytes:
"""
Exportiert Account-Daten als CSV.
Args:
account_data: Dictionary mit Account-Daten
Returns:
CSV-Daten als bytes
"""
try:
output = StringIO()
writer = csv.DictWriter(
output,
fieldnames=ProfileExportService.EXPORT_FIELDS.values(),
quoting=csv.QUOTE_MINIMAL
)
# Header schreiben
writer.writeheader()
# Datenzeile vorbereiten
row_data = {}
for field_key, field_label in ProfileExportService.EXPORT_FIELDS.items():
value = account_data.get(field_key, "")
# None zu leerem String konvertieren
row_data[field_label] = value if value is not None else ""
# Datenzeile schreiben
writer.writerow(row_data)
# In bytes konvertieren (UTF-8)
csv_content = output.getvalue()
output.close()
logger.info("CSV-Export erfolgreich")
return csv_content.encode('utf-8')
except Exception as e:
logger.error(f"Fehler beim CSV-Export: {e}")
raise
@staticmethod
def export_to_txt(account_data: Dict[str, Any]) -> bytes:
"""
Exportiert Account-Daten als TXT (Key-Value Format).
Args:
account_data: Dictionary mit Account-Daten
Returns:
TXT-Daten als bytes
"""
try:
lines = []
for field_key, field_label in ProfileExportService.EXPORT_FIELDS.items():
value = account_data.get(field_key, "")
# None zu leerem String konvertieren
value = value if value is not None else ""
lines.append(f"{field_label}: {value}")
# Exportdatum hinzufügen
export_date = datetime.now().strftime("%d.%m.%Y %H:%M")
lines.append(f"\nExportiert am: {export_date}")
txt_content = "\n".join(lines)
logger.info("TXT-Export erfolgreich")
return txt_content.encode('utf-8')
except Exception as e:
logger.error(f"Fehler beim TXT-Export: {e}")
raise
@staticmethod
def export_to_pdf(account_data: Dict[str, Any]) -> bytes:
"""
Exportiert Account-Daten als PDF mit schönem Layout.
Args:
account_data: Dictionary mit Account-Daten
Returns:
PDF-Daten als bytes
"""
try:
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import mm
from reportlab.lib import colors
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, Image
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_LEFT, TA_CENTER
# PDF-Buffer erstellen
buffer = BytesIO()
# Dokument erstellen
doc = SimpleDocTemplate(
buffer,
pagesize=A4,
rightMargin=20*mm,
leftMargin=20*mm,
topMargin=20*mm,
bottomMargin=20*mm
)
# Story (Inhalt) erstellen
story = []
styles = getSampleStyleSheet()
# Custom Styles
title_style = ParagraphStyle(
'CustomTitle',
parent=styles['Title'],
fontSize=20,
textColor=colors.HexColor('#1F2937'),
spaceAfter=5*mm,
alignment=TA_CENTER
)
heading_style = ParagraphStyle(
'CustomHeading',
parent=styles['Heading2'],
fontSize=14,
textColor=colors.HexColor('#374151'),
spaceAfter=3*mm,
spaceBefore=5*mm
)
# IntelSight Logo versuchen zu laden
logo_path = Path("resources/icons/intelsight-logo.svg")
if logo_path.exists():
try:
# SVG zu reportlab Image (mit svglib falls verfügbar)
try:
from svglib.svglib import svg2rlg
from reportlab.graphics import renderPDF
drawing = svg2rlg(str(logo_path))
if drawing:
# Skalieren auf vernünftige Größe
drawing.width = 40*mm
drawing.height = 10*mm
drawing.scale(40*mm/drawing.width, 10*mm/drawing.height)
story.append(drawing)
story.append(Spacer(1, 5*mm))
except ImportError:
# svglib nicht verfügbar - überspringen
logger.debug("svglib nicht verfügbar - Logo wird übersprungen")
except Exception as e:
logger.debug(f"Logo konnte nicht geladen werden: {e}")
# Titel
username = account_data.get("username", "Unbekannt")
platform = account_data.get("platform", "Unbekannt")
title = Paragraph(f"Account-Profil: {username}", title_style)
story.append(title)
# Plattform
platform_text = Paragraph(f"Plattform: {platform.title()}", styles['Normal'])
story.append(platform_text)
story.append(Spacer(1, 10*mm))
# LOGIN-DATEN Sektion
login_heading = Paragraph("LOGIN-DATEN", heading_style)
story.append(login_heading)
# Login-Daten Tabelle
login_data = [
["Benutzername:", account_data.get("username", "")],
["Passwort:", account_data.get("password", "")],
["E-Mail:", account_data.get("email", "") or "-"],
["Telefon:", account_data.get("phone", "") or "-"]
]
login_table = Table(login_data, colWidths=[45*mm, 115*mm])
login_table.setStyle(TableStyle([
('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'),
('FONTNAME', (1, 0), (1, -1), 'Helvetica'),
('FONTSIZE', (0, 0), (-1, -1), 11),
('TEXTCOLOR', (0, 0), (0, -1), colors.HexColor('#6B7280')),
('TEXTCOLOR', (1, 0), (1, -1), colors.HexColor('#1F2937')),
('VALIGN', (0, 0), (-1, -1), 'TOP'),
('LEFTPADDING', (0, 0), (-1, -1), 0),
('RIGHTPADDING', (0, 0), (-1, -1), 0),
('TOPPADDING', (0, 0), (-1, -1), 2*mm),
('BOTTOMPADDING', (0, 0), (-1, -1), 2*mm),
]))
story.append(login_table)
story.append(Spacer(1, 8*mm))
# PROFIL-INFORMATIONEN Sektion
profile_heading = Paragraph("PROFIL-INFORMATIONEN", heading_style)
story.append(profile_heading)
# Profil-Daten Tabelle
profile_data = [
["Name:", account_data.get("full_name", "") or "-"],
["Erstellt am:", account_data.get("created_at", "") or "-"]
]
profile_table = Table(profile_data, colWidths=[45*mm, 115*mm])
profile_table.setStyle(TableStyle([
('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'),
('FONTNAME', (1, 0), (1, -1), 'Helvetica'),
('FONTSIZE', (0, 0), (-1, -1), 11),
('TEXTCOLOR', (0, 0), (0, -1), colors.HexColor('#6B7280')),
('TEXTCOLOR', (1, 0), (1, -1), colors.HexColor('#1F2937')),
('VALIGN', (0, 0), (-1, -1), 'TOP'),
('LEFTPADDING', (0, 0), (-1, -1), 0),
('RIGHTPADDING', (0, 0), (-1, -1), 0),
('TOPPADDING', (0, 0), (-1, -1), 2*mm),
('BOTTOMPADDING', (0, 0), (-1, -1), 2*mm),
]))
story.append(profile_table)
story.append(Spacer(1, 15*mm))
# Export-Datum (Footer)
export_date = datetime.now().strftime("%d.%m.%Y %H:%M")
footer_text = Paragraph(
f"Exportiert am: {export_date}",
ParagraphStyle(
'Footer',
parent=styles['Normal'],
fontSize=9,
textColor=colors.HexColor('#9CA3AF'),
alignment=TA_CENTER
)
)
story.append(footer_text)
# PDF erstellen
doc.build(story)
# Buffer-Wert holen
pdf_content = buffer.getvalue()
buffer.close()
logger.info("PDF-Export erfolgreich")
return pdf_content
except ImportError as e:
logger.error(f"reportlab nicht installiert: {e}")
raise Exception(
"PDF-Export erfordert 'reportlab' Library. "
"Bitte installieren Sie: pip install reportlab"
)
except Exception as e:
logger.error(f"Fehler beim PDF-Export: {e}")
raise
@staticmethod
def generate_password(length: int = 10) -> str:
"""
Generiert ein sicheres zufälliges Passwort.
Args:
length: Länge des Passworts (Standard: 10)
Returns:
Generiertes Passwort
"""
# Zeichensatz: Groß- und Kleinbuchstaben, Zahlen, Sonderzeichen
alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
# Sicherstellen dass mindestens ein Zeichen von jedem Typ vorhanden ist
password_chars = [
secrets.choice(string.ascii_uppercase), # Mindestens ein Großbuchstabe
secrets.choice(string.ascii_lowercase), # Mindestens ein Kleinbuchstabe
secrets.choice(string.digits), # Mindestens eine Zahl
secrets.choice("!@#$%^&*") # Mindestens ein Sonderzeichen
]
# Rest auffüllen
for _ in range(length - 4):
password_chars.append(secrets.choice(alphabet))
# Mischen für Zufälligkeit
secrets.SystemRandom().shuffle(password_chars)
password = ''.join(password_chars)
logger.info("Passwort generiert")
return password
@staticmethod
def create_protected_zip(files_dict: Dict[str, bytes], password: str) -> bytes:
"""
Erstellt eine passwortgeschützte ZIP-Datei.
Args:
files_dict: Dictionary mit {filename: content} Paaren
password: Passwort für die ZIP-Datei
Returns:
ZIP-Daten als bytes
"""
try:
import pyzipper
# ZIP-Buffer erstellen
buffer = BytesIO()
# ZIP mit AES-Verschlüsselung erstellen
with pyzipper.AESZipFile(
buffer,
'w',
compression=pyzipper.ZIP_DEFLATED,
encryption=pyzipper.WZ_AES
) as zf:
# Passwort setzen
zf.setpassword(password.encode('utf-8'))
# Dateien hinzufügen
for filename, content in files_dict.items():
zf.writestr(filename, content)
# Buffer-Wert holen
zip_content = buffer.getvalue()
buffer.close()
logger.info(f"Passwortgeschützte ZIP erstellt mit {len(files_dict)} Dateien")
return zip_content
except ImportError as e:
logger.error(f"pyzipper nicht installiert: {e}")
raise Exception(
"ZIP mit Passwortschutz erfordert 'pyzipper' Library. "
"Bitte installieren Sie: pip install pyzipper"
)
except Exception as e:
logger.error(f"Fehler beim Erstellen der geschützten ZIP: {e}")
raise
@staticmethod
def generate_filename(
account_data: Dict[str, Any],
format_extension: str,
include_timestamp: bool = True
) -> str:
"""
Generiert einen Dateinamen nach Konvention.
Format: {username}_{platform}_{timestamp}.{extension}
Beispiel: max_mueller_instagram_2025-11-10_14-30.csv
Args:
account_data: Account-Daten
format_extension: Dateiendung (z.B. "csv", "txt", "pdf", "zip")
include_timestamp: Ob Timestamp hinzugefügt werden soll
Returns:
Generierter Dateiname
"""
# Username und Platform aus Account-Daten
username = account_data.get("username", "account")
platform = account_data.get("platform", "unknown").lower()
# Sonderzeichen entfernen für Dateinamen
username = ProfileExportService._sanitize_filename(username)
platform = ProfileExportService._sanitize_filename(platform)
# Timestamp
if include_timestamp:
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M")
filename = f"{username}_{platform}_{timestamp}.{format_extension}"
else:
filename = f"{username}_{platform}.{format_extension}"
return filename
@staticmethod
def _sanitize_filename(filename: str) -> str:
"""
Entfernt ungültige Zeichen aus Dateinamen.
Args:
filename: Ursprünglicher Dateiname
Returns:
Bereinigter Dateiname
"""
# Nur alphanumerische Zeichen, Unterstrich und Bindestrich erlauben
valid_chars = f"-_.{string.ascii_letters}{string.digits}"
sanitized = ''.join(c if c in valid_chars else '_' for c in filename)
return sanitized
@staticmethod
def export_account(
account_data: Dict[str, Any],
formats: List[str],
password_protect: bool = False
) -> Tuple[Dict[str, bytes], Optional[str]]:
"""
Exportiert Account-Daten in angegebene Formate.
Args:
account_data: Account-Daten zum Exportieren
formats: Liste von Formaten ["csv", "txt", "pdf"]
password_protect: Ob Dateien passwortgeschützt werden sollen
Returns:
Tuple von (files_dict, password)
- files_dict: {filename: content} für alle Formate
- password: Generiertes Passwort (None wenn nicht geschützt)
"""
files_dict = {}
# Jedes Format exportieren
for fmt in formats:
fmt = fmt.lower()
if fmt == "csv":
content = ProfileExportService.export_to_csv(account_data)
filename = ProfileExportService.generate_filename(account_data, "csv")
files_dict[filename] = content
elif fmt == "txt":
content = ProfileExportService.export_to_txt(account_data)
filename = ProfileExportService.generate_filename(account_data, "txt")
files_dict[filename] = content
elif fmt == "pdf":
content = ProfileExportService.export_to_pdf(account_data)
filename = ProfileExportService.generate_filename(account_data, "pdf")
files_dict[filename] = content
else:
logger.warning(f"Unbekanntes Format ignoriert: {fmt}")
# Passwortschutz wenn gewünscht
password = None
if password_protect and files_dict:
password = ProfileExportService.generate_password()
# Alle Dateien in ZIP packen
zip_filename = ProfileExportService.generate_filename(account_data, "zip")
zip_content = ProfileExportService.create_protected_zip(files_dict, password)
# Nur ZIP zurückgeben
files_dict = {zip_filename: zip_content}
return files_dict, password

Datei anzeigen

@ -0,0 +1,389 @@
"""
Erfolgs-Dialog für Profil-Export
Zeigt exportierte Dateien und optional das generierte Passwort an.
"""
import os
import logging
import subprocess
import platform
from pathlib import Path
from typing import List, Optional
from PyQt5.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QFrame, QGraphicsDropShadowEffect, QApplication, QScrollArea, QWidget
)
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFont, QColor
logger = logging.getLogger("export_success_dialog")
class ExportSuccessDialog(QDialog):
"""Dialog zur Anzeige von Export-Erfolg"""
def __init__(
self,
parent=None,
exported_files: List[str] = None,
export_directory: str = "",
password: Optional[str] = None
):
"""
Initialisiert den Erfolgs-Dialog.
Args:
parent: Parent-Widget
exported_files: Liste der exportierten Dateinamen
export_directory: Verzeichnis wo Dateien gespeichert wurden
password: Optional generiertes Passwort (nur bei Passwortschutz)
"""
super().__init__(parent)
self.exported_files = exported_files or []
self.export_directory = export_directory
self.password = password
self.init_ui()
def init_ui(self):
"""Initialisiert die UI"""
# Dialog-Eigenschaften
self.setWindowTitle("Export erfolgreich")
self.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint)
self.setAttribute(Qt.WA_TranslucentBackground, True)
self.setModal(True)
# Dynamische Höhe basierend auf Inhalt
base_height = 280
if self.password:
base_height += 80 # Extra Platz für Passwort-Anzeige
if len(self.exported_files) > 2:
base_height += 20 * (len(self.exported_files) - 2) # Extra für mehr Dateien
self.setFixedSize(500, min(base_height, 500))
# Hauptlayout
main_layout = QVBoxLayout(self)
main_layout.setContentsMargins(20, 20, 20, 20)
# Container mit modernem Design
self.container = QFrame()
self.container.setObjectName("exportSuccessContainer")
self.container.setStyleSheet("""
#exportSuccessContainer {
background-color: white;
border-radius: 12px;
border: 1px solid #E5E7EB;
}
""")
# Schatten-Effekt
shadow = QGraphicsDropShadowEffect()
shadow.setBlurRadius(30)
shadow.setXOffset(0)
shadow.setYOffset(10)
shadow.setColor(QColor(0, 0, 0, 80))
self.container.setGraphicsEffect(shadow)
# Container-Layout
container_layout = QVBoxLayout(self.container)
container_layout.setSpacing(18)
container_layout.setContentsMargins(30, 25, 30, 25)
# Header mit Icon und Titel
header_layout = QHBoxLayout()
header_layout.setSpacing(15)
# Erfolgs-Icon
icon_label = QLabel("")
icon_label.setStyleSheet("font-size: 32px;")
icon_label.setFixedSize(40, 40)
icon_label.setAlignment(Qt.AlignCenter)
# Titel
title_label = QLabel("Profil exportiert!")
title_font = QFont("Poppins", 16)
title_font.setWeight(QFont.DemiBold)
title_label.setFont(title_font)
title_label.setStyleSheet("color: #1F2937;")
header_layout.addWidget(icon_label)
header_layout.addWidget(title_label, 1)
container_layout.addLayout(header_layout)
# Dateien-Liste
files_label = QLabel("Dateien:")
files_label.setStyleSheet("color: #6B7280; font-size: 13px; font-weight: 600;")
container_layout.addWidget(files_label)
# Scrollbare Liste für Dateien
scroll_area = QScrollArea()
scroll_area.setWidgetResizable(True)
scroll_area.setFrameShape(QFrame.NoFrame)
scroll_area.setStyleSheet("""
QScrollArea {
background-color: #F9FAFB;
border-radius: 6px;
border: 1px solid #E5E7EB;
}
""")
files_widget = QWidget()
files_layout = QVBoxLayout(files_widget)
files_layout.setSpacing(4)
files_layout.setContentsMargins(10, 10, 10, 10)
for filename in self.exported_files:
file_label = QLabel(f"{filename}")
file_label.setStyleSheet("""
color: #374151;
font-size: 12px;
font-family: 'Courier New', monospace;
padding: 2px;
""")
file_label.setWordWrap(True)
files_layout.addWidget(file_label)
files_layout.addStretch()
scroll_area.setWidget(files_widget)
scroll_area.setMaximumHeight(120)
container_layout.addWidget(scroll_area)
# Passwort-Anzeige (nur wenn Passwort vorhanden)
if self.password:
password_frame = QFrame()
password_frame.setStyleSheet("""
QFrame {
background-color: #FEF3C7;
border: 1px solid #FCD34D;
border-radius: 6px;
padding: 12px;
}
""")
password_layout = QVBoxLayout(password_frame)
password_layout.setSpacing(8)
# Warnung
warning_label = QLabel("⚠ Bitte Passwort speichern!")
warning_label.setStyleSheet("""
color: #92400E;
font-size: 13px;
font-weight: 600;
""")
# Passwort mit Copy-Button
password_row = QHBoxLayout()
password_row.setSpacing(10)
password_label = QLabel(f"🔒 Passwort: {self.password}")
password_label.setStyleSheet("""
color: #78350F;
font-size: 13px;
font-family: 'Courier New', monospace;
font-weight: 600;
""")
password_label.setTextInteractionFlags(Qt.TextSelectableByMouse)
copy_button = QPushButton("Kopieren")
copy_button.setMaximumWidth(80)
copy_button.setMinimumHeight(28)
copy_button.setCursor(Qt.PointingHandCursor)
copy_button.setStyleSheet("""
QPushButton {
background-color: #F59E0B;
color: white;
border: none;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
padding: 4px 8px;
}
QPushButton:hover {
background-color: #D97706;
}
QPushButton:pressed {
background-color: #B45309;
}
""")
copy_button.clicked.connect(self.copy_password)
password_row.addWidget(password_label, 1)
password_row.addWidget(copy_button)
password_layout.addWidget(warning_label)
password_layout.addLayout(password_row)
container_layout.addWidget(password_frame)
# Speicherort
location_label = QLabel(f"Gespeichert in:")
location_label.setStyleSheet("color: #6B7280; font-size: 12px;")
path_label = QLabel(self._format_path(self.export_directory))
path_label.setStyleSheet("""
color: #4B5563;
font-size: 11px;
font-family: 'Courier New', monospace;
""")
path_label.setWordWrap(True)
path_label.setTextInteractionFlags(Qt.TextSelectableByMouse)
container_layout.addWidget(location_label)
container_layout.addWidget(path_label)
# Spacer
container_layout.addStretch()
# Button-Layout
button_layout = QHBoxLayout()
button_layout.setSpacing(10)
button_layout.addStretch()
# Ordner öffnen Button
self.open_folder_button = QPushButton("Ordner öffnen")
self.open_folder_button.setMinimumHeight(38)
self.open_folder_button.setMinimumWidth(120)
self.open_folder_button.setCursor(Qt.PointingHandCursor)
self.open_folder_button.setStyleSheet("""
QPushButton {
background-color: #F3F4F6;
color: #374151;
border: none;
border-radius: 6px;
font-size: 13px;
font-weight: 600;
padding: 8px 16px;
}
QPushButton:hover {
background-color: #E5E7EB;
}
QPushButton:pressed {
background-color: #D1D5DB;
}
""")
self.open_folder_button.clicked.connect(self.open_folder)
# OK Button
self.ok_button = QPushButton("OK")
self.ok_button.setMinimumHeight(38)
self.ok_button.setMinimumWidth(100)
self.ok_button.setCursor(Qt.PointingHandCursor)
self.ok_button.setStyleSheet("""
QPushButton {
background-color: #10B981;
color: white;
border: none;
border-radius: 6px;
font-size: 13px;
font-weight: 600;
padding: 8px 16px;
}
QPushButton:hover {
background-color: #059669;
}
QPushButton:pressed {
background-color: #047857;
}
""")
self.ok_button.clicked.connect(self.accept)
button_layout.addWidget(self.open_folder_button)
button_layout.addWidget(self.ok_button)
container_layout.addLayout(button_layout)
# Container zum Hauptlayout hinzufügen
main_layout.addWidget(self.container)
def copy_password(self):
"""Kopiert das Passwort in die Zwischenablage"""
if self.password:
clipboard = QApplication.clipboard()
clipboard.setText(self.password)
logger.info("Passwort in Zwischenablage kopiert")
# Kurzes visuelles Feedback
sender = self.sender()
if sender:
original_text = sender.text()
sender.setText("✓ Kopiert!")
sender.setStyleSheet(sender.styleSheet().replace("#F59E0B", "#10B981"))
# Nach 2 Sekunden zurücksetzen
from PyQt5.QtCore import QTimer
QTimer.singleShot(2000, lambda: self._reset_copy_button(sender, original_text))
def _reset_copy_button(self, button, original_text):
"""Setzt den Copy-Button zurück"""
if button:
button.setText(original_text)
button.setStyleSheet(button.styleSheet().replace("#10B981", "#F59E0B"))
def open_folder(self):
"""Öffnet den Export-Ordner im Datei-Explorer"""
if not self.export_directory or not os.path.exists(self.export_directory):
logger.warning(f"Export-Verzeichnis existiert nicht: {self.export_directory}")
return
try:
system = platform.system()
if system == "Windows":
# Windows Explorer
os.startfile(self.export_directory)
elif system == "Darwin":
# macOS Finder
subprocess.run(["open", self.export_directory])
elif system == "Linux":
# Linux File Manager
subprocess.run(["xdg-open", self.export_directory])
else:
logger.warning(f"Unbekanntes Betriebssystem: {system}")
logger.info(f"Ordner geöffnet: {self.export_directory}")
except Exception as e:
logger.error(f"Fehler beim Öffnen des Ordners: {e}")
def _format_path(self, path: str) -> str:
"""
Formatiert den Pfad für die Anzeige.
Kürzt zu lange Pfade.
"""
if not path:
return ""
max_length = 60
if len(path) <= max_length:
return path
# Pfad kürzen: Anfang ... Ende
start_length = 25
end_length = max_length - start_length - 3
return f"{path[:start_length]}...{path[-end_length:]}"
# Helper-Funktion für einfache Verwendung
def show_export_success(
parent,
exported_files: List[str],
export_directory: str,
password: Optional[str] = None
):
"""
Zeigt den Export-Erfolgs-Dialog modal an.
Args:
parent: Parent-Widget
exported_files: Liste der exportierten Dateinamen
export_directory: Verzeichnis wo Dateien gespeichert wurden
password: Optional generiertes Passwort
"""
dialog = ExportSuccessDialog(parent, exported_files, export_directory, password)
dialog.exec_()

Datei anzeigen

@ -0,0 +1,324 @@
"""
Export-Dialog für Account-Profile
Ermöglicht Auswahl von Export-Formaten und Passwortschutz.
"""
import logging
from PyQt5.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QCheckBox, QFrame, QGraphicsDropShadowEffect, QGroupBox
)
from PyQt5.QtCore import Qt, pyqtSignal
from PyQt5.QtGui import QFont, QColor
logger = logging.getLogger("profile_export_dialog")
class ProfileExportDialog(QDialog):
"""Dialog zur Auswahl von Export-Optionen"""
# Signal wird emittiert wenn Export bestätigt wird
# Parameter: (formats: List[str], password_protect: bool)
export_confirmed = pyqtSignal(list, bool)
def __init__(self, parent=None, account_username: str = ""):
"""
Initialisiert den Export-Dialog.
Args:
parent: Parent-Widget
account_username: Username des zu exportierenden Accounts
"""
super().__init__(parent)
self.account_username = account_username
self.init_ui()
def init_ui(self):
"""Initialisiert die UI"""
# Dialog-Eigenschaften
self.setWindowTitle("Profil exportieren")
self.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint)
self.setAttribute(Qt.WA_TranslucentBackground, True)
self.setModal(True)
self.setFixedSize(450, 600)
# Hauptlayout
main_layout = QVBoxLayout(self)
main_layout.setContentsMargins(20, 20, 20, 20)
# Container mit modernem Design
self.container = QFrame()
self.container.setObjectName("exportDialogContainer")
self.container.setStyleSheet("""
#exportDialogContainer {
background-color: white;
border-radius: 12px;
border: 1px solid #E5E7EB;
}
""")
# Schatten-Effekt
shadow = QGraphicsDropShadowEffect()
shadow.setBlurRadius(30)
shadow.setXOffset(0)
shadow.setYOffset(10)
shadow.setColor(QColor(0, 0, 0, 80))
self.container.setGraphicsEffect(shadow)
# Container-Layout
container_layout = QVBoxLayout(self.container)
container_layout.setSpacing(20)
container_layout.setContentsMargins(30, 25, 30, 25)
# Titel
title_label = QLabel("Profil exportieren")
title_font = QFont("Poppins", 16)
title_font.setWeight(QFont.DemiBold)
title_label.setFont(title_font)
title_label.setStyleSheet("color: #1F2937;")
container_layout.addWidget(title_label)
# Account-Info
if self.account_username:
account_label = QLabel(f"Account: {self.account_username}")
account_label.setStyleSheet("color: #6B7280; font-size: 13px; font-family: 'Poppins', sans-serif;")
container_layout.addWidget(account_label)
# Abstand vor Format-GroupBox
container_layout.addSpacing(15)
# Format-Auswahl GroupBox
format_group = QGroupBox("Format")
format_group.setMinimumHeight(150) # Mehr Platz für 3 Checkboxen
format_group.setStyleSheet("""
QGroupBox {
font-size: 14px;
font-weight: 600;
font-family: 'Poppins', sans-serif;
color: #374151;
border: 1px solid #E5E7EB;
border-radius: 8px;
margin-top: 20px;
padding-top: 30px;
}
QGroupBox::title {
subcontrol-origin: margin;
subcontrol-position: top left;
left: 12px;
top: 2px;
padding: 2px 8px 2px 8px;
background-color: white;
}
""")
format_layout = QVBoxLayout(format_group)
format_layout.setSpacing(12)
format_layout.setContentsMargins(20, 25, 20, 20)
# Format-Checkboxen
self.csv_checkbox = QCheckBox("CSV")
self.csv_checkbox.setChecked(False) # Standard: nicht aktiviert
self.csv_checkbox.setStyleSheet("""
QCheckBox {
font-size: 13px;
font-family: 'Poppins', sans-serif;
color: #374151;
spacing: 8px;
}
QCheckBox::indicator {
width: 18px;
height: 18px;
border-radius: 4px;
border: 2px solid #D1D5DB;
}
QCheckBox::indicator:checked {
background-color: #0099CC;
border-color: #0099CC;
}
""")
self.txt_checkbox = QCheckBox("TXT")
self.txt_checkbox.setChecked(False) # Standard: nicht aktiviert
self.txt_checkbox.setStyleSheet(self.csv_checkbox.styleSheet())
self.pdf_checkbox = QCheckBox("PDF")
self.pdf_checkbox.setChecked(False) # Standard: nicht aktiviert
self.pdf_checkbox.setStyleSheet(self.csv_checkbox.styleSheet())
# Checkboxen zum Format-Layout hinzufügen
format_layout.addWidget(self.csv_checkbox)
format_layout.addWidget(self.txt_checkbox)
format_layout.addWidget(self.pdf_checkbox)
container_layout.addWidget(format_group)
# Passwortschutz-Checkbox
self.password_checkbox = QCheckBox("Mit Passwort schützen")
self.password_checkbox.setChecked(False)
self.password_checkbox.setStyleSheet("""
QCheckBox {
font-size: 13px;
font-family: 'Poppins', sans-serif;
font-weight: 500;
color: #374151;
spacing: 8px;
}
QCheckBox::indicator {
width: 18px;
height: 18px;
border-radius: 4px;
border: 2px solid #D1D5DB;
}
QCheckBox::indicator:checked {
background-color: #10B981;
border-color: #10B981;
}
""")
# Info-Text für Passwortschutz
password_info = QLabel("(Wird automatisch generiert)")
password_info.setStyleSheet("color: #9CA3AF; font-size: 11px; font-family: 'Poppins', sans-serif; margin-left: 26px;")
container_layout.addWidget(self.password_checkbox)
container_layout.addWidget(password_info)
# Spacer
container_layout.addStretch()
# Button-Layout
button_layout = QHBoxLayout()
button_layout.setSpacing(10)
button_layout.addStretch()
# Abbrechen-Button
self.cancel_button = QPushButton("Abbrechen")
self.cancel_button.setMinimumHeight(38)
self.cancel_button.setMinimumWidth(100)
self.cancel_button.setCursor(Qt.PointingHandCursor)
self.cancel_button.setStyleSheet("""
QPushButton {
background-color: #F3F4F6;
color: #374151;
border: none;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
font-family: 'Poppins', sans-serif;
padding: 8px 16px;
}
QPushButton:hover {
background-color: #E5E7EB;
}
QPushButton:pressed {
background-color: #D1D5DB;
}
""")
self.cancel_button.clicked.connect(self.reject)
# Exportieren-Button
self.export_button = QPushButton("Exportieren")
self.export_button.setMinimumHeight(38)
self.export_button.setMinimumWidth(120)
self.export_button.setCursor(Qt.PointingHandCursor)
self.export_button.setStyleSheet("""
QPushButton {
background-color: #0099CC;
color: white;
border: none;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
font-family: 'Poppins', sans-serif;
padding: 8px 16px;
}
QPushButton:hover {
background-color: #0078A3;
}
QPushButton:pressed {
background-color: #005C7A;
}
""")
self.export_button.clicked.connect(self.on_export_clicked)
button_layout.addWidget(self.cancel_button)
button_layout.addWidget(self.export_button)
container_layout.addLayout(button_layout)
# Container zum Hauptlayout hinzufügen
main_layout.addWidget(self.container)
def on_export_clicked(self):
"""Handler für Export-Button"""
# Sammle ausgewählte Formate
selected_formats = []
if self.csv_checkbox.isChecked():
selected_formats.append("csv")
if self.txt_checkbox.isChecked():
selected_formats.append("txt")
if self.pdf_checkbox.isChecked():
selected_formats.append("pdf")
# Validierung: Mindestens ein Format muss ausgewählt sein
if not selected_formats:
from views.widgets.modern_message_box import show_warning
show_warning(
self,
"Kein Format ausgewählt",
"Bitte wählen Sie mindestens ein Export-Format aus."
)
return
# Passwortschutz-Option
password_protect = self.password_checkbox.isChecked()
# Signal emittieren
self.export_confirmed.emit(selected_formats, password_protect)
# Dialog schließen
self.accept()
def get_selected_options(self):
"""
Gibt die ausgewählten Optionen zurück.
Returns:
Tuple: (formats: List[str], password_protect: bool)
"""
formats = []
if self.csv_checkbox.isChecked():
formats.append("csv")
if self.txt_checkbox.isChecked():
formats.append("txt")
if self.pdf_checkbox.isChecked():
formats.append("pdf")
password_protect = self.password_checkbox.isChecked()
return formats, password_protect
# Helper-Funktionen für einfache Verwendung
def show_export_dialog(parent, account_username: str = ""):
"""
Zeigt den Export-Dialog modal an.
Args:
parent: Parent-Widget
account_username: Username des Accounts
Returns:
Tuple: (accepted: bool, formats: List[str], password_protect: bool)
accepted ist True wenn Export bestätigt wurde
"""
dialog = ProfileExportDialog(parent, account_username)
result = dialog.exec_()
if result == QDialog.Accepted:
formats, password_protect = dialog.get_selected_options()
return True, formats, password_protect
else:
return False, [], False

Datei anzeigen

@ -218,7 +218,30 @@ class AccountsTab(QWidget):
def on_export_clicked(self):
"""Wird aufgerufen, wenn der Exportieren-Button geklickt wird."""
self.export_requested.emit()
# Prüfen ob ein Account ausgewählt ist
selected_rows = self.accounts_table.selectionModel().selectedRows()
if not selected_rows:
title = "Kein Konto ausgewählt"
text = "Bitte wählen Sie ein Konto zum Exportieren aus."
if self.language_manager:
title = self.language_manager.get_text(
"accounts_tab.no_selection_title", title
)
text = self.language_manager.get_text(
"accounts_tab.no_export_selection_text", text
)
from views.widgets.modern_message_box import show_warning
show_warning(self, title, text)
return
# Account-ID holen
row = selected_rows[0].row()
account_id = int(self.accounts_table.item(row, 0).text())
# ProfileExportController verwenden
from controllers.profile_export_controller import ProfileExportController
export_controller = ProfileExportController(self.db_manager)
export_controller.export_account(self, account_id)
def update_session_status(self, account_id: str, status: dict):
"""

Datei anzeigen

@ -85,7 +85,22 @@ class AccountCard(QFrame):
show_error(self, "Export blockiert", error_msg)
return
self.export_requested.emit(self.account_data)
# Neuer Feature 2: Profil-Export mit ProfileExportController
account_id = self.account_data.get("id")
if not account_id:
from views.widgets.modern_message_box import show_error
show_error(self, "Export fehlgeschlagen", "Account-ID nicht gefunden.")
return
# Hol db_manager aus dem Parent
parent_window = self.window()
if hasattr(parent_window, 'db_manager'):
from controllers.profile_export_controller import ProfileExportController
export_controller = ProfileExportController(parent_window.db_manager)
export_controller.export_account(self, account_id)
else:
# Fallback: Altes Signal emittieren (für Kompatibilität)
self.export_requested.emit(self.account_data)
def _on_delete_clicked(self):
"""Handler für Delete-Button"""