Dieser Commit ist enthalten in:
Claude Project Manager
2025-08-01 23:50:28 +02:00
Commit 04585e95b6
290 geänderte Dateien mit 64086 neuen und 0 gelöschten Zeilen

420
views/widgets/account_card.py Normale Datei
Datei anzeigen

@ -0,0 +1,420 @@
"""
Account Card Widget - Kompakte Account-Karte nach Styleguide
"""
from PyQt5.QtWidgets import (
QFrame, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QGridLayout, QWidget, QApplication
)
from PyQt5.QtCore import Qt, pyqtSignal, QSize, QTimer
from PyQt5.QtGui import QFont, QPixmap
import os
from views.widgets.icon_factory import IconFactory
class AccountCard(QFrame):
"""
Kompakte Account-Karte nach Styleguide für Light Mode
"""
# Signals
login_requested = pyqtSignal(dict) # Account-Daten
export_requested = pyqtSignal(dict) # Account-Daten
delete_requested = pyqtSignal(dict) # Account-Daten
def __init__(self, account_data, language_manager=None):
super().__init__()
self.account_data = account_data
self.language_manager = language_manager
self.password_visible = False
# Timer für Icon-Animation
self.email_copy_timer = QTimer()
self.email_copy_timer.timeout.connect(self._restore_email_copy_icon)
self.password_copy_timer = QTimer()
self.password_copy_timer.timeout.connect(self._restore_password_copy_icon)
# Original Icons speichern
self.copy_icon = None
self.check_icon = None
self.init_ui()
if self.language_manager:
self.language_manager.language_changed.connect(self.update_texts)
self.update_texts()
def _on_login_clicked(self):
"""Handler für Login-Button"""
self.login_requested.emit(self.account_data)
def init_ui(self):
"""Initialisiert die UI nach Styleguide"""
self.setObjectName("accountCard")
# Status-basiertes Styling anwenden
self._apply_status_styling()
# Setze feste Breite für die Karte
self.setFixedWidth(360)
# Hauptlayout
layout = QVBoxLayout(self)
layout.setContentsMargins(16, 16, 16, 16)
layout.setSpacing(12)
# Header Zeile
header_layout = QHBoxLayout()
# Platform Icon + Username
info_layout = QHBoxLayout()
info_layout.setSpacing(8)
# Status wird jetzt über Karten-Hintergrund und Umrandung angezeigt
# Platform Icon
platform_icon = IconFactory.create_icon_label(
self.account_data.get("platform", "").lower(),
size=18
)
info_layout.addWidget(platform_icon)
# Username
username_label = QLabel(self.account_data.get("username", ""))
username_font = QFont("Poppins", 16)
username_font.setWeight(QFont.DemiBold)
username_label.setFont(username_font)
username_label.setStyleSheet("""
color: #1A365D;
font-family: 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
""")
info_layout.addWidget(username_label)
info_layout.addStretch()
header_layout.addLayout(info_layout)
# Login Button
self.login_btn = QPushButton("Login")
self.login_btn.setCursor(Qt.PointingHandCursor)
self.login_btn.setStyleSheet("""
QPushButton {
background-color: #3182CE;
color: #FFFFFF;
border: none;
border-radius: 6px;
padding: 6px 16px;
font-size: 12px;
font-weight: 500;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
min-width: 60px;
}
QPushButton:hover {
background-color: #2563EB;
}
QPushButton:pressed {
background-color: #1D4ED8;
}
""")
self.login_btn.clicked.connect(lambda: self._on_login_clicked())
header_layout.addWidget(self.login_btn)
layout.addLayout(header_layout)
# Details Grid
details_grid = QGridLayout()
details_grid.setSpacing(8)
# Email
email_icon = IconFactory.create_icon_label("mail", size=14)
details_grid.addWidget(email_icon, 0, 0)
# Email container with copy button
email_container = QWidget()
email_layout = QHBoxLayout(email_container)
email_layout.setContentsMargins(0, 0, 0, 0)
email_layout.setSpacing(8)
email_label = QLabel(self.account_data.get("email", ""))
email_label.setStyleSheet("""
color: #4A5568;
font-size: 13px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
""")
email_layout.addWidget(email_label)
# Email Copy Button
email_copy_btn = QPushButton()
email_copy_btn.setToolTip("E-Mail kopieren")
email_copy_btn.setCursor(Qt.PointingHandCursor)
email_copy_btn.setStyleSheet("""
QPushButton {
background: transparent;
border: none;
padding: 2px;
min-width: 20px;
max-width: 20px;
min-height: 20px;
max-height: 20px;
}
QPushButton:hover {
background-color: #F7FAFC;
border-radius: 4px;
}
""")
self.copy_icon = IconFactory.get_icon("copy", size=16)
self.check_icon = IconFactory.get_icon("check", size=16, color="#10B981")
self.email_copy_btn = email_copy_btn
self.email_copy_btn.setIcon(self.copy_icon)
self.email_copy_btn.setIconSize(QSize(16, 16))
email_copy_btn.clicked.connect(self._copy_email)
email_layout.addWidget(email_copy_btn)
email_layout.addStretch()
details_grid.addWidget(email_container, 0, 1)
# Password
pass_icon = IconFactory.create_icon_label("key", size=14)
details_grid.addWidget(pass_icon, 1, 0)
pass_container = QWidget()
pass_layout = QHBoxLayout(pass_container)
pass_layout.setContentsMargins(0, 0, 0, 0)
pass_layout.setSpacing(8)
self.password_label = QLabel("••••••••")
self.password_label.setStyleSheet("""
color: #4A5568;
font-size: 13px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
""")
pass_layout.addWidget(self.password_label)
# Copy Button
copy_btn = QPushButton()
copy_btn.setToolTip("Passwort kopieren")
copy_btn.setCursor(Qt.PointingHandCursor)
copy_btn.setStyleSheet("""
QPushButton {
background: transparent;
border: none;
padding: 2px;
min-width: 20px;
max-width: 20px;
min-height: 20px;
max-height: 20px;
}
QPushButton:hover {
background-color: #F7FAFC;
border-radius: 4px;
}
""")
self.password_copy_btn = copy_btn
self.password_copy_btn.setIcon(self.copy_icon)
self.password_copy_btn.setIconSize(QSize(16, 16))
copy_btn.clicked.connect(self._copy_password)
pass_layout.addWidget(copy_btn)
# Show/Hide Button
self.visibility_btn = QPushButton()
self.visibility_btn.setToolTip("Passwort anzeigen")
self.visibility_btn.setCursor(Qt.PointingHandCursor)
self.visibility_btn.setStyleSheet("""
QPushButton {
background: transparent;
border: none;
padding: 2px;
min-width: 20px;
max-width: 20px;
min-height: 20px;
max-height: 20px;
}
QPushButton:hover {
background-color: #F7FAFC;
border-radius: 4px;
}
""")
self.eye_icon = IconFactory.get_icon("eye", size=16)
self.eye_slash_icon = IconFactory.get_icon("eye-slash", size=16)
self.visibility_btn.setIcon(self.eye_icon)
self.visibility_btn.setIconSize(QSize(16, 16))
self.visibility_btn.clicked.connect(self._toggle_password)
pass_layout.addWidget(self.visibility_btn)
pass_layout.addStretch()
details_grid.addWidget(pass_container, 1, 1)
# Created Date
date_icon = IconFactory.create_icon_label("calendar", size=14)
details_grid.addWidget(date_icon, 2, 0)
date_label = QLabel(self.account_data.get("created_at", ""))
date_label.setStyleSheet("""
color: #A0AEC0;
font-size: 12px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
""")
details_grid.addWidget(date_label, 2, 1)
layout.addLayout(details_grid)
# Action buttons
actions_layout = QHBoxLayout()
actions_layout.setSpacing(8)
# Export Button
self.export_btn = QPushButton("Profil\nexportieren")
self.export_btn.setCursor(Qt.PointingHandCursor)
self.export_btn.setStyleSheet("""
QPushButton {
background-color: #10B981;
color: #FFFFFF;
border: none;
border-radius: 6px;
padding: 4px 12px;
font-size: 12px;
font-weight: 500;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
min-width: 120px;
min-height: 36px;
text-align: center;
}
QPushButton:hover {
background-color: #059669;
}
QPushButton:pressed {
background-color: #047857;
}
""")
self.export_btn.clicked.connect(lambda: self.export_requested.emit(self.account_data))
actions_layout.addWidget(self.export_btn)
actions_layout.addStretch()
# Delete Button
self.delete_btn = QPushButton("Löschen")
self.delete_btn.setCursor(Qt.PointingHandCursor)
self.delete_btn.setStyleSheet("""
QPushButton {
background-color: #DC2626;
color: #FFFFFF;
border: none;
border-radius: 6px;
padding: 8px 16px;
font-size: 12px;
font-weight: 500;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
min-width: 90px;
}
QPushButton:hover {
background-color: #B91C1C;
}
QPushButton:pressed {
background-color: #991B1B;
}
""")
self.delete_btn.clicked.connect(lambda: self.delete_requested.emit(self.account_data))
actions_layout.addWidget(self.delete_btn)
layout.addLayout(actions_layout)
def _toggle_password(self):
"""Zeigt/Versteckt das Passwort"""
self.password_visible = not self.password_visible
if self.password_visible:
self.password_label.setText(self.account_data.get("password", ""))
self.visibility_btn.setIcon(self.eye_slash_icon)
else:
self.password_label.setText("••••••••")
self.visibility_btn.setIcon(self.eye_icon)
def _copy_password(self):
"""Kopiert das Passwort in die Zwischenablage"""
clipboard = QApplication.clipboard()
clipboard.setText(self.account_data.get("password", ""))
# Icon zu Check wechseln
self.password_copy_btn.setIcon(self.check_icon)
# Timer starten um nach 2 Sekunden zurückzuwechseln
self.password_copy_timer.stop()
self.password_copy_timer.start(2000)
def _copy_email(self):
"""Kopiert die E-Mail in die Zwischenablage"""
clipboard = QApplication.clipboard()
clipboard.setText(self.account_data.get("email", ""))
# Icon zu Check wechseln
self.email_copy_btn.setIcon(self.check_icon)
# Timer starten um nach 2 Sekunden zurückzuwechseln
self.email_copy_timer.stop()
self.email_copy_timer.start(2000)
def update_texts(self):
"""Aktualisiert die Texte gemäß der aktuellen Sprache"""
if not self.language_manager:
return
self.login_btn.setText(
self.language_manager.get_text("account_card.login", "Login")
)
self.export_btn.setText(
self.language_manager.get_text("account_card.export", "Profil\nexportieren")
)
self.delete_btn.setText(
self.language_manager.get_text("account_card.delete", "Löschen")
)
def _restore_email_copy_icon(self):
"""Stellt das ursprüngliche Copy Icon für Email wieder her"""
self.email_copy_btn.setIcon(self.copy_icon)
self.email_copy_timer.stop()
def _restore_password_copy_icon(self):
"""Stellt das ursprüngliche Copy Icon für Passwort wieder her"""
self.password_copy_btn.setIcon(self.copy_icon)
self.password_copy_timer.stop()
def _apply_status_styling(self):
"""Wendet Status-basiertes Styling mit Pastel-Hintergrund und farbiger Umrandung an"""
# Status aus Account-Daten lesen - Standard ist "active" (grün)
status = self.account_data.get("status", "active")
# Status-Farben definieren (nur Grün/Rot)
status_styles = {
"active": {
"background": "#F0FDF4", # Sehr helles Mintgrün
"border": "#10B981", # Kräftiges Grün
"hover_border": "#059669" # Dunkleres Grün beim Hover
},
"inactive": {
"background": "#FEF2F2", # Sehr helles Rosa
"border": "#EF4444", # Kräftiges Rot
"hover_border": "#DC2626" # Dunkleres Rot beim Hover
}
}
# Aktueller Status oder Fallback auf active (grün)
current_style = status_styles.get(status, status_styles["active"])
# CSS-Styling anwenden
self.setStyleSheet(f"""
QFrame#accountCard {{
background-color: {current_style["background"]};
border: 2px solid {current_style["border"]};
border-radius: 8px;
padding: 16px;
}}
QFrame#accountCard:hover {{
border: 2px solid {current_style["hover_border"]};
background-color: {current_style["background"]};
}}
""")
def update_status(self, new_status: str):
"""Aktualisiert den Status der Account-Karte und das Styling"""
self.account_data["status"] = new_status
self._apply_status_styling()

Datei anzeigen

@ -0,0 +1,209 @@
"""
Account Creation Modal - Spezialisiertes Modal für Account-Erstellung
"""
import logging
from typing import Optional, List
from PyQt5.QtCore import QTimer, pyqtSignal
from PyQt5.QtWidgets import QVBoxLayout, QLabel, QProgressBar
from PyQt5.QtGui import QFont
from views.widgets.progress_modal import ProgressModal
from styles.modal_styles import ModalStyles
logger = logging.getLogger("account_creation_modal")
class AccountCreationModal(ProgressModal):
"""
Spezialisiertes Modal für Account-Erstellung mit Step-by-Step Anzeige.
"""
# Signale
step_completed = pyqtSignal(str) # Step-Name
def __init__(self, parent=None, platform: str = "Social Media", language_manager=None, style_manager=None):
self.platform = platform
self.current_step = 0
self.total_steps = 0
self.steps = []
super().__init__(parent, "account_creation", language_manager, style_manager)
# Erweitere UI für Steps
self.setup_steps_ui()
def setup_steps_ui(self):
"""Erweitert die UI um Step-Anzeige"""
container_layout = self.modal_container.layout()
# Progress Bar (zwischen Animation und Status)
self.progress_bar = QProgressBar()
self.progress_bar.setVisible(False)
self.progress_bar.setFixedHeight(self.style_manager.SIZES['progress_bar_height'])
self.progress_bar.setStyleSheet(self.style_manager.get_progress_bar_style())
# Füge Progress Bar nach Animation Widget ein (Index 2)
container_layout.insertWidget(2, self.progress_bar)
# Steps Label (zwischen Progress Bar und Status)
self.steps_label = QLabel()
self.steps_label.setVisible(False)
self.steps_label.setAlignment(self.title_label.alignment())
self.steps_label.setFont(self.style_manager.create_font('steps'))
self.steps_label.setStyleSheet(self.style_manager.get_steps_label_style())
# Füge Steps Label nach Progress Bar ein (Index 3)
container_layout.insertWidget(3, self.steps_label)
def set_steps(self, steps: List[str]):
"""
Setzt die Liste der Schritte für die Account-Erstellung.
Args:
steps: Liste der Step-Namen
"""
self.steps = steps
self.total_steps = len(steps)
self.current_step = 0
if self.total_steps > 0:
self.progress_bar.setMaximum(self.total_steps)
self.progress_bar.setValue(0)
self.progress_bar.setVisible(True)
self.steps_label.setVisible(True)
self._update_steps_display()
def start_step(self, step_name: str, detail: str = None):
"""
Startet einen neuen Schritt.
Args:
step_name: Name des Schritts
detail: Optional - Detail-Information
"""
if step_name in self.steps:
self.current_step = self.steps.index(step_name) + 1
else:
self.current_step += 1
# Progress Bar aktualisieren
if self.progress_bar.isVisible():
self.progress_bar.setValue(self.current_step)
# Status aktualisieren
self.update_status(f"🔄 {step_name}", detail)
# Steps Display aktualisieren
self._update_steps_display()
logger.info(f"Account Creation Step gestartet: {step_name} ({self.current_step}/{self.total_steps})")
def complete_step(self, step_name: str, next_step: str = None):
"""
Markiert einen Schritt als abgeschlossen.
Args:
step_name: Name des abgeschlossenen Schritts
next_step: Optional - Name des nächsten Schritts
"""
# Step-completed Signal senden
self.step_completed.emit(step_name)
# Kurz "Completed" anzeigen
self.update_status(f"{step_name} abgeschlossen")
# Nach kurzer Verzögerung nächsten Schritt starten
if next_step:
QTimer.singleShot(self.style_manager.ANIMATIONS['step_delay'], lambda: self.start_step(next_step))
logger.info(f"Account Creation Step abgeschlossen: {step_name}")
def fail_step(self, step_name: str, error_message: str, retry_callback=None):
"""
Markiert einen Schritt als fehlgeschlagen.
Args:
step_name: Name des fehlgeschlagenen Schritts
error_message: Fehlermeldung
retry_callback: Optional - Callback für Retry
"""
self.update_status(f"{step_name} fehlgeschlagen", error_message)
# Animation stoppen
self.animation_widget.stop_animation()
logger.error(f"Account Creation Step fehlgeschlagen: {step_name} - {error_message}")
def show_platform_specific_process(self):
"""Zeigt plattform-spezifische Account-Erstellung an"""
# Plattform-spezifische Steps
platform_steps = self._get_platform_steps()
self.set_steps(platform_steps)
# Titel anpassen
title = f"🔄 {self.platform} Account wird erstellt"
self.title_label.setText(title)
# Modal anzeigen
self.show_process("account_creation")
def _get_platform_steps(self) -> List[str]:
"""Gibt plattform-spezifische Schritte zurück"""
return self.style_manager.get_platform_steps(self.platform)
def _update_steps_display(self):
"""Aktualisiert die Steps-Anzeige"""
if self.total_steps > 0:
steps_text = f"Schritt {self.current_step} von {self.total_steps}"
self.steps_label.setText(steps_text)
def show_success(self, account_data: dict = None):
"""
Zeigt Erfolgs-Status an.
Args:
account_data: Optional - Account-Daten für Anzeige
"""
# Animation stoppen
self.animation_widget.stop_animation()
# Success Status
platform_name = account_data.get('platform', self.platform) if account_data else self.platform
username = account_data.get('username', '') if account_data else ''
title = f"{platform_name} Account erstellt!"
status = f"Account '{username}' wurde erfolgreich erstellt" if username else "Account wurde erfolgreich erstellt"
self.title_label.setText(title)
self.update_status(status, "Das Fenster schließt automatisch...")
# Progress Bar auf Maximum setzen
if self.progress_bar.isVisible():
self.progress_bar.setValue(self.total_steps)
# Auto-Close nach konfigurierbarer Zeit
QTimer.singleShot(self.style_manager.ANIMATIONS['auto_close_delay'], self.hide_process)
logger.info(f"Account Creation erfolgreich für: {platform_name}")
def estimate_time_remaining(self) -> str:
"""
Schätzt die verbleibende Zeit basierend auf aktueller Step.
Returns:
str: Geschätzte Zeit als String
"""
if self.total_steps == 0:
return "Unbekannt"
# Grobe Zeitschätzung: ~30 Sekunden pro Step
remaining_steps = max(0, self.total_steps - self.current_step)
estimated_seconds = remaining_steps * 30
if estimated_seconds < 60:
return f"~{estimated_seconds}s"
else:
minutes = estimated_seconds // 60
return f"~{minutes} Min"

Datei anzeigen

@ -0,0 +1,625 @@
"""
Account Creation Modal V2 - Verbessertes Design mit eindringlicher Warnung
Basierend auf dem Corporate Design Styleguide
"""
import logging
from typing import Optional, List, Dict
from datetime import datetime, timedelta
from PyQt5.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QFrame, QWidget, QScrollArea,
QProgressBar, QGraphicsOpacityEffect
)
from PyQt5.QtCore import (
Qt, QTimer, pyqtSignal, QPropertyAnimation,
QEasingCurve, QRect, QSize
)
from PyQt5.QtGui import QFont, QPainter, QBrush, QColor, QPen, QPixmap
logger = logging.getLogger("account_creation_modal_v2")
class AccountCreationModalV2(QDialog):
"""
Verbessertes Modal für Account-Erstellung mit:
- Prominenter Warnung
- Besserer Step-Visualisierung
- Größerem Fenster
- Klarerer Kommunikation
"""
# Signale
cancel_clicked = pyqtSignal()
process_completed = pyqtSignal()
def __init__(self, parent=None, platform: str = "Social Media"):
super().__init__(parent)
self.platform = platform
self.current_step = 0
self.total_steps = 0
self.steps = []
self.start_time = None
self.is_process_running = False
# Timer für Updates
self.update_timer = QTimer()
self.update_timer.timeout.connect(self.update_time_display)
self.update_timer.setInterval(1000) # Jede Sekunde
# Animation Timer für Warning
self.warning_animation_timer = QTimer()
self.warning_animation_timer.timeout.connect(self.animate_warning)
self.warning_animation_timer.setInterval(100) # Smooth animation
self.warning_opacity = 1.0
self.warning_direction = -0.02
self.init_ui()
def init_ui(self):
"""Initialisiert die UI mit verbessertem Design"""
# Modal-Eigenschaften
self.setModal(True)
self.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
self.setAttribute(Qt.WA_TranslucentBackground)
# Größeres Fenster
self.setFixedSize(650, 700)
# Zentriere auf Parent oder Bildschirm
if self.parent():
parent_rect = self.parent().geometry()
x = parent_rect.x() + (parent_rect.width() - self.width()) // 2
y = parent_rect.y() + (parent_rect.height() - self.height()) // 2
self.move(x, y)
# Hauptlayout
main_layout = QVBoxLayout(self)
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.setSpacing(0)
# Container mit weißem Hintergrund
self.container = QFrame()
self.container.setObjectName("mainContainer")
self.container.setStyleSheet("""
QFrame#mainContainer {
background-color: #FFFFFF;
border: 1px solid #E2E8F0;
border-radius: 16px;
}
""")
container_layout = QVBoxLayout(self.container)
container_layout.setContentsMargins(0, 0, 0, 0)
container_layout.setSpacing(0)
# 1. WARNING BANNER (Sehr auffällig!)
self.warning_banner = self.create_warning_banner()
container_layout.addWidget(self.warning_banner)
# Content Area mit Padding
content_widget = QWidget()
content_layout = QVBoxLayout(content_widget)
content_layout.setContentsMargins(40, 30, 40, 40)
content_layout.setSpacing(24)
# 2. Titel-Bereich
self.title_label = QLabel(f"Account wird erstellt für {self.platform}")
self.title_label.setAlignment(Qt.AlignCenter)
self.title_label.setStyleSheet("""
QLabel {
color: #1A365D;
font-size: 28px;
font-weight: 700;
font-family: 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
margin-bottom: 8px;
}
""")
content_layout.addWidget(self.title_label)
# 3. Progress-Bereich
progress_widget = self.create_progress_section()
content_layout.addWidget(progress_widget)
# 4. Steps-Liste
self.steps_widget = self.create_steps_list()
content_layout.addWidget(self.steps_widget)
# 5. Live-Status
self.status_widget = self.create_status_section()
content_layout.addWidget(self.status_widget)
# 6. Info-Box
info_box = self.create_info_box()
content_layout.addWidget(info_box)
# Spacer
content_layout.addStretch()
# Container zum Hauptlayout hinzufügen
container_layout.addWidget(content_widget)
main_layout.addWidget(self.container)
def create_warning_banner(self) -> QFrame:
"""Erstellt den auffälligen Warning Banner"""
banner = QFrame()
banner.setObjectName("warningBanner")
banner.setFixedHeight(100)
banner.setStyleSheet("""
QFrame#warningBanner {
background-color: #DC2626;
border-top-left-radius: 16px;
border-top-right-radius: 16px;
border-bottom: none;
}
""")
layout = QVBoxLayout(banner)
layout.setContentsMargins(40, 20, 40, 20)
layout.setSpacing(8)
# Warning Icon + Haupttext
main_warning = QLabel("⚠️ NICHT DEN BROWSER BERÜHREN!")
main_warning.setAlignment(Qt.AlignCenter)
main_warning.setStyleSheet("""
QLabel {
color: #FFFFFF;
font-size: 24px;
font-weight: 700;
font-family: 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
letter-spacing: 1px;
}
""")
# Untertitel
subtitle = QLabel("Die Automatisierung läuft. Jede Interaktion kann den Prozess unterbrechen.")
subtitle.setAlignment(Qt.AlignCenter)
subtitle.setWordWrap(True)
subtitle.setStyleSheet("""
QLabel {
color: rgba(255, 255, 255, 0.9);
font-size: 14px;
font-weight: 400;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
""")
layout.addWidget(main_warning)
layout.addWidget(subtitle)
return banner
def create_progress_section(self) -> QWidget:
"""Erstellt den Progress-Bereich"""
widget = QWidget()
layout = QHBoxLayout(widget)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(24)
# Progress Bar
progress_container = QWidget()
progress_layout = QVBoxLayout(progress_container)
progress_layout.setContentsMargins(0, 0, 0, 0)
progress_layout.setSpacing(8)
self.progress_bar = QProgressBar()
self.progress_bar.setFixedHeight(12)
self.progress_bar.setStyleSheet("""
QProgressBar {
background-color: #E2E8F0;
border: none;
border-radius: 6px;
text-align: center;
}
QProgressBar::chunk {
background-color: #3182CE;
border-radius: 6px;
}
""")
self.progress_label = QLabel("Schritt 0 von 0")
self.progress_label.setStyleSheet("""
QLabel {
color: #4A5568;
font-size: 14px;
font-weight: 500;
}
""")
progress_layout.addWidget(self.progress_bar)
progress_layout.addWidget(self.progress_label)
# Zeit-Anzeige
time_container = QWidget()
time_container.setFixedWidth(180)
time_layout = QVBoxLayout(time_container)
time_layout.setContentsMargins(0, 0, 0, 0)
time_layout.setSpacing(4)
self.time_label = QLabel("~2 Min verbleibend")
self.time_label.setAlignment(Qt.AlignRight)
self.time_label.setStyleSheet("""
QLabel {
color: #1A365D;
font-size: 16px;
font-weight: 600;
font-family: 'Poppins', -apple-system, sans-serif;
}
""")
self.elapsed_label = QLabel("Verstrichene Zeit: 0:00")
self.elapsed_label.setAlignment(Qt.AlignRight)
self.elapsed_label.setStyleSheet("""
QLabel {
color: #718096;
font-size: 12px;
font-weight: 400;
}
""")
time_layout.addWidget(self.time_label)
time_layout.addWidget(self.elapsed_label)
layout.addWidget(progress_container, 1)
layout.addWidget(time_container)
return widget
def create_steps_list(self) -> QWidget:
"""Erstellt die visuelle Steps-Liste"""
container = QFrame()
container.setStyleSheet("""
QFrame {
background-color: #F8FAFC;
border: 1px solid #E2E8F0;
border-radius: 12px;
padding: 20px;
}
""")
self.steps_layout = QVBoxLayout(container)
self.steps_layout.setSpacing(12)
# Wird dynamisch befüllt
return container
def create_status_section(self) -> QWidget:
"""Erstellt den Live-Status Bereich"""
container = QFrame()
container.setStyleSheet("""
QFrame {
background-color: #1A365D;
border-radius: 12px;
padding: 20px;
min-height: 80px;
}
""")
layout = QVBoxLayout(container)
layout.setSpacing(8)
status_title = QLabel("Aktueller Status:")
status_title.setStyleSheet("""
QLabel {
color: rgba(255, 255, 255, 0.7);
font-size: 12px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 1px;
}
""")
self.current_status_label = QLabel("Initialisiere Browser...")
self.current_status_label.setWordWrap(True)
self.current_status_label.setStyleSheet("""
QLabel {
color: #FFFFFF;
font-size: 16px;
font-weight: 400;
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
line-height: 1.5;
}
""")
layout.addWidget(status_title)
layout.addWidget(self.current_status_label)
return container
def create_info_box(self) -> QWidget:
"""Erstellt die Info-Box"""
container = QFrame()
container.setStyleSheet("""
QFrame {
background-color: #DBEAFE;
border: 1px solid #2563EB;
border-radius: 12px;
padding: 16px;
}
""")
layout = QHBoxLayout(container)
layout.setSpacing(16)
# Info Icon
icon_label = QLabel("")
icon_label.setStyleSheet("""
QLabel {
font-size: 24px;
color: #2563EB;
}
""")
# Info Text
info_layout = QVBoxLayout()
info_layout.setSpacing(4)
info_title = QLabel("Was passiert gerade?")
info_title.setStyleSheet("""
QLabel {
color: #1E40AF;
font-size: 14px;
font-weight: 600;
}
""")
self.info_text = QLabel("Der Browser simuliert menschliches Verhalten beim Ausfüllen des Formulars.")
self.info_text.setWordWrap(True)
self.info_text.setStyleSheet("""
QLabel {
color: #1E40AF;
font-size: 13px;
font-weight: 400;
line-height: 1.4;
}
""")
info_layout.addWidget(info_title)
info_layout.addWidget(self.info_text)
layout.addWidget(icon_label)
layout.addWidget(info_layout, 1)
return container
def set_steps(self, steps: List[str]):
"""Setzt die Steps und erstellt die visuelle Liste"""
self.steps = steps
self.total_steps = len(steps)
self.current_step = 0
# Progress Bar Setup
self.progress_bar.setMaximum(self.total_steps)
self.progress_bar.setValue(0)
# Clear existing steps
for i in reversed(range(self.steps_layout.count())):
self.steps_layout.itemAt(i).widget().setParent(None)
# Create step items
self.step_widgets = []
for i, step in enumerate(steps):
step_widget = self.create_step_item(i, step)
self.steps_layout.addWidget(step_widget)
self.step_widgets.append(step_widget)
def create_step_item(self, index: int, text: str) -> QWidget:
"""Erstellt ein einzelnes Step-Item"""
widget = QWidget()
layout = QHBoxLayout(widget)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(12)
# Status Icon
icon_label = QLabel()
icon_label.setFixedSize(24, 24)
icon_label.setAlignment(Qt.AlignCenter)
icon_label.setObjectName(f"stepIcon_{index}")
# Text
text_label = QLabel(text)
text_label.setObjectName(f"stepText_{index}")
# Initial state (pending)
self.update_step_appearance(widget, 'pending')
layout.addWidget(icon_label)
layout.addWidget(text_label, 1)
return widget
def update_step_appearance(self, widget: QWidget, status: str):
"""Aktualisiert das Aussehen eines Steps basierend auf Status"""
icon_label = widget.findChild(QLabel, QRegExp("stepIcon_*"))
text_label = widget.findChild(QLabel, QRegExp("stepText_*"))
if status == 'completed':
icon_label.setText("")
text_label.setStyleSheet("""
QLabel {
color: #059669;
font-size: 14px;
font-weight: 500;
}
""")
elif status == 'active':
icon_label.setText("🔄")
text_label.setStyleSheet("""
QLabel {
color: #2563EB;
font-size: 14px;
font-weight: 600;
}
""")
# Rotation animation würde hier hinzugefügt
else: # pending
icon_label.setText("")
text_label.setStyleSheet("""
QLabel {
color: #718096;
font-size: 14px;
font-weight: 400;
}
""")
def start_process(self):
"""Startet den Account-Erstellungsprozess"""
self.is_process_running = True
self.start_time = datetime.now()
self.update_timer.start()
self.warning_animation_timer.start()
self.show()
def next_step(self, status_text: str = None, info_text: str = None):
"""Geht zum nächsten Schritt"""
if self.current_step < self.total_steps:
# Aktuellen Step als aktiv markieren
if self.current_step > 0:
self.update_step_appearance(self.step_widgets[self.current_step - 1], 'completed')
if self.current_step < len(self.step_widgets):
self.update_step_appearance(self.step_widgets[self.current_step], 'active')
self.current_step += 1
self.progress_bar.setValue(self.current_step)
self.progress_label.setText(f"Schritt {self.current_step} von {self.total_steps}")
# Update status
if status_text:
self.current_status_label.setText(status_text)
# Update info
if info_text:
self.info_text.setText(info_text)
# Update time estimate
self.update_time_estimate()
def update_time_estimate(self):
"""Aktualisiert die Zeitschätzung"""
if self.total_steps > 0 and self.current_step > 0:
elapsed = (datetime.now() - self.start_time).total_seconds()
avg_time_per_step = elapsed / self.current_step
remaining_steps = self.total_steps - self.current_step
estimated_remaining = remaining_steps * avg_time_per_step
if estimated_remaining < 60:
self.time_label.setText(f"~{int(estimated_remaining)}s verbleibend")
else:
minutes = int(estimated_remaining / 60)
self.time_label.setText(f"~{minutes} Min verbleibend")
def update_time_display(self):
"""Aktualisiert die verstrichene Zeit"""
if self.start_time:
elapsed = datetime.now() - self.start_time
minutes = int(elapsed.total_seconds() / 60)
seconds = int(elapsed.total_seconds() % 60)
self.elapsed_label.setText(f"Verstrichene Zeit: {minutes}:{seconds:02d}")
def animate_warning(self):
"""Animiert den Warning Banner (Pulseffekt)"""
self.warning_opacity += self.warning_direction
if self.warning_opacity <= 0.7:
self.warning_opacity = 0.7
self.warning_direction = 0.02
elif self.warning_opacity >= 1.0:
self.warning_opacity = 1.0
self.warning_direction = -0.02
# Opacity-Effekt anwenden
effect = QGraphicsOpacityEffect()
effect.setOpacity(self.warning_opacity)
self.warning_banner.setGraphicsEffect(effect)
def set_status(self, status: str, info: str = None):
"""Setzt den aktuellen Status"""
self.current_status_label.setText(status)
if info:
self.info_text.setText(info)
def complete_process(self):
"""Beendet den Prozess erfolgreich"""
self.is_process_running = False
self.update_timer.stop()
self.warning_animation_timer.stop()
# Letzten Step als completed markieren
if self.current_step > 0 and self.current_step <= len(self.step_widgets):
self.update_step_appearance(self.step_widgets[self.current_step - 1], 'completed')
# Success message
self.current_status_label.setText("✅ Account erfolgreich erstellt!")
self.info_text.setText("Der Prozess wurde erfolgreich abgeschlossen. Das Fenster schließt sich in wenigen Sekunden.")
# Auto-close nach 3 Sekunden
QTimer.singleShot(3000, self.close)
def paintEvent(self, event):
"""Custom paint event für Transparenz"""
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
# Semi-transparenter Hintergrund
painter.fillRect(self.rect(), QColor(0, 0, 0, 120))
def keyPressEvent(self, event):
"""Verhindert das Schließen mit ESC"""
if event.key() == Qt.Key_Escape:
event.ignore()
else:
super().keyPressEvent(event)
def closeEvent(self, event):
"""Verhindert das Schließen während der Prozess läuft"""
if self.is_process_running:
event.ignore()
else:
self.update_timer.stop()
self.warning_animation_timer.stop()
super().closeEvent(event)
# Beispiel-Verwendung
if __name__ == "__main__":
from PyQt5.QtWidgets import QApplication, QMainWindow
import sys
app = QApplication(sys.argv)
# Test window
main_window = QMainWindow()
main_window.setGeometry(100, 100, 800, 600)
main_window.show()
# Create and show modal
modal = AccountCreationModalV2(main_window, "Instagram")
modal.set_steps([
"Browser vorbereiten",
"Instagram-Seite laden",
"Registrierungsformular öffnen",
"Benutzerdaten eingeben",
"Account erstellen",
"E-Mail-Verifizierung",
"Profil einrichten"
])
# Simulate process
modal.start_process()
# Simulate step progression
def next_step():
modal.next_step(
f"Führe Schritt {modal.current_step + 1} aus...",
"Der Browser füllt automatisch die erforderlichen Felder aus."
)
if modal.current_step < modal.total_steps:
QTimer.singleShot(2000, next_step)
else:
modal.complete_process()
QTimer.singleShot(1000, next_step)
sys.exit(app.exec_())

Datei anzeigen

@ -0,0 +1,272 @@
"""
Forge Animation Widget - Verbesserter Dialog für Account-Erstellung mit prominenter Warnung
"""
from PyQt5.QtWidgets import QDialog, QWidget, QVBoxLayout, QLabel, QTextEdit, QPushButton, QHBoxLayout, QFrame
from PyQt5.QtCore import Qt, pyqtSignal, QTimer
from PyQt5.QtGui import QFont, QMovie, QPixmap
class ForgeAnimationDialog(QDialog):
"""Modal-Dialog für die Account-Erstellung mit verbessertem Design"""
# Signal wenn Abbrechen geklickt wird
cancel_clicked = pyqtSignal()
# Signal wenn Dialog geschlossen wird
closed = pyqtSignal()
def __init__(self, parent=None, platform_name="", is_login=False):
super().__init__(parent)
self.platform_name = platform_name
self.is_login = is_login
self.init_ui()
# Timer für das regelmäßige Nach-vorne-Holen
self.raise_timer = QTimer()
self.raise_timer.timeout.connect(self._raise_to_front)
self.raise_timer.setInterval(500) # Alle 500ms
def init_ui(self):
"""Initialisiert die UI mit verbessertem Design"""
# Nur Dialog im Vordergrund, nicht das ganze Hauptfenster
self.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
self.setModal(False) # Nicht modal - blockiert nicht das Hauptfenster
self.setFixedSize(650, 600) # Ursprüngliche Größe beibehalten
# Styling für Light Theme
self.setStyleSheet("""
ForgeAnimationDialog {
background-color: #FFFFFF;
border: 1px solid #E2E8F0;
border-radius: 8px;
}
""")
# Hauptlayout
layout = QVBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
# NEUE WARNUNG OBEN - Sehr auffällig!
warning_banner = QFrame()
warning_banner.setFixedHeight(120) # Angepasste Höhe
warning_banner.setStyleSheet("""
QFrame {
background-color: #DC2626;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
}
""")
warning_layout = QVBoxLayout(warning_banner)
warning_layout.setContentsMargins(10, 10, 10, 10) # Reduzierte Margins
warning_layout.setSpacing(5) # Weniger Abstand zwischen den Elementen
# Großer Warning Text
warning_text = QLabel("⚠️ BROWSER NICHT BERÜHREN!")
warning_text.setAlignment(Qt.AlignCenter)
warning_text.setFixedHeight(40) # Feste Höhe für das Label
warning_text.setStyleSheet("""
QLabel {
color: #FFFFFF;
font-size: 22px;
font-weight: 700;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
padding: 0px;
margin: 0px;
}
""")
warning_subtext = QLabel("Jede Interaktion kann den Prozess unterbrechen")
warning_subtext.setAlignment(Qt.AlignCenter)
warning_subtext.setFixedHeight(25) # Feste Höhe
warning_subtext.setStyleSheet("""
QLabel {
color: rgba(255, 255, 255, 0.9);
font-size: 13px;
font-weight: 400;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
padding: 0px;
margin: 0px;
}
""")
warning_layout.addWidget(warning_text)
warning_layout.addWidget(warning_subtext)
layout.addWidget(warning_banner)
# Content Container
content_container = QWidget()
content_layout = QVBoxLayout(content_container)
content_layout.setContentsMargins(30, 25, 30, 30)
content_layout.setSpacing(20)
# Titel mit Plattform-Name
platform_display = self.platform_name if self.platform_name else "Account"
if self.is_login:
title_label = QLabel(f"{platform_display}-Login läuft")
else:
title_label = QLabel(f"{platform_display}-Account wird erstellt")
title_label.setObjectName("titleLabel")
title_label.setAlignment(Qt.AlignCenter)
title_label.setStyleSheet("""
QLabel#titleLabel {
color: #1A365D;
font-size: 26px;
font-weight: 600;
font-family: 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
padding-bottom: 20px;
border: none;
}
""")
content_layout.addWidget(title_label)
# Verstecktes Status-Label für Kompatibilität
self.status_label = QLabel()
self.status_label.setVisible(False) # Nicht sichtbar
content_layout.addWidget(self.status_label)
# Log-Ausgabe (größer da mehr Platz vorhanden)
self.log_output = QTextEdit()
self.log_output.setReadOnly(True)
self.log_output.setMinimumHeight(200) # Mehr Platz für Logs
self.log_output.setStyleSheet("""
QTextEdit {
background-color: #F8FAFC;
color: #2D3748;
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', monospace;
font-size: 12px;
border: 1px solid #CBD5E0;
border-radius: 8px;
padding: 12px;
}
""")
content_layout.addWidget(self.log_output)
# Button-Container
button_layout = QHBoxLayout()
button_layout.addStretch()
# Abbrechen-Button (weniger prominent)
self.cancel_button = QPushButton("Abbrechen")
self.cancel_button.setStyleSheet("""
QPushButton {
background-color: #F0F4F8;
color: #4A5568;
font-size: 14px;
font-weight: 500;
padding: 8px 24px;
border-radius: 24px;
border: 1px solid #E2E8F0;
min-height: 40px;
}
QPushButton:hover {
background-color: #FEE2E2;
color: #DC2626;
border-color: #DC2626;
}
QPushButton:pressed {
background-color: #DC2626;
color: #FFFFFF;
}
""")
self.cancel_button.clicked.connect(self.cancel_clicked.emit)
button_layout.addWidget(self.cancel_button)
button_layout.addStretch()
content_layout.addLayout(button_layout)
layout.addWidget(content_container)
self.setLayout(layout)
# Volle Sichtbarkeit
self.setWindowOpacity(1.0)
def start_animation(self):
"""Zeigt den Dialog an"""
self.status_label.setText("Initialisiere...")
self.raise_timer.start() # Starte Timer für Always-on-Top
def stop_animation(self):
"""Stoppt die Animation und den Timer"""
self.raise_timer.stop()
def set_status(self, status: str):
"""Aktualisiert den Status-Text"""
self.status_label.setText(status)
def add_log(self, message: str):
"""Fügt eine Log-Nachricht hinzu"""
self.log_output.append(message)
# Auto-scroll zum Ende
scrollbar = self.log_output.verticalScrollBar()
scrollbar.setValue(scrollbar.maximum())
def clear_log(self):
"""Löscht alle Log-Nachrichten"""
self.log_output.clear()
def set_progress(self, value: int):
"""Setzt den Fortschritt (0-100) - wird ignoriert da wir Spinner nutzen"""
pass # Spinner braucht keinen Fortschritt
def closeEvent(self, event):
"""Wird aufgerufen wenn der Dialog geschlossen wird"""
self.stop_animation()
self.closed.emit()
event.accept()
def keyPressEvent(self, event):
"""Verhindert das Schließen mit ESC"""
if event.key() == Qt.Key_Escape:
event.ignore()
else:
super().keyPressEvent(event)
def _raise_to_front(self):
"""Holt den Dialog in den Vordergrund"""
self.raise_()
# Nicht activateWindow() aufrufen - das holt das Hauptfenster mit
def show(self):
"""Überschreibt show() um den Dialog richtig zu positionieren"""
super().show()
self._raise_to_front() # Initial in den Vordergrund holen
# Zusätzliche Widget-Klasse für den Progress Modal
class ForgeAnimationWidget(QLabel):
"""
Einfaches Animation Widget für den Progress Modal
Kann einen Spinner oder andere Animation anzeigen
"""
def __init__(self):
super().__init__()
self.setText("⚙️") # Placeholder Icon
self.setAlignment(Qt.AlignCenter)
self.setStyleSheet("""
QLabel {
font-size: 48px;
color: #3182CE;
}
""")
# Animation Timer
self.animation_timer = QTimer()
self.animation_timer.timeout.connect(self.rotate_icon)
self.rotation_state = 0
def start_animation(self):
"""Startet die Animation"""
self.animation_timer.start(100)
def stop_animation(self):
"""Stoppt die Animation"""
self.animation_timer.stop()
def rotate_icon(self):
"""Einfache Rotation Animation"""
icons = ["⚙️", "🔧", "🔨", "⚒️"]
self.setText(icons[self.rotation_state % len(icons)])
self.rotation_state += 1

Datei anzeigen

@ -0,0 +1,370 @@
"""
Forge Animation Widget V2 - Verbessertes Design mit prominenter Warnung
Angepasst an den bestehenden Code mit minimalen Änderungen
"""
from PyQt5.QtWidgets import QDialog, QWidget, QVBoxLayout, QLabel, QTextEdit, QPushButton, QHBoxLayout, QFrame
from PyQt5.QtCore import Qt, pyqtSignal, QTimer
from PyQt5.QtGui import QFont, QMovie, QPixmap
class ForgeAnimationDialog(QDialog):
"""Modal-Dialog für die Account-Erstellung mit verbessertem Design"""
# Signal wenn Abbrechen geklickt wird
cancel_clicked = pyqtSignal()
# Signal wenn Dialog geschlossen wird
closed = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
self.init_ui()
# Timer für das regelmäßige Nach-vorne-Holen
self.raise_timer = QTimer()
self.raise_timer.timeout.connect(self._raise_to_front)
self.raise_timer.setInterval(500) # Alle 500ms
def init_ui(self):
"""Initialisiert die UI mit verbessertem Design"""
# Nur Dialog im Vordergrund, nicht das ganze Hauptfenster
self.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
self.setModal(False) # Nicht modal - blockiert nicht das Hauptfenster
self.setFixedSize(650, 600) # Größer für bessere Sichtbarkeit
# Styling für Light Theme
self.setStyleSheet("""
ForgeAnimationDialog {
background-color: #FFFFFF;
border: 1px solid #E2E8F0;
border-radius: 8px;
}
""")
# Hauptlayout
layout = QVBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
# NEUE WARNUNG OBEN - Sehr auffällig!
warning_banner = QFrame()
warning_banner.setFixedHeight(80)
warning_banner.setStyleSheet("""
QFrame {
background-color: #DC2626;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
padding: 15px 30px;
}
""")
warning_layout = QVBoxLayout(warning_banner)
warning_layout.setContentsMargins(0, 10, 0, 10)
# Großer Warning Text
warning_text = QLabel("⚠️ BROWSER NICHT BERÜHREN!")
warning_text.setAlignment(Qt.AlignCenter)
warning_text.setStyleSheet("""
QLabel {
color: #FFFFFF;
font-size: 22px;
font-weight: 700;
font-family: 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
letter-spacing: 1px;
}
""")
warning_subtext = QLabel("Jede Interaktion kann den Prozess unterbrechen")
warning_subtext.setAlignment(Qt.AlignCenter)
warning_subtext.setStyleSheet("""
QLabel {
color: rgba(255, 255, 255, 0.9);
font-size: 13px;
font-weight: 400;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
""")
warning_layout.addWidget(warning_text)
warning_layout.addWidget(warning_subtext)
layout.addWidget(warning_banner)
# Content Container
content_container = QWidget()
content_layout = QVBoxLayout(content_container)
content_layout.setContentsMargins(30, 25, 30, 30)
content_layout.setSpacing(20)
# Titel (etwas größer)
title_label = QLabel("Account wird erstellt")
title_label.setObjectName("titleLabel")
title_label.setAlignment(Qt.AlignCenter)
title_label.setStyleSheet("""
QLabel#titleLabel {
color: #1A365D;
font-size: 26px;
font-weight: 600;
font-family: 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
padding-bottom: 10px;
border: none;
}
""")
content_layout.addWidget(title_label)
# Moderne Info-Karte (angepasst)
info_frame = QFrame()
info_frame.setStyleSheet("""
QFrame {
background: #DBEAFE;
border: 1px solid #2563EB;
border-radius: 12px;
min-height: 100px;
}
""")
info_layout = QHBoxLayout(info_frame)
info_layout.setContentsMargins(20, 15, 20, 15)
info_layout.setSpacing(15)
# Icon Container
icon_container = QFrame()
icon_container.setFixedSize(50, 50)
icon_container.setStyleSheet("""
QFrame {
background: #2563EB;
border-radius: 25px;
}
""")
icon_layout = QVBoxLayout(icon_container)
icon_layout.setContentsMargins(0, 0, 0, 0)
icon_label = QLabel("🤖")
icon_label.setAlignment(Qt.AlignCenter)
icon_label.setStyleSheet("""
QLabel {
font-size: 24px;
background: transparent;
border: none;
color: white;
}
""")
icon_layout.addWidget(icon_label)
# Text Container
text_container = QFrame()
text_layout = QVBoxLayout(text_container)
text_layout.setContentsMargins(0, 0, 0, 0)
text_layout.setSpacing(5)
# Titel
info_title = QLabel("Automatisierung läuft")
info_title.setObjectName("infoTitle")
info_title.setStyleSheet("""
QLabel#infoTitle {
color: #1E40AF;
font-size: 17px;
font-weight: 600;
font-family: 'Segoe UI', -apple-system, sans-serif;
background-color: transparent;
border: none;
}
""")
# Beschreibung (deutlicher)
info_desc = QLabel("Der Browser arbeitet automatisch. Bitte warten Sie, bis der Vorgang abgeschlossen ist.")
info_desc.setObjectName("infoDesc")
info_desc.setWordWrap(True)
info_desc.setStyleSheet("""
QLabel#infoDesc {
color: #1E40AF;
font-size: 14px;
font-weight: 500;
font-family: 'Segoe UI', -apple-system, sans-serif;
background-color: transparent;
border: none;
line-height: 20px;
}
""")
text_layout.addWidget(info_title)
text_layout.addWidget(info_desc)
text_layout.addStretch()
info_layout.addWidget(icon_container)
info_layout.addWidget(text_container, 1)
content_layout.addWidget(info_frame)
# Status Container
status_container = QFrame()
status_container.setStyleSheet("""
QFrame {
background-color: #1A365D;
border-radius: 8px;
padding: 15px;
}
""")
status_layout = QVBoxLayout(status_container)
# Status-Label (größer)
self.status_label = QLabel("Initialisiere...")
self.status_label.setObjectName("statusLabel")
self.status_label.setAlignment(Qt.AlignLeft)
self.status_label.setStyleSheet("""
QLabel#statusLabel {
color: #FFFFFF;
font-size: 16px;
padding: 5px;
font-weight: 500;
border: none;
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
}
""")
status_layout.addWidget(self.status_label)
content_layout.addWidget(status_container)
# Log-Ausgabe (größer)
self.log_output = QTextEdit()
self.log_output.setReadOnly(True)
self.log_output.setMaximumHeight(180) # Etwas größer
self.log_output.setStyleSheet("""
QTextEdit {
background-color: #F8FAFC;
color: #2D3748;
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', monospace;
font-size: 12px;
border: 1px solid #CBD5E0;
border-radius: 8px;
padding: 12px;
}
""")
content_layout.addWidget(self.log_output)
# Button-Container
button_layout = QHBoxLayout()
button_layout.addStretch()
# Abbrechen-Button (weniger prominent)
self.cancel_button = QPushButton("Abbrechen")
self.cancel_button.setStyleSheet("""
QPushButton {
background-color: #F0F4F8;
color: #4A5568;
font-size: 14px;
font-weight: 500;
padding: 8px 24px;
border-radius: 24px;
border: 1px solid #E2E8F0;
min-height: 40px;
}
QPushButton:hover {
background-color: #FEE2E2;
color: #DC2626;
border-color: #DC2626;
}
QPushButton:pressed {
background-color: #DC2626;
color: #FFFFFF;
}
""")
self.cancel_button.clicked.connect(self.cancel_clicked.emit)
button_layout.addWidget(self.cancel_button)
button_layout.addStretch()
content_layout.addLayout(button_layout)
layout.addWidget(content_container)
self.setLayout(layout)
# Volle Sichtbarkeit
self.setWindowOpacity(1.0)
def start_animation(self):
"""Zeigt den Dialog an"""
self.status_label.setText("Initialisiere...")
self.raise_timer.start() # Starte Timer für Always-on-Top
def stop_animation(self):
"""Stoppt die Animation und den Timer"""
self.raise_timer.stop()
def set_status(self, status: str):
"""Aktualisiert den Status-Text"""
self.status_label.setText(status)
def add_log(self, message: str):
"""Fügt eine Log-Nachricht hinzu"""
self.log_output.append(message)
# Auto-scroll zum Ende
scrollbar = self.log_output.verticalScrollBar()
scrollbar.setValue(scrollbar.maximum())
def clear_log(self):
"""Löscht alle Log-Nachrichten"""
self.log_output.clear()
def set_progress(self, value: int):
"""Setzt den Fortschritt (0-100) - wird ignoriert da wir Spinner nutzen"""
pass
def closeEvent(self, event):
"""Wird aufgerufen wenn der Dialog geschlossen wird"""
self.stop_animation()
self.closed.emit()
event.accept()
def keyPressEvent(self, event):
"""Verhindert das Schließen mit ESC"""
if event.key() == Qt.Key_Escape:
event.ignore()
else:
super().keyPressEvent(event)
def _raise_to_front(self):
"""Holt den Dialog in den Vordergrund"""
self.raise_()
def show(self):
"""Überschreibt show() um den Dialog richtig zu positionieren"""
super().show()
self._raise_to_front() # Initial in den Vordergrund holen
# Zusätzliche Widget-Klasse für den AccountForger
class ForgeAnimationWidget(QLabel):
"""
Einfaches Animation Widget für den Progress Modal
Kann einen Spinner oder andere Animation anzeigen
"""
def __init__(self):
super().__init__()
self.setText("⚙️") # Placeholder Icon
self.setAlignment(Qt.AlignCenter)
self.setStyleSheet("""
QLabel {
font-size: 48px;
color: #3182CE;
}
""")
# Animation Timer
self.animation_timer = QTimer()
self.animation_timer.timeout.connect(self.rotate_icon)
self.rotation_state = 0
def start_animation(self):
"""Startet die Animation"""
self.animation_timer.start(100)
def stop_animation(self):
"""Stoppt die Animation"""
self.animation_timer.stop()
def rotate_icon(self):
"""Einfache Rotation Animation"""
icons = ["⚙️", "🔧", "🔨", "⚒️"]
self.setText(icons[self.rotation_state % len(icons)])
self.rotation_state += 1

192
views/widgets/icon_factory.py Normale Datei
Datei anzeigen

@ -0,0 +1,192 @@
"""
Icon Factory - Zentrale Icon-Verwaltung nach Clean Architecture
"""
from PyQt5.QtWidgets import QLabel
from PyQt5.QtCore import Qt, QByteArray
from PyQt5.QtGui import QIcon, QPixmap, QPainter, QColor
from PyQt5.QtSvg import QSvgRenderer
import os
import logging
from config.paths import PathConfig
logger = logging.getLogger("icon_factory")
class IconFactory:
"""Factory für die Erstellung und Verwaltung von Icons"""
# Cache für geladene Icons
_icon_cache = {}
# Standard SVG Icons
ICONS = {
"mail": '''<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 8L8.44992 11.6333C9.73295 12.4886 10.3745 12.9163 11.0678 13.0825C11.6806 13.2293 12.3194 13.2293 12.9322 13.0825C13.6255 12.9163 14.2671 12.4886 15.5501 11.6333L21 8M6.2 19H17.8C18.9201 19 19.4802 19 19.908 18.782C20.2843 18.5903 20.5903 18.2843 20.782 17.908C21 17.4802 21 16.9201 21 15.8V8.2C21 7.0799 21 6.51984 20.782 6.09202C20.5903 5.71569 20.2843 5.40973 19.908 5.21799C19.4802 5 18.9201 5 17.8 5H6.2C5.0799 5 4.51984 5 4.09202 5.21799C3.71569 5.40973 3.40973 5.71569 3.21799 6.09202C3 6.51984 3 7.07989 3 8.2V15.8C3 16.9201 3 17.4802 3.21799 17.908C3.40973 18.2843 3.71569 18.5903 4.09202 18.782C4.51984 19 5.07989 19 6.2 19Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>''',
"key": '''<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.3212 10.6852L4 19L6 21M7 16L9 18M20 7.5C20 9.98528 17.9853 12 15.5 12C13.0147 12 11 9.98528 11 7.5C11 5.01472 13.0147 3 15.5 3C17.9853 3 20 5.01472 20 7.5Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>''',
"calendar": '''<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 10H21M7 3V5M17 3V5M6.2 21H17.8C18.9201 21 19.4802 21 19.908 20.782C20.2843 20.5903 20.5903 20.2843 20.782 19.908C21 19.4802 21 18.9201 21 17.8V8.2C21 7.07989 21 6.51984 20.782 6.09202C20.5903 5.71569 20.2843 5.40973 19.908 5.21799C19.4802 5 18.9201 5 17.8 5H6.2C5.0799 5 4.51984 5 4.09202 5.21799C3.71569 5.40973 3.40973 5.71569 3.21799 6.09202C3 6.51984 3 7.07989 3 8.2V17.8C3 18.9201 3 19.4802 3.21799 19.908C3.40973 20.2843 3.71569 20.5903 4.09202 20.782C4.51984 21 5.07989 21 6.2 21Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>''',
"copy": '''<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 8H7.2C6.0799 8 5.51984 8 5.09202 8.21799C4.71569 8.40973 4.40973 8.71569 4.21799 9.09202C4 9.51984 4 10.0799 4 11.2V16.8C4 17.9201 4 18.4802 4.21799 18.908C4.40973 19.2843 4.71569 19.5903 5.09202 19.782C5.51984 20 6.0799 20 7.2 20H12.8C13.9201 20 14.4802 20 14.908 19.782C15.2843 19.5903 15.5903 19.2843 15.782 18.908C16 18.4802 16 17.9201 16 16.8V16M11.2 16H16.8C17.9201 16 18.4802 16 18.908 15.782C19.2843 15.5903 19.5903 15.2843 19.782 14.908C20 14.4802 20 13.9201 20 12.8V7.2C20 6.0799 20 5.51984 19.782 5.09202C19.5903 4.71569 19.2843 4.40973 18.908 4.21799C18.4802 4 17.9201 4 16.8 4H11.2C10.0799 4 9.51984 4 9.09202 4.21799C8.71569 4.40973 8.40973 4.71569 8.21799 5.09202C8 5.51984 8 6.07989 8 7.2V12.8C8 13.9201 8 14.4802 8.21799 14.908C8.40973 15.2843 8.71569 15.5903 9.09202 15.782C9.51984 16 10.0799 16 11.2 16Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>''',
"eye": '''<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.0007 12C15.0007 13.6569 13.6576 15 12.0007 15C10.3439 15 9.00073 13.6569 9.00073 12C9.00073 10.3431 10.3439 9 12.0007 9C13.6576 9 15.0007 10.3431 15.0007 12Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.0012 5C7.52354 5 3.73326 7.94288 2.45898 12C3.73324 16.0571 7.52354 19 12.0012 19C16.4788 19 20.2691 16.0571 21.5434 12C20.2691 7.94291 16.4788 5 12.0012 5Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>''',
"eye-slash": '''<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.99902 3L20.999 21M9.8433 9.91364C9.32066 10.4536 8.99902 11.1892 8.99902 12C8.99902 13.6569 10.3422 15 11.999 15C12.8215 15 13.5667 14.669 14.1086 14.133M6.49902 6.64715C4.59972 7.90034 3.15305 9.78394 2.45703 12C3.73128 16.0571 7.52159 19 11.9992 19C13.9881 19 15.8414 18.4194 17.3988 17.4184M10.999 5.04939C11.328 5.01673 11.6617 5 11.9992 5C16.4769 5 20.2672 7.94291 21.5414 12C21.2607 12.894 20.8577 13.7338 20.3522 14.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>'''
}
@classmethod
def get_icon(cls, icon_name: str, size: int = 16, color: str = "#718096") -> QIcon:
"""
Erstellt ein QIcon aus SVG
Args:
icon_name: Name des Icons
size: Größe des Icons
color: Farbe des Icons (hex)
Returns:
QIcon Objekt
"""
cache_key = f"{icon_name}_{size}_{color}"
if cache_key in cls._icon_cache:
return cls._icon_cache[cache_key]
# Erstelle Pixmap
pixmap = cls.get_pixmap(icon_name, size, color)
icon = QIcon(pixmap)
# Cache das Icon
cls._icon_cache[cache_key] = icon
return icon
@classmethod
def get_pixmap(cls, icon_name: str, size: int = 16, color: str = "#718096") -> QPixmap:
"""
Erstellt ein QPixmap aus SVG
Args:
icon_name: Name des Icons
size: Größe des Icons
color: Farbe des Icons (hex)
Returns:
QPixmap Objekt
"""
# Versuche zuerst aus Datei zu laden
icon_path = PathConfig.get_icon_path(icon_name)
if PathConfig.file_exists(icon_path):
pixmap = QPixmap(icon_path)
if not pixmap.isNull():
return pixmap.scaled(size, size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
# Fallback auf eingebettete SVGs
svg_content = cls.ICONS.get(icon_name)
if svg_content:
# Ersetze currentColor durch die angegebene Farbe
svg_content = svg_content.replace('currentColor', color)
# Erstelle SVG Renderer
renderer = QSvgRenderer(QByteArray(svg_content.encode()))
# Erstelle Pixmap
pixmap = QPixmap(size, size)
pixmap.fill(Qt.transparent)
# Rendere SVG
painter = QPainter(pixmap)
painter.setRenderHint(QPainter.Antialiasing)
renderer.render(painter)
painter.end()
return pixmap
# Fallback: Erstelle einen farbigen Kreis
logger.warning(f"Icon '{icon_name}' nicht gefunden, verwende Platzhalter")
pixmap = QPixmap(size, size)
pixmap.fill(Qt.transparent)
painter = QPainter(pixmap)
painter.setRenderHint(QPainter.Antialiasing)
painter.setBrush(QColor(color))
painter.setPen(Qt.NoPen)
painter.drawEllipse(0, 0, size, size)
painter.end()
return pixmap
@classmethod
def create_icon_label(cls, icon_name: str, size: int = 16, color: str = "#718096") -> QLabel:
"""
Erstellt ein QLabel mit Icon
Args:
icon_name: Name des Icons
size: Größe des Icons
color: Farbe des Icons (hex)
Returns:
QLabel mit Icon
"""
label = QLabel()
label.setFixedSize(size, size)
# Prüfe ob es eine Plattform ist
platform_names = ["instagram", "facebook", "twitter", "x", "tiktok", "vk", "ok", "gmail"]
if icon_name.lower() in platform_names:
# Verwende get_platform_icon für Plattformen
icon = cls.get_platform_icon(icon_name, size)
pixmap = icon.pixmap(size, size)
else:
# Normale Icons
pixmap = cls.get_pixmap(icon_name, size, color)
label.setPixmap(pixmap)
label.setScaledContents(True)
return label
@classmethod
def get_platform_icon(cls, platform: str, size: int = 18) -> QIcon:
"""
Gibt ein Platform-spezifisches Icon zurück
Args:
platform: Name der Plattform
size: Größe des Icons
Returns:
QIcon für die Plattform
"""
# Spezialbehandlung für Twitter/X
if platform.lower() == "twitter" or platform.lower() == "x":
icon_name = "twitter"
else:
icon_name = platform.lower()
# Platform Icons haben eigene Farben
platform_colors = {
"instagram": "#E4405F",
"facebook": "#1877F2",
"twitter": "#1DA1F2",
"x": "#000000",
"tiktok": "#000000",
"vk": "#0077FF"
}
color = platform_colors.get(icon_name, "#718096")
return cls.get_icon(icon_name, size, color)

Datei anzeigen

@ -0,0 +1,203 @@
# Path: views/widgets/language_dropdown.py
"""
Benutzerdefiniertes Dropdown-Widget für die Sprachauswahl mit Flaggen-Icons.
"""
import os
from PyQt5.QtWidgets import (QWidget, QComboBox, QLabel, QHBoxLayout,
QVBoxLayout, QFrame, QListWidget, QListWidgetItem,
QAbstractItemView, QApplication)
from PyQt5.QtCore import Qt, QSize, QEvent, pyqtSignal
from PyQt5.QtGui import QIcon, QPainter, QPen, QColor, QCursor
class LanguageDropdown(QWidget):
"""Benutzerdefiniertes Dropdown für die Sprachauswahl mit Flaggen."""
def __init__(self, language_manager):
super().__init__()
self.language_manager = language_manager
self.is_open = False
self.languages = {}
self.current_language = self.language_manager.get_current_language()
# QApplication-Instanz merken, um einen Event-Filter installieren zu können
self.app = QApplication.instance()
# Verfügbare Sprachen aus dem Manager holen
self.available_languages = self.language_manager.get_available_languages()
self.init_ui()
# Verbinde Signal des Language Managers
self.language_manager.language_changed.connect(self.on_language_changed)
def init_ui(self):
"""Initialisiert die Benutzeroberfläche."""
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
# Container für die aktuelle Sprachauswahl
self.current_language_container = QFrame()
self.current_language_container.setObjectName("languageSelector")
self.current_language_container.setCursor(Qt.PointingHandCursor)
self.current_language_container.setStyleSheet("""
QFrame#languageSelector {
background-color: transparent;
border-radius: 4px;
}
QFrame#languageSelector:hover {
background-color: rgba(200, 200, 200, 30);
}
""")
current_layout = QHBoxLayout(self.current_language_container)
current_layout.setContentsMargins(5, 5, 5, 5)
# Icon der aktuellen Sprache
self.current_flag = QLabel()
self.current_flag.setFixedSize(24, 16) # Correct flag aspect ratio
# Pfad zum Icon
icon_path = self.get_language_icon_path(self.current_language)
if icon_path:
self.current_flag.setPixmap(QIcon(icon_path).pixmap(QSize(24, 16)))
current_layout.addWidget(self.current_flag)
# Kleiner Pfeil nach unten
arrow_label = QLabel("")
arrow_label.setStyleSheet("font-size: 8px; color: #888888;")
current_layout.addWidget(arrow_label)
layout.addWidget(self.current_language_container)
# Dropdown-Liste (anfangs versteckt)
self.dropdown_list = QListWidget()
self.dropdown_list.setWindowFlags(Qt.Popup | Qt.FramelessWindowHint)
self.dropdown_list.setFocusPolicy(Qt.NoFocus)
self.dropdown_list.setMouseTracking(True)
self.dropdown_list.setFrameShape(QFrame.NoFrame)
self.dropdown_list.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.dropdown_list.setEditTriggers(QAbstractItemView.NoEditTriggers)
self.dropdown_list.setSelectionMode(QAbstractItemView.NoSelection)
self.dropdown_list.setStyleSheet("""
QListWidget {
background-color: white;
border: 1px solid #CCCCCC;
border-radius: 5px;
padding: 5px;
}
QListWidget::item {
padding: 4px;
border-radius: 3px;
}
QListWidget::item:hover {
background-color: #F0F0F0;
}
""")
# Sprachen zum Dropdown hinzufügen
self.populate_dropdown()
# Event-Verbindungen
self.current_language_container.mousePressEvent = self.toggle_dropdown
self.dropdown_list.itemClicked.connect(self.on_language_selected)
# Zugänglichkeit mit Tastaturfokus
self.setFocusPolicy(Qt.StrongFocus)
self.current_language_container.setFocusPolicy(Qt.StrongFocus)
def get_language_icon_path(self, language_code):
"""Gibt den Pfad zum Icon für den angegebenen Sprachcode zurück."""
# Projektbasis-Verzeichnis ermitteln
base_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
icon_path = os.path.join(base_dir, "resources", "icons", f"{language_code}.svg")
if os.path.exists(icon_path):
return icon_path
return None
def populate_dropdown(self):
"""Füllt das Dropdown mit den verfügbaren Sprachen."""
self.dropdown_list.clear()
self.languages = {}
for code, name in self.available_languages.items():
item = QListWidgetItem(name)
# Icon erstellen
icon_path = self.get_language_icon_path(code)
if icon_path:
item.setIcon(QIcon(icon_path))
# Sprach-Code speichern
item.setData(Qt.UserRole, code)
# Zum Dropdown hinzufügen
self.dropdown_list.addItem(item)
self.languages[code] = item
def toggle_dropdown(self, event):
"""Öffnet oder schließt das Dropdown-Menü."""
if not self.is_open:
# Position des Dropdowns unter dem Button berechnen
pos = self.current_language_container.mapToGlobal(self.current_language_container.rect().bottomLeft())
self.dropdown_list.setGeometry(pos.x(), pos.y(), 120, 120) # Größe anpassen
self.dropdown_list.show()
self.is_open = True
if self.app:
self.app.installEventFilter(self)
else:
self.dropdown_list.hide()
self.is_open = False
if self.app:
self.app.removeEventFilter(self)
def on_language_selected(self, item):
"""Wird aufgerufen, wenn eine Sprache im Dropdown ausgewählt wird."""
language_code = item.data(Qt.UserRole)
# Sprache wechseln
if language_code != self.current_language:
self.language_manager.change_language(language_code)
# Dropdown schließen
self.dropdown_list.hide()
self.is_open = False
if self.app:
self.app.removeEventFilter(self)
def on_language_changed(self, language_code):
"""Wird aufgerufen, wenn sich die Sprache im LanguageManager ändert."""
self.current_language = language_code
# Icon aktualisieren
icon_path = self.get_language_icon_path(language_code)
if icon_path:
self.current_flag.setPixmap(QIcon(icon_path).pixmap(QSize(24, 16)))
# Texte aktualisieren (falls vorhanden)
if hasattr(self.parent(), "update_texts"):
self.parent().update_texts()
def keyPressEvent(self, event):
"""Behandelt Tastatureingaben für verbesserte Zugänglichkeit."""
if event.key() == Qt.Key_Space or event.key() == Qt.Key_Return:
self.toggle_dropdown(None)
else:
super().keyPressEvent(event)
def eventFilter(self, obj, event):
"""Schließt das Dropdown, wenn außerhalb geklickt wird."""
if self.is_open and event.type() == QEvent.MouseButtonPress:
# Klickposition relativ zum Dropdown ermitteln
if not self.dropdown_list.geometry().contains(event.globalPos()) and \
not self.current_language_container.geometry().contains(
self.mapFromGlobal(event.globalPos())):
self.dropdown_list.hide()
self.is_open = False
if self.app:
self.app.removeEventFilter(self)
return super().eventFilter(obj, event)

Datei anzeigen

@ -0,0 +1,295 @@
"""
Login Process Modal - Spezialisiertes Modal für Login-Prozesse
"""
import logging
from typing import Optional, Dict, Any
from PyQt5.QtCore import QTimer, pyqtSignal
from PyQt5.QtWidgets import QHBoxLayout, QLabel
from PyQt5.QtGui import QFont, QPixmap
from PyQt5.QtCore import Qt
from views.widgets.progress_modal import ProgressModal
logger = logging.getLogger("login_process_modal")
class LoginProcessModal(ProgressModal):
"""
Spezialisiertes Modal für Login-Prozesse mit Session-Wiederherstellung.
"""
# Signale
login_completed = pyqtSignal(bool, str) # success, message
session_restored = pyqtSignal(str) # platform
def __init__(self, parent=None, platform: str = "Social Media", language_manager=None):
self.platform = platform
self.login_method = "session" # "session" oder "credentials"
self.account_username = ""
super().__init__(parent, "login_process", language_manager)
# Erweitere UI für Login-spezifische Anzeige
self.setup_login_ui()
def setup_login_ui(self):
"""Erweitert die UI um Login-spezifische Elemente"""
container_layout = self.modal_container.layout()
# Platform/Account Info (zwischen Titel und Animation)
self.account_info_widget = QLabel()
self.account_info_widget.setVisible(False)
self.account_info_widget.setAlignment(Qt.AlignCenter)
info_font = QFont("Inter", 13)
info_font.setBold(True)
self.account_info_widget.setFont(info_font)
self.account_info_widget.setStyleSheet("""
QLabel {
color: #2D3748;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: rgba(66, 153, 225, 0.1);
border-radius: 8px;
padding: 8px 16px;
margin: 4px 0px;
}
""")
# Füge nach Titel ein (Index 1)
container_layout.insertWidget(1, self.account_info_widget)
# Login-Methode Indicator (nach Animation Widget)
self.method_label = QLabel()
self.method_label.setVisible(False)
self.method_label.setAlignment(Qt.AlignCenter)
method_font = QFont("Inter", 11)
self.method_label.setFont(method_font)
self.method_label.setStyleSheet("""
QLabel {
color: #4A5568;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-style: italic;
margin: 4px 0px;
}
""")
# Füge nach Animation Widget ein (Index 3, wegen account_info_widget)
container_layout.insertWidget(3, self.method_label)
def show_session_login(self, account_username: str, platform: str = None):
"""
Zeigt Session-basiertes Login an.
Args:
account_username: Benutzername des Accounts
platform: Optional - Platform überschreibung
"""
if platform:
self.platform = platform
self.account_username = account_username
self.login_method = "session"
# UI aktualisieren
self._update_login_display()
# Modal anzeigen
self.show_process("login_process")
logger.info(f"Session Login Modal angezeigt für: {account_username} @ {self.platform}")
def show_credentials_login(self, account_username: str, platform: str = None):
"""
Zeigt Credential-basiertes Login an.
Args:
account_username: Benutzername des Accounts
platform: Optional - Platform überschreibung
"""
if platform:
self.platform = platform
self.account_username = account_username
self.login_method = "credentials"
# UI aktualisieren
self._update_login_display()
# Modal anzeigen
self.show_process("login_process")
logger.info(f"Credentials Login Modal angezeigt für: {account_username} @ {self.platform}")
def _update_login_display(self):
"""Aktualisiert die Anzeige basierend auf Login-Methode"""
# Titel anpassen
title = f"🔐 {self.platform} Login"
self.title_label.setText(title)
# Account Info anzeigen
if self.account_username:
account_info = f"👤 {self.account_username}"
self.account_info_widget.setText(account_info)
self.account_info_widget.setVisible(True)
# Methode anzeigen
if self.login_method == "session":
method_text = "🔄 Session wird wiederhergestellt"
status_text = "Gespeicherte Anmeldedaten werden geladen..."
else:
method_text = "🔑 Anmeldedaten werden eingegeben"
status_text = "Benutzername und Passwort werden verwendet..."
self.method_label.setText(method_text)
self.method_label.setVisible(True)
# Status setzen
self.update_status(status_text)
def update_login_progress(self, step: str, detail: str = None):
"""
Aktualisiert den Login-Fortschritt.
Args:
step: Aktueller Schritt
detail: Optional - Detail-Information
"""
step_emojis = {
'browser_init': '🌐',
'page_load': '📄',
'session_restore': '🔄',
'credential_input': '✍️',
'verification': '🔐',
'success_check': '',
'finalizing': '🏁'
}
emoji = step_emojis.get(step, '')
status_text = f"{emoji} {step.replace('_', ' ').title()}"
self.update_status(status_text, detail)
logger.debug(f"Login Progress: {step} - {detail or 'No detail'}")
def show_session_restored(self, platform_data: Dict[str, Any] = None):
"""
Zeigt erfolgreiche Session-Wiederherstellung an.
Args:
platform_data: Optional - Platform-spezifische Daten
"""
self.update_status("✅ Session erfolgreich wiederhergestellt", "Anmeldung abgeschlossen")
# Session restored Signal
self.session_restored.emit(self.platform)
# Auto-Close nach 2 Sekunden
QTimer.singleShot(2000, lambda: self._complete_login(True, "Session wiederhergestellt"))
logger.info(f"Session erfolgreich wiederhergestellt für: {self.account_username} @ {self.platform}")
def show_credentials_success(self):
"""Zeigt erfolgreiche Credential-Anmeldung an"""
self.update_status("✅ Anmeldung erfolgreich", "Account ist bereit")
# Auto-Close nach 2 Sekunden
QTimer.singleShot(2000, lambda: self._complete_login(True, "Anmeldung erfolgreich"))
logger.info(f"Credential-Login erfolgreich für: {self.account_username} @ {self.platform}")
def show_login_failed(self, reason: str, retry_available: bool = False):
"""
Zeigt fehlgeschlagene Anmeldung an.
Args:
reason: Grund für Fehlschlag
retry_available: Ob Retry möglich ist
"""
# Animation stoppen
self.animation_widget.stop_animation()
error_title = "❌ Anmeldung fehlgeschlagen"
if self.login_method == "session":
error_detail = "Session ist abgelaufen oder ungültig"
else:
error_detail = "Benutzername oder Passwort falsch"
self.title_label.setText(error_title)
self.update_status(reason, error_detail)
# Auto-Close nach 4 Sekunden
auto_close_time = 4000
QTimer.singleShot(auto_close_time, lambda: self._complete_login(False, reason))
logger.error(f"Login fehlgeschlagen für: {self.account_username} @ {self.platform} - {reason}")
def show_session_expired(self):
"""Zeigt abgelaufene Session an"""
self.show_login_failed("Session ist abgelaufen", retry_available=True)
# Spezielle Behandlung für Session-Ablauf
self.method_label.setText("⚠️ Manuelle Anmeldung erforderlich")
def show_captcha_required(self):
"""Zeigt an, dass Captcha erforderlich ist"""
self.update_status("🤖 Captcha-Verifizierung erforderlich", "Bitte manuell lösen...")
# Längere Auto-Close Zeit für Captcha
QTimer.singleShot(10000, lambda: self._complete_login(False, "Captcha erforderlich"))
def show_two_factor_required(self):
"""Zeigt an, dass Zwei-Faktor-Authentifizierung erforderlich ist"""
self.update_status("📱 Zwei-Faktor-Authentifizierung", "Code wird benötigt...")
# Längere Auto-Close Zeit für 2FA
QTimer.singleShot(8000, lambda: self._complete_login(False, "2FA erforderlich"))
def _complete_login(self, success: bool, message: str):
"""
Schließt den Login-Prozess ab.
Args:
success: Ob Login erfolgreich war
message: Abschließende Nachricht
"""
# Signal senden
self.login_completed.emit(success, message)
# Modal verstecken
self.hide_process()
logger.info(f"Login-Prozess abgeschlossen: {success} - {message}")
def get_platform_icon_path(self) -> Optional[str]:
"""
Gibt den Pfad zum Platform-Icon zurück.
Returns:
Optional[str]: Icon-Pfad oder None
"""
try:
import os
current_dir = os.path.dirname(os.path.abspath(__file__))
parent_dir = os.path.dirname(os.path.dirname(os.path.dirname(current_dir)))
icons_dir = os.path.join(parent_dir, "resources", "icons")
platform_name = self.platform.lower().replace('.', '').replace(' ', '')
if platform_name == "x":
platform_name = "twitter"
elif platform_name == "okru":
platform_name = "ok"
icon_path = os.path.join(icons_dir, f"{platform_name}.svg")
if os.path.exists(icon_path):
return icon_path
except Exception as e:
logger.warning(f"Konnte Platform-Icon nicht laden: {e}")
return None

Datei anzeigen

@ -0,0 +1,316 @@
"""
Moderne, schöne MessageBox als Alternative zu den hässlichen Qt Standard-Dialogen
"""
from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QFrame, QGraphicsDropShadowEffect)
from PyQt5.QtCore import Qt, QTimer, pyqtSignal
from PyQt5.QtGui import QFont, QPixmap, QPainter, QColor, QIcon
class ModernMessageBox(QDialog):
"""Moderne, schöne MessageBox mit glasmorphism Design"""
clicked = pyqtSignal(str) # "ok", "cancel", "yes", "no"
def __init__(self, parent=None, title="", message="", msg_type="info", buttons=None):
super().__init__(parent)
self.msg_type = msg_type.lower()
self.result_value = None
if buttons is None:
buttons = ["OK"]
self.init_ui(title, message, buttons)
self.setup_animations()
def init_ui(self, title, message, buttons):
"""Initialisiert die moderne UI"""
# Dialog-Eigenschaften
self.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint)
self.setAttribute(Qt.WA_TranslucentBackground, True)
self.setModal(True)
self.setFixedSize(400, 250)
# Hauptlayout
main_layout = QVBoxLayout(self)
main_layout.setContentsMargins(20, 20, 20, 20)
# Container mit Glasmorphism-Effekt
self.container = QFrame()
self.container.setObjectName("messageContainer")
# 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)
# Header mit Icon und Titel
header_layout = QHBoxLayout()
# Icon basierend auf Nachrichtentyp
icon_label = QLabel()
icon_label.setFixedSize(32, 32)
icon_label.setAlignment(Qt.AlignCenter)
icon_text = self.get_icon_for_type(self.msg_type)
icon_label.setText(icon_text)
icon_label.setObjectName(f"icon{self.msg_type.title()}")
# Titel
title_label = QLabel(title)
title_label.setObjectName("messageTitle")
title_label.setWordWrap(True)
header_layout.addWidget(icon_label)
header_layout.addWidget(title_label, 1)
header_layout.setSpacing(15)
# Nachricht
message_label = QLabel(message)
message_label.setObjectName("messageText")
message_label.setWordWrap(True)
message_label.setAlignment(Qt.AlignLeft | Qt.AlignTop)
# Buttons
button_layout = QHBoxLayout()
button_layout.addStretch()
for i, button_text in enumerate(buttons):
btn = QPushButton(button_text)
btn.setObjectName("messageButton" if i == 0 else "messageButtonSecondary")
btn.setMinimumHeight(35)
btn.setMinimumWidth(80)
btn.clicked.connect(lambda checked, text=button_text.lower(): self.button_clicked(text))
button_layout.addWidget(btn)
if i < len(buttons) - 1:
button_layout.addSpacing(10)
# Layout zusammenfügen
container_layout.addLayout(header_layout)
container_layout.addWidget(message_label, 1)
container_layout.addLayout(button_layout)
main_layout.addWidget(self.container)
# Stil anwenden
self.apply_styles()
def get_icon_for_type(self, msg_type):
"""Gibt das passende Icon für den Nachrichtentyp zurück"""
icons = {
"info": "",
"success": "",
"warning": "⚠️",
"error": "",
"critical": "🚨",
"question": ""
}
return icons.get(msg_type, "")
def apply_styles(self):
"""Wendet die modernen Styles an"""
# Farben basierend auf Typ
colors = {
"info": {
"bg": "rgba(59, 130, 246, 0.1)",
"border": "rgba(59, 130, 246, 0.3)",
"accent": "#3B82F6"
},
"success": {
"bg": "rgba(34, 197, 94, 0.1)",
"border": "rgba(34, 197, 94, 0.3)",
"accent": "#22C55E"
},
"warning": {
"bg": "rgba(245, 158, 11, 0.1)",
"border": "rgba(245, 158, 11, 0.3)",
"accent": "#F59E0B"
},
"error": {
"bg": "rgba(239, 68, 68, 0.1)",
"border": "rgba(239, 68, 68, 0.3)",
"accent": "#EF4444"
},
"critical": {
"bg": "rgba(220, 38, 38, 0.1)",
"border": "rgba(220, 38, 38, 0.3)",
"accent": "#DC2626"
},
"question": {
"bg": "rgba(168, 85, 247, 0.1)",
"border": "rgba(168, 85, 247, 0.3)",
"accent": "#A855F7"
}
}
color_scheme = colors.get(self.msg_type, colors["info"])
style = f"""
QDialog {{
background: transparent;
}}
#messageContainer {{
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
stop:0 rgba(255, 255, 255, 0.95),
stop:1 rgba(248, 250, 252, 0.95));
border: 1px solid {color_scheme['border']};
border-radius: 16px;
backdrop-filter: blur(10px);
}}
#messageTitle {{
font-family: 'Segoe UI', Arial, sans-serif;
font-size: 16px;
font-weight: 600;
color: #1e293b;
margin: 0;
}}
#messageText {{
font-family: 'Segoe UI', Arial, sans-serif;
font-size: 14px;
color: #475569;
line-height: 1.5;
}}
#icon{self.msg_type.title()} {{
font-size: 24px;
background: {color_scheme['bg']};
border: 1px solid {color_scheme['border']};
border-radius: 16px;
padding: 4px;
}}
#messageButton {{
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
stop:0 {color_scheme['accent']},
stop:1 {color_scheme['accent']});
color: white;
border: none;
border-radius: 8px;
font-family: 'Segoe UI', Arial, sans-serif;
font-size: 13px;
font-weight: 500;
padding: 8px 16px;
}}
#messageButton:hover {{
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
stop:0 {color_scheme['accent']},
stop:1 {color_scheme['accent']});
transform: translateY(-1px);
}}
#messageButton:pressed {{
transform: translateY(0px);
}}
#messageButtonSecondary {{
background: rgba(148, 163, 184, 0.1);
color: #64748b;
border: 1px solid rgba(148, 163, 184, 0.3);
border-radius: 8px;
font-family: 'Segoe UI', Arial, sans-serif;
font-size: 13px;
font-weight: 500;
padding: 8px 16px;
}}
#messageButtonSecondary:hover {{
background: rgba(148, 163, 184, 0.2);
border-color: rgba(148, 163, 184, 0.5);
}}
"""
self.setStyleSheet(style)
def setup_animations(self):
"""Setzt Animationen ein"""
# Sanftes Einblenden
self.setWindowOpacity(0)
self.fade_timer = QTimer()
self.fade_timer.timeout.connect(self.fade_in)
self.opacity = 0
self.fade_timer.start(16) # ~60 FPS
def fade_in(self):
"""Fade-in Animation"""
self.opacity += 0.1
if self.opacity >= 1:
self.opacity = 1
self.fade_timer.stop()
self.setWindowOpacity(self.opacity)
def button_clicked(self, button_text):
"""Behandelt Button-Clicks"""
self.result_value = button_text
self.clicked.emit(button_text)
self.accept()
def mousePressEvent(self, event):
"""Ermöglicht das Verschieben des Dialogs"""
if event.button() == Qt.LeftButton:
self.drag_position = event.globalPos() - self.frameGeometry().topLeft()
event.accept()
def mouseMoveEvent(self, event):
"""Verschiebt den Dialog"""
if event.buttons() == Qt.LeftButton and hasattr(self, 'drag_position'):
self.move(event.globalPos() - self.drag_position)
event.accept()
# Convenience-Funktionen für einfache Nutzung
def show_info(parent=None, title="Information", message="", buttons=None):
"""Zeigt eine moderne Info-MessageBox"""
if buttons is None:
buttons = ["OK"]
dialog = ModernMessageBox(parent, title, message, "info", buttons)
return dialog.exec_()
def show_success(parent=None, title="Erfolg", message="", buttons=None):
"""Zeigt eine moderne Erfolg-MessageBox"""
if buttons is None:
buttons = ["OK"]
dialog = ModernMessageBox(parent, title, message, "success", buttons)
return dialog.exec_()
def show_warning(parent=None, title="Warnung", message="", buttons=None):
"""Zeigt eine moderne Warnung-MessageBox"""
if buttons is None:
buttons = ["OK"]
dialog = ModernMessageBox(parent, title, message, "warning", buttons)
return dialog.exec_()
def show_error(parent=None, title="Fehler", message="", buttons=None):
"""Zeigt eine moderne Fehler-MessageBox"""
if buttons is None:
buttons = ["OK"]
dialog = ModernMessageBox(parent, title, message, "error", buttons)
return dialog.exec_()
def show_critical(parent=None, title="Kritischer Fehler", message="", buttons=None):
"""Zeigt eine moderne kritische Fehler-MessageBox"""
if buttons is None:
buttons = ["OK"]
dialog = ModernMessageBox(parent, title, message, "critical", buttons)
return dialog.exec_()
def show_question(parent=None, title="Frage", message="", buttons=None):
"""Zeigt eine moderne Frage-MessageBox"""
if buttons is None:
buttons = ["Ja", "Nein"]
dialog = ModernMessageBox(parent, title, message, "question", buttons)
return dialog.exec_()

Datei anzeigen

@ -0,0 +1,89 @@
# Path: views/widgets/platform_button.py
"""
Benutzerdefinierter Button für die Plattformauswahl.
"""
import os
from PyQt5.QtWidgets import QPushButton, QVBoxLayout, QLabel, QWidget
from PyQt5.QtCore import QSize, Qt, pyqtSignal
from PyQt5.QtGui import QIcon, QFont
class PlatformButton(QWidget):
"""Angepasster Button-Widget für Plattformauswahl mit Icon."""
# Signal wenn geklickt
clicked = pyqtSignal()
def __init__(self, platform_name, icon_path=None, enabled=True):
super().__init__()
self.platform = platform_name.lower()
self.setMinimumSize(200, 200)
self.setEnabled(enabled)
# Layout für den Container
layout = QVBoxLayout(self)
layout.setAlignment(Qt.AlignCenter)
layout.setContentsMargins(10, 10, 10, 10)
# Icon-Button
self.icon_button = QPushButton()
self.icon_button.setFlat(True)
self.icon_button.setCursor(Qt.PointingHandCursor)
# Icon setzen, falls vorhanden
if icon_path and os.path.exists(icon_path):
self.icon_button.setIcon(QIcon(icon_path))
self.icon_button.setIconSize(QSize(120, 120)) # Größeres Icon
self.icon_button.setMinimumSize(150, 150)
# Platform button styling based on Styleguide
self.icon_button.setStyleSheet("""
QPushButton {
background-color: #F5F7FF;
border: 1px solid transparent;
border-radius: 16px;
padding: 32px;
}
QPushButton:hover {
background-color: #E8EBFF;
border: 1px solid #0099CC;
}
QPushButton:pressed {
background-color: #DCE2FF;
padding: 23px;
}
QPushButton:disabled {
background-color: #F0F0F0;
opacity: 0.5;
}
""")
# Button-Signal verbinden
self.icon_button.clicked.connect(self.clicked)
# Name-Label
self.name_label = QLabel(platform_name)
self.name_label.setAlignment(Qt.AlignCenter)
name_font = QFont()
name_font.setPointSize(12)
name_font.setBold(True)
self.name_label.setFont(name_font)
# Name label styling based on Styleguide
self.name_label.setStyleSheet("""
QLabel {
color: #232D53;
font-family: 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-weight: 600;
letter-spacing: 0.5px;
}
""")
# Widgets zum Layout hinzufügen
layout.addWidget(self.icon_button, 0, Qt.AlignCenter)
layout.addWidget(self.name_label, 0, Qt.AlignCenter)
# Styling für den deaktivierten Zustand
if not enabled:
self.setStyleSheet("opacity: 0.5;")

