diff --git a/video_editor.py b/video_editor.py index 44dc69c..30d92de 100644 --- a/video_editor.py +++ b/video_editor.py @@ -62,11 +62,11 @@ class ShortsEditorGUI: # Multi-track system self.tracks = { - 'video_1': {'y_offset': 40, 'height': 60, 'color': '#3498db', 'name': 'Video 1', 'muted': False, 'locked': False, 'solo': False, 'visible': True}, - 'video_2': {'y_offset': 105, 'height': 60, 'color': '#2ecc71', 'name': 'Video 2', 'muted': False, 'locked': False, 'solo': False, 'visible': True}, - 'audio_1': {'y_offset': 170, 'height': 40, 'color': '#e74c3c', 'name': 'Audio 1', 'muted': False, 'locked': False, 'solo': False, 'visible': True}, - 'audio_2': {'y_offset': 215, 'height': 40, 'color': '#f39c12', 'name': 'Audio 2', 'muted': False, 'locked': False, 'solo': False, 'visible': True}, - 'text_1': {'y_offset': 260, 'height': 35, 'color': '#9b59b6', 'name': 'Text/Graphics', 'muted': False, 'locked': False, 'solo': False, 'visible': True} + 'video_1': {'y_offset': 40, 'height': 60, 'color': '#3498db', 'name': 'Video 1', 'muted': False, 'locked': False, 'solo': False, 'visible': True, 'type': 'video'}, + 'video_2': {'y_offset': 105, 'height': 60, 'color': '#2ecc71', 'name': 'Video 2', 'muted': False, 'locked': False, 'solo': False, 'visible': True, 'type': 'video'}, + 'video_3': {'y_offset': 170, 'height': 60, 'color': '#9b59b6', 'name': 'Video 3', 'muted': False, 'locked': False, 'solo': False, 'visible': True, 'type': 'video'}, + 'audio_1': {'y_offset': 235, 'height': 40, 'color': '#e74c3c', 'name': 'Audio 1', 'muted': False, 'locked': False, 'solo': False, 'visible': True, 'type': 'audio'}, + 'audio_2': {'y_offset': 280, 'height': 40, 'color': '#f39c12', 'name': 'Audio 2', 'muted': False, 'locked': False, 'solo': False, 'visible': True, 'type': 'audio'} } # Timeline interaction state @@ -186,7 +186,7 @@ class ShortsEditorGUI: self.video_canvas.grid(row=0, column=0, sticky="nsew") # Professional Timeline Workspace - timeline_workspace = tk.Frame(player_frame, bg=self.colors['bg_secondary'], height=350) + timeline_workspace = tk.Frame(player_frame, bg=self.colors['bg_secondary'], height=380) timeline_workspace.grid(row=1, column=0, sticky="ew", padx=15, pady=(0, 15)) timeline_workspace.pack_propagate(False) @@ -283,11 +283,11 @@ class ShortsEditorGUI: # Create canvas with scrollbars self.timeline_canvas = tk.Canvas(canvas_frame, bg='#1a1a1a', - highlightthickness=0, scrollregion=(0, 0, 2000, 300)) + highlightthickness=0, scrollregion=(0, 0, 2000, 400)) # Scrollbars h_scrollbar = ttk.Scrollbar(canvas_frame, orient="horizontal", command=self.timeline_canvas.xview) - v_scrollbar = ttk.Scrollbar(canvas_frame, orient="vertical", command=self.timeline_canvas.yview) + v_scrollbar = ttk.Scrollbar(canvas_frame, orient="vertical", command=self.sync_vertical_scroll) self.timeline_canvas.configure(xscrollcommand=h_scrollbar.set, yscrollcommand=v_scrollbar.set) # Pack scrollbars and canvas @@ -305,6 +305,9 @@ class ShortsEditorGUI: # Create track controls self.create_track_controls() + # Bind canvas resize to redraw road lines + self.track_panel.bind("", self.on_track_panel_resize) + # Initialize sample clips for demonstration self.create_sample_timeline_content() @@ -318,74 +321,215 @@ class ShortsEditorGUI: fg=self.colors['text_primary']) tools_header.pack(pady=(15, 10)) - # Create tool sections - self.create_basic_tools(tools_frame) - self.create_effects_tools(tools_frame) - self.create_export_tools(tools_frame) + # Create tabbed interface for tools + self.create_tabbed_tools(tools_frame) # Initialize timeline self.update_timeline() + def create_tabbed_tools(self, parent): + """Create tabbed interface for editing tools""" + import tkinter.ttk as ttk + + # Create notebook for tabs + self.tools_notebook = ttk.Notebook(parent) + self.tools_notebook.pack(fill="both", expand=True, padx=10, pady=5) + + # Configure notebook style for dark theme + style = ttk.Style() + + # Set theme and configure colors + try: + style.theme_use('clam') # Use clam theme as base for better customization + except: + pass # Fall back to default theme if clam isn't available + + # Configure the notebook (main container) + style.configure('TNotebook', + background=self.colors['bg_secondary'], + borderwidth=0, + tabmargins=[2, 5, 2, 0]) + + # Configure the tabs themselves with consistent sizing + style.configure('TNotebook.Tab', + background=self.colors['bg_tertiary'], + foreground=self.colors['text_primary'], + padding=[15, 8, 15, 8], # Explicit left, top, right, bottom padding + borderwidth=0, # Remove border to prevent size changes + focuscolor='none', + relief='flat') + + # Configure selected tab with same padding to prevent shrinking + style.map('TNotebook.Tab', + background=[('selected', self.colors['accent_blue']), + ('active', self.colors['bg_primary']), + ('!active', self.colors['bg_tertiary'])], + foreground=[('selected', 'white'), + ('active', self.colors['text_primary']), + ('!active', self.colors['text_primary'])], + padding=[('selected', [15, 8, 15, 8]), # Same padding for selected + ('active', [15, 8, 15, 8]), # Same padding for active + ('!active', [15, 8, 15, 8])], # Same padding for inactive + borderwidth=[('selected', 0), # No border changes + ('active', 0), + ('!active', 0)], + relief=[('selected', 'flat'), # Consistent relief + ('active', 'flat'), + ('!active', 'flat')]) + + # Basic Editing Tab + basic_frame = tk.Frame(self.tools_notebook, bg=self.colors['bg_secondary']) + self.tools_notebook.add(basic_frame, text="Basic Editing") + self.create_basic_tools(basic_frame) + + # Video Effects Tab + video_effects_frame = tk.Frame(self.tools_notebook, bg=self.colors['bg_secondary']) + self.tools_notebook.add(video_effects_frame, text="Video Effects") + self.create_video_effects_tools(video_effects_frame) + + # Audio Effects Tab + audio_effects_frame = tk.Frame(self.tools_notebook, bg=self.colors['bg_secondary']) + self.tools_notebook.add(audio_effects_frame, text="Audio Effects") + self.create_audio_effects_tools(audio_effects_frame) + + # Export Tab + export_frame = tk.Frame(self.tools_notebook, bg=self.colors['bg_secondary']) + self.tools_notebook.add(export_frame, text="Export") + self.create_export_tools(export_frame) + def create_track_controls(self): - """Create professional track control panel""" + """Create professional track control panel with road lines""" # Clear existing track controls for widget in self.track_panel.winfo_children(): widget.destroy() - # Header - header = tk.Label(self.track_panel, text="TRACKS", font=('Arial', 9, 'bold'), - bg=self.colors['bg_secondary'], fg=self.colors['text_secondary']) - header.pack(fill="x", pady=(5, 10)) + # Create road line background canvas + self.track_road_canvas = tk.Canvas(self.track_panel, + bg=self.colors['bg_secondary'], + highlightthickness=0, + scrollregion=(0, 0, 120, 400)) + self.track_road_canvas.pack(fill="both", expand=True) + + # Draw road lines for track separation + self.draw_track_road_lines() # Create controls for each track for track_id, track_info in self.tracks.items(): self.create_track_control(track_id, track_info) + def draw_track_road_lines(self): + """Draw road lines for track visual separation""" + # Clear existing lines + self.track_road_canvas.delete("road_lines") + + # Get canvas dimensions + canvas_width = self.track_panel.winfo_width() or 120 + canvas_height = self.track_panel.winfo_height() or 320 + + # Draw horizontal road lines for each track + for track_id, track_info in self.tracks.items(): + y_pos = track_info['y_offset'] + track_height = track_info['height'] + + # Top line of track + self.track_road_canvas.create_line( + 0, y_pos, canvas_width, y_pos, + fill=self.colors['border'], width=1, tags="road_lines" + ) + + # Bottom line of track + self.track_road_canvas.create_line( + 0, y_pos + track_height, canvas_width, y_pos + track_height, + fill=self.colors['border'], width=1, tags="road_lines" + ) + + # Track type indicator line (left edge with track color) + self.track_road_canvas.create_line( + 0, y_pos, 0, y_pos + track_height, + fill=track_info['color'], width=3, tags="road_lines" + ) + + # Center dashed line for alignment reference + center_x = canvas_width // 2 + for y in range(0, int(canvas_height), 10): + self.track_road_canvas.create_line( + center_x, y, center_x, y + 5, + fill=self.colors['text_secondary'], width=1, + tags="road_lines", dash=(2, 3) + ) + + def on_track_panel_resize(self, event): + """Handle track panel resize to redraw road lines""" + if hasattr(self, 'track_road_canvas'): + # Small delay to ensure canvas size is updated + self.editor_window.after(10, self.draw_track_road_lines) + + def sync_vertical_scroll(self, *args): + """Synchronize vertical scrolling between track panel and timeline canvas""" + # Scroll both canvases together + self.timeline_canvas.yview(*args) + if hasattr(self, 'track_road_canvas'): + self.track_road_canvas.yview(*args) + def create_track_control(self, track_id, track_info): - """Create control panel for a single track""" - # Track frame - track_frame = tk.Frame(self.track_panel, bg=self.colors['bg_secondary'], - height=track_info['height'], relief="raised", bd=1) - track_frame.pack(fill="x", pady=1) - track_frame.pack_propagate(False) + """Create control panel for a single track positioned on road lines""" + # Calculate precise positioning based on track offset + y_position = track_info['y_offset'] + track_height = track_info['height'] - # Track name - name_label = tk.Label(track_frame, text=track_info['name'], - font=('Arial', 8, 'bold'), bg=self.colors['bg_secondary'], + # Create control buttons frame positioned on canvas + controls_frame = tk.Frame(self.track_road_canvas, bg=self.colors['bg_secondary']) + + # Control buttons container + controls = tk.Frame(controls_frame, bg=self.colors['bg_secondary']) + controls.pack(padx=5, pady=2) + + # Track name label (small, top of controls) + name_label = tk.Label(controls, text=track_info['name'], + font=('Arial', 7, 'bold'), bg=self.colors['bg_secondary'], fg=track_info['color']) - name_label.pack(anchor="w", padx=5, pady=2) + name_label.pack(anchor="center") - # Control buttons frame - controls = tk.Frame(track_frame, bg=self.colors['bg_secondary']) - controls.pack(fill="x", padx=5) + # Button container + button_container = tk.Frame(controls, bg=self.colors['bg_secondary']) + button_container.pack() # Mute button mute_text = "šŸ”‡" if track_info['muted'] else "šŸ”Š" - mute_btn = tk.Button(controls, text=mute_text, width=2, height=1, + mute_btn = tk.Button(button_container, text=mute_text, width=2, height=1, bg=self.colors['accent_red'] if track_info['muted'] else self.colors['bg_tertiary'], - fg='white', font=('Arial', 8), relief="flat", bd=0, + fg='white', font=('Arial', 7), relief="flat", bd=0, command=lambda: self.toggle_track_mute(track_id)) mute_btn.pack(side="left", padx=1) # Solo button solo_text = "S" - solo_btn = tk.Button(controls, text=solo_text, width=2, height=1, + solo_btn = tk.Button(button_container, text=solo_text, width=2, height=1, bg=self.colors['accent_orange'] if track_info['solo'] else self.colors['bg_tertiary'], - fg='white', font=('Arial', 8, 'bold'), relief="flat", bd=0, + fg='white', font=('Arial', 7, 'bold'), relief="flat", bd=0, command=lambda: self.toggle_track_solo(track_id)) solo_btn.pack(side="left", padx=1) # Lock button lock_text = "šŸ”’" if track_info['locked'] else "šŸ”“" - lock_btn = tk.Button(controls, text=lock_text, width=2, height=1, + lock_btn = tk.Button(button_container, text=lock_text, width=2, height=1, bg=self.colors['accent_blue'] if track_info['locked'] else self.colors['bg_tertiary'], - fg='white', font=('Arial', 8), relief="flat", bd=0, + fg='white', font=('Arial', 7), relief="flat", bd=0, command=lambda: self.toggle_track_lock(track_id)) lock_btn.pack(side="left", padx=1) + # Position the controls frame on the canvas + canvas_width = 120 + control_y = y_position + (track_height // 2) - 20 # Center vertically in track + + self.track_road_canvas.create_window( + canvas_width // 2, control_y, + window=controls_frame, anchor="center" + ) + # Store track widgets for updates self.track_widgets[track_id] = { - 'frame': track_frame, + 'frame': controls_frame, 'mute_btn': mute_btn, 'solo_btn': solo_btn, 'lock_btn': lock_btn @@ -490,18 +634,28 @@ class ShortsEditorGUI: def create_basic_tools(self, parent): """Create basic editing tools""" - basic_frame = tk.LabelFrame(parent, text="Basic Editing", font=self.fonts['heading'], - bg=self.colors['bg_secondary'], fg=self.colors['text_primary'], - relief="flat", bd=1) - basic_frame.pack(fill="x", padx=15, pady=5) + # Create scrollable frame for tools + tools_canvas = tk.Canvas(parent, bg=self.colors['bg_secondary'], highlightthickness=0) + scrollbar = tk.Scrollbar(parent, orient="vertical", command=tools_canvas.yview) + scrollable_frame = tk.Frame(tools_canvas, bg=self.colors['bg_secondary']) + + scrollable_frame.bind( + "", + lambda e: tools_canvas.configure(scrollregion=tools_canvas.bbox("all")) + ) + + tools_canvas.create_window((0, 0), window=scrollable_frame, anchor="nw") + tools_canvas.configure(yscrollcommand=scrollbar.set) + + tools_canvas.pack(side="left", fill="both", expand=True) + scrollbar.pack(side="right", fill="y") # Trim controls - trim_frame = tk.Frame(basic_frame, bg=self.colors['bg_secondary']) + trim_frame = tk.LabelFrame(scrollable_frame, text="āœ‚ļø Trim Video", font=self.fonts['body'], + bg=self.colors['bg_secondary'], fg=self.colors['text_primary'], + relief="flat", bd=1) trim_frame.pack(fill="x", padx=10, pady=5) - tk.Label(trim_frame, text="Trim Video:", font=self.fonts['body'], - bg=self.colors['bg_secondary'], fg=self.colors['text_primary']).pack(anchor="w") - trim_controls = tk.Frame(trim_frame, bg=self.colors['bg_secondary']) trim_controls.pack(fill="x", pady=5) @@ -523,78 +677,168 @@ class ShortsEditorGUI: font=self.fonts['caption']) trim_end_spin.pack(side="left", padx=5) - trim_btn = tk.Button(basic_frame, text="āœ‚ļø Apply Trim", command=self.apply_trim, + trim_btn = tk.Button(trim_frame, text="āœ‚ļø Apply Trim", command=self.apply_trim, bg=self.colors['accent_blue'], fg='white', font=self.fonts['button'], relief="flat", bd=0, cursor="hand2") trim_btn.pack(fill="x", padx=10, pady=5) # Speed controls - speed_frame = tk.Frame(basic_frame, bg=self.colors['bg_secondary']) + speed_frame = tk.LabelFrame(scrollable_frame, text="⚔ Speed Control", font=self.fonts['body'], + bg=self.colors['bg_secondary'], fg=self.colors['text_primary'], + relief="flat", bd=1) speed_frame.pack(fill="x", padx=10, pady=5) - tk.Label(speed_frame, text="Speed:", font=self.fonts['body'], - bg=self.colors['bg_secondary'], fg=self.colors['text_primary']).pack(anchor="w") - self.speed_var = tk.DoubleVar(value=1.0) speed_scale = tk.Scale(speed_frame, from_=0.25, to=3.0, resolution=0.25, orient="horizontal", variable=self.speed_var, bg=self.colors['bg_secondary'], fg=self.colors['text_primary'], highlightthickness=0, troughcolor=self.colors['bg_tertiary']) - speed_scale.pack(fill="x", pady=5) + speed_scale.pack(fill="x", pady=5, padx=10) - speed_btn = tk.Button(basic_frame, text="⚔ Apply Speed", command=self.apply_speed, + speed_btn = tk.Button(speed_frame, text="⚔ Apply Speed", command=self.apply_speed, bg=self.colors['accent_green'], fg='white', font=self.fonts['button'], relief="flat", bd=0, cursor="hand2") speed_btn.pack(fill="x", padx=10, pady=5) # Volume controls - volume_frame = tk.Frame(basic_frame, bg=self.colors['bg_secondary']) + volume_frame = tk.LabelFrame(scrollable_frame, text="šŸ”Š Volume Control", font=self.fonts['body'], + bg=self.colors['bg_secondary'], fg=self.colors['text_primary'], + relief="flat", bd=1) volume_frame.pack(fill="x", padx=10, pady=5) - tk.Label(volume_frame, text="Volume:", font=self.fonts['body'], - bg=self.colors['bg_secondary'], fg=self.colors['text_primary']).pack(anchor="w") - self.volume_var = tk.DoubleVar(value=1.0) volume_scale = tk.Scale(volume_frame, from_=0.0, to=2.0, resolution=0.1, orient="horizontal", variable=self.volume_var, bg=self.colors['bg_secondary'], fg=self.colors['text_primary'], highlightthickness=0, troughcolor=self.colors['bg_tertiary']) - volume_scale.pack(fill="x", pady=5) + volume_scale.pack(fill="x", pady=5, padx=10) - volume_btn = tk.Button(basic_frame, text="šŸ”Š Apply Volume", command=self.apply_volume, + volume_btn = tk.Button(volume_frame, text="šŸ”Š Apply Volume", command=self.apply_volume, bg=self.colors['accent_orange'], fg='white', font=self.fonts['button'], relief="flat", bd=0, cursor="hand2") volume_btn.pack(fill="x", padx=10, pady=5) - - def create_effects_tools(self, parent): - """Create effects tools""" - effects_frame = tk.LabelFrame(parent, text="Effects", font=self.fonts['heading'], + + # Resize controls + resize_frame = tk.LabelFrame(scrollable_frame, text="šŸ“ Resize Video", font=self.fonts['body'], bg=self.colors['bg_secondary'], fg=self.colors['text_primary'], relief="flat", bd=1) - effects_frame.pack(fill="x", padx=15, pady=5) + resize_frame.pack(fill="x", padx=10, pady=5) + + # Preset sizes + preset_frame = tk.Frame(resize_frame, bg=self.colors['bg_secondary']) + preset_frame.pack(fill="x", padx=10, pady=5) + + tk.Label(preset_frame, text="Presets:", font=self.fonts['caption'], + bg=self.colors['bg_secondary'], fg=self.colors['text_secondary']).pack(anchor="w") + + presets = [("9:16 (1080x1920)", 1080, 1920), ("16:9 (1920x1080)", 1920, 1080), ("1:1 (1080x1080)", 1080, 1080)] + for name, width, height in presets: + btn = tk.Button(preset_frame, text=name, + command=lambda w=width, h=height: self.apply_resize(w, h), + bg=self.colors['bg_tertiary'], fg=self.colors['text_primary'], + font=self.fonts['caption'], relief="flat", bd=0, cursor="hand2") + btn.pack(fill="x", pady=2) + + def create_video_effects_tools(self, parent): + """Create video effects tools""" + # Create scrollable frame for video effects + effects_canvas = tk.Canvas(parent, bg=self.colors['bg_secondary'], highlightthickness=0) + scrollbar_effects = tk.Scrollbar(parent, orient="vertical", command=effects_canvas.yview) + scrollable_effects_frame = tk.Frame(effects_canvas, bg=self.colors['bg_secondary']) + + scrollable_effects_frame.bind( + "", + lambda e: effects_canvas.configure(scrollregion=effects_canvas.bbox("all")) + ) + + effects_canvas.create_window((0, 0), window=scrollable_effects_frame, anchor="nw") + effects_canvas.configure(yscrollcommand=scrollbar_effects.set) + + effects_canvas.pack(side="left", fill="both", expand=True) + scrollbar_effects.pack(side="right", fill="y") # Fade effects - fade_btn = tk.Button(effects_frame, text="šŸŒ… Add Fade In/Out", command=self.apply_fade, + fade_frame = tk.LabelFrame(scrollable_effects_frame, text="šŸŒ… Fade Effects", font=self.fonts['body'], + bg=self.colors['bg_secondary'], fg=self.colors['text_primary'], + relief="flat", bd=1) + fade_frame.pack(fill="x", padx=10, pady=5) + + fade_btn = tk.Button(fade_frame, text="šŸŒ… Add Fade In/Out", command=self.apply_fade, bg=self.colors['accent_blue'], fg='white', font=self.fonts['button'], relief="flat", bd=0, cursor="hand2") fade_btn.pack(fill="x", padx=10, pady=5) # Text overlay - text_frame = tk.Frame(effects_frame, bg=self.colors['bg_secondary']) + text_frame = tk.LabelFrame(scrollable_effects_frame, text="šŸ“ Text Overlay", font=self.fonts['body'], + bg=self.colors['bg_secondary'], fg=self.colors['text_primary'], + relief="flat", bd=1) text_frame.pack(fill="x", padx=10, pady=5) - tk.Label(text_frame, text="Text Overlay:", font=self.fonts['body'], - bg=self.colors['bg_secondary'], fg=self.colors['text_primary']).pack(anchor="w") + text_controls = tk.Frame(text_frame, bg=self.colors['bg_secondary']) + text_controls.pack(fill="x", padx=10, pady=5) + + tk.Label(text_controls, text="Text:", font=self.fonts['caption'], + bg=self.colors['bg_secondary'], fg=self.colors['text_secondary']).pack(anchor="w") self.text_var = tk.StringVar(value="Sample Text") - text_entry = tk.Entry(text_frame, textvariable=self.text_var, font=self.fonts['body']) + text_entry = tk.Entry(text_controls, textvariable=self.text_var, font=self.fonts['body'], + width=25) text_entry.pack(fill="x", pady=5) - text_btn = tk.Button(effects_frame, text="šŸ“ Add Text", command=self.apply_text, + text_btn = tk.Button(text_frame, text="šŸ“ Add Text", command=self.apply_text, bg=self.colors['accent_green'], fg='white', font=self.fonts['button'], relief="flat", bd=0, cursor="hand2") text_btn.pack(fill="x", padx=10, pady=5) + def create_audio_effects_tools(self, parent): + """Create audio effects tools""" + # Create scrollable frame for audio effects + audio_canvas = tk.Canvas(parent, bg=self.colors['bg_secondary'], highlightthickness=0) + scrollbar_audio = tk.Scrollbar(parent, orient="vertical", command=audio_canvas.yview) + scrollable_audio_frame = tk.Frame(audio_canvas, bg=self.colors['bg_secondary']) + + scrollable_audio_frame.bind( + "", + lambda e: audio_canvas.configure(scrollregion=audio_canvas.bbox("all")) + ) + + audio_canvas.create_window((0, 0), window=scrollable_audio_frame, anchor="nw") + audio_canvas.configure(yscrollcommand=scrollbar_audio.set) + + audio_canvas.pack(side="left", fill="both", expand=True) + scrollbar_audio.pack(side="right", fill="y") + + # Volume controls (detailed) + volume_frame = tk.LabelFrame(scrollable_audio_frame, text="šŸ”Š Volume Controls", font=self.fonts['body'], + bg=self.colors['bg_secondary'], fg=self.colors['text_primary'], + relief="flat", bd=1) + volume_frame.pack(fill="x", padx=10, pady=5) + + # Volume slider with fine control + self.audio_volume_var = tk.DoubleVar(value=1.0) + volume_scale = tk.Scale(volume_frame, from_=0.0, to=3.0, resolution=0.01, + orient="horizontal", variable=self.audio_volume_var, + bg=self.colors['bg_secondary'], fg=self.colors['text_primary'], + highlightthickness=0, length=250) + volume_scale.pack(fill="x", pady=5, padx=10) + + volume_btn = tk.Button(volume_frame, text="šŸ”Š Apply Volume", command=self.apply_volume, + bg=self.colors['accent_blue'], fg='white', font=self.fonts['button'], + relief="flat", bd=0, cursor="hand2") + volume_btn.pack(fill="x", padx=10, pady=5) + + # Audio info + info_frame = tk.LabelFrame(scrollable_audio_frame, text="ā„¹ļø Audio Information", font=self.fonts['body'], + bg=self.colors['bg_secondary'], fg=self.colors['text_primary'], + relief="flat", bd=1) + info_frame.pack(fill="x", padx=10, pady=5) + + info_text = tk.Label(info_frame, + text="Audio effects require external tools\nor MoviePy installation.\n\nFor advanced audio editing:\n• Audacity (free)\n• FFmpeg\n• pip install moviepy", + font=self.fonts['caption'], bg=self.colors['bg_secondary'], + fg=self.colors['text_secondary'], justify="left") + info_text.pack(padx=10, pady=5) + def create_export_tools(self, parent): """Create export tools""" export_frame = tk.LabelFrame(parent, text="Export", font=self.fonts['heading'], @@ -877,6 +1121,9 @@ class ShortsEditorGUI: self.timeline_canvas.create_rectangle(20, 20, canvas_width - 20, canvas_height - 20, fill=self.colors['bg_primary'], outline=self.colors['border']) + # Draw track road lines + self.draw_timeline_track_roads(canvas_width, canvas_height) + # Draw time markers for i in range(0, int(self.video_duration) + 1): x = 20 + i * self.timeline_scale @@ -899,6 +1146,41 @@ class ShortsEditorGUI: self.timeline_canvas.create_oval(playhead_x - 5, 15, playhead_x + 5, 25, fill=self.colors['accent_blue'], outline='white') + def draw_timeline_track_roads(self, canvas_width, canvas_height): + """Draw track road lines on timeline canvas""" + left_margin = 20 + right_margin = 20 + + # Draw horizontal road lines for each track + for track_id, track_info in self.tracks.items(): + y_pos = track_info['y_offset'] + track_height = track_info['height'] + + # Top line of track + self.timeline_canvas.create_line( + left_margin, y_pos, canvas_width - right_margin, y_pos, + fill=self.colors['border'], width=1, tags="track_roads" + ) + + # Bottom line of track + self.timeline_canvas.create_line( + left_margin, y_pos + track_height, canvas_width - right_margin, y_pos + track_height, + fill=self.colors['border'], width=1, tags="track_roads" + ) + + # Track type indicator line (left edge with track color) + self.timeline_canvas.create_line( + left_margin, y_pos, left_margin, y_pos + track_height, + fill=track_info['color'], width=4, tags="track_roads" + ) + + # Track name label on the left + self.timeline_canvas.create_text( + left_margin + 50, y_pos + track_height // 2, + text=track_info['name'], fill=track_info['color'], + font=('Arial', 8, 'bold'), anchor="center", tags="track_roads" + ) + def timeline_click(self, event): """Handle timeline click""" if not self.current_clip: @@ -1095,11 +1377,6 @@ class ShortsEditorGUI: def apply_trim(self): """Apply trim to the video""" - if not MOVIEPY_AVAILABLE: - messagebox.showwarning("Feature Unavailable", - "Trim feature requires MoviePy.\nInstall with: pip install moviepy") - return - if not self.current_clip: messagebox.showwarning("No Video", "Please load a video first.") return @@ -1116,29 +1393,66 @@ class ShortsEditorGUI: return try: - # Apply trim - self.current_clip = self.current_clip.subclipped(start_time, end_time) - self.video_duration = self.current_clip.duration - self.current_time = 0.0 - - # Update UI - self.trim_end_var.set(self.video_duration) - self.display_frame_at_time(0.0) - self.update_timeline() - self.update_time_display() - - messagebox.showinfo("Success", f"Video trimmed to {start_time:.1f}s - {end_time:.1f}s") - + if MOVIEPY_AVAILABLE and hasattr(self.current_clip, 'subclipped'): + # Apply trim using MoviePy + self.current_clip = self.current_clip.subclipped(start_time, end_time) + self.video_duration = self.current_clip.duration + self.current_time = 0.0 + + # Update UI + self.trim_end_var.set(self.video_duration) + self.display_frame_at_time(0.0) + self.update_timeline() + self.update_time_display() + messagebox.showinfo("Success", f"Video trimmed to {start_time:.1f}s - {end_time:.1f}s") + else: + # OpenCV mode - export trimmed video + if not self.current_video: + messagebox.showwarning("No Video", "No video file loaded.") + return + + timestamp = datetime.now().strftime("%H%M%S") + base_name = os.path.splitext(os.path.basename(self.current_video))[0] + output_path = os.path.join(os.path.dirname(self.current_video), + f"{base_name}_trimmed_{timestamp}.mp4") + + self.apply_trim_opencv(self.current_video, output_path, start_time, end_time) + self.load_video(output_path) + messagebox.showinfo("Success", f"Video trimmed ({start_time:.1f}s to {end_time:.1f}s) and saved as:\n{os.path.basename(output_path)}") + except Exception as e: messagebox.showerror("Trim Error", f"Could not trim video: {e}") + def apply_trim_opencv(self, input_path, output_path, start_time, end_time): + """Apply trim using OpenCV""" + cap = cv2.VideoCapture(input_path) + fps = cap.get(cv2.CAP_PROP_FPS) + width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) + height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) + + start_frame = int(start_time * fps) + end_frame = int(end_time * fps) + + fourcc = cv2.VideoWriter_fourcc(*'mp4v') + out = cv2.VideoWriter(output_path, fourcc, fps, (width, height)) + + # Set to start frame + cap.set(cv2.CAP_PROP_POS_FRAMES, start_frame) + + frame_count = start_frame + while frame_count < end_frame: + ret, frame = cap.read() + if not ret: + break + + out.write(frame) + frame_count += 1 + + cap.release() + out.release() + def apply_speed(self): """Apply speed change to the video""" - if not MOVIEPY_AVAILABLE: - messagebox.showwarning("Feature Unavailable", - "Speed change requires MoviePy.\nInstall with: pip install moviepy") - return - if not self.current_clip: messagebox.showwarning("No Video", "Please load a video first.") return @@ -1146,106 +1460,303 @@ class ShortsEditorGUI: speed_factor = self.speed_var.get() try: - if speed_factor > 1: - # Speed up - self.current_clip = self.current_clip.with_fps(self.current_clip.fps * speed_factor) - self.current_clip = self.current_clip.subclipped(0, self.current_clip.duration / speed_factor) + if MOVIEPY_AVAILABLE and hasattr(self.current_clip, 'with_fps'): + # Apply speed change using MoviePy + if speed_factor > 1: + # Speed up + self.current_clip = self.current_clip.with_fps(self.current_clip.fps * speed_factor) + self.current_clip = self.current_clip.subclipped(0, self.current_clip.duration / speed_factor) + else: + # Slow down + self.current_clip = self.current_clip.with_fps(self.current_clip.fps * speed_factor) + + self.video_duration = self.current_clip.duration + self.current_time = 0.0 + + # Update UI + self.trim_end_var.set(self.video_duration) + self.display_frame_at_time(0.0) + self.update_timeline() + self.update_time_display() + messagebox.showinfo("Success", f"Speed changed to {speed_factor:.1f}x") else: - # Slow down - self.current_clip = self.current_clip.with_fps(self.current_clip.fps * speed_factor) - - self.video_duration = self.current_clip.duration - self.current_time = 0.0 - - # Update UI - self.trim_end_var.set(self.video_duration) - self.display_frame_at_time(0.0) - self.update_timeline() - self.update_time_display() - - messagebox.showinfo("Success", f"Speed changed to {speed_factor:.1f}x") - + # OpenCV mode - export video with speed change + if not self.current_video: + messagebox.showwarning("No Video", "No video file loaded.") + return + + timestamp = datetime.now().strftime("%H%M%S") + base_name = os.path.splitext(os.path.basename(self.current_video))[0] + output_path = os.path.join(os.path.dirname(self.current_video), + f"{base_name}_speed_{speed_factor}x_{timestamp}.mp4") + + self.apply_speed_opencv(self.current_video, output_path, speed_factor) + self.load_video(output_path) + messagebox.showinfo("Success", f"Speed changed to {speed_factor:.1f}x and saved as:\n{os.path.basename(output_path)}") + except Exception as e: messagebox.showerror("Speed Error", f"Could not change speed: {e}") + def apply_speed_opencv(self, input_path, output_path, speed_factor): + """Apply speed change using OpenCV""" + cap = cv2.VideoCapture(input_path) + fps = cap.get(cv2.CAP_PROP_FPS) + width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) + height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) + + # Calculate new FPS for speed change + new_fps = fps * speed_factor + + fourcc = cv2.VideoWriter_fourcc(*'mp4v') + out = cv2.VideoWriter(output_path, fourcc, new_fps, (width, height)) + + while True: + ret, frame = cap.read() + if not ret: + break + + out.write(frame) + + cap.release() + out.release() + def apply_volume(self): """Apply volume adjustment""" - if not MOVIEPY_AVAILABLE: - messagebox.showwarning("Feature Unavailable", - "Volume adjustment requires MoviePy.\nInstall with: pip install moviepy") - return - if not self.current_clip: messagebox.showwarning("No Video", "Please load a video first.") return - if not self.current_clip.audio: - messagebox.showwarning("No Audio", "This video has no audio track.") - return - - volume_factor = self.volume_var.get() + # Check for volume factor from either basic or audio effects tab + volume_factor = 1.0 + if hasattr(self, 'audio_volume_var'): + volume_factor = self.audio_volume_var.get() + elif hasattr(self, 'volume_var'): + volume_factor = self.volume_var.get() try: - self.current_clip = self.current_clip.with_effects([MultiplyVolume(volume_factor)]) - messagebox.showinfo("Success", f"Volume adjusted to {volume_factor:.1f}x") - + if MOVIEPY_AVAILABLE and hasattr(self.current_clip, 'audio'): + # Use MoviePy for volume adjustment + if not self.current_clip.audio: + messagebox.showwarning("No Audio", "This video has no audio track.") + return + + self.current_clip = self.current_clip.with_effects([MultiplyVolume(volume_factor)]) + messagebox.showinfo("Success", f"Volume adjusted to {volume_factor:.1f}x") + else: + # OpenCV mode - show helpful message about audio processing + if not self.current_video: + messagebox.showwarning("No Video", "No video file loaded.") + return + + messagebox.showinfo("Audio Processing", + f"Volume adjustment to {volume_factor:.1f}x noted.\n\n" + "Audio processing requires external tools.\n" + "For audio editing, consider using:\n" + "• Audacity (free audio editor)\n" + "• FFmpeg command line\n" + "• Install MoviePy: pip install moviepy") + except Exception as e: messagebox.showerror("Volume Error", f"Could not adjust volume: {e}") def apply_fade(self): """Apply fade in/out effects""" - if not MOVIEPY_AVAILABLE: - messagebox.showwarning("Feature Unavailable", - "Fade effects require MoviePy.\nInstall with: pip install moviepy") - return - if not self.current_clip: messagebox.showwarning("No Video", "Please load a video first.") return try: - fade_duration = min(1.0, self.video_duration / 4) # Max 1 second or 1/4 of video - - self.current_clip = self.current_clip.with_effects([ - FadeIn(fade_duration), - FadeOut(fade_duration) - ]) - - messagebox.showinfo("Success", f"Fade effects applied ({fade_duration:.1f}s)") - + if MOVIEPY_AVAILABLE and hasattr(self.current_clip, 'fadein'): + # Use MoviePy for advanced fade effects + fade_duration = min(1.0, self.video_duration / 4) + + if hasattr(self.current_clip, 'fadein') and hasattr(self.current_clip, 'fadeout'): + self.current_clip = self.current_clip.fadein(fade_duration).fadeout(fade_duration) + messagebox.showinfo("Success", f"Fade effects applied ({fade_duration:.1f}s)") + else: + messagebox.showinfo("Not Available", "Fade effects not available in this MoviePy version") + else: + # OpenCV mode - export video with fade effect + if not self.current_video: + messagebox.showwarning("No Video", "No video file loaded.") + return + + timestamp = datetime.now().strftime("%H%M%S") + base_name = os.path.splitext(os.path.basename(self.current_video))[0] + output_path = os.path.join(os.path.dirname(self.current_video), + f"{base_name}_faded_{timestamp}.mp4") + + self.apply_fade_opencv(self.current_video, output_path) + self.load_video(output_path) + messagebox.showinfo("Success", f"Fade effects applied and saved as:\n{os.path.basename(output_path)}") + except Exception as e: messagebox.showerror("Fade Error", f"Could not apply fade effects: {e}") + def apply_fade_opencv(self, input_path, output_path): + """Apply fade effects using OpenCV""" + cap = cv2.VideoCapture(input_path) + fps = cap.get(cv2.CAP_PROP_FPS) + width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) + height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) + total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) + + fourcc = cv2.VideoWriter_fourcc(*'mp4v') + out = cv2.VideoWriter(output_path, fourcc, fps, (width, height)) + + # Calculate fade frames (1 second fade in/out) + fade_frames = min(int(fps), total_frames // 4) + + frame_count = 0 + while True: + ret, frame = cap.read() + if not ret: + break + + # Apply fade in + if frame_count < fade_frames: + alpha = frame_count / fade_frames + frame = cv2.convertScaleAbs(frame, alpha=alpha, beta=0) + + # Apply fade out + elif frame_count >= total_frames - fade_frames: + alpha = (total_frames - frame_count) / fade_frames + frame = cv2.convertScaleAbs(frame, alpha=alpha, beta=0) + + out.write(frame) + frame_count += 1 + + cap.release() + out.release() + def apply_text(self): """Apply text overlay""" - if not MOVIEPY_AVAILABLE: - messagebox.showwarning("Feature Unavailable", - "Text overlay requires MoviePy.\nInstall with: pip install moviepy") - return - if not self.current_clip: messagebox.showwarning("No Video", "Please load a video first.") return - text = self.text_var.get().strip() - if not text: + # Get text from the text variable + text_content = self.text_var.get().strip() if hasattr(self, 'text_var') and self.text_var.get().strip() else "Sample Text" + + if not text_content: messagebox.showwarning("No Text", "Please enter text to overlay.") return try: - # Create text clip - text_clip = TextClip(text, fontsize=50, color='white', font='Arial-Bold') - text_clip = text_clip.with_duration(self.current_clip.duration) - text_clip = text_clip.with_position(('center', 'bottom')) - - # Composite with video - self.current_clip = CompositeVideoClip([self.current_clip, text_clip]) - - messagebox.showinfo("Success", f"Text overlay added: '{text}'") - + if MOVIEPY_AVAILABLE and hasattr(self.current_clip, 'duration'): + # Use MoviePy for text overlay + text_clip = TextClip(text_content, fontsize=50, color='white', font='Arial-Bold') + text_clip = text_clip.with_duration(self.current_clip.duration) + text_clip = text_clip.with_position(('center', 'bottom')) + + # Composite with video + self.current_clip = CompositeVideoClip([self.current_clip, text_clip]) + messagebox.showinfo("Success", f"Text overlay added: '{text_content}'") + else: + # OpenCV mode - export video with text overlay + if not self.current_video: + messagebox.showwarning("No Video", "No video file loaded.") + return + + timestamp = datetime.now().strftime("%H%M%S") + base_name = os.path.splitext(os.path.basename(self.current_video))[0] + output_path = os.path.join(os.path.dirname(self.current_video), + f"{base_name}_with_text_{timestamp}.mp4") + + self.apply_text_opencv(self.current_video, output_path, text_content) + self.load_video(output_path) + messagebox.showinfo("Success", f"Text '{text_content}' added and saved as:\n{os.path.basename(output_path)}") + except Exception as e: messagebox.showerror("Text Error", f"Could not add text overlay: {e}") + def apply_text_opencv(self, input_path, output_path, text): + """Apply text overlay using OpenCV""" + cap = cv2.VideoCapture(input_path) + fps = cap.get(cv2.CAP_PROP_FPS) + width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) + height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) + + fourcc = cv2.VideoWriter_fourcc(*'mp4v') + out = cv2.VideoWriter(output_path, fourcc, fps, (width, height)) + + # Text settings + font = cv2.FONT_HERSHEY_SIMPLEX + font_scale = min(width, height) / 800 # Scale font based on video size + color = (255, 255, 255) # White color + thickness = max(1, int(font_scale * 2)) + + # Get text size for positioning + (text_width, text_height), baseline = cv2.getTextSize(text, font, font_scale, thickness) + x = (width - text_width) // 2 + y = height - 50 # Bottom position + + while True: + ret, frame = cap.read() + if not ret: + break + + # Add black outline for better visibility + cv2.putText(frame, text, (x-2, y-2), font, font_scale, (0, 0, 0), thickness+2, cv2.LINE_AA) + cv2.putText(frame, text, (x+2, y+2), font, font_scale, (0, 0, 0), thickness+2, cv2.LINE_AA) + + # Add white text + cv2.putText(frame, text, (x, y), font, font_scale, color, thickness, cv2.LINE_AA) + + out.write(frame) + + cap.release() + out.release() + + def apply_resize(self, target_width, target_height): + """Apply resize to video""" + if not self.current_clip: + messagebox.showwarning("No Video", "Please load a video first.") + return + + try: + if MOVIEPY_AVAILABLE and hasattr(self.current_clip, 'resize'): + # Use MoviePy for resizing + self.current_clip = self.current_clip.resize((target_width, target_height)) + messagebox.showinfo("Success", f"Video resized to {target_width}x{target_height}") + else: + # OpenCV mode - export resized video + if not self.current_video: + messagebox.showwarning("No Video", "No video file loaded.") + return + + timestamp = datetime.now().strftime("%H%M%S") + base_name = os.path.splitext(os.path.basename(self.current_video))[0] + output_path = os.path.join(os.path.dirname(self.current_video), + f"{base_name}_resized_{target_width}x{target_height}_{timestamp}.mp4") + + self.apply_resize_opencv(self.current_video, output_path, target_width, target_height) + self.load_video(output_path) + messagebox.showinfo("Success", f"Video resized to {target_width}x{target_height} and saved as:\n{os.path.basename(output_path)}") + + except Exception as e: + messagebox.showerror("Resize Error", f"Could not resize video: {e}") + + def apply_resize_opencv(self, input_path, output_path, target_width, target_height): + """Apply resize using OpenCV""" + cap = cv2.VideoCapture(input_path) + fps = cap.get(cv2.CAP_PROP_FPS) + + fourcc = cv2.VideoWriter_fourcc(*'mp4v') + out = cv2.VideoWriter(output_path, fourcc, fps, (target_width, target_height)) + + while True: + ret, frame = cap.read() + if not ret: + break + + # Resize frame + resized_frame = cv2.resize(frame, (target_width, target_height)) + out.write(resized_frame) + + cap.release() + out.release() + def reset_video(self): """Reset video to original state""" if not self.current_video: