Skip to content

Commit 6e7123a

Browse files
committed
Adding a directory view.
The directory view allows a more obvious browsing of the tree. For the very top of the directory tree we dont auto-scan for media, once you get down to 4 levels it will start scanning. We have also made a config.json file to store configurations of things like the auto_scan_threashold, along with adding a list of folders that need to be ignored. Signed-off-by: Sam.Richards@taurich.org <Sam.Richards@taurich.org>
1 parent 0a683ca commit 6e7123a

4 files changed

Lines changed: 874 additions & 19 deletions

File tree

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{
2+
"extensions": [
3+
".mov",
4+
".mp4",
5+
".mkv",
6+
".exr",
7+
".jpg",
8+
".jpeg",
9+
".png",
10+
".dpx",
11+
".tiff",
12+
".tif",
13+
".wav",
14+
".mp3",
15+
".pdf"
16+
],
17+
"ignore_dirs": [
18+
".git",
19+
".quarantine",
20+
"eryx_unreal_plugin",
21+
".DS_Store"
22+
],
23+
"root_ignore_dirs": [
24+
"/Applications",
25+
"/bin",
26+
"/cores",
27+
"/dev",
28+
"/etc",
29+
"/Library",
30+
"/opt",
31+
"/private",
32+
"/sbin",
33+
"/System",
34+
"/usr",
35+
"/var",
36+
"/proc",
37+
"/sys",
38+
"/snap"
39+
],
40+
"max_recursion_depth": 6,
41+
"auto_scan_threshold": 4
42+
}

src/plugin/python_plugins/filesystem_browser/filesystem_browser.py

Lines changed: 206 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ def __init__(self, connection):
3636
"Filesystem Browser",
3737
qml_folder="qml/FilesystemBrowser.1"
3838
)
39-
39+
# Load Configuration
40+
self.config = self.load_config()
4041

4142
# self.main_executor = MainThreadExecutor()
4243

@@ -161,14 +162,40 @@ def __init__(self, connection):
161162
)
162163
self.scanned_dirs_attr.expose_in_ui_attrs_group("Filesystem Browser")
163164

164-
# New: Recursion limit attribute
165+
# New: Directory Query Result (for Tree View)
166+
self.directory_query_result = self.add_attribute(
167+
"directory_query_result",
168+
"{}",
169+
{"title": "directory_query_result"},
170+
register_as_preference=False
171+
)
172+
self.directory_query_result.expose_in_ui_attrs_group("Filesystem Browser")
173+
165174
self.depth_limit_attr = self.add_attribute(
166175
"recursion_limit",
167-
6,
176+
self.config.get("max_recursion_depth", 6),
168177
{"title": "Recursion Limit"},
169178
register_as_preference=True
170179
)
171180
self.depth_limit_attr.expose_in_ui_attrs_group("Filesystem Browser")
181+
182+
# New: Scan Required flag (for manual scan mode)
183+
self.scan_required_attr = self.add_attribute(
184+
"scan_required",
185+
False,
186+
{"title": "scan_required"},
187+
register_as_preference=False
188+
)
189+
self.scan_required_attr.expose_in_ui_attrs_group("Filesystem Browser")
190+
191+
# Auto-scan threshold (read-only for UI logic)
192+
self.auto_scan_threshold_attr = self.add_attribute(
193+
"auto_scan_threshold",
194+
self.config.get("auto_scan_threshold", 4),
195+
{"title": "auto_scan_threshold"},
196+
register_as_preference=False
197+
)
198+
self.auto_scan_threshold_attr.expose_in_ui_attrs_group("Filesystem Browser")
172199

