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 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()
|
||||||
|
|||||||
Reference in New Issue
Block a user