refactor: stop blocking UI when collecting files
This commit is contained in:
+82
-27
@@ -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,11 +81,78 @@ class MenuApp(App[None]):
|
||||
yield Footer()
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self.gather_files()
|
||||
if self.usable_files:
|
||||
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."""
|
||||
if not self.current_file or not self.current_file.exists():
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user