feat(main.py): add state saving and loading for video editor session

This commit is contained in:
2025-09-07 22:39:56 +02:00
parent a7c5398faf
commit 4a8492dcd2

View File

@@ -7,6 +7,7 @@ from pathlib import Path
from typing import Optional, Tuple, List
import time
import re
import json
class VideoEditor:
@@ -133,6 +134,93 @@ class VideoEditor:
# Crop adjustment settings
self.crop_size_step = self.CROP_SIZE_STEP
def _get_state_file_path(self) -> Path:
"""Get the state file path for the current media file"""
if not hasattr(self, 'video_path') or not self.video_path:
return None
return self.video_path.with_suffix('.json')
def save_state(self):
"""Save current editor state to JSON file"""
state_file = self._get_state_file_path()
if not state_file:
return False
try:
state = {
'timestamp': time.time(),
'current_frame': getattr(self, 'current_frame', 0),
'crop_rect': self.crop_rect,
'zoom_factor': self.zoom_factor,
'zoom_center': self.zoom_center,
'rotation_angle': self.rotation_angle,
'brightness': self.brightness,
'contrast': self.contrast,
'cut_start_frame': self.cut_start_frame,
'cut_end_frame': self.cut_end_frame,
'looping_between_markers': self.looping_between_markers,
'display_offset': self.display_offset,
'playback_speed': getattr(self, 'playback_speed', 1.0),
'is_playing': getattr(self, 'is_playing', False)
}
with open(state_file, 'w') as f:
json.dump(state, f, indent=2)
return True
except Exception as e:
print(f"Error saving state: {e}")
return False
def load_state(self) -> bool:
"""Load editor state from JSON file"""
state_file = self._get_state_file_path()
if not state_file or not state_file.exists():
return False
try:
with open(state_file, 'r') as f:
state = json.load(f)
# Restore state values
if 'current_frame' in state:
self.current_frame = state['current_frame']
if 'crop_rect' in state and state['crop_rect']:
self.crop_rect = tuple(state['crop_rect'])
if 'zoom_factor' in state:
self.zoom_factor = state['zoom_factor']
if 'zoom_center' in state and state['zoom_center']:
self.zoom_center = tuple(state['zoom_center'])
if 'rotation_angle' in state:
self.rotation_angle = state['rotation_angle']
if 'brightness' in state:
self.brightness = state['brightness']
if 'contrast' in state:
self.contrast = state['contrast']
if 'cut_start_frame' in state:
self.cut_start_frame = state['cut_start_frame']
if 'cut_end_frame' in state:
self.cut_end_frame = state['cut_end_frame']
if 'looping_between_markers' in state:
self.looping_between_markers = state['looping_between_markers']
if 'display_offset' in state:
self.display_offset = state['display_offset']
if 'playback_speed' in state:
self.playback_speed = state['playback_speed']
if 'is_playing' in state:
self.is_playing = state['is_playing']
# Validate and clamp values
self.current_frame = max(0, min(self.current_frame, getattr(self, 'total_frames', 1) - 1))
self.zoom_factor = max(self.MIN_ZOOM, min(self.MAX_ZOOM, self.zoom_factor))
self.brightness = max(-100, min(100, self.brightness))
self.contrast = max(0.1, min(3.0, self.contrast))
self.playback_speed = max(self.MIN_PLAYBACK_SPEED, min(self.MAX_PLAYBACK_SPEED, self.playback_speed))
return True
except Exception as e:
print(f"Error loading state: {e}")
return False
def _is_video_file(self, file_path: Path) -> bool:
"""Check if file is a supported video format"""
return file_path.suffix.lower() in self.VIDEO_EXTENSIONS
@@ -349,6 +437,10 @@ class VideoEditor:
self.cut_end_frame = None
self.display_offset = [0, 0]
# Try to load saved state for this media file
if self.load_state():
print("Loaded saved state for this media file")
def switch_to_video(self, index: int):
"""Switch to a specific video by index"""
if 0 <= index < len(self.video_files):
@@ -411,6 +503,7 @@ class VideoEditor:
"""Seek to specific frame"""
self.current_frame = max(0, min(frame_number, self.total_frames - 1))
self.load_current_frame()
self.save_state()
def advance_frame(self) -> bool:
"""Advance to next frame - optimized to avoid seeking, handles playback speed"""
@@ -534,6 +627,7 @@ class VideoEditor:
def rotate_clockwise(self):
"""Rotate video 90 degrees clockwise"""
self.rotation_angle = (self.rotation_angle + 90) % 360
self.save_state()
def apply_brightness_contrast(self, frame):
"""Apply brightness and contrast adjustments to frame"""
@@ -552,10 +646,12 @@ class VideoEditor:
def adjust_brightness(self, delta: int):
"""Adjust brightness by delta (-100 to 100)"""
self.brightness = max(-100, min(100, self.brightness + delta))
self.save_state()
def adjust_contrast(self, delta: float):
"""Adjust contrast by delta (0.1 to 3.0)"""
self.contrast = max(0.1, min(3.0, self.contrast + delta))
self.save_state()
def show_progress_bar(self, text: str = "Processing..."):
"""Show progress bar with given text"""
@@ -1086,6 +1182,7 @@ class VideoEditor:
# Handle zoom center (Ctrl + click)
if flags & cv2.EVENT_FLAG_CTRLKEY and event == cv2.EVENT_LBUTTONDOWN:
self.zoom_center = (x, y)
self.save_state()
# Handle scroll wheel for zoom (Ctrl + scroll)
if flags & cv2.EVENT_FLAG_CTRLKEY:
@@ -1098,6 +1195,7 @@ class VideoEditor:
self.zoom_factor = max(
self.MIN_ZOOM, self.zoom_factor - self.ZOOM_INCREMENT
)
self.save_state()
def set_crop_from_screen_coords(self, screen_rect):
"""Convert screen coordinates to video frame coordinates and set crop"""
@@ -1217,6 +1315,7 @@ class VideoEditor:
if self.crop_rect:
self.crop_history.append(self.crop_rect)
self.crop_rect = (original_x, original_y, original_w, original_h)
self.save_state()
def seek_to_timeline_position(self, mouse_x, bar_x_start, bar_width):
"""Seek to position based on mouse click on timeline"""
@@ -1231,6 +1330,7 @@ class VideoEditor:
self.crop_rect = self.crop_history.pop()
else:
self.crop_rect = None
self.save_state()
def toggle_marker_looping(self):
"""Toggle looping between cut markers"""
@@ -1286,22 +1386,26 @@ class VideoEditor:
new_y = max(0, y - amount)
new_h = h + (y - new_y)
self.crop_rect = (x, new_y, w, new_h)
self.save_state()
else:
# Contract from bottom - decrease height
new_h = max(10, h - amount) # Minimum size of 10 pixels
self.crop_rect = (x, y, w, new_h)
self.save_state()
elif direction == 'down':
if expand:
# Expand downward - increase height
new_h = min(self.frame_height - y, h + amount)
self.crop_rect = (x, y, w, new_h)
self.save_state()
else:
# Contract from top - increase y, decrease height
amount = min(amount, h - 10) # Don't make it smaller than 10 pixels
new_y = y + amount
new_h = h - amount
self.crop_rect = (x, new_y, w, new_h)
self.save_state()
elif direction == 'left':
if expand:
@@ -1309,22 +1413,26 @@ class VideoEditor:
new_x = max(0, x - amount)
new_w = w + (x - new_x)
self.crop_rect = (new_x, y, new_w, h)
self.save_state()
else:
# Contract from right - decrease width
new_w = max(10, w - amount) # Minimum size of 10 pixels
self.crop_rect = (x, y, new_w, h)
self.save_state()
elif direction == 'right':
if expand:
# Expand rightward - increase width
new_w = min(self.frame_width - x, w + amount)
self.crop_rect = (x, y, new_w, h)
self.save_state()
else:
# Contract from left - increase x, decrease width
amount = min(amount, w - 10) # Don't make it smaller than 10 pixels
new_x = x + amount
new_w = w - amount
self.crop_rect = (new_x, y, new_w, h)
self.save_state()
def render_video(self, output_path: str):
"""Render video or save image with current edits applied"""
@@ -1633,6 +1741,7 @@ class VideoEditor:
# Don't allow play/pause for images
if not self.is_image_mode:
self.is_playing = not self.is_playing
self.save_state()
elif key == ord("a") or key == ord("A"):
# Seeking only for videos
if not self.is_image_mode:
@@ -1670,12 +1779,14 @@ class VideoEditor:
self.playback_speed = min(
self.MAX_PLAYBACK_SPEED, self.playback_speed + self.SPEED_INCREMENT
)
self.save_state()
elif key == ord("S"):
# Speed control only for videos
if not self.is_image_mode:
self.playback_speed = max(
self.MIN_PLAYBACK_SPEED, self.playback_speed - self.SPEED_INCREMENT
)
self.save_state()
elif key == ord("e") or key == ord("E"):
# Brightness adjustment: E (increase), Shift+E (decrease)
if key == ord("E"):
@@ -1698,16 +1809,19 @@ class VideoEditor:
if self.crop_rect:
self.crop_history.append(self.crop_rect)
self.crop_rect = None
self.save_state()
elif key == ord("1"):
# Cut markers only for videos
if not self.is_image_mode:
self.cut_start_frame = self.current_frame
print(f"Set cut start at frame {self.current_frame}")
self.save_state()
elif key == ord("2"):
# Cut markers only for videos
if not self.is_image_mode:
self.cut_end_frame = self.current_frame
print(f"Set cut end at frame {self.current_frame}")
self.save_state()
elif key == ord("N"):
if len(self.video_files) > 1:
self.previous_video()
@@ -1759,6 +1873,7 @@ class VideoEditor:
# Marker looping only for videos
if not self.is_image_mode:
self.toggle_marker_looping()
self.save_state()
# Individual direction controls using shift combinations we can detect
elif key == ord("J"): # Shift+i - expand up