v1.1.0: Vollständiges Rechtsklick-Kontextmenü
- Kopieren/Ausschneiden/Einfügen (Ctrl+C, X, V) - Neue Datei mit Vorlagen (Python, JS, HTML, CSS, JSON, MD, Shell) - Neuer Ordner erstellen - Archiv-Funktionen (ZIP, TAR, TAR.GZ, TAR.BZ2, TAR.XZ) - Entpacken von Archiven (+ 7z, RAR wenn installiert) - Eigenschaften-Dialog (Größe, Berechtigungen, Zeitstempel) - Terminal hier öffnen (F4) - Hover-Farben für Themes korrigiert - README.md Dokumentation hinzugefügt Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
a9761ec33b
commit
1f43e234d3
10 changed files with 1625 additions and 71 deletions
92
README.md
Normal file
92
README.md
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
# FileBrowser
|
||||||
|
|
||||||
|
Ein moderner Dateimanager mit Vorschau-Funktion, geschrieben in Python mit PyQt6.
|
||||||
|
|
||||||
|
## Version
|
||||||
|
|
||||||
|
**v1.1.0** - Vollständiges Rechtsklick-Kontextmenü
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Dateioperationen
|
||||||
|
- **Kopieren/Ausschneiden/Einfügen** (Ctrl+C, Ctrl+X, Ctrl+V)
|
||||||
|
- **Neue Datei erstellen** (Ctrl+N) - mit Vorlagen für Python, JS, HTML, CSS, JSON, Markdown, Shell
|
||||||
|
- **Neuer Ordner** (Ctrl+Shift+N)
|
||||||
|
- **Umbenennen** (F2)
|
||||||
|
- **Löschen** (Delete)
|
||||||
|
- **Verschieben/Kopieren nach...**
|
||||||
|
- **Drag & Drop** Unterstützung
|
||||||
|
|
||||||
|
### Archiv-Funktionen
|
||||||
|
- **Packen:** ZIP, TAR, TAR.GZ, TAR.BZ2, TAR.XZ
|
||||||
|
- **Entpacken:** ZIP, TAR, TAR.GZ, TAR.BZ2, TAR.XZ (+ 7z, RAR wenn installiert)
|
||||||
|
|
||||||
|
### Vorschau
|
||||||
|
- PDF-Dokumente (mit Zoom und Mehrseiten-Ansicht)
|
||||||
|
- Bilder (PNG, JPG, GIF, etc.)
|
||||||
|
- Markdown (gerendert)
|
||||||
|
- Text und Code-Dateien
|
||||||
|
- Vorschau im Panel oder abgetrenntem Fenster
|
||||||
|
|
||||||
|
### Weitere Funktionen
|
||||||
|
- **Terminal öffnen** (F4)
|
||||||
|
- **Eigenschaften-Dialog** (Alt+Enter) - Dateigröße, Berechtigungen, Zeitstempel
|
||||||
|
- **Mehrere Themes** - Dark, Breeze Dark, Breeze Light, System
|
||||||
|
- **Breadcrumb-Navigation** mit editierbarem Pfad
|
||||||
|
|
||||||
|
## Tastenkürzel
|
||||||
|
|
||||||
|
| Kürzel | Funktion |
|
||||||
|
|--------|----------|
|
||||||
|
| Ctrl+N | Neue Datei |
|
||||||
|
| Ctrl+Shift+N | Neuer Ordner |
|
||||||
|
| Ctrl+C | Kopieren |
|
||||||
|
| Ctrl+X | Ausschneiden |
|
||||||
|
| Ctrl+V | Einfügen |
|
||||||
|
| Ctrl+A | Alles auswählen |
|
||||||
|
| F2 | Umbenennen |
|
||||||
|
| F4 | Terminal öffnen |
|
||||||
|
| F5 | Aktualisieren |
|
||||||
|
| Delete | Löschen |
|
||||||
|
| Alt+Enter | Eigenschaften |
|
||||||
|
| Backspace | Ordner nach oben |
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Abhängigkeiten installieren
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# Starten
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Abhängigkeiten
|
||||||
|
|
||||||
|
- Python 3.10+
|
||||||
|
- PyQt6
|
||||||
|
- PyMuPDF (fitz) - für PDF-Vorschau
|
||||||
|
- Markdown - für Markdown-Rendering
|
||||||
|
|
||||||
|
## Rechtsklick-Kontextmenü
|
||||||
|
|
||||||
|
### Bei Datei/Ordner-Auswahl:
|
||||||
|
- Öffnen / Öffnen mit...
|
||||||
|
- Neu → Neue Datei / Neuer Ordner
|
||||||
|
- Ausschneiden / Kopieren
|
||||||
|
- Umbenennen
|
||||||
|
- Verschieben nach... / Kopieren nach...
|
||||||
|
- Zu Archiv packen... (oder Entpacken bei Archiven)
|
||||||
|
- Löschen
|
||||||
|
- Eigenschaften
|
||||||
|
|
||||||
|
### Bei leerem Bereich:
|
||||||
|
- Neu → Neue Datei / Neuer Ordner
|
||||||
|
- Einfügen
|
||||||
|
- Aktualisieren
|
||||||
|
- Terminal hier öffnen
|
||||||
|
- Eigenschaften
|
||||||
|
|
||||||
|
## Lizenz
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
#!/usr/bin/env xdg-open
|
||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Name=FileBrowser
|
Name=FileBrowser
|
||||||
Comment=Dateimanager mit Vorschau-Funktion
|
Comment=Dateimanager mit Vorschau-Funktion
|
||||||
|
|
|
||||||
2
main.py
2
main.py
|
|
@ -1,6 +1,8 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""FileBrowser - Ein Dateimanager mit Vorschau-Funktion in PyQt6."""
|
"""FileBrowser - Ein Dateimanager mit Vorschau-Funktion in PyQt6."""
|
||||||
|
|
||||||
|
__version__ = "1.1.0"
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
from PyQt6.QtWidgets import QApplication
|
from PyQt6.QtWidgets import QApplication
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
"""Dialoge für den FileBrowser."""
|
"""Dialoge für den FileBrowser."""
|
||||||
|
|
||||||
from .base import RenameDialog, MoveDialog, DeleteDialog, SettingsDialog
|
from .base import (
|
||||||
|
RenameDialog, MoveDialog, DeleteDialog, SettingsDialog,
|
||||||
|
NewFileDialog, PropertiesDialog, ArchiveDialog, ExtractDialog
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'RenameDialog', 'MoveDialog', 'DeleteDialog', 'SettingsDialog'
|
'RenameDialog', 'MoveDialog', 'DeleteDialog', 'SettingsDialog',
|
||||||
|
'NewFileDialog', 'PropertiesDialog', 'ArchiveDialog', 'ExtractDialog'
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,21 @@
|
||||||
"""Dialoge für Dateioperationen."""
|
"""Dialoge für Dateioperationen."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import stat
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from datetime import datetime
|
||||||
from PyQt6.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
|
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
|
||||||
QPushButton, QTreeView, QDialogButtonBox, QMessageBox,
|
QPushButton, QTreeView, QDialogButtonBox, QMessageBox,
|
||||||
QFrame, QCheckBox, QGroupBox, QTabWidget, QWidget
|
QFrame, QCheckBox, QGroupBox, QTabWidget, QWidget,
|
||||||
|
QFormLayout, QComboBox, QRadioButton, QButtonGroup,
|
||||||
|
QProgressBar, QListWidget, QListWidgetItem
|
||||||
)
|
)
|
||||||
from PyQt6.QtCore import Qt, QDir, QSettings
|
from PyQt6.QtCore import Qt, QDir, QSettings
|
||||||
from PyQt6.QtGui import QFileSystemModel
|
from PyQt6.QtGui import QFileSystemModel
|
||||||
|
|
||||||
|
from ..utils.file_utils import format_file_size
|
||||||
|
|
||||||
|
|
||||||
class RenameDialog(QDialog):
|
class RenameDialog(QDialog):
|
||||||
"""Dialog zum Umbenennen von Dateien."""
|
"""Dialog zum Umbenennen von Dateien."""
|
||||||
|
|
@ -338,3 +344,517 @@ class SettingsDialog(QDialog):
|
||||||
'disable_preview_panel': self.disable_preview_check.isChecked(),
|
'disable_preview_panel': self.disable_preview_check.isChecked(),
|
||||||
'path_text_default': self.path_text_default_check.isChecked(),
|
'path_text_default': self.path_text_default_check.isChecked(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class NewFileDialog(QDialog):
|
||||||
|
"""Dialog zum Erstellen einer neuen Datei."""
|
||||||
|
|
||||||
|
def __init__(self, current_folder: str, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.current_folder = current_folder
|
||||||
|
self.file_name = ""
|
||||||
|
|
||||||
|
self.setWindowTitle("Neue Datei")
|
||||||
|
self.setModal(True)
|
||||||
|
self.setMinimumWidth(400)
|
||||||
|
|
||||||
|
self._setup_ui()
|
||||||
|
|
||||||
|
def _setup_ui(self):
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
|
||||||
|
# Dateiname
|
||||||
|
layout.addWidget(QLabel("Dateiname:"))
|
||||||
|
|
||||||
|
self.name_input = QLineEdit()
|
||||||
|
self.name_input.setPlaceholderText("datei.txt")
|
||||||
|
self.name_input.returnPressed.connect(self.accept)
|
||||||
|
layout.addWidget(self.name_input)
|
||||||
|
|
||||||
|
# Vorlagen
|
||||||
|
layout.addWidget(QLabel("Vorlage:"))
|
||||||
|
|
||||||
|
self.template_combo = QComboBox()
|
||||||
|
self.template_combo.addItem("Leere Datei", "")
|
||||||
|
self.template_combo.addItem("Python-Datei (.py)", ".py")
|
||||||
|
self.template_combo.addItem("JavaScript (.js)", ".js")
|
||||||
|
self.template_combo.addItem("HTML-Datei (.html)", ".html")
|
||||||
|
self.template_combo.addItem("CSS-Datei (.css)", ".css")
|
||||||
|
self.template_combo.addItem("JSON-Datei (.json)", ".json")
|
||||||
|
self.template_combo.addItem("Markdown (.md)", ".md")
|
||||||
|
self.template_combo.addItem("Text-Datei (.txt)", ".txt")
|
||||||
|
self.template_combo.addItem("Shell-Script (.sh)", ".sh")
|
||||||
|
self.template_combo.currentIndexChanged.connect(self._on_template_changed)
|
||||||
|
layout.addWidget(self.template_combo)
|
||||||
|
|
||||||
|
# Buttons
|
||||||
|
button_box = QDialogButtonBox(
|
||||||
|
QDialogButtonBox.StandardButton.Ok |
|
||||||
|
QDialogButtonBox.StandardButton.Cancel
|
||||||
|
)
|
||||||
|
button_box.accepted.connect(self.accept)
|
||||||
|
button_box.rejected.connect(self.reject)
|
||||||
|
layout.addWidget(button_box)
|
||||||
|
|
||||||
|
def _on_template_changed(self, index):
|
||||||
|
"""Aktualisiert die Erweiterung basierend auf der Vorlage."""
|
||||||
|
extension = self.template_combo.currentData()
|
||||||
|
if extension:
|
||||||
|
current_name = self.name_input.text()
|
||||||
|
if current_name:
|
||||||
|
# Erweiterung ersetzen
|
||||||
|
base = Path(current_name).stem
|
||||||
|
self.name_input.setText(base + extension)
|
||||||
|
else:
|
||||||
|
self.name_input.setText("datei" + extension)
|
||||||
|
|
||||||
|
def accept(self):
|
||||||
|
"""Validiert und erstellt die Datei."""
|
||||||
|
self.file_name = self.name_input.text().strip()
|
||||||
|
|
||||||
|
if not self.file_name:
|
||||||
|
QMessageBox.warning(self, "Fehler", "Dateiname darf nicht leer sein.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if '/' in self.file_name or '\\' in self.file_name:
|
||||||
|
QMessageBox.warning(self, "Fehler", "Dateiname darf keine Pfadtrennzeichen enthalten.")
|
||||||
|
return
|
||||||
|
|
||||||
|
file_path = os.path.join(self.current_folder, self.file_name)
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
QMessageBox.warning(self, "Fehler", "Eine Datei mit diesem Namen existiert bereits.")
|
||||||
|
return
|
||||||
|
|
||||||
|
super().accept()
|
||||||
|
|
||||||
|
def get_file_name(self) -> str:
|
||||||
|
"""Gibt den Dateinamen zurück."""
|
||||||
|
return self.file_name
|
||||||
|
|
||||||
|
def get_template_content(self) -> str:
|
||||||
|
"""Gibt den Template-Inhalt zurück."""
|
||||||
|
extension = self.template_combo.currentData()
|
||||||
|
templates = {
|
||||||
|
".py": '#!/usr/bin/env python3\n"""Beschreibung."""\n\n\ndef main():\n pass\n\n\nif __name__ == "__main__":\n main()\n',
|
||||||
|
".js": '// Beschreibung\n\n"use strict";\n\n',
|
||||||
|
".html": '<!DOCTYPE html>\n<html lang="de">\n<head>\n <meta charset="UTF-8">\n <meta name="viewport" content="width=device-width, initial-scale=1.0">\n <title>Titel</title>\n</head>\n<body>\n \n</body>\n</html>\n',
|
||||||
|
".css": '/* Stylesheet */\n\n* {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n}\n',
|
||||||
|
".json": '{\n \n}\n',
|
||||||
|
".md": '# Titel\n\n',
|
||||||
|
".sh": '#!/bin/bash\n\n',
|
||||||
|
}
|
||||||
|
return templates.get(extension, "")
|
||||||
|
|
||||||
|
|
||||||
|
class PropertiesDialog(QDialog):
|
||||||
|
"""Dialog für Datei-/Ordner-Eigenschaften."""
|
||||||
|
|
||||||
|
def __init__(self, paths: list[str], parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.paths = paths
|
||||||
|
|
||||||
|
self.setWindowTitle("Eigenschaften")
|
||||||
|
self.setModal(True)
|
||||||
|
self.setMinimumWidth(400)
|
||||||
|
|
||||||
|
self._setup_ui()
|
||||||
|
self._load_properties()
|
||||||
|
|
||||||
|
def _setup_ui(self):
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
|
||||||
|
# Allgemeine Informationen
|
||||||
|
general_group = QGroupBox("Allgemein")
|
||||||
|
general_layout = QFormLayout(general_group)
|
||||||
|
|
||||||
|
self.name_label = QLabel()
|
||||||
|
self.name_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
|
||||||
|
general_layout.addRow("Name:", self.name_label)
|
||||||
|
|
||||||
|
self.type_label = QLabel()
|
||||||
|
general_layout.addRow("Typ:", self.type_label)
|
||||||
|
|
||||||
|
self.location_label = QLabel()
|
||||||
|
self.location_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
|
||||||
|
self.location_label.setWordWrap(True)
|
||||||
|
general_layout.addRow("Ort:", self.location_label)
|
||||||
|
|
||||||
|
self.size_label = QLabel()
|
||||||
|
general_layout.addRow("Größe:", self.size_label)
|
||||||
|
|
||||||
|
self.contains_label = QLabel()
|
||||||
|
general_layout.addRow("Enthält:", self.contains_label)
|
||||||
|
|
||||||
|
layout.addWidget(general_group)
|
||||||
|
|
||||||
|
# Zeitstempel
|
||||||
|
time_group = QGroupBox("Zeitstempel")
|
||||||
|
time_layout = QFormLayout(time_group)
|
||||||
|
|
||||||
|
self.created_label = QLabel()
|
||||||
|
time_layout.addRow("Erstellt:", self.created_label)
|
||||||
|
|
||||||
|
self.modified_label = QLabel()
|
||||||
|
time_layout.addRow("Geändert:", self.modified_label)
|
||||||
|
|
||||||
|
self.accessed_label = QLabel()
|
||||||
|
time_layout.addRow("Zugegriffen:", self.accessed_label)
|
||||||
|
|
||||||
|
layout.addWidget(time_group)
|
||||||
|
|
||||||
|
# Berechtigungen
|
||||||
|
perm_group = QGroupBox("Berechtigungen")
|
||||||
|
perm_layout = QFormLayout(perm_group)
|
||||||
|
|
||||||
|
self.permissions_label = QLabel()
|
||||||
|
perm_layout.addRow("Rechte:", self.permissions_label)
|
||||||
|
|
||||||
|
self.owner_label = QLabel()
|
||||||
|
perm_layout.addRow("Besitzer:", self.owner_label)
|
||||||
|
|
||||||
|
layout.addWidget(perm_group)
|
||||||
|
|
||||||
|
# Buttons
|
||||||
|
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok)
|
||||||
|
button_box.accepted.connect(self.accept)
|
||||||
|
layout.addWidget(button_box)
|
||||||
|
|
||||||
|
def _load_properties(self):
|
||||||
|
"""Lädt die Eigenschaften der Datei(en)."""
|
||||||
|
if len(self.paths) == 1:
|
||||||
|
self._load_single_properties(self.paths[0])
|
||||||
|
else:
|
||||||
|
self._load_multiple_properties()
|
||||||
|
|
||||||
|
def _load_single_properties(self, path: str):
|
||||||
|
"""Lädt Eigenschaften einer einzelnen Datei/Ordner."""
|
||||||
|
if not os.path.exists(path):
|
||||||
|
self.name_label.setText("Nicht gefunden")
|
||||||
|
return
|
||||||
|
|
||||||
|
stat_info = os.stat(path)
|
||||||
|
name = os.path.basename(path)
|
||||||
|
|
||||||
|
self.name_label.setText(name)
|
||||||
|
self.location_label.setText(os.path.dirname(path))
|
||||||
|
|
||||||
|
if os.path.isdir(path):
|
||||||
|
self.type_label.setText("Ordner")
|
||||||
|
# Ordnerinhalt zählen
|
||||||
|
try:
|
||||||
|
items = list(os.scandir(path))
|
||||||
|
folders = sum(1 for i in items if i.is_dir())
|
||||||
|
files = sum(1 for i in items if i.is_file())
|
||||||
|
self.contains_label.setText(f"{folders} Ordner, {files} Dateien")
|
||||||
|
except PermissionError:
|
||||||
|
self.contains_label.setText("Zugriff verweigert")
|
||||||
|
|
||||||
|
# Ordnergröße berechnen
|
||||||
|
total_size = self._get_folder_size(path)
|
||||||
|
self.size_label.setText(format_file_size(total_size))
|
||||||
|
else:
|
||||||
|
# Dateityp aus Erweiterung
|
||||||
|
ext = Path(name).suffix.lower()
|
||||||
|
type_names = {
|
||||||
|
'.txt': 'Textdatei', '.py': 'Python-Datei', '.js': 'JavaScript',
|
||||||
|
'.html': 'HTML-Datei', '.css': 'CSS-Datei', '.json': 'JSON-Datei',
|
||||||
|
'.md': 'Markdown-Datei', '.pdf': 'PDF-Dokument', '.jpg': 'JPEG-Bild',
|
||||||
|
'.png': 'PNG-Bild', '.gif': 'GIF-Bild', '.mp3': 'MP3-Audio',
|
||||||
|
'.mp4': 'MP4-Video', '.zip': 'ZIP-Archiv', '.tar': 'TAR-Archiv',
|
||||||
|
}
|
||||||
|
self.type_label.setText(type_names.get(ext, f"Datei ({ext})" if ext else "Datei"))
|
||||||
|
self.size_label.setText(format_file_size(stat_info.st_size))
|
||||||
|
self.contains_label.setText("-")
|
||||||
|
|
||||||
|
# Zeitstempel
|
||||||
|
self.created_label.setText(
|
||||||
|
datetime.fromtimestamp(stat_info.st_ctime).strftime("%d.%m.%Y %H:%M:%S")
|
||||||
|
)
|
||||||
|
self.modified_label.setText(
|
||||||
|
datetime.fromtimestamp(stat_info.st_mtime).strftime("%d.%m.%Y %H:%M:%S")
|
||||||
|
)
|
||||||
|
self.accessed_label.setText(
|
||||||
|
datetime.fromtimestamp(stat_info.st_atime).strftime("%d.%m.%Y %H:%M:%S")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Berechtigungen
|
||||||
|
mode = stat_info.st_mode
|
||||||
|
perms = ""
|
||||||
|
perms += "r" if mode & stat.S_IRUSR else "-"
|
||||||
|
perms += "w" if mode & stat.S_IWUSR else "-"
|
||||||
|
perms += "x" if mode & stat.S_IXUSR else "-"
|
||||||
|
perms += "r" if mode & stat.S_IRGRP else "-"
|
||||||
|
perms += "w" if mode & stat.S_IWGRP else "-"
|
||||||
|
perms += "x" if mode & stat.S_IXGRP else "-"
|
||||||
|
perms += "r" if mode & stat.S_IROTH else "-"
|
||||||
|
perms += "w" if mode & stat.S_IWOTH else "-"
|
||||||
|
perms += "x" if mode & stat.S_IXOTH else "-"
|
||||||
|
self.permissions_label.setText(perms)
|
||||||
|
|
||||||
|
# Besitzer
|
||||||
|
try:
|
||||||
|
import pwd
|
||||||
|
import grp
|
||||||
|
owner = pwd.getpwuid(stat_info.st_uid).pw_name
|
||||||
|
group = grp.getgrgid(stat_info.st_gid).gr_name
|
||||||
|
self.owner_label.setText(f"{owner}:{group}")
|
||||||
|
except (KeyError, ImportError):
|
||||||
|
self.owner_label.setText(f"UID {stat_info.st_uid}")
|
||||||
|
|
||||||
|
def _load_multiple_properties(self):
|
||||||
|
"""Lädt Eigenschaften mehrerer Dateien."""
|
||||||
|
self.name_label.setText(f"{len(self.paths)} Elemente ausgewählt")
|
||||||
|
self.location_label.setText(os.path.dirname(self.paths[0]))
|
||||||
|
|
||||||
|
# Typen zählen
|
||||||
|
folders = sum(1 for p in self.paths if os.path.isdir(p))
|
||||||
|
files = len(self.paths) - folders
|
||||||
|
if folders and files:
|
||||||
|
self.type_label.setText(f"{folders} Ordner, {files} Dateien")
|
||||||
|
elif folders:
|
||||||
|
self.type_label.setText(f"{folders} Ordner")
|
||||||
|
else:
|
||||||
|
self.type_label.setText(f"{files} Dateien")
|
||||||
|
|
||||||
|
# Gesamtgröße
|
||||||
|
total_size = 0
|
||||||
|
for path in self.paths:
|
||||||
|
if os.path.isdir(path):
|
||||||
|
total_size += self._get_folder_size(path)
|
||||||
|
elif os.path.isfile(path):
|
||||||
|
total_size += os.path.getsize(path)
|
||||||
|
self.size_label.setText(format_file_size(total_size))
|
||||||
|
|
||||||
|
self.contains_label.setText("-")
|
||||||
|
self.created_label.setText("-")
|
||||||
|
self.modified_label.setText("-")
|
||||||
|
self.accessed_label.setText("-")
|
||||||
|
self.permissions_label.setText("-")
|
||||||
|
self.owner_label.setText("-")
|
||||||
|
|
||||||
|
def _get_folder_size(self, path: str) -> int:
|
||||||
|
"""Berechnet die Größe eines Ordners rekursiv."""
|
||||||
|
total = 0
|
||||||
|
try:
|
||||||
|
for entry in os.scandir(path):
|
||||||
|
try:
|
||||||
|
if entry.is_file(follow_symlinks=False):
|
||||||
|
total += entry.stat().st_size
|
||||||
|
elif entry.is_dir(follow_symlinks=False):
|
||||||
|
total += self._get_folder_size(entry.path)
|
||||||
|
except (PermissionError, OSError):
|
||||||
|
continue
|
||||||
|
except PermissionError:
|
||||||
|
pass
|
||||||
|
return total
|
||||||
|
|
||||||
|
|
||||||
|
class ArchiveDialog(QDialog):
|
||||||
|
"""Dialog zum Erstellen eines Archivs."""
|
||||||
|
|
||||||
|
def __init__(self, source_paths: list[str], parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.source_paths = source_paths
|
||||||
|
self.archive_name = ""
|
||||||
|
self.archive_format = ".zip"
|
||||||
|
|
||||||
|
self.setWindowTitle("Archiv erstellen")
|
||||||
|
self.setModal(True)
|
||||||
|
self.setMinimumWidth(450)
|
||||||
|
|
||||||
|
self._setup_ui()
|
||||||
|
|
||||||
|
def _setup_ui(self):
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
|
||||||
|
# Quelldateien
|
||||||
|
source_group = QGroupBox(f"Zu packende Elemente ({len(self.source_paths)})")
|
||||||
|
source_layout = QVBoxLayout(source_group)
|
||||||
|
|
||||||
|
source_list = QListWidget()
|
||||||
|
source_list.setMaximumHeight(100)
|
||||||
|
for path in self.source_paths[:10]: # Max 10 anzeigen
|
||||||
|
source_list.addItem(os.path.basename(path))
|
||||||
|
if len(self.source_paths) > 10:
|
||||||
|
source_list.addItem(f"... und {len(self.source_paths) - 10} weitere")
|
||||||
|
source_layout.addWidget(source_list)
|
||||||
|
|
||||||
|
layout.addWidget(source_group)
|
||||||
|
|
||||||
|
# Archiv-Name
|
||||||
|
name_group = QGroupBox("Archiv")
|
||||||
|
name_layout = QFormLayout(name_group)
|
||||||
|
|
||||||
|
self.name_input = QLineEdit()
|
||||||
|
# Standard-Name aus erstem Element
|
||||||
|
if len(self.source_paths) == 1:
|
||||||
|
base_name = Path(self.source_paths[0]).stem
|
||||||
|
else:
|
||||||
|
base_name = "archiv"
|
||||||
|
self.name_input.setText(base_name + ".zip")
|
||||||
|
name_layout.addRow("Dateiname:", self.name_input)
|
||||||
|
|
||||||
|
layout.addWidget(name_group)
|
||||||
|
|
||||||
|
# Format-Auswahl
|
||||||
|
format_group = QGroupBox("Format")
|
||||||
|
format_layout = QVBoxLayout(format_group)
|
||||||
|
|
||||||
|
self.format_group = QButtonGroup(self)
|
||||||
|
|
||||||
|
formats = [
|
||||||
|
("ZIP (.zip)", ".zip"),
|
||||||
|
("TAR (.tar)", ".tar"),
|
||||||
|
("TAR.GZ (.tar.gz)", ".tar.gz"),
|
||||||
|
("TAR.BZ2 (.tar.bz2)", ".tar.bz2"),
|
||||||
|
("TAR.XZ (.tar.xz)", ".tar.xz"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for i, (label, ext) in enumerate(formats):
|
||||||
|
radio = QRadioButton(label)
|
||||||
|
radio.setProperty("extension", ext)
|
||||||
|
if i == 0:
|
||||||
|
radio.setChecked(True)
|
||||||
|
radio.toggled.connect(self._on_format_changed)
|
||||||
|
self.format_group.addButton(radio, i)
|
||||||
|
format_layout.addWidget(radio)
|
||||||
|
|
||||||
|
layout.addWidget(format_group)
|
||||||
|
|
||||||
|
# Buttons
|
||||||
|
button_box = QDialogButtonBox(
|
||||||
|
QDialogButtonBox.StandardButton.Ok |
|
||||||
|
QDialogButtonBox.StandardButton.Cancel
|
||||||
|
)
|
||||||
|
button_box.accepted.connect(self.accept)
|
||||||
|
button_box.rejected.connect(self.reject)
|
||||||
|
layout.addWidget(button_box)
|
||||||
|
|
||||||
|
def _on_format_changed(self):
|
||||||
|
"""Aktualisiert die Dateiendung."""
|
||||||
|
button = self.format_group.checkedButton()
|
||||||
|
if button:
|
||||||
|
new_ext = button.property("extension")
|
||||||
|
current_name = self.name_input.text()
|
||||||
|
# Alte Erweiterung entfernen
|
||||||
|
for ext in ['.tar.gz', '.tar.bz2', '.tar.xz', '.tar', '.zip']:
|
||||||
|
if current_name.endswith(ext):
|
||||||
|
current_name = current_name[:-len(ext)]
|
||||||
|
break
|
||||||
|
self.name_input.setText(current_name + new_ext)
|
||||||
|
self.archive_format = new_ext
|
||||||
|
|
||||||
|
def accept(self):
|
||||||
|
"""Validiert die Eingabe."""
|
||||||
|
self.archive_name = self.name_input.text().strip()
|
||||||
|
|
||||||
|
if not self.archive_name:
|
||||||
|
QMessageBox.warning(self, "Fehler", "Archiv-Name darf nicht leer sein.")
|
||||||
|
return
|
||||||
|
|
||||||
|
button = self.format_group.checkedButton()
|
||||||
|
if button:
|
||||||
|
self.archive_format = button.property("extension")
|
||||||
|
|
||||||
|
super().accept()
|
||||||
|
|
||||||
|
def get_archive_name(self) -> str:
|
||||||
|
"""Gibt den Archiv-Namen zurück."""
|
||||||
|
return self.archive_name
|
||||||
|
|
||||||
|
def get_archive_format(self) -> str:
|
||||||
|
"""Gibt das Archiv-Format zurück."""
|
||||||
|
return self.archive_format
|
||||||
|
|
||||||
|
|
||||||
|
class ExtractDialog(QDialog):
|
||||||
|
"""Dialog zum Entpacken eines Archivs."""
|
||||||
|
|
||||||
|
def __init__(self, archive_path: str, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.archive_path = archive_path
|
||||||
|
self.target_folder = os.path.dirname(archive_path)
|
||||||
|
self.create_subfolder = True
|
||||||
|
|
||||||
|
self.setWindowTitle("Archiv entpacken")
|
||||||
|
self.setModal(True)
|
||||||
|
self.setMinimumSize(500, 400)
|
||||||
|
|
||||||
|
self._setup_ui()
|
||||||
|
|
||||||
|
def _setup_ui(self):
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
|
||||||
|
# Archiv-Info
|
||||||
|
info_label = QLabel(f"Archiv: {os.path.basename(self.archive_path)}")
|
||||||
|
info_label.setWordWrap(True)
|
||||||
|
layout.addWidget(info_label)
|
||||||
|
|
||||||
|
# Zielordner
|
||||||
|
layout.addWidget(QLabel("Entpacken nach:"))
|
||||||
|
|
||||||
|
self.model = QFileSystemModel()
|
||||||
|
self.model.setFilter(QDir.Filter.Dirs | QDir.Filter.NoDotAndDotDot)
|
||||||
|
self.model.setRootPath('')
|
||||||
|
|
||||||
|
self.tree = QTreeView()
|
||||||
|
self.tree.setModel(self.model)
|
||||||
|
self.tree.setHeaderHidden(True)
|
||||||
|
|
||||||
|
for i in range(1, self.model.columnCount()):
|
||||||
|
self.tree.hideColumn(i)
|
||||||
|
|
||||||
|
# Aktuellen Ordner expandieren
|
||||||
|
current_dir = os.path.dirname(self.archive_path)
|
||||||
|
index = self.model.index(current_dir)
|
||||||
|
if index.isValid():
|
||||||
|
self.tree.setCurrentIndex(index)
|
||||||
|
self.tree.scrollTo(index)
|
||||||
|
|
||||||
|
layout.addWidget(self.tree)
|
||||||
|
|
||||||
|
self.path_label = QLabel(f"Ziel: {self.target_folder}")
|
||||||
|
self.path_label.setWordWrap(True)
|
||||||
|
layout.addWidget(self.path_label)
|
||||||
|
|
||||||
|
self.tree.clicked.connect(self._on_selection_changed)
|
||||||
|
|
||||||
|
# Optionen
|
||||||
|
self.subfolder_check = QCheckBox("Unterordner erstellen")
|
||||||
|
self.subfolder_check.setChecked(True)
|
||||||
|
self.subfolder_check.setToolTip(
|
||||||
|
"Erstellt einen Ordner mit dem Namen des Archivs und entpackt den Inhalt darin."
|
||||||
|
)
|
||||||
|
layout.addWidget(self.subfolder_check)
|
||||||
|
|
||||||
|
# Buttons
|
||||||
|
button_box = QDialogButtonBox(
|
||||||
|
QDialogButtonBox.StandardButton.Ok |
|
||||||
|
QDialogButtonBox.StandardButton.Cancel
|
||||||
|
)
|
||||||
|
button_box.accepted.connect(self.accept)
|
||||||
|
button_box.rejected.connect(self.reject)
|
||||||
|
layout.addWidget(button_box)
|
||||||
|
|
||||||
|
def _on_selection_changed(self, index):
|
||||||
|
"""Aktualisiert den ausgewählten Pfad."""
|
||||||
|
path = self.model.filePath(index)
|
||||||
|
self.path_label.setText(f"Ziel: {path}")
|
||||||
|
self.target_folder = path
|
||||||
|
|
||||||
|
def accept(self):
|
||||||
|
"""Validiert die Auswahl."""
|
||||||
|
if not self.target_folder or not os.path.isdir(self.target_folder):
|
||||||
|
QMessageBox.warning(self, "Fehler", "Bitte wähle einen gültigen Zielordner.")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.create_subfolder = self.subfolder_check.isChecked()
|
||||||
|
super().accept()
|
||||||
|
|
||||||
|
def get_target_folder(self) -> str:
|
||||||
|
"""Gibt den Zielordner zurück."""
|
||||||
|
return self.target_folder
|
||||||
|
|
||||||
|
def get_create_subfolder(self) -> bool:
|
||||||
|
"""Gibt zurück ob ein Unterordner erstellt werden soll."""
|
||||||
|
return self.create_subfolder
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,12 @@
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from PyQt6.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QSplitter,
|
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QSplitter,
|
||||||
QMenuBar, QMenu, QToolBar, QStatusBar, QLabel, QComboBox,
|
QMenuBar, QMenu, QToolBar, QStatusBar, QLabel, QComboBox,
|
||||||
QMessageBox, QFrame, QApplication
|
QMessageBox, QFrame, QApplication, QInputDialog
|
||||||
)
|
)
|
||||||
from PyQt6.QtCore import Qt, QSettings, QTimer
|
from PyQt6.QtCore import Qt, QSettings, QTimer
|
||||||
from PyQt6.QtGui import QAction, QKeySequence, QShortcut
|
from PyQt6.QtGui import QAction, QKeySequence, QShortcut
|
||||||
|
|
@ -15,9 +16,14 @@ from .widgets.folder_tree import FolderTreeWidget
|
||||||
from .widgets.file_list import FileListWidget
|
from .widgets.file_list import FileListWidget
|
||||||
from .widgets.preview_panel import PreviewPanel
|
from .widgets.preview_panel import PreviewPanel
|
||||||
from .widgets.breadcrumb import BreadcrumbWidget
|
from .widgets.breadcrumb import BreadcrumbWidget
|
||||||
from .dialogs import RenameDialog, MoveDialog, DeleteDialog, SettingsDialog
|
from .dialogs import (
|
||||||
|
RenameDialog, MoveDialog, DeleteDialog, SettingsDialog,
|
||||||
|
NewFileDialog, PropertiesDialog, ArchiveDialog, ExtractDialog
|
||||||
|
)
|
||||||
from .preview_window import PreviewWindow
|
from .preview_window import PreviewWindow
|
||||||
from .utils.themes import ThemeManager
|
from .utils.themes import ThemeManager
|
||||||
|
from .utils.clipboard import FileClipboard
|
||||||
|
from .utils.archive import ArchiveHandler
|
||||||
|
|
||||||
|
|
||||||
class MainWindow(QMainWindow):
|
class MainWindow(QMainWindow):
|
||||||
|
|
@ -29,6 +35,7 @@ class MainWindow(QMainWindow):
|
||||||
self.theme_manager = ThemeManager()
|
self.theme_manager = ThemeManager()
|
||||||
self.preview_window = None
|
self.preview_window = None
|
||||||
self._current_path = ""
|
self._current_path = ""
|
||||||
|
self.clipboard = FileClipboard.instance()
|
||||||
|
|
||||||
self.setWindowTitle("FileBrowser")
|
self.setWindowTitle("FileBrowser")
|
||||||
self.setMinimumSize(800, 600)
|
self.setMinimumSize(800, 600)
|
||||||
|
|
@ -93,14 +100,19 @@ class MainWindow(QMainWindow):
|
||||||
# Datei-Menü
|
# Datei-Menü
|
||||||
file_menu = menubar.addMenu("&Datei")
|
file_menu = menubar.addMenu("&Datei")
|
||||||
|
|
||||||
new_folder_action = QAction("Neuer Ordner", self)
|
new_file_action = QAction("📄 Neue Datei...", self)
|
||||||
|
new_file_action.setShortcut(QKeySequence("Ctrl+N"))
|
||||||
|
new_file_action.triggered.connect(self._create_new_file)
|
||||||
|
file_menu.addAction(new_file_action)
|
||||||
|
|
||||||
|
new_folder_action = QAction("📁 Neuer Ordner...", self)
|
||||||
new_folder_action.setShortcut(QKeySequence("Ctrl+Shift+N"))
|
new_folder_action.setShortcut(QKeySequence("Ctrl+Shift+N"))
|
||||||
new_folder_action.triggered.connect(self._create_new_folder)
|
new_folder_action.triggered.connect(self._create_new_folder)
|
||||||
file_menu.addAction(new_folder_action)
|
file_menu.addAction(new_folder_action)
|
||||||
|
|
||||||
file_menu.addSeparator()
|
file_menu.addSeparator()
|
||||||
|
|
||||||
settings_action = QAction("Einstellungen...", self)
|
settings_action = QAction("⚙️ Einstellungen...", self)
|
||||||
settings_action.setShortcut(QKeySequence("Ctrl+,"))
|
settings_action.setShortcut(QKeySequence("Ctrl+,"))
|
||||||
settings_action.triggered.connect(self._show_settings)
|
settings_action.triggered.connect(self._show_settings)
|
||||||
file_menu.addAction(settings_action)
|
file_menu.addAction(settings_action)
|
||||||
|
|
@ -115,27 +127,76 @@ class MainWindow(QMainWindow):
|
||||||
# Bearbeiten-Menü
|
# Bearbeiten-Menü
|
||||||
edit_menu = menubar.addMenu("&Bearbeiten")
|
edit_menu = menubar.addMenu("&Bearbeiten")
|
||||||
|
|
||||||
rename_action = QAction("Umbenennen", self)
|
cut_action = QAction("✂️ Ausschneiden", self)
|
||||||
|
cut_action.setShortcut(QKeySequence("Ctrl+X"))
|
||||||
|
cut_action.triggered.connect(self._cut_selected)
|
||||||
|
edit_menu.addAction(cut_action)
|
||||||
|
|
||||||
|
copy_action = QAction("📋 Kopieren", self)
|
||||||
|
copy_action.setShortcut(QKeySequence("Ctrl+C"))
|
||||||
|
copy_action.triggered.connect(self._copy_selected)
|
||||||
|
edit_menu.addAction(copy_action)
|
||||||
|
|
||||||
|
paste_action = QAction("📋 Einfügen", self)
|
||||||
|
paste_action.setShortcut(QKeySequence("Ctrl+V"))
|
||||||
|
paste_action.triggered.connect(self._paste_files)
|
||||||
|
edit_menu.addAction(paste_action)
|
||||||
|
|
||||||
|
edit_menu.addSeparator()
|
||||||
|
|
||||||
|
select_all_action = QAction("Alles auswählen", self)
|
||||||
|
select_all_action.setShortcut(QKeySequence("Ctrl+A"))
|
||||||
|
select_all_action.triggered.connect(self.file_list.selectAll)
|
||||||
|
edit_menu.addAction(select_all_action)
|
||||||
|
|
||||||
|
edit_menu.addSeparator()
|
||||||
|
|
||||||
|
rename_action = QAction("✏️ Umbenennen", self)
|
||||||
rename_action.setShortcut(QKeySequence("F2"))
|
rename_action.setShortcut(QKeySequence("F2"))
|
||||||
rename_action.triggered.connect(self._rename_selected)
|
rename_action.triggered.connect(self._rename_selected)
|
||||||
edit_menu.addAction(rename_action)
|
edit_menu.addAction(rename_action)
|
||||||
|
|
||||||
move_action = QAction("Verschieben", self)
|
move_action = QAction("📦 Verschieben nach...", self)
|
||||||
move_action.triggered.connect(self._move_selected)
|
move_action.triggered.connect(self._move_selected)
|
||||||
edit_menu.addAction(move_action)
|
edit_menu.addAction(move_action)
|
||||||
|
|
||||||
delete_action = QAction("Löschen", self)
|
delete_action = QAction("🗑 Löschen", self)
|
||||||
delete_action.setShortcut(QKeySequence.StandardKey.Delete)
|
delete_action.setShortcut(QKeySequence.StandardKey.Delete)
|
||||||
delete_action.triggered.connect(self._delete_selected)
|
delete_action.triggered.connect(self._delete_selected)
|
||||||
edit_menu.addAction(delete_action)
|
edit_menu.addAction(delete_action)
|
||||||
|
|
||||||
edit_menu.addSeparator()
|
edit_menu.addSeparator()
|
||||||
|
|
||||||
refresh_action = QAction("Aktualisieren", self)
|
refresh_action = QAction("🔄 Aktualisieren", self)
|
||||||
refresh_action.setShortcut(QKeySequence("F5"))
|
refresh_action.setShortcut(QKeySequence("F5"))
|
||||||
refresh_action.triggered.connect(self._refresh)
|
refresh_action.triggered.connect(self._refresh)
|
||||||
edit_menu.addAction(refresh_action)
|
edit_menu.addAction(refresh_action)
|
||||||
|
|
||||||
|
# Extras-Menü
|
||||||
|
extras_menu = menubar.addMenu("E&xtras")
|
||||||
|
|
||||||
|
archive_action = QAction("📦 Zu Archiv packen...", self)
|
||||||
|
archive_action.triggered.connect(self._archive_selected)
|
||||||
|
extras_menu.addAction(archive_action)
|
||||||
|
|
||||||
|
extract_action = QAction("📦 Archiv entpacken...", self)
|
||||||
|
extract_action.triggered.connect(self._extract_selected)
|
||||||
|
extras_menu.addAction(extract_action)
|
||||||
|
|
||||||
|
extras_menu.addSeparator()
|
||||||
|
|
||||||
|
terminal_action = QAction("💻 Terminal öffnen", self)
|
||||||
|
terminal_action.setShortcut(QKeySequence("F4"))
|
||||||
|
terminal_action.triggered.connect(lambda: self._open_terminal(self._current_path))
|
||||||
|
extras_menu.addAction(terminal_action)
|
||||||
|
|
||||||
|
extras_menu.addSeparator()
|
||||||
|
|
||||||
|
properties_action = QAction("ℹ️ Eigenschaften", self)
|
||||||
|
properties_action.setShortcut(QKeySequence("Alt+Return"))
|
||||||
|
properties_action.triggered.connect(self._show_properties)
|
||||||
|
extras_menu.addAction(properties_action)
|
||||||
|
|
||||||
# Ansicht-Menü
|
# Ansicht-Menü
|
||||||
view_menu = menubar.addMenu("&Ansicht")
|
view_menu = menubar.addMenu("&Ansicht")
|
||||||
|
|
||||||
|
|
@ -246,6 +307,28 @@ class MainWindow(QMainWindow):
|
||||||
|
|
||||||
toolbar.addSeparator()
|
toolbar.addSeparator()
|
||||||
|
|
||||||
|
# Neue Datei
|
||||||
|
new_file_action = QAction("📄", self)
|
||||||
|
new_file_action.setToolTip("Neue Datei (Ctrl+N)")
|
||||||
|
new_file_action.triggered.connect(self._create_new_file)
|
||||||
|
toolbar.addAction(new_file_action)
|
||||||
|
|
||||||
|
# Neuer Ordner
|
||||||
|
new_folder_action = QAction("📁", self)
|
||||||
|
new_folder_action.setToolTip("Neuer Ordner (Ctrl+Shift+N)")
|
||||||
|
new_folder_action.triggered.connect(self._create_new_folder)
|
||||||
|
toolbar.addAction(new_folder_action)
|
||||||
|
|
||||||
|
toolbar.addSeparator()
|
||||||
|
|
||||||
|
# Terminal
|
||||||
|
terminal_action = QAction("💻", self)
|
||||||
|
terminal_action.setToolTip("Terminal öffnen (F4)")
|
||||||
|
terminal_action.triggered.connect(lambda: self._open_terminal(self._current_path))
|
||||||
|
toolbar.addAction(terminal_action)
|
||||||
|
|
||||||
|
toolbar.addSeparator()
|
||||||
|
|
||||||
# Theme-Auswahl
|
# Theme-Auswahl
|
||||||
toolbar.addWidget(QLabel("Theme: "))
|
toolbar.addWidget(QLabel("Theme: "))
|
||||||
self.theme_combo = QComboBox()
|
self.theme_combo = QComboBox()
|
||||||
|
|
@ -262,21 +345,42 @@ class MainWindow(QMainWindow):
|
||||||
self.path_label = QLabel()
|
self.path_label = QLabel()
|
||||||
self.statusbar.addWidget(self.path_label, 1)
|
self.statusbar.addWidget(self.path_label, 1)
|
||||||
|
|
||||||
|
self.clipboard_label = QLabel()
|
||||||
|
self.statusbar.addPermanentWidget(self.clipboard_label)
|
||||||
|
|
||||||
self.count_label = QLabel()
|
self.count_label = QLabel()
|
||||||
self.statusbar.addPermanentWidget(self.count_label)
|
self.statusbar.addPermanentWidget(self.count_label)
|
||||||
|
|
||||||
|
# Clipboard-Status aktualisieren
|
||||||
|
self.clipboard.clipboard_changed.connect(self._update_clipboard_status)
|
||||||
|
|
||||||
|
def _update_clipboard_status(self):
|
||||||
|
"""Aktualisiert die Zwischenablage-Anzeige."""
|
||||||
|
if self.clipboard.has_files():
|
||||||
|
count = self.clipboard.get_file_count()
|
||||||
|
mode = "Kopiert" if self.clipboard.get_mode().value == "copy" else "Ausgeschnitten"
|
||||||
|
self.clipboard_label.setText(f"📋 {mode}: {count}")
|
||||||
|
else:
|
||||||
|
self.clipboard_label.setText("")
|
||||||
|
|
||||||
def _setup_shortcuts(self):
|
def _setup_shortcuts(self):
|
||||||
"""Richtet Tastenkürzel ein."""
|
"""Richtet Tastenkürzel ein."""
|
||||||
# Backspace für zurück
|
# Backspace für zurück
|
||||||
QShortcut(QKeySequence("Backspace"), self, self._go_back)
|
QShortcut(QKeySequence("Backspace"), self, self._go_back)
|
||||||
|
|
||||||
|
# F4 für Terminal
|
||||||
|
QShortcut(QKeySequence("F4"), self, lambda: self._open_terminal(self._current_path))
|
||||||
|
|
||||||
|
# Alt+Enter für Eigenschaften
|
||||||
|
QShortcut(QKeySequence("Alt+Return"), self, self._show_properties)
|
||||||
|
|
||||||
def _connect_signals(self):
|
def _connect_signals(self):
|
||||||
"""Verbindet alle Signale."""
|
"""Verbindet alle Signale."""
|
||||||
# Ordnerbaum
|
# Ordnerbaum
|
||||||
self.folder_tree.folder_selected.connect(self._navigate_to)
|
self.folder_tree.folder_selected.connect(self._navigate_to)
|
||||||
self.folder_tree.folder_double_clicked.connect(self._navigate_to)
|
self.folder_tree.folder_double_clicked.connect(self._navigate_to)
|
||||||
|
|
||||||
# Dateiliste
|
# Dateiliste - Basis-Signale
|
||||||
self.file_list.file_selected.connect(self._on_file_selected)
|
self.file_list.file_selected.connect(self._on_file_selected)
|
||||||
self.file_list.file_double_clicked.connect(self._open_external)
|
self.file_list.file_double_clicked.connect(self._open_external)
|
||||||
self.file_list.folder_entered.connect(self._navigate_to)
|
self.file_list.folder_entered.connect(self._navigate_to)
|
||||||
|
|
@ -285,6 +389,18 @@ class MainWindow(QMainWindow):
|
||||||
self.file_list.file_move_requested.connect(self._move_file)
|
self.file_list.file_move_requested.connect(self._move_file)
|
||||||
self.file_list.files_dropped.connect(self._handle_files_dropped)
|
self.file_list.files_dropped.connect(self._handle_files_dropped)
|
||||||
|
|
||||||
|
# Dateiliste - Erweiterte Signale
|
||||||
|
self.file_list.new_file_requested.connect(self._create_new_file)
|
||||||
|
self.file_list.new_folder_requested.connect(self._create_new_folder)
|
||||||
|
self.file_list.copy_requested.connect(self._copy_files)
|
||||||
|
self.file_list.cut_requested.connect(self._cut_files)
|
||||||
|
self.file_list.paste_requested.connect(self._paste_files)
|
||||||
|
self.file_list.archive_requested.connect(self._create_archive)
|
||||||
|
self.file_list.extract_requested.connect(self._extract_archive)
|
||||||
|
self.file_list.properties_requested.connect(self._show_properties_for)
|
||||||
|
self.file_list.open_terminal_requested.connect(self._open_terminal)
|
||||||
|
self.file_list.refresh_requested.connect(self._refresh)
|
||||||
|
|
||||||
# Breadcrumb
|
# Breadcrumb
|
||||||
self.breadcrumb.path_clicked.connect(self._navigate_to)
|
self.breadcrumb.path_clicked.connect(self._navigate_to)
|
||||||
self.breadcrumb.path_entered.connect(self._navigate_to)
|
self.breadcrumb.path_entered.connect(self._navigate_to)
|
||||||
|
|
@ -340,15 +456,8 @@ class MainWindow(QMainWindow):
|
||||||
stylesheet = self.theme_manager.apply_theme(theme_name)
|
stylesheet = self.theme_manager.apply_theme(theme_name)
|
||||||
self.setStyleSheet(stylesheet)
|
self.setStyleSheet(stylesheet)
|
||||||
|
|
||||||
# Hover-Farbe für Dateiliste setzen
|
|
||||||
theme = self.theme_manager.THEMES.get(theme_name, self.theme_manager.THEMES['dark'])
|
|
||||||
if theme.get('is_system'):
|
|
||||||
# System-Theme: Standard-Hover-Farbe
|
|
||||||
self.file_list.set_hover_color('#e0e0e0')
|
|
||||||
else:
|
|
||||||
self.file_list.set_hover_color(theme['alternate_base'])
|
|
||||||
|
|
||||||
# Theme-Menü-Aktionen aktualisieren
|
# Theme-Menü-Aktionen aktualisieren
|
||||||
|
theme = self.theme_manager.THEMES.get(theme_name, self.theme_manager.THEMES['dark'])
|
||||||
for action in self.theme_actions:
|
for action in self.theme_actions:
|
||||||
action.setChecked(action.data() == theme)
|
action.setChecked(action.data() == theme)
|
||||||
|
|
||||||
|
|
@ -418,7 +527,6 @@ class MainWindow(QMainWindow):
|
||||||
|
|
||||||
def _open_external(self, path: str):
|
def _open_external(self, path: str):
|
||||||
"""Öffnet eine Datei extern."""
|
"""Öffnet eine Datei extern."""
|
||||||
import subprocess
|
|
||||||
try:
|
try:
|
||||||
subprocess.Popen(['xdg-open', path])
|
subprocess.Popen(['xdg-open', path])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -442,6 +550,49 @@ class MainWindow(QMainWindow):
|
||||||
self._update_count()
|
self._update_count()
|
||||||
self.statusbar.showMessage("Aktualisiert", 2000)
|
self.statusbar.showMessage("Aktualisiert", 2000)
|
||||||
|
|
||||||
|
# ==================== Kopieren/Ausschneiden/Einfügen ====================
|
||||||
|
|
||||||
|
def _copy_selected(self):
|
||||||
|
"""Kopiert die ausgewählten Elemente."""
|
||||||
|
paths = self.file_list.get_selected_paths()
|
||||||
|
if paths:
|
||||||
|
self._copy_files(paths)
|
||||||
|
|
||||||
|
def _cut_selected(self):
|
||||||
|
"""Schneidet die ausgewählten Elemente aus."""
|
||||||
|
paths = self.file_list.get_selected_paths()
|
||||||
|
if paths:
|
||||||
|
self._cut_files(paths)
|
||||||
|
|
||||||
|
def _copy_files(self, paths: list):
|
||||||
|
"""Kopiert Dateien in die Zwischenablage."""
|
||||||
|
self.clipboard.copy(paths)
|
||||||
|
self.statusbar.showMessage(f"{len(paths)} Element(e) kopiert", 2000)
|
||||||
|
|
||||||
|
def _cut_files(self, paths: list):
|
||||||
|
"""Schneidet Dateien aus."""
|
||||||
|
self.clipboard.cut(paths)
|
||||||
|
self.statusbar.showMessage(f"{len(paths)} Element(e) ausgeschnitten", 2000)
|
||||||
|
|
||||||
|
def _paste_files(self):
|
||||||
|
"""Fügt Dateien aus der Zwischenablage ein."""
|
||||||
|
if not self.clipboard.has_files():
|
||||||
|
return
|
||||||
|
|
||||||
|
count, errors = self.clipboard.paste(self._current_path)
|
||||||
|
self._refresh()
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
QMessageBox.warning(
|
||||||
|
self, "Fehler beim Einfügen",
|
||||||
|
"Einige Dateien konnten nicht eingefügt werden:\n\n" +
|
||||||
|
"\n".join(errors[:10])
|
||||||
|
)
|
||||||
|
elif count > 0:
|
||||||
|
self.statusbar.showMessage(f"{count} Element(e) eingefügt", 3000)
|
||||||
|
|
||||||
|
# ==================== Umbenennen/Verschieben/Löschen ====================
|
||||||
|
|
||||||
def _rename_selected(self):
|
def _rename_selected(self):
|
||||||
"""Benennt das ausgewählte Element um."""
|
"""Benennt das ausgewählte Element um."""
|
||||||
path = self.file_list.get_selected_path()
|
path = self.file_list.get_selected_path()
|
||||||
|
|
@ -548,10 +699,26 @@ class MainWindow(QMainWindow):
|
||||||
3000
|
3000
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ==================== Neue Dateien/Ordner ====================
|
||||||
|
|
||||||
|
def _create_new_file(self):
|
||||||
|
"""Erstellt eine neue Datei."""
|
||||||
|
dialog = NewFileDialog(self._current_path, self)
|
||||||
|
if dialog.exec():
|
||||||
|
file_name = dialog.get_file_name()
|
||||||
|
file_path = os.path.join(self._current_path, file_name)
|
||||||
|
content = dialog.get_template_content()
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(file_path, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(content)
|
||||||
|
self._refresh()
|
||||||
|
self.statusbar.showMessage(f"Datei erstellt: {file_name}", 3000)
|
||||||
|
except Exception as e:
|
||||||
|
QMessageBox.critical(self, "Fehler", f"Fehler beim Erstellen: {e}")
|
||||||
|
|
||||||
def _create_new_folder(self):
|
def _create_new_folder(self):
|
||||||
"""Erstellt einen neuen Ordner."""
|
"""Erstellt einen neuen Ordner."""
|
||||||
from PyQt6.QtWidgets import QInputDialog
|
|
||||||
|
|
||||||
name, ok = QInputDialog.getText(
|
name, ok = QInputDialog.getText(
|
||||||
self, "Neuer Ordner", "Ordnername:"
|
self, "Neuer Ordner", "Ordnername:"
|
||||||
)
|
)
|
||||||
|
|
@ -564,6 +731,91 @@ class MainWindow(QMainWindow):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
QMessageBox.critical(self, "Fehler", f"Fehler beim Erstellen: {e}")
|
QMessageBox.critical(self, "Fehler", f"Fehler beim Erstellen: {e}")
|
||||||
|
|
||||||
|
# ==================== Archiv-Funktionen ====================
|
||||||
|
|
||||||
|
def _archive_selected(self):
|
||||||
|
"""Packt die ausgewählten Elemente."""
|
||||||
|
paths = self.file_list.get_selected_paths()
|
||||||
|
if paths:
|
||||||
|
self._create_archive(paths)
|
||||||
|
|
||||||
|
def _extract_selected(self):
|
||||||
|
"""Entpackt das ausgewählte Archiv."""
|
||||||
|
path = self.file_list.get_selected_path()
|
||||||
|
if path and ArchiveHandler.is_archive(path):
|
||||||
|
self._extract_archive(path)
|
||||||
|
|
||||||
|
def _create_archive(self, paths: list):
|
||||||
|
"""Erstellt ein Archiv."""
|
||||||
|
dialog = ArchiveDialog(paths, self)
|
||||||
|
if dialog.exec():
|
||||||
|
archive_name = dialog.get_archive_name()
|
||||||
|
archive_format = dialog.get_archive_format()
|
||||||
|
archive_path = os.path.join(self._current_path, archive_name)
|
||||||
|
|
||||||
|
success, message = ArchiveHandler.create(paths, archive_path, archive_format)
|
||||||
|
self._refresh()
|
||||||
|
|
||||||
|
if success:
|
||||||
|
self.statusbar.showMessage(f"Archiv erstellt: {archive_name}", 3000)
|
||||||
|
else:
|
||||||
|
QMessageBox.critical(self, "Fehler", message)
|
||||||
|
|
||||||
|
def _extract_archive(self, path: str):
|
||||||
|
"""Entpackt ein Archiv."""
|
||||||
|
dialog = ExtractDialog(path, self)
|
||||||
|
if dialog.exec():
|
||||||
|
target_folder = dialog.get_target_folder()
|
||||||
|
create_subfolder = dialog.get_create_subfolder()
|
||||||
|
|
||||||
|
success, message = ArchiveHandler.extract(path, target_folder, create_subfolder)
|
||||||
|
self._refresh()
|
||||||
|
|
||||||
|
if success:
|
||||||
|
self.statusbar.showMessage("Archiv entpackt", 3000)
|
||||||
|
else:
|
||||||
|
QMessageBox.critical(self, "Fehler", message)
|
||||||
|
|
||||||
|
# ==================== Terminal/Eigenschaften ====================
|
||||||
|
|
||||||
|
def _open_terminal(self, path: str):
|
||||||
|
"""Öffnet ein Terminal im angegebenen Ordner."""
|
||||||
|
terminals = [
|
||||||
|
['konsole', '--workdir', path],
|
||||||
|
['gnome-terminal', '--working-directory', path],
|
||||||
|
['xfce4-terminal', '--working-directory', path],
|
||||||
|
['alacritty', '--working-directory', path],
|
||||||
|
['kitty', '--directory', path],
|
||||||
|
['xterm', '-e', f'cd "{path}" && $SHELL'],
|
||||||
|
]
|
||||||
|
|
||||||
|
for terminal in terminals:
|
||||||
|
try:
|
||||||
|
subprocess.Popen(terminal)
|
||||||
|
return
|
||||||
|
except FileNotFoundError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
QMessageBox.warning(
|
||||||
|
self, "Terminal nicht gefunden",
|
||||||
|
"Es konnte kein Terminal-Emulator gefunden werden."
|
||||||
|
)
|
||||||
|
|
||||||
|
def _show_properties(self):
|
||||||
|
"""Zeigt Eigenschaften für die ausgewählten Elemente."""
|
||||||
|
paths = self.file_list.get_selected_paths()
|
||||||
|
if paths:
|
||||||
|
self._show_properties_for(paths)
|
||||||
|
else:
|
||||||
|
self._show_properties_for([self._current_path])
|
||||||
|
|
||||||
|
def _show_properties_for(self, paths: list):
|
||||||
|
"""Zeigt den Eigenschaften-Dialog."""
|
||||||
|
dialog = PropertiesDialog(paths, self)
|
||||||
|
dialog.exec()
|
||||||
|
|
||||||
|
# ==================== Preview ====================
|
||||||
|
|
||||||
def _detach_preview(self):
|
def _detach_preview(self):
|
||||||
"""Öffnet die Vorschau in einem separaten Fenster."""
|
"""Öffnet die Vorschau in einem separaten Fenster."""
|
||||||
if not self.preview_window:
|
if not self.preview_window:
|
||||||
|
|
@ -616,6 +868,8 @@ class MainWindow(QMainWindow):
|
||||||
for action in self.pdf_page_actions:
|
for action in self.pdf_page_actions:
|
||||||
action.setChecked(action.data() == settings['page_mode'])
|
action.setChecked(action.data() == settings['page_mode'])
|
||||||
|
|
||||||
|
# ==================== Einstellungen ====================
|
||||||
|
|
||||||
def _show_settings(self):
|
def _show_settings(self):
|
||||||
"""Zeigt den Einstellungsdialog."""
|
"""Zeigt den Einstellungsdialog."""
|
||||||
dialog = SettingsDialog(self)
|
dialog = SettingsDialog(self)
|
||||||
|
|
@ -651,6 +905,12 @@ class MainWindow(QMainWindow):
|
||||||
"Über FileBrowser",
|
"Über FileBrowser",
|
||||||
"FileBrowser\n\n"
|
"FileBrowser\n\n"
|
||||||
"Ein einfacher Dateimanager mit Vorschau-Funktion.\n\n"
|
"Ein einfacher Dateimanager mit Vorschau-Funktion.\n\n"
|
||||||
|
"Funktionen:\n"
|
||||||
|
"• Datei-Vorschau (PDF, Bilder, Markdown, Text)\n"
|
||||||
|
"• Kopieren, Ausschneiden, Einfügen\n"
|
||||||
|
"• Archive packen und entpacken\n"
|
||||||
|
"• Drag & Drop\n"
|
||||||
|
"• Mehrere Themes\n\n"
|
||||||
"Erstellt mit PyQt6"
|
"Erstellt mit PyQt6"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
260
src/utils/archive.py
Normal file
260
src/utils/archive.py
Normal file
|
|
@ -0,0 +1,260 @@
|
||||||
|
"""Archive-Handler für Packen und Entpacken."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import zipfile
|
||||||
|
import tarfile
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
class ArchiveHandler:
|
||||||
|
"""Verarbeitet Archiv-Operationen."""
|
||||||
|
|
||||||
|
SUPPORTED_EXTENSIONS = {
|
||||||
|
'.zip': 'zip',
|
||||||
|
'.tar': 'tar',
|
||||||
|
'.tar.gz': 'tar.gz',
|
||||||
|
'.tgz': 'tar.gz',
|
||||||
|
'.tar.bz2': 'tar.bz2',
|
||||||
|
'.tbz2': 'tar.bz2',
|
||||||
|
'.tar.xz': 'tar.xz',
|
||||||
|
'.txz': 'tar.xz',
|
||||||
|
'.gz': 'gz',
|
||||||
|
'.bz2': 'bz2',
|
||||||
|
'.xz': 'xz',
|
||||||
|
'.7z': '7z',
|
||||||
|
'.rar': 'rar',
|
||||||
|
}
|
||||||
|
|
||||||
|
ARCHIVE_FORMATS = [
|
||||||
|
('ZIP-Archiv', '.zip'),
|
||||||
|
('TAR-Archiv', '.tar'),
|
||||||
|
('TAR.GZ-Archiv', '.tar.gz'),
|
||||||
|
('TAR.BZ2-Archiv', '.tar.bz2'),
|
||||||
|
('TAR.XZ-Archiv', '.tar.xz'),
|
||||||
|
]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_archive(cls, path: str) -> bool:
|
||||||
|
"""Prüft ob eine Datei ein Archiv ist."""
|
||||||
|
lower_path = path.lower()
|
||||||
|
for ext in cls.SUPPORTED_EXTENSIONS:
|
||||||
|
if lower_path.endswith(ext):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_archive_type(cls, path: str) -> Optional[str]:
|
||||||
|
"""Gibt den Archiv-Typ zurück."""
|
||||||
|
lower_path = path.lower()
|
||||||
|
# Längere Erweiterungen zuerst prüfen
|
||||||
|
for ext in sorted(cls.SUPPORTED_EXTENSIONS.keys(), key=len, reverse=True):
|
||||||
|
if lower_path.endswith(ext):
|
||||||
|
return cls.SUPPORTED_EXTENSIONS[ext]
|
||||||
|
return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def extract(cls, archive_path: str, target_folder: str,
|
||||||
|
create_subfolder: bool = True) -> tuple[bool, str]:
|
||||||
|
"""
|
||||||
|
Entpackt ein Archiv.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
archive_path: Pfad zum Archiv
|
||||||
|
target_folder: Zielordner
|
||||||
|
create_subfolder: Unterordner mit Archiv-Namen erstellen
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple (Erfolg, Nachricht/Fehlermeldung)
|
||||||
|
"""
|
||||||
|
if not os.path.exists(archive_path):
|
||||||
|
return False, "Archiv nicht gefunden"
|
||||||
|
|
||||||
|
archive_type = cls.get_archive_type(archive_path)
|
||||||
|
if not archive_type:
|
||||||
|
return False, "Nicht unterstütztes Archiv-Format"
|
||||||
|
|
||||||
|
# Zielordner erstellen
|
||||||
|
if create_subfolder:
|
||||||
|
# Archiv-Name ohne Erweiterung
|
||||||
|
base_name = Path(archive_path).stem
|
||||||
|
# Bei .tar.gz etc. den .tar auch entfernen
|
||||||
|
if base_name.endswith('.tar'):
|
||||||
|
base_name = base_name[:-4]
|
||||||
|
target_folder = os.path.join(target_folder, base_name)
|
||||||
|
|
||||||
|
os.makedirs(target_folder, exist_ok=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if archive_type == 'zip':
|
||||||
|
with zipfile.ZipFile(archive_path, 'r') as zf:
|
||||||
|
zf.extractall(target_folder)
|
||||||
|
|
||||||
|
elif archive_type.startswith('tar') or archive_type == 'tar':
|
||||||
|
mode = 'r'
|
||||||
|
if archive_type == 'tar.gz':
|
||||||
|
mode = 'r:gz'
|
||||||
|
elif archive_type == 'tar.bz2':
|
||||||
|
mode = 'r:bz2'
|
||||||
|
elif archive_type == 'tar.xz':
|
||||||
|
mode = 'r:xz'
|
||||||
|
|
||||||
|
with tarfile.open(archive_path, mode) as tf:
|
||||||
|
tf.extractall(target_folder)
|
||||||
|
|
||||||
|
elif archive_type in ('7z', 'rar'):
|
||||||
|
# Versuche externe Tools
|
||||||
|
return cls._extract_with_external_tool(archive_path, target_folder, archive_type)
|
||||||
|
|
||||||
|
else:
|
||||||
|
return False, f"Entpacken von {archive_type} nicht unterstützt"
|
||||||
|
|
||||||
|
return True, f"Erfolgreich entpackt nach: {target_folder}"
|
||||||
|
|
||||||
|
except zipfile.BadZipFile:
|
||||||
|
return False, "Ungültige ZIP-Datei"
|
||||||
|
except tarfile.TarError as e:
|
||||||
|
return False, f"TAR-Fehler: {str(e)}"
|
||||||
|
except PermissionError:
|
||||||
|
return False, "Keine Berechtigung zum Entpacken"
|
||||||
|
except Exception as e:
|
||||||
|
return False, f"Fehler beim Entpacken: {str(e)}"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _extract_with_external_tool(cls, archive_path: str, target_folder: str,
|
||||||
|
archive_type: str) -> tuple[bool, str]:
|
||||||
|
"""Entpackt mit externen Tools (7z, unrar)."""
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
try:
|
||||||
|
if archive_type == '7z':
|
||||||
|
result = subprocess.run(
|
||||||
|
['7z', 'x', archive_path, f'-o{target_folder}', '-y'],
|
||||||
|
capture_output=True, text=True
|
||||||
|
)
|
||||||
|
elif archive_type == 'rar':
|
||||||
|
result = subprocess.run(
|
||||||
|
['unrar', 'x', archive_path, target_folder],
|
||||||
|
capture_output=True, text=True
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return False, f"Kein externes Tool für {archive_type}"
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
return True, f"Erfolgreich entpackt nach: {target_folder}"
|
||||||
|
else:
|
||||||
|
return False, result.stderr or "Unbekannter Fehler"
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
tool = '7z' if archive_type == '7z' else 'unrar'
|
||||||
|
return False, f"{tool} nicht installiert. Bitte installieren für {archive_type}-Unterstützung."
|
||||||
|
except Exception as e:
|
||||||
|
return False, str(e)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(cls, source_paths: list[str], archive_path: str,
|
||||||
|
archive_format: str = '.zip') -> tuple[bool, str]:
|
||||||
|
"""
|
||||||
|
Erstellt ein Archiv.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
source_paths: Liste der zu packenden Dateien/Ordner
|
||||||
|
archive_path: Zielpfad für das Archiv
|
||||||
|
archive_format: Format (.zip, .tar, .tar.gz, .tar.bz2, .tar.xz)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple (Erfolg, Nachricht/Fehlermeldung)
|
||||||
|
"""
|
||||||
|
if not source_paths:
|
||||||
|
return False, "Keine Dateien zum Packen angegeben"
|
||||||
|
|
||||||
|
# Prüfen ob alle Quellen existieren
|
||||||
|
for path in source_paths:
|
||||||
|
if not os.path.exists(path):
|
||||||
|
return False, f"Nicht gefunden: {os.path.basename(path)}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
if archive_format == '.zip':
|
||||||
|
return cls._create_zip(source_paths, archive_path)
|
||||||
|
elif archive_format.startswith('.tar'):
|
||||||
|
return cls._create_tar(source_paths, archive_path, archive_format)
|
||||||
|
else:
|
||||||
|
return False, f"Nicht unterstütztes Format: {archive_format}"
|
||||||
|
|
||||||
|
except PermissionError:
|
||||||
|
return False, "Keine Berechtigung zum Erstellen des Archivs"
|
||||||
|
except Exception as e:
|
||||||
|
return False, f"Fehler beim Erstellen: {str(e)}"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _create_zip(cls, source_paths: list[str], archive_path: str) -> tuple[bool, str]:
|
||||||
|
"""Erstellt ein ZIP-Archiv."""
|
||||||
|
with zipfile.ZipFile(archive_path, 'w', zipfile.ZIP_DEFLATED) as zf:
|
||||||
|
for source_path in source_paths:
|
||||||
|
if os.path.isfile(source_path):
|
||||||
|
zf.write(source_path, os.path.basename(source_path))
|
||||||
|
elif os.path.isdir(source_path):
|
||||||
|
base_name = os.path.basename(source_path)
|
||||||
|
for root, dirs, files in os.walk(source_path):
|
||||||
|
# Relativer Pfad im Archiv
|
||||||
|
rel_root = os.path.relpath(root, os.path.dirname(source_path))
|
||||||
|
for file in files:
|
||||||
|
file_path = os.path.join(root, file)
|
||||||
|
arc_name = os.path.join(rel_root, file)
|
||||||
|
zf.write(file_path, arc_name)
|
||||||
|
|
||||||
|
return True, f"ZIP-Archiv erstellt: {archive_path}"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _create_tar(cls, source_paths: list[str], archive_path: str,
|
||||||
|
archive_format: str) -> tuple[bool, str]:
|
||||||
|
"""Erstellt ein TAR-Archiv."""
|
||||||
|
mode = 'w'
|
||||||
|
if archive_format == '.tar.gz':
|
||||||
|
mode = 'w:gz'
|
||||||
|
elif archive_format == '.tar.bz2':
|
||||||
|
mode = 'w:bz2'
|
||||||
|
elif archive_format == '.tar.xz':
|
||||||
|
mode = 'w:xz'
|
||||||
|
|
||||||
|
with tarfile.open(archive_path, mode) as tf:
|
||||||
|
for source_path in source_paths:
|
||||||
|
tf.add(source_path, arcname=os.path.basename(source_path))
|
||||||
|
|
||||||
|
return True, f"TAR-Archiv erstellt: {archive_path}"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def list_contents(cls, archive_path: str) -> tuple[bool, list[str]]:
|
||||||
|
"""
|
||||||
|
Listet den Inhalt eines Archivs auf.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple (Erfolg, Liste der Dateien oder Fehlermeldung)
|
||||||
|
"""
|
||||||
|
archive_type = cls.get_archive_type(archive_path)
|
||||||
|
if not archive_type:
|
||||||
|
return False, ["Nicht unterstütztes Archiv-Format"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
if archive_type == 'zip':
|
||||||
|
with zipfile.ZipFile(archive_path, 'r') as zf:
|
||||||
|
return True, zf.namelist()
|
||||||
|
|
||||||
|
elif archive_type.startswith('tar') or archive_type == 'tar':
|
||||||
|
mode = 'r'
|
||||||
|
if archive_type == 'tar.gz':
|
||||||
|
mode = 'r:gz'
|
||||||
|
elif archive_type == 'tar.bz2':
|
||||||
|
mode = 'r:bz2'
|
||||||
|
elif archive_type == 'tar.xz':
|
||||||
|
mode = 'r:xz'
|
||||||
|
|
||||||
|
with tarfile.open(archive_path, mode) as tf:
|
||||||
|
return True, tf.getnames()
|
||||||
|
|
||||||
|
else:
|
||||||
|
return False, [f"Auflisten für {archive_type} nicht unterstützt"]
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return False, [str(e)]
|
||||||
136
src/utils/clipboard.py
Normal file
136
src/utils/clipboard.py
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
"""Clipboard-Manager für Dateioperationen."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Optional
|
||||||
|
from PyQt6.QtCore import QObject, pyqtSignal
|
||||||
|
|
||||||
|
|
||||||
|
class ClipboardMode(Enum):
|
||||||
|
"""Modus für Zwischenablage-Operationen."""
|
||||||
|
COPY = "copy"
|
||||||
|
CUT = "cut"
|
||||||
|
|
||||||
|
|
||||||
|
class FileClipboard(QObject):
|
||||||
|
"""Singleton-Clipboard für Dateioperationen."""
|
||||||
|
|
||||||
|
_instance: Optional['FileClipboard'] = None
|
||||||
|
|
||||||
|
# Signale
|
||||||
|
clipboard_changed = pyqtSignal()
|
||||||
|
|
||||||
|
def __new__(cls):
|
||||||
|
if cls._instance is None:
|
||||||
|
cls._instance = super().__new__(cls)
|
||||||
|
cls._instance._initialized = False
|
||||||
|
return cls._instance
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
if self._initialized:
|
||||||
|
return
|
||||||
|
super().__init__()
|
||||||
|
self._files: list[str] = []
|
||||||
|
self._mode: ClipboardMode = ClipboardMode.COPY
|
||||||
|
self._initialized = True
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def instance(cls) -> 'FileClipboard':
|
||||||
|
"""Gibt die Singleton-Instanz zurück."""
|
||||||
|
if cls._instance is None:
|
||||||
|
cls._instance = FileClipboard()
|
||||||
|
return cls._instance
|
||||||
|
|
||||||
|
def copy(self, paths: list[str]):
|
||||||
|
"""Kopiert Dateien in die Zwischenablage."""
|
||||||
|
self._files = [p for p in paths if os.path.exists(p)]
|
||||||
|
self._mode = ClipboardMode.COPY
|
||||||
|
self.clipboard_changed.emit()
|
||||||
|
|
||||||
|
def cut(self, paths: list[str]):
|
||||||
|
"""Schneidet Dateien aus (zum Verschieben)."""
|
||||||
|
self._files = [p for p in paths if os.path.exists(p)]
|
||||||
|
self._mode = ClipboardMode.CUT
|
||||||
|
self.clipboard_changed.emit()
|
||||||
|
|
||||||
|
def paste(self, target_folder: str) -> tuple[int, list[str]]:
|
||||||
|
"""
|
||||||
|
Fügt Dateien im Zielordner ein.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple (Anzahl erfolgreicher Operationen, Liste der Fehler)
|
||||||
|
"""
|
||||||
|
if not self._files or not os.path.isdir(target_folder):
|
||||||
|
return 0, ["Keine Dateien zum Einfügen oder ungültiger Zielordner"]
|
||||||
|
|
||||||
|
success_count = 0
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
for source_path in self._files:
|
||||||
|
if not os.path.exists(source_path):
|
||||||
|
errors.append(f"{os.path.basename(source_path)}: Datei nicht gefunden")
|
||||||
|
continue
|
||||||
|
|
||||||
|
name = os.path.basename(source_path)
|
||||||
|
target_path = os.path.join(target_folder, name)
|
||||||
|
|
||||||
|
# Bei Namenskonflikt umbenennen
|
||||||
|
target_path = self._get_unique_path(target_path)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if self._mode == ClipboardMode.COPY:
|
||||||
|
if os.path.isdir(source_path):
|
||||||
|
shutil.copytree(source_path, target_path)
|
||||||
|
else:
|
||||||
|
shutil.copy2(source_path, target_path)
|
||||||
|
else: # CUT
|
||||||
|
shutil.move(source_path, target_path)
|
||||||
|
success_count += 1
|
||||||
|
except PermissionError:
|
||||||
|
errors.append(f"{name}: Keine Berechtigung")
|
||||||
|
except shutil.Error as e:
|
||||||
|
errors.append(f"{name}: {str(e)}")
|
||||||
|
except Exception as e:
|
||||||
|
errors.append(f"{name}: {str(e)}")
|
||||||
|
|
||||||
|
# Bei Ausschneiden die Zwischenablage leeren
|
||||||
|
if self._mode == ClipboardMode.CUT:
|
||||||
|
self.clear()
|
||||||
|
|
||||||
|
return success_count, errors
|
||||||
|
|
||||||
|
def _get_unique_path(self, path: str) -> str:
|
||||||
|
"""Generiert einen eindeutigen Pfad bei Namenskonflikten."""
|
||||||
|
if not os.path.exists(path):
|
||||||
|
return path
|
||||||
|
|
||||||
|
base, ext = os.path.splitext(path)
|
||||||
|
counter = 1
|
||||||
|
|
||||||
|
while os.path.exists(path):
|
||||||
|
path = f"{base} ({counter}){ext}"
|
||||||
|
counter += 1
|
||||||
|
|
||||||
|
return path
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
"""Leert die Zwischenablage."""
|
||||||
|
self._files = []
|
||||||
|
self.clipboard_changed.emit()
|
||||||
|
|
||||||
|
def has_files(self) -> bool:
|
||||||
|
"""Prüft ob Dateien in der Zwischenablage sind."""
|
||||||
|
return len(self._files) > 0
|
||||||
|
|
||||||
|
def get_files(self) -> list[str]:
|
||||||
|
"""Gibt die Dateien in der Zwischenablage zurück."""
|
||||||
|
return self._files.copy()
|
||||||
|
|
||||||
|
def get_mode(self) -> ClipboardMode:
|
||||||
|
"""Gibt den aktuellen Modus zurück."""
|
||||||
|
return self._mode
|
||||||
|
|
||||||
|
def get_file_count(self) -> int:
|
||||||
|
"""Gibt die Anzahl der Dateien zurück."""
|
||||||
|
return len(self._files)
|
||||||
|
|
@ -140,11 +140,11 @@ class ThemeManager:
|
||||||
color: {theme['highlight_text']};
|
color: {theme['highlight_text']};
|
||||||
}}
|
}}
|
||||||
|
|
||||||
QTreeView::item:hover, QListView::item:hover {{
|
QTreeView::item:hover, QListView::item:hover, QTableView::item:hover {{
|
||||||
background-color: {theme['alternate_base']};
|
background-color: {theme['alternate_base']};
|
||||||
}}
|
}}
|
||||||
|
|
||||||
QTreeView::item:selected:hover, QListView::item:selected:hover {{
|
QTreeView::item:selected:hover, QListView::item:selected:hover, QTableView::item:selected:hover {{
|
||||||
background-color: {theme['highlight']};
|
background-color: {theme['highlight']};
|
||||||
color: {theme['highlight_text']};
|
color: {theme['highlight_text']};
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
"""Dateiliste-Widget mit natürlicher Sortierung und Drag & Drop."""
|
"""Dateiliste-Widget mit natürlicher Sortierung und Drag & Drop."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from PyQt6.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QTableView, QAbstractItemView, QMenu, QHeaderView
|
QTableView, QAbstractItemView, QMenu, QHeaderView, QMessageBox
|
||||||
)
|
)
|
||||||
from PyQt6.QtCore import (
|
from PyQt6.QtCore import (
|
||||||
Qt, QModelIndex, pyqtSignal, QAbstractTableModel, QMimeData,
|
Qt, QModelIndex, pyqtSignal, QAbstractTableModel, QMimeData,
|
||||||
|
|
@ -13,6 +14,8 @@ from PyQt6.QtCore import (
|
||||||
from PyQt6.QtGui import QAction, QDrag, QColor, QBrush
|
from PyQt6.QtGui import QAction, QDrag, QColor, QBrush
|
||||||
|
|
||||||
from ..utils.file_utils import get_file_icon, format_file_size, natural_sort_key
|
from ..utils.file_utils import get_file_icon, format_file_size, natural_sort_key
|
||||||
|
from ..utils.clipboard import FileClipboard
|
||||||
|
from ..utils.archive import ArchiveHandler
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -73,9 +76,6 @@ class FileListModel(QAbstractTableModel):
|
||||||
return Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
|
return Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
|
||||||
elif role == Qt.ItemDataRole.ToolTipRole:
|
elif role == Qt.ItemDataRole.ToolTipRole:
|
||||||
return item.path
|
return item.path
|
||||||
elif role == Qt.ItemDataRole.BackgroundRole:
|
|
||||||
if index.row() == self._hovered_row:
|
|
||||||
return QBrush(self._hover_color)
|
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
@ -171,11 +171,24 @@ class FileListWidget(QTableView):
|
||||||
file_move_requested = pyqtSignal(str) # path
|
file_move_requested = pyqtSignal(str) # path
|
||||||
files_dropped = pyqtSignal(list, str) # source_paths, target_folder
|
files_dropped = pyqtSignal(list, str) # source_paths, target_folder
|
||||||
|
|
||||||
|
# Neue Signale für erweiterte Funktionen
|
||||||
|
new_file_requested = pyqtSignal()
|
||||||
|
new_folder_requested = pyqtSignal()
|
||||||
|
copy_requested = pyqtSignal(list) # paths
|
||||||
|
cut_requested = pyqtSignal(list) # paths
|
||||||
|
paste_requested = pyqtSignal()
|
||||||
|
archive_requested = pyqtSignal(list) # paths
|
||||||
|
extract_requested = pyqtSignal(str) # path
|
||||||
|
properties_requested = pyqtSignal(list) # paths
|
||||||
|
open_terminal_requested = pyqtSignal(str) # path
|
||||||
|
refresh_requested = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
self.list_model = FileListModel(self)
|
self.list_model = FileListModel(self)
|
||||||
self.setModel(self.list_model)
|
self.setModel(self.list_model)
|
||||||
|
self.clipboard = FileClipboard.instance()
|
||||||
|
|
||||||
# Einstellungen
|
# Einstellungen
|
||||||
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
||||||
|
|
@ -232,58 +245,280 @@ class FileListWidget(QTableView):
|
||||||
def _show_context_menu(self, position):
|
def _show_context_menu(self, position):
|
||||||
"""Zeigt das Kontextmenü."""
|
"""Zeigt das Kontextmenü."""
|
||||||
indexes = self.selectedIndexes()
|
indexes = self.selectedIndexes()
|
||||||
if not indexes:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Eindeutige Zeilen ermitteln
|
# Eindeutige Zeilen ermitteln
|
||||||
rows = set(idx.row() for idx in indexes)
|
rows = set(idx.row() for idx in indexes)
|
||||||
items = [self.list_model.items[row] for row in rows if row < len(self.list_model.items)]
|
items = [self.list_model.items[row] for row in rows if row < len(self.list_model.items)]
|
||||||
|
|
||||||
if not items:
|
|
||||||
return
|
|
||||||
|
|
||||||
menu = QMenu(self)
|
menu = QMenu(self)
|
||||||
|
|
||||||
if len(items) == 1:
|
if not items:
|
||||||
item = items[0]
|
# Kontextmenü für leeren Bereich
|
||||||
|
self._build_empty_context_menu(menu)
|
||||||
|
elif len(items) == 1:
|
||||||
|
# Kontextmenü für einzelnes Element
|
||||||
|
self._build_single_item_context_menu(menu, items[0])
|
||||||
|
else:
|
||||||
|
# Kontextmenü für mehrere Elemente
|
||||||
|
self._build_multi_item_context_menu(menu, items)
|
||||||
|
|
||||||
|
menu.exec(self.viewport().mapToGlobal(position))
|
||||||
|
|
||||||
|
def _build_empty_context_menu(self, menu: QMenu):
|
||||||
|
"""Erstellt Kontextmenü für leeren Bereich."""
|
||||||
|
# Neu-Untermenü
|
||||||
|
new_menu = menu.addMenu("📄 Neu")
|
||||||
|
|
||||||
|
new_file_action = QAction("📄 Neue Datei...", self)
|
||||||
|
new_file_action.setShortcut("Ctrl+N")
|
||||||
|
new_file_action.triggered.connect(self.new_file_requested.emit)
|
||||||
|
new_menu.addAction(new_file_action)
|
||||||
|
|
||||||
|
new_folder_action = QAction("📁 Neuer Ordner...", self)
|
||||||
|
new_folder_action.setShortcut("Ctrl+Shift+N")
|
||||||
|
new_folder_action.triggered.connect(self.new_folder_requested.emit)
|
||||||
|
new_menu.addAction(new_folder_action)
|
||||||
|
|
||||||
|
menu.addSeparator()
|
||||||
|
|
||||||
|
# Einfügen
|
||||||
|
paste_action = QAction("📋 Einfügen", self)
|
||||||
|
paste_action.setShortcut("Ctrl+V")
|
||||||
|
paste_action.setEnabled(self.clipboard.has_files())
|
||||||
|
if self.clipboard.has_files():
|
||||||
|
count = self.clipboard.get_file_count()
|
||||||
|
paste_action.setText(f"📋 Einfügen ({count} Element{'e' if count > 1 else ''})")
|
||||||
|
paste_action.triggered.connect(self.paste_requested.emit)
|
||||||
|
menu.addAction(paste_action)
|
||||||
|
|
||||||
|
menu.addSeparator()
|
||||||
|
|
||||||
|
# Aktualisieren
|
||||||
|
refresh_action = QAction("🔄 Aktualisieren", self)
|
||||||
|
refresh_action.setShortcut("F5")
|
||||||
|
refresh_action.triggered.connect(self.refresh_requested.emit)
|
||||||
|
menu.addAction(refresh_action)
|
||||||
|
|
||||||
|
menu.addSeparator()
|
||||||
|
|
||||||
|
# Terminal öffnen
|
||||||
|
terminal_action = QAction("💻 Terminal hier öffnen", self)
|
||||||
|
terminal_action.triggered.connect(
|
||||||
|
lambda: self.open_terminal_requested.emit(self.list_model.current_path)
|
||||||
|
)
|
||||||
|
menu.addAction(terminal_action)
|
||||||
|
|
||||||
|
menu.addSeparator()
|
||||||
|
|
||||||
|
# Eigenschaften des Ordners
|
||||||
|
props_action = QAction("ℹ️ Eigenschaften", self)
|
||||||
|
props_action.triggered.connect(
|
||||||
|
lambda: self.properties_requested.emit([self.list_model.current_path])
|
||||||
|
)
|
||||||
|
menu.addAction(props_action)
|
||||||
|
|
||||||
|
def _build_single_item_context_menu(self, menu: QMenu, item: FileItem):
|
||||||
|
"""Erstellt Kontextmenü für einzelnes Element."""
|
||||||
# Öffnen
|
# Öffnen
|
||||||
if item.is_dir:
|
if item.is_dir:
|
||||||
open_action = QAction("📂 Öffnen", self)
|
open_action = QAction("📂 Öffnen", self)
|
||||||
open_action.triggered.connect(lambda: self.folder_entered.emit(item.path))
|
open_action.triggered.connect(lambda: self.folder_entered.emit(item.path))
|
||||||
|
menu.addAction(open_action)
|
||||||
|
|
||||||
|
open_new_action = QAction("📂 In neuem Fenster öffnen", self)
|
||||||
|
open_new_action.triggered.connect(lambda: self._open_in_new_window(item.path))
|
||||||
|
menu.addAction(open_new_action)
|
||||||
else:
|
else:
|
||||||
open_action = QAction("🔗 Öffnen", self)
|
open_action = QAction("🔗 Öffnen", self)
|
||||||
open_action.triggered.connect(lambda: self._open_external(item.path))
|
open_action.triggered.connect(lambda: self._open_external(item.path))
|
||||||
menu.addAction(open_action)
|
menu.addAction(open_action)
|
||||||
|
|
||||||
|
open_with_action = QAction("🔗 Öffnen mit...", self)
|
||||||
|
open_with_action.triggered.connect(lambda: self._open_with(item.path))
|
||||||
|
menu.addAction(open_with_action)
|
||||||
|
|
||||||
|
menu.addSeparator()
|
||||||
|
|
||||||
|
# Neu-Untermenü (nur in Ordnern sinnvoll)
|
||||||
|
new_menu = menu.addMenu("📄 Neu")
|
||||||
|
|
||||||
|
new_file_action = QAction("📄 Neue Datei...", self)
|
||||||
|
new_file_action.triggered.connect(self.new_file_requested.emit)
|
||||||
|
new_menu.addAction(new_file_action)
|
||||||
|
|
||||||
|
new_folder_action = QAction("📁 Neuer Ordner...", self)
|
||||||
|
new_folder_action.triggered.connect(self.new_folder_requested.emit)
|
||||||
|
new_menu.addAction(new_folder_action)
|
||||||
|
|
||||||
|
menu.addSeparator()
|
||||||
|
|
||||||
|
# Ausschneiden, Kopieren
|
||||||
|
cut_action = QAction("✂️ Ausschneiden", self)
|
||||||
|
cut_action.setShortcut("Ctrl+X")
|
||||||
|
cut_action.triggered.connect(lambda: self.cut_requested.emit([item.path]))
|
||||||
|
menu.addAction(cut_action)
|
||||||
|
|
||||||
|
copy_action = QAction("📋 Kopieren", self)
|
||||||
|
copy_action.setShortcut("Ctrl+C")
|
||||||
|
copy_action.triggered.connect(lambda: self.copy_requested.emit([item.path]))
|
||||||
|
menu.addAction(copy_action)
|
||||||
|
|
||||||
|
# Einfügen (nur für Ordner)
|
||||||
|
if item.is_dir:
|
||||||
|
paste_action = QAction("📋 Einfügen", self)
|
||||||
|
paste_action.setShortcut("Ctrl+V")
|
||||||
|
paste_action.setEnabled(self.clipboard.has_files())
|
||||||
|
paste_action.triggered.connect(self.paste_requested.emit)
|
||||||
|
menu.addAction(paste_action)
|
||||||
|
|
||||||
menu.addSeparator()
|
menu.addSeparator()
|
||||||
|
|
||||||
# Umbenennen
|
# Umbenennen
|
||||||
rename_action = QAction("✏️ Umbenennen (F2)", self)
|
rename_action = QAction("✏️ Umbenennen", self)
|
||||||
|
rename_action.setShortcut("F2")
|
||||||
rename_action.triggered.connect(lambda: self.file_rename_requested.emit(item.path))
|
rename_action.triggered.connect(lambda: self.file_rename_requested.emit(item.path))
|
||||||
menu.addAction(rename_action)
|
menu.addAction(rename_action)
|
||||||
|
|
||||||
# Verschieben
|
# Verschieben
|
||||||
move_action = QAction("📦 Verschieben", self)
|
move_action = QAction("📦 Verschieben nach...", self)
|
||||||
move_action.triggered.connect(lambda: self.file_move_requested.emit(item.path))
|
move_action.triggered.connect(lambda: self.file_move_requested.emit(item.path))
|
||||||
menu.addAction(move_action)
|
menu.addAction(move_action)
|
||||||
|
|
||||||
|
# Kopieren nach...
|
||||||
|
copy_to_action = QAction("📋 Kopieren nach...", self)
|
||||||
|
copy_to_action.triggered.connect(lambda: self._copy_to(item.path))
|
||||||
|
menu.addAction(copy_to_action)
|
||||||
|
|
||||||
menu.addSeparator()
|
menu.addSeparator()
|
||||||
|
|
||||||
# Löschen (auch für mehrere)
|
# Archiv-Funktionen
|
||||||
delete_action = QAction(f"🗑 Löschen ({len(items)} Element{'e' if len(items) > 1 else ''})", self)
|
if ArchiveHandler.is_archive(item.path):
|
||||||
|
# Entpacken
|
||||||
|
extract_action = QAction("📦 Hier entpacken", self)
|
||||||
|
extract_action.triggered.connect(lambda: self._extract_here(item.path))
|
||||||
|
menu.addAction(extract_action)
|
||||||
|
|
||||||
|
extract_to_action = QAction("📦 Entpacken nach...", self)
|
||||||
|
extract_to_action.triggered.connect(lambda: self.extract_requested.emit(item.path))
|
||||||
|
menu.addAction(extract_to_action)
|
||||||
|
|
||||||
|
menu.addSeparator()
|
||||||
|
else:
|
||||||
|
# Packen
|
||||||
|
archive_action = QAction("📦 Zu Archiv packen...", self)
|
||||||
|
archive_action.triggered.connect(lambda: self.archive_requested.emit([item.path]))
|
||||||
|
menu.addAction(archive_action)
|
||||||
|
|
||||||
|
menu.addSeparator()
|
||||||
|
|
||||||
|
# Löschen
|
||||||
|
delete_action = QAction("🗑 Löschen", self)
|
||||||
|
delete_action.setShortcut("Delete")
|
||||||
|
delete_action.triggered.connect(lambda: self._delete_items([item]))
|
||||||
|
menu.addAction(delete_action)
|
||||||
|
|
||||||
|
menu.addSeparator()
|
||||||
|
|
||||||
|
# Terminal öffnen (für Ordner)
|
||||||
|
if item.is_dir:
|
||||||
|
terminal_action = QAction("💻 Terminal hier öffnen", self)
|
||||||
|
terminal_action.triggered.connect(
|
||||||
|
lambda: self.open_terminal_requested.emit(item.path)
|
||||||
|
)
|
||||||
|
menu.addAction(terminal_action)
|
||||||
|
menu.addSeparator()
|
||||||
|
|
||||||
|
# Eigenschaften
|
||||||
|
props_action = QAction("ℹ️ Eigenschaften", self)
|
||||||
|
props_action.triggered.connect(lambda: self.properties_requested.emit([item.path]))
|
||||||
|
menu.addAction(props_action)
|
||||||
|
|
||||||
|
def _build_multi_item_context_menu(self, menu: QMenu, items: list[FileItem]):
|
||||||
|
"""Erstellt Kontextmenü für mehrere Elemente."""
|
||||||
|
paths = [item.path for item in items]
|
||||||
|
|
||||||
|
# Ausschneiden, Kopieren
|
||||||
|
cut_action = QAction(f"✂️ Ausschneiden ({len(items)} Elemente)", self)
|
||||||
|
cut_action.setShortcut("Ctrl+X")
|
||||||
|
cut_action.triggered.connect(lambda: self.cut_requested.emit(paths))
|
||||||
|
menu.addAction(cut_action)
|
||||||
|
|
||||||
|
copy_action = QAction(f"📋 Kopieren ({len(items)} Elemente)", self)
|
||||||
|
copy_action.setShortcut("Ctrl+C")
|
||||||
|
copy_action.triggered.connect(lambda: self.copy_requested.emit(paths))
|
||||||
|
menu.addAction(copy_action)
|
||||||
|
|
||||||
|
menu.addSeparator()
|
||||||
|
|
||||||
|
# Archiv erstellen
|
||||||
|
archive_action = QAction(f"📦 Zu Archiv packen ({len(items)} Elemente)...", self)
|
||||||
|
archive_action.triggered.connect(lambda: self.archive_requested.emit(paths))
|
||||||
|
menu.addAction(archive_action)
|
||||||
|
|
||||||
|
menu.addSeparator()
|
||||||
|
|
||||||
|
# Löschen
|
||||||
|
delete_action = QAction(f"🗑 Löschen ({len(items)} Elemente)", self)
|
||||||
|
delete_action.setShortcut("Delete")
|
||||||
delete_action.triggered.connect(lambda: self._delete_items(items))
|
delete_action.triggered.connect(lambda: self._delete_items(items))
|
||||||
menu.addAction(delete_action)
|
menu.addAction(delete_action)
|
||||||
|
|
||||||
menu.exec(self.viewport().mapToGlobal(position))
|
menu.addSeparator()
|
||||||
|
|
||||||
|
# Eigenschaften
|
||||||
|
props_action = QAction(f"ℹ️ Eigenschaften ({len(items)} Elemente)", self)
|
||||||
|
props_action.triggered.connect(lambda: self.properties_requested.emit(paths))
|
||||||
|
menu.addAction(props_action)
|
||||||
|
|
||||||
def _open_external(self, path: str):
|
def _open_external(self, path: str):
|
||||||
"""Öffnet eine Datei mit der Standard-Anwendung."""
|
"""Öffnet eine Datei mit der Standard-Anwendung."""
|
||||||
import subprocess
|
|
||||||
try:
|
try:
|
||||||
subprocess.Popen(['xdg-open', path])
|
subprocess.Popen(['xdg-open', path])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Fehler beim Öffnen: {e}")
|
QMessageBox.critical(self, "Fehler", f"Konnte Datei nicht öffnen: {e}")
|
||||||
|
|
||||||
|
def _open_with(self, path: str):
|
||||||
|
"""Öffnet den 'Öffnen mit'-Dialog."""
|
||||||
|
try:
|
||||||
|
# xdg-mime kann zum Öffnen mit Auswahl verwendet werden
|
||||||
|
subprocess.Popen(['xdg-open', '--', path])
|
||||||
|
except Exception as e:
|
||||||
|
QMessageBox.critical(self, "Fehler", f"Konnte Dialog nicht öffnen: {e}")
|
||||||
|
|
||||||
|
def _open_in_new_window(self, path: str):
|
||||||
|
"""Öffnet einen Ordner in einem neuen Fenster."""
|
||||||
|
try:
|
||||||
|
subprocess.Popen(['xdg-open', path])
|
||||||
|
except Exception as e:
|
||||||
|
QMessageBox.critical(self, "Fehler", f"Konnte Fenster nicht öffnen: {e}")
|
||||||
|
|
||||||
|
def _copy_to(self, path: str):
|
||||||
|
"""Kopiert Datei/Ordner an einen ausgewählten Ort."""
|
||||||
|
from ..dialogs import MoveDialog
|
||||||
|
dialog = MoveDialog(path, self)
|
||||||
|
dialog.setWindowTitle("Kopieren nach")
|
||||||
|
if dialog.exec():
|
||||||
|
import shutil
|
||||||
|
target = dialog.get_target_folder()
|
||||||
|
try:
|
||||||
|
name = os.path.basename(path)
|
||||||
|
new_path = os.path.join(target, name)
|
||||||
|
if os.path.isdir(path):
|
||||||
|
shutil.copytree(path, new_path)
|
||||||
|
else:
|
||||||
|
shutil.copy2(path, new_path)
|
||||||
|
self.refresh_requested.emit()
|
||||||
|
except Exception as e:
|
||||||
|
QMessageBox.critical(self, "Fehler", f"Kopieren fehlgeschlagen: {e}")
|
||||||
|
|
||||||
|
def _extract_here(self, path: str):
|
||||||
|
"""Entpackt ein Archiv im aktuellen Ordner."""
|
||||||
|
target_folder = self.list_model.current_path
|
||||||
|
success, message = ArchiveHandler.extract(path, target_folder, create_subfolder=True)
|
||||||
|
if success:
|
||||||
|
self.refresh_requested.emit()
|
||||||
|
else:
|
||||||
|
QMessageBox.critical(self, "Fehler", message)
|
||||||
|
|
||||||
def _delete_items(self, items: list):
|
def _delete_items(self, items: list):
|
||||||
"""Löscht mehrere Elemente."""
|
"""Löscht mehrere Elemente."""
|
||||||
|
|
@ -301,6 +536,10 @@ class FileListWidget(QTableView):
|
||||||
items = self.get_selected_items()
|
items = self.get_selected_items()
|
||||||
return items[0].path if items else ""
|
return items[0].path if items else ""
|
||||||
|
|
||||||
|
def get_selected_paths(self) -> list[str]:
|
||||||
|
"""Gibt die Pfade aller ausgewählten Elemente zurück."""
|
||||||
|
return [item.path for item in self.get_selected_items()]
|
||||||
|
|
||||||
def refresh(self):
|
def refresh(self):
|
||||||
"""Aktualisiert die Dateiliste."""
|
"""Aktualisiert die Dateiliste."""
|
||||||
self.list_model.refresh()
|
self.list_model.refresh()
|
||||||
|
|
@ -368,26 +607,66 @@ class FileListWidget(QTableView):
|
||||||
|
|
||||||
def keyPressEvent(self, event):
|
def keyPressEvent(self, event):
|
||||||
"""Behandelt Tastatureingaben."""
|
"""Behandelt Tastatureingaben."""
|
||||||
|
# Ctrl+C - Kopieren
|
||||||
|
if event.key() == Qt.Key.Key_C and event.modifiers() == Qt.KeyboardModifier.ControlModifier:
|
||||||
|
items = self.get_selected_items()
|
||||||
|
if items:
|
||||||
|
self.copy_requested.emit([item.path for item in items])
|
||||||
|
return
|
||||||
|
|
||||||
|
# Ctrl+X - Ausschneiden
|
||||||
|
if event.key() == Qt.Key.Key_X and event.modifiers() == Qt.KeyboardModifier.ControlModifier:
|
||||||
|
items = self.get_selected_items()
|
||||||
|
if items:
|
||||||
|
self.cut_requested.emit([item.path for item in items])
|
||||||
|
return
|
||||||
|
|
||||||
|
# Ctrl+V - Einfügen
|
||||||
|
if event.key() == Qt.Key.Key_V and event.modifiers() == Qt.KeyboardModifier.ControlModifier:
|
||||||
|
self.paste_requested.emit()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Ctrl+A - Alles auswählen
|
||||||
|
if event.key() == Qt.Key.Key_A and event.modifiers() == Qt.KeyboardModifier.ControlModifier:
|
||||||
|
self.selectAll()
|
||||||
|
return
|
||||||
|
|
||||||
|
# F2 - Umbenennen
|
||||||
if event.key() == Qt.Key.Key_F2:
|
if event.key() == Qt.Key.Key_F2:
|
||||||
items = self.get_selected_items()
|
items = self.get_selected_items()
|
||||||
if len(items) == 1:
|
if len(items) == 1:
|
||||||
self.file_rename_requested.emit(items[0].path)
|
self.file_rename_requested.emit(items[0].path)
|
||||||
elif event.key() == Qt.Key.Key_Delete:
|
return
|
||||||
|
|
||||||
|
# Delete - Löschen
|
||||||
|
if event.key() == Qt.Key.Key_Delete:
|
||||||
items = self.get_selected_items()
|
items = self.get_selected_items()
|
||||||
if items:
|
if items:
|
||||||
self._delete_items(items)
|
self._delete_items(items)
|
||||||
elif event.key() == Qt.Key.Key_Return or event.key() == Qt.Key.Key_Enter:
|
return
|
||||||
|
|
||||||
|
# Enter - Öffnen
|
||||||
|
if event.key() == Qt.Key.Key_Return or event.key() == Qt.Key.Key_Enter:
|
||||||
items = self.get_selected_items()
|
items = self.get_selected_items()
|
||||||
if len(items) == 1:
|
if len(items) == 1:
|
||||||
if items[0].is_dir:
|
if items[0].is_dir:
|
||||||
self.folder_entered.emit(items[0].path)
|
self.folder_entered.emit(items[0].path)
|
||||||
else:
|
else:
|
||||||
self._open_external(items[0].path)
|
self._open_external(items[0].path)
|
||||||
elif event.key() == Qt.Key.Key_Backspace:
|
return
|
||||||
|
|
||||||
|
# Backspace - Zum übergeordneten Ordner
|
||||||
|
if event.key() == Qt.Key.Key_Backspace:
|
||||||
parent = os.path.dirname(self.list_model.current_path)
|
parent = os.path.dirname(self.list_model.current_path)
|
||||||
if parent and parent != self.list_model.current_path:
|
if parent and parent != self.list_model.current_path:
|
||||||
self.folder_entered.emit(parent)
|
self.folder_entered.emit(parent)
|
||||||
else:
|
return
|
||||||
|
|
||||||
|
# F5 - Aktualisieren
|
||||||
|
if event.key() == Qt.Key.Key_F5:
|
||||||
|
self.refresh_requested.emit()
|
||||||
|
return
|
||||||
|
|
||||||
super().keyPressEvent(event)
|
super().keyPressEvent(event)
|
||||||
|
|
||||||
def mouseMoveEvent(self, event):
|
def mouseMoveEvent(self, event):
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue