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:
@@ -6,9 +6,11 @@ Uses CustomTkinter for a native look.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from tkinter import filedialog
|
||||
|
||||
try:
|
||||
import customtkinter as ctk
|
||||
@@ -112,20 +114,58 @@ class ClipCard(CTkFrame):
|
||||
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:
|
||||
"""Main GUI window for KB Capture."""
|
||||
|
||||
def __init__(self, projects_root: Path):
|
||||
def __init__(self, projects_root: Optional[Path] = None):
|
||||
if not HAS_CTK:
|
||||
raise RuntimeError("CustomTkinter not installed")
|
||||
|
||||
self.projects_root = projects_root
|
||||
# Load saved config
|
||||
self.config = load_config()
|
||||
|
||||
# App
|
||||
self.app = KBCaptureApp(
|
||||
projects_root=projects_root,
|
||||
on_status_change=self._on_status_change,
|
||||
)
|
||||
# Use saved path, provided path, or None
|
||||
if projects_root and projects_root.exists():
|
||||
self.projects_root = projects_root
|
||||
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
|
||||
ctk.set_appearance_mode("dark")
|
||||
@@ -133,26 +173,61 @@ class KBCaptureGUI:
|
||||
|
||||
self.window = CTk()
|
||||
self.window.title("KB Capture")
|
||||
self.window.geometry("500x600")
|
||||
self.window.minsize(400, 500)
|
||||
self.window.geometry("500x650")
|
||||
self.window.minsize(400, 550)
|
||||
|
||||
# Build UI
|
||||
self._build_ui()
|
||||
|
||||
# Start app
|
||||
# Start app if ready
|
||||
if self.app:
|
||||
self.app.start()
|
||||
|
||||
# Cleanup 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):
|
||||
"""Build the main UI."""
|
||||
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 = 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)
|
||||
|
||||
self.status_indicator = CTkLabel(
|
||||
@@ -181,7 +256,7 @@ class KBCaptureGUI:
|
||||
|
||||
# === Session Info / Start Form ===
|
||||
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)
|
||||
|
||||
# Project (dropdown)
|
||||
@@ -190,9 +265,12 @@ class KBCaptureGUI:
|
||||
)
|
||||
|
||||
# Get available projects
|
||||
if self.app:
|
||||
projects = self.app.session_manager.list_projects()
|
||||
if not projects:
|
||||
projects = ["(No projects found)"]
|
||||
else:
|
||||
projects = ["(Select folder first)"]
|
||||
|
||||
self.project_menu = CTkOptionMenu(
|
||||
self.session_frame,
|
||||
@@ -246,7 +324,7 @@ class KBCaptureGUI:
|
||||
|
||||
# === Clips List ===
|
||||
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)
|
||||
|
||||
CTkLabel(
|
||||
@@ -268,9 +346,9 @@ class KBCaptureGUI:
|
||||
self.window,
|
||||
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.window.grid_rowconfigure(3, weight=1)
|
||||
self.window.grid_rowconfigure(4, weight=1)
|
||||
|
||||
# Empty state
|
||||
self.empty_label = CTkLabel(
|
||||
@@ -284,7 +362,7 @@ class KBCaptureGUI:
|
||||
|
||||
# === Control Bar ===
|
||||
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)
|
||||
|
||||
self.record_btn = CTkButton(
|
||||
@@ -332,8 +410,45 @@ class KBCaptureGUI:
|
||||
)
|
||||
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):
|
||||
"""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()
|
||||
if not projects:
|
||||
projects = ["(No projects found)"]
|
||||
@@ -343,8 +458,12 @@ class KBCaptureGUI:
|
||||
|
||||
def _start_session(self):
|
||||
"""Start a new session."""
|
||||
if not self.app:
|
||||
self.status_label.configure(text="Select a projects folder first!")
|
||||
return
|
||||
|
||||
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!")
|
||||
return
|
||||
|
||||
@@ -446,6 +565,8 @@ class KBCaptureGUI:
|
||||
|
||||
def _update_clips_list(self):
|
||||
"""Update the clips list display."""
|
||||
if not self.app:
|
||||
return
|
||||
session = self.app.session_manager.current_session
|
||||
if not session:
|
||||
return
|
||||
@@ -485,6 +606,7 @@ class KBCaptureGUI:
|
||||
|
||||
def _on_close(self):
|
||||
"""Handle window close."""
|
||||
if self.app:
|
||||
self.app.stop()
|
||||
self.window.destroy()
|
||||
|
||||
@@ -500,18 +622,18 @@ def main():
|
||||
print("Install with: pip install customtkinter")
|
||||
sys.exit(1)
|
||||
|
||||
# Default projects location
|
||||
# Windows: Look for ATODrive or Documents
|
||||
# Try to find a projects folder automatically
|
||||
# Windows: Look for ATODrive, SeaDrive, or Documents
|
||||
# Linux: Look for obsidian-vault (Syncthing) or home
|
||||
if sys.platform == "win32":
|
||||
# Try common Windows locations
|
||||
candidates = [
|
||||
Path("D:/ATODrive/Projects"),
|
||||
Path("C:/ATODrive/Projects"),
|
||||
# Common SeaDrive paths
|
||||
Path.home() / "SeaDrive" / "My Libraries" / "ATODrive" / "Projects",
|
||||
Path.home() / "Documents" / "Projects",
|
||||
]
|
||||
else:
|
||||
# Linux/Clawdbot - use Syncthing path
|
||||
candidates = [
|
||||
Path.home() / "obsidian-vault" / "2-Projects",
|
||||
Path.home() / "ATODrive" / "Projects",
|
||||
@@ -521,15 +643,13 @@ def main():
|
||||
for path in candidates:
|
||||
if path.exists():
|
||||
projects_root = path
|
||||
print(f"Found projects at: {projects_root}")
|
||||
break
|
||||
|
||||
# If not found, GUI will prompt user to browse
|
||||
if not projects_root:
|
||||
# Fallback: create in Documents
|
||||
projects_root = Path.home() / "Documents" / "Projects"
|
||||
projects_root.mkdir(parents=True, exist_ok=True)
|
||||
print(f"No projects folder found. Using: {projects_root}")
|
||||
|
||||
print(f"Projects root: {projects_root}")
|
||||
print("No projects folder found automatically.")
|
||||
print("You'll be prompted to select one.")
|
||||
|
||||
gui = KBCaptureGUI(projects_root=projects_root)
|
||||
gui.run()
|
||||
|
||||
Reference in New Issue
Block a user