Batch-Export geht jetzt

Dieser Commit ist enthalten in:
Claude Project Manager
2025-11-16 23:24:04 +01:00
Ursprung 404100abc4
Commit 2d276f167c
4 geänderte Dateien mit 471 neuen und 28 gelöschten Zeilen

Datei anzeigen

@ -176,9 +176,6 @@ class ProfileExportController:
""" """
Exportiert mehrere Accounts gleichzeitig. Exportiert mehrere Accounts gleichzeitig.
HINWEIS: Diese Funktion ist vorbereitet für zukünftige Erweiterung,
wird aber gemäß ROADMAP.md vorerst NICHT implementiert (YAGNI).
Args: Args:
parent_widget: Parent-Widget parent_widget: Parent-Widget
account_ids: Liste von Account-IDs account_ids: Liste von Account-IDs
@ -186,10 +183,154 @@ class ProfileExportController:
Returns: Returns:
True bei Erfolg, False bei Fehler True bei Erfolg, False bei Fehler
""" """
show_warning( try:
if not account_ids:
logger.warning("Keine Account-IDs zum Export übergeben")
return False
logger.info(f"Starte Batch-Export für {len(account_ids)} Accounts")
# 1. Alle Account-Daten laden
accounts_data = []
for account_id in account_ids:
account_data = self.db_manager.get_account(account_id)
if account_data:
accounts_data.append(account_data)
else:
logger.warning(f"Account ID {account_id} nicht gefunden")
if not accounts_data:
show_error(
parent_widget, parent_widget,
"Nicht verfügbar", "Keine Accounts gefunden",
"Mehrfach-Export ist derzeit nicht verfügbar.\n" "Keiner der ausgewählten Accounts konnte geladen werden."
"Bitte exportieren Sie Accounts einzeln." )
return False
logger.info(f"{len(accounts_data)} Accounts erfolgreich geladen")
# 2. Export-Dialog anzeigen
accepted, formats, _ = show_export_dialog(parent_widget, f"{len(accounts_data)} Accounts")
if not accepted:
logger.info("Batch-Export abgebrochen durch Nutzer")
return False
logger.info(f"Batch-Export-Optionen: Formate={formats}")
# 3. Hauptordner wählen
from datetime import datetime
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M")
suggested_folder_name = f"AccountForge_Export_{timestamp}"
save_directory = QFileDialog.getExistingDirectory(
parent_widget,
"Hauptordner für Batch-Export auswählen",
str(Path.home() / "Downloads" / suggested_folder_name)
)
if not save_directory:
logger.info("Kein Speicherort ausgewählt - Export abgebrochen")
return False
# Hauptordner erstellen falls nicht vorhanden
os.makedirs(save_directory, exist_ok=True)
logger.info(f"Batch-Export-Ordner: {save_directory}")
# 4. Für jeden Account exportieren
exported_files = []
failed_accounts = []
for account_data in accounts_data:
username = account_data.get("username", "unknown")
platform = account_data.get("platform", "unknown").lower()
try:
# Plattform-Unterordner erstellen
platform_dir = os.path.join(save_directory, platform)
os.makedirs(platform_dir, exist_ok=True)
# Export durchführen
files_dict = self.export_service.export_account(
account_data,
formats,
password_protect=False
)
# Dateien mit vereinfachten Namen speichern (ohne Timestamp)
for filename, content in files_dict.items():
# Vereinfachter Name: username.extension
ext = filename.split('.')[-1]
simple_filename = f"{username}.{ext}"
file_path = os.path.join(platform_dir, simple_filename)
with open(file_path, 'wb') as f:
f.write(content)
exported_files.append(f"{platform}/{simple_filename}")
logger.info(f"Exportiert: {platform}/{simple_filename}")
except Exception as e:
logger.error(f"Fehler beim Export von {username}: {e}")
failed_accounts.append(username)
# 5. Summary-Datei erstellen
summary_path = os.path.join(save_directory, "export_summary.txt")
with open(summary_path, 'w', encoding='utf-8') as f:
f.write(f"AccountForge Batch-Export\n")
f.write(f"="*50 + "\n\n")
f.write(f"Exportiert am: {datetime.now().strftime('%d.%m.%Y %H:%M:%S')}\n")
f.write(f"Anzahl Accounts: {len(accounts_data)}\n")
f.write(f"Erfolgreich: {len(accounts_data) - len(failed_accounts)}\n")
if failed_accounts:
f.write(f"Fehlgeschlagen: {len(failed_accounts)}\n")
f.write(f"\nFormate: {', '.join(formats).upper()}\n")
f.write(f"\n" + "="*50 + "\n\n")
# Gruppiere nach Plattform
platforms = {}
for account_data in accounts_data:
platform = account_data.get("platform", "unknown").lower()
if platform not in platforms:
platforms[platform] = []
platforms[platform].append(account_data.get("username", ""))
for platform, usernames in sorted(platforms.items()):
f.write(f"{platform.capitalize()}:\n")
for username in usernames:
if username in failed_accounts:
f.write(f"{username} (FEHLER)\n")
else:
f.write(f"{username}\n")
f.write(f"\n")
exported_files.append("export_summary.txt")
logger.info("Summary-Datei erstellt")
# 6. Erfolgs-Dialog anzeigen
show_export_success(
parent_widget,
exported_files,
save_directory
)
if failed_accounts:
show_warning(
parent_widget,
"Teilweise erfolgreich",
f"Export abgeschlossen, aber {len(failed_accounts)} Account(s) fehlgeschlagen:\n" +
"\n".join(f"- {name}" for name in failed_accounts[:5]) +
(f"\n... und {len(failed_accounts)-5} weitere" if len(failed_accounts) > 5 else "")
)
logger.info(f"Batch-Export erfolgreich: {len(exported_files)} Datei(en)")
return True
except Exception as e:
logger.error(f"Fehler beim Batch-Export: {e}", exc_info=True)
show_error(
parent_widget,
"Batch-Export fehlgeschlagen",
f"Beim Batch-Export ist ein Fehler aufgetreten:\n\n{str(e)}"
) )
return False return False

Datei anzeigen

@ -5,7 +5,7 @@ Accounts Overview View - Account-Übersicht im Mockup-Style
import logging import logging
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QWidget, QHBoxLayout, QVBoxLayout, QLabel, QPushButton, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QPushButton,
QScrollArea, QGridLayout, QFrame, QMessageBox QScrollArea, QGridLayout, QFrame, QMessageBox, QCheckBox
) )
from PyQt5.QtCore import Qt, pyqtSignal from PyQt5.QtCore import Qt, pyqtSignal
from PyQt5.QtGui import QFont from PyQt5.QtGui import QFont
@ -106,6 +106,7 @@ class AccountsOverviewView(QWidget):
Account-Übersicht im Mockup-Style mit: Account-Übersicht im Mockup-Style mit:
- Sidebar-Filter - Sidebar-Filter
- Grid-Layout mit Account-Karten - Grid-Layout mit Account-Karten
- Batch-Export mit Checkbox-Auswahl
""" """
# Signals # Signals
@ -113,6 +114,7 @@ class AccountsOverviewView(QWidget):
account_export_requested = pyqtSignal(dict) account_export_requested = pyqtSignal(dict)
account_delete_requested = pyqtSignal(dict) account_delete_requested = pyqtSignal(dict)
export_requested = pyqtSignal() # Für Kompatibilität export_requested = pyqtSignal() # Für Kompatibilität
bulk_export_requested = pyqtSignal(list) # List[int] - account_ids
def __init__(self, db_manager=None, language_manager=None): def __init__(self, db_manager=None, language_manager=None):
super().__init__() super().__init__()
@ -120,6 +122,9 @@ class AccountsOverviewView(QWidget):
self.language_manager = language_manager self.language_manager = language_manager
self.current_filter = "all" self.current_filter = "all"
self.accounts = [] self.accounts = []
self.selected_account_ids = set() # Set of selected account IDs
self.account_cards = [] # List of AccountCard widgets
self._updating_selection = False # Flag to prevent selection loops
self.init_ui() self.init_ui()
if self.language_manager: if self.language_manager:
@ -159,8 +164,142 @@ class AccountsOverviewView(QWidget):
header_layout.addStretch() header_layout.addStretch()
# Selection Mode Button
self.selection_mode_btn = QPushButton("Exportieren")
self.selection_mode_btn.setCursor(Qt.PointingHandCursor)
self.selection_mode_btn.setStyleSheet("""
QPushButton {
background-color: #10B981;
color: white;
border: none;
border-radius: 6px;
font-size: 13px;
font-weight: 600;
font-family: 'Poppins', sans-serif;
padding: 8px 16px;
min-width: 100px;
}
QPushButton:hover {
background-color: #059669;
}
QPushButton:pressed {
background-color: #047857;
}
""")
self.selection_mode_btn.clicked.connect(self._enable_selection_mode)
header_layout.addWidget(self.selection_mode_btn)
content_layout.addLayout(header_layout) content_layout.addLayout(header_layout)
# Selection Toolbar (initially hidden)
self.toolbar = QFrame()
self.toolbar.setObjectName("selection_toolbar")
self.toolbar.setVisible(False)
self.toolbar.setStyleSheet("""
#selection_toolbar {
background-color: #F3F4F6;
border-radius: 8px;
padding: 12px 16px;
}
""")
toolbar_layout = QHBoxLayout(self.toolbar)
toolbar_layout.setContentsMargins(0, 0, 0, 0)
toolbar_layout.setSpacing(16)
# "Alle auswählen" Checkbox
self.select_all_checkbox = QCheckBox("Alle auswählen")
self.select_all_checkbox.setTristate(True)
self.select_all_checkbox.setStyleSheet("""
QCheckBox {
font-size: 13px;
font-family: 'Poppins', sans-serif;
font-weight: 500;
color: #374151;
}
QCheckBox::indicator {
width: 18px;
height: 18px;
border-radius: 4px;
border: 2px solid #D1D5DB;
}
QCheckBox::indicator:checked {
background-color: #0099CC;
border-color: #0099CC;
}
QCheckBox::indicator:indeterminate {
background-color: #0099CC;
border-color: #0099CC;
opacity: 0.6;
}
""")
self.select_all_checkbox.stateChanged.connect(self._on_select_all_changed)
toolbar_layout.addWidget(self.select_all_checkbox)
# Selection count label
self.selection_count_label = QLabel("0 ausgewählt")
self.selection_count_label.setStyleSheet("""
color: #6B7280;
font-size: 13px;
font-family: 'Poppins', sans-serif;
""")
toolbar_layout.addWidget(self.selection_count_label)
toolbar_layout.addStretch()
# Export button
self.bulk_export_btn = QPushButton("Exportieren")
self.bulk_export_btn.setEnabled(False)
self.bulk_export_btn.setCursor(Qt.PointingHandCursor)
self.bulk_export_btn.setStyleSheet("""
QPushButton {
background-color: #0099CC;
color: white;
border: none;
border-radius: 6px;
font-size: 13px;
font-weight: 600;
font-family: 'Poppins', sans-serif;
padding: 8px 16px;
min-width: 100px;
}
QPushButton:hover {
background-color: #0078A3;
}
QPushButton:pressed {
background-color: #005C7A;
}
QPushButton:disabled {
background-color: #D1D5DB;
color: #9CA3AF;
}
""")
self.bulk_export_btn.clicked.connect(self._on_bulk_export_clicked)
toolbar_layout.addWidget(self.bulk_export_btn)
# Close selection mode button
close_btn = QPushButton("")
close_btn.setToolTip("Auswahl beenden")
close_btn.setCursor(Qt.PointingHandCursor)
close_btn.setStyleSheet("""
QPushButton {
background-color: transparent;
color: #6B7280;
border: none;
font-size: 18px;
padding: 4px;
min-width: 24px;
max-width: 24px;
}
QPushButton:hover {
color: #374151;
}
""")
close_btn.clicked.connect(self._disable_selection_mode)
toolbar_layout.addWidget(close_btn)
content_layout.addWidget(self.toolbar)
# Scroll Area für Accounts # Scroll Area für Accounts
self.scroll = QScrollArea() self.scroll = QScrollArea()
self.scroll.setWidgetResizable(True) self.scroll.setWidgetResizable(True)
@ -202,7 +341,8 @@ class AccountsOverviewView(QWidget):
def _update_display(self): def _update_display(self):
"""Aktualisiert die Anzeige basierend auf dem aktuellen Filter""" """Aktualisiert die Anzeige basierend auf dem aktuellen Filter"""
# Clear existing widgets # Clear existing widgets and account_cards list
self.account_cards = []
while self.grid_layout.count(): while self.grid_layout.count():
child = self.grid_layout.takeAt(0) child = self.grid_layout.takeAt(0)
if child.widget(): if child.widget():
@ -292,6 +432,10 @@ class AccountsOverviewView(QWidget):
card.login_requested.connect(self.account_login_requested.emit) card.login_requested.connect(self.account_login_requested.emit)
card.export_requested.connect(self.account_export_requested.emit) card.export_requested.connect(self.account_export_requested.emit)
card.delete_requested.connect(self._on_delete_requested) card.delete_requested.connect(self._on_delete_requested)
card.selection_changed.connect(self._on_card_selection_changed)
# Zu Liste hinzufügen für Batch-Operations
self.account_cards.append(card)
return card return card
@ -377,3 +521,90 @@ class AccountsOverviewView(QWidget):
""" """
# Session-Funktionalität wurde entfernt - diese Methode macht nichts mehr # Session-Funktionalität wurde entfernt - diese Methode macht nichts mehr
pass pass
# ========== Selection Mode Methods ==========
def _enable_selection_mode(self):
"""Aktiviert den Selection-Modus"""
# Show toolbar, hide selection button
self.toolbar.setVisible(True)
self.selection_mode_btn.setVisible(False)
# Enable selection mode on all cards
for card in self.account_cards:
card.set_selection_mode(True)
# Reset selection state
self.selected_account_ids.clear()
self._update_selection_ui()
def _disable_selection_mode(self):
"""Deaktiviert den Selection-Modus"""
# Hide toolbar, show selection button
self.toolbar.setVisible(False)
self.selection_mode_btn.setVisible(True)
# Disable selection mode on all cards
for card in self.account_cards:
card.set_selection_mode(False)
# Clear selection
self.selected_account_ids.clear()
self._update_selection_ui()
def _on_card_selection_changed(self, account_id: int, selected: bool):
"""Handler wenn eine Card ausgewählt/abgewählt wird"""
if selected:
self.selected_account_ids.add(account_id)
else:
self.selected_account_ids.discard(account_id)
self._update_selection_ui()
def _on_select_all_changed(self, state):
"""Handler für "Alle auswählen" Checkbox"""
if self._updating_selection:
return
select_all = (state == Qt.Checked)
# Set all cards to the same selection state
self._updating_selection = True
for card in self.account_cards:
card.set_selected(select_all)
self._updating_selection = False
def _update_selection_ui(self):
"""Aktualisiert die Selection-UI (Count-Label, Button-Status)"""
count = len(self.selected_account_ids)
# Update count label
if count == 0:
self.selection_count_label.setText("0 ausgewählt")
elif count == 1:
self.selection_count_label.setText("1 ausgewählt")
else:
self.selection_count_label.setText(f"{count} ausgewählt")
# Enable/Disable export button
self.bulk_export_btn.setEnabled(count > 0)
# Update "Alle auswählen" checkbox state
total_cards = len(self.account_cards)
if total_cards > 0:
self._updating_selection = True
if count == 0:
self.select_all_checkbox.setCheckState(Qt.Unchecked)
elif count == total_cards:
self.select_all_checkbox.setCheckState(Qt.Checked)
else:
self.select_all_checkbox.setCheckState(Qt.PartiallyChecked)
self._updating_selection = False
def _on_bulk_export_clicked(self):
"""Handler für Bulk-Export-Button"""
if len(self.selected_account_ids) == 0:
return
# Emit signal with list of selected account IDs
self.bulk_export_requested.emit(list(self.selected_account_ids))

Datei anzeigen

@ -60,6 +60,7 @@ class PlatformSelector(QWidget):
self.accounts_overview.account_login_requested.connect(self._on_login_requested) self.accounts_overview.account_login_requested.connect(self._on_login_requested)
self.accounts_overview.account_export_requested.connect(self._on_export_requested) self.accounts_overview.account_export_requested.connect(self._on_export_requested)
self.accounts_overview.account_delete_requested.connect(self._on_delete_requested) self.accounts_overview.account_delete_requested.connect(self._on_delete_requested)
self.accounts_overview.bulk_export_requested.connect(self._on_bulk_export_requested)
self.content_stack.addWidget(self.accounts_overview) self.content_stack.addWidget(self.accounts_overview)
# Für Kompatibilität mit MainController - accounts_tab Referenz # Für Kompatibilität mit MainController - accounts_tab Referenz
@ -123,6 +124,13 @@ class PlatformSelector(QWidget):
except Exception as e: except Exception as e:
print(f"Fehler beim Löschen des Accounts: {e}") print(f"Fehler beim Löschen des Accounts: {e}")
def _on_bulk_export_requested(self, account_ids):
"""Behandelt Bulk-Export-Anfragen."""
from controllers.profile_export_controller import ProfileExportController
if self.db_manager:
controller = ProfileExportController(self.db_manager)
controller.export_multiple_accounts(self, account_ids)
def update_texts(self): def update_texts(self):
"""Aktualisiert die Texte gemäß der aktuellen Sprache.""" """Aktualisiert die Texte gemäß der aktuellen Sprache."""
# Die Komponenten aktualisieren ihre Texte selbst # Die Komponenten aktualisieren ihre Texte selbst

