Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion ExplorerPro.spec
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ icon_file = project_root / 'ExplorerPro.ico'

a = Analysis(
[str(src_dir / 'main.py')],
pathex=[str(src_dir)],
# project_root: translator.py liegt neben src/ (gui/batch_rename_dialog, diff_dialog, settings_dialog)
pathex=[str(src_dir), str(project_root)],
binaries=[],
datas=[
(str(icon_file), '.'),
(str(project_root / 'assets'), 'assets'),
(str(project_root / 'locales'), 'locales'),
(str(project_root / 'LICENSE'), '.'),
(str(project_root / 'THIRD_PARTY_LICENSES.txt'), '.'),
(str(project_root / 'PRIVACY_POLICY.md'), '.'),
Expand Down
6 changes: 5 additions & 1 deletion src/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,9 @@ def _init_file_index(self):
db_path = config_dir / "fileindex.db"
self.file_index = FileIndex(str(db_path))

# Index an Sidebar weitergeben
# Index an Sidebar und Metadaten-Panel (Tags/Notizen) weitergeben
self.sidebar.set_file_index(self.file_index)
self.preview_panel.metadata_panel.set_file_index(self.file_index)

logging.info("FileIndex initialisiert")

Expand Down Expand Up @@ -235,6 +236,9 @@ def closeEvent(self, event):
sp.search_worker.cancel()
sp.search_worker.wait(3000)

# Offene Tag-/Notiz-Eingaben sichern
self.preview_panel.metadata_panel.save_user_data()

# Einstellungen speichern
settings = QSettings()
settings.setValue("window/geometry", self.saveGeometry())
Expand Down
84 changes: 83 additions & 1 deletion src/core/file_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,9 +269,15 @@ def index_file(self, filepath: str, calculate_hash: bool = True) -> bool:
cursor = conn.cursor()

cursor.execute('''
INSERT OR REPLACE INTO files
INSERT INTO files
(path, filename, extension, size, modified, created, hash, category, text_content, indexed_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(path) DO UPDATE SET
filename = excluded.filename, extension = excluded.extension,
size = excluded.size, modified = excluded.modified,
created = excluded.created, hash = excluded.hash,
category = excluded.category, text_content = excluded.text_content,
indexed_at = excluded.indexed_at
''', (
filepath,
filename,
Expand Down Expand Up @@ -305,6 +311,82 @@ def remove_file(self, filepath: str) -> bool:
finally:
conn.close()

def _ensure_file_id(self, cursor, filepath: str) -> int:
"""Liefert die files.id eines Pfads und legt bei Bedarf einen Minimaleintrag an."""
filepath = os.path.normpath(filepath)
filename = os.path.basename(filepath)
cursor.execute(
'INSERT INTO files (path, filename, extension, category) VALUES (?, ?, ?, ?) '
'ON CONFLICT(path) DO NOTHING',
(filepath, filename, os.path.splitext(filename)[1].lower(), self.get_category(filename)),
)
cursor.execute('SELECT id FROM files WHERE path = ?', (filepath,))
return cursor.fetchone()[0]

def get_tags(self, filepath: str) -> List[str]:
"""Gibt die Tags einer Datei alphabetisch zurück."""
filepath = os.path.normpath(filepath) # Browser liefert Slash, os.walk Backslash
conn = sqlite3.connect(self.db_path)
try:
rows = conn.execute(
'SELECT t.name FROM tags t JOIN file_tags ft ON ft.tag_id = t.id '
'JOIN files f ON f.id = ft.file_id WHERE f.path = ? ORDER BY t.name',
(filepath,),
).fetchall()
return [r[0] for r in rows]
finally:
conn.close()

def set_tags(self, filepath: str, tags: List[str]) -> None:
"""Ersetzt die Tags einer Datei (leere Liste entfernt alle Tags)."""
clean = sorted({t.strip() for t in tags if t and t.strip()})
conn = sqlite3.connect(self.db_path)
try:
cursor = conn.cursor()
file_id = self._ensure_file_id(cursor, filepath)
cursor.execute('DELETE FROM file_tags WHERE file_id = ?', (file_id,))
for name in clean:
cursor.execute('INSERT OR IGNORE INTO tags (name) VALUES (?)', (name,))
cursor.execute(
'INSERT OR IGNORE INTO file_tags (file_id, tag_id) '
'SELECT ?, id FROM tags WHERE name = ?',
(file_id, name),
)
conn.commit()
finally:
conn.close()

def get_note(self, filepath: str) -> str:
"""Gibt die Notiz zu einer Datei zurück (leer, wenn keine existiert)."""
filepath = os.path.normpath(filepath)
conn = sqlite3.connect(self.db_path)
try:
row = conn.execute(
'SELECT n.content FROM notes n JOIN files f ON f.id = n.file_id WHERE f.path = ?',
(filepath,),
).fetchone()
return (row[0] or "") if row else ""
finally:
conn.close()

def set_note(self, filepath: str, content: str) -> None:
"""Speichert die Notiz zu einer Datei; leerer Text löscht sie."""
conn = sqlite3.connect(self.db_path)
try:
cursor = conn.cursor()
file_id = self._ensure_file_id(cursor, filepath)
if content.strip():
cursor.execute(
'INSERT INTO notes (file_id, content) VALUES (?, ?) ON CONFLICT(file_id) '
'DO UPDATE SET content = excluded.content, updated_at = CURRENT_TIMESTAMP',
(file_id, content),
)
else:
cursor.execute('DELETE FROM notes WHERE file_id = ?', (file_id,))
conn.commit()
finally:
conn.close()

def get_file(self, filepath: str) -> Optional[Dict]:
"""Gibt Informationen zu einer indizierten Datei zurück oder None"""
conn = sqlite3.connect(self.db_path)
Expand Down
12 changes: 10 additions & 2 deletions src/gui/browser/file_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,11 +334,11 @@ def _show_context_menu(self, pos):
menu.addAction(index_action)

meta_action = QAction("📊 Metadaten anzeigen", self)
meta_action.triggered.connect(lambda: self.file_selected.emit(file_path))
meta_action.triggered.connect(lambda: self._show_metadata(file_path))
menu.addAction(meta_action)

tags_action = QAction("🏷️ Tags bearbeiten", self)
tags_action.triggered.connect(lambda: self.file_selected.emit(file_path))
tags_action.triggered.connect(lambda: self._show_metadata(file_path, focus_tags=True))
menu.addAction(tags_action)

menu.addSeparator()
Expand Down Expand Up @@ -444,6 +444,14 @@ def _edit_file(self, path: str):
editor = QuickEditorDialog(path, self.window())
editor.exec()

def _show_metadata(self, path: str, focus_tags: bool = False):
"""Zeigt Metadaten/Tags im (ggf. ausgeblendeten) Vorschau-Panel des Hauptfensters."""
main_win = self.window()
if hasattr(main_win, "show_file_metadata"):
main_win.show_file_metadata(path, focus_tags=focus_tags)
else:
self.file_selected.emit(path)

def _show_checksums(self, path: str):
"""Öffnet den Prüfsummen-Dialog für eine Datei"""
if not os.path.isfile(path):
Expand Down
10 changes: 10 additions & 0 deletions src/gui/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,16 @@ def _toggle_preview(self):
"""Preview ein-/ausblenden"""
self.preview_panel.setVisible(self.toggle_preview.isChecked())

def show_file_metadata(self, path: str, focus_tags: bool = False):
"""Blendet das Vorschau-/Metadaten-Panel ein und zeigt die Datei dort an."""
self.toggle_preview.setChecked(True)
self.preview_panel.setVisible(True)
self.preview_panel.show_preview(path)
if focus_tags:
tags_edit = self.preview_panel.metadata_panel.tags_edit
tags_edit.setFocus()
tags_edit.selectAll()

def _go_home(self):
"""Zum Home-Verzeichnis"""
home = QStandardPaths.writableLocation(
Expand Down
34 changes: 34 additions & 0 deletions src/gui/preview/preview_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,8 +276,38 @@ class MetadataPanel(QWidget):

def __init__(self, parent=None):
super().__init__(parent)
self._current_path = None
self._file_index = None
self._loaded_user_data = ("", "")
self._setup_ui()

def set_file_index(self, file_index):
"""Verbindet das Panel mit dem Index, in dem Tags und Notizen gespeichert werden."""
self._file_index = file_index

def _user_data(self) -> tuple:
return (self.tags_edit.text().strip(), self.notes_edit.toPlainText())

def save_user_data(self):
"""Speichert geänderte Tags/Notizen der angezeigten Datei im Index."""
if not self._file_index or not self._current_path:
return
tags, notes = self._user_data()
if (tags, notes) == self._loaded_user_data:
return
self._file_index.set_tags(self._current_path, tags.split(","))
self._file_index.set_note(self._current_path, notes)
self._loaded_user_data = (tags, notes)

def _load_user_data(self, path: str):
tags, notes = "", ""
if self._file_index:
tags = ", ".join(self._file_index.get_tags(path))
notes = self._file_index.get_note(path)
self.tags_edit.setText(tags)
self.notes_edit.setPlainText(notes)
self._loaded_user_data = self._user_data()

def _setup_ui(self):
layout = QVBoxLayout(self)
layout.setContentsMargins(8, 8, 8, 8)
Expand Down Expand Up @@ -318,6 +348,7 @@ def _setup_ui(self):
self.tags_edit.setPlaceholderText("Tags hinzufügen (kommagetrennt)")
self.tags_edit.setAccessibleName("Metadaten-Tags")
self.tags_edit.setToolTip("Kommagetrennte Tags für die Datei eingeben")
self.tags_edit.editingFinished.connect(self.save_user_data)
tags_layout.addWidget(self.tags_edit)

layout.addWidget(tags_group)
Expand All @@ -339,6 +370,7 @@ def _setup_ui(self):

def clear_metadata(self):
"""Setzt die Metadaten-Anzeige vollständig zurück."""
self.save_user_data()
self.name_label.setText("-")
self.type_label.setText("-")
self.size_label.setText("-")
Expand All @@ -352,6 +384,7 @@ def clear_metadata(self):

def show_metadata(self, path: str):
"""Zeigt Metadaten einer Datei"""
self.save_user_data()
if not path or not os.path.exists(path):
self.clear_metadata()
return
Expand Down Expand Up @@ -402,6 +435,7 @@ def show_metadata(self, path: str):
self.created_label.setText("-")

self._current_path = path
self._load_user_data(path)
if hasattr(self, "checksum_btn"):
self.checksum_btn.setEnabled(os.path.isfile(path))

Expand Down
18 changes: 16 additions & 2 deletions src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@
if hasattr(sys.stderr, 'reconfigure'):
sys.stderr.reconfigure(encoding='utf-8')

# Pfad hinzufügen
# Pfad hinzufügen (src/ und Projektwurzel mit translator.py)
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

from PySide6.QtWidgets import QApplication
from PySide6.QtCore import Qt
from PySide6.QtCore import Qt, QTranslator, QLibraryInfo
from PySide6.QtGui import QIcon

from app import ExplorerProApp
Expand All @@ -49,6 +50,17 @@ def load_app_icon() -> QIcon:
return QIcon()


def install_qt_translations(app: QApplication, lang: str):
"""Lädt Qts eigene Übersetzung (qtbase_<lang>.qm), damit Standard-Buttons
wie Ja/Nein, Speichern/Verwerfen/Abbrechen in der UI-Sprache erscheinen."""
translator = QTranslator(app)
path = QLibraryInfo.path(QLibraryInfo.LibraryPath.TranslationsPath)
if translator.load(f"qtbase_{lang}", path):
app.installTranslator(translator)
return translator
return None


def main():
"""Haupteinstiegspunkt für ExplorerPro"""
# High DPI Support
Expand All @@ -59,6 +71,8 @@ def main():
app = QApplication(sys.argv)
app.setApplicationName("ExplorerPro")
app.setOrganizationName("ExplorerPro")
from translator import get_translator
install_qt_translations(app, get_translator().get_language())
app.setApplicationVersion("0.1.0")
icon = load_app_icon()
if not icon.isNull():
Expand Down
18 changes: 11 additions & 7 deletions src/modules/editor/quick_editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,10 @@ def _setup_ui(self):
self.editor.setAccessibleDescription(
"Mehrzeiliger Quelltext-Editor mit Zeilennummern und Syntax-Hervorhebung."
)
self.editor.textChanged.connect(self._on_text_changed)
# modificationChanged statt textChanged: der Syntax-Highlighter formatiert
# verzögert nach dem Laden und löst dabei textChanged aus, ohne den Text
# zu ändern -> sonst wäre jede frisch geöffnete Datei "ungespeichert".
self.editor.document().modificationChanged.connect(self._on_modification_changed)
splitter.addWidget(self.editor)

# Output Panel
Expand Down Expand Up @@ -401,6 +404,7 @@ def _load_file(self, filepath: str):
self.file_label.setText(path.name)
self.setWindowTitle(f"Quick Editor - {path.name}")

self.editor.document().setModified(False)
self._modified = False
self.modified_label.setText("")

Expand All @@ -427,6 +431,7 @@ def _save_file(self):
with open(self.filepath, 'w', encoding='utf-8') as f:
f.write(self.editor.toPlainText())

self.editor.document().setModified(False)
self._modified = False
self.modified_label.setText("")
self.file_label.setText(Path(self.filepath).name)
Expand Down Expand Up @@ -460,11 +465,10 @@ def _validate_file(self):
color = "#4EC9B0" if ok else "#F14C4C"
self._add_output(msg + "\n", color)

def _on_text_changed(self):
"""Handler für Textänderungen"""
if not self._modified:
self._modified = True
self.modified_label.setText("●")
def _on_modification_changed(self, modified: bool):
"""Handler für den Änderungsstatus des Dokuments"""
self._modified = modified
self.modified_label.setText("●" if modified else "")

def _update_cursor_position(self):
"""Aktualisiert die Cursor-Position in der Statusbar"""
Expand Down Expand Up @@ -590,7 +594,7 @@ def closeEvent(self, event):
if self._modified:
reply = QMessageBox.question(
self, "Ungespeicherte Änderungen",
"Es gibt ungespeicherte Änderungen.\nTrotzdem schließen?",
"Es gibt ungespeicherte Änderungen.\nMöchten Sie die Änderungen speichern?",
QMessageBox.StandardButton.Save |
QMessageBox.StandardButton.Discard |
QMessageBox.StandardButton.Cancel
Expand Down
Loading
Loading