Datei anzeigen

@ -0,0 +1,312 @@
"""
Progress Modal - Basis-Klasse für Prozess-Modals
"""
import logging
from typing import Optional, Dict, Any
from PyQt5.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QFrame, QGraphicsBlurEffect
)
from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QPropertyAnimation, QEasingCurve
from PyQt5.QtGui import QFont, QMovie, QPainter, QBrush, QColor
from views.widgets.forge_animation_widget import ForgeAnimationWidget
from styles.modal_styles import ModalStyles
logger = logging.getLogger("progress_modal")
class ProgressModal(QDialog):
"""
Basis-Klasse für Progress-Modals während Automatisierungsprozessen.
Zeigt den Benutzer eine nicht-unterbrechbare Fortschrittsanzeige.
"""
# Signale
force_closed = pyqtSignal() # Wird ausgelöst wenn Modal zwangsweise geschlossen wird
def __init__(self, parent=None, modal_type: str = "generic", language_manager=None, style_manager=None):
super().__init__(parent)
self.modal_type = modal_type
self.language_manager = language_manager
self.style_manager = style_manager or ModalStyles()
self.is_process_running = False
self.auto_close_timer = None
self.fade_animation = None
# Modal-Texte laden
self.modal_texts = self.style_manager.get_modal_texts()
self.init_ui()
self.setup_animations()
if self.language_manager:
self.language_manager.language_changed.connect(self.update_texts)
self.update_texts()
def init_ui(self):
"""Initialisiert die UI nach AccountForger Styleguide"""
# Modal-Eigenschaften
self.setModal(True)
self.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint)
# Transparenz entfernt - solider Hintergrund
# self.setAttribute(Qt.WA_TranslucentBackground)
self.setFixedSize(
self.style_manager.SIZES['modal_width'],
self.style_manager.SIZES['modal_height']
)
# Zentriere auf Parent oder Bildschirm
if self.parent():
parent_rect = self.parent().geometry()
x = parent_rect.x() + (parent_rect.width() - self.width()) // 2
y = parent_rect.y() + (parent_rect.height() - self.height()) // 2
self.move(x, y)
# Hauptlayout
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
# Modal-Container mit solidem Hintergrund
self.modal_container = QFrame()
self.modal_container.setObjectName("modal_container")
self.modal_container.setStyleSheet(self.style_manager.get_modal_container_style())
# Container-Layout
container_layout = QVBoxLayout(self.modal_container)
padding = self.style_manager.SIZES['padding_large']
container_layout.setContentsMargins(padding, padding, padding, padding)
container_layout.setSpacing(self.style_manager.SIZES['spacing_default'])
# Titel
self.title_label = QLabel()
self.title_label.setAlignment(Qt.AlignCenter)
self.title_label.setObjectName("modal_title")
self.title_label.setFont(self.style_manager.create_font('title'))
self.title_label.setStyleSheet(self.style_manager.get_title_label_style())
container_layout.addWidget(self.title_label)
# Animation Widget (Spinner/Forge Animation)
self.animation_widget = ForgeAnimationWidget()
animation_size = self.style_manager.SIZES['animation_size']
self.animation_widget.setFixedSize(animation_size, animation_size)
self.animation_widget.setAlignment(Qt.AlignCenter)
animation_layout = QHBoxLayout()
animation_layout.addStretch()
animation_layout.addWidget(self.animation_widget)
animation_layout.addStretch()
container_layout.addLayout(animation_layout)
# Subtitle/Status
self.status_label = QLabel()
self.status_label.setAlignment(Qt.AlignCenter)
self.status_label.setObjectName("modal_status")
self.status_label.setFont(self.style_manager.create_font('status'))
self.status_label.setStyleSheet(self.style_manager.get_status_label_style())
container_layout.addWidget(self.status_label)
# Detail-Status (optional)
self.detail_label = QLabel()
self.detail_label.setAlignment(Qt.AlignCenter)
self.detail_label.setObjectName("modal_detail")
self.detail_label.setVisible(False)
self.detail_label.setFont(self.style_manager.create_font('detail'))
self.detail_label.setStyleSheet(self.style_manager.get_detail_label_style())
container_layout.addWidget(self.detail_label)
layout.addWidget(self.modal_container)
# Failsafe Timer
self.failsafe_timer = QTimer()
self.failsafe_timer.setSingleShot(True)
self.failsafe_timer.timeout.connect(self._force_close)
def setup_animations(self):
"""Richtet Fade-Animationen ein"""
self.fade_animation = QPropertyAnimation(self, b"windowOpacity")
self.fade_animation.setDuration(self.style_manager.ANIMATIONS['fade_duration'])
self.fade_animation.setEasingCurve(QEasingCurve.OutCubic)
def _get_modal_texts(self) -> Dict[str, Dict[str, str]]:
"""Gibt die Modal-Texte zurück"""
# Diese Methode ist jetzt nur für Rückwärtskompatibilität
# Die echten Texte kommen vom style_manager
return self.style_manager.get_modal_texts()
def show_process(self, process_type: Optional[str] = None):
"""
Zeigt das Modal für einen bestimmten Prozess-Typ an.
Args:
process_type: Optional - überschreibt den Modal-Typ
"""
if process_type:
self.modal_type = process_type
self.is_process_running = True
# Texte aktualisieren
self.update_texts()
# Animation starten
self.animation_widget.start_animation()
# Failsafe Timer starten
self.failsafe_timer.start(self.style_manager.ANIMATIONS['failsafe_timeout'])
# Modal anzeigen ohne Fade-In (immer voll sichtbar)
self.setWindowOpacity(1.0)
self.show()
# Fade-Animation deaktiviert für volle Sichtbarkeit
# if self.fade_animation:
# self.fade_animation.setStartValue(0.0)
# self.fade_animation.setEndValue(1.0)
# self.fade_animation.start()
logger.info(f"Progress Modal angezeigt für: {self.modal_type}")
def hide_process(self):
"""Versteckt das Modal mit Fade-Out Animation"""
if not self.is_process_running:
return
self.is_process_running = False
# Timer stoppen
self.failsafe_timer.stop()
# Animation stoppen
self.animation_widget.stop_animation()
# Fade-Out Animation deaktiviert - sofort verstecken
self._finish_hide()
# if self.fade_animation:
# self.fade_animation.setStartValue(1.0)
# self.fade_animation.setEndValue(0.0)
# self.fade_animation.finished.connect(self._finish_hide)
# self.fade_animation.start()
# else:
# self._finish_hide()
logger.info(f"Progress Modal versteckt für: {self.modal_type}")
def _finish_hide(self):
"""Beendet das Verstecken des Modals"""
self.hide()
if self.fade_animation:
self.fade_animation.finished.disconnect()
def update_status(self, status: str, detail: str = None):
"""
Aktualisiert den Status-Text des Modals.
Args:
status: Haupt-Status-Text
detail: Optional - Detail-Text
"""
self.status_label.setText(status)
if detail:
self.detail_label.setText(detail)
self.detail_label.setVisible(True)
else:
self.detail_label.setVisible(False)
def show_error(self, error_message: str, auto_close_seconds: int = 3):
"""
Zeigt eine Fehlermeldung im Modal an.
Args:
error_message: Fehlermeldung
auto_close_seconds: Sekunden bis automatisches Schließen
"""
self.title_label.setText("❌ Fehler aufgetreten")
self.status_label.setText(error_message)
self.detail_label.setVisible(False)
# Animation stoppen
self.animation_widget.stop_animation()
# Auto-Close Timer
if auto_close_seconds > 0:
self.auto_close_timer = QTimer()
self.auto_close_timer.setSingleShot(True)
self.auto_close_timer.timeout.connect(self.hide_process)
self.auto_close_timer.start(auto_close_seconds * 1000)
def _force_close(self):
"""Zwangsschließung nach Timeout"""
logger.warning(f"Progress Modal Timeout erreicht für: {self.modal_type}")
self.force_closed.emit()
self.hide_process()
def update_texts(self):
"""Aktualisiert die Texte gemäß der aktuellen Sprache"""
if not self.language_manager:
# Fallback zu Standardtexten
texts = self.modal_texts.get(self.modal_type, self.modal_texts['generic'])
self.title_label.setText(texts['title'])
self.status_label.setText(texts['status'])
if texts['detail']:
self.detail_label.setText(texts['detail'])
self.detail_label.setVisible(True)
return
# Multilingual texts (für zukünftige Erweiterung)
title_key = f"modal.{self.modal_type}.title"
status_key = f"modal.{self.modal_type}.status"
detail_key = f"modal.{self.modal_type}.detail"
# Fallback zu Standardtexten wenn Übersetzung nicht vorhanden
texts = self.modal_texts.get(self.modal_type, self.modal_texts['generic'])
self.title_label.setText(
self.language_manager.get_text(title_key, texts['title'])
)
self.status_label.setText(
self.language_manager.get_text(status_key, texts['status'])
)
detail_text = self.language_manager.get_text(detail_key, texts['detail'])
if detail_text:
self.detail_label.setText(detail_text)
self.detail_label.setVisible(True)
else:
self.detail_label.setVisible(False)
def paintEvent(self, event):
"""Custom Paint Event für soliden Hintergrund"""
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
# Solider Hintergrund (komplett undurchsichtig)
overlay_color = QColor(self.style_manager.COLORS['overlay'])
brush = QBrush(overlay_color)
painter.fillRect(self.rect(), brush)
super().paintEvent(event)
def keyPressEvent(self, event):
"""Verhindert das Schließen mit Escape während Prozess läuft"""
if self.is_process_running and event.key() == Qt.Key_Escape:
event.ignore()
return
super().keyPressEvent(event)
def closeEvent(self, event):
"""Verhindert das Schließen während Prozess läuft"""
if self.is_process_running:
event.ignore()
return
super().closeEvent(event)