Compare commits
No commits in common. "main" and "v1.0.0" have entirely different histories.
13 changed files with 30 additions and 1184 deletions
|
|
@ -1,9 +0,0 @@
|
||||||
[Desktop Entry]
|
|
||||||
Name=FileBrowser
|
|
||||||
Comment=Dateimanager mit Vorschau-Funktion
|
|
||||||
Exec=python3 "/mnt/17 - Entwicklungen/20 - Projekte/FileBrowser/main.py"
|
|
||||||
Icon=/mnt/17 - Entwicklungen/20 - Projekte/FileBrowser/resources/icon.png
|
|
||||||
Terminal=false
|
|
||||||
Type=Application
|
|
||||||
Categories=Utility;FileManager;
|
|
||||||
StartupNotify=true
|
|
||||||
8
main.py
8
main.py
|
|
@ -2,10 +2,8 @@
|
||||||
"""FileBrowser - Ein Dateimanager mit Vorschau-Funktion in PyQt6."""
|
"""FileBrowser - Ein Dateimanager mit Vorschau-Funktion in PyQt6."""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
|
||||||
from PyQt6.QtWidgets import QApplication
|
from PyQt6.QtWidgets import QApplication
|
||||||
from PyQt6.QtCore import Qt
|
from PyQt6.QtCore import Qt
|
||||||
from PyQt6.QtGui import QIcon
|
|
||||||
|
|
||||||
from src.main_window import MainWindow
|
from src.main_window import MainWindow
|
||||||
|
|
||||||
|
|
@ -20,12 +18,6 @@ def main():
|
||||||
app.setApplicationName("FileBrowser")
|
app.setApplicationName("FileBrowser")
|
||||||
app.setOrganizationName("FileBrowser")
|
app.setOrganizationName("FileBrowser")
|
||||||
|
|
||||||
# Icon setzen
|
|
||||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
icon_path = os.path.join(script_dir, 'resources', 'icon.png')
|
|
||||||
if os.path.exists(icon_path):
|
|
||||||
app.setWindowIcon(QIcon(icon_path))
|
|
||||||
|
|
||||||
# Hauptfenster erstellen und anzeigen
|
# Hauptfenster erstellen und anzeigen
|
||||||
window = MainWindow()
|
window = MainWindow()
|
||||||
window.show()
|
window.show()
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 8.4 KiB |
|
|
@ -1,41 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="256" height="256" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="folderGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
||||||
<stop offset="0%" style="stop-color:#60a5fa;stop-opacity:1" />
|
|
||||||
<stop offset="100%" style="stop-color:#3b82f6;stop-opacity:1" />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="previewGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
||||||
<stop offset="0%" style="stop-color:#f1f5f9;stop-opacity:1" />
|
|
||||||
<stop offset="100%" style="stop-color:#e2e8f0;stop-opacity:1" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
<!-- Hintergrund -->
|
|
||||||
<rect x="16" y="16" width="224" height="224" rx="24" fill="#1e293b"/>
|
|
||||||
|
|
||||||
<!-- Ordner -->
|
|
||||||
<path d="M40 70 L40 190 Q40 200 50 200 L150 200 Q160 200 160 190 L160 90 Q160 80 150 80 L100 80 L90 65 Q85 60 80 60 L50 60 Q40 60 40 70 Z" fill="url(#folderGrad)"/>
|
|
||||||
|
|
||||||
<!-- Ordner Tab -->
|
|
||||||
<path d="M40 70 L40 75 Q40 65 50 65 L78 65 L88 78 L100 78 L90 65 Q85 60 80 60 L50 60 Q40 60 40 70 Z" fill="#93c5fd"/>
|
|
||||||
|
|
||||||
<!-- Preview Panel -->
|
|
||||||
<rect x="170" y="60" width="70" height="140" rx="8" fill="url(#previewGrad)"/>
|
|
||||||
|
|
||||||
<!-- Preview Linien (Text) -->
|
|
||||||
<rect x="180" y="80" width="50" height="6" rx="3" fill="#94a3b8"/>
|
|
||||||
<rect x="180" y="95" width="40" height="6" rx="3" fill="#94a3b8"/>
|
|
||||||
<rect x="180" y="110" width="45" height="6" rx="3" fill="#94a3b8"/>
|
|
||||||
|
|
||||||
<!-- Preview Bild Placeholder -->
|
|
||||||
<rect x="180" y="130" width="50" height="35" rx="4" fill="#3b82f6" opacity="0.3"/>
|
|
||||||
<circle cx="190" cy="145" r="6" fill="#3b82f6" opacity="0.5"/>
|
|
||||||
<path d="M180 160 L195 150 L205 158 L215 148 L230 165 L180 165 Z" fill="#3b82f6" opacity="0.5"/>
|
|
||||||
|
|
||||||
<!-- Dateien im Ordner -->
|
|
||||||
<rect x="55" y="100" width="90" height="12" rx="3" fill="#ffffff" opacity="0.9"/>
|
|
||||||
<rect x="55" y="120" width="90" height="12" rx="3" fill="#ffffff" opacity="0.7"/>
|
|
||||||
<rect x="55" y="140" width="90" height="12" rx="3" fill="#ffffff" opacity="0.5"/>
|
|
||||||
<rect x="55" y="160" width="90" height="12" rx="3" fill="#ffffff" opacity="0.3"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2 KiB |
|
|
@ -5,9 +5,9 @@ from pathlib import Path
|
||||||
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
|
||||||
)
|
)
|
||||||
from PyQt6.QtCore import Qt, QDir, QSettings
|
from PyQt6.QtCore import Qt, QDir
|
||||||
from PyQt6.QtGui import QFileSystemModel
|
from PyQt6.QtGui import QFileSystemModel
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -228,113 +228,3 @@ class DeleteDialog(QDialog):
|
||||||
button_box.addButton(cancel_btn, QDialogButtonBox.ButtonRole.RejectRole)
|
button_box.addButton(cancel_btn, QDialogButtonBox.ButtonRole.RejectRole)
|
||||||
|
|
||||||
layout.addWidget(button_box)
|
layout.addWidget(button_box)
|
||||||
|
|
||||||
|
|
||||||
class SettingsDialog(QDialog):
|
|
||||||
"""Einstellungsdialog."""
|
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
|
||||||
super().__init__(parent)
|
|
||||||
self.settings = QSettings('FileBrowser', 'FileBrowser')
|
|
||||||
|
|
||||||
self.setWindowTitle("Einstellungen")
|
|
||||||
self.setModal(True)
|
|
||||||
self.setMinimumSize(450, 350)
|
|
||||||
|
|
||||||
self._setup_ui()
|
|
||||||
self._load_settings()
|
|
||||||
|
|
||||||
def _setup_ui(self):
|
|
||||||
layout = QVBoxLayout(self)
|
|
||||||
|
|
||||||
# Tab-Widget für verschiedene Kategorien
|
|
||||||
tabs = QTabWidget()
|
|
||||||
layout.addWidget(tabs)
|
|
||||||
|
|
||||||
# Ansicht-Tab
|
|
||||||
view_tab = QWidget()
|
|
||||||
view_layout = QVBoxLayout(view_tab)
|
|
||||||
|
|
||||||
# Preview-Gruppe
|
|
||||||
preview_group = QGroupBox("Vorschau-Panel")
|
|
||||||
preview_layout = QVBoxLayout(preview_group)
|
|
||||||
|
|
||||||
self.disable_preview_check = QCheckBox(
|
|
||||||
"Vorschau-Panel deaktivieren (immer externes Fenster verwenden)"
|
|
||||||
)
|
|
||||||
self.disable_preview_check.setToolTip(
|
|
||||||
"Wenn aktiviert, wird das Vorschau-Panel im Hauptfenster ausgeblendet "
|
|
||||||
"und Dateien werden automatisch im externen Vorschau-Fenster geöffnet."
|
|
||||||
)
|
|
||||||
preview_layout.addWidget(self.disable_preview_check)
|
|
||||||
|
|
||||||
view_layout.addWidget(preview_group)
|
|
||||||
|
|
||||||
# Pfad-Gruppe
|
|
||||||
path_group = QGroupBox("Pfadanzeige")
|
|
||||||
path_layout = QVBoxLayout(path_group)
|
|
||||||
|
|
||||||
self.path_text_default_check = QCheckBox(
|
|
||||||
"Pfad-Textfeld standardmäßig anzeigen (statt Breadcrumb)"
|
|
||||||
)
|
|
||||||
self.path_text_default_check.setToolTip(
|
|
||||||
"Wenn aktiviert, wird der Pfad standardmäßig als editierbares Textfeld "
|
|
||||||
"angezeigt statt als Breadcrumb-Navigation."
|
|
||||||
)
|
|
||||||
path_layout.addWidget(self.path_text_default_check)
|
|
||||||
|
|
||||||
view_layout.addWidget(path_group)
|
|
||||||
view_layout.addStretch()
|
|
||||||
|
|
||||||
tabs.addTab(view_tab, "Ansicht")
|
|
||||||
|
|
||||||
# PDF-Tab
|
|
||||||
pdf_tab = QWidget()
|
|
||||||
pdf_layout = QVBoxLayout(pdf_tab)
|
|
||||||
|
|
||||||
pdf_info = QLabel(
|
|
||||||
"PDF-Einstellungen können direkt im Vorschau-Panel oder über "
|
|
||||||
"das Menü Ansicht → PDF-Vorschau geändert werden."
|
|
||||||
)
|
|
||||||
pdf_info.setWordWrap(True)
|
|
||||||
pdf_layout.addWidget(pdf_info)
|
|
||||||
pdf_layout.addStretch()
|
|
||||||
|
|
||||||
tabs.addTab(pdf_tab, "PDF")
|
|
||||||
|
|
||||||
# Buttons
|
|
||||||
button_box = QDialogButtonBox(
|
|
||||||
QDialogButtonBox.StandardButton.Ok |
|
|
||||||
QDialogButtonBox.StandardButton.Cancel |
|
|
||||||
QDialogButtonBox.StandardButton.Apply
|
|
||||||
)
|
|
||||||
button_box.accepted.connect(self._save_and_accept)
|
|
||||||
button_box.rejected.connect(self.reject)
|
|
||||||
button_box.button(QDialogButtonBox.StandardButton.Apply).clicked.connect(self._apply_settings)
|
|
||||||
layout.addWidget(button_box)
|
|
||||||
|
|
||||||
def _load_settings(self):
|
|
||||||
"""Lädt die aktuellen Einstellungen."""
|
|
||||||
self.disable_preview_check.setChecked(
|
|
||||||
self.settings.value('disable_preview_panel', False, type=bool)
|
|
||||||
)
|
|
||||||
self.path_text_default_check.setChecked(
|
|
||||||
self.settings.value('breadcrumb_text_mode', False, type=bool)
|
|
||||||
)
|
|
||||||
|
|
||||||
def _apply_settings(self):
|
|
||||||
"""Wendet die Einstellungen an ohne zu schließen."""
|
|
||||||
self.settings.setValue('disable_preview_panel', self.disable_preview_check.isChecked())
|
|
||||||
self.settings.setValue('breadcrumb_text_mode', self.path_text_default_check.isChecked())
|
|
||||||
|
|
||||||
def _save_and_accept(self):
|
|
||||||
"""Speichert und schließt den Dialog."""
|
|
||||||
self._apply_settings()
|
|
||||||
self.accept()
|
|
||||||
|
|
||||||
def get_settings(self) -> dict:
|
|
||||||
"""Gibt die Einstellungen als Dictionary zurück."""
|
|
||||||
return {
|
|
||||||
'disable_preview_panel': self.disable_preview_check.isChecked(),
|
|
||||||
'path_text_default': self.path_text_default_check.isChecked(),
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
"""Dialoge für den FileBrowser."""
|
|
||||||
|
|
||||||
from .base import RenameDialog, MoveDialog, DeleteDialog, SettingsDialog
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
'RenameDialog', 'MoveDialog', 'DeleteDialog', 'SettingsDialog'
|
|
||||||
]
|
|
||||||
|
|
@ -15,7 +15,7 @@ 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
|
||||||
from .preview_window import PreviewWindow
|
from .preview_window import PreviewWindow
|
||||||
from .utils.themes import ThemeManager
|
from .utils.themes import ThemeManager
|
||||||
|
|
||||||
|
|
@ -74,11 +74,11 @@ class MainWindow(QMainWindow):
|
||||||
|
|
||||||
# Panel 3: Vorschau
|
# Panel 3: Vorschau
|
||||||
self.preview_panel = PreviewPanel()
|
self.preview_panel = PreviewPanel()
|
||||||
self.preview_panel.setMinimumWidth(420)
|
self.preview_panel.setMinimumWidth(200)
|
||||||
self.main_splitter.addWidget(self.preview_panel)
|
self.main_splitter.addWidget(self.preview_panel)
|
||||||
|
|
||||||
# Standardgrößen
|
# Standardgrößen
|
||||||
self.main_splitter.setSizes([200, 450, 450])
|
self.main_splitter.setSizes([250, 500, 350])
|
||||||
self.main_splitter.setStretchFactor(0, 0)
|
self.main_splitter.setStretchFactor(0, 0)
|
||||||
self.main_splitter.setStretchFactor(1, 1)
|
self.main_splitter.setStretchFactor(1, 1)
|
||||||
self.main_splitter.setStretchFactor(2, 0)
|
self.main_splitter.setStretchFactor(2, 0)
|
||||||
|
|
@ -100,13 +100,6 @@ class MainWindow(QMainWindow):
|
||||||
|
|
||||||
file_menu.addSeparator()
|
file_menu.addSeparator()
|
||||||
|
|
||||||
settings_action = QAction("Einstellungen...", self)
|
|
||||||
settings_action.setShortcut(QKeySequence("Ctrl+,"))
|
|
||||||
settings_action.triggered.connect(self._show_settings)
|
|
||||||
file_menu.addAction(settings_action)
|
|
||||||
|
|
||||||
file_menu.addSeparator()
|
|
||||||
|
|
||||||
quit_action = QAction("Beenden", self)
|
quit_action = QAction("Beenden", self)
|
||||||
quit_action.setShortcut(QKeySequence.StandardKey.Quit)
|
quit_action.setShortcut(QKeySequence.StandardKey.Quit)
|
||||||
quit_action.triggered.connect(self.close)
|
quit_action.triggered.connect(self.close)
|
||||||
|
|
@ -295,7 +288,6 @@ class MainWindow(QMainWindow):
|
||||||
self.preview_panel.delete_requested.connect(self._delete_file)
|
self.preview_panel.delete_requested.connect(self._delete_file)
|
||||||
self.preview_panel.open_external_requested.connect(self._open_external)
|
self.preview_panel.open_external_requested.connect(self._open_external)
|
||||||
self.preview_panel.detach_requested.connect(self._detach_preview)
|
self.preview_panel.detach_requested.connect(self._detach_preview)
|
||||||
self.preview_panel.content_changed.connect(self._on_preview_content_changed)
|
|
||||||
|
|
||||||
def _load_settings(self):
|
def _load_settings(self):
|
||||||
"""Lädt gespeicherte Einstellungen."""
|
"""Lädt gespeicherte Einstellungen."""
|
||||||
|
|
@ -324,9 +316,6 @@ class MainWindow(QMainWindow):
|
||||||
if index >= 0:
|
if index >= 0:
|
||||||
self.theme_combo.setCurrentIndex(index)
|
self.theme_combo.setCurrentIndex(index)
|
||||||
|
|
||||||
# Preview-Panel beim Start verstecken (hat noch keinen Inhalt)
|
|
||||||
self.preview_panel.setVisible(False)
|
|
||||||
|
|
||||||
def _save_settings(self):
|
def _save_settings(self):
|
||||||
"""Speichert Einstellungen."""
|
"""Speichert Einstellungen."""
|
||||||
self.settings.setValue('window_geometry', self.saveGeometry())
|
self.settings.setValue('window_geometry', self.saveGeometry())
|
||||||
|
|
@ -336,18 +325,10 @@ class MainWindow(QMainWindow):
|
||||||
|
|
||||||
def _apply_theme(self):
|
def _apply_theme(self):
|
||||||
"""Wendet das aktuelle Theme an."""
|
"""Wendet das aktuelle Theme an."""
|
||||||
theme_name = self.theme_manager.get_current_theme()
|
theme = self.theme_manager.get_current_theme()
|
||||||
stylesheet = self.theme_manager.apply_theme(theme_name)
|
stylesheet = self.theme_manager.apply_theme(theme)
|
||||||
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
|
||||||
for action in self.theme_actions:
|
for action in self.theme_actions:
|
||||||
action.setChecked(action.data() == theme)
|
action.setChecked(action.data() == theme)
|
||||||
|
|
@ -403,16 +384,9 @@ class MainWindow(QMainWindow):
|
||||||
|
|
||||||
def _on_file_selected(self, path: str, name: str):
|
def _on_file_selected(self, path: str, name: str):
|
||||||
"""Behandelt Dateiauswahl."""
|
"""Behandelt Dateiauswahl."""
|
||||||
# Prüfen ob Preview-Panel deaktiviert ist
|
|
||||||
if self.settings.value('disable_preview_panel', False, type=bool):
|
|
||||||
# Automatisch externes Fenster öffnen
|
|
||||||
self._detach_preview()
|
|
||||||
if self.preview_window:
|
|
||||||
self.preview_window.load_file(path, name)
|
|
||||||
else:
|
|
||||||
self.preview_panel.load_file(path, name)
|
self.preview_panel.load_file(path, name)
|
||||||
|
|
||||||
# Preview-Fenster aktualisieren (falls offen)
|
# Preview-Fenster aktualisieren
|
||||||
if self.preview_window and self.preview_window.isVisible():
|
if self.preview_window and self.preview_window.isVisible():
|
||||||
self.preview_window.load_file(path, name)
|
self.preview_window.load_file(path, name)
|
||||||
|
|
||||||
|
|
@ -586,14 +560,6 @@ class MainWindow(QMainWindow):
|
||||||
"""Behandelt das Schließen des Preview-Fensters."""
|
"""Behandelt das Schließen des Preview-Fensters."""
|
||||||
pass # Preview-Fenster bleibt im Speicher für schnelles Wiedereröffnen
|
pass # Preview-Fenster bleibt im Speicher für schnelles Wiedereröffnen
|
||||||
|
|
||||||
def _on_preview_content_changed(self, has_content: bool):
|
|
||||||
"""Zeigt oder versteckt das Preview-Panel je nach Inhalt."""
|
|
||||||
# Wenn Preview-Panel deaktiviert ist, immer verstecken
|
|
||||||
if self.settings.value('disable_preview_panel', False, type=bool):
|
|
||||||
self.preview_panel.setVisible(False)
|
|
||||||
else:
|
|
||||||
self.preview_panel.setVisible(has_content)
|
|
||||||
|
|
||||||
def _set_pdf_zoom_mode(self, mode_id: str):
|
def _set_pdf_zoom_mode(self, mode_id: str):
|
||||||
"""Setzt den PDF-Zoom-Modus."""
|
"""Setzt den PDF-Zoom-Modus."""
|
||||||
settings = self.preview_panel.pdf_preview.get_settings()
|
settings = self.preview_panel.pdf_preview.get_settings()
|
||||||
|
|
@ -616,34 +582,6 @@ 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'])
|
||||||
|
|
||||||
def _show_settings(self):
|
|
||||||
"""Zeigt den Einstellungsdialog."""
|
|
||||||
dialog = SettingsDialog(self)
|
|
||||||
if dialog.exec():
|
|
||||||
# Einstellungen wurden geändert - UI aktualisieren
|
|
||||||
self._apply_settings()
|
|
||||||
|
|
||||||
def _apply_settings(self):
|
|
||||||
"""Wendet die aktuellen Einstellungen an."""
|
|
||||||
# Preview-Panel aktualisieren
|
|
||||||
disable_preview = self.settings.value('disable_preview_panel', False, type=bool)
|
|
||||||
if disable_preview:
|
|
||||||
self.preview_panel.setVisible(False)
|
|
||||||
else:
|
|
||||||
# Nur anzeigen wenn Inhalt vorhanden
|
|
||||||
path, name = self.preview_panel.get_current_file()
|
|
||||||
self.preview_panel.setVisible(bool(path))
|
|
||||||
|
|
||||||
# Breadcrumb aktualisieren
|
|
||||||
text_mode = self.settings.value('breadcrumb_text_mode', False, type=bool)
|
|
||||||
self.breadcrumb.toggle_btn.setChecked(text_mode)
|
|
||||||
if text_mode:
|
|
||||||
self.breadcrumb.stack.setCurrentIndex(1)
|
|
||||||
self.breadcrumb.toggle_btn.setText("🗂️")
|
|
||||||
else:
|
|
||||||
self.breadcrumb.stack.setCurrentIndex(0)
|
|
||||||
self.breadcrumb.toggle_btn.setText("📝")
|
|
||||||
|
|
||||||
def _show_about(self):
|
def _show_about(self):
|
||||||
"""Zeigt den Über-Dialog."""
|
"""Zeigt den Über-Dialog."""
|
||||||
QMessageBox.about(
|
QMessageBox.about(
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ from PyQt6.QtWidgets import (
|
||||||
from PyQt6.QtCore import Qt, pyqtSignal, QSettings
|
from PyQt6.QtCore import Qt, pyqtSignal, QSettings
|
||||||
from PyQt6.QtGui import QFont
|
from PyQt6.QtGui import QFont
|
||||||
|
|
||||||
from .widgets.preview_panel import ImagePreview, TextPreview, PdfPreview, MarkdownPreview, NoPreview
|
from .widgets.preview_panel import ImagePreview, TextPreview, PdfPreview, NoPreview
|
||||||
from .utils.file_utils import format_file_size, get_file_type
|
from .utils.file_utils import format_file_size, get_file_type
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -23,7 +23,7 @@ class PreviewWindow(QMainWindow):
|
||||||
self._current_name = ""
|
self._current_name = ""
|
||||||
|
|
||||||
self.setWindowTitle("Vorschau")
|
self.setWindowTitle("Vorschau")
|
||||||
self.setMinimumSize(550, 400)
|
self.setMinimumSize(400, 300)
|
||||||
|
|
||||||
self._setup_ui()
|
self._setup_ui()
|
||||||
self._load_settings()
|
self._load_settings()
|
||||||
|
|
@ -69,7 +69,6 @@ class PreviewWindow(QMainWindow):
|
||||||
self.image_preview = None
|
self.image_preview = None
|
||||||
self.text_preview = None
|
self.text_preview = None
|
||||||
self.pdf_preview = None
|
self.pdf_preview = None
|
||||||
self.markdown_preview = None
|
|
||||||
self.no_preview = None
|
self.no_preview = None
|
||||||
self._current_preview = None
|
self._current_preview = None
|
||||||
|
|
||||||
|
|
@ -79,11 +78,8 @@ class PreviewWindow(QMainWindow):
|
||||||
geometry = settings.value('preview_window_geometry')
|
geometry = settings.value('preview_window_geometry')
|
||||||
if geometry:
|
if geometry:
|
||||||
self.restoreGeometry(geometry)
|
self.restoreGeometry(geometry)
|
||||||
# Mindestgröße sicherstellen
|
|
||||||
if self.width() < 550 or self.height() < 400:
|
|
||||||
self.resize(700, 550)
|
|
||||||
else:
|
else:
|
||||||
self.resize(700, 550)
|
self.resize(600, 500)
|
||||||
|
|
||||||
def _save_settings(self):
|
def _save_settings(self):
|
||||||
"""Speichert Fenstereinstellungen."""
|
"""Speichert Fenstereinstellungen."""
|
||||||
|
|
@ -138,12 +134,6 @@ class PreviewWindow(QMainWindow):
|
||||||
self.pdf_preview.load_pdf(path)
|
self.pdf_preview.load_pdf(path)
|
||||||
self._current_preview = self.pdf_preview
|
self._current_preview = self.pdf_preview
|
||||||
|
|
||||||
elif file_type == 'markdown':
|
|
||||||
if not self.markdown_preview:
|
|
||||||
self.markdown_preview = MarkdownPreview()
|
|
||||||
self.markdown_preview.load_markdown(path)
|
|
||||||
self._current_preview = self.markdown_preview
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if not self.no_preview:
|
if not self.no_preview:
|
||||||
self.no_preview = NoPreview()
|
self.no_preview = NoPreview()
|
||||||
|
|
|
||||||
|
|
@ -75,16 +75,13 @@ def get_file_type(filename: str) -> str:
|
||||||
ext = Path(filename).suffix.lower()
|
ext = Path(filename).suffix.lower()
|
||||||
|
|
||||||
image_exts = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.tiff', '.svg'}
|
image_exts = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.tiff', '.svg'}
|
||||||
markdown_exts = {'.md', '.markdown'}
|
text_exts = {'.txt', '.md', '.log', '.py', '.js', '.ts', '.html', '.css',
|
||||||
text_exts = {'.txt', '.log', '.py', '.js', '.ts', '.html', '.css',
|
|
||||||
'.json', '.xml', '.yaml', '.yml', '.sh', '.bash', '.csv', '.ini',
|
'.json', '.xml', '.yaml', '.yml', '.sh', '.bash', '.csv', '.ini',
|
||||||
'.conf', '.cfg'}
|
'.conf', '.cfg'}
|
||||||
pdf_exts = {'.pdf'}
|
pdf_exts = {'.pdf'}
|
||||||
|
|
||||||
if ext in image_exts:
|
if ext in image_exts:
|
||||||
return 'image'
|
return 'image'
|
||||||
elif ext in markdown_exts:
|
|
||||||
return 'markdown'
|
|
||||||
elif ext in text_exts:
|
elif ext in text_exts:
|
||||||
return 'text'
|
return 'text'
|
||||||
elif ext in pdf_exts:
|
elif ext in pdf_exts:
|
||||||
|
|
|
||||||
|
|
@ -1,385 +0,0 @@
|
||||||
"""PDF-Bearbeitungsfunktionen mit qpdf und ghostscript."""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
import tempfile
|
|
||||||
import shutil
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Optional, List, Tuple
|
|
||||||
|
|
||||||
|
|
||||||
def check_tools() -> dict:
|
|
||||||
"""Prüft welche PDF-Tools verfügbar sind."""
|
|
||||||
tools = {
|
|
||||||
'qpdf': shutil.which('qpdf') is not None,
|
|
||||||
'gs': shutil.which('gs') is not None,
|
|
||||||
'libreoffice': shutil.which('libreoffice') is not None,
|
|
||||||
}
|
|
||||||
return tools
|
|
||||||
|
|
||||||
|
|
||||||
def get_page_count(pdf_path: str) -> int:
|
|
||||||
"""Gibt die Seitenanzahl einer PDF zurück."""
|
|
||||||
try:
|
|
||||||
result = subprocess.run(
|
|
||||||
['qpdf', '--show-npages', pdf_path],
|
|
||||||
capture_output=True, text=True, timeout=30
|
|
||||||
)
|
|
||||||
if result.returncode == 0:
|
|
||||||
return int(result.stdout.strip())
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
def rotate_pages(pdf_path: str, output_path: str, rotation: int,
|
|
||||||
pages: Optional[str] = None) -> Tuple[bool, str]:
|
|
||||||
"""
|
|
||||||
Dreht Seiten in einer PDF.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
pdf_path: Eingabe-PDF
|
|
||||||
output_path: Ausgabe-PDF
|
|
||||||
rotation: Drehung in Grad (90, 180, 270)
|
|
||||||
pages: Seitenbereiche z.B. "1-3,5,7-9" oder None für alle
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
(success, message)
|
|
||||||
"""
|
|
||||||
if rotation not in [90, 180, 270]:
|
|
||||||
return False, "Ungültige Drehung. Erlaubt: 90, 180, 270"
|
|
||||||
|
|
||||||
try:
|
|
||||||
page_spec = pages if pages else "1-z"
|
|
||||||
# qpdf rotation syntax: +90, +180, +270 oder -90, -180, -270
|
|
||||||
cmd = [
|
|
||||||
'qpdf', pdf_path,
|
|
||||||
f'--rotate=+{rotation}:{page_spec}',
|
|
||||||
output_path
|
|
||||||
]
|
|
||||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
|
||||||
|
|
||||||
if result.returncode == 0 or (result.returncode == 3 and os.path.exists(output_path)):
|
|
||||||
return True, f"Seiten erfolgreich um {rotation}° gedreht"
|
|
||||||
else:
|
|
||||||
return False, f"Fehler: {result.stderr}"
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
return False, "Timeout beim Drehen der Seiten"
|
|
||||||
except Exception as e:
|
|
||||||
return False, f"Fehler: {str(e)}"
|
|
||||||
|
|
||||||
|
|
||||||
def delete_pages(pdf_path: str, output_path: str, pages_to_delete: str) -> Tuple[bool, str]:
|
|
||||||
"""
|
|
||||||
Löscht Seiten aus einer PDF.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
pdf_path: Eingabe-PDF
|
|
||||||
output_path: Ausgabe-PDF
|
|
||||||
pages_to_delete: Seiten zum Löschen z.B. "1,3,5-7"
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
(success, message)
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
total_pages = get_page_count(pdf_path)
|
|
||||||
if total_pages == 0:
|
|
||||||
return False, "Konnte Seitenanzahl nicht ermitteln"
|
|
||||||
|
|
||||||
# Parse die zu löschenden Seiten
|
|
||||||
delete_set = set()
|
|
||||||
for part in pages_to_delete.split(','):
|
|
||||||
part = part.strip()
|
|
||||||
if '-' in part:
|
|
||||||
start, end = part.split('-', 1)
|
|
||||||
delete_set.update(range(int(start), int(end) + 1))
|
|
||||||
else:
|
|
||||||
delete_set.add(int(part))
|
|
||||||
|
|
||||||
# Erstelle Liste der zu behaltenden Seiten
|
|
||||||
keep_pages = [i for i in range(1, total_pages + 1) if i not in delete_set]
|
|
||||||
|
|
||||||
if not keep_pages:
|
|
||||||
return False, "Kann nicht alle Seiten löschen"
|
|
||||||
|
|
||||||
# qpdf mit --pages Option
|
|
||||||
page_range = ','.join(str(p) for p in keep_pages)
|
|
||||||
cmd = [
|
|
||||||
'qpdf', pdf_path,
|
|
||||||
'--pages', pdf_path, page_range, '--',
|
|
||||||
output_path
|
|
||||||
]
|
|
||||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
|
||||||
|
|
||||||
if result.returncode == 0 or (result.returncode == 3 and os.path.exists(output_path)):
|
|
||||||
deleted_count = len(delete_set)
|
|
||||||
return True, f"{deleted_count} Seite(n) gelöscht"
|
|
||||||
else:
|
|
||||||
return False, f"Fehler: {result.stderr}"
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
return False, "Timeout beim Löschen der Seiten"
|
|
||||||
except Exception as e:
|
|
||||||
return False, f"Fehler: {str(e)}"
|
|
||||||
|
|
||||||
|
|
||||||
def extract_pages(pdf_path: str, output_path: str, pages: str) -> Tuple[bool, str]:
|
|
||||||
"""
|
|
||||||
Extrahiert Seiten aus einer PDF.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
pdf_path: Eingabe-PDF
|
|
||||||
output_path: Ausgabe-PDF
|
|
||||||
pages: Seitenbereiche z.B. "1-3,5,7-9"
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
(success, message)
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
cmd = [
|
|
||||||
'qpdf', pdf_path,
|
|
||||||
'--pages', pdf_path, pages, '--',
|
|
||||||
output_path
|
|
||||||
]
|
|
||||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
|
||||||
|
|
||||||
if result.returncode == 0 or (result.returncode == 3 and os.path.exists(output_path)):
|
|
||||||
return True, f"Seiten {pages} extrahiert"
|
|
||||||
else:
|
|
||||||
return False, f"Fehler: {result.stderr}"
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
return False, "Timeout beim Extrahieren"
|
|
||||||
except Exception as e:
|
|
||||||
return False, f"Fehler: {str(e)}"
|
|
||||||
|
|
||||||
|
|
||||||
def merge_pdfs(pdf_paths: List[str], output_path: str) -> Tuple[bool, str]:
|
|
||||||
"""
|
|
||||||
Führt mehrere PDFs zusammen.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
pdf_paths: Liste der PDF-Pfade
|
|
||||||
output_path: Ausgabe-PDF
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
(success, message)
|
|
||||||
"""
|
|
||||||
if len(pdf_paths) < 2:
|
|
||||||
return False, "Mindestens 2 PDFs benötigt"
|
|
||||||
|
|
||||||
try:
|
|
||||||
# qpdf merge syntax
|
|
||||||
cmd = ['qpdf', '--empty', '--pages']
|
|
||||||
for pdf in pdf_paths:
|
|
||||||
cmd.extend([pdf, '1-z'])
|
|
||||||
cmd.extend(['--', output_path])
|
|
||||||
|
|
||||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
|
||||||
|
|
||||||
if result.returncode == 0 or (result.returncode == 3 and os.path.exists(output_path)):
|
|
||||||
return True, f"{len(pdf_paths)} PDFs zusammengeführt"
|
|
||||||
else:
|
|
||||||
return False, f"Fehler: {result.stderr}"
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
return False, "Timeout beim Zusammenführen"
|
|
||||||
except Exception as e:
|
|
||||||
return False, f"Fehler: {str(e)}"
|
|
||||||
|
|
||||||
|
|
||||||
def split_pdf(pdf_path: str, output_dir: str, pages_per_file: int = 1) -> Tuple[bool, str]:
|
|
||||||
"""
|
|
||||||
Teilt eine PDF in mehrere Dateien.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
pdf_path: Eingabe-PDF
|
|
||||||
output_dir: Ausgabe-Verzeichnis
|
|
||||||
pages_per_file: Seiten pro Datei
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
(success, message)
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
total_pages = get_page_count(pdf_path)
|
|
||||||
if total_pages == 0:
|
|
||||||
return False, "Konnte Seitenanzahl nicht ermitteln"
|
|
||||||
|
|
||||||
base_name = Path(pdf_path).stem
|
|
||||||
created_files = 0
|
|
||||||
|
|
||||||
for start in range(1, total_pages + 1, pages_per_file):
|
|
||||||
end = min(start + pages_per_file - 1, total_pages)
|
|
||||||
output_path = os.path.join(output_dir, f"{base_name}_seiten_{start}-{end}.pdf")
|
|
||||||
|
|
||||||
cmd = [
|
|
||||||
'qpdf', pdf_path,
|
|
||||||
'--pages', pdf_path, f'{start}-{end}', '--',
|
|
||||||
output_path
|
|
||||||
]
|
|
||||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
|
||||||
|
|
||||||
if result.returncode == 0 or (result.returncode == 3 and os.path.exists(output_path)):
|
|
||||||
created_files += 1
|
|
||||||
else:
|
|
||||||
return False, f"Fehler bei Seiten {start}-{end}: {result.stderr}"
|
|
||||||
|
|
||||||
return True, f"PDF in {created_files} Dateien aufgeteilt"
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
return False, "Timeout beim Aufteilen"
|
|
||||||
except Exception as e:
|
|
||||||
return False, f"Fehler: {str(e)}"
|
|
||||||
|
|
||||||
|
|
||||||
def compress_pdf(pdf_path: str, output_path: str) -> Tuple[bool, str]:
|
|
||||||
"""
|
|
||||||
Komprimiert eine PDF mit Ghostscript.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
pdf_path: Eingabe-PDF
|
|
||||||
output_path: Ausgabe-PDF
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
(success, message)
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
original_size = os.path.getsize(pdf_path)
|
|
||||||
|
|
||||||
cmd = [
|
|
||||||
'gs', '-sDEVICE=pdfwrite',
|
|
||||||
'-dCompatibilityLevel=1.4',
|
|
||||||
'-dPDFSETTINGS=/ebook', # Gute Qualität, kleinere Größe
|
|
||||||
'-dNOPAUSE', '-dQUIET', '-dBATCH',
|
|
||||||
f'-sOutputFile={output_path}',
|
|
||||||
pdf_path
|
|
||||||
]
|
|
||||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
|
||||||
|
|
||||||
if result.returncode == 0 and os.path.exists(output_path):
|
|
||||||
new_size = os.path.getsize(output_path)
|
|
||||||
reduction = ((original_size - new_size) / original_size) * 100
|
|
||||||
return True, f"Komprimiert: {reduction:.1f}% kleiner"
|
|
||||||
else:
|
|
||||||
return False, f"Fehler: {result.stderr}"
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
return False, "Timeout beim Komprimieren"
|
|
||||||
except Exception as e:
|
|
||||||
return False, f"Fehler: {str(e)}"
|
|
||||||
|
|
||||||
|
|
||||||
def add_password(pdf_path: str, output_path: str,
|
|
||||||
user_password: str, owner_password: Optional[str] = None) -> Tuple[bool, str]:
|
|
||||||
"""
|
|
||||||
Fügt Passwortschutz zu einer PDF hinzu.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
pdf_path: Eingabe-PDF
|
|
||||||
output_path: Ausgabe-PDF
|
|
||||||
user_password: Passwort zum Öffnen
|
|
||||||
owner_password: Passwort für Bearbeitungsrechte (optional)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
(success, message)
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
owner_pw = owner_password or user_password
|
|
||||||
cmd = [
|
|
||||||
'qpdf', '--encrypt', user_password, owner_pw, '256', '--',
|
|
||||||
pdf_path, output_path
|
|
||||||
]
|
|
||||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
|
||||||
|
|
||||||
if result.returncode == 0 or (result.returncode == 3 and os.path.exists(output_path)):
|
|
||||||
return True, "Passwortschutz hinzugefügt"
|
|
||||||
else:
|
|
||||||
return False, f"Fehler: {result.stderr}"
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
return False, "Timeout beim Verschlüsseln"
|
|
||||||
except Exception as e:
|
|
||||||
return False, f"Fehler: {str(e)}"
|
|
||||||
|
|
||||||
|
|
||||||
def remove_password(pdf_path: str, output_path: str, password: str) -> Tuple[bool, str]:
|
|
||||||
"""
|
|
||||||
Entfernt Passwortschutz von einer PDF.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
pdf_path: Eingabe-PDF (verschlüsselt)
|
|
||||||
output_path: Ausgabe-PDF (unverschlüsselt)
|
|
||||||
password: Passwort zum Entschlüsseln
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
(success, message)
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
cmd = [
|
|
||||||
'qpdf', '--password=' + password, '--decrypt',
|
|
||||||
pdf_path, output_path
|
|
||||||
]
|
|
||||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
|
||||||
|
|
||||||
if result.returncode == 0 or (result.returncode == 3 and os.path.exists(output_path)):
|
|
||||||
return True, "Passwortschutz entfernt"
|
|
||||||
else:
|
|
||||||
return False, f"Fehler: {result.stderr}"
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
return False, "Timeout beim Entschlüsseln"
|
|
||||||
except Exception as e:
|
|
||||||
return False, f"Fehler: {str(e)}"
|
|
||||||
|
|
||||||
|
|
||||||
def sign_pdf_libreoffice(pdf_path: str, output_path: str) -> Tuple[bool, str]:
|
|
||||||
"""
|
|
||||||
Öffnet PDF in LibreOffice Draw zum Signieren.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
pdf_path: PDF zum Signieren
|
|
||||||
output_path: Wird ignoriert - LibreOffice speichert selbst
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
(success, message)
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# LibreOffice Draw öffnen - Benutzer kann dann signieren
|
|
||||||
cmd = ['libreoffice', '--draw', pdf_path]
|
|
||||||
subprocess.Popen(cmd)
|
|
||||||
return True, "PDF in LibreOffice Draw geöffnet.\nNutze Einfügen → Signaturzeile zum Signieren."
|
|
||||||
except Exception as e:
|
|
||||||
return False, f"Fehler: {str(e)}"
|
|
||||||
|
|
||||||
|
|
||||||
def pdf_to_images(pdf_path: str, output_dir: str, dpi: int = 150,
|
|
||||||
format: str = 'png') -> Tuple[bool, str]:
|
|
||||||
"""
|
|
||||||
Konvertiert PDF-Seiten in Bilder.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
pdf_path: Eingabe-PDF
|
|
||||||
output_dir: Ausgabe-Verzeichnis
|
|
||||||
dpi: Auflösung
|
|
||||||
format: Bildformat (png, jpg)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
(success, message)
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
base_name = Path(pdf_path).stem
|
|
||||||
output_pattern = os.path.join(output_dir, f"{base_name}_seite_%d.{format}")
|
|
||||||
|
|
||||||
device = 'png16m' if format == 'png' else 'jpeg'
|
|
||||||
cmd = [
|
|
||||||
'gs', f'-sDEVICE={device}',
|
|
||||||
f'-r{dpi}',
|
|
||||||
'-dNOPAUSE', '-dQUIET', '-dBATCH',
|
|
||||||
f'-sOutputFile={output_pattern}',
|
|
||||||
pdf_path
|
|
||||||
]
|
|
||||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
|
|
||||||
|
|
||||||
if result.returncode == 0:
|
|
||||||
# Zähle erstellte Dateien
|
|
||||||
files = list(Path(output_dir).glob(f"{base_name}_seite_*.{format}"))
|
|
||||||
return True, f"{len(files)} Bilder erstellt"
|
|
||||||
else:
|
|
||||||
return False, f"Fehler: {result.stderr}"
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
return False, "Timeout bei der Konvertierung"
|
|
||||||
except Exception as e:
|
|
||||||
return False, f"Fehler: {str(e)}"
|
|
||||||
|
|
@ -9,10 +9,6 @@ class ThemeManager:
|
||||||
"""Verwaltet die Themes der Anwendung."""
|
"""Verwaltet die Themes der Anwendung."""
|
||||||
|
|
||||||
THEMES = {
|
THEMES = {
|
||||||
'system': {
|
|
||||||
'name': 'System',
|
|
||||||
'is_system': True, # Marker für System-Theme
|
|
||||||
},
|
|
||||||
'dark': {
|
'dark': {
|
||||||
'name': 'Dark',
|
'name': 'Dark',
|
||||||
'window': '#0f172a',
|
'window': '#0f172a',
|
||||||
|
|
@ -76,11 +72,6 @@ class ThemeManager:
|
||||||
theme = self.THEMES[theme_name]
|
theme = self.THEMES[theme_name]
|
||||||
app = QApplication.instance()
|
app = QApplication.instance()
|
||||||
|
|
||||||
# System-Theme: Standardpalette und leeres Stylesheet verwenden
|
|
||||||
if theme.get('is_system'):
|
|
||||||
app.setPalette(app.style().standardPalette())
|
|
||||||
return "" # Leeres Stylesheet = System-Styling
|
|
||||||
|
|
||||||
palette = QPalette()
|
palette = QPalette()
|
||||||
palette.setColor(QPalette.ColorRole.Window, QColor(theme['window']))
|
palette.setColor(QPalette.ColorRole.Window, QColor(theme['window']))
|
||||||
palette.setColor(QPalette.ColorRole.WindowText, QColor(theme['window_text']))
|
palette.setColor(QPalette.ColorRole.WindowText, QColor(theme['window_text']))
|
||||||
|
|
@ -112,10 +103,6 @@ class ThemeManager:
|
||||||
|
|
||||||
theme = self.THEMES.get(theme_name, self.THEMES['dark'])
|
theme = self.THEMES.get(theme_name, self.THEMES['dark'])
|
||||||
|
|
||||||
# System-Theme: kein Stylesheet
|
|
||||||
if theme.get('is_system'):
|
|
||||||
return ""
|
|
||||||
|
|
||||||
return f"""
|
return f"""
|
||||||
QMainWindow, QWidget {{
|
QMainWindow, QWidget {{
|
||||||
background-color: {theme['window']};
|
background-color: {theme['window']};
|
||||||
|
|
@ -140,15 +127,10 @@ 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 {{
|
|
||||||
background-color: {theme['highlight']};
|
|
||||||
color: {theme['highlight_text']};
|
|
||||||
}}
|
|
||||||
|
|
||||||
QHeaderView::section {{
|
QHeaderView::section {{
|
||||||
background-color: {theme['button']};
|
background-color: {theme['button']};
|
||||||
color: {theme['button_text']};
|
color: {theme['button_text']};
|
||||||
|
|
@ -284,8 +266,6 @@ class ThemeManager:
|
||||||
border: 1px solid {theme['border']};
|
border: 1px solid {theme['border']};
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
padding-right: 25px;
|
|
||||||
min-height: 20px;
|
|
||||||
}}
|
}}
|
||||||
|
|
||||||
QComboBox:hover {{
|
QComboBox:hover {{
|
||||||
|
|
@ -293,30 +273,14 @@ class ThemeManager:
|
||||||
}}
|
}}
|
||||||
|
|
||||||
QComboBox::drop-down {{
|
QComboBox::drop-down {{
|
||||||
subcontrol-origin: padding;
|
|
||||||
subcontrol-position: center right;
|
|
||||||
width: 20px;
|
|
||||||
border: none;
|
border: none;
|
||||||
background: transparent;
|
width: 20px;
|
||||||
}}
|
|
||||||
|
|
||||||
QComboBox::down-arrow {{
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
border-left: 5px solid transparent;
|
|
||||||
border-right: 5px solid transparent;
|
|
||||||
border-top: 6px solid {theme['button_text']};
|
|
||||||
}}
|
|
||||||
|
|
||||||
QComboBox::down-arrow:hover {{
|
|
||||||
border-top-color: {theme['highlight']};
|
|
||||||
}}
|
}}
|
||||||
|
|
||||||
QComboBox QAbstractItemView {{
|
QComboBox QAbstractItemView {{
|
||||||
background-color: {theme['base']};
|
background-color: {theme['base']};
|
||||||
color: {theme['text']};
|
color: {theme['text']};
|
||||||
selection-background-color: {theme['highlight']};
|
selection-background-color: {theme['highlight']};
|
||||||
border: 1px solid {theme['border']};
|
|
||||||
}}
|
}}
|
||||||
|
|
||||||
QTabWidget::pane {{
|
QTabWidget::pane {{
|
||||||
|
|
|
||||||
|
|
@ -4,19 +4,18 @@ import os
|
||||||
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,
|
||||||
|
QStyledItemDelegate, QStyle
|
||||||
)
|
)
|
||||||
from PyQt6.QtCore import (
|
from PyQt6.QtCore import (
|
||||||
Qt, QModelIndex, pyqtSignal, QAbstractTableModel, QMimeData,
|
Qt, QModelIndex, pyqtSignal, QAbstractTableModel, QMimeData,
|
||||||
QUrl
|
QUrl, QVariant, QSize
|
||||||
)
|
)
|
||||||
from PyQt6.QtGui import QAction, QDrag, QColor, QBrush
|
from PyQt6.QtGui import QAction, QDrag, QIcon
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class FileItem:
|
class FileItem:
|
||||||
"""Repräsentiert einen Dateieintrag."""
|
"""Repräsentiert einen Dateieintrag."""
|
||||||
|
|
||||||
|
|
@ -33,14 +32,12 @@ class FileItem:
|
||||||
class FileListModel(QAbstractTableModel):
|
class FileListModel(QAbstractTableModel):
|
||||||
"""Model für die Dateiliste."""
|
"""Model für die Dateiliste."""
|
||||||
|
|
||||||
HEADERS = ['📄', 'Name', 'Größe', 'Geändert']
|
HEADERS = ['', 'Name', 'Größe', 'Geändert']
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.items: list[FileItem] = []
|
self.items: list[FileItem] = []
|
||||||
self.current_path = ""
|
self.current_path = ""
|
||||||
self._hovered_row = -1
|
|
||||||
self._hover_color = QColor("#334155") # Default, wird vom Theme überschrieben
|
|
||||||
|
|
||||||
def rowCount(self, parent=None):
|
def rowCount(self, parent=None):
|
||||||
return len(self.items)
|
return len(self.items)
|
||||||
|
|
@ -73,33 +70,9 @@ 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
|
||||||
|
|
||||||
def set_hovered_row(self, row: int):
|
|
||||||
"""Setzt die aktuell gehoverte Zeile."""
|
|
||||||
if self._hovered_row != row:
|
|
||||||
old_row = self._hovered_row
|
|
||||||
self._hovered_row = row
|
|
||||||
# Alte und neue Zeile aktualisieren
|
|
||||||
if old_row >= 0 and old_row < len(self.items):
|
|
||||||
self.dataChanged.emit(
|
|
||||||
self.index(old_row, 0),
|
|
||||||
self.index(old_row, self.columnCount() - 1)
|
|
||||||
)
|
|
||||||
if row >= 0 and row < len(self.items):
|
|
||||||
self.dataChanged.emit(
|
|
||||||
self.index(row, 0),
|
|
||||||
self.index(row, self.columnCount() - 1)
|
|
||||||
)
|
|
||||||
|
|
||||||
def set_hover_color(self, color: QColor):
|
|
||||||
"""Setzt die Hover-Farbe."""
|
|
||||||
self._hover_color = color
|
|
||||||
|
|
||||||
def headerData(self, section, orientation, role=Qt.ItemDataRole.DisplayRole):
|
def headerData(self, section, orientation, role=Qt.ItemDataRole.DisplayRole):
|
||||||
if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole:
|
if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole:
|
||||||
return self.HEADERS[section]
|
return self.HEADERS[section]
|
||||||
|
|
@ -181,10 +154,9 @@ class FileListWidget(QTableView):
|
||||||
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
||||||
self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||||
self.setShowGrid(False)
|
self.setShowGrid(False)
|
||||||
self.setAlternatingRowColors(False)
|
self.setAlternatingRowColors(True)
|
||||||
self.verticalHeader().setVisible(False)
|
self.verticalHeader().setVisible(False)
|
||||||
self.setWordWrap(False)
|
self.setWordWrap(False)
|
||||||
self.setMouseTracking(True)
|
|
||||||
|
|
||||||
# Spaltenbreiten
|
# Spaltenbreiten
|
||||||
header = self.horizontalHeader()
|
header = self.horizontalHeader()
|
||||||
|
|
@ -192,7 +164,7 @@ class FileListWidget(QTableView):
|
||||||
header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
|
header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
|
||||||
header.setSectionResizeMode(2, QHeaderView.ResizeMode.Fixed)
|
header.setSectionResizeMode(2, QHeaderView.ResizeMode.Fixed)
|
||||||
header.setSectionResizeMode(3, QHeaderView.ResizeMode.Fixed)
|
header.setSectionResizeMode(3, QHeaderView.ResizeMode.Fixed)
|
||||||
self.setColumnWidth(0, 36)
|
self.setColumnWidth(0, 30)
|
||||||
self.setColumnWidth(2, 80)
|
self.setColumnWidth(2, 80)
|
||||||
self.setColumnWidth(3, 130)
|
self.setColumnWidth(3, 130)
|
||||||
|
|
||||||
|
|
@ -389,19 +361,3 @@ class FileListWidget(QTableView):
|
||||||
self.folder_entered.emit(parent)
|
self.folder_entered.emit(parent)
|
||||||
else:
|
else:
|
||||||
super().keyPressEvent(event)
|
super().keyPressEvent(event)
|
||||||
|
|
||||||
def mouseMoveEvent(self, event):
|
|
||||||
"""Verfolgt die Zeile unter dem Mauszeiger für Hover-Effekt."""
|
|
||||||
index = self.indexAt(event.pos())
|
|
||||||
row = index.row() if index.isValid() else -1
|
|
||||||
self.list_model.set_hovered_row(row)
|
|
||||||
super().mouseMoveEvent(event)
|
|
||||||
|
|
||||||
def leaveEvent(self, event):
|
|
||||||
"""Entfernt den Hover-Effekt wenn die Maus das Widget verlässt."""
|
|
||||||
self.list_model.set_hovered_row(-1)
|
|
||||||
super().leaveEvent(event)
|
|
||||||
|
|
||||||
def set_hover_color(self, color: str):
|
|
||||||
"""Setzt die Hover-Farbe (für Theme-Unterstützung)."""
|
|
||||||
self.list_model.set_hover_color(QColor(color))
|
|
||||||
|
|
|
||||||
|
|
@ -6,20 +6,14 @@ from pathlib import Path
|
||||||
from PyQt6.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
||||||
QScrollArea, QPlainTextEdit, QStackedWidget, QSizePolicy,
|
QScrollArea, QPlainTextEdit, QStackedWidget, QSizePolicy,
|
||||||
QFrame, QApplication, QComboBox, QSpinBox, QMenu, QInputDialog,
|
QFrame, QApplication, QComboBox, QSpinBox
|
||||||
QFileDialog, QMessageBox, QTextEdit
|
|
||||||
)
|
)
|
||||||
import markdown
|
|
||||||
from PyQt6.QtCore import Qt, pyqtSignal, QUrl, QSize, QTimer, QSettings
|
from PyQt6.QtCore import Qt, pyqtSignal, QUrl, QSize, QTimer, QSettings
|
||||||
from PyQt6.QtGui import QAction
|
|
||||||
import tempfile
|
|
||||||
import shutil
|
|
||||||
from PyQt6.QtGui import QPixmap, QImage, QFont, QPainter
|
from PyQt6.QtGui import QPixmap, QImage, QFont, QPainter
|
||||||
from PyQt6.QtPdf import QPdfDocument
|
from PyQt6.QtPdf import QPdfDocument
|
||||||
from PyQt6.QtPdfWidgets import QPdfView
|
from PyQt6.QtPdfWidgets import QPdfView
|
||||||
|
|
||||||
from ..utils.file_utils import get_file_icon, format_file_size, get_file_type
|
from ..utils.file_utils import get_file_icon, format_file_size, get_file_type
|
||||||
from ..utils import pdf_tools
|
|
||||||
|
|
||||||
|
|
||||||
class ImagePreview(QScrollArea):
|
class ImagePreview(QScrollArea):
|
||||||
|
|
@ -109,338 +103,8 @@ class TextPreview(QPlainTextEdit):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
class MarkdownPreview(QWidget):
|
|
||||||
"""Markdown-Vorschau mit WYSIWYG-Ansicht und Bearbeitungsmodus."""
|
|
||||||
|
|
||||||
MAX_SIZE = 1024 * 1024 # 1 MB
|
|
||||||
|
|
||||||
# CSS für Markdown-Rendering
|
|
||||||
MARKDOWN_CSS = """
|
|
||||||
body { font-family: sans-serif; padding: 10px; line-height: 1.6; }
|
|
||||||
h1, h2, h3 { color: #2563eb; margin-top: 1em; }
|
|
||||||
code { background: #f1f5f9; padding: 2px 6px; border-radius: 4px; font-family: monospace; }
|
|
||||||
pre { background: #1e293b; color: #f1f5f9; padding: 12px; border-radius: 6px; overflow-x: auto; }
|
|
||||||
pre code { background: transparent; padding: 0; }
|
|
||||||
blockquote { border-left: 4px solid #3b82f6; margin: 0; padding-left: 16px; color: #64748b; }
|
|
||||||
table { border-collapse: collapse; width: 100%; }
|
|
||||||
th, td { border: 1px solid #cbd5e1; padding: 8px; text-align: left; }
|
|
||||||
th { background: #f1f5f9; }
|
|
||||||
a { color: #3b82f6; }
|
|
||||||
ul, ol { padding-left: 24px; }
|
|
||||||
hr { border: none; border-top: 1px solid #e2e8f0; margin: 20px 0; }
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
|
||||||
super().__init__(parent)
|
|
||||||
self._current_path = ""
|
|
||||||
self._edit_mode = False
|
|
||||||
|
|
||||||
layout = QVBoxLayout(self)
|
|
||||||
layout.setContentsMargins(0, 0, 0, 0)
|
|
||||||
layout.setSpacing(2)
|
|
||||||
|
|
||||||
# Haupt-Toolbar
|
|
||||||
toolbar = QWidget()
|
|
||||||
toolbar.setFixedHeight(32)
|
|
||||||
toolbar_layout = QHBoxLayout(toolbar)
|
|
||||||
toolbar_layout.setContentsMargins(4, 0, 4, 0)
|
|
||||||
toolbar_layout.setSpacing(8)
|
|
||||||
|
|
||||||
toolbar_layout.addWidget(QLabel("📝 Markdown"))
|
|
||||||
toolbar_layout.addStretch()
|
|
||||||
|
|
||||||
# Bearbeiten/Speichern Button
|
|
||||||
self.edit_btn = QPushButton("✏️ Bearbeiten")
|
|
||||||
self.edit_btn.setFixedWidth(100)
|
|
||||||
self.edit_btn.clicked.connect(self._toggle_edit_mode)
|
|
||||||
toolbar_layout.addWidget(self.edit_btn)
|
|
||||||
|
|
||||||
layout.addWidget(toolbar)
|
|
||||||
|
|
||||||
# Formatierungs-Toolbar (nur im Edit-Modus sichtbar)
|
|
||||||
self.format_toolbar = QWidget()
|
|
||||||
self.format_toolbar.setFixedHeight(32)
|
|
||||||
self.format_toolbar.setVisible(False)
|
|
||||||
fmt_layout = QHBoxLayout(self.format_toolbar)
|
|
||||||
fmt_layout.setContentsMargins(4, 0, 4, 0)
|
|
||||||
fmt_layout.setSpacing(2)
|
|
||||||
|
|
||||||
# Überschriften
|
|
||||||
h1_btn = QPushButton("H1")
|
|
||||||
h1_btn.setFixedSize(28, 26)
|
|
||||||
h1_btn.setToolTip("Überschrift 1")
|
|
||||||
h1_btn.clicked.connect(lambda: self._insert_format("# ", ""))
|
|
||||||
fmt_layout.addWidget(h1_btn)
|
|
||||||
|
|
||||||
h2_btn = QPushButton("H2")
|
|
||||||
h2_btn.setFixedSize(28, 26)
|
|
||||||
h2_btn.setToolTip("Überschrift 2")
|
|
||||||
h2_btn.clicked.connect(lambda: self._insert_format("## ", ""))
|
|
||||||
fmt_layout.addWidget(h2_btn)
|
|
||||||
|
|
||||||
h3_btn = QPushButton("H3")
|
|
||||||
h3_btn.setFixedSize(28, 26)
|
|
||||||
h3_btn.setToolTip("Überschrift 3")
|
|
||||||
h3_btn.clicked.connect(lambda: self._insert_format("### ", ""))
|
|
||||||
fmt_layout.addWidget(h3_btn)
|
|
||||||
|
|
||||||
fmt_layout.addSpacing(8)
|
|
||||||
|
|
||||||
# Text-Formatierung
|
|
||||||
bold_btn = QPushButton("B")
|
|
||||||
bold_btn.setFixedSize(28, 26)
|
|
||||||
bold_btn.setToolTip("Fett (Ctrl+B)")
|
|
||||||
bold_btn.setStyleSheet("font-weight: bold;")
|
|
||||||
bold_btn.clicked.connect(lambda: self._wrap_selection("**", "**"))
|
|
||||||
fmt_layout.addWidget(bold_btn)
|
|
||||||
|
|
||||||
italic_btn = QPushButton("I")
|
|
||||||
italic_btn.setFixedSize(28, 26)
|
|
||||||
italic_btn.setToolTip("Kursiv (Ctrl+I)")
|
|
||||||
italic_btn.setStyleSheet("font-style: italic;")
|
|
||||||
italic_btn.clicked.connect(lambda: self._wrap_selection("*", "*"))
|
|
||||||
fmt_layout.addWidget(italic_btn)
|
|
||||||
|
|
||||||
strike_btn = QPushButton("S̶")
|
|
||||||
strike_btn.setFixedSize(28, 26)
|
|
||||||
strike_btn.setToolTip("Durchgestrichen")
|
|
||||||
strike_btn.clicked.connect(lambda: self._wrap_selection("~~", "~~"))
|
|
||||||
fmt_layout.addWidget(strike_btn)
|
|
||||||
|
|
||||||
code_btn = QPushButton("</>")
|
|
||||||
code_btn.setFixedSize(32, 26)
|
|
||||||
code_btn.setToolTip("Code")
|
|
||||||
code_btn.clicked.connect(lambda: self._wrap_selection("`", "`"))
|
|
||||||
fmt_layout.addWidget(code_btn)
|
|
||||||
|
|
||||||
fmt_layout.addSpacing(8)
|
|
||||||
|
|
||||||
# Listen
|
|
||||||
ul_btn = QPushButton("•")
|
|
||||||
ul_btn.setFixedSize(28, 26)
|
|
||||||
ul_btn.setToolTip("Aufzählung")
|
|
||||||
ul_btn.clicked.connect(lambda: self._insert_format("- ", ""))
|
|
||||||
fmt_layout.addWidget(ul_btn)
|
|
||||||
|
|
||||||
ol_btn = QPushButton("1.")
|
|
||||||
ol_btn.setFixedSize(28, 26)
|
|
||||||
ol_btn.setToolTip("Nummerierte Liste")
|
|
||||||
ol_btn.clicked.connect(lambda: self._insert_format("1. ", ""))
|
|
||||||
fmt_layout.addWidget(ol_btn)
|
|
||||||
|
|
||||||
task_btn = QPushButton("☑")
|
|
||||||
task_btn.setFixedSize(28, 26)
|
|
||||||
task_btn.setToolTip("Aufgabe")
|
|
||||||
task_btn.clicked.connect(lambda: self._insert_format("- [ ] ", ""))
|
|
||||||
fmt_layout.addWidget(task_btn)
|
|
||||||
|
|
||||||
fmt_layout.addSpacing(8)
|
|
||||||
|
|
||||||
# Blöcke
|
|
||||||
quote_btn = QPushButton("❝")
|
|
||||||
quote_btn.setFixedSize(28, 26)
|
|
||||||
quote_btn.setToolTip("Zitat")
|
|
||||||
quote_btn.clicked.connect(lambda: self._insert_format("> ", ""))
|
|
||||||
fmt_layout.addWidget(quote_btn)
|
|
||||||
|
|
||||||
codeblock_btn = QPushButton("```")
|
|
||||||
codeblock_btn.setFixedSize(32, 26)
|
|
||||||
codeblock_btn.setToolTip("Code-Block")
|
|
||||||
codeblock_btn.clicked.connect(self._insert_codeblock)
|
|
||||||
fmt_layout.addWidget(codeblock_btn)
|
|
||||||
|
|
||||||
hr_btn = QPushButton("―")
|
|
||||||
hr_btn.setFixedSize(28, 26)
|
|
||||||
hr_btn.setToolTip("Horizontale Linie")
|
|
||||||
hr_btn.clicked.connect(lambda: self._insert_format("\n---\n", ""))
|
|
||||||
fmt_layout.addWidget(hr_btn)
|
|
||||||
|
|
||||||
fmt_layout.addSpacing(8)
|
|
||||||
|
|
||||||
# Links und Bilder
|
|
||||||
link_btn = QPushButton("🔗")
|
|
||||||
link_btn.setFixedSize(28, 26)
|
|
||||||
link_btn.setToolTip("Link einfügen")
|
|
||||||
link_btn.clicked.connect(self._insert_link)
|
|
||||||
fmt_layout.addWidget(link_btn)
|
|
||||||
|
|
||||||
img_btn = QPushButton("🖼")
|
|
||||||
img_btn.setFixedSize(28, 26)
|
|
||||||
img_btn.setToolTip("Bild einfügen")
|
|
||||||
img_btn.clicked.connect(self._insert_image)
|
|
||||||
fmt_layout.addWidget(img_btn)
|
|
||||||
|
|
||||||
table_btn = QPushButton("▦")
|
|
||||||
table_btn.setFixedSize(28, 26)
|
|
||||||
table_btn.setToolTip("Tabelle einfügen")
|
|
||||||
table_btn.clicked.connect(self._insert_table)
|
|
||||||
fmt_layout.addWidget(table_btn)
|
|
||||||
|
|
||||||
fmt_layout.addStretch()
|
|
||||||
|
|
||||||
layout.addWidget(self.format_toolbar)
|
|
||||||
|
|
||||||
# Stacked Widget für Preview/Edit
|
|
||||||
self.stack = QStackedWidget()
|
|
||||||
|
|
||||||
# Preview (gerendert)
|
|
||||||
self.preview = QTextEdit()
|
|
||||||
self.preview.setReadOnly(True)
|
|
||||||
self.stack.addWidget(self.preview)
|
|
||||||
|
|
||||||
# Editor (Quelltext)
|
|
||||||
self.editor = QPlainTextEdit()
|
|
||||||
font = QFont("Monospace", 10)
|
|
||||||
font.setStyleHint(QFont.StyleHint.Monospace)
|
|
||||||
self.editor.setFont(font)
|
|
||||||
self.stack.addWidget(self.editor)
|
|
||||||
|
|
||||||
layout.addWidget(self.stack, 1)
|
|
||||||
|
|
||||||
# Shortcuts für Editor
|
|
||||||
from PyQt6.QtGui import QShortcut, QKeySequence
|
|
||||||
self._bold_shortcut = QShortcut(QKeySequence("Ctrl+B"), self.editor)
|
|
||||||
self._bold_shortcut.activated.connect(lambda: self._wrap_selection("**", "**"))
|
|
||||||
self._italic_shortcut = QShortcut(QKeySequence("Ctrl+I"), self.editor)
|
|
||||||
self._italic_shortcut.activated.connect(lambda: self._wrap_selection("*", "*"))
|
|
||||||
|
|
||||||
def _render_markdown(self, content: str) -> str:
|
|
||||||
"""Rendert Markdown zu HTML mit Styling."""
|
|
||||||
html = markdown.markdown(
|
|
||||||
content,
|
|
||||||
extensions=['tables', 'fenced_code', 'codehilite', 'toc']
|
|
||||||
)
|
|
||||||
return f"<style>{self.MARKDOWN_CSS}</style>{html}"
|
|
||||||
|
|
||||||
def load_markdown(self, path: str) -> bool:
|
|
||||||
"""Lädt eine Markdown-Datei."""
|
|
||||||
self._current_path = path
|
|
||||||
try:
|
|
||||||
size = os.path.getsize(path)
|
|
||||||
if size > self.MAX_SIZE:
|
|
||||||
self.preview.setPlainText(f"Datei zu groß ({format_file_size(size)})")
|
|
||||||
return False
|
|
||||||
|
|
||||||
with open(path, 'r', encoding='utf-8', errors='replace') as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
self.preview.setHtml(self._render_markdown(content))
|
|
||||||
self.editor.setPlainText(content)
|
|
||||||
|
|
||||||
# Zurück zur Vorschau
|
|
||||||
self._edit_mode = False
|
|
||||||
self.format_toolbar.setVisible(False)
|
|
||||||
self.stack.setCurrentIndex(0)
|
|
||||||
self.edit_btn.setText("✏️ Bearbeiten")
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.preview.setPlainText(f"Fehler beim Laden: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _toggle_edit_mode(self):
|
|
||||||
"""Wechselt zwischen Vorschau und Bearbeiten."""
|
|
||||||
if self._edit_mode:
|
|
||||||
self._save_and_render()
|
|
||||||
else:
|
|
||||||
self._edit_mode = True
|
|
||||||
self.format_toolbar.setVisible(True)
|
|
||||||
self.stack.setCurrentIndex(1)
|
|
||||||
self.edit_btn.setText("💾 Speichern")
|
|
||||||
|
|
||||||
def _save_and_render(self):
|
|
||||||
"""Speichert die Änderungen und rendert neu."""
|
|
||||||
if not self._current_path:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
content = self.editor.toPlainText()
|
|
||||||
|
|
||||||
with open(self._current_path, 'w', encoding='utf-8') as f:
|
|
||||||
f.write(content)
|
|
||||||
|
|
||||||
self.preview.setHtml(self._render_markdown(content))
|
|
||||||
|
|
||||||
self._edit_mode = False
|
|
||||||
self.format_toolbar.setVisible(False)
|
|
||||||
self.stack.setCurrentIndex(0)
|
|
||||||
self.edit_btn.setText("✏️ Bearbeiten")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
QMessageBox.warning(self, "Fehler", f"Speichern fehlgeschlagen: {e}")
|
|
||||||
|
|
||||||
def _wrap_selection(self, prefix: str, suffix: str):
|
|
||||||
"""Umschließt die Auswahl mit Prefix und Suffix."""
|
|
||||||
cursor = self.editor.textCursor()
|
|
||||||
selected = cursor.selectedText()
|
|
||||||
if selected:
|
|
||||||
cursor.insertText(f"{prefix}{selected}{suffix}")
|
|
||||||
else:
|
|
||||||
pos = cursor.position()
|
|
||||||
cursor.insertText(f"{prefix}{suffix}")
|
|
||||||
cursor.setPosition(pos + len(prefix))
|
|
||||||
self.editor.setTextCursor(cursor)
|
|
||||||
|
|
||||||
def _insert_format(self, prefix: str, suffix: str):
|
|
||||||
"""Fügt Formatierung am Zeilenanfang ein."""
|
|
||||||
cursor = self.editor.textCursor()
|
|
||||||
selected = cursor.selectedText()
|
|
||||||
if selected:
|
|
||||||
# Jede Zeile formatieren
|
|
||||||
lines = selected.split('\u2029') # Paragraph separator
|
|
||||||
formatted = '\n'.join(f"{prefix}{line}{suffix}" for line in lines)
|
|
||||||
cursor.insertText(formatted)
|
|
||||||
else:
|
|
||||||
cursor.insertText(f"{prefix}{suffix}")
|
|
||||||
|
|
||||||
def _insert_codeblock(self):
|
|
||||||
"""Fügt einen Code-Block ein."""
|
|
||||||
cursor = self.editor.textCursor()
|
|
||||||
selected = cursor.selectedText()
|
|
||||||
if selected:
|
|
||||||
cursor.insertText(f"```\n{selected}\n```")
|
|
||||||
else:
|
|
||||||
cursor.insertText("```\n\n```")
|
|
||||||
cursor.movePosition(cursor.MoveOperation.Up)
|
|
||||||
self.editor.setTextCursor(cursor)
|
|
||||||
|
|
||||||
def _insert_link(self):
|
|
||||||
"""Fügt einen Link ein."""
|
|
||||||
cursor = self.editor.textCursor()
|
|
||||||
selected = cursor.selectedText()
|
|
||||||
if selected:
|
|
||||||
cursor.insertText(f"[{selected}](url)")
|
|
||||||
else:
|
|
||||||
cursor.insertText("[Text](url)")
|
|
||||||
|
|
||||||
def _insert_image(self):
|
|
||||||
"""Fügt ein Bild ein."""
|
|
||||||
cursor = self.editor.textCursor()
|
|
||||||
cursor.insertText("")
|
|
||||||
|
|
||||||
def _insert_table(self):
|
|
||||||
"""Fügt eine Tabelle ein."""
|
|
||||||
table = """| Spalte 1 | Spalte 2 | Spalte 3 |
|
|
||||||
|----------|----------|----------|
|
|
||||||
| Zelle 1 | Zelle 2 | Zelle 3 |
|
|
||||||
| Zelle 4 | Zelle 5 | Zelle 6 |"""
|
|
||||||
cursor = self.editor.textCursor()
|
|
||||||
cursor.insertText(table)
|
|
||||||
|
|
||||||
def clear(self):
|
|
||||||
"""Leert die Vorschau."""
|
|
||||||
self._current_path = ""
|
|
||||||
self.preview.clear()
|
|
||||||
self.editor.clear()
|
|
||||||
self._edit_mode = False
|
|
||||||
self.format_toolbar.setVisible(False)
|
|
||||||
self.stack.setCurrentIndex(0)
|
|
||||||
self.edit_btn.setText("✏️ Bearbeiten")
|
|
||||||
|
|
||||||
|
|
||||||
class PdfPreview(QWidget):
|
class PdfPreview(QWidget):
|
||||||
"""PDF-Vorschau mit nativem Qt PDF-Renderer und Bearbeitungsfunktionen."""
|
"""PDF-Vorschau mit nativem Qt PDF-Renderer und Einstellungen."""
|
||||||
|
|
||||||
# Zoom-Modi
|
# Zoom-Modi
|
||||||
ZOOM_MODES = [
|
ZOOM_MODES = [
|
||||||
|
|
@ -472,9 +136,9 @@ class PdfPreview(QWidget):
|
||||||
toolbar_layout.setSpacing(8)
|
toolbar_layout.setSpacing(8)
|
||||||
|
|
||||||
# Zoom-Modus
|
# Zoom-Modus
|
||||||
toolbar_layout.addWidget(QLabel("🔍 Zoom:"))
|
toolbar_layout.addWidget(QLabel("Zoom:"))
|
||||||
self.zoom_combo = QComboBox()
|
self.zoom_combo = QComboBox()
|
||||||
self.zoom_combo.setFixedWidth(130)
|
self.zoom_combo.setFixedWidth(120)
|
||||||
for key, label, _ in self.ZOOM_MODES:
|
for key, label, _ in self.ZOOM_MODES:
|
||||||
self.zoom_combo.addItem(label, key)
|
self.zoom_combo.addItem(label, key)
|
||||||
self.zoom_combo.currentIndexChanged.connect(self._on_zoom_mode_changed)
|
self.zoom_combo.currentIndexChanged.connect(self._on_zoom_mode_changed)
|
||||||
|
|
@ -493,9 +157,9 @@ class PdfPreview(QWidget):
|
||||||
toolbar_layout.addSpacing(16)
|
toolbar_layout.addSpacing(16)
|
||||||
|
|
||||||
# Seiten-Modus
|
# Seiten-Modus
|
||||||
toolbar_layout.addWidget(QLabel("📄 Ansicht:"))
|
toolbar_layout.addWidget(QLabel("Ansicht:"))
|
||||||
self.page_combo = QComboBox()
|
self.page_combo = QComboBox()
|
||||||
self.page_combo.setFixedWidth(140)
|
self.page_combo.setFixedWidth(120)
|
||||||
for key, label, _ in self.PAGE_MODES:
|
for key, label, _ in self.PAGE_MODES:
|
||||||
self.page_combo.addItem(label, key)
|
self.page_combo.addItem(label, key)
|
||||||
self.page_combo.currentIndexChanged.connect(self._on_page_mode_changed)
|
self.page_combo.currentIndexChanged.connect(self._on_page_mode_changed)
|
||||||
|
|
@ -503,35 +167,6 @@ class PdfPreview(QWidget):
|
||||||
|
|
||||||
toolbar_layout.addStretch()
|
toolbar_layout.addStretch()
|
||||||
|
|
||||||
# Schnell-Rotation Buttons
|
|
||||||
self.rotate_left_btn = QPushButton("↶")
|
|
||||||
self.rotate_left_btn.setToolTip("90° nach links drehen")
|
|
||||||
self.rotate_left_btn.setFixedSize(32, 28)
|
|
||||||
self.rotate_left_btn.clicked.connect(lambda: self._quick_rotate(270))
|
|
||||||
toolbar_layout.addWidget(self.rotate_left_btn)
|
|
||||||
|
|
||||||
self.rotate_right_btn = QPushButton("↷")
|
|
||||||
self.rotate_right_btn.setToolTip("90° nach rechts drehen")
|
|
||||||
self.rotate_right_btn.setFixedSize(32, 28)
|
|
||||||
self.rotate_right_btn.clicked.connect(lambda: self._quick_rotate(90))
|
|
||||||
toolbar_layout.addWidget(self.rotate_right_btn)
|
|
||||||
|
|
||||||
toolbar_layout.addSpacing(8)
|
|
||||||
|
|
||||||
# Komprimieren Button
|
|
||||||
self.compress_btn = QPushButton("📦")
|
|
||||||
self.compress_btn.setToolTip("PDF komprimieren")
|
|
||||||
self.compress_btn.setFixedSize(32, 28)
|
|
||||||
self.compress_btn.clicked.connect(self._compress_pdf)
|
|
||||||
toolbar_layout.addWidget(self.compress_btn)
|
|
||||||
|
|
||||||
# LibreOffice Draw Button (für Annotationen)
|
|
||||||
self.draw_btn = QPushButton("🖊️")
|
|
||||||
self.draw_btn.setToolTip("In LibreOffice Draw öffnen (Annotationen)")
|
|
||||||
self.draw_btn.setFixedSize(32, 28)
|
|
||||||
self.draw_btn.clicked.connect(self._open_in_draw)
|
|
||||||
toolbar_layout.addWidget(self.draw_btn)
|
|
||||||
|
|
||||||
layout.addWidget(toolbar)
|
layout.addWidget(toolbar)
|
||||||
|
|
||||||
# PDF Document und View
|
# PDF Document und View
|
||||||
|
|
@ -616,70 +251,11 @@ class PdfPreview(QWidget):
|
||||||
|
|
||||||
def load_pdf(self, path: str) -> bool:
|
def load_pdf(self, path: str) -> bool:
|
||||||
"""Lädt eine PDF-Datei."""
|
"""Lädt eine PDF-Datei."""
|
||||||
self._current_path = path
|
|
||||||
error = self.pdf_document.load(path)
|
error = self.pdf_document.load(path)
|
||||||
if error != QPdfDocument.Error.None_:
|
if error != QPdfDocument.Error.None_:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _open_in_draw(self):
|
|
||||||
"""Öffnet PDF in LibreOffice Draw für Annotationen."""
|
|
||||||
if not hasattr(self, '_current_path') or not self._current_path:
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
subprocess.Popen(['libreoffice', '--draw', self._current_path])
|
|
||||||
except Exception as e:
|
|
||||||
QMessageBox.warning(self, "Fehler", f"LibreOffice konnte nicht gestartet werden: {e}")
|
|
||||||
|
|
||||||
def _quick_rotate(self, degrees: int):
|
|
||||||
"""Dreht die PDF direkt und speichert."""
|
|
||||||
if not hasattr(self, '_current_path') or not self._current_path:
|
|
||||||
return
|
|
||||||
|
|
||||||
path = self._current_path
|
|
||||||
fd, temp_path = tempfile.mkstemp(suffix='.pdf')
|
|
||||||
os.close(fd)
|
|
||||||
|
|
||||||
try:
|
|
||||||
success, msg = pdf_tools.rotate_pages(path, temp_path, degrees)
|
|
||||||
if success:
|
|
||||||
self.pdf_document.close()
|
|
||||||
shutil.move(temp_path, path)
|
|
||||||
self.load_pdf(path)
|
|
||||||
else:
|
|
||||||
QMessageBox.warning(self, "Fehler", f"Rotation fehlgeschlagen: {msg}")
|
|
||||||
if os.path.exists(temp_path):
|
|
||||||
os.remove(temp_path)
|
|
||||||
except Exception as e:
|
|
||||||
QMessageBox.warning(self, "Fehler", f"Fehler bei Rotation: {e}")
|
|
||||||
if os.path.exists(temp_path):
|
|
||||||
os.remove(temp_path)
|
|
||||||
|
|
||||||
def _compress_pdf(self):
|
|
||||||
"""Komprimiert die PDF."""
|
|
||||||
if not hasattr(self, '_current_path') or not self._current_path:
|
|
||||||
return
|
|
||||||
|
|
||||||
path = self._current_path
|
|
||||||
fd, temp_path = tempfile.mkstemp(suffix='.pdf')
|
|
||||||
os.close(fd)
|
|
||||||
|
|
||||||
try:
|
|
||||||
success, msg = pdf_tools.compress_pdf(path, temp_path)
|
|
||||||
if success:
|
|
||||||
self.pdf_document.close()
|
|
||||||
shutil.move(temp_path, path)
|
|
||||||
self.load_pdf(path)
|
|
||||||
QMessageBox.information(self, "Komprimiert", msg)
|
|
||||||
else:
|
|
||||||
QMessageBox.warning(self, "Fehler", f"Komprimierung fehlgeschlagen: {msg}")
|
|
||||||
if os.path.exists(temp_path):
|
|
||||||
os.remove(temp_path)
|
|
||||||
except Exception as e:
|
|
||||||
QMessageBox.warning(self, "Fehler", f"Fehler bei Komprimierung: {e}")
|
|
||||||
if os.path.exists(temp_path):
|
|
||||||
os.remove(temp_path)
|
|
||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
"""Leert die Vorschau."""
|
"""Leert die Vorschau."""
|
||||||
self.pdf_document.close()
|
self.pdf_document.close()
|
||||||
|
|
@ -756,7 +332,6 @@ class PreviewPanel(QWidget):
|
||||||
delete_requested = pyqtSignal(str)
|
delete_requested = pyqtSignal(str)
|
||||||
open_external_requested = pyqtSignal(str)
|
open_external_requested = pyqtSignal(str)
|
||||||
detach_requested = pyqtSignal()
|
detach_requested = pyqtSignal()
|
||||||
content_changed = pyqtSignal(bool) # True = hat Inhalt, False = leer
|
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
@ -847,11 +422,7 @@ class PreviewPanel(QWidget):
|
||||||
self.pdf_preview = PdfPreview()
|
self.pdf_preview = PdfPreview()
|
||||||
self.preview_stack.addWidget(self.pdf_preview)
|
self.preview_stack.addWidget(self.pdf_preview)
|
||||||
|
|
||||||
# Markdown-Vorschau (Index 4)
|
# Keine Vorschau (Index 4)
|
||||||
self.markdown_preview = MarkdownPreview()
|
|
||||||
self.preview_stack.addWidget(self.markdown_preview)
|
|
||||||
|
|
||||||
# Keine Vorschau (Index 5)
|
|
||||||
self.no_preview = NoPreview()
|
self.no_preview = NoPreview()
|
||||||
self.no_preview.open_external.connect(self.open_external_requested.emit)
|
self.no_preview.open_external.connect(self.open_external_requested.emit)
|
||||||
self.preview_stack.addWidget(self.no_preview)
|
self.preview_stack.addWidget(self.no_preview)
|
||||||
|
|
@ -894,15 +465,9 @@ class PreviewPanel(QWidget):
|
||||||
elif file_type == 'pdf':
|
elif file_type == 'pdf':
|
||||||
self.pdf_preview.load_pdf(path)
|
self.pdf_preview.load_pdf(path)
|
||||||
self.preview_stack.setCurrentIndex(3)
|
self.preview_stack.setCurrentIndex(3)
|
||||||
elif file_type == 'markdown':
|
|
||||||
self.markdown_preview.load_markdown(path)
|
|
||||||
self.preview_stack.setCurrentIndex(4)
|
|
||||||
else:
|
else:
|
||||||
self.no_preview.set_file(path, name)
|
self.no_preview.set_file(path, name)
|
||||||
self.preview_stack.setCurrentIndex(5)
|
self.preview_stack.setCurrentIndex(4)
|
||||||
|
|
||||||
# Signal senden: Hat jetzt Inhalt
|
|
||||||
self.content_changed.emit(True)
|
|
||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
"""Leert die Vorschau."""
|
"""Leert die Vorschau."""
|
||||||
|
|
@ -915,12 +480,8 @@ class PreviewPanel(QWidget):
|
||||||
self.image_preview.clear()
|
self.image_preview.clear()
|
||||||
self.text_preview.clear()
|
self.text_preview.clear()
|
||||||
self.pdf_preview.clear()
|
self.pdf_preview.clear()
|
||||||
self.markdown_preview.clear()
|
|
||||||
self.preview_stack.setCurrentIndex(0)
|
self.preview_stack.setCurrentIndex(0)
|
||||||
|
|
||||||
# Signal senden: Ist jetzt leer
|
|
||||||
self.content_changed.emit(False)
|
|
||||||
|
|
||||||
def get_current_file(self) -> tuple:
|
def get_current_file(self) -> tuple:
|
||||||
"""Gibt den aktuellen Pfad und Namen zurück."""
|
"""Gibt den aktuellen Pfad und Namen zurück."""
|
||||||
return self._current_path, self._current_name
|
return self._current_path, self._current_name
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue