feat(gui): Add topic field and smart video naming
- Added AddVideoDialog for entering session topic when adding videos
- Videos now renamed to: gen-{num}_{topic}_{timestamp}.ext
- Shows next generation number in UI
- Shows project info (total videos, pending, processed)
- Better error handling and user feedback
- Cleaner layout with sections
When adding a video, user can enter a topic like 'lateral-support-detail'
and the video is automatically renamed to a standardized format.
This commit is contained in:
@@ -5,6 +5,9 @@ from tkinter import ttk, filedialog, messagebox
|
||||
from pathlib import Path
|
||||
import threading
|
||||
import json
|
||||
import shutil
|
||||
import re
|
||||
from datetime import datetime
|
||||
|
||||
try:
|
||||
import customtkinter as ctk
|
||||
@@ -20,13 +23,136 @@ from .incremental import IncrementalProcessor
|
||||
from .config import load_config
|
||||
|
||||
|
||||
def slugify(text: str) -> str:
|
||||
"""Convert text to a filename-safe slug."""
|
||||
text = text.lower().strip()
|
||||
text = re.sub(r'[^\w\s-]', '', text)
|
||||
text = re.sub(r'[\s_-]+', '-', text)
|
||||
return text[:50] # Limit length
|
||||
|
||||
|
||||
class AddVideoDialog:
|
||||
"""Dialog for adding a video with topic."""
|
||||
|
||||
def __init__(self, parent, video_path: Path, gen_number: int):
|
||||
self.result = None
|
||||
self.video_path = video_path
|
||||
self.gen_number = gen_number
|
||||
|
||||
self.dialog = ctk.CTkToplevel(parent) if USE_CTK else tk.Toplevel(parent)
|
||||
self.dialog.title("Add Video")
|
||||
self.dialog.geometry("500x250")
|
||||
self.dialog.transient(parent)
|
||||
self.dialog.grab_set()
|
||||
|
||||
# Make modal
|
||||
self.dialog.focus_set()
|
||||
|
||||
# Center on parent
|
||||
self.dialog.update_idletasks()
|
||||
x = parent.winfo_x() + (parent.winfo_width() - 500) // 2
|
||||
y = parent.winfo_y() + (parent.winfo_height() - 250) // 2
|
||||
self.dialog.geometry(f"+{x}+{y}")
|
||||
|
||||
self._create_widgets()
|
||||
|
||||
def _create_widgets(self):
|
||||
frame = ctk.CTkFrame(self.dialog) if USE_CTK else ttk.Frame(self.dialog)
|
||||
frame.pack(fill="both", expand=True, padx=20, pady=20)
|
||||
|
||||
# Video info
|
||||
info_text = f"Adding: {self.video_path.name}"
|
||||
if USE_CTK:
|
||||
ctk.CTkLabel(frame, text=info_text, wraplength=450).pack(anchor="w", pady=(0, 10))
|
||||
else:
|
||||
ttk.Label(frame, text=info_text, wraplength=450).pack(anchor="w", pady=(0, 10))
|
||||
|
||||
# Generation number
|
||||
gen_text = f"This will be: Generation {self.gen_number:03d}"
|
||||
if USE_CTK:
|
||||
ctk.CTkLabel(frame, text=gen_text, text_color="gray").pack(anchor="w", pady=(0, 15))
|
||||
else:
|
||||
ttk.Label(frame, text=gen_text).pack(anchor="w", pady=(0, 15))
|
||||
|
||||
# Topic input
|
||||
if USE_CTK:
|
||||
ctk.CTkLabel(frame, text="Session Topic (optional):").pack(anchor="w")
|
||||
else:
|
||||
ttk.Label(frame, text="Session Topic (optional):").pack(anchor="w")
|
||||
|
||||
self.topic_var = tk.StringVar()
|
||||
if USE_CTK:
|
||||
self.topic_entry = ctk.CTkEntry(frame, textvariable=self.topic_var, width=400,
|
||||
placeholder_text="e.g., lateral-support-detail, stage2-joints")
|
||||
else:
|
||||
self.topic_entry = ttk.Entry(frame, textvariable=self.topic_var, width=50)
|
||||
self.topic_entry.pack(fill="x", pady=(5, 10))
|
||||
self.topic_entry.focus_set()
|
||||
|
||||
# Preview
|
||||
self.preview_var = tk.StringVar()
|
||||
self._update_preview()
|
||||
self.topic_var.trace_add("write", lambda *args: self._update_preview())
|
||||
|
||||
if USE_CTK:
|
||||
ctk.CTkLabel(frame, text="Will be saved as:").pack(anchor="w", pady=(10, 0))
|
||||
self.preview_label = ctk.CTkLabel(frame, textvariable=self.preview_var, text_color="cyan")
|
||||
else:
|
||||
ttk.Label(frame, text="Will be saved as:").pack(anchor="w", pady=(10, 0))
|
||||
self.preview_label = ttk.Label(frame, textvariable=self.preview_var)
|
||||
self.preview_label.pack(anchor="w")
|
||||
|
||||
# Buttons
|
||||
btn_frame = ctk.CTkFrame(frame) if USE_CTK else ttk.Frame(frame)
|
||||
btn_frame.pack(fill="x", pady=(20, 0))
|
||||
|
||||
if USE_CTK:
|
||||
ctk.CTkButton(btn_frame, text="Cancel", width=100, command=self._cancel).pack(side="right", padx=5)
|
||||
ctk.CTkButton(btn_frame, text="Add Video", width=100, command=self._confirm).pack(side="right", padx=5)
|
||||
else:
|
||||
ttk.Button(btn_frame, text="Cancel", command=self._cancel).pack(side="right", padx=5)
|
||||
ttk.Button(btn_frame, text="Add Video", command=self._confirm).pack(side="right", padx=5)
|
||||
|
||||
# Bind Enter key
|
||||
self.dialog.bind("<Return>", lambda e: self._confirm())
|
||||
self.dialog.bind("<Escape>", lambda e: self._cancel())
|
||||
|
||||
def _update_preview(self):
|
||||
topic = self.topic_var.get().strip()
|
||||
topic_slug = slugify(topic) if topic else "session"
|
||||
timestamp = datetime.now().strftime("%Y%m%d-%H%M")
|
||||
new_name = f"gen-{self.gen_number:03d}_{topic_slug}_{timestamp}{self.video_path.suffix}"
|
||||
self.preview_var.set(new_name)
|
||||
|
||||
def _confirm(self):
|
||||
topic = self.topic_var.get().strip()
|
||||
topic_slug = slugify(topic) if topic else "session"
|
||||
timestamp = datetime.now().strftime("%Y%m%d-%H%M")
|
||||
new_name = f"gen-{self.gen_number:03d}_{topic_slug}_{timestamp}{self.video_path.suffix}"
|
||||
|
||||
self.result = {
|
||||
"topic": topic,
|
||||
"new_filename": new_name,
|
||||
"gen_number": self.gen_number
|
||||
}
|
||||
self.dialog.destroy()
|
||||
|
||||
def _cancel(self):
|
||||
self.result = None
|
||||
self.dialog.destroy()
|
||||
|
||||
def show(self):
|
||||
self.dialog.wait_window()
|
||||
return self.result
|
||||
|
||||
|
||||
class CADDocumenterGUI:
|
||||
"""Main GUI application."""
|
||||
|
||||
def __init__(self):
|
||||
self.root = ctk.CTk() if USE_CTK else tk.Tk()
|
||||
self.root.title("CAD Documenter")
|
||||
self.root.geometry("700x600")
|
||||
self.root.geometry("750x650")
|
||||
|
||||
self.current_project: Project | None = None
|
||||
self.config = load_config()
|
||||
@@ -40,45 +166,55 @@ class CADDocumenterGUI:
|
||||
main_frame = ctk.CTkFrame(self.root) if USE_CTK else ttk.Frame(self.root)
|
||||
main_frame.pack(fill="both", expand=True, padx=10, pady=10)
|
||||
|
||||
# Project selection
|
||||
# === Project Section ===
|
||||
project_frame = ctk.CTkFrame(main_frame) if USE_CTK else ttk.LabelFrame(main_frame, text="Project")
|
||||
project_frame.pack(fill="x", pady=(0, 10))
|
||||
|
||||
if USE_CTK:
|
||||
ctk.CTkLabel(project_frame, text="Project:").pack(side="left", padx=5)
|
||||
else:
|
||||
ttk.Label(project_frame, text="Project:").pack(side="left", padx=5)
|
||||
ctk.CTkLabel(project_frame, text="Project:", font=("", 14, "bold")).pack(anchor="w", padx=10, pady=(10, 5))
|
||||
|
||||
proj_row = ctk.CTkFrame(project_frame) if USE_CTK else ttk.Frame(project_frame)
|
||||
proj_row.pack(fill="x", padx=10, pady=(0, 10))
|
||||
|
||||
self.project_var = tk.StringVar()
|
||||
self.project_combo = ctk.CTkComboBox(
|
||||
project_frame,
|
||||
proj_row,
|
||||
variable=self.project_var,
|
||||
values=[],
|
||||
width=400,
|
||||
command=self._on_project_selected
|
||||
) if USE_CTK else ttk.Combobox(project_frame, textvariable=self.project_var)
|
||||
self.project_combo.pack(side="left", fill="x", expand=True, padx=5)
|
||||
) if USE_CTK else ttk.Combobox(proj_row, textvariable=self.project_var, width=50)
|
||||
self.project_combo.pack(side="left", padx=(0, 10))
|
||||
|
||||
if not USE_CTK:
|
||||
self.project_combo.bind("<<ComboboxSelected>>", lambda e: self._on_project_selected(None))
|
||||
|
||||
btn_new = ctk.CTkButton(project_frame, text="+ New", width=70, command=self._new_project) if USE_CTK else ttk.Button(project_frame, text="+ New", command=self._new_project)
|
||||
btn_new = ctk.CTkButton(proj_row, text="+ New", width=80, command=self._new_project) if USE_CTK else ttk.Button(proj_row, text="+ New", command=self._new_project)
|
||||
btn_new.pack(side="left", padx=5)
|
||||
|
||||
btn_open = ctk.CTkButton(project_frame, text="Open", width=70, command=self._open_project) if USE_CTK else ttk.Button(project_frame, text="Open", command=self._open_project)
|
||||
btn_open = ctk.CTkButton(proj_row, text="Open", width=80, command=self._open_project) if USE_CTK else ttk.Button(proj_row, text="Open", command=self._open_project)
|
||||
btn_open.pack(side="left", padx=5)
|
||||
|
||||
# Videos list
|
||||
# Project info
|
||||
self.project_info_var = tk.StringVar(value="No project loaded")
|
||||
if USE_CTK:
|
||||
self.project_info = ctk.CTkLabel(project_frame, textvariable=self.project_info_var, text_color="gray")
|
||||
else:
|
||||
self.project_info = ttk.Label(project_frame, textvariable=self.project_info_var)
|
||||
self.project_info.pack(anchor="w", padx=10, pady=(0, 10))
|
||||
|
||||
# === Videos Section ===
|
||||
videos_frame = ctk.CTkFrame(main_frame) if USE_CTK else ttk.LabelFrame(main_frame, text="Videos")
|
||||
videos_frame.pack(fill="both", expand=True, pady=(0, 10))
|
||||
|
||||
if USE_CTK:
|
||||
ctk.CTkLabel(videos_frame, text="Videos:").pack(anchor="w", padx=5, pady=5)
|
||||
ctk.CTkLabel(videos_frame, text="Session Videos:", font=("", 14, "bold")).pack(anchor="w", padx=10, pady=(10, 5))
|
||||
|
||||
# Listbox for videos
|
||||
list_frame = ctk.CTkFrame(videos_frame) if USE_CTK else ttk.Frame(videos_frame)
|
||||
list_frame.pack(fill="both", expand=True, padx=5, pady=5)
|
||||
list_frame.pack(fill="both", expand=True, padx=10, pady=5)
|
||||
|
||||
self.video_listbox = tk.Listbox(list_frame, height=10, selectmode="extended")
|
||||
self.video_listbox = tk.Listbox(list_frame, height=12, selectmode="extended", font=("Consolas", 10))
|
||||
self.video_listbox.pack(side="left", fill="both", expand=True)
|
||||
|
||||
scrollbar = ttk.Scrollbar(list_frame, orient="vertical", command=self.video_listbox.yview)
|
||||
@@ -87,67 +223,100 @@ class CADDocumenterGUI:
|
||||
|
||||
# Video buttons
|
||||
video_btn_frame = ctk.CTkFrame(videos_frame) if USE_CTK else ttk.Frame(videos_frame)
|
||||
video_btn_frame.pack(fill="x", padx=5, pady=5)
|
||||
video_btn_frame.pack(fill="x", padx=10, pady=10)
|
||||
|
||||
btn_add = ctk.CTkButton(video_btn_frame, text="Add Videos", command=self._add_videos) if USE_CTK else ttk.Button(video_btn_frame, text="Add Videos", command=self._add_videos)
|
||||
btn_add.pack(side="left", padx=5)
|
||||
btn_add = ctk.CTkButton(video_btn_frame, text="+ Add Video", width=120, command=self._add_videos) if USE_CTK else ttk.Button(video_btn_frame, text="+ Add Video", command=self._add_videos)
|
||||
btn_add.pack(side="left", padx=(0, 10))
|
||||
|
||||
btn_remove = ctk.CTkButton(video_btn_frame, text="Remove", command=self._remove_video) if USE_CTK else ttk.Button(video_btn_frame, text="Remove", command=self._remove_video)
|
||||
btn_remove.pack(side="left", padx=5)
|
||||
btn_remove = ctk.CTkButton(video_btn_frame, text="Remove", width=100, command=self._remove_video, fg_color="gray") if USE_CTK else ttk.Button(video_btn_frame, text="Remove", command=self._remove_video)
|
||||
btn_remove.pack(side="left")
|
||||
|
||||
# Drop zone hint
|
||||
# Next gen hint
|
||||
self.next_gen_var = tk.StringVar(value="")
|
||||
if USE_CTK:
|
||||
ctk.CTkLabel(video_btn_frame, text="(Drag & drop videos here)", text_color="gray").pack(side="right", padx=5)
|
||||
ctk.CTkLabel(video_btn_frame, textvariable=self.next_gen_var, text_color="cyan").pack(side="right", padx=10)
|
||||
else:
|
||||
ttk.Label(video_btn_frame, textvariable=self.next_gen_var).pack(side="right", padx=10)
|
||||
|
||||
# Options
|
||||
# === Options Section ===
|
||||
options_frame = ctk.CTkFrame(main_frame) if USE_CTK else ttk.LabelFrame(main_frame, text="Options")
|
||||
options_frame.pack(fill="x", pady=(0, 10))
|
||||
|
||||
if USE_CTK:
|
||||
ctk.CTkLabel(options_frame, text="Options:").pack(anchor="w", padx=5, pady=5)
|
||||
ctk.CTkLabel(options_frame, text="Processing Options:", font=("", 14, "bold")).pack(anchor="w", padx=10, pady=(10, 5))
|
||||
|
||||
opts_inner = ctk.CTkFrame(options_frame) if USE_CTK else ttk.Frame(options_frame)
|
||||
opts_inner.pack(fill="x", padx=10, pady=(0, 10))
|
||||
|
||||
self.export_only_var = tk.BooleanVar(value=True)
|
||||
cb_export = ctk.CTkCheckBox(options_frame, text="Export for Clawdbot (no API)", variable=self.export_only_var) if USE_CTK else ttk.Checkbutton(options_frame, text="Export for Clawdbot (no API)", variable=self.export_only_var)
|
||||
cb_export.pack(anchor="w", padx=20, pady=2)
|
||||
cb_export = ctk.CTkCheckBox(opts_inner, text="Export for Clawdbot (recommended)", variable=self.export_only_var) if USE_CTK else ttk.Checkbutton(opts_inner, text="Export for Clawdbot (recommended)", variable=self.export_only_var)
|
||||
cb_export.pack(anchor="w", pady=2)
|
||||
|
||||
self.scene_detect_var = tk.BooleanVar(value=True)
|
||||
cb_scene = ctk.CTkCheckBox(options_frame, text="Use scene detection", variable=self.scene_detect_var) if USE_CTK else ttk.Checkbutton(options_frame, text="Use scene detection", variable=self.scene_detect_var)
|
||||
cb_scene.pack(anchor="w", padx=20, pady=2)
|
||||
cb_scene = ctk.CTkCheckBox(opts_inner, text="Smart frame extraction (scene detection)", variable=self.scene_detect_var) if USE_CTK else ttk.Checkbutton(opts_inner, text="Smart frame extraction", variable=self.scene_detect_var)
|
||||
cb_scene.pack(anchor="w", pady=2)
|
||||
|
||||
self.transcribe_var = tk.BooleanVar(value=True)
|
||||
cb_trans = ctk.CTkCheckBox(options_frame, text="Whisper transcription", variable=self.transcribe_var) if USE_CTK else ttk.Checkbutton(options_frame, text="Whisper transcription", variable=self.transcribe_var)
|
||||
cb_trans.pack(anchor="w", padx=20, pady=2)
|
||||
cb_trans = ctk.CTkCheckBox(opts_inner, text="Whisper transcription (local GPU)", variable=self.transcribe_var) if USE_CTK else ttk.Checkbutton(opts_inner, text="Whisper transcription", variable=self.transcribe_var)
|
||||
cb_trans.pack(anchor="w", pady=2)
|
||||
|
||||
# Process button
|
||||
# === Process Button ===
|
||||
self.btn_process = ctk.CTkButton(
|
||||
main_frame,
|
||||
text="Process Videos",
|
||||
height=40,
|
||||
text="▶ Process Pending Videos",
|
||||
height=45,
|
||||
font=("", 16, "bold"),
|
||||
command=self._process_videos
|
||||
) if USE_CTK else ttk.Button(main_frame, text="Process Videos", command=self._process_videos)
|
||||
) if USE_CTK else ttk.Button(main_frame, text="Process Pending Videos", command=self._process_videos)
|
||||
self.btn_process.pack(fill="x", pady=(0, 10))
|
||||
|
||||
# Status
|
||||
# === Status Section ===
|
||||
status_frame = ctk.CTkFrame(main_frame) if USE_CTK else ttk.Frame(main_frame)
|
||||
status_frame.pack(fill="x")
|
||||
|
||||
self.status_var = tk.StringVar(value="Ready")
|
||||
self.status_var = tk.StringVar(value="Ready — Open or create a project to start")
|
||||
status_label = ctk.CTkLabel(status_frame, textvariable=self.status_var) if USE_CTK else ttk.Label(status_frame, textvariable=self.status_var)
|
||||
status_label.pack(side="left", padx=5)
|
||||
|
||||
self.progress = ctk.CTkProgressBar(status_frame) if USE_CTK else ttk.Progressbar(status_frame, mode="indeterminate")
|
||||
self.progress.pack(side="right", fill="x", expand=True, padx=5)
|
||||
self.progress = ctk.CTkProgressBar(status_frame, width=200) if USE_CTK else ttk.Progressbar(status_frame, mode="indeterminate", length=200)
|
||||
self.progress.pack(side="right", padx=5)
|
||||
if USE_CTK:
|
||||
self.progress.set(0)
|
||||
|
||||
def _get_next_gen_number(self) -> int:
|
||||
"""Get the next generation number for the current project."""
|
||||
if not self.current_project:
|
||||
return 1
|
||||
|
||||
# Count existing videos as generations
|
||||
return len(self.current_project.manifest.videos) + 1
|
||||
|
||||
def _update_project_list(self):
|
||||
"""Update the project dropdown with recent projects."""
|
||||
# For now, just clear - could load from config/history
|
||||
pass
|
||||
pass # Could load from config/history
|
||||
|
||||
def _update_project_info(self):
|
||||
"""Update the project info display."""
|
||||
if not self.current_project:
|
||||
self.project_info_var.set("No project loaded")
|
||||
self.next_gen_var.set("")
|
||||
return
|
||||
|
||||
m = self.current_project.manifest
|
||||
total = len(m.videos)
|
||||
pending = len(self.current_project.get_pending_videos())
|
||||
processed = total - pending
|
||||
|
||||
info = f"📁 {m.name} — {total} videos ({processed} processed, {pending} pending)"
|
||||
self.project_info_var.set(info)
|
||||
|
||||
next_gen = self._get_next_gen_number()
|
||||
self.next_gen_var.set(f"Next: Gen {next_gen:03d}")
|
||||
|
||||
def _on_project_selected(self, value):
|
||||
"""Handle project selection."""
|
||||
self._refresh_video_list()
|
||||
self._update_project_info()
|
||||
|
||||
def _new_project(self):
|
||||
"""Create a new project."""
|
||||
@@ -156,20 +325,21 @@ class CADDocumenterGUI:
|
||||
return
|
||||
|
||||
name = Path(folder).name
|
||||
# Simple dialog for project name
|
||||
dialog = ctk.CTkInputDialog(
|
||||
text="Enter project name:",
|
||||
title="New Project"
|
||||
) if USE_CTK else None
|
||||
|
||||
# Dialog for project name
|
||||
if USE_CTK:
|
||||
dialog = ctk.CTkInputDialog(
|
||||
text="Enter project name:",
|
||||
title="New Project"
|
||||
)
|
||||
name = dialog.get_input() or name
|
||||
|
||||
try:
|
||||
self.current_project = Project.create(Path(folder), name, "")
|
||||
self.project_var.set(f"{name} ({folder})")
|
||||
self.status_var.set(f"Created project: {name}")
|
||||
self.project_var.set(f"{name}")
|
||||
self.status_var.set(f"✓ Created project: {name}")
|
||||
self._refresh_video_list()
|
||||
self._update_project_info()
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error", str(e))
|
||||
|
||||
@@ -181,9 +351,12 @@ class CADDocumenterGUI:
|
||||
|
||||
try:
|
||||
self.current_project = Project.load(Path(folder))
|
||||
self.project_var.set(f"{self.current_project.manifest.name} ({folder})")
|
||||
self.status_var.set(f"Loaded project: {self.current_project.manifest.name}")
|
||||
self.project_var.set(f"{self.current_project.manifest.name}")
|
||||
self.status_var.set(f"✓ Loaded: {self.current_project.manifest.name}")
|
||||
self._refresh_video_list()
|
||||
self._update_project_info()
|
||||
except FileNotFoundError:
|
||||
messagebox.showerror("Error", f"No project.json found in {folder}\n\nUse '+ New' to create a new project.")
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error", f"Failed to load project: {e}")
|
||||
|
||||
@@ -194,7 +367,7 @@ class CADDocumenterGUI:
|
||||
if not self.current_project:
|
||||
return
|
||||
|
||||
for video in self.current_project.manifest.videos:
|
||||
for i, video in enumerate(self.current_project.manifest.videos, 1):
|
||||
status_icon = {
|
||||
"pending": "⏳",
|
||||
"processed": "✓",
|
||||
@@ -202,35 +375,80 @@ class CADDocumenterGUI:
|
||||
"error": "✗"
|
||||
}.get(video.status, "?")
|
||||
|
||||
self.video_listbox.insert(tk.END, f"{status_icon} {video.filename}")
|
||||
self.video_listbox.insert(tk.END, f"{status_icon} Gen {i:03d}: {video.filename}")
|
||||
|
||||
self._update_project_info()
|
||||
|
||||
def _add_videos(self):
|
||||
"""Add videos to the project."""
|
||||
"""Add videos to the project with topic dialog."""
|
||||
if not self.current_project:
|
||||
messagebox.showwarning("Warning", "Please create or open a project first")
|
||||
return
|
||||
|
||||
files = filedialog.askopenfilenames(
|
||||
title="Select videos",
|
||||
title="Select video to add",
|
||||
filetypes=[
|
||||
("Video files", "*.mp4 *.mkv *.avi *.mov *.webm"),
|
||||
("All files", "*.*")
|
||||
]
|
||||
)
|
||||
|
||||
added = 0
|
||||
for f in files:
|
||||
video_path = Path(f)
|
||||
gen_num = self._get_next_gen_number()
|
||||
|
||||
# Show dialog for topic
|
||||
dialog = AddVideoDialog(self.root, video_path, gen_num)
|
||||
result = dialog.show()
|
||||
|
||||
if result is None:
|
||||
continue # Cancelled
|
||||
|
||||
try:
|
||||
self.current_project.add_video(Path(f), copy=True)
|
||||
# Copy with new name
|
||||
dest = self.current_project.videos_dir / result["new_filename"]
|
||||
shutil.copy2(video_path, dest)
|
||||
|
||||
# Add to manifest (using new filename)
|
||||
from .project import VideoEntry
|
||||
entry = VideoEntry(
|
||||
filename=result["new_filename"],
|
||||
added_at=datetime.now().isoformat(),
|
||||
)
|
||||
self.current_project.manifest.videos.append(entry)
|
||||
self.current_project.save()
|
||||
|
||||
added += 1
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error", f"Failed to add {f}: {e}")
|
||||
|
||||
self._refresh_video_list()
|
||||
self.status_var.set(f"Added {len(files)} video(s)")
|
||||
if added > 0:
|
||||
self._refresh_video_list()
|
||||
self.status_var.set(f"✓ Added {added} video(s)")
|
||||
|
||||
def _remove_video(self):
|
||||
"""Remove selected video from project."""
|
||||
# TODO: Implement video removal
|
||||
messagebox.showinfo("Info", "Video removal not yet implemented")
|
||||
selection = self.video_listbox.curselection()
|
||||
if not selection:
|
||||
return
|
||||
|
||||
if not messagebox.askyesno("Confirm", "Remove selected video(s) from project?"):
|
||||
return
|
||||
|
||||
# Remove in reverse order to maintain indices
|
||||
for idx in reversed(selection):
|
||||
video = self.current_project.manifest.videos[idx]
|
||||
# Delete file
|
||||
video_path = self.current_project.videos_dir / video.filename
|
||||
if video_path.exists():
|
||||
video_path.unlink()
|
||||
# Remove from manifest
|
||||
del self.current_project.manifest.videos[idx]
|
||||
|
||||
self.current_project.save()
|
||||
self._refresh_video_list()
|
||||
self.status_var.set(f"✓ Removed {len(selection)} video(s)")
|
||||
|
||||
def _process_videos(self):
|
||||
"""Process videos in background thread."""
|
||||
@@ -245,7 +463,7 @@ class CADDocumenterGUI:
|
||||
|
||||
# Disable button during processing
|
||||
self.btn_process.configure(state="disabled")
|
||||
self.status_var.set("Processing...")
|
||||
self.status_var.set(f"Processing {len(pending)} video(s)...")
|
||||
|
||||
if USE_CTK:
|
||||
self.progress.set(0)
|
||||
@@ -262,6 +480,8 @@ class CADDocumenterGUI:
|
||||
|
||||
self.root.after(0, lambda: self._on_process_complete(results))
|
||||
except Exception as e:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
self.root.after(0, lambda: self._on_process_error(str(e)))
|
||||
|
||||
thread = threading.Thread(target=process_thread, daemon=True)
|
||||
@@ -278,11 +498,13 @@ class CADDocumenterGUI:
|
||||
self.btn_process.configure(state="normal")
|
||||
self._refresh_video_list()
|
||||
|
||||
msg = f"Processed {results.get('processed', 0)} video(s)"
|
||||
if self.export_only_var.get():
|
||||
msg += f"\n\nExports ready! Tell Clawdbot:\n\"Process CAD report for {self.current_project.manifest.name}\""
|
||||
name = self.current_project.manifest.name
|
||||
msg = f"✓ Processed {results.get('processed', 0)} video(s)"
|
||||
|
||||
self.status_var.set("Complete!")
|
||||
if self.export_only_var.get():
|
||||
msg += f"\n\n📤 Exports ready in clawdbot_export/\n\nTell Mario:\n\"Process CAD session for {name}\""
|
||||
|
||||
self.status_var.set("✓ Complete!")
|
||||
messagebox.showinfo("Success", msg)
|
||||
|
||||
def _on_process_error(self, error):
|
||||
@@ -294,8 +516,8 @@ class CADDocumenterGUI:
|
||||
self.progress.stop()
|
||||
|
||||
self.btn_process.configure(state="normal")
|
||||
self.status_var.set("Error")
|
||||
messagebox.showerror("Error", f"Processing failed: {error}")
|
||||
self.status_var.set("✗ Error")
|
||||
messagebox.showerror("Error", f"Processing failed:\n\n{error}")
|
||||
|
||||
def run(self):
|
||||
"""Run the application."""
|
||||
|
||||
Reference in New Issue
Block a user