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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.1.0/).
- **Ordner-Vorschau**: Verzeichnis wird im Hintergrund gelesen (Platzhalter bzw. letzter Stand sofort sichtbar).
- 10 neue Tests (`tests/test_gui_freeze.py`) messen die Blockadezeit der Ereignisschleife bei künstlich verzögerten Laufwerken.
- **Laufwerkswechsel ohne Hänger (de)**: Seitenleiste, Adressleiste, Tastatur und Favoriten wechseln über denselben Weg. Unbekannte Pfade werden im Hintergrund auf Erreichbarkeit geprüft (Hinweis „wird geladen …“, alte Ansicht bleibt bedienbar, nach 15 s „Keine Antwort – Laufwerk reagiert nicht“, neuere Navigation hat Vorrang). Die Startseite jedes Laufwerks wird nach dem Start im Hintergrund vorgeladen und vorgehalten (gedrosselt, lokale Platten zuerst, dann Wechsel- und Netzlaufwerke, mit Timeout); ein Klick darauf zeigt sie sofort.
- **Nicht erreichbares Laufwerk (de)**: Der Wechsel zeigt sofort eine leere Seite mit dem Ziel in der Adressleiste und dem Hinweis „wird geladen …“ bzw. „Laufwerk nicht erreichbar“ / „Keine Antwort – Laufwerk reagiert nicht“; der vorige Ordner bleibt nicht stehen und der Hinweis liegt nie über Einträgen. „Zurück“ funktioniert weiter, ein erneuter Klick prüft erneut. Bereits bekannte Wechsel-/Netzlaufwerke zeigen den vorgehaltenen Stand sofort und werden im Hintergrund nachgeprüft; schlägt das fehl, erscheint die leere Seite.

### Fix: GUI no longer freezes on slow drives (en)
- **Folder tree**: expanding a folder lists subfolders in the background. First time shows "loading …", afterwards the last known state at once; the fresh list replaces it only when complete. Answers for collapsed nodes are discarded; a folder silent for 15 s shows a hint instead of hanging.
Expand All @@ -22,6 +23,7 @@ Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.1.0/).
- **Folder preview**: directory is read in the background (placeholder or last state shown at once).
- 10 new tests (`tests/test_gui_freeze.py`) measure event-loop blocking with artificially delayed drives.
- **Drive switching without hangs (en)**: sidebar, address bar, keyboard and favorites share one path. Unknown paths are checked for reachability in the background ("loading …" hint, previous view stays usable, after 15 s "No response – drive is not answering", newer navigation wins). Each drive's start page is preloaded and kept ready in the background after startup (throttled, local disks first, then removable and network drives, with timeout); a click shows it at once.
- **Unreachable drive (en)**: switching shows an empty page at once with the target in the address bar and the hint "loading …" / "Drive not reachable" / "No response – drive is not answering"; the previous folder no longer stays and the hint never covers entries. Back keeps working, another click checks again. Known removable/network drives show the kept state at once and are re-checked in the background; if that fails the empty page appears.

### Native ZIP- & Archiv-Suite (2026-10-08, TW-EP-12)
- **Core-Archiv-Service (`src/core/archive_service.py`)**:
Expand Down
8 changes: 8 additions & 0 deletions locales/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -9438,5 +9438,13 @@
"zh": "无响应 – 驱动器未应答",
"ja": "応答がありません – ドライブが応答しません",
"ru": "Нет ответа – диск не отвечает"
},
"Laufwerk nicht erreichbar": {
"de": "Laufwerk nicht erreichbar",
"en": "Drive not reachable",
"es": "Unidad no accesible",
"zh": "驱动器无法访问",
"ja": "ドライブにアクセスできません",
"ru": "Диск недоступен"
}
}
4 changes: 2 additions & 2 deletions releases/windowsstore/v1.0.9/release-notes.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"de-de": "ExplorerPro 1.0.9\n• Die Oberfläche friert bei langsamen Laufwerken (Netzwerk, Google Drive, OneDrive-Platzhalter, leere Kartenleser) nicht mehr ein: Ordner im Baum und die Ordner-Vorschau laden im Hintergrund.\n• Es erscheint sofort der zuletzt bekannte Stand (auch die Speicherbelegung beim Start); neue Werte ersetzen ihn, sobald sie vollständig vorliegen.\n• Die Speicherbelegung wird seltener automatisch geprüft; die Schaltfläche aktualisiert auf Wunsch.\n• Der Wechsel zwischen Laufwerken hängt nicht mehr: Die Startseite jedes Laufwerks wird im Hintergrund vorgeladen und beim Klick sofort angezeigt.\n• Neu: ZIP-Archive erstellen, durchsuchen, auf Integrität prüfen und sicher entpacken.",
"en-us": "ExplorerPro 1.0.9\n• The window no longer freezes on slow drives (network, Google Drive, OneDrive placeholders, empty card readers): folders in the tree and the folder preview load in the background.\n• The last known state (including drive capacity at startup) is shown immediately; new values replace it once they are complete.\n• Drive capacity is checked automatically less often; the button refreshes on demand.\n• Switching between drives no longer hangs: each drive's start page is preloaded in the background and shown instantly on click.\n• New: create, browse, verify and safely extract ZIP archives."
"de-de": "ExplorerPro 1.0.9\n• Die Oberfläche friert bei langsamen Laufwerken (Netzwerk, Google Drive, OneDrive-Platzhalter, leere Kartenleser) nicht mehr ein: Ordner im Baum und die Ordner-Vorschau laden im Hintergrund.\n• Es erscheint sofort der zuletzt bekannte Stand (auch die Speicherbelegung beim Start); neue Werte ersetzen ihn, sobald sie vollständig vorliegen.\n• Die Speicherbelegung wird seltener automatisch geprüft; die Schaltfläche aktualisiert auf Wunsch.\n• Der Wechsel zwischen Laufwerken hängt nicht mehr: Die Startseite jedes Laufwerks wird im Hintergrund vorgeladen und beim Klick sofort angezeigt.\n• Ist ein Laufwerk nicht erreichbar, zeigt ExplorerPro sofort eine leere Seite mit Hinweis statt des vorigen Ordners.\n• Neu: ZIP-Archive erstellen, durchsuchen, auf Integrität prüfen und sicher entpacken.",
"en-us": "ExplorerPro 1.0.9\n• The window no longer freezes on slow drives (network, Google Drive, OneDrive placeholders, empty card readers): folders in the tree and the folder preview load in the background.\n• The last known state (including drive capacity at startup) is shown immediately; new values replace it once they are complete.\n• Drive capacity is checked automatically less often; the button refreshes on demand.\n• Switching between drives no longer hangs: each drive's start page is preloaded in the background and shown instantly on click.\n• If a drive is not reachable, ExplorerPro shows an empty page with a hint at once instead of the previous folder.\n• New: create, browse, verify and safely extract ZIP archives."
}
126 changes: 89 additions & 37 deletions src/gui/browser/file_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,11 +163,16 @@ def __init__(self, parent=None):
self._preload_queue = []
self._preload_busy = False
self._preload_seq = 0
self._pending_path = None
self._nav_record = True
self._fs = AsyncFs(self)
self._fs.finished.connect(self._on_fs_result)
self._setup_ui()
self._busy = QLabel(t("wird geladen …"), self.table.viewport())
# Status text for an EMPTY page only; it is hidden as soon as rows exist.
self._busy = QLabel(t("wird geladen …"), self)
self._busy.setAlignment(Qt.AlignmentFlag.AlignCenter)
self._busy.hide()
self.proxy.rowsInserted.connect(self._on_rows_inserted)

# Startverzeichnis (Einstellung "Startordner", sonst Benutzerordner)
self._navigate_sync(self.default_start_path())
Expand Down Expand Up @@ -315,26 +320,83 @@ def navigate_to(self, path: str):
"""Navigiert zu einem Pfad; unbekannte Pfade werden im Hintergrund geprüft."""
if not path:
return
path = normalize_user_path(path)
self._switch(normalize_user_path(path), True)

def _switch(self, path: str, record: bool):
if _norm(path) in self._known_dirs:
self._go(path)
self._go(path, record) # last known state at once ...
if self.ASYNC_VALIDATE and drive_kind(path) >= 1:
# ... but removable/network drives are re-checked in the background.
self._nav_token += 1
self._submit(self._nav_token, "recheck", path, _classify, self.NAV_TIMEOUT_MS)
elif not self.ASYNC_VALIDATE:
self._navigate_sync(path)
if record:
self._navigate_sync(path)
else:
self._go(path, False)
else:
self._nav_token += 1
self._set_busy(t("wird geladen …"))
self._nav_record = record
self._enter_pending(path, record)
self._submit(self._nav_token, "nav", path, _classify, self.NAV_TIMEOUT_MS)

def _set_busy(self, text):
if text:
self._busy.setText(text)
self._busy.adjustSize()
self._busy.move(12, 40)
self._center_busy()
self._busy.show()
self._busy.raise_()
else:
self._busy.hide()

def _center_busy(self):
area = self.table.geometry()
self._busy.move(area.x() + max(0, (area.width() - self._busy.width()) // 2),
area.y() + max(0, (area.height() - self._busy.height()) // 3))

def resizeEvent(self, event):
super().resizeEvent(event)
self._center_busy()

def _on_rows_inserted(self, parent, *_):
if not self.table.isHidden() and parent == self.table.rootIndex():
self._busy.hide() # never a status text over entries

def _push_history(self, path: str):
if self._current_path and self._current_path != path:
self._history = self._history[:self._history_index + 1]
self._history.append(path)
self._history_index = len(self._history) - 1
elif not self._history:
self._history.append(path)
self._history_index = 0

def _enter_pending(self, path: str, record: bool):
"""Show the target at once as an EMPTY page (no old entries) until it is confirmed."""
if record:
self._push_history(path)
self._current_path = path
self._pending_path = path
self._file_count = 0
self.table.hide() # empty page: no entries at all, only the status text
self._set_busy(t("wird geladen …"))
self.folder_changed.emit(path)
self.path_changed.emit(path)

def _show_unreachable(self, path: str, text=None):
"""The empty page stays; history/back keep working and another click checks again."""
self._forget(path)
self._set_busy(text or t("Laufwerk nicht erreichbar"))
if not self.table.isHidden():
self._enter_pending(path, False)
self._set_busy(text or t("Laufwerk nicht erreichbar"))

def _forget(self, path: str):
"""The path (and everything below it) is no longer trusted; other drives stay known."""
key = _norm(path)
self._known_dirs = {k for k in self._known_dirs if k != key and not k.startswith(key.rstrip(r"\/") + os.sep)}

def _submit(self, token, kind, path, fn, timeout_ms):
self._tokens[token] = (kind, path)
self._fs.submit(token, fn, path)
Expand All @@ -354,16 +416,20 @@ def _on_fs_result(self, token, result, error):
self.model.fetchMore(index) # warm the model for an instant first view
QTimer.singleShot(self.PRELOAD_GAP_MS, self._preload_next)
return
if kind == "recheck":
if (error is not None or result is None) and token == self._nav_token \
and _norm(self._current_path) == _norm(path):
self._show_unreachable(path) # the shown last state was stale
return
if error is not None or result is None:
if token == self._nav_token:
self._set_busy(None)
self._show_unreachable(path)
return
folder = path if result == "dir" else os.path.dirname(path)
self._known_dirs.add(_norm(folder))
if token != self._nav_token: # the user went elsewhere meanwhile
return
self._set_busy(None)
self._go(folder)
self._go(folder, self._nav_record)
if result == "file":
self.file_selected.emit(path)

Expand All @@ -375,7 +441,7 @@ def _on_timeout(self, token):
self._preload_busy = False
QTimer.singleShot(self.PRELOAD_GAP_MS, self._preload_next)
elif token == self._nav_token:
self._set_busy(t("Keine Antwort – Laufwerk reagiert nicht"))
self._show_unreachable(entry[1], t("Keine Antwort – Laufwerk reagiert nicht"))

def preload_roots(self, paths):
"""Warm the start page of each drive in the background: local disks first, one at a time."""
Expand Down Expand Up @@ -406,22 +472,22 @@ def _navigate_sync(self, path: str):
return
self._go(path)

def _go(self, path: str):
def _go(self, path: str, record: bool = True):
"""Switch the view; callers have established that *path* is a reachable folder."""
for folder in (Path(path), *Path(path).parents): # ancestors exist too: "up" is instant
self._known_dirs.add(_norm(str(folder)))

# History aktualisieren
if self._current_path and self._current_path != path:
# Vorwärts-History löschen
self._history = self._history[:self._history_index + 1]
self._history.append(path)
self._history_index = len(self._history) - 1
elif not self._history:
self._history.append(path)
self._history_index = 0
if record:
pending = self._pending_path
if pending and self._history and self._history[self._history_index] == pending and path != pending:
self._history[self._history_index] = path # e.g. a file path resolved to its folder
self._current_path = path
else:
self._push_history(path)

self._current_path = path
self._pending_path = None
self.table.show()

# QFileSystemModel loads directories asynchronously. index(path) alone
# can point at a valid directory while its rows remain empty forever.
Expand All @@ -430,7 +496,7 @@ def _go(self, path: str):
self.table.setRootIndex(proxy_index)

self._update_file_count()
# Nothing cached yet: say so until the (asynchronous) listing arrives.
# Nothing cached yet: say so (on the empty page) until the listing arrives.
self._set_busy(t("wird geladen …") if self.model.rowCount(source_index) == 0 else None)

# Signale senden
Expand All @@ -441,27 +507,13 @@ def go_back(self):
"""Geht einen Schritt zurück"""
if self._history_index > 0:
self._history_index -= 1
path = self._history[self._history_index]
self._current_path = path
source_index = self.model.setRootPath(path)
proxy_index = self.proxy.mapFromSource(source_index)
self.table.setRootIndex(proxy_index)
self._update_file_count()
self.folder_changed.emit(path)
self.path_changed.emit(path)
self._switch(self._history[self._history_index], False)

def go_forward(self):
"""Geht einen Schritt vorwärts"""
if self._history_index < len(self._history) - 1:
self._history_index += 1
path = self._history[self._history_index]
self._current_path = path
source_index = self.model.setRootPath(path)
proxy_index = self.proxy.mapFromSource(source_index)
self.table.setRootIndex(proxy_index)
self._update_file_count()
self.folder_changed.emit(path)
self.path_changed.emit(path)
self._switch(self._history[self._history_index], False)

def go_up(self):
"""Geht zum übergeordneten Ordner"""
Expand Down
Loading
Loading