Add Browse button for projects folder selection

- Added Browse... button to select projects folder
- Saves selected folder to config file (persistent)
- Works with SeaDrive paths
- Graceful handling when no folder selected
- Auto-detects common paths on startup
This commit is contained in:
Mario Lavoie
2026-02-09 21:55:16 +00:00
parent 0266fda42b
commit 978c79abc0

View File

@@ -6,9 +6,11 @@ Uses CustomTkinter for a native look.
""" """
import sys import sys
import json
import threading import threading
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
from tkinter import filedialog
try: try:
import customtkinter as ctk import customtkinter as ctk
@@ -112,20 +114,58 @@ class ClipCard(CTkFrame):
delete_btn.grid(row=0, column=3, padx=(0, 10)) delete_btn.grid(row=0, column=3, padx=(0, 10))
def get_config_path() -> Path:
"""Get path to config file."""
if sys.platform == "win32":
config_dir = Path.home() / "AppData" / "Local" / "KBCapture"
else:
config_dir = Path.home() / ".config" / "kb-capture"
config_dir.mkdir(parents=True, exist_ok=True)
return config_dir / "config.json"
def load_config() -> dict:
"""Load config from file."""
config_path = get_config_path()
if config_path.exists():
try:
with open(config_path) as f:
return json.load(f)
except:
pass
return {}
def save_config(config: dict) -> None:
"""Save config to file."""
config_path = get_config_path()
with open(config_path, "w") as f:
json.dump(config, f, indent=2)
class KBCaptureGUI: class KBCaptureGUI:
"""Main GUI window for KB Capture.""" """Main GUI window for KB Capture."""
def __init__(self, projects_root: Path): def __init__(self, projects_root: Optional[Path] = None):
if not HAS_CTK: if not HAS_CTK:
raise RuntimeError("CustomTkinter not installed") raise RuntimeError("CustomTkinter not installed")
self.projects_root = projects_root # Load saved config
self.config = load_config()
# App # Use saved path, provided path, or None
self.app = KBCaptureApp( if projects_root and projects_root.exists():
projects_root=projects_root, self.projects_root = projects_root
on_status_change=self._on_status_change, elif self.config.get("projects_root"):
) saved = Path(self.config["projects_root"])
self.projects_root = saved if saved.exists() else None
else:
self.projects_root = None
# App (may be None if no projects root)
self.app = None
if self.projects_root:
self._init_app()
# Window # Window
ctk.set_appearance_mode("dark") ctk.set_appearance_mode("dark")
@@ -133,26 +173,61 @@ class KBCaptureGUI:
self.window = CTk() self.window = CTk()
self.window.title("KB Capture") self.window.title("KB Capture")
self.window.geometry("500x600") self.window.geometry("500x650")
self.window.minsize(400, 500) self.window.minsize(400, 550)
# Build UI # Build UI
self._build_ui() self._build_ui()
# Start app # Start app if ready
if self.app:
self.app.start() self.app.start()
# Cleanup on close # Cleanup on close
self.window.protocol("WM_DELETE_WINDOW", self._on_close) self.window.protocol("WM_DELETE_WINDOW", self._on_close)
def _init_app(self):
"""Initialize the app with current projects_root."""
self.app = KBCaptureApp(
projects_root=self.projects_root,
on_status_change=self._on_status_change,
)
def _build_ui(self): def _build_ui(self):
"""Build the main UI.""" """Build the main UI."""
self.window.grid_columnconfigure(0, weight=1) self.window.grid_columnconfigure(0, weight=1)
self.window.grid_rowconfigure(2, weight=1) self.window.grid_rowconfigure(3, weight=1)
# === Projects Folder Selector ===
folder_frame = CTkFrame(self.window, fg_color=COLORS["bg_light"], corner_radius=8)
folder_frame.grid(row=0, column=0, sticky="ew", padx=20, pady=(20, 10))
folder_frame.grid_columnconfigure(1, weight=1)
CTkLabel(folder_frame, text="📁", font=("", 20)).grid(
row=0, column=0, padx=(15, 5), pady=10
)
self.folder_label = CTkLabel(
folder_frame,
text=str(self.projects_root) if self.projects_root else "No folder selected",
font=("", 12),
text_color=COLORS["text"] if self.projects_root else COLORS["text_dim"],
anchor="w",
)
self.folder_label.grid(row=0, column=1, sticky="ew", padx=5, pady=10)
browse_btn = CTkButton(
folder_frame,
text="Browse...",
width=80,
height=28,
command=self._browse_folder,
)
browse_btn.grid(row=0, column=2, padx=(5, 15), pady=10)
# === Header === # === Header ===
header = CTkFrame(self.window, fg_color="transparent") header = CTkFrame(self.window, fg_color="transparent")
header.grid(row=0, column=0, sticky="ew", padx=20, pady=(20, 10)) header.grid(row=1, column=0, sticky="ew", padx=20, pady=(10, 10))
header.grid_columnconfigure(1, weight=1) header.grid_columnconfigure(1, weight=1)
self.status_indicator = CTkLabel( self.status_indicator = CTkLabel(
@@ -181,7 +256,7 @@ class KBCaptureGUI:
# === Session Info / Start Form === # === Session Info / Start Form ===
self.session_frame = CTkFrame(self.window, fg_color=COLORS["bg_light"], corner_radius=12) self.session_frame = CTkFrame(self.window, fg_color=COLORS["bg_light"], corner_radius=12)
self.session_frame.grid(row=1, column=0, sticky="ew", padx=20, pady=10) self.session_frame.grid(row=2, column=0, sticky="ew", padx=20, pady=10)
self.session_frame.grid_columnconfigure(1, weight=1) self.session_frame.grid_columnconfigure(1, weight=1)
# Project (dropdown) # Project (dropdown)
@@ -190,9 +265,12 @@ class KBCaptureGUI:
) )
# Get available projects # Get available projects
if self.app:
projects = self.app.session_manager.list_projects() projects = self.app.session_manager.list_projects()
if not projects: if not projects:
projects = ["(No projects found)"] projects = ["(No projects found)"]
else:
projects = ["(Select folder first)"]
self.project_menu = CTkOptionMenu( self.project_menu = CTkOptionMenu(
self.session_frame, self.session_frame,
@@ -246,7 +324,7 @@ class KBCaptureGUI:
# === Clips List === # === Clips List ===
clips_header = CTkFrame(self.window, fg_color="transparent") clips_header = CTkFrame(self.window, fg_color="transparent")
clips_header.grid(row=2, column=0, sticky="new", padx=20, pady=(10, 0)) clips_header.grid(row=3, column=0, sticky="new", padx=20, pady=(10, 0))
clips_header.grid_columnconfigure(0, weight=1) clips_header.grid_columnconfigure(0, weight=1)
CTkLabel( CTkLabel(
@@ -268,9 +346,9 @@ class KBCaptureGUI:
self.window, self.window,
fg_color="transparent", fg_color="transparent",
) )
self.clips_frame.grid(row=3, column=0, sticky="nsew", padx=20, pady=10) self.clips_frame.grid(row=4, column=0, sticky="nsew", padx=20, pady=10)
self.clips_frame.grid_columnconfigure(0, weight=1) self.clips_frame.grid_columnconfigure(0, weight=1)
self.window.grid_rowconfigure(3, weight=1) self.window.grid_rowconfigure(4, weight=1)
# Empty state # Empty state
self.empty_label = CTkLabel( self.empty_label = CTkLabel(
@@ -284,7 +362,7 @@ class KBCaptureGUI:
# === Control Bar === # === Control Bar ===
controls = CTkFrame(self.window, fg_color=COLORS["bg_light"], corner_radius=0) controls = CTkFrame(self.window, fg_color=COLORS["bg_light"], corner_radius=0)
controls.grid(row=4, column=0, sticky="sew", pady=0) controls.grid(row=5, column=0, sticky="sew", pady=0)
controls.grid_columnconfigure((0, 1, 2), weight=1) controls.grid_columnconfigure((0, 1, 2), weight=1)
self.record_btn = CTkButton( self.record_btn = CTkButton(
@@ -332,8 +410,45 @@ class KBCaptureGUI:
) )
hints.grid(row=1, column=0, columnspan=3, pady=(0, 10)) hints.grid(row=1, column=0, columnspan=3, pady=(0, 10))
def _browse_folder(self):
"""Browse for projects folder."""
initial_dir = str(self.projects_root) if self.projects_root else str(Path.home())
folder = filedialog.askdirectory(
title="Select Projects Folder",
initialdir=initial_dir,
)
if folder:
self.projects_root = Path(folder)
self.folder_label.configure(
text=str(self.projects_root),
text_color=COLORS["text"],
)
# Save to config
self.config["projects_root"] = str(self.projects_root)
save_config(self.config)
# Initialize or reinitialize app
if self.app:
self.app.stop()
self._init_app()
self.app.start()
# Refresh projects list
self._refresh_projects()
# Update status
self.status_label.configure(text="Ready")
def _refresh_projects(self): def _refresh_projects(self):
"""Refresh the project list.""" """Refresh the project list."""
if not self.app:
self.project_menu.configure(values=["(Select folder first)"])
self.project_menu.set("(Select folder first)")
return
projects = self.app.session_manager.list_projects() projects = self.app.session_manager.list_projects()
if not projects: if not projects:
projects = ["(No projects found)"] projects = ["(No projects found)"]
@@ -343,8 +458,12 @@ class KBCaptureGUI:
def _start_session(self): def _start_session(self):
"""Start a new session.""" """Start a new session."""
if not self.app:
self.status_label.configure(text="Select a projects folder first!")
return
project = self.project_menu.get() project = self.project_menu.get()
if project == "(No projects found)": if project in ("(No projects found)", "(Select folder first)"):
self.status_label.configure(text="No project selected!") self.status_label.configure(text="No project selected!")
return return
@@ -446,6 +565,8 @@ class KBCaptureGUI:
def _update_clips_list(self): def _update_clips_list(self):
"""Update the clips list display.""" """Update the clips list display."""
if not self.app:
return
session = self.app.session_manager.current_session session = self.app.session_manager.current_session
if not session: if not session:
return return
@@ -485,6 +606,7 @@ class KBCaptureGUI:
def _on_close(self): def _on_close(self):
"""Handle window close.""" """Handle window close."""
if self.app:
self.app.stop() self.app.stop()
self.window.destroy() self.window.destroy()
@@ -500,18 +622,18 @@ def main():
print("Install with: pip install customtkinter") print("Install with: pip install customtkinter")
sys.exit(1) sys.exit(1)
# Default projects location # Try to find a projects folder automatically
# Windows: Look for ATODrive or Documents # Windows: Look for ATODrive, SeaDrive, or Documents
# Linux: Look for obsidian-vault (Syncthing) or home # Linux: Look for obsidian-vault (Syncthing) or home
if sys.platform == "win32": if sys.platform == "win32":
# Try common Windows locations
candidates = [ candidates = [
Path("D:/ATODrive/Projects"), Path("D:/ATODrive/Projects"),
Path("C:/ATODrive/Projects"), Path("C:/ATODrive/Projects"),
# Common SeaDrive paths
Path.home() / "SeaDrive" / "My Libraries" / "ATODrive" / "Projects",
Path.home() / "Documents" / "Projects", Path.home() / "Documents" / "Projects",
] ]
else: else:
# Linux/Clawdbot - use Syncthing path
candidates = [ candidates = [
Path.home() / "obsidian-vault" / "2-Projects", Path.home() / "obsidian-vault" / "2-Projects",
Path.home() / "ATODrive" / "Projects", Path.home() / "ATODrive" / "Projects",
@@ -521,15 +643,13 @@ def main():
for path in candidates: for path in candidates:
if path.exists(): if path.exists():
projects_root = path projects_root = path
print(f"Found projects at: {projects_root}")
break break
# If not found, GUI will prompt user to browse
if not projects_root: if not projects_root:
# Fallback: create in Documents print("No projects folder found automatically.")
projects_root = Path.home() / "Documents" / "Projects" print("You'll be prompted to select one.")
projects_root.mkdir(parents=True, exist_ok=True)
print(f"No projects folder found. Using: {projects_root}")
print(f"Projects root: {projects_root}")
gui = KBCaptureGUI(projects_root=projects_root) gui = KBCaptureGUI(projects_root=projects_root)
gui.run() gui.run()