Initial commit
Dieser Commit ist enthalten in:
420
views/widgets/account_card.py
Normale Datei
420
views/widgets/account_card.py
Normale Datei
@ -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()
|
||||
209
views/widgets/account_creation_modal.py
Normale Datei
209
views/widgets/account_creation_modal.py
Normale Datei
@ -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"
|
||||
625
views/widgets/account_creation_modal_v2.py
Normale Datei
625
views/widgets/account_creation_modal_v2.py
Normale Datei
@ -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_())
|
||||
272
views/widgets/forge_animation_widget.py
Normale Datei
272
views/widgets/forge_animation_widget.py
Normale Datei
@ -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
|
||||
370
views/widgets/forge_animation_widget_v2.py
Normale Datei
370
views/widgets/forge_animation_widget_v2.py
Normale Datei
@ -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
192
views/widgets/icon_factory.py
Normale Datei
@ -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)
|
||||
203
views/widgets/language_dropdown.py
Normale Datei
203
views/widgets/language_dropdown.py
Normale Datei
@ -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)
|
||||
295
views/widgets/login_process_modal.py
Normale Datei
295
views/widgets/login_process_modal.py
Normale Datei
@ -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
|
||||
316
views/widgets/modern_message_box.py
Normale Datei
316
views/widgets/modern_message_box.py
Normale Datei
@ -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_()
|
||||
89
views/widgets/platform_button.py
Normale Datei
89
views/widgets/platform_button.py
Normale Datei
@ -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;")
|
||||
312
views/widgets/progress_modal.py
Normale Datei
312
views/widgets/progress_modal.py
Normale Datei
@ -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)
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren