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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.1.0/).

## [Unreleased]

### Review & Fehlerbehebung: Cloud-Ordner, Vorschau, Datenschutz, Sprache (2026-10-06)
- **Cloud-Ordner sichtbar:** Unter Windows blendete die Dateiliste OneDrive-/Cloud-Platzhalter (Reparse-Points) und Verknüpfungen (`*.lnk`) aus; der Filter enthält jetzt `QDir.System`. Neue Seitenleisten-Gruppe **Cloud-Speicher** (OneDrive privat/geschäftlich, Dropbox, Google Drive, iCloud, Nextcloud u. a.; Windows-SyncRootManager nur lesend). Schnellzugriff-Einträge sind aufklappbar.
- **Ordnernamen statt „Ordner“:** Typ- und Datumsspalte nutzten `ResizeToContents` und konnten die Namensspalte auf wenige Pixel zusammendrücken. Feste, verstellbare Breiten; die Namensspalte behält mindestens 160 px, lange Namen werden mittig gekürzt.
- **Absturz/Hänger bei Klick auf Datei:** Vorschau fängt jeden Fehler ab, lädt keine Cloud-Platzhalter („nur online“) mehr herunter, beachtet die maximale Vorschaugröße und die Vorschau-Schalter, dekodiert Bilder verkleinert (inkl. EXIF-Drehung), rendert PDFs speicherbegrenzt und kopiert den Pixelpuffer. Windows-Verknüpfungen werden mit einem nativen Parser statt per PowerShell-Prozess aufgelöst. Dateizählung blockiert den GUI-Thread nicht mehr. Native Abstürze und unbehandelte Ausnahmen werden lokal in `~/.explorerpro/logs/crash.log` protokolliert.
- **Datenschutz-Ampel bedienbar:** Klick öffnet die Datenschutz-Einstellungen, Rechtsklick ein Schnellmenü (Überwachung ein/aus, Ampel zurücksetzen). Neuer Dialog mit editierbarer **Blacklist** und **Whitelist** (Hinzufügen, Bearbeiten, Filtern, Import/Export TXT/CSV/XLSX) und Testbereich. Die Überwachung lässt sich wieder einschalten, nachdem sie gestoppt wurde.
- **Spracheinstellung wirkt:** Ein Laufzeit-Übersetzer übersetzt alle Fenster, Menüs, Tooltips und Barrierefreiheitstexte und schaltet beim Speichern der Einstellungen live um. Dynamische Meldungen nutzen `t()`-Vorlagen; der Katalog wurde um die fehlenden Texte in allen sechs Sprachen ergänzt. Zur Laufzeit wird `translations.json` nicht mehr beschrieben (im Store-Paket schreibgeschützt).
- **Einstellungen werden angewendet:** Startordner, versteckte Dateien, Farbschema, Schriftgröße (0 = Systemstandard), Symbolgröße, Vorschau-Optionen, Überwachung und Hinweisdialoge sowie „Fenstergröße merken“. Indizierung beachtet maximale Dateigröße und Ausschlussmuster und liest keine Cloud-Platzhalter.

### Übersetzte Hilfetexte und gespeicherte Sprache (2026-10-01)
- 86 bislang fehlende Hilfetexte, Beschriftungen und Fehlermeldungen im Katalog ergänzt; Hilfetexte über die Übersetzungsfunktion angebunden (DE, EN, ES, ZH, JA, RU).
- Die Sprachauswahl erklärt in allen sechs Sprachen, dass Sprachänderungen beim nächsten Start übernommen werden.
Expand Down
5,400 changes: 5,400 additions & 0 deletions locales/translations.json

Large diffs are not rendered by default.

59 changes: 42 additions & 17 deletions src/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
from gui.main_window import MainWindow
from modules.privacy.privacy_monitor import PrivacyMonitor
from core.file_index import FileIndex
from core.settings_manager import SettingsManager
from translator import t

logging.basicConfig(level=logging.INFO)

Expand Down Expand Up @@ -42,6 +44,7 @@ def __init__(self):
# Einstellungen & Verbindungen
self._load_settings()
self._setup_connections()
self._apply_settings(startup=True)

logging.info("ExplorerPro gestartet")

Expand All @@ -57,8 +60,11 @@ def _init_privacy_monitor(self):
self.privacy_monitor.warning.connect(self._on_privacy_warning)
self.privacy_monitor.alert.connect(self._on_privacy_alert)

# Monitor starten
self.privacy_monitor.start()
# Monitor starten (sofern in den Einstellungen nicht deaktiviert)
if SettingsManager.instance().get("privacy", "enable_clipboard_monitor", True):
self.privacy_monitor.start()
else:
self.privacy_monitor.stop()
logging.info("PrivacyMonitor initialisiert")

def _init_file_index(self):
Expand All @@ -79,7 +85,8 @@ def _load_settings(self):
settings = QSettings()

# Fenstergeometrie
geometry = settings.value("window/geometry")
remember = SettingsManager.instance().get("general", "remember_window_size", True)
geometry = settings.value("window/geometry") if remember else None
if geometry:
self.restoreGeometry(geometry)
else:
Expand Down Expand Up @@ -118,6 +125,7 @@ def _setup_connections(self):

# Browser -> Preview
self.file_browser.file_selected.connect(self.preview_panel.show_preview)
self.file_browser.path_changed.connect(lambda _path: self.preview_panel.clear_preview())

# Browser -> StatusBar
self.file_browser.path_changed.connect(self.status_widget.update_path)
Expand Down Expand Up @@ -149,40 +157,52 @@ def _on_search(self, query: str):

if results:
self.statusBar().showMessage(
f"{len(results)} Treffer für: {query}", 5000
t("{count} Treffer für: {query}").format(count=len(results), query=query), 5000
)
# Ergebnisse im SearchPanel anzeigen
self.sidebar.search_panel.show_results(results)
# Zum Such-Tab wechseln
self.sidebar.switch_to_search()
else:
self.statusBar().showMessage(f"Keine Treffer für: {query}", 3000)
self.statusBar().showMessage(t("Keine Treffer für: {query}").format(query=query), 3000)

def _on_privacy_warning(self, message: str):
"""Handler für Datenschutz-Warnungen"""
self.statusBar().showMessage(f"⚠️ {message}", 5000)

def _on_privacy_alert(self, alert):
"""Handler für Datenschutz-Alerts"""
if alert.status.value == 'red':
QMessageBox.warning(
self,
"Datenschutz-Warnung",
f"{alert.message}\n\nErkannt: {', '.join(alert.detected_patterns)}"
)
if alert.status.value != 'red':
return
if not SettingsManager.instance().get("privacy", "show_notifications", True):
return
box = QMessageBox(self)
box.setIcon(QMessageBox.Icon.Warning)
box.setWindowTitle(t("Datenschutz-Warnung"))
box.setText(alert.message)
box.setInformativeText(
t("Erkannt: {items}").format(items=", ".join(alert.detected_patterns))
)
box.setStandardButtons(QMessageBox.StandardButton.Ok)
settings_btn = box.addButton(t("Einstellungen..."), QMessageBox.ButtonRole.ActionRole)
box.exec()
if box.clickedButton() is settings_btn:
self._show_privacy_settings()

def _on_app_launched(self, path: str):
"""Handler für gestartete Apps"""
self.statusBar().showMessage(f"🚀 App gestartet: {Path(path).name}", 3000)
self.statusBar().showMessage("🚀 " + t("App gestartet: {name}").format(name=Path(path).name), 3000)

def _on_prompt_copied(self, content: str):
"""Handler für kopierte Prompts"""
preview = content[:50] + "..." if len(content) > 50 else content
self.statusBar().showMessage(f"📋 Prompt kopiert: {preview}", 3000)
self.statusBar().showMessage("📋 " + t("Prompt kopiert: {text}").format(text=preview), 3000)

def _on_sync_finished(self, count: int):
"""Handler für abgeschlossene Synchronisation"""
self.statusBar().showMessage(f"🔄 Synchronisation abgeschlossen: {count} Dateien", 5000)
self.statusBar().showMessage(
"🔄 " + t("Synchronisation abgeschlossen: {count} Dateien").format(count=count), 5000
)

def index_current_folder(self):
"""Indiziert den aktuellen Ordner"""
Expand All @@ -195,10 +215,14 @@ def index_current_folder(self):
# Worker starten
self.index_worker = IndexWorker(self.file_index, current_path)
self.index_worker.progress.connect(
lambda c, t: self.statusBar().showMessage(f"Indiziere: {c}/{t}")
lambda current, total: self.statusBar().showMessage(
t("Indiziere: {current}/{total}").format(current=current, total=total)
)
)
self.index_worker.finished_indexing.connect(
lambda n: self.statusBar().showMessage(f"✅ {n} Dateien indiziert", 5000)
lambda n: self.statusBar().showMessage(
"✅ " + t("{count} Dateien indiziert").format(count=n), 5000
)
)
self.index_worker.start()

Expand Down Expand Up @@ -251,7 +275,8 @@ def closeEvent(self, event):

# Einstellungen speichern
settings = QSettings()
settings.setValue("window/geometry", self.saveGeometry())
if SettingsManager.instance().get("general", "remember_window_size", True):
settings.setValue("window/geometry", self.saveGeometry())
for key, splitter in (
("splitter/main", self.main_splitter),
("splitter/right", self.right_splitter),
Expand Down
78 changes: 78 additions & 0 deletions src/core/appearance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
appearance — wendet Farbschema und Schriftgröße auf die QApplication an.

* ``system``: folgt dem Hell/Dunkel-Modus des Betriebssystems
* ``light`` / ``dark``: erzwingt das jeweilige Schema
* Schriftgröße ``0`` bedeutet Systemstandard
"""
from __future__ import annotations

from PySide6.QtCore import Qt
from PySide6.QtGui import QColor, QPalette
from PySide6.QtWidgets import QApplication

THEMES = ("system", "light", "dark")


def _dark_palette() -> QPalette:
palette = QPalette()
window = QColor(45, 45, 48)
base = QColor(30, 30, 30)
text = QColor(230, 230, 230)
disabled = QColor(130, 130, 130)
highlight = QColor(0, 120, 215)
palette.setColor(QPalette.ColorRole.Window, window)
palette.setColor(QPalette.ColorRole.WindowText, text)
palette.setColor(QPalette.ColorRole.Base, base)
palette.setColor(QPalette.ColorRole.AlternateBase, QColor(40, 40, 42))
palette.setColor(QPalette.ColorRole.ToolTipBase, QColor(60, 60, 64))
palette.setColor(QPalette.ColorRole.ToolTipText, text)
palette.setColor(QPalette.ColorRole.Text, text)
palette.setColor(QPalette.ColorRole.Button, window)
palette.setColor(QPalette.ColorRole.ButtonText, text)
palette.setColor(QPalette.ColorRole.BrightText, QColor(255, 80, 80))
palette.setColor(QPalette.ColorRole.Link, QColor(80, 160, 255))
palette.setColor(QPalette.ColorRole.Highlight, highlight)
palette.setColor(QPalette.ColorRole.HighlightedText, QColor(255, 255, 255))
palette.setColor(QPalette.ColorRole.PlaceholderText, disabled)
for role in (QPalette.ColorRole.WindowText, QPalette.ColorRole.Text, QPalette.ColorRole.ButtonText):
palette.setColor(QPalette.ColorGroup.Disabled, role, disabled)
return palette


def apply_theme(app: QApplication, theme: str) -> None:
"""Setzt das Farbschema; unbekannte Werte gelten als ``system``."""
theme = theme if theme in THEMES else "system"
hints = app.styleHints()
if hasattr(hints, "setColorScheme"): # Qt >= 6.8
scheme = {
"system": Qt.ColorScheme.Unknown,
"light": Qt.ColorScheme.Light,
"dark": Qt.ColorScheme.Dark,
}[theme]
hints.setColorScheme(scheme)
app.setPalette(app.style().standardPalette())
return
if theme == "dark":
app.setPalette(_dark_palette())
else:
app.setPalette(app.style().standardPalette())


def apply_font_size(app: QApplication, point_size: int) -> None:
"""Setzt die Basisschriftgröße; 0 stellt die ursprüngliche Größe wieder her."""
original = getattr(app, "_ep_original_point_size", None)
font = app.font()
if original is None:
original = font.pointSizeF()
app._ep_original_point_size = original
try:
size = float(point_size)
except (TypeError, ValueError):
size = 0
target = size if 6 <= size <= 32 else original
if target > 0 and abs(font.pointSizeF() - target) > 0.01:
font.setPointSizeF(target)
app.setFont(font)
Loading
Loading