173200
# New: Filter attributes
174201
self.filter_time_attr = self.add_attribute(
@@ -277,8 +304,10 @@ def __init__(self, connection):
277304
# attribute_changed method handles all.
278305

279306
# Internal state
280-
self.extensions = {".mov", ".exr", ".png", ".mp4"}
281-
self.ignore_dirs = {".git", ".svn", "__pycache__", ".DS_Store"}
307+
# Load extensions and ignore dirs from config
308+
self.extensions = set(self.config.get("extensions", []))
309+
self.ignore_dirs = set(self.config.get("ignore_dirs", []))
310+
self.root_ignore_dirs = set(self.config.get("root_ignore_dirs", []))
282311
self.search_thread = None
283312
self.cancel_search = False
284313
self.results_lock = threading.Lock() # Protects current_scan_results
@@ -339,7 +368,13 @@ def attribute_changed(self, attribute, role):
339368

340369
# Check if it's our command attribute and the Value changed
341370
if attribute.uuid == self.command_attr.uuid and role == AttributeRole.Value:
342-
val = self.command_attr.value()
371+
# Safely get value
372+
try:
373+
val = self.command_attr.value()
374+
except TypeError:
375+
# Can happen if connection is shutting down or not ready
376+
return
377+
343378
if not val:
344379
return # Empty command
345380

@@ -389,13 +424,28 @@ def attribute_changed(self, attribute, role):
389424
self.depth_limit_attr.set_value(attr_value)
390425

391426
elif action == "add_pin":
427+
path = data.get("path")
428+
self._add_pin(path)
429+
430+
elif action == "remove_pin":
431+
path = data.get("path")
432+
self._remove_pin(path)
433+
434+
elif action == "force_scan":
435+
# User clicked "Scan" button
436+
current = self.current_path_attr.value()
437+
self.start_search(current, force=True)
392438
name = data.get("name")
393439
path = data.get("path")
394440
self._add_pin(name, path)
395441

396442
elif action == "remove_pin":
397443
path = data.get("path")
398444
self._remove_pin(path)
445+
446+
elif action == "get_subdirs":
447+
path = data.get("path")
448+
self._get_subdirs(path)
399449

400450
# Clear command channel
401451
self.command_attr.set_value("")
@@ -454,6 +504,13 @@ def compute_completions(self, partial_path):
454504

455505
# Sort and limit
456506
candidates.sort()
507+
508+
except Exception as e:
509+
print(f"Search thread error: {e}")
510+
self.searching_attr.set_value(False)
511+
512+
# Sort and limit
513+
candidates.sort()
457514
import json
458515
self.completions_attr.set_value(json.dumps(candidates[:10]))
459516

@@ -462,6 +519,65 @@ def compute_completions(self, partial_path):
462519
self.completions_attr.set_value("[]")
463520

464521

522+
523+
def load_config(self):
524+
"""Load configuration from config.json in the plugin directory."""
525+
config_path = os.path.join(os.path.dirname(__file__), "config.json")
526+
default_config = {
527+
"extensions": [".mov", ".mp4", ".mkv", ".exr", ".jpg", ".jpeg", ".png",
528+
".dpx", ".tiff", ".tif", ".wav", ".mp3"],
529+
"ignore_dirs": [".git", ".quarantine", "eryx_unreal_plugin", ".DS_Store"],
530+
"root_ignore_dirs": [],
531+
"max_recursion_depth": 6,
532+
"auto_scan_threshold": 4
533+
}
534+
535+
if os.path.exists(config_path):
536+
try:
537+
with open(config_path, 'r') as f:
538+
loaded_config = json.load(f)
539+
# Merge with defaults
540+
for key, value in loaded_config.items():
541+
default_config[key] = value
542+
print(f"FilesystemBrowser: Loaded config from {config_path}")
543+
except Exception as e:
544+
print(f"FilesystemBrowser: Error loading config: {e}")
545+
546+
return default_config
547+
548+
def _get_subdirs(self, path):
549+
"""Fetch subdirectories for the given path and update attribute."""
550+
print(f"FilesystemBrowser: _get_subdirs called for {path}")
551+
result = {"path": path, "dirs": []}
552+
try:
553+
if os.path.exists(path) and os.path.isdir(path):
554+
dirs = []
555+
with os.scandir(path) as it:
556+
for entry in it:
557+
# Check ignore dirs (names)
558+
if entry.name in self.ignore_dirs or entry.name.startswith('.'):
559+
continue
560+
561+
# Check root ignore dirs (paths)
562+
if entry.path in self.root_ignore_dirs:
563+
continue
564+
565+
if entry.is_dir():
566+
dirs.append({
567+
"name": entry.name,
568+
"path": entry.path
569+
})
570+
# Sort alphabetically
571+
dirs.sort(key=lambda x: x["name"].lower())
572+
result["dirs"] = dirs
573+
print(f"FilesystemBrowser: Found {len(dirs)} subdirs in {path}")
574+
except Exception as e:
575+
print(f"Error getting subdirs for {path}: {e}")
576+
577+
# Ensure we use JSON dumping
578+
import json
579+
self.directory_query_result.set_value(json.dumps(result))
580+
465581
def load_file(self, path):
466582
# Handle directory navigation
467583
if os.path.isdir(path):
@@ -648,7 +764,59 @@ def load_file(self, path):
648764
print(f"Error loading file: {e}")
649765

650766

651-
def start_search(self, start_path):
767+
def start_search(self, start_path, force=False):
768+
"""
769+
Start the file search in a separate thread.
770+
If force=False and depth <= 4, skip auto-scan and ask user to confirm.
771+
"""
772+
if not start_path:
773+
return
774+
775+
# Check path depth
776+
# Normalize path
777+
norm_path = os.path.normpath(start_path)
778+
# Split path
779+
parts = norm_path.strip(os.sep).split(os.sep)
780+
# On Mac/Linux, root is empty string at start if absolute?
781+
# len(parts) for "/Users/sam" -> ["Users", "sam"] -> 2
782+
# Root "/" -> [""] -> 1 (empty string)
783+
# Let's count non-empty parts
784+
depth = len([p for p in parts if p])
785+
786+
# User requested: top 4 levels don't auto-scan.
787+
# e.g. / (0), /Users (1), /Users/sam (2), /Users/sam/Desktop (3) -> Auto scan?
788+
# Requirement: "looking at any directory in the top 4 levels ... shouldnt start recursive search"
789+
# So depth <= threshold skips scan.
790+
threshold = self.config.get("auto_scan_threshold", 4)
791+
792+
if not force and depth <= threshold:
793+
print(f"FilesystemBrowser: Path '{start_path}' (depth {depth}) requires manual scan.")
794+
self.scan_required_attr.set_value(True)
795+
self.searching_attr.set_value(False)
796+
self.progress_attr.set_value("0")
797+
798+
# Clear previous results
799+
with self.results_lock:
800+
self.current_scan_results = []
801+
self.scanned_attr.set_value("0")
802+
self.scanned_dirs_attr.set_value("[]")
803+
804+
# Update UI with empty list but correct path
805+
self.apply_filters() # Will clear list
806+
807+
# Ensure we cancel any running thread
808+
if self.search_thread and self.search_thread.is_alive():
809+
self.cancel_search = True
810+
if hasattr(self, 'scanner'):
811+
self.scanner.stop()
812+
self.search_thread.join()
813+
814+
return
815+
816+
# Proceed with scan
817+
self.scan_required_attr.set_value(False)
818+
819+
# Stop existing search if running
652820
if self.search_thread and self.search_thread.is_alive():
653821
self.cancel_search = True
654822
if hasattr(self, 'scanner'):
@@ -666,6 +834,10 @@ def _search_worker(self, start_path):
666834

667835
from .scanner import FileScanner
668836

837+
# Cache current filter values for this search to avoid threading issues with attribute access
838+
self.cached_filter_time = self.filter_time_attr.value()
839+
self.cached_filter_version = self.filter_version_attr.value()
840+
669841
# Config (could be loaded from prefs)
670842
max_depth = self.depth_limit_attr.value()
671843
config = {
@@ -696,7 +868,7 @@ def progress_callback(results, info):
696868
# to make it feel more linear to the user.
697869
biased_progress = pow(progress / 100.0, 2.0)*100
698870
self.progress_attr.set_value(str(biased_progress))
699-
self.scanned_attr.set_value(str(scanned))
871+
self.scanned_attr.set_value(str(scanned)) # Fixed type to string
700872
#self.scanProgress.set_value(str(progress))
701873

702874
# Accumulate scanned dirs
@@ -745,16 +917,34 @@ def progress_callback(results, info):
745917
self.searching_attr.set_value(False)
746918

747919
def apply_filters(self):
748-
# Filtering logic
749-
750-
with self.results_lock:
751-
results = list(self.current_scan_results)
920+
"""Re-run filtering logic on the current results cache."""
921+
try:
922+
with self.results_lock:
923+
results = list(self.current_scan_results)
752924

753-
self._apply_filters_logic(results)
754-
925+
# Offload heavy filtering if list is huge?
926+
# For now, do it in main thread or worker?
927+
# Safe to do in main thread if count < 100k?
928+
# Better to spawn a thread if we want UI responsiveness.
929+
930+
# Doing it synchronously for now, but catching errors
931+
self._apply_filters_logic(results)
932+
except Exception as e:
933+
print(f"Error applying filters: {e}")
934+
755935
def _apply_filters_logic(self, results):
756-
filter_time = self.filter_time_attr.value() if hasattr(self, 'filter_time_attr') else "Any"
757-
filter_version = self.filter_version_attr.value() if hasattr(self, 'filter_version_attr') else "All Versions"
936+
# Use cached values if available (from worker), else fetch live (UI update)
937+
if hasattr(self, 'cached_filter_time'):
938+
filter_time = self.cached_filter_time
939+
else:
940+
filter_time = self.filter_time_attr.value() if hasattr(self, 'filter_time_attr') else "Any"
941+
942+
if hasattr(self, 'cached_filter_version'):
943+
filter_version = self.cached_filter_version
944+
else:
945+
filter_version = self.filter_version_attr.value() if hasattr(self, 'filter_version_attr') else "All Versions"
946+
947+
print(f"Applying filters: Time={filter_time}, Version={filter_version}, Count={len(results)}")
758948

759949
# Separate directories and files
760950
dirs = []

0 commit comments

Comments
 (0)