from __future__ import annotations from pathlib import Path from typing import Dict, Optional from PyQt6.QtCore import Qt from PyQt6.QtGui import QAction, QKeySequence from PyQt6.QtWidgets import ( QFileDialog, QLabel, QMainWindow, QStatusBar, QVBoxLayout, QWidget, QMessageBox, ) from photo_sorter.core.config_manager import AppConfig, ConfigManager, TargetConfig from photo_sorter.core.file_operations import FileOperations, FileOperationError from photo_sorter.core.image_loader import ImageLoader from photo_sorter.ui.image_viewer import ImageViewer from photo_sorter.ui.settings_dialog import SettingsDialog, KEYS from photo_sorter.ui.target_bar import TargetBar, TargetDisplay class MainWindow(QMainWindow): def __init__(self, config_manager: ConfigManager, parent: Optional[QWidget] = None) -> None: # type: ignore[name-defined] super().__init__(parent) self.setWindowTitle("Photo Sorter") self.resize(1000, 700) self._config_manager = config_manager self._config: AppConfig = self._config_manager.load() self._image_loader = ImageLoader() self._file_ops = FileOperations() self._targets_by_key: Dict[str, TargetConfig] = {t.key: t for t in self._config.targets} self._setup_ui() self._apply_config_to_ui() # Beim Start direkt Quellverzeichnis wählen self._select_source_directory(initial=True) # --- UI Setup --- def _setup_ui(self) -> None: central = QWidget(self) self.setCentralWidget(central) layout = QVBoxLayout(central) layout.setContentsMargins(8, 8, 8, 4) layout.setSpacing(8) self._info_label = QLabel("Kein Bild geladen", self) self._info_label.setAlignment(Qt.AlignmentFlag.AlignCenter) layout.addWidget(self._info_label) self._viewer = ImageViewer(self) layout.addWidget(self._viewer, stretch=1) self._target_bar = TargetBar(self) layout.addWidget(self._target_bar) status = QStatusBar(self) self.setStatusBar(status) self._create_menus() def _create_menus(self) -> None: menubar = self.menuBar() file_menu = menubar.addMenu("Datei") open_src_action = QAction("Quellverzeichnis öffnen…", self) open_src_action.setShortcut(QKeySequence.StandardKey.Open) open_src_action.triggered.connect(self._select_source_directory) # type: ignore[arg-type] file_menu.addAction(open_src_action) file_menu.addSeparator() exit_action = QAction("Beenden", self) exit_action.setShortcut(QKeySequence.StandardKey.Quit) exit_action.triggered.connect(self.close) # type: ignore[arg-type] file_menu.addAction(exit_action) settings_menu = menubar.addMenu("Einstellungen") targets_action = QAction("Ziele konfigurieren…", self) targets_action.triggered.connect(self._open_settings) # type: ignore[arg-type] settings_menu.addAction(targets_action) # --- Konfiguration & Zielleiste --- def _apply_config_to_ui(self) -> None: self._targets_by_key = {t.key: t for t in self._config.targets} displays = [TargetDisplay(key=t.key, label=t.label) for t in self._config.targets] self._target_bar.set_targets(displays) def _open_settings(self) -> None: dialog = SettingsDialog(self._config, self) if dialog.exec() == dialog.DialogCode.Accepted: # type: ignore[comparison-overlap] new_config = dialog.get_config() self._config = new_config self._config_manager.save(new_config) self._apply_config_to_ui() # --- Quellverzeichnis & Bildanzeige --- def _select_source_directory(self, initial: bool = False) -> None: directory = QFileDialog.getExistingDirectory(self, "Quellverzeichnis wählen") if not directory: if initial: # Wenn beim ersten Start nichts gewählt wird, bleibt die App einfach leer geöffnet. return return self._image_loader.load_from_directory(Path(directory)) if self._image_loader.count == 0: QMessageBox.information( self, "Keine Bilder", "Im gewählten Verzeichnis wurden keine unterstützten Bilddateien gefunden.", ) self._viewer.clear() self._info_label.setText("Keine Bilder gefunden") return self._show_current_image() def _show_current_image(self) -> None: path = self._image_loader.get_current() count = self._image_loader.count index = self._image_loader.current_index if path is None: self._viewer.clear() self._info_label.setText("Kein Bild geladen") return self._viewer.set_image(path) self._info_label.setText(f"Bild {index + 1} / {count} – {path.name}") # --- Tastatursteuerung --- def keyPressEvent(self, event) -> None: # type: ignore[override] key = event.key() if key in (Qt.Key.Key_Right, Qt.Key.Key_Space): self._skip_forward() return if key == Qt.Key.Key_Left: self._go_back() return if key in (Qt.Key.Key_Z,) and (event.modifiers() & Qt.KeyboardModifier.ControlModifier): self._undo_action() return if key == Qt.Key.Key_Z: self._undo_action() return # Zieltasten 1–0 key_char = None if key == Qt.Key.Key_1: key_char = "1" elif key == Qt.Key.Key_2: key_char = "2" elif key == Qt.Key.Key_3: key_char = "3" elif key == Qt.Key.Key_4: key_char = "4" elif key == Qt.Key.Key_5: key_char = "5" elif key == Qt.Key.Key_6: key_char = "6" elif key == Qt.Key.Key_7: key_char = "7" elif key == Qt.Key.Key_8: key_char = "8" elif key == Qt.Key.Key_9: key_char = "9" elif key == Qt.Key.Key_0: key_char = "0" if key_char is not None: self._apply_target_action(key_char) return super().keyPressEvent(event) def _skip_forward(self) -> None: if self._image_loader.count == 0: return self._image_loader.next() self._show_current_image() def _go_back(self) -> None: if self._image_loader.count == 0: return self._image_loader.previous() self._show_current_image() def _apply_target_action(self, key: str) -> None: if self._image_loader.count == 0: return target = self._targets_by_key.get(key) if not target: return current = self._image_loader.get_current() if current is None: return try: new_path = self._file_ops.move_or_copy( current, Path(target.path), self._config.default_action, self._image_loader.current_index, ) except FileOperationError as e: QMessageBox.warning(self, "Fehler beim Verschieben/Kopieren", str(e)) return # Pfad in der Bildliste aktualisieren (für Zurückblättern etc.) self._image_loader.update_current_path(new_path) # Visuelles Highlight des Ziels self._target_bar.highlight_target(key) # Nächstes Bild laden self._image_loader.next() self._show_current_image() def _undo_action(self) -> None: from photo_sorter.core.file_operations import FileAction action = self._file_ops.undo_last() if action is None: return # Bildliste wieder auf den Index der Aktion setzen self._image_loader.set_index(action.index) # Pfad in der Liste anpassen (Move: wieder Originalpfad, Copy: Original bleibt, Liste ändert sich nicht) if action.action == "move": self._image_loader.update_current_path(action.original_path) self._show_current_image()