Export drin noch nicht komplett
Dieser Commit ist enthalten in:
@ -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.
|
||||
|
||||
234
controllers/profile_export_controller.py
Normale Datei
234
controllers/profile_export_controller.py
Normale Datei
@ -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
|
||||
@ -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)
|
||||
|
||||
479
utils/profile_export_service.py
Normale Datei
479
utils/profile_export_service.py
Normale Datei
@ -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
|
||||
389
views/dialogs/export_success_dialog.py
Normale Datei
389
views/dialogs/export_success_dialog.py
Normale Datei
@ -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_()
|
||||
324
views/dialogs/profile_export_dialog.py
Normale Datei
324
views/dialogs/profile_export_dialog.py
Normale Datei
@ -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
|
||||
@ -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):
|
||||
"""
|
||||
|
||||
@ -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"""
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren