From 291cefc44f116d984c81720d3375b6b7de7b378a Mon Sep 17 00:00:00 2001 From: klop51 Date: Mon, 11 Aug 2025 00:14:06 +0200 Subject: [PATCH] feat: Implement professional timeline features with multi-track support and enhanced editing controls --- video_editor.py | 538 ++++++++++++++++++++++++++++++++++++++++-- video_editor_clean.py | 0 2 files changed, 512 insertions(+), 26 deletions(-) create mode 100644 video_editor_clean.py diff --git a/video_editor.py b/video_editor.py index 10afa17..44dc69c 100644 --- a/video_editor.py +++ b/video_editor.py @@ -55,6 +55,41 @@ class ShortsEditorGUI: self.timeline_scale = 1.0 # Pixels per second self.timeline_width = 800 + # Professional timeline features + self.timeline_clips = [] + self.selected_clip = None + self.markers = [] + + # 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} + } + + # Timeline interaction state + self.dragging_clip = None + self.drag_start_x = None + self.drag_start_time = None + self.drag_offset = 0 + self.snap_enabled = True + self.magnetic_timeline = True + self.grid_size = 1.0 # Snap grid in seconds + + # Timeline editing modes + self.edit_mode = 'select' # 'select', 'cut', 'trim', 'ripple' + + # Visual enhancements + self.show_thumbnails = True + self.show_waveforms = True + self.clip_thumbnails = {} + self.audio_waveforms = {} + + # Track widgets for UI + self.track_widgets = {} + # Modern color scheme self.colors = { 'bg_primary': '#1a1a1a', @@ -150,58 +185,128 @@ class ShortsEditorGUI: self.video_canvas = tk.Canvas(video_container, bg='black', highlightthickness=0) self.video_canvas.grid(row=0, column=0, sticky="nsew") - # Timeline workspace - timeline_workspace = tk.Frame(player_frame, bg=self.colors['bg_secondary'], height=200) + # Professional Timeline Workspace + timeline_workspace = tk.Frame(player_frame, bg=self.colors['bg_secondary'], height=350) timeline_workspace.grid(row=1, column=0, sticky="ew", padx=15, pady=(0, 15)) timeline_workspace.pack_propagate(False) - # Timeline Controls - controls_frame = tk.Frame(timeline_workspace, bg=self.colors['bg_secondary']) - controls_frame.pack(fill="x", pady=(10, 0)) + # Timeline header with editing tools + header_frame = tk.Frame(timeline_workspace, bg=self.colors['bg_secondary']) + header_frame.pack(fill="x", pady=(10, 5)) - # Timeline control buttons - btn_frame = tk.Frame(controls_frame, bg=self.colors['bg_secondary']) - btn_frame.pack(side="left") + # Left side - Timeline controls + left_controls = tk.Frame(header_frame, bg=self.colors['bg_secondary']) + left_controls.pack(side="left") - self.timeline_play_btn = tk.Button(btn_frame, text="▶️ Play", + # Editing mode selector + tk.Label(left_controls, text="Mode:", font=self.fonts['caption'], + bg=self.colors['bg_secondary'], fg=self.colors['text_secondary']).pack(side="left") + + self.mode_var = tk.StringVar(value="select") + mode_combo = ttk.Combobox(left_controls, textvariable=self.mode_var, width=8, + values=["select", "cut", "trim", "ripple"], state="readonly") + mode_combo.pack(side="left", padx=(5, 10)) + mode_combo.bind('<>', self.on_mode_change) + + # Snap and magnetic timeline toggles + self.snap_var = tk.BooleanVar(value=True) + snap_check = tk.Checkbutton(left_controls, text="Snap", variable=self.snap_var, + bg=self.colors['bg_secondary'], fg=self.colors['text_primary'], + selectcolor=self.colors['accent_blue'], command=self.toggle_snap) + snap_check.pack(side="left", padx=5) + + self.magnetic_var = tk.BooleanVar(value=True) + magnetic_check = tk.Checkbutton(left_controls, text="Magnetic", variable=self.magnetic_var, + bg=self.colors['bg_secondary'], fg=self.colors['text_primary'], + selectcolor=self.colors['accent_blue'], command=self.toggle_magnetic) + magnetic_check.pack(side="left", padx=5) + + # Center - Playback controls + center_controls = tk.Frame(header_frame, bg=self.colors['bg_secondary']) + center_controls.pack(side="left", padx=20) + + self.timeline_play_btn = tk.Button(center_controls, text="▶️", command=self.timeline_play, bg=self.colors['accent_green'], fg='white', - font=self.fonts['button'], padx=15, pady=5, + font=('Arial', 12, 'bold'), width=3, height=1, relief="flat", bd=0, cursor="hand2") - self.timeline_play_btn.pack(side="left", padx=(0, 5)) + self.timeline_play_btn.pack(side="left", padx=2) - self.timeline_pause_btn = tk.Button(btn_frame, text="⏸️ Pause", + self.timeline_pause_btn = tk.Button(center_controls, text="⏸️", command=self.timeline_pause, bg=self.colors['accent_orange'], fg='white', - font=self.fonts['button'], padx=15, pady=5, + font=('Arial', 12, 'bold'), width=3, height=1, relief="flat", bd=0, cursor="hand2") - self.timeline_pause_btn.pack(side="left", padx=5) + self.timeline_pause_btn.pack(side="left", padx=2) - self.timeline_stop_btn = tk.Button(btn_frame, text="⏹️ Stop", + self.timeline_stop_btn = tk.Button(center_controls, text="⏹️", command=self.timeline_stop, bg=self.colors['accent_red'], fg='white', - font=self.fonts['button'], padx=15, pady=5, + font=('Arial', 12, 'bold'), width=3, height=1, relief="flat", bd=0, cursor="hand2") - self.timeline_stop_btn.pack(side="left", padx=5) + self.timeline_stop_btn.pack(side="left", padx=2) + + # Right side - Zoom and time display + right_controls = tk.Frame(header_frame, bg=self.colors['bg_secondary']) + right_controls.pack(side="right") + + # Zoom control + tk.Label(right_controls, text="Zoom:", font=self.fonts['caption'], + bg=self.colors['bg_secondary'], fg=self.colors['text_secondary']).pack(side="left") + + self.zoom_var = tk.DoubleVar(value=1.0) + zoom_scale = tk.Scale(right_controls, from_=0.1, to=5.0, resolution=0.1, + orient="horizontal", variable=self.zoom_var, + command=self.on_zoom_change, length=150, + bg=self.colors['bg_secondary'], fg=self.colors['text_primary'], + highlightthickness=0, troughcolor=self.colors['bg_tertiary']) + zoom_scale.pack(side="left", padx=10) # Time display - self.time_display = tk.Label(controls_frame, text="00:00 / 00:00", + self.time_display = tk.Label(right_controls, text="00:00 / 00:00", font=self.fonts['body'], bg=self.colors['bg_secondary'], fg=self.colors['text_primary']) - self.time_display.pack(side="right", padx=20) + self.time_display.pack(side="left", padx=20) - # Timeline canvas - timeline_frame = tk.Frame(timeline_workspace, bg=self.colors['bg_tertiary']) - timeline_frame.pack(fill="both", expand=True, pady=10) + # Main timeline container + timeline_container = tk.Frame(timeline_workspace, bg=self.colors['bg_tertiary']) + timeline_container.pack(fill="both", expand=True, pady=5) - self.timeline_canvas = tk.Canvas(timeline_frame, bg=self.colors['bg_tertiary'], - height=120, highlightthickness=1, - highlightbackground=self.colors['border']) + # Track labels panel (left side) + self.track_panel = tk.Frame(timeline_container, bg=self.colors['bg_secondary'], width=120) + self.track_panel.pack(side="left", fill="y") + self.track_panel.pack_propagate(False) + + # Timeline canvas with scrollbars + canvas_frame = tk.Frame(timeline_container, bg=self.colors['bg_tertiary']) + canvas_frame.pack(side="right", fill="both", expand=True) + + # Create canvas with scrollbars + self.timeline_canvas = tk.Canvas(canvas_frame, bg='#1a1a1a', + highlightthickness=0, scrollregion=(0, 0, 2000, 300)) + + # 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) + self.timeline_canvas.configure(xscrollcommand=h_scrollbar.set, yscrollcommand=v_scrollbar.set) + + # Pack scrollbars and canvas + h_scrollbar.pack(side="bottom", fill="x") + v_scrollbar.pack(side="right", fill="y") self.timeline_canvas.pack(side="left", fill="both", expand=True) - # Bind timeline events + # Bind professional timeline events self.timeline_canvas.bind("", self.timeline_click) self.timeline_canvas.bind("", self.timeline_drag) + self.timeline_canvas.bind("", self.on_timeline_drag_end) + self.timeline_canvas.bind("", self.on_timeline_right_click) + self.timeline_canvas.bind("", self.on_timeline_double_click) + + # Create track controls + self.create_track_controls() + + # Initialize sample clips for demonstration + self.create_sample_timeline_content() # Right panel - Tools and effects tools_frame = tk.Frame(main_frame, bg=self.colors['bg_secondary']) @@ -221,6 +326,168 @@ class ShortsEditorGUI: # Initialize timeline self.update_timeline() + def create_track_controls(self): + """Create professional track control panel""" + # 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 controls for each track + for track_id, track_info in self.tracks.items(): + self.create_track_control(track_id, track_info) + + 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) + + # Track name + name_label = tk.Label(track_frame, text=track_info['name'], + font=('Arial', 8, 'bold'), bg=self.colors['bg_secondary'], + fg=track_info['color']) + name_label.pack(anchor="w", padx=5, pady=2) + + # Control buttons frame + controls = tk.Frame(track_frame, bg=self.colors['bg_secondary']) + controls.pack(fill="x", padx=5) + + # Mute button + mute_text = "🔇" if track_info['muted'] else "🔊" + mute_btn = tk.Button(controls, 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, + 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, + bg=self.colors['accent_orange'] if track_info['solo'] else self.colors['bg_tertiary'], + fg='white', font=('Arial', 8, '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, + bg=self.colors['accent_blue'] if track_info['locked'] else self.colors['bg_tertiary'], + fg='white', font=('Arial', 8), relief="flat", bd=0, + command=lambda: self.toggle_track_lock(track_id)) + lock_btn.pack(side="left", padx=1) + + # Store track widgets for updates + self.track_widgets[track_id] = { + 'frame': track_frame, + 'mute_btn': mute_btn, + 'solo_btn': solo_btn, + 'lock_btn': lock_btn + } + + def create_sample_timeline_content(self): + """Create sample timeline content for demonstration""" + if self.current_video and self.video_duration > 0: + # Create a sample clip representing the loaded video + sample_clip = { + 'id': 1, + 'name': os.path.basename(self.current_video) if self.current_video else 'Sample Video', + 'start_time': 0, + 'end_time': min(self.video_duration, 10), # Cap at 10 seconds for demo + 'track': 'video_1', + 'color': self.tracks['video_1']['color'], + 'file_path': self.current_video, + 'type': 'video' + } + self.timeline_clips = [sample_clip] + + # Add sample markers + self.markers = [ + {'time': 2.0, 'name': 'Intro End', 'color': '#ffeb3b'}, + {'time': 5.0, 'name': 'Mid Point', 'color': '#4caf50'}, + {'time': 8.0, 'name': 'Outro Start', 'color': '#f44336'} + ] + + self.update_timeline() + + # Professional timeline interaction methods + def on_mode_change(self, event=None): + """Handle editing mode change""" + self.edit_mode = self.mode_var.get() + print(f"🎬 Editing mode changed to: {self.edit_mode}") + + # Update cursor based on mode + cursor_map = { + 'select': 'hand2', + 'cut': 'crosshair', + 'trim': 'sb_h_double_arrow', + 'ripple': 'fleur' + } + self.timeline_canvas.configure(cursor=cursor_map.get(self.edit_mode, 'hand2')) + + def toggle_snap(self): + """Toggle snap to grid""" + self.snap_enabled = self.snap_var.get() + print(f"🧲 Snap enabled: {self.snap_enabled}") + + def toggle_magnetic(self): + """Toggle magnetic timeline""" + self.magnetic_timeline = self.magnetic_var.get() + print(f"🧲 Magnetic timeline: {self.magnetic_timeline}") + + def toggle_track_mute(self, track_id): + """Toggle track mute""" + self.tracks[track_id]['muted'] = not self.tracks[track_id]['muted'] + self.update_track_controls() + print(f"🔇 Track {track_id} muted: {self.tracks[track_id]['muted']}") + + def toggle_track_solo(self, track_id): + """Toggle track solo""" + self.tracks[track_id]['solo'] = not self.tracks[track_id]['solo'] + self.update_track_controls() + print(f"🎵 Track {track_id} solo: {self.tracks[track_id]['solo']}") + + def toggle_track_lock(self, track_id): + """Toggle track lock""" + self.tracks[track_id]['locked'] = not self.tracks[track_id]['locked'] + self.update_track_controls() + print(f"🔒 Track {track_id} locked: {self.tracks[track_id]['locked']}") + + def update_track_controls(self): + """Update track control button states""" + for track_id, widgets in self.track_widgets.items(): + track_info = self.tracks[track_id] + + # Update mute button + mute_text = "🔇" if track_info['muted'] else "🔊" + mute_color = self.colors['accent_red'] if track_info['muted'] else self.colors['bg_tertiary'] + widgets['mute_btn'].configure(text=mute_text, bg=mute_color) + + # Update solo button + solo_color = self.colors['accent_orange'] if track_info['solo'] else self.colors['bg_tertiary'] + widgets['solo_btn'].configure(bg=solo_color) + + # Update lock button + lock_text = "🔒" if track_info['locked'] else "🔓" + lock_color = self.colors['accent_blue'] if track_info['locked'] else self.colors['bg_tertiary'] + widgets['lock_btn'].configure(text=lock_text, bg=lock_color) + + def on_zoom_change(self, value): + """Handle timeline zoom change""" + zoom_level = float(value) + self.timeline_scale = 50 * zoom_level # Base scale of 50 pixels per second + self.update_timeline() + print(f"🔍 Timeline zoom: {zoom_level:.1f}x") + + # Initialize timeline + self.update_timeline() + def create_basic_tools(self, parent): """Create basic editing tools""" basic_frame = tk.LabelFrame(parent, text="Basic Editing", font=self.fonts['heading'], @@ -656,6 +923,74 @@ class ShortsEditorGUI: """Handle timeline dragging""" self.timeline_click(event) # Same behavior as click for now + def on_timeline_drag_end(self, event): + """End timeline drag operation""" + if hasattr(self, 'dragging_clip') and self.dragging_clip: + print(f"🎬 Moved clip '{self.dragging_clip['name']}' to {self.dragging_clip['start_time']:.2f}s") + + # Clear drag state + if hasattr(self, 'dragging_clip'): + self.dragging_clip = None + if hasattr(self, 'drag_start_x'): + self.drag_start_x = None + if hasattr(self, 'drag_start_time'): + self.drag_start_time = None + if hasattr(self, 'drag_offset'): + self.drag_offset = 0 + + def on_timeline_right_click(self, event): + """Handle right-click context menu""" + try: + canvas_x = self.timeline_canvas.canvasx(event.x) + canvas_y = self.timeline_canvas.canvasy(event.y) + clicked_clip = self.get_clip_at_position(canvas_x, canvas_y) if hasattr(self, 'get_clip_at_position') else None + + # Create context menu + context_menu = tk.Menu(self.root, tearoff=0, bg=self.colors['bg_secondary'], + fg=self.colors['text_primary']) + + if clicked_clip: + # Clip context menu + self.selected_clip = clicked_clip + context_menu.add_command(label=f"Cut '{clicked_clip['name']}'", + command=lambda: self.cut_clip_at_playhead()) + context_menu.add_command(label=f"Delete '{clicked_clip['name']}'", + command=lambda: self.delete_clip(clicked_clip)) + context_menu.add_separator() + context_menu.add_command(label="Duplicate Clip", + command=lambda: self.duplicate_clip(clicked_clip)) + context_menu.add_command(label="Properties", + command=lambda: self.show_clip_properties(clicked_clip)) + else: + # Timeline context menu + click_time = canvas_x / self.timeline_scale if hasattr(self, 'timeline_scale') else 0 + context_menu.add_command(label="Add Marker", + command=lambda: self.add_marker_at_time(click_time)) + context_menu.add_command(label="Zoom to Fit", command=self.zoom_to_fit) + + try: + context_menu.tk_popup(event.x_root, event.y_root) + finally: + context_menu.grab_release() + except Exception as e: + print(f"Context menu error: {e}") + + def on_timeline_double_click(self, event): + """Handle timeline double-click""" + try: + canvas_x = self.timeline_canvas.canvasx(event.x) + canvas_y = self.timeline_canvas.canvasy(event.y) + clicked_clip = self.get_clip_at_position(canvas_x, canvas_y) if hasattr(self, 'get_clip_at_position') else None + + if clicked_clip: + self.show_clip_properties(clicked_clip) + else: + # Add marker on double-click + click_time = canvas_x / self.timeline_scale if hasattr(self, 'timeline_scale') else 0 + self.add_marker_at_time(click_time) + except Exception as e: + print(f"Double-click error: {e}") + def update_time_display(self): """Update the time display""" current_min = int(self.current_time // 60) @@ -988,6 +1323,157 @@ class ShortsEditorGUI: # Start export in background thread threading.Thread(target=export_thread, daemon=True).start() + + # Professional timeline helper methods + def get_clip_at_position(self, x, y): + """Get the clip at the given canvas position""" + time_pos = x / self.timeline_scale + + for clip in self.timeline_clips: + if clip['start_time'] <= time_pos <= clip['end_time']: + # Check if Y position is within the clip's track + track_info = self.tracks[clip['track']] + if track_info['y_offset'] <= y <= track_info['y_offset'] + track_info['height']: + return clip + return None + + def snap_to_grid(self, time_value): + """Snap time value to grid""" + if self.snap_enabled and self.grid_size > 0: + return round(time_value / self.grid_size) * self.grid_size + return time_value + + def magnetic_snap(self, new_time, dragging_clip): + """Apply magnetic timeline snapping to other clips""" + if not self.magnetic_timeline: + return new_time + + snap_distance = 0.2 # 200ms snap distance + clip_duration = dragging_clip['end_time'] - dragging_clip['start_time'] + + for clip in self.timeline_clips: + if clip == dragging_clip or clip['track'] != dragging_clip['track']: + continue + + # Snap to start of other clips + if abs(new_time - clip['start_time']) < snap_distance: + return clip['start_time'] + + # Snap to end of other clips + if abs(new_time - clip['end_time']) < snap_distance: + return clip['end_time'] + + # Snap end of dragging clip to start of other clips + if abs((new_time + clip_duration) - clip['start_time']) < snap_distance: + return clip['start_time'] - clip_duration + + return new_time + + def cut_clip_at_position(self, clip, cut_time): + """Cut a clip at the specified time""" + if cut_time <= clip['start_time'] or cut_time >= clip['end_time']: + return + + # Create two new clips + first_clip = clip.copy() + first_clip['id'] = len(self.timeline_clips) + 1 + first_clip['end_time'] = cut_time + first_clip['name'] = f"{clip['name']} (1)" + + second_clip = clip.copy() + second_clip['id'] = len(self.timeline_clips) + 2 + second_clip['start_time'] = cut_time + second_clip['name'] = f"{clip['name']} (2)" + + # Remove original clip and add new ones + self.timeline_clips.remove(clip) + self.timeline_clips.extend([first_clip, second_clip]) + + self.selected_clip = first_clip + self.update_timeline() + print(f"✂️ Cut clip at {cut_time:.2f}s") + + def cut_clip_at_playhead(self): + """Cut selected clip at current playhead position""" + if self.selected_clip: + self.cut_clip_at_position(self.selected_clip, self.current_time) + + def delete_clip(self, clip): + """Delete a clip from timeline""" + if clip in self.timeline_clips: + self.timeline_clips.remove(clip) + if self.selected_clip == clip: + self.selected_clip = None + self.update_timeline() + print(f"🗑️ Deleted clip: {clip['name']}") + + def duplicate_clip(self, clip): + """Duplicate a clip""" + new_clip = clip.copy() + new_clip['id'] = len(self.timeline_clips) + 1 + new_clip['name'] = f"{clip['name']} (Copy)" + + # Place after original clip + duration = clip['end_time'] - clip['start_time'] + new_clip['start_time'] = clip['end_time'] + new_clip['end_time'] = clip['end_time'] + duration + + self.timeline_clips.append(new_clip) + self.selected_clip = new_clip + self.update_timeline() + print(f"📄 Duplicated clip: {new_clip['name']}") + + def add_marker_at_time(self, time): + """Add a marker at specified time""" + marker = { + 'time': time, + 'name': f"Marker {len(self.markers) + 1}", + 'color': '#ffeb3b' + } + self.markers.append(marker) + self.update_timeline() + print(f"📍 Added marker at {time:.2f}s") + + def show_clip_properties(self, clip): + """Show clip properties dialog""" + props_window = tk.Toplevel(self.root) + props_window.title(f"Clip Properties - {clip['name']}") + props_window.configure(bg=self.colors['bg_primary']) + props_window.geometry("400x300") + + # Clip info + tk.Label(props_window, text=f"Clip: {clip['name']}", + font=self.fonts['heading'], bg=self.colors['bg_primary'], + fg=self.colors['text_primary']).pack(pady=10) + + info_frame = tk.Frame(props_window, bg=self.colors['bg_primary']) + info_frame.pack(fill='x', padx=20) + + # Duration, start time, etc. + duration = clip['end_time'] - clip['start_time'] + tk.Label(info_frame, text=f"Duration: {duration:.2f}s", + bg=self.colors['bg_primary'], fg=self.colors['text_secondary']).pack(anchor='w') + tk.Label(info_frame, text=f"Start: {clip['start_time']:.2f}s", + bg=self.colors['bg_primary'], fg=self.colors['text_secondary']).pack(anchor='w') + tk.Label(info_frame, text=f"End: {clip['end_time']:.2f}s", + bg=self.colors['bg_primary'], fg=self.colors['text_secondary']).pack(anchor='w') + tk.Label(info_frame, text=f"Track: {clip['track']}", + bg=self.colors['bg_primary'], fg=self.colors['text_secondary']).pack(anchor='w') + + def zoom_to_fit(self): + """Zoom timeline to fit all content""" + if not self.timeline_clips: + return + + # Find the last clip end time + max_time = max(clip['end_time'] for clip in self.timeline_clips) + canvas_width = self.timeline_canvas.winfo_width() + + if max_time > 0 and canvas_width > 100: + zoom_level = (canvas_width - 100) / (max_time * 50) # 50 is base scale + self.zoom_var.set(max(0.1, min(5.0, zoom_level))) + self.on_zoom_change(zoom_level) + print(f"🔍 Zoomed to fit content ({zoom_level:.1f}x)") def open_shorts_editor(shorts_folder="shorts"): """Open the shorts editor as a standalone application""" diff --git a/video_editor_clean.py b/video_editor_clean.py new file mode 100644 index 0000000..e69de29