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
@@ -26,7 +28,7 @@ from .session import SessionType, ClipStatus
# Colors # Colors
COLORS = { COLORS = {
"idle": "#6B7280", # Gray "idle": "#6B7280", # Gray
"session": "#10B981", # Green "session": "#10B981", # Green
"recording": "#EF4444", # Red "recording": "#EF4444", # Red
"preview": "#F59E0B", # Amber "preview": "#F59E0B", # Amber
"bg_dark": "#1F2937", "bg_dark": "#1F2937",
@@ -38,7 +40,7 @@ COLORS = {
class ClipCard(CTkFrame): class ClipCard(CTkFrame):
"""A card showing a single clip.""" """A card showing a single clip."""
def __init__( def __init__(
self, self,
master, master,
@@ -50,9 +52,9 @@ class ClipCard(CTkFrame):
**kwargs **kwargs
): ):
super().__init__(master, **kwargs) super().__init__(master, **kwargs)
self.configure(fg_color=COLORS["bg_light"], corner_radius=8) self.configure(fg_color=COLORS["bg_light"], corner_radius=8)
# Status indicator # Status indicator
status_colors = { status_colors = {
ClipStatus.KEPT: "#10B981", ClipStatus.KEPT: "#10B981",
@@ -61,18 +63,18 @@ class ClipCard(CTkFrame):
ClipStatus.RECORDING: "#EF4444", ClipStatus.RECORDING: "#EF4444",
} }
color = status_colors.get(status, COLORS["idle"]) color = status_colors.get(status, COLORS["idle"])
# Layout # Layout
self.grid_columnconfigure(1, weight=1) self.grid_columnconfigure(1, weight=1)
# Status dot # Status dot
dot = CTkLabel(self, text="", text_color=color, font=("", 16)) dot = CTkLabel(self, text="", text_color=color, font=("", 16))
dot.grid(row=0, column=0, padx=(10, 5), pady=10) dot.grid(row=0, column=0, padx=(10, 5), pady=10)
# Clip info # Clip info
info_frame = CTkFrame(self, fg_color="transparent") info_frame = CTkFrame(self, fg_color="transparent")
info_frame.grid(row=0, column=1, sticky="w", pady=10) info_frame.grid(row=0, column=1, sticky="w", pady=10)
title = CTkLabel( title = CTkLabel(
info_frame, info_frame,
text=clip_id, text=clip_id,
@@ -80,7 +82,7 @@ class ClipCard(CTkFrame):
text_color=COLORS["text"], text_color=COLORS["text"],
) )
title.pack(anchor="w") title.pack(anchor="w")
subtitle = CTkLabel( subtitle = CTkLabel(
info_frame, info_frame,
text=f"{duration:.1f}s" + (f"{note}" if note else ""), text=f"{duration:.1f}s" + (f"{note}" if note else ""),
@@ -88,7 +90,7 @@ class ClipCard(CTkFrame):
text_color=COLORS["text_dim"], text_color=COLORS["text_dim"],
) )
subtitle.pack(anchor="w") subtitle.pack(anchor="w")
# Duration # Duration
duration_label = CTkLabel( duration_label = CTkLabel(
self, self,
@@ -97,7 +99,7 @@ class ClipCard(CTkFrame):
text_color=COLORS["text_dim"], text_color=COLORS["text_dim"],
) )
duration_label.grid(row=0, column=2, padx=10) duration_label.grid(row=0, column=2, padx=10)
# Delete button (only for preview/kept) # Delete button (only for preview/kept)
if status in (ClipStatus.PREVIEW, ClipStatus.KEPT) and on_delete: if status in (ClipStatus.PREVIEW, ClipStatus.KEPT) and on_delete:
delete_btn = CTkButton( delete_btn = CTkButton(
@@ -112,49 +114,122 @@ 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
self.app = KBCaptureApp( # Use saved path, provided path, or None
projects_root=projects_root, if projects_root and projects_root.exists():
on_status_change=self._on_status_change, 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 # Window
ctk.set_appearance_mode("dark") ctk.set_appearance_mode("dark")
ctk.set_default_color_theme("blue") ctk.set_default_color_theme("blue")
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
self.app.start() if self.app:
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(
header, header,
text="", text="",
@@ -162,7 +237,7 @@ class KBCaptureGUI:
text_color=COLORS["idle"], text_color=COLORS["idle"],
) )
self.status_indicator.grid(row=0, column=0, padx=(0, 10)) self.status_indicator.grid(row=0, column=0, padx=(0, 10))
self.status_label = CTkLabel( self.status_label = CTkLabel(
header, header,
text="Ready", text="Ready",
@@ -170,7 +245,7 @@ class KBCaptureGUI:
text_color=COLORS["text"], text_color=COLORS["text"],
) )
self.status_label.grid(row=0, column=1, sticky="w") self.status_label.grid(row=0, column=1, sticky="w")
self.duration_label = CTkLabel( self.duration_label = CTkLabel(
header, header,
text="", text="",
@@ -178,29 +253,32 @@ class KBCaptureGUI:
text_color=COLORS["text_dim"], text_color=COLORS["text_dim"],
) )
self.duration_label.grid(row=0, column=2) self.duration_label.grid(row=0, column=2)
# === 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)
CTkLabel(self.session_frame, text="Project:", text_color=COLORS["text_dim"]).grid( CTkLabel(self.session_frame, text="Project:", text_color=COLORS["text_dim"]).grid(
row=0, column=0, padx=15, pady=(15, 5), sticky="w" row=0, column=0, padx=15, pady=(15, 5), sticky="w"
) )
# Get available projects # Get available projects
projects = self.app.session_manager.list_projects() if self.app:
if not projects: projects = self.app.session_manager.list_projects()
projects = ["(No projects found)"] if not projects:
projects = ["(No projects found)"]
else:
projects = ["(Select folder first)"]
self.project_menu = CTkOptionMenu( self.project_menu = CTkOptionMenu(
self.session_frame, self.session_frame,
values=projects, values=projects,
width=250, width=250,
) )
self.project_menu.grid(row=0, column=1, padx=(0, 15), pady=(15, 5), sticky="ew") self.project_menu.grid(row=0, column=1, padx=(0, 15), pady=(15, 5), sticky="ew")
# Refresh button # Refresh button
refresh_btn = CTkButton( refresh_btn = CTkButton(
self.session_frame, self.session_frame,
@@ -211,7 +289,7 @@ class KBCaptureGUI:
command=self._refresh_projects, command=self._refresh_projects,
) )
refresh_btn.grid(row=0, column=2, padx=(0, 15), pady=(15, 5)) refresh_btn.grid(row=0, column=2, padx=(0, 15), pady=(15, 5))
# Session name # Session name
CTkLabel(self.session_frame, text="Session:", text_color=COLORS["text_dim"]).grid( CTkLabel(self.session_frame, text="Session:", text_color=COLORS["text_dim"]).grid(
row=1, column=0, padx=15, pady=5, sticky="w" row=1, column=0, padx=15, pady=5, sticky="w"
@@ -222,7 +300,7 @@ class KBCaptureGUI:
width=250, width=250,
) )
self.name_entry.grid(row=1, column=1, padx=(0, 15), pady=5, sticky="ew") self.name_entry.grid(row=1, column=1, padx=(0, 15), pady=5, sticky="ew")
# Session type # Session type
CTkLabel(self.session_frame, text="Type:", text_color=COLORS["text_dim"]).grid( CTkLabel(self.session_frame, text="Type:", text_color=COLORS["text_dim"]).grid(
row=2, column=0, padx=15, pady=5, sticky="w" row=2, column=0, padx=15, pady=5, sticky="w"
@@ -233,7 +311,7 @@ class KBCaptureGUI:
width=150, width=150,
) )
self.type_menu.grid(row=2, column=1, padx=(0, 15), pady=5, sticky="w") self.type_menu.grid(row=2, column=1, padx=(0, 15), pady=5, sticky="w")
# Start button # Start button
self.start_btn = CTkButton( self.start_btn = CTkButton(
self.session_frame, self.session_frame,
@@ -243,19 +321,19 @@ class KBCaptureGUI:
command=self._start_session, command=self._start_session,
) )
self.start_btn.grid(row=3, column=0, columnspan=2, padx=15, pady=15, sticky="ew") self.start_btn.grid(row=3, column=0, columnspan=2, padx=15, pady=15, sticky="ew")
# === 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(
clips_header, clips_header,
text="Clips", text="Clips",
font=("", 16, "bold"), font=("", 16, "bold"),
text_color=COLORS["text"], text_color=COLORS["text"],
).grid(row=0, column=0, sticky="w") ).grid(row=0, column=0, sticky="w")
self.clips_count = CTkLabel( self.clips_count = CTkLabel(
clips_header, clips_header,
text="0 clips • 0:00", text="0 clips • 0:00",
@@ -263,15 +341,15 @@ class KBCaptureGUI:
text_color=COLORS["text_dim"], text_color=COLORS["text_dim"],
) )
self.clips_count.grid(row=0, column=1, sticky="e") self.clips_count.grid(row=0, column=1, sticky="e")
self.clips_frame = CTkScrollableFrame( self.clips_frame = CTkScrollableFrame(
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(
self.clips_frame, self.clips_frame,
@@ -281,12 +359,12 @@ class KBCaptureGUI:
justify="center", justify="center",
) )
self.empty_label.grid(row=0, column=0, pady=40) self.empty_label.grid(row=0, column=0, pady=40)
# === 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(
controls, controls,
text="⏺️ Record", text="⏺️ Record",
@@ -298,7 +376,7 @@ class KBCaptureGUI:
state="disabled", state="disabled",
) )
self.record_btn.grid(row=0, column=0, padx=5, pady=10, sticky="ew") self.record_btn.grid(row=0, column=0, padx=5, pady=10, sticky="ew")
self.keep_btn = CTkButton( self.keep_btn = CTkButton(
controls, controls,
text="✓ Keep", text="✓ Keep",
@@ -310,7 +388,7 @@ class KBCaptureGUI:
state="disabled", state="disabled",
) )
self.keep_btn.grid(row=0, column=1, padx=5, pady=10, sticky="ew") self.keep_btn.grid(row=0, column=1, padx=5, pady=10, sticky="ew")
self.end_btn = CTkButton( self.end_btn = CTkButton(
controls, controls,
text="End Session", text="End Session",
@@ -322,7 +400,7 @@ class KBCaptureGUI:
state="disabled", state="disabled",
) )
self.end_btn.grid(row=0, column=2, padx=5, pady=10, sticky="ew") self.end_btn.grid(row=0, column=2, padx=5, pady=10, sticky="ew")
# Hotkey hints # Hotkey hints
hints = CTkLabel( hints = CTkLabel(
controls, controls,
@@ -331,32 +409,73 @@ class KBCaptureGUI:
text_color=COLORS["text_dim"], text_color=COLORS["text_dim"],
) )
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)"]
self.project_menu.configure(values=projects) self.project_menu.configure(values=projects)
if projects: if projects:
self.project_menu.set(projects[0]) self.project_menu.set(projects[0])
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
name = self.name_entry.get() or "Recording Session" name = self.name_entry.get() or "Recording Session"
session_type = SessionType.DESIGN if self.type_menu.get() == "Design" else SessionType.ANALYSIS session_type = SessionType.DESIGN if self.type_menu.get() == "Design" else SessionType.ANALYSIS
try: try:
self.app.start_session(name, project, session_type) self.app.start_session(name, project, session_type)
except ValueError as e: except ValueError as e:
self.status_label.configure(text=str(e)) self.status_label.configure(text=str(e))
return return
# Update UI # Update UI
self.start_btn.configure(state="disabled", text="Session Active") self.start_btn.configure(state="disabled", text="Session Active")
self.record_btn.configure(state="normal") self.record_btn.configure(state="normal")
@@ -364,15 +483,15 @@ class KBCaptureGUI:
self.project_menu.configure(state="disabled") self.project_menu.configure(state="disabled")
self.name_entry.configure(state="disabled") self.name_entry.configure(state="disabled")
self.type_menu.configure(state="disabled") self.type_menu.configure(state="disabled")
def _end_session(self): def _end_session(self):
"""End the current session.""" """End the current session."""
session = self.app.end_session() session = self.app.end_session()
if session: if session:
# Show summary # Show summary
self.status_label.configure(text="Session Saved!") self.status_label.configure(text="Session Saved!")
# Reset UI # Reset UI
self.start_btn.configure(state="normal", text="Start Session") self.start_btn.configure(state="normal", text="Start Session")
self.record_btn.configure(state="disabled") self.record_btn.configure(state="disabled")
@@ -381,7 +500,7 @@ class KBCaptureGUI:
self.project_menu.configure(state="normal") self.project_menu.configure(state="normal")
self.name_entry.configure(state="normal") self.name_entry.configure(state="normal")
self.type_menu.configure(state="normal") self.type_menu.configure(state="normal")
# Clear clips # Clear clips
for widget in self.clips_frame.winfo_children(): for widget in self.clips_frame.winfo_children():
widget.destroy() widget.destroy()
@@ -393,12 +512,12 @@ class KBCaptureGUI:
justify="center", justify="center",
) )
self.empty_label.grid(row=0, column=0, pady=40) self.empty_label.grid(row=0, column=0, pady=40)
def _on_status_change(self, status: AppStatus): def _on_status_change(self, status: AppStatus):
"""Handle status updates from the app.""" """Handle status updates from the app."""
# Run on main thread # Run on main thread
self.window.after(0, lambda: self._update_ui(status)) self.window.after(0, lambda: self._update_ui(status))
def _update_ui(self, status: AppStatus): def _update_ui(self, status: AppStatus):
"""Update UI based on status.""" """Update UI based on status."""
# State colors and labels # State colors and labels
@@ -408,11 +527,11 @@ class KBCaptureGUI:
AppState.RECORDING: (COLORS["recording"], "Recording", ""), AppState.RECORDING: (COLORS["recording"], "Recording", ""),
AppState.PREVIEW: (COLORS["preview"], "Review Clip", ""), AppState.PREVIEW: (COLORS["preview"], "Review Clip", ""),
} }
color, label, _ = state_config.get(status.state, (COLORS["idle"], "Ready", "")) color, label, _ = state_config.get(status.state, (COLORS["idle"], "Ready", ""))
self.status_indicator.configure(text_color=color) self.status_indicator.configure(text_color=color)
if status.state == AppState.RECORDING: if status.state == AppState.RECORDING:
self.status_label.configure(text=f"Recording... {status.current_clip_duration:.1f}s") self.status_label.configure(text=f"Recording... {status.current_clip_duration:.1f}s")
self.record_btn.configure(text="⏹️ Stop", fg_color=COLORS["recording"]) self.record_btn.configure(text="⏹️ Stop", fg_color=COLORS["recording"])
@@ -425,7 +544,7 @@ class KBCaptureGUI:
self.record_btn.configure(text="⏺️ Record", fg_color=COLORS["recording"]) self.record_btn.configure(text="⏺️ Record", fg_color=COLORS["recording"])
if status.state != AppState.IDLE: if status.state != AppState.IDLE:
self.keep_btn.configure(state="disabled") self.keep_btn.configure(state="disabled")
# Duration # Duration
if status.state == AppState.RECORDING: if status.state == AppState.RECORDING:
secs = int(status.current_clip_duration) secs = int(status.current_clip_duration)
@@ -435,25 +554,27 @@ class KBCaptureGUI:
self.duration_label.configure(text=f"Total: {secs//60}:{secs%60:02d}") self.duration_label.configure(text=f"Total: {secs//60}:{secs%60:02d}")
else: else:
self.duration_label.configure(text="") self.duration_label.configure(text="")
# Clips count # Clips count
self.clips_count.configure( self.clips_count.configure(
text=f"{status.clip_count} clips • {int(status.total_duration//60)}:{int(status.total_duration%60):02d}" text=f"{status.clip_count} clips • {int(status.total_duration//60)}:{int(status.total_duration%60):02d}"
) )
# Update clips list # Update clips list
self._update_clips_list() self._update_clips_list()
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
# Clear existing # Clear existing
for widget in self.clips_frame.winfo_children(): for widget in self.clips_frame.winfo_children():
widget.destroy() widget.destroy()
if not session.clips: if not session.clips:
self.empty_label = CTkLabel( self.empty_label = CTkLabel(
self.clips_frame, self.clips_frame,
@@ -464,7 +585,7 @@ class KBCaptureGUI:
) )
self.empty_label.grid(row=0, column=0, pady=40) self.empty_label.grid(row=0, column=0, pady=40)
return return
# Add clip cards (reversed for newest first) # Add clip cards (reversed for newest first)
for i, clip in enumerate(reversed(session.clips)): for i, clip in enumerate(reversed(session.clips)):
if clip.status != ClipStatus.DELETED: if clip.status != ClipStatus.DELETED:
@@ -477,17 +598,18 @@ class KBCaptureGUI:
on_delete=self._delete_clip, on_delete=self._delete_clip,
) )
card.grid(row=i, column=0, sticky="ew", pady=2) card.grid(row=i, column=0, sticky="ew", pady=2)
def _delete_clip(self, clip_id: str): def _delete_clip(self, clip_id: str):
"""Delete a clip.""" """Delete a clip."""
self.app.session_manager.delete_clip(clip_id) self.app.session_manager.delete_clip(clip_id)
self._update_clips_list() self._update_clips_list()
def _on_close(self): def _on_close(self):
"""Handle window close.""" """Handle window close."""
self.app.stop() if self.app:
self.app.stop()
self.window.destroy() self.window.destroy()
def run(self): def run(self):
"""Run the GUI main loop.""" """Run the GUI main loop."""
self.window.mainloop() self.window.mainloop()
@@ -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()