Datei anzeigen

@ -4,7 +4,7 @@ Account Card Widget - Kompakte Account-Karte nach Styleguide
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QFrame, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QFrame, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QGridLayout, QWidget, QApplication QGridLayout, QWidget, QApplication, QCheckBox
) )
from PyQt5.QtCore import Qt, pyqtSignal, QSize, QTimer from PyQt5.QtCore import Qt, pyqtSignal, QSize, QTimer
from PyQt5.QtGui import QFont, QPixmap from PyQt5.QtGui import QFont, QPixmap
@ -22,12 +22,15 @@ class AccountCard(QFrame):
login_requested = pyqtSignal(dict) # Account-Daten login_requested = pyqtSignal(dict) # Account-Daten
export_requested = pyqtSignal(dict) # Account-Daten export_requested = pyqtSignal(dict) # Account-Daten
delete_requested = pyqtSignal(dict) # Account-Daten delete_requested = pyqtSignal(dict) # Account-Daten
selection_changed = pyqtSignal(int, bool) # account_id, selected
def __init__(self, account_data, language_manager=None): def __init__(self, account_data, language_manager=None):
super().__init__() super().__init__()
self.account_data = account_data self.account_data = account_data
self.language_manager = language_manager self.language_manager = language_manager
self.password_visible = False self.password_visible = False
self.selection_mode = False
self.is_selected_state = False
# Timer für Icon-Animation # Timer für Icon-Animation
self.email_copy_timer = QTimer() self.email_copy_timer = QTimer()
@ -143,6 +146,24 @@ class AccountCard(QFrame):
# Status wird jetzt über Karten-Hintergrund und Umrandung angezeigt # Status wird jetzt über Karten-Hintergrund und Umrandung angezeigt
# Selection Checkbox (hidden by default)
self.selection_checkbox = QCheckBox()
self.selection_checkbox.setVisible(False)
self.selection_checkbox.stateChanged.connect(self._on_selection_changed)
self.selection_checkbox.setStyleSheet("""
QCheckBox::indicator {
width: 20px;
height: 20px;
border-radius: 4px;
border: 2px solid #D1D5DB;
}
QCheckBox::indicator:checked {
background-color: #0099CC;
border-color: #0099CC;
}
""")
info_layout.addWidget(self.selection_checkbox)
# Platform Icon # Platform Icon
platform_icon = IconFactory.create_icon_label( platform_icon = IconFactory.create_icon_label(
self.account_data.get("platform", "").lower(), self.account_data.get("platform", "").lower(),
@ -379,3 +400,45 @@ class AccountCard(QFrame):
"""Aktualisiert den Status der Account-Karte und das Styling""" """Aktualisiert den Status der Account-Karte und das Styling"""
self.account_data["status"] = new_status self.account_data["status"] = new_status
self._apply_status_styling() self._apply_status_styling()
# ========== Selection Mode Methods ==========
def set_selection_mode(self, enabled: bool):
"""
Aktiviert/Deaktiviert den Selection-Modus.
Args:
enabled: True = Checkbox anzeigen, False = Checkbox verstecken
"""
self.selection_mode = enabled
self.selection_checkbox.setVisible(enabled)
if not enabled:
# Reset selection when mode is disabled
self.set_selected(False)
def _on_selection_changed(self, state):
"""Handler für Checkbox state change"""
self.is_selected_state = (state == Qt.Checked)
account_id = self.account_data.get("id")
if account_id:
self.selection_changed.emit(account_id, self.is_selected_state)
def is_selected(self) -> bool:
"""
Gibt zurück ob die Card ausgewählt ist.
Returns:
True wenn ausgewählt, sonst False
"""
return self.is_selected_state
def set_selected(self, selected: bool):
"""
Setzt den Auswahl-Status.
Args:
selected: True = auswählen, False = Auswahl aufheben
"""
self.is_selected_state = selected
self.selection_checkbox.setChecked(selected)