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 pathlib
|
||||||
import random
|
import random
|
||||||
from typing import override
|
from typing import cast, override
|
||||||
|
|
||||||
import typed_argparse as tap
|
import typed_argparse as tap
|
||||||
|
from textual import work
|
||||||
from textual.app import App, ComposeResult
|
from textual.app import App, ComposeResult
|
||||||
from textual.binding import BindingType
|
from textual.binding import BindingType
|
||||||
from textual.widgets import Footer, Header, Label
|
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.core import IMG_EXTENSIONS, collect_files, move_file_to_move_folder, trash_file, upload_file
|
||||||
from random_file_picker.openers import (
|
from random_file_picker.openers import (
|
||||||
@@ -69,6 +71,7 @@ class MenuApp(App[None]):
|
|||||||
self.usable_files: list[pathlib.Path] = []
|
self.usable_files: list[pathlib.Path] = []
|
||||||
self.current_index: int = 0
|
self.current_index: int = 0
|
||||||
self.current_file: pathlib.Path | None = None
|
self.current_file: pathlib.Path | None = None
|
||||||
|
self.is_loading: bool = False
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
@@ -78,11 +81,78 @@ class MenuApp(App[None]):
|
|||||||
yield Footer()
|
yield Footer()
|
||||||
|
|
||||||
def on_mount(self) -> None:
|
def on_mount(self) -> None:
|
||||||
self.gather_files()
|
self.start_gathering_files()
|
||||||
if self.usable_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.current_index = 0
|
||||||
self.display_current_file()
|
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:
|
def assert_current_file_exists(self) -> bool:
|
||||||
"""Check if the current file exists and update the status label accordingly."""
|
"""Check if the current file exists and update the status label accordingly."""
|
||||||
if not self.current_file or not self.current_file.exists():
|
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.query_one("#current-file", Label).update(f"📄 {self.current_file} {file_count_str}")
|
||||||
self.open_file()
|
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:
|
def open_file(self) -> None:
|
||||||
"""Open the current file - display images in terminal, play others externally."""
|
"""Open the current file - display images in terminal, play others externally."""
|
||||||
if not self.current_file:
|
if not self.current_file:
|
||||||
@@ -159,7 +207,7 @@ class MenuApp(App[None]):
|
|||||||
self.query_one("#status", Label).update(f"❌ Upload failed: {str(e)}")
|
self.query_one("#status", Label).update(f"❌ Upload failed: {str(e)}")
|
||||||
|
|
||||||
def delete_file(self) -> None:
|
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):
|
if not self.current_file or self.current_index >= len(self.usable_files):
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -210,7 +258,7 @@ class MenuApp(App[None]):
|
|||||||
self.query_one("#status", Label).update(f"❌ Move failed: {str(e)}")
|
self.query_one("#status", Label).update(f"❌ Move failed: {str(e)}")
|
||||||
|
|
||||||
def open_in_folder(self) -> None:
|
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:
|
if not self.current_file:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -221,30 +269,37 @@ class MenuApp(App[None]):
|
|||||||
self.query_one("#status", Label).update("📂 Opened file in folder")
|
self.query_one("#status", Label).update("📂 Opened file in folder")
|
||||||
|
|
||||||
def action_upload(self) -> None:
|
def action_upload(self) -> None:
|
||||||
|
"""Upload the current file to Discord webhook."""
|
||||||
self.send_file()
|
self.send_file()
|
||||||
|
|
||||||
def action_delete(self) -> None:
|
def action_delete(self) -> None:
|
||||||
|
"""Delete the current file (move to trash if enabled)."""
|
||||||
self.delete_file()
|
self.delete_file()
|
||||||
|
|
||||||
def action_move_to_folder(self) -> None:
|
def action_move_to_folder(self) -> None:
|
||||||
|
"""Move the current file to the move folder."""
|
||||||
self.move_to_move_folder()
|
self.move_to_move_folder()
|
||||||
|
|
||||||
def action_open_folder(self) -> None:
|
def action_open_folder(self) -> None:
|
||||||
|
"""Open the current file's folder in the file manager."""
|
||||||
self.open_in_folder()
|
self.open_in_folder()
|
||||||
|
|
||||||
def action_next_file(self) -> None:
|
def action_next_file(self) -> None:
|
||||||
|
"""Display the next file."""
|
||||||
if not self.usable_files:
|
if not self.usable_files:
|
||||||
return
|
return
|
||||||
self.current_index = (self.current_index + 1) % len(self.usable_files)
|
self.current_index = (self.current_index + 1) % len(self.usable_files)
|
||||||
self.display_current_file()
|
self.display_current_file()
|
||||||
|
|
||||||
def action_prev_file(self) -> None:
|
def action_prev_file(self) -> None:
|
||||||
|
"""Display the previous file."""
|
||||||
if not self.usable_files:
|
if not self.usable_files:
|
||||||
return
|
return
|
||||||
self.current_index = (self.current_index - 1) % len(self.usable_files)
|
self.current_index = (self.current_index - 1) % len(self.usable_files)
|
||||||
self.display_current_file()
|
self.display_current_file()
|
||||||
|
|
||||||
def action_reopen_file(self) -> None:
|
def action_reopen_file(self) -> None:
|
||||||
|
"""Reopen the current file."""
|
||||||
if not self.current_file:
|
if not self.current_file:
|
||||||
return
|
return
|
||||||
self.open_file()
|
self.open_file()
|
||||||
|
|||||||
Reference in New Issue
Block a user