- Implemented a new thumbnail editor using Tkinter and MoviePy - Added functionality to load video files and capture frames - Created a modern user interface with a dark theme and responsive design - Included tools for adding text and stickers to thumbnails - Implemented export options for saving edited thumbnails - Added default emoji stickers and functionality to load custom stickers - Enhanced user experience with hover effects and modern button styles
1174 lines
53 KiB
Python
1174 lines
53 KiB
Python
import tkinter as tk
|
|
from tkinter import ttk, filedialog, messagebox
|
|
import threading
|
|
import time
|
|
import os
|
|
import sys
|
|
|
|
# Import the ShortsGeneratorGUI from shorts_generator2.py
|
|
try:
|
|
from shorts_generator2 import ShortsGeneratorGUI
|
|
except ImportError:
|
|
messagebox.showerror("Error", "Could not import ShortsGeneratorGUI from shorts_generator2.py")
|
|
sys.exit(1)
|
|
|
|
class ProgressWindow:
|
|
def __init__(self, parent, title="Processing"):
|
|
self.parent = parent
|
|
self.window = tk.Toplevel(parent)
|
|
self.window.title(title)
|
|
self.window.geometry("450x180")
|
|
self.window.minsize(400, 160)
|
|
self.window.resizable(True, False)
|
|
self.window.transient(parent)
|
|
self.window.grab_set()
|
|
|
|
# Modern colors
|
|
self.colors = {
|
|
'bg_primary': '#1a1a1a',
|
|
'bg_secondary': '#2d2d2d',
|
|
'text_primary': '#ffffff',
|
|
'text_secondary': '#b8b8b8',
|
|
'accent_blue': '#007acc',
|
|
'accent_red': '#dc3545'
|
|
}
|
|
|
|
self.window.configure(bg=self.colors['bg_primary'])
|
|
|
|
# Make window responsive
|
|
self.window.columnconfigure(0, weight=1)
|
|
|
|
# Center the window
|
|
self.window.update_idletasks()
|
|
x = (self.window.winfo_screenwidth() // 2) - (450 // 2)
|
|
y = (self.window.winfo_screenheight() // 2) - (180 // 2)
|
|
self.window.geometry(f"450x180+{x}+{y}")
|
|
|
|
# Bind resize event
|
|
self.window.bind('<Configure>', self.on_window_resize)
|
|
|
|
# Create modern progress interface
|
|
main_frame = tk.Frame(self.window, bg=self.colors['bg_secondary'], relief="flat", bd=0)
|
|
main_frame.pack(fill="both", expand=True, padx=20, pady=20)
|
|
main_frame.columnconfigure(0, weight=1)
|
|
|
|
# Title
|
|
title_label = tk.Label(main_frame, text=title,
|
|
font=('Segoe UI', 14, 'bold'),
|
|
bg=self.colors['bg_secondary'], fg=self.colors['text_primary'])
|
|
title_label.grid(row=0, column=0, sticky="ew", pady=(0, 15))
|
|
|
|
# Status
|
|
self.status_label = tk.Label(main_frame, text="Initializing...",
|
|
font=('Segoe UI', 10), bg=self.colors['bg_secondary'],
|
|
fg=self.colors['text_primary'], anchor="w")
|
|
self.status_label.grid(row=1, column=0, sticky="ew", pady=(0, 5))
|
|
|
|
# Time info
|
|
self.time_label = tk.Label(main_frame, text="Elapsed: 0.0s | Remaining: --s",
|
|
font=('Segoe UI', 9), bg=self.colors['bg_secondary'],
|
|
fg=self.colors['text_secondary'], anchor="w")
|
|
self.time_label.grid(row=2, column=0, sticky="ew", pady=(0, 10))
|
|
|
|
# Modern progress bar styling
|
|
style = ttk.Style()
|
|
style.theme_use('clam')
|
|
style.configure("Modern.Horizontal.TProgressbar",
|
|
background=self.colors['accent_blue'],
|
|
troughcolor=self.colors['bg_primary'],
|
|
borderwidth=0, lightcolor=self.colors['accent_blue'],
|
|
darkcolor=self.colors['accent_blue'])
|
|
|
|
# Main progress bar
|
|
self.progress_var = tk.DoubleVar()
|
|
self.progress_bar = ttk.Progressbar(main_frame, variable=self.progress_var,
|
|
maximum=100, style="Modern.Horizontal.TProgressbar")
|
|
self.progress_bar.grid(row=3, column=0, sticky="ew", pady=(0, 8))
|
|
|
|
# Detection progress bar (hidden by default)
|
|
self.detection_label = tk.Label(main_frame, text="", font=('Segoe UI', 9),
|
|
bg=self.colors['bg_secondary'], fg=self.colors['accent_blue'],
|
|
anchor="w")
|
|
self.detection_progress_var = tk.DoubleVar()
|
|
self.detection_progress_bar = ttk.Progressbar(main_frame, variable=self.detection_progress_var,
|
|
maximum=100, style="Modern.Horizontal.TProgressbar")
|
|
|
|
# Modern cancel button
|
|
self.cancel_btn = tk.Button(main_frame, text="Cancel", command=self.cancel,
|
|
bg=self.colors['accent_red'], fg='white',
|
|
font=('Segoe UI', 10, 'bold'), relief="flat", bd=0,
|
|
pady=8, cursor="hand2")
|
|
self.cancel_btn.grid(row=6, column=0, pady=(10, 0))
|
|
|
|
self.start_time = time.time()
|
|
self.cancelled = False
|
|
|
|
def on_window_resize(self, event):
|
|
"""Handle window resize events"""
|
|
if event.widget == self.window:
|
|
# Update progress bar length based on window width
|
|
width = self.window.winfo_width()
|
|
progress_length = max(250, width - 80) # Minimum 250px, with padding
|
|
try:
|
|
self.progress_bar.config(length=progress_length)
|
|
self.detection_progress_bar.config(length=progress_length)
|
|
except:
|
|
pass
|
|
|
|
def show_detection_progress(self):
|
|
"""Show the detection progress bar - thread safe"""
|
|
def _show():
|
|
self.detection_label.grid(row=3, column=0, sticky="ew", pady=(3, 0))
|
|
self.detection_progress_bar.grid(row=4, column=0, sticky="ew", pady=(3, 5))
|
|
|
|
self.window.after(0, _show)
|
|
|
|
def hide_detection_progress(self):
|
|
"""Hide the detection progress bar - thread safe"""
|
|
def _hide():
|
|
self.detection_label.grid_remove()
|
|
self.detection_progress_bar.grid_remove()
|
|
|
|
self.window.after(0, _hide)
|
|
|
|
def update_progress(self, message, percent):
|
|
"""Update main progress bar - thread safe"""
|
|
def _update():
|
|
if self.cancelled:
|
|
return
|
|
self.status_label.config(text=message)
|
|
self.progress_var.set(percent)
|
|
elapsed = time.time() - self.start_time
|
|
if percent > 0:
|
|
remaining = (elapsed / percent) * (100 - percent)
|
|
self.time_label.config(text=f"Elapsed: {elapsed:.1f}s | Remaining: {remaining:.1f}s")
|
|
|
|
# Schedule the update on the main thread
|
|
self.window.after(0, _update)
|
|
|
|
def update_detection_progress(self, message, percent):
|
|
"""Update detection progress bar - thread safe"""
|
|
def _update():
|
|
if self.cancelled:
|
|
return
|
|
self.detection_label.config(text=message)
|
|
self.detection_progress_var.set(percent)
|
|
|
|
# Schedule the update on the main thread
|
|
self.window.after(0, _update)
|
|
|
|
def cancel(self):
|
|
"""Cancel the current operation"""
|
|
self.cancelled = True
|
|
self.window.destroy()
|
|
|
|
def close(self):
|
|
"""Close the progress window"""
|
|
if not self.cancelled:
|
|
self.window.destroy()
|
|
|
|
class ClipSelectionWindow:
|
|
def __init__(self, parent, clips, video_path, detection_mode):
|
|
self.parent = parent
|
|
self.clips = clips
|
|
self.video_path = video_path
|
|
self.detection_mode = detection_mode
|
|
self.selected_clips = []
|
|
|
|
# Modern colors
|
|
self.colors = {
|
|
'bg_primary': '#1a1a1a',
|
|
'bg_secondary': '#2d2d2d',
|
|
'bg_tertiary': '#3d3d3d',
|
|
'text_primary': '#ffffff',
|
|
'text_secondary': '#b8b8b8',
|
|
'accent_green': '#28a745',
|
|
'accent_red': '#dc3545',
|
|
'accent_blue': '#007acc',
|
|
'border': '#404040'
|
|
}
|
|
|
|
# Create modern window
|
|
self.window = tk.Toplevel(parent.root)
|
|
self.window.title("Select Clips to Generate")
|
|
self.window.geometry("700x600")
|
|
self.window.minsize(500, 400)
|
|
self.window.resizable(True, True)
|
|
self.window.transient(parent.root)
|
|
self.window.grab_set()
|
|
self.window.configure(bg=self.colors['bg_primary'])
|
|
|
|
# Make window responsive
|
|
self.window.rowconfigure(1, weight=1)
|
|
self.window.columnconfigure(0, weight=1)
|
|
|
|
# Center the window
|
|
self.window.update_idletasks()
|
|
x = (self.window.winfo_screenwidth() // 2) - (700 // 2)
|
|
y = (self.window.winfo_screenheight() // 2) - (600 // 2)
|
|
self.window.geometry(f"700x600+{x}+{y}")
|
|
|
|
# Bind resize event
|
|
self.window.bind('<Configure>', self.on_window_resize)
|
|
|
|
self.setup_gui()
|
|
|
|
def setup_gui(self):
|
|
# Header section
|
|
header_frame = tk.Frame(self.window, bg=self.colors['bg_secondary'], relief="flat", bd=0)
|
|
header_frame.grid(row=0, column=0, sticky="ew", padx=20, pady=(20, 0))
|
|
header_frame.columnconfigure(0, weight=1)
|
|
|
|
# Modern title
|
|
title_label = tk.Label(header_frame,
|
|
text=f"🎯 Found {len(self.clips)} clips using {self.detection_mode} detection",
|
|
font=('Segoe UI', 16, 'bold'), bg=self.colors['bg_secondary'],
|
|
fg=self.colors['text_primary'])
|
|
title_label.pack(pady=20)
|
|
|
|
# Instructions with modern styling
|
|
self.instruction_label = tk.Label(header_frame,
|
|
text="Select the clips you want to generate by checking the boxes below:",
|
|
font=('Segoe UI', 11), bg=self.colors['bg_secondary'],
|
|
fg=self.colors['text_secondary'], wraplength=600)
|
|
self.instruction_label.pack(pady=(0, 20))
|
|
|
|
# Main content area
|
|
content_frame = tk.Frame(self.window, bg=self.colors['bg_primary'])
|
|
content_frame.grid(row=1, column=0, sticky="nsew", padx=20, pady=10)
|
|
content_frame.rowconfigure(0, weight=1)
|
|
content_frame.columnconfigure(0, weight=1)
|
|
|
|
# Modern clips list with card design
|
|
list_frame = tk.Frame(content_frame, bg=self.colors['bg_secondary'], relief="flat", bd=0)
|
|
list_frame.grid(row=0, column=0, sticky="nsew", padx=0, pady=0)
|
|
list_frame.rowconfigure(0, weight=1)
|
|
list_frame.columnconfigure(0, weight=1)
|
|
|
|
# Scrollable canvas with modern scrollbar
|
|
canvas = tk.Canvas(list_frame, bg=self.colors['bg_secondary'], highlightthickness=0)
|
|
|
|
# Modern scrollbar styling
|
|
scrollbar = ttk.Scrollbar(list_frame, orient="vertical", command=canvas.yview)
|
|
scrollable_frame = tk.Frame(canvas, bg=self.colors['bg_secondary'])
|
|
|
|
scrollable_frame.bind(
|
|
"<Configure>",
|
|
lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
|
|
)
|
|
|
|
canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
|
|
canvas.configure(yscrollcommand=scrollbar.set)
|
|
|
|
# Modern clip cards
|
|
self.clip_vars = []
|
|
for i, (start, end) in enumerate(self.clips):
|
|
var = tk.BooleanVar(value=True)
|
|
self.clip_vars.append(var)
|
|
|
|
duration = end - start
|
|
|
|
# Modern clip card
|
|
clip_card = tk.Frame(scrollable_frame, bg=self.colors['bg_tertiary'],
|
|
relief="flat", bd=1, highlightbackground=self.colors['border'],
|
|
highlightthickness=1)
|
|
clip_card.pack(fill="x", pady=8, padx=15)
|
|
clip_card.columnconfigure(1, weight=1)
|
|
|
|
# Modern checkbox
|
|
checkbox = tk.Checkbutton(clip_card, variable=var, text="", width=2,
|
|
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary'],
|
|
selectcolor=self.colors['accent_blue'],
|
|
activebackground=self.colors['bg_tertiary'],
|
|
relief="flat", bd=0)
|
|
checkbox.grid(row=0, column=0, padx=15, pady=15, sticky="w")
|
|
|
|
# Clip info with modern typography
|
|
info_frame = tk.Frame(clip_card, bg=self.colors['bg_tertiary'])
|
|
info_frame.grid(row=0, column=1, sticky="ew", padx=(0, 15), pady=15)
|
|
|
|
clip_title = tk.Label(info_frame, text=f"Clip {i+1}",
|
|
font=('Segoe UI', 11, 'bold'), bg=self.colors['bg_tertiary'],
|
|
fg=self.colors['text_primary'], anchor="w")
|
|
clip_title.pack(anchor="w")
|
|
|
|
clip_details = tk.Label(info_frame,
|
|
text=f"⏱️ {start:.1f}s - {end:.1f}s • Duration: {duration:.1f}s",
|
|
font=('Segoe UI', 10), bg=self.colors['bg_tertiary'],
|
|
fg=self.colors['text_secondary'], anchor="w")
|
|
clip_details.pack(anchor="w", pady=(2, 0))
|
|
|
|
canvas.grid(row=0, column=0, sticky="nsew")
|
|
scrollbar.grid(row=0, column=1, sticky="ns")
|
|
|
|
# Modern action buttons
|
|
action_frame = tk.Frame(self.window, bg=self.colors['bg_primary'])
|
|
action_frame.grid(row=2, column=0, sticky="ew", padx=20, pady=(10, 20))
|
|
action_frame.columnconfigure(0, weight=1)
|
|
action_frame.columnconfigure(1, weight=1)
|
|
action_frame.columnconfigure(2, weight=1)
|
|
action_frame.columnconfigure(3, weight=1)
|
|
|
|
# Selection buttons
|
|
select_all_btn = self.create_modern_button(action_frame, "✅ Select All",
|
|
self.select_all, self.colors['accent_blue'])
|
|
select_all_btn.grid(row=0, column=0, padx=(0, 5), sticky="ew")
|
|
|
|
select_none_btn = self.create_modern_button(action_frame, "❌ Select None",
|
|
self.select_none, self.colors['bg_tertiary'])
|
|
select_none_btn.grid(row=0, column=1, padx=5, sticky="ew")
|
|
|
|
# Action buttons
|
|
cancel_btn = self.create_modern_button(action_frame, "Cancel",
|
|
self.cancel, self.colors['accent_red'])
|
|
cancel_btn.grid(row=0, column=2, padx=5, sticky="ew")
|
|
|
|
generate_btn = self.create_modern_button(action_frame, "🎬 Generate Selected",
|
|
self.generate_selected, self.colors['accent_green'])
|
|
generate_btn.grid(row=0, column=3, padx=(5, 0), sticky="ew")
|
|
|
|
def create_modern_button(self, parent, text, command, color):
|
|
"""Create a modern button for the clip selection window"""
|
|
button = tk.Button(parent, text=text, command=command,
|
|
bg=color, fg='white', font=('Segoe UI', 10, 'bold'),
|
|
relief="flat", bd=0, pady=10, cursor="hand2")
|
|
|
|
# Add hover effect
|
|
original_color = color
|
|
def on_enter(e):
|
|
# Lighten color on hover
|
|
button.config(bg=self.lighten_color(original_color, 0.2))
|
|
|
|
def on_leave(e):
|
|
button.config(bg=original_color)
|
|
|
|
button.bind("<Enter>", on_enter)
|
|
button.bind("<Leave>", on_leave)
|
|
|
|
return button
|
|
|
|
def lighten_color(self, hex_color, factor):
|
|
"""Lighten a hex color by a factor (0.0 to 1.0)"""
|
|
hex_color = hex_color.lstrip('#')
|
|
rgb = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
|
|
rgb = tuple(min(255, int(c + (255 - c) * factor)) for c in rgb)
|
|
return f"#{rgb[0]:02x}{rgb[1]:02x}{rgb[2]:02x}"
|
|
|
|
def on_window_resize(self, event):
|
|
"""Handle window resize events"""
|
|
if event.widget == self.window:
|
|
# Get current window size
|
|
width = self.window.winfo_width()
|
|
|
|
# Adjust wrap length for instruction text
|
|
wrap_length = max(300, width - 100)
|
|
try:
|
|
self.instruction_label.config(wraplength=wrap_length)
|
|
except:
|
|
pass
|
|
|
|
def select_all(self):
|
|
"""Select all clips"""
|
|
for var in self.clip_vars:
|
|
var.set(True)
|
|
|
|
def select_none(self):
|
|
"""Deselect all clips"""
|
|
for var in self.clip_vars:
|
|
var.set(False)
|
|
|
|
def cancel(self):
|
|
"""Cancel selection"""
|
|
self.window.destroy()
|
|
|
|
def generate_selected(self):
|
|
"""Generate the selected clips"""
|
|
# Get selected clips
|
|
self.selected_clips = []
|
|
for i, var in enumerate(self.clip_vars):
|
|
if var.get():
|
|
self.selected_clips.append(self.clips[i])
|
|
|
|
if not self.selected_clips:
|
|
messagebox.showwarning("No Selection", "Please select at least one clip to generate.")
|
|
return
|
|
|
|
# Close selection window
|
|
self.window.destroy()
|
|
|
|
# Trigger generation with selected clips
|
|
if hasattr(self.parent, 'generate_selected_clips'):
|
|
self.parent.generate_selected_clips(self.selected_clips)
|
|
else:
|
|
messagebox.showerror("Error", "Generation method not available.")
|
|
|
|
class MainApplication:
|
|
def __init__(self):
|
|
self.root = tk.Tk()
|
|
self.root.title("AI Shorts Generator - Main Controller")
|
|
self.root.geometry("800x600") # Wider window for horizontal layout
|
|
self.root.minsize(700, 500) # Increased minimum width for horizontal layout
|
|
|
|
# Modern color scheme
|
|
self.colors = {
|
|
'bg_primary': '#1a1a1a', # Dark background
|
|
'bg_secondary': '#2d2d2d', # Card backgrounds
|
|
'bg_tertiary': '#3d3d3d', # Elevated elements
|
|
'accent_blue': '#007acc', # Primary blue
|
|
'accent_green': '#28a745', # Success green
|
|
'accent_orange': '#fd7e14', # Warning orange
|
|
'accent_purple': '#6f42c1', # Secondary purple
|
|
'accent_red': '#dc3545', # Error red
|
|
'text_primary': '#ffffff', # Primary text
|
|
'text_secondary': '#b8b8b8', # Secondary text
|
|
'text_muted': '#6c757d', # Muted text
|
|
'border': '#404040', # Border color
|
|
'hover': '#4a4a4a' # Hover state
|
|
}
|
|
|
|
self.root.configure(bg=self.colors['bg_primary'])
|
|
|
|
# Modern fonts
|
|
self.fonts = {
|
|
'title': ('Segoe UI', 18, 'bold'),
|
|
'heading': ('Segoe UI', 12, 'bold'),
|
|
'body': ('Segoe UI', 10),
|
|
'caption': ('Segoe UI', 9),
|
|
'button': ('Segoe UI', 10, 'bold')
|
|
}
|
|
|
|
# Make window responsive
|
|
self.root.rowconfigure(0, weight=1)
|
|
self.root.columnconfigure(0, weight=1)
|
|
|
|
# Initialize the ShortsGeneratorGUI will be created when needed
|
|
self.shorts_generator = None
|
|
|
|
self.setup_gui()
|
|
|
|
# Bind resize event for responsive updates
|
|
self.root.bind('<Configure>', self.on_window_resize)
|
|
|
|
def get_shorts_generator(self):
|
|
"""Get or create a minimal ShortsGenerator instance when needed"""
|
|
if self.shorts_generator is None:
|
|
try:
|
|
# Create a simple container class with just the attributes we need
|
|
class MinimalShortsGenerator:
|
|
def __init__(self):
|
|
self.video_path = None
|
|
self.output_folder = "shorts"
|
|
self.max_clips = 3
|
|
self.threshold_db = -30
|
|
self.clip_duration = 5
|
|
self.detection_mode_var = tk.StringVar(value="loud")
|
|
|
|
self.shorts_generator = MinimalShortsGenerator()
|
|
print("✅ Minimal ShortsGenerator initialized successfully")
|
|
except Exception as e:
|
|
print(f"❌ Failed to initialize ShortsGenerator: {e}")
|
|
messagebox.showerror("Initialization Error", f"Failed to initialize ShortsGenerator: {e}")
|
|
return None
|
|
return self.shorts_generator
|
|
def setup_gui(self):
|
|
"""Setup the main GUI with modern horizontal design"""
|
|
# Create main container that fills the window
|
|
main_container = tk.Frame(self.root, bg=self.colors['bg_primary'])
|
|
main_container.pack(fill="both", expand=True, padx=20, pady=20)
|
|
|
|
# Modern title with gradient effect simulation
|
|
title_frame = tk.Frame(main_container, bg=self.colors['bg_primary'])
|
|
title_frame.pack(fill="x", pady=(0, 20))
|
|
|
|
title_label = tk.Label(title_frame, text="🎬 AI Shorts Generator",
|
|
font=self.fonts['title'], bg=self.colors['bg_primary'],
|
|
fg=self.colors['text_primary'])
|
|
title_label.pack()
|
|
|
|
subtitle_label = tk.Label(title_frame, text="Create viral content with AI-powered video analysis",
|
|
font=self.fonts['caption'], bg=self.colors['bg_primary'],
|
|
fg=self.colors['text_secondary'])
|
|
subtitle_label.pack(pady=(5, 0))
|
|
|
|
# Create horizontal layout with left and right panels
|
|
content_frame = tk.Frame(main_container, bg=self.colors['bg_primary'])
|
|
content_frame.pack(fill="both", expand=True)
|
|
|
|
# Left panel - Video Selection and Settings
|
|
left_panel = tk.Frame(content_frame, bg=self.colors['bg_primary'])
|
|
left_panel.pack(side="left", fill="both", expand=True, padx=(0, 15))
|
|
|
|
# Modern card-style file selection frame
|
|
file_card = self.create_modern_card(left_panel, "📁 Video Selection")
|
|
|
|
self.file_label = tk.Label(file_card, text="No video selected",
|
|
font=self.fonts['body'], bg=self.colors['bg_tertiary'],
|
|
fg=self.colors['text_secondary'], relief="flat",
|
|
anchor="w", pady=12, padx=15, bd=1,
|
|
highlightbackground=self.colors['border'],
|
|
highlightthickness=1)
|
|
self.file_label.pack(fill="x", pady=(0, 10))
|
|
|
|
# Modern button with hover effect
|
|
select_btn = self.create_modern_button(file_card, "📁 Select Video File",
|
|
self.select_video_file, self.colors['accent_blue'])
|
|
select_btn.pack(fill="x", pady=5)
|
|
|
|
# Modern settings card
|
|
settings_card = self.create_modern_card(left_panel, "⚙️ Detection Settings")
|
|
|
|
# Detection mode with modern styling
|
|
mode_label = tk.Label(settings_card, text="Detection Mode:",
|
|
font=self.fonts['heading'], bg=self.colors['bg_secondary'],
|
|
fg=self.colors['text_primary'])
|
|
mode_label.pack(anchor="w", pady=(0, 10))
|
|
|
|
self.detection_var = tk.StringVar(value="loud")
|
|
detection_container = tk.Frame(settings_card, bg=self.colors['bg_secondary'])
|
|
detection_container.pack(fill="x", pady=5)
|
|
|
|
modes = [("🔊 Loud Moments", "loud"), ("🎬 Scene Changes", "scene"), ("🏃 Motion", "motion"),
|
|
("💬 Speech", "speech"), ("🎵 Audio Peaks", "peaks"), ("🎯 Combined", "combined")]
|
|
|
|
# Create modern radio buttons in rows
|
|
for i in range(0, len(modes), 3): # 3 per row
|
|
row_frame = tk.Frame(detection_container, bg=self.colors['bg_secondary'])
|
|
row_frame.pack(fill="x", pady=3)
|
|
|
|
for j in range(3):
|
|
if i + j < len(modes):
|
|
text, value = modes[i + j]
|
|
radio_frame = tk.Frame(row_frame, bg=self.colors['bg_tertiary'],
|
|
relief="flat", bd=1)
|
|
radio_frame.pack(side="left", fill="x", expand=True, padx=5)
|
|
|
|
radio = tk.Radiobutton(radio_frame, text=text, variable=self.detection_var,
|
|
value=value, bg=self.colors['bg_tertiary'],
|
|
fg=self.colors['text_primary'], font=self.fonts['body'],
|
|
selectcolor=self.colors['accent_blue'],
|
|
activebackground=self.colors['hover'],
|
|
activeforeground=self.colors['text_primary'],
|
|
relief="flat", bd=0)
|
|
radio.pack(pady=8, padx=10)
|
|
|
|
# Right panel - Actions and Controls
|
|
right_panel = tk.Frame(content_frame, bg=self.colors['bg_primary'])
|
|
right_panel.pack(side="right", fill="y", padx=(15, 0))
|
|
right_panel.config(width=300) # Fixed width for actions panel
|
|
|
|
# Modern action buttons card
|
|
button_card = self.create_modern_card(right_panel, "🚀 Actions")
|
|
|
|
# Preview button
|
|
self.preview_btn = self.create_modern_button(button_card, "🔍 Preview Clips",
|
|
self.preview_clips_threaded,
|
|
self.colors['accent_blue'])
|
|
self.preview_btn.pack(fill="x", pady=5)
|
|
|
|
# Generate button - primary action
|
|
self.generate_btn = self.create_modern_button(button_card, "🎬 Generate All Clips",
|
|
self.generate_shorts_threaded,
|
|
self.colors['accent_green'], large=True)
|
|
self.generate_btn.pack(fill="x", pady=5)
|
|
|
|
# Secondary action buttons
|
|
self.edit_btn = self.create_modern_button(button_card, "✏️ Edit Generated Shorts",
|
|
self.open_editor, self.colors['accent_orange'])
|
|
self.edit_btn.pack(fill="x", pady=5)
|
|
|
|
self.thumbnail_btn = self.create_modern_button(button_card, "📸 Create Thumbnails",
|
|
self.open_thumbnails, self.colors['accent_purple'])
|
|
self.thumbnail_btn.pack(fill="x", pady=5)
|
|
|
|
# Info tip with modern styling - moved to bottom
|
|
tip_frame = tk.Frame(button_card, bg=self.colors['bg_secondary'])
|
|
tip_frame.pack(fill="x", pady=(20, 0))
|
|
|
|
tip_icon = tk.Label(tip_frame, text="💡", font=self.fonts['body'],
|
|
bg=self.colors['bg_secondary'], fg=self.colors['accent_orange'])
|
|
tip_icon.pack(side="left", padx=(0, 8))
|
|
|
|
info_label = tk.Label(tip_frame, text="Tip: Use 'Preview Clips' to select specific clips for faster processing",
|
|
font=self.fonts['caption'], fg=self.colors['text_muted'],
|
|
bg=self.colors['bg_secondary'], wraplength=250, anchor="w")
|
|
info_label.pack(side="left", fill="x", expand=True)
|
|
|
|
# Modern status bar at the bottom
|
|
status_frame = tk.Frame(main_container, bg=self.colors['bg_tertiary'],
|
|
relief="flat", bd=1)
|
|
status_frame.pack(fill="x", pady=(20, 0))
|
|
|
|
status_icon = tk.Label(status_frame, text="●", font=self.fonts['body'],
|
|
bg=self.colors['bg_tertiary'], fg=self.colors['accent_green'])
|
|
status_icon.pack(side="left", padx=15, pady=8)
|
|
|
|
self.status_label = tk.Label(status_frame, text="Ready - Select a video to begin",
|
|
font=self.fonts['caption'], fg=self.colors['text_secondary'],
|
|
bg=self.colors['bg_tertiary'], wraplength=400, anchor="w")
|
|
self.status_label.pack(side="left", fill="x", expand=True, pady=8, padx=(0, 15))
|
|
|
|
# Store detected clips for selection
|
|
self.detected_clips = []
|
|
|
|
def create_modern_card(self, parent, title):
|
|
"""Create a modern card-style container"""
|
|
card_frame = tk.Frame(parent, bg=self.colors['bg_secondary'], relief="flat", bd=0)
|
|
card_frame.pack(fill="x", pady=10)
|
|
|
|
# Card header
|
|
header_frame = tk.Frame(card_frame, bg=self.colors['bg_secondary'])
|
|
header_frame.pack(fill="x", padx=20, pady=(15, 5))
|
|
|
|
header_label = tk.Label(header_frame, text=title, font=self.fonts['heading'],
|
|
bg=self.colors['bg_secondary'], fg=self.colors['text_primary'])
|
|
header_label.pack(anchor="w")
|
|
|
|
# Card content area
|
|
content_frame = tk.Frame(card_frame, bg=self.colors['bg_secondary'])
|
|
content_frame.pack(fill="both", expand=True, padx=20, pady=(5, 20))
|
|
|
|
return content_frame
|
|
|
|
def create_modern_button(self, parent, text, command, color, large=False):
|
|
"""Create a modern button with hover effects"""
|
|
font = self.fonts['button'] if not large else ('Segoe UI', 12, 'bold')
|
|
pady = 12 if not large else 15
|
|
|
|
button = tk.Button(parent, text=text, command=command,
|
|
bg=color, fg='white', font=font,
|
|
relief="flat", bd=0, pady=pady,
|
|
activebackground=self.adjust_color(color, -20),
|
|
activeforeground='white',
|
|
cursor="hand2")
|
|
|
|
# Add hover effects
|
|
def on_enter(e):
|
|
button.config(bg=self.adjust_color(color, 15))
|
|
|
|
def on_leave(e):
|
|
button.config(bg=color)
|
|
|
|
button.bind("<Enter>", on_enter)
|
|
button.bind("<Leave>", on_leave)
|
|
|
|
return button
|
|
|
|
def adjust_color(self, hex_color, adjustment):
|
|
"""Adjust color brightness for hover effects"""
|
|
# Remove # if present
|
|
hex_color = hex_color.lstrip('#')
|
|
|
|
# Convert to RGB
|
|
rgb = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
|
|
|
|
# Adjust brightness
|
|
adjusted = tuple(max(0, min(255, c + adjustment)) for c in rgb)
|
|
|
|
# Convert back to hex
|
|
return f"#{adjusted[0]:02x}{adjusted[1]:02x}{adjusted[2]:02x}"
|
|
|
|
def on_window_resize(self, event):
|
|
"""Handle window resize events for responsive layout"""
|
|
if event.widget == self.root:
|
|
# Get current window size
|
|
width = self.root.winfo_width()
|
|
height = self.root.winfo_height()
|
|
|
|
# Adjust font sizes based on window width
|
|
if width < 450:
|
|
title_font_size = 14
|
|
button_font_size = 10
|
|
info_wrap_length = 300
|
|
elif width < 550:
|
|
title_font_size = 15
|
|
button_font_size = 11
|
|
info_wrap_length = 350
|
|
else:
|
|
title_font_size = 16
|
|
button_font_size = 12
|
|
info_wrap_length = 400
|
|
|
|
# Update wraplength for text elements
|
|
try:
|
|
# Find and update info label
|
|
for widget in self.root.winfo_children():
|
|
if isinstance(widget, tk.Frame):
|
|
for subwidget in widget.winfo_children():
|
|
if isinstance(subwidget, tk.Frame):
|
|
for subsubwidget in subwidget.winfo_children():
|
|
if isinstance(subsubwidget, tk.Label) and "Tip:" in str(subsubwidget.cget("text")):
|
|
subsubwidget.config(wraplength=info_wrap_length)
|
|
elif isinstance(subsubwidget, tk.Label) and "Ready" in str(subsubwidget.cget("text")):
|
|
subsubwidget.config(wraplength=info_wrap_length)
|
|
except:
|
|
pass # Ignore any errors during dynamic updates
|
|
|
|
def select_video_file(self):
|
|
"""Select video file"""
|
|
filetypes = [
|
|
("Video files", "*.mp4 *.avi *.mov *.mkv *.wmv *.flv *.webm"),
|
|
("All files", "*.*")
|
|
]
|
|
|
|
file_path = filedialog.askopenfilename(
|
|
title="Select Video File",
|
|
filetypes=filetypes
|
|
)
|
|
|
|
if file_path:
|
|
# Update display
|
|
filename = os.path.basename(file_path)
|
|
self.file_label.config(text=filename)
|
|
|
|
# Update shorts generator
|
|
generator = self.get_shorts_generator()
|
|
if generator:
|
|
generator.video_path = file_path
|
|
if hasattr(generator, 'video_label'):
|
|
generator.video_label.config(text=os.path.basename(file_path))
|
|
|
|
self.status_label.config(text=f"Video loaded: {filename}")
|
|
|
|
def preview_clips_threaded(self):
|
|
"""Run preview clips with progress window"""
|
|
generator = self.get_shorts_generator()
|
|
if not generator or not hasattr(generator, 'video_path') or not generator.video_path:
|
|
messagebox.showwarning("No Video", "Please select a video file first.")
|
|
return
|
|
|
|
# Update settings in shorts generator
|
|
self.update_shorts_generator_settings()
|
|
|
|
# Create progress window
|
|
progress_window = ProgressWindow(self.root, "Previewing Clips")
|
|
|
|
# Show detection progress for heavy modes
|
|
detection_mode = self.detection_var.get()
|
|
if detection_mode in ["scene", "motion", "speech", "peaks", "combined"]:
|
|
progress_window.show_detection_progress()
|
|
|
|
def run_preview():
|
|
try:
|
|
from shorts_generator2 import (detect_loud_moments, detect_scene_changes_with_progress,
|
|
detect_motion_intensity_with_progress, detect_speech_emotion_with_progress,
|
|
detect_audio_peaks_with_progress, detect_combined_intensity_with_progress,
|
|
validate_video)
|
|
|
|
generator = self.get_shorts_generator()
|
|
if not generator or not hasattr(generator, 'video_path'):
|
|
raise Exception("ShortsGeneratorGUI not properly initialized")
|
|
|
|
video_path = generator.video_path
|
|
clip_duration = 5 # Fixed clip duration since we removed the setting
|
|
detection_mode = self.detection_var.get()
|
|
|
|
# Thread-safe progress callbacks with cancellation checks
|
|
def progress_callback(message, percent):
|
|
if progress_window.cancelled:
|
|
raise InterruptedError("Preview cancelled by user")
|
|
self.root.after(0, lambda: progress_window.update_progress(message, percent))
|
|
|
|
def detection_callback(message, percent):
|
|
if progress_window.cancelled:
|
|
raise InterruptedError("Preview cancelled by user")
|
|
self.root.after(0, lambda: progress_window.update_detection_progress(message, percent))
|
|
|
|
# Check for cancellation before starting
|
|
if progress_window.cancelled:
|
|
return
|
|
|
|
# Validate video first
|
|
self.root.after(0, lambda: progress_callback("Validating video...", 5))
|
|
validate_video(video_path, min_duration=clip_duration * 2)
|
|
|
|
# Run detection based on mode
|
|
self.root.after(0, lambda: progress_callback(f"Analyzing {detection_mode} moments...", 10))
|
|
|
|
if detection_mode == "loud":
|
|
moments = detect_loud_moments(video_path, chunk_duration=clip_duration, threshold_db=-30)
|
|
elif detection_mode == "scene":
|
|
moments = detect_scene_changes_with_progress(video_path, chunk_duration=clip_duration,
|
|
progress_callback=detection_callback)
|
|
elif detection_mode == "motion":
|
|
moments = detect_motion_intensity_with_progress(video_path, chunk_duration=clip_duration,
|
|
progress_callback=detection_callback)
|
|
elif detection_mode == "speech":
|
|
moments = detect_speech_emotion_with_progress(video_path, chunk_duration=clip_duration,
|
|
progress_callback=detection_callback)
|
|
elif detection_mode == "peaks":
|
|
moments = detect_audio_peaks_with_progress(video_path, chunk_duration=clip_duration,
|
|
progress_callback=detection_callback)
|
|
elif detection_mode == "combined":
|
|
moments = detect_combined_intensity_with_progress(video_path, chunk_duration=clip_duration,
|
|
progress_callback=detection_callback)
|
|
else:
|
|
moments = detect_loud_moments(video_path, chunk_duration=clip_duration, threshold_db=-30)
|
|
|
|
# Final progress updates without cancellation checks (process is nearly complete)
|
|
if not progress_window.cancelled:
|
|
self.root.after(0, lambda: progress_window.update_progress("Analysis complete!", 90))
|
|
|
|
# Show results with selection window
|
|
def show_results():
|
|
if progress_window.cancelled:
|
|
return
|
|
if moments:
|
|
self.detected_clips = moments # Store for potential generation
|
|
progress_window.close()
|
|
|
|
# Show clip selection window
|
|
ClipSelectionWindow(self, moments, video_path, detection_mode)
|
|
else:
|
|
progress_window.close()
|
|
messagebox.showwarning("No Results", "No interesting moments found with current settings.")
|
|
|
|
if not progress_window.cancelled:
|
|
self.root.after(0, lambda: progress_window.update_progress("Preparing results...", 100))
|
|
self.root.after(500, show_results)
|
|
|
|
except InterruptedError:
|
|
# Preview was cancelled by user
|
|
def show_cancelled():
|
|
progress_window.close()
|
|
# Don't show an error message for user cancellation
|
|
|
|
self.root.after(0, show_cancelled)
|
|
|
|
except Exception as e:
|
|
error_msg = str(e)
|
|
def show_error():
|
|
progress_window.close()
|
|
messagebox.showerror("Preview Error", f"An error occurred during preview:\n{error_msg}")
|
|
|
|
self.root.after(0, show_error)
|
|
|
|
# Start the preview in a background thread
|
|
threading.Thread(target=run_preview, daemon=True).start()
|
|
|
|
def generate_shorts_threaded(self):
|
|
"""Run generate shorts with progress window"""
|
|
generator = self.get_shorts_generator()
|
|
if not generator or not hasattr(generator, 'video_path') or not generator.video_path:
|
|
messagebox.showwarning("No Video", "Please select a video file first.")
|
|
return
|
|
|
|
# Update settings
|
|
self.update_shorts_generator_settings()
|
|
|
|
# Create progress window
|
|
progress_window = ProgressWindow(self.root, "Generating Shorts")
|
|
|
|
# Show detection progress for heavy modes
|
|
detection_mode = self.detection_var.get()
|
|
if detection_mode in ["scene", "motion", "speech", "peaks", "combined"]:
|
|
progress_window.show_detection_progress()
|
|
|
|
def run_generation():
|
|
try:
|
|
from shorts_generator2 import generate_shorts
|
|
|
|
generator = self.get_shorts_generator()
|
|
if not generator or not hasattr(generator, 'video_path'):
|
|
raise Exception("ShortsGeneratorGUI not properly initialized")
|
|
|
|
video_path = generator.video_path
|
|
detection_mode = self.detection_var.get()
|
|
clip_duration = 5 # Default duration
|
|
|
|
# Thread-safe progress callbacks with cancellation checks
|
|
def progress_callback(message, percent):
|
|
if progress_window.cancelled:
|
|
raise InterruptedError("Generation cancelled by user")
|
|
self.root.after(0, lambda: progress_window.update_progress(message, percent))
|
|
|
|
def detection_callback(message, percent):
|
|
if progress_window.cancelled:
|
|
raise InterruptedError("Generation cancelled by user")
|
|
self.root.after(0, lambda: progress_window.update_detection_progress(message, percent))
|
|
|
|
# Check for cancellation before starting
|
|
if progress_window.cancelled:
|
|
return
|
|
|
|
# Run the actual generation (no max clips limit - user will select from preview)
|
|
generate_shorts(
|
|
video_path,
|
|
max_clips=50, # Generate a reasonable number for preview/selection
|
|
output_folder="shorts",
|
|
progress_callback=progress_callback,
|
|
detection_progress_callback=detection_callback,
|
|
threshold_db=-30,
|
|
clip_duration=5,
|
|
detection_mode=detection_mode
|
|
)
|
|
|
|
def show_success():
|
|
progress_window.close()
|
|
messagebox.showinfo("Success", "Successfully generated shorts!\n\nCheck the 'shorts' folder for your videos.")
|
|
|
|
self.root.after(0, show_success)
|
|
|
|
except InterruptedError:
|
|
# Generation was cancelled by user
|
|
def show_cancelled():
|
|
progress_window.close()
|
|
# Don't show an error message for user cancellation
|
|
|
|
self.root.after(0, show_cancelled)
|
|
|
|
except Exception as e:
|
|
error_msg = str(e)
|
|
def show_error():
|
|
progress_window.close()
|
|
messagebox.showerror("Generation Error", f"An error occurred during generation:\n{error_msg}")
|
|
|
|
self.root.after(0, show_error)
|
|
|
|
# Start the generation in a background thread
|
|
threading.Thread(target=run_generation, daemon=True).start()
|
|
|
|
def generate_selected_clips(self, selected_clips):
|
|
"""Generate only the selected clips"""
|
|
generator = self.get_shorts_generator()
|
|
if not generator or not hasattr(generator, 'video_path') or not generator.video_path:
|
|
messagebox.showerror("Error", "No video selected.")
|
|
return
|
|
|
|
# Create progress window
|
|
progress_window = ProgressWindow(self.root, "Generating Selected Clips")
|
|
|
|
def run_selected_generation():
|
|
try:
|
|
generator = self.get_shorts_generator()
|
|
if not generator or not hasattr(generator, 'video_path'):
|
|
raise Exception("ShortsGeneratorGUI not properly initialized")
|
|
|
|
video_path = generator.video_path
|
|
|
|
# Thread-safe progress callback with cancellation checks
|
|
def progress_callback(message, percent):
|
|
if progress_window.cancelled:
|
|
raise InterruptedError("Generation cancelled by user")
|
|
self.root.after(0, lambda: progress_window.update_progress(message, percent))
|
|
|
|
# Check for cancellation before starting
|
|
if progress_window.cancelled:
|
|
return
|
|
|
|
# Use a custom generation function for selected clips
|
|
self.generate_clips_from_moments(
|
|
video_path,
|
|
selected_clips,
|
|
progress_callback,
|
|
progress_window
|
|
)
|
|
|
|
def show_success():
|
|
progress_window.close()
|
|
messagebox.showinfo("Success", f"Successfully generated {len(selected_clips)} selected clips!\n\nCheck the 'shorts' folder for your videos.")
|
|
|
|
self.root.after(0, show_success)
|
|
|
|
except InterruptedError:
|
|
# Generation was cancelled by user
|
|
def show_cancelled():
|
|
progress_window.close()
|
|
# Don't show an error message for user cancellation
|
|
|
|
self.root.after(0, show_cancelled)
|
|
|
|
except Exception as e:
|
|
error_msg = str(e)
|
|
def show_error():
|
|
progress_window.close()
|
|
messagebox.showerror("Generation Error", f"An error occurred during generation:\n{error_msg}")
|
|
|
|
self.root.after(0, show_error)
|
|
|
|
# Start the generation in a background thread
|
|
threading.Thread(target=run_selected_generation, daemon=True).start()
|
|
|
|
def generate_clips_from_moments(self, video_path, moments, progress_callback, progress_window):
|
|
"""Generate video clips from specific moments"""
|
|
try:
|
|
import os
|
|
from moviepy.editor import VideoFileClip
|
|
|
|
os.makedirs("shorts", exist_ok=True)
|
|
|
|
total_clips = len(moments)
|
|
for i, (start, end) in enumerate(moments):
|
|
if progress_window.cancelled:
|
|
break
|
|
|
|
progress_callback(f"Processing clip {i+1}/{total_clips}...", (i/total_clips) * 90)
|
|
|
|
# Create clip
|
|
with VideoFileClip(video_path) as video:
|
|
clip = video.subclipped(start, end)
|
|
output_path = os.path.join("shorts", f"short_{i+1}.mp4")
|
|
clip.write_videofile(output_path, verbose=False, logger=None)
|
|
clip.close()
|
|
|
|
progress_callback("All clips generated successfully!", 100)
|
|
|
|
except ImportError as e:
|
|
# MoviePy not available, use alternative approach
|
|
progress_callback("Using alternative generation method...", 10)
|
|
|
|
# Check for cancellation before starting
|
|
if progress_window.cancelled:
|
|
return
|
|
|
|
# Import and use the existing generate_shorts function
|
|
from shorts_generator2 import generate_shorts
|
|
|
|
# Create a temporary callback that updates our progress window and checks for cancellation
|
|
def alt_progress_callback(message, percent):
|
|
if progress_window.cancelled:
|
|
# Try to indicate cancellation to the calling function
|
|
raise InterruptedError("Generation cancelled by user")
|
|
progress_callback(message, percent)
|
|
|
|
def alt_detection_callback(message, percent):
|
|
if progress_window.cancelled:
|
|
raise InterruptedError("Generation cancelled by user")
|
|
progress_callback(f"Detection: {message}", percent)
|
|
|
|
try:
|
|
# Use generate_shorts with the exact number of selected clips
|
|
# This ensures we generate exactly what the user selected
|
|
generate_shorts(
|
|
video_path,
|
|
max_clips=len(moments), # Generate exactly the number of selected clips
|
|
output_folder="shorts",
|
|
progress_callback=alt_progress_callback,
|
|
detection_progress_callback=alt_detection_callback,
|
|
threshold_db=-30,
|
|
clip_duration=5,
|
|
detection_mode="loud" # Default detection mode
|
|
)
|
|
except InterruptedError:
|
|
# Generation was cancelled
|
|
progress_callback("Generation cancelled by user", 0)
|
|
return
|
|
|
|
except Exception as e:
|
|
progress_callback(f"Error: {str(e)}", 100)
|
|
raise
|
|
|
|
def update_shorts_generator_settings(self):
|
|
"""Update the shorts generator with current settings"""
|
|
generator = self.get_shorts_generator()
|
|
if generator and hasattr(generator, 'detection_mode_var'):
|
|
generator.detection_mode_var.set(self.detection_var.get())
|
|
|
|
def open_editor(self):
|
|
"""Open the shorts editor"""
|
|
try:
|
|
# Import and create the editor directly
|
|
from shorts_generator2 import ShortsEditorGUI
|
|
|
|
# Get the output folder from generator if available, otherwise use default
|
|
generator = self.get_shorts_generator()
|
|
output_folder = getattr(generator, 'output_folder', 'shorts') if generator else 'shorts'
|
|
|
|
# Create and open the editor
|
|
editor = ShortsEditorGUI(self.root, output_folder)
|
|
editor.open_editor()
|
|
|
|
except Exception as e:
|
|
print(f"Editor Error: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
messagebox.showerror("Editor Error", f"Could not open editor: {e}")
|
|
|
|
def open_thumbnails(self):
|
|
"""Open the thumbnail editor"""
|
|
try:
|
|
import os
|
|
import glob
|
|
|
|
# Check if there are any video files to work with
|
|
video_files = []
|
|
|
|
# Check for original video
|
|
generator = self.get_shorts_generator()
|
|
if generator and hasattr(generator, 'video_path') and generator.video_path:
|
|
video_files.append(("Original Video", generator.video_path))
|
|
|
|
# Check for generated shorts
|
|
output_folder = getattr(generator, 'output_folder', 'shorts') if generator else 'shorts'
|
|
if os.path.exists(output_folder):
|
|
shorts = glob.glob(os.path.join(output_folder, "*.mp4"))
|
|
for short in shorts:
|
|
video_files.append((os.path.basename(short), short))
|
|
|
|
if not video_files:
|
|
messagebox.showinfo("No Videos Found",
|
|
"Please select a video or generate some shorts first!")
|
|
return
|
|
|
|
# If only one video, open it directly
|
|
if len(video_files) == 1:
|
|
selected_video = video_files[0][1]
|
|
else:
|
|
# Let user choose which video to edit
|
|
choice_window = tk.Toplevel(self.root)
|
|
choice_window.title("Select Video for Thumbnail")
|
|
choice_window.geometry("400x300")
|
|
choice_window.transient(self.root)
|
|
choice_window.grab_set()
|
|
choice_window.configure(bg=self.colors['bg_primary'])
|
|
|
|
tk.Label(choice_window, text="📸 Select Video for Thumbnail Creation",
|
|
font=("Segoe UI", 12, "bold"), bg=self.colors['bg_primary'],
|
|
fg=self.colors['text_primary']).pack(pady=20)
|
|
|
|
selected_video = None
|
|
|
|
def on_video_select(video_path):
|
|
nonlocal selected_video
|
|
selected_video = video_path
|
|
choice_window.destroy()
|
|
|
|
# Create list of videos with modern styling
|
|
for display_name, video_path in video_files:
|
|
btn = tk.Button(choice_window, text=f"📹 {display_name}",
|
|
command=lambda vp=video_path: on_video_select(vp),
|
|
font=("Segoe UI", 10), pady=8, width=40,
|
|
bg=self.colors['accent_blue'], fg='white',
|
|
relief="flat", bd=0, cursor="hand2")
|
|
btn.pack(pady=3, padx=20, fill="x")
|
|
|
|
cancel_btn = tk.Button(choice_window, text="Cancel",
|
|
command=choice_window.destroy,
|
|
font=("Segoe UI", 10), pady=8,
|
|
bg=self.colors['accent_red'], fg='white',
|
|
relief="flat", bd=0, cursor="hand2")
|
|
cancel_btn.pack(pady=15)
|
|
|
|
# Wait for selection
|
|
choice_window.wait_window()
|
|
|
|
if not selected_video:
|
|
return
|
|
|
|
# Import and open thumbnail editor
|
|
from thumbnail_editor import open_thumbnail_editor
|
|
open_thumbnail_editor(selected_video)
|
|
|
|
except Exception as e:
|
|
print(f"Thumbnail Error: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
messagebox.showerror("Thumbnail Error", f"Could not open thumbnail editor: {e}")
|
|
|
|
def run(self):
|
|
"""Start the main application"""
|
|
self.root.mainloop()
|
|
|
|
if __name__ == "__main__":
|
|
app = MainApplication()
|
|
app.run()
|