Try refactor everything into separate files
This commit is contained in:
351
croppa/project_view.py
Normal file
351
croppa/project_view.py
Normal file
@@ -0,0 +1,351 @@
|
||||
import cv2
|
||||
import json
|
||||
import numpy as np
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class ProjectView:
|
||||
"""Project view that displays videos in current directory with progress bars"""
|
||||
|
||||
# Project view configuration
|
||||
THUMBNAIL_SIZE = (200, 150) # Width, Height
|
||||
THUMBNAIL_MARGIN = 20
|
||||
PROGRESS_BAR_HEIGHT = 8
|
||||
TEXT_HEIGHT = 30
|
||||
|
||||
# Colors
|
||||
BG_COLOR = (40, 40, 40)
|
||||
THUMBNAIL_BG_COLOR = (60, 60, 60)
|
||||
PROGRESS_BG_COLOR = (80, 80, 80)
|
||||
PROGRESS_FILL_COLOR = (0, 120, 255)
|
||||
TEXT_COLOR = (255, 255, 255)
|
||||
SELECTED_COLOR = (255, 165, 0)
|
||||
|
||||
def __init__(self, directory: Path, video_editor):
|
||||
self.directory = directory
|
||||
self.video_editor = video_editor
|
||||
self.video_files = []
|
||||
self.thumbnails = {}
|
||||
self.progress_data = {}
|
||||
self.selected_index = 0
|
||||
self.scroll_offset = 0
|
||||
self.items_per_row = 2 # Default to 2 items per row
|
||||
self.window_width = 1920 # Increased to accommodate 1080p videos
|
||||
self.window_height = 1200
|
||||
|
||||
self._load_video_files()
|
||||
self._load_progress_data()
|
||||
|
||||
def _calculate_thumbnail_size(self, window_width: int) -> tuple:
|
||||
"""Calculate thumbnail size based on items per row and window width"""
|
||||
available_width = window_width - self.THUMBNAIL_MARGIN
|
||||
item_width = (available_width - (self.items_per_row - 1) * self.THUMBNAIL_MARGIN) // self.items_per_row
|
||||
thumbnail_width = max(50, item_width) # Minimum 50px width
|
||||
thumbnail_height = int(thumbnail_width * self.THUMBNAIL_SIZE[1] / self.THUMBNAIL_SIZE[0]) # Maintain aspect ratio
|
||||
return (thumbnail_width, thumbnail_height)
|
||||
|
||||
def _load_video_files(self):
|
||||
"""Load all video files from directory"""
|
||||
self.video_files = []
|
||||
for file_path in self.directory.iterdir():
|
||||
if (file_path.is_file() and
|
||||
file_path.suffix.lower() in self.video_editor.VIDEO_EXTENSIONS):
|
||||
self.video_files.append(file_path)
|
||||
self.video_files.sort(key=lambda x: x.name)
|
||||
|
||||
def _load_progress_data(self):
|
||||
"""Load progress data from JSON state files"""
|
||||
self.progress_data = {}
|
||||
for video_path in self.video_files:
|
||||
state_file = video_path.with_suffix('.json')
|
||||
if state_file.exists():
|
||||
try:
|
||||
with open(state_file, 'r') as f:
|
||||
state = json.load(f)
|
||||
current_frame = state.get('current_frame', 0)
|
||||
|
||||
# Get total frames from video
|
||||
cap = cv2.VideoCapture(str(video_path))
|
||||
if cap.isOpened():
|
||||
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
||||
cap.release()
|
||||
|
||||
if total_frames > 0:
|
||||
progress = current_frame / (total_frames - 1)
|
||||
self.progress_data[video_path] = {
|
||||
'current_frame': current_frame,
|
||||
'total_frames': total_frames,
|
||||
'progress': progress
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"Error loading progress for {video_path.name}: {e}")
|
||||
|
||||
def refresh_progress_data(self):
|
||||
"""Refresh progress data from JSON files (call when editor state changes)"""
|
||||
self._load_progress_data()
|
||||
|
||||
def get_progress_for_video(self, video_path: Path) -> float:
|
||||
"""Get progress (0.0 to 1.0) for a video"""
|
||||
if video_path in self.progress_data:
|
||||
return self.progress_data[video_path]['progress']
|
||||
return 0.0
|
||||
|
||||
def get_thumbnail_for_video(self, video_path: Path, size: tuple = None) -> np.ndarray:
|
||||
"""Get thumbnail for a video, generating it if needed"""
|
||||
if size is None:
|
||||
size = self.THUMBNAIL_SIZE
|
||||
|
||||
# Cache the original thumbnail by video path only (not size)
|
||||
if video_path in self.thumbnails:
|
||||
original_thumbnail = self.thumbnails[video_path]
|
||||
# Resize the cached thumbnail to the requested size
|
||||
return cv2.resize(original_thumbnail, size)
|
||||
|
||||
# Generate original thumbnail on demand (only once per video)
|
||||
try:
|
||||
cap = cv2.VideoCapture(str(video_path))
|
||||
if cap.isOpened():
|
||||
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
||||
if total_frames > 0:
|
||||
middle_frame = total_frames // 2
|
||||
cap.set(cv2.CAP_PROP_POS_FRAMES, middle_frame)
|
||||
ret, frame = cap.read()
|
||||
if ret:
|
||||
# Store original thumbnail at original size
|
||||
original_thumbnail = cv2.resize(frame, self.THUMBNAIL_SIZE)
|
||||
self.thumbnails[video_path] = original_thumbnail
|
||||
cap.release()
|
||||
# Return resized version
|
||||
return cv2.resize(original_thumbnail, size)
|
||||
cap.release()
|
||||
except Exception as e:
|
||||
print(f"Error generating thumbnail for {video_path.name}: {e}")
|
||||
|
||||
# Return a placeholder if thumbnail generation failed
|
||||
placeholder = np.full((size[1], size[0], 3),
|
||||
self.THUMBNAIL_BG_COLOR, dtype=np.uint8)
|
||||
return placeholder
|
||||
|
||||
def draw(self) -> np.ndarray:
|
||||
"""Draw the project view"""
|
||||
# Get actual window size dynamically
|
||||
try:
|
||||
# Try to get the actual window size from OpenCV
|
||||
window_rect = cv2.getWindowImageRect("Project View")
|
||||
if window_rect[2] > 0 and window_rect[3] > 0: # width and height > 0
|
||||
actual_width = window_rect[2]
|
||||
actual_height = window_rect[3]
|
||||
else:
|
||||
# Fallback to default size
|
||||
actual_width = self.window_width
|
||||
actual_height = self.window_height
|
||||
except:
|
||||
# Fallback to default size
|
||||
actual_width = self.window_width
|
||||
actual_height = self.window_height
|
||||
|
||||
canvas = np.full((actual_height, actual_width, 3), self.BG_COLOR, dtype=np.uint8)
|
||||
|
||||
if not self.video_files:
|
||||
# No videos message
|
||||
text = "No videos found in directory"
|
||||
font = cv2.FONT_HERSHEY_SIMPLEX
|
||||
text_size = cv2.getTextSize(text, font, 1.0, 2)[0]
|
||||
text_x = (actual_width - text_size[0]) // 2
|
||||
text_y = (actual_height - text_size[1]) // 2
|
||||
cv2.putText(canvas, text, (text_x, text_y), font, 1.0, self.TEXT_COLOR, 2)
|
||||
return canvas
|
||||
|
||||
# Calculate layout - use fixed items_per_row and calculate thumbnail size to fit
|
||||
items_per_row = min(self.items_per_row, len(self.video_files)) # Don't exceed number of videos
|
||||
|
||||
# Calculate thumbnail size to fit the desired number of items per row
|
||||
thumbnail_width, thumbnail_height = self._calculate_thumbnail_size(actual_width)
|
||||
|
||||
# Calculate item height dynamically based on thumbnail size
|
||||
item_height = thumbnail_height + self.PROGRESS_BAR_HEIGHT + self.TEXT_HEIGHT + self.THUMBNAIL_MARGIN
|
||||
|
||||
item_width = (actual_width - (items_per_row + 1) * self.THUMBNAIL_MARGIN) // items_per_row
|
||||
|
||||
# Draw videos in grid
|
||||
for i, video_path in enumerate(self.video_files):
|
||||
row = i // items_per_row
|
||||
col = i % items_per_row
|
||||
|
||||
# Skip if scrolled out of view
|
||||
if row < self.scroll_offset:
|
||||
continue
|
||||
if row > self.scroll_offset + (actual_height // item_height):
|
||||
break
|
||||
|
||||
# Calculate position
|
||||
x = self.THUMBNAIL_MARGIN + col * (item_width + self.THUMBNAIL_MARGIN)
|
||||
y = self.THUMBNAIL_MARGIN + (row - self.scroll_offset) * item_height
|
||||
|
||||
# Draw thumbnail background
|
||||
cv2.rectangle(canvas,
|
||||
(x, y),
|
||||
(x + thumbnail_width, y + thumbnail_height),
|
||||
self.THUMBNAIL_BG_COLOR, -1)
|
||||
|
||||
# Draw selection highlight
|
||||
if i == self.selected_index:
|
||||
cv2.rectangle(canvas,
|
||||
(x - 2, y - 2),
|
||||
(x + thumbnail_width + 2, y + thumbnail_height + 2),
|
||||
self.SELECTED_COLOR, 3)
|
||||
|
||||
# Draw thumbnail
|
||||
thumbnail = self.get_thumbnail_for_video(video_path, (thumbnail_width, thumbnail_height))
|
||||
# Thumbnail is already the correct size, no need to resize
|
||||
resized_thumbnail = thumbnail
|
||||
|
||||
# Ensure thumbnail doesn't exceed canvas bounds
|
||||
end_y = min(y + thumbnail_height, actual_height)
|
||||
end_x = min(x + thumbnail_width, actual_width)
|
||||
thumb_height = end_y - y
|
||||
thumb_width = end_x - x
|
||||
|
||||
if thumb_height > 0 and thumb_width > 0:
|
||||
# Resize thumbnail to fit within bounds if necessary
|
||||
if thumb_height != thumbnail_height or thumb_width != thumbnail_width:
|
||||
resized_thumbnail = cv2.resize(thumbnail, (thumb_width, thumb_height))
|
||||
|
||||
canvas[y:end_y, x:end_x] = resized_thumbnail
|
||||
|
||||
# Draw progress bar
|
||||
progress_y = y + thumbnail_height + 5
|
||||
progress_width = thumbnail_width
|
||||
progress = self.get_progress_for_video(video_path)
|
||||
|
||||
# Progress background
|
||||
cv2.rectangle(canvas,
|
||||
(x, progress_y),
|
||||
(x + progress_width, progress_y + self.PROGRESS_BAR_HEIGHT),
|
||||
self.PROGRESS_BG_COLOR, -1)
|
||||
|
||||
# Progress fill
|
||||
if progress > 0:
|
||||
fill_width = int(progress_width * progress)
|
||||
cv2.rectangle(canvas,
|
||||
(x, progress_y),
|
||||
(x + fill_width, progress_y + self.PROGRESS_BAR_HEIGHT),
|
||||
self.PROGRESS_FILL_COLOR, -1)
|
||||
|
||||
# Draw filename
|
||||
filename = video_path.name
|
||||
# Truncate if too long
|
||||
if len(filename) > 25:
|
||||
filename = filename[:22] + "..."
|
||||
|
||||
text_y = progress_y + self.PROGRESS_BAR_HEIGHT + 20
|
||||
cv2.putText(canvas, filename, (x, text_y),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 0.6, self.TEXT_COLOR, 2)
|
||||
|
||||
# Draw progress percentage
|
||||
if video_path in self.progress_data:
|
||||
progress_text = f"{progress * 100:.0f}%"
|
||||
text_size = cv2.getTextSize(progress_text, cv2.FONT_HERSHEY_SIMPLEX, 0.4, 1)[0]
|
||||
progress_text_x = x + progress_width - text_size[0]
|
||||
cv2.putText(canvas, progress_text, (progress_text_x, text_y),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 0.4, self.TEXT_COLOR, 1)
|
||||
|
||||
# Draw instructions
|
||||
instructions = [
|
||||
"Project View - Videos in current directory",
|
||||
"WASD: Navigate | E: Open video | Q: Fewer items per row | Y: More items per row | q: Quit | ESC: Back to editor",
|
||||
f"Showing {len(self.video_files)} videos | {items_per_row} per row | Thumbnail: {thumbnail_width}x{thumbnail_height}"
|
||||
]
|
||||
|
||||
for i, instruction in enumerate(instructions):
|
||||
y_pos = actual_height - 60 + i * 20
|
||||
cv2.putText(canvas, instruction, (10, y_pos),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 0.5, self.TEXT_COLOR, 1)
|
||||
|
||||
return canvas
|
||||
|
||||
def handle_key(self, key: int) -> str:
|
||||
"""Handle keyboard input, returns action taken"""
|
||||
if key == 27: # ESC
|
||||
return "back_to_editor"
|
||||
elif key == ord('q'): # lowercase q - Quit
|
||||
return "quit"
|
||||
elif key == ord('e') or key == ord('E'): # E - Open video
|
||||
if self.video_files and 0 <= self.selected_index < len(self.video_files):
|
||||
return f"open_video:{self.video_files[self.selected_index]}"
|
||||
elif key == ord('w') or key == ord('W'): # W - Up
|
||||
current_items_per_row = min(self.items_per_row, len(self.video_files))
|
||||
if self.selected_index >= current_items_per_row:
|
||||
self.selected_index -= current_items_per_row
|
||||
else:
|
||||
self.selected_index = 0
|
||||
self._update_scroll()
|
||||
elif key == ord('s') or key == ord('S'): # S - Down
|
||||
current_items_per_row = min(self.items_per_row, len(self.video_files))
|
||||
if self.selected_index + current_items_per_row < len(self.video_files):
|
||||
self.selected_index += current_items_per_row
|
||||
else:
|
||||
self.selected_index = len(self.video_files) - 1
|
||||
self._update_scroll()
|
||||
elif key == ord('a') or key == ord('A'): # A - Left
|
||||
if self.selected_index > 0:
|
||||
self.selected_index -= 1
|
||||
self._update_scroll()
|
||||
elif key == ord('d') or key == ord('D'): # D - Right
|
||||
if self.selected_index < len(self.video_files) - 1:
|
||||
self.selected_index += 1
|
||||
self._update_scroll()
|
||||
elif key == ord('Q'): # uppercase Q - Fewer items per row (larger thumbnails)
|
||||
if self.items_per_row > 1:
|
||||
self.items_per_row -= 1
|
||||
print(f"Items per row: {self.items_per_row}")
|
||||
elif key == ord('y') or key == ord('Y'): # Y - More items per row (smaller thumbnails)
|
||||
self.items_per_row += 1
|
||||
print(f"Items per row: {self.items_per_row}")
|
||||
|
||||
return "none"
|
||||
|
||||
def _update_scroll(self):
|
||||
"""Update scroll offset based on selected item"""
|
||||
if not self.video_files:
|
||||
return
|
||||
|
||||
# Use fixed items per row
|
||||
items_per_row = min(self.items_per_row, len(self.video_files))
|
||||
|
||||
# Get window dimensions for calculations
|
||||
try:
|
||||
window_rect = cv2.getWindowImageRect("Project View")
|
||||
if window_rect[2] > 0 and window_rect[3] > 0:
|
||||
window_width = window_rect[2]
|
||||
window_height = window_rect[3]
|
||||
else:
|
||||
window_width = self.window_width
|
||||
window_height = self.window_height
|
||||
except:
|
||||
window_width = self.window_width
|
||||
window_height = self.window_height
|
||||
|
||||
# Calculate thumbnail size and item height dynamically
|
||||
thumbnail_width, thumbnail_height = self._calculate_thumbnail_size(window_width)
|
||||
item_height = thumbnail_height + self.PROGRESS_BAR_HEIGHT + self.TEXT_HEIGHT + self.THUMBNAIL_MARGIN
|
||||
|
||||
selected_row = self.selected_index // items_per_row
|
||||
visible_rows = max(1, window_height // item_height)
|
||||
|
||||
# Calculate how many rows we can actually show
|
||||
total_rows = (len(self.video_files) + items_per_row - 1) // items_per_row
|
||||
|
||||
# If we can show all rows, no scrolling needed
|
||||
if total_rows <= visible_rows:
|
||||
self.scroll_offset = 0
|
||||
return
|
||||
|
||||
# Update scroll to keep selected item visible
|
||||
if selected_row < self.scroll_offset:
|
||||
self.scroll_offset = selected_row
|
||||
elif selected_row >= self.scroll_offset + visible_rows:
|
||||
self.scroll_offset = selected_row - visible_rows + 1
|
||||
|
||||
# Ensure scroll offset doesn't go negative or beyond available content
|
||||
self.scroll_offset = max(0, min(self.scroll_offset, total_rows - visible_rows))
|
||||
Reference in New Issue
Block a user