refactor: stop blocking UI when collecting files

This commit is contained in:
2026-08-10 17:44:25 +03:00
parent da13b7b38b
commit da7ae925f2
+84 -29
View File
@@ -9,12 +9,14 @@ Here's an example usage:
import pathlib
import random
from typing import override
from typing import cast, override
import typed_argparse as tap
from textual import work
from textual.app import App, ComposeResult
from textual.binding import BindingType
from textual.widgets import Footer, Header, Label
from textual.worker import Worker, WorkerState
from random_file_picker.core import IMG_EXTENSIONS, collect_files, move_file_to_move_folder, trash_file, upload_file
from random_file_picker.openers import (
@@ -69,6 +71,7 @@ class MenuApp(App[None]):
self.usable_files: list[pathlib.Path] = []
self.current_index: int = 0
self.current_file: pathlib.Path | None = None
self.is_loading: bool = False
@override
def compose(self) -> ComposeResult:
@@ -78,10 +81,77 @@ class MenuApp(App[None]):
yield Footer()
def on_mount(self) -> None:
self.gather_files()
if self.usable_files:
self.current_index = 0
self.display_current_file()
self.start_gathering_files()
@override
def check_action(self, action: str, parameters: tuple[object, ...]) -> bool | None:
"""Disable every action except quit while files are being gathered."""
if self.is_loading and action != "quit":
return None
return True
def start_gathering_files(self) -> None:
"""Start file collection in a background worker so the UI stays responsive."""
if not self.directory.exists() or not self.directory.is_dir():
self.query_one("#status", Label).update(f"❌ Directory '{self.directory}' not found")
return
self.is_loading = True
self.refresh_bindings()
self.query_one("#current-file", Label).update(f"🔍 Scanning '{self.directory}' for files, please wait...")
status = self.query_one("#status", Label)
status.update("")
status.loading = True
_ = self.gather_files_worker(
self.directory, self.file_limit, self.blacklist_patterns, self.whitelist_patterns,
recurse=self.recurse,
)
@work(thread=True, exclusive=True)
def gather_files_worker(
self,
directory: pathlib.Path,
file_limit: int,
blacklist_patterns: list[str],
whitelist_patterns: list[str],
*,
recurse: bool,
) -> list[pathlib.Path]:
"""Run file collection in a worker thread so a large/recursive scan doesn't block the UI."""
return collect_files(
search_path=directory,
search_subdirectories=recurse,
file_size_limit=file_limit,
blacklist_patterns=blacklist_patterns,
whitelist_patterns=whitelist_patterns,
)
def on_worker_state_changed(self, event: Worker.StateChanged) -> None:
"""Handle the completion of the background file scan, updating the UI with results or errors."""
worker = cast("Worker[list[pathlib.Path]]", event.worker)
if worker.name != "gather_files_worker":
return
status = self.query_one("#status", Label)
self.is_loading = False
status.loading = False
self.refresh_bindings()
if event.state == WorkerState.SUCCESS:
files = worker.result or []
self.usable_files = list(files)
random.shuffle(self.usable_files)
if not self.usable_files:
status.update("❌ No usable files found")
else:
status.update(f"✅ Found {len(self.usable_files)} files")
self.current_index = 0
self.display_current_file()
elif event.state == WorkerState.ERROR:
status.update(f"❌ Failed to scan directory: {worker.error}")
def assert_current_file_exists(self) -> bool:
"""Check if the current file exists and update the status label accordingly."""
@@ -102,28 +172,6 @@ class MenuApp(App[None]):
self.query_one("#current-file", Label).update(f"📄 {self.current_file} {file_count_str}")
self.open_file()
def gather_files(self) -> None:
"""Collect files from the directory based on filters."""
self.usable_files = []
if not self.directory.exists() or not self.directory.is_dir():
self.query_one("#status", Label).update(f"❌ Directory '{self.directory}' not found")
return
self.usable_files = collect_files(
search_path=self.directory,
search_subdirectories=self.recurse,
file_size_limit=self.file_limit,
blacklist_patterns=self.blacklist_patterns,
whitelist_patterns=self.whitelist_patterns,
)
if not self.usable_files:
self.query_one("#status", Label).update("❌ No usable files found")
else:
self.query_one("#status", Label).update(f"✅ Found {len(self.usable_files)} files")
random.shuffle(self.usable_files)
def open_file(self) -> None:
"""Open the current file - display images in terminal, play others externally."""
if not self.current_file:
@@ -159,7 +207,7 @@ class MenuApp(App[None]):
self.query_one("#status", Label).update(f"❌ Upload failed: {str(e)}")
def delete_file(self) -> None:
"""Delete the current file."""
"""Delete the current file (move to trash if enabled)."""
if not self.current_file or self.current_index >= len(self.usable_files):
return
@@ -210,7 +258,7 @@ class MenuApp(App[None]):
self.query_one("#status", Label).update(f"❌ Move failed: {str(e)}")
def open_in_folder(self) -> None:
"""Open the file's folder in file manager."""
"""Open the current file's folder in the file manager."""
if not self.current_file:
return
@@ -221,30 +269,37 @@ class MenuApp(App[None]):
self.query_one("#status", Label).update("📂 Opened file in folder")
def action_upload(self) -> None:
"""Upload the current file to Discord webhook."""
self.send_file()
def action_delete(self) -> None:
"""Delete the current file (move to trash if enabled)."""
self.delete_file()
def action_move_to_folder(self) -> None:
"""Move the current file to the move folder."""
self.move_to_move_folder()
def action_open_folder(self) -> None:
"""Open the current file's folder in the file manager."""
self.open_in_folder()
def action_next_file(self) -> None:
"""Display the next file."""
if not self.usable_files:
return
self.current_index = (self.current_index + 1) % len(self.usable_files)
self.display_current_file()
def action_prev_file(self) -> None:
"""Display the previous file."""
if not self.usable_files:
return
self.current_index = (self.current_index - 1) % len(self.usable_files)
self.display_current_file()
def action_reopen_file(self) -> None:
"""Reopen the current file."""
if not self.current_file:
return
self.open_file()