AnimeVideoEditot/gui/video_item.py
stirelshka8 b8211b5220 UPD
2026-03-24 14:38:40 +03:00

294 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
import tkinter as tk
from tkinter import ttk
import os
from utils.file_utils import get_file_size
from gui.theme import COLORS
try:
from PIL import Image, ImageTk
_HAS_PIL = True
except Exception:
Image = None
ImageTk = None
_HAS_PIL = False
class VideoItem(ttk.Frame):
"""Карточка одного видео в списке с диапазонами и стильным оформлением."""
def __init__(self, parent, file_path, on_remove=None, on_move_up=None, on_move_down=None, order=1):
super().__init__(parent)
self.file_path = file_path
self.time_ranges = []
self._duration = 0
self.on_remove = on_remove
self.on_move_up = on_move_up
self.on_move_down = on_move_down
self.order_var = tk.StringVar(value=str(order))
self._thumbnail_photo = None
self._duration_label = None
self._thumb_label = None
self._card = None
self._hovered = False
self._flash_job = None
self.setup_ui()
def setup_ui(self):
# Контейнер-карточка с фоном
self.configure(style="Card.TFrame")
card = tk.Frame(self, bg=COLORS["surface"], highlightbackground=COLORS["border"], highlightthickness=1)
card.pack(fill=tk.BOTH, expand=True, padx=0, pady=0)
self._card = card
inner = tk.Frame(card, bg=COLORS["surface"], padx=12, pady=10)
inner.pack(fill=tk.BOTH, expand=True)
# Верхняя строка: миниатюра, имя, длительность, порядок, перемещение, удалить
top = tk.Frame(inner, bg=COLORS["surface"])
top.pack(fill=tk.X, pady=(0, 6))
thumb_wrap = tk.Frame(top, bg=COLORS["overlay"], width=360, height=202, highlightthickness=1, highlightbackground=COLORS["border"])
thumb_wrap.pack(side=tk.LEFT, padx=(0, 12), anchor=tk.N)
thumb_wrap.pack_propagate(False)
thumb_label = tk.Label(
thumb_wrap,
bg=COLORS["overlay"],
fg=COLORS["subtext"],
text="Загрузка...",
relief=tk.FLAT,
)
thumb_label.pack(fill=tk.BOTH, expand=True)
self._thumb_label = thumb_label
text_wrap = tk.Frame(top, bg=COLORS["surface"])
text_wrap.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, anchor=tk.N)
file_name = os.path.basename(self.file_path)
try:
size_str = get_file_size(self.file_path)
file_info = f"{file_name} · {size_str}"
except Exception:
file_info = file_name
lbl_name = tk.Label(text_wrap, text=file_info, font=("Tahoma", 10, "bold"),
fg=COLORS["text"], bg=COLORS["surface"])
lbl_name.pack(anchor=tk.W)
lbl_dur = tk.Label(text_wrap, text="Длительность: ...", font=("Tahoma", 9),
fg=COLORS["subtext"], bg=COLORS["surface"])
lbl_dur.pack(anchor=tk.W, pady=(2, 0))
self._duration_label = lbl_dur
controls_wrap = tk.Frame(top, bg=COLORS["surface"])
controls_wrap.pack(side=tk.RIGHT, padx=(8, 0))
order_wrap = tk.Frame(controls_wrap, bg=COLORS["surface"])
order_wrap.pack(anchor=tk.E, pady=(0, 4))
tk.Label(order_wrap, text="Порядок:", font=("Tahoma", 8), fg=COLORS["subtext"], bg=COLORS["surface"]).pack(side=tk.LEFT, padx=(0, 4))
tk.Entry(
order_wrap,
textvariable=self.order_var,
width=5,
font=("Consolas", 9),
bg=COLORS["overlay"],
fg=COLORS["text"],
insertbackground=COLORS["text"],
relief=tk.FLAT,
highlightthickness=1,
highlightbackground=COLORS["border"],
).pack(side=tk.LEFT, ipady=3, ipadx=4)
move_wrap = tk.Frame(controls_wrap, bg=COLORS["surface"])
move_wrap.pack(anchor=tk.E)
tk.Button(
move_wrap,
text="",
font=("Tahoma", 9),
width=3,
fg=COLORS["text"],
bg=COLORS["overlay"],
activebackground=COLORS["accent"],
activeforeground=COLORS["text"],
relief=tk.FLAT,
cursor="hand2",
command=self._on_move_up_click,
).pack(side=tk.LEFT, padx=(0, 4))
tk.Button(
move_wrap,
text="",
font=("Tahoma", 9),
width=3,
fg=COLORS["text"],
bg=COLORS["overlay"],
activebackground=COLORS["accent"],
activeforeground=COLORS["text"],
relief=tk.FLAT,
cursor="hand2",
command=self._on_move_down_click,
).pack(side=tk.LEFT, padx=(0, 4))
btn_remove = tk.Button(move_wrap, text="", font=("Tahoma", 9), fg=COLORS["text"],
bg=COLORS["overlay"], activebackground=COLORS["red"],
activeforeground=COLORS["text"], relief=tk.FLAT, cursor="hand2",
command=self._on_remove_click)
btn_remove.pack(side=tk.LEFT)
# Блок диапазонов на удаление (остальное попадёт в итоговое видео)
ranges_lbl = tk.Label(inner, text="Удалить из видео (исключить эти фрагменты)", font=("Tahoma", 9),
fg=COLORS["subtext"], bg=COLORS["surface"])
ranges_lbl.pack(anchor=tk.W, pady=(4, 4))
self.ranges_container = tk.Frame(inner, bg=COLORS["surface"])
self.ranges_container.pack(fill=tk.X, pady=(0, 6))
ttk.Button(inner, text="+ Добавить диапазон на удаление", command=self.add_time_range).pack(anchor=tk.W)
self.columnconfigure(0, weight=1)
self._bind_hover(card)
self._bind_hover(inner)
self._bind_hover(top)
self._bind_hover(text_wrap)
self._bind_hover(thumb_wrap)
self._bind_hover(thumb_label)
def format_time(self, seconds):
h = int(seconds // 3600)
m = int((seconds % 3600) // 60)
s = int(seconds % 60)
return f"{h:02d}:{m:02d}:{s:02d}"
def add_time_range(self, start_time=0, end_time=0):
row = tk.Frame(self.ranges_container, bg=COLORS["surface"])
row.pack(fill=tk.X, pady=2)
tk.Label(row, text="Удалить с", font=("Tahoma", 8), fg=COLORS["subtext"], bg=COLORS["surface"]).pack(side=tk.LEFT, padx=(0, 4))
start_var = tk.StringVar(value=self.format_time(start_time))
ent_start = tk.Entry(row, textvariable=start_var, width=10, font=("Consolas", 9),
bg=COLORS["overlay"], fg=COLORS["text"], insertbackground=COLORS["text"],
relief=tk.FLAT, highlightthickness=1, highlightbackground=COLORS["border"])
ent_start.pack(side=tk.LEFT, padx=(0, 12), ipady=4, ipadx=6)
tk.Label(row, text="по", font=("Tahoma", 8), fg=COLORS["subtext"], bg=COLORS["surface"]).pack(side=tk.LEFT, padx=(0, 4))
end_var = tk.StringVar(value=self.format_time(end_time))
ent_end = tk.Entry(row, textvariable=end_var, width=10, font=("Consolas", 9),
bg=COLORS["overlay"], fg=COLORS["text"], insertbackground=COLORS["text"],
relief=tk.FLAT, highlightthickness=1, highlightbackground=COLORS["border"])
ent_end.pack(side=tk.LEFT, padx=(0, 8), ipady=4, ipadx=6)
def remove_range():
row.destroy()
self.time_ranges[:] = [r for r in self.time_ranges if r["frame"] != row]
btn_del = tk.Button(row, text="×", font=("Tahoma", 9), fg=COLORS["subtext"],
bg=COLORS["surface"], activebackground=COLORS["red"],
activeforeground=COLORS["text"], relief=tk.FLAT, cursor="hand2",
command=remove_range)
btn_del.pack(side=tk.LEFT)
self.time_ranges.append({"frame": row, "start_var": start_var, "end_var": end_var})
def get_time_ranges(self):
out = []
for r in self.time_ranges:
try:
start = self.parse_time(r["start_var"].get())
end = self.parse_time(r["end_var"].get())
if start < end:
out.append((start, end))
except (ValueError, TypeError):
continue
return out
def parse_time(self, time_str):
time_str = (time_str or "").strip()
if not time_str:
return 0
parts = time_str.split(":")
if len(parts) == 3:
return int(parts[0]) * 3600 + int(parts[1]) * 60 + float(parts[2])
if len(parts) == 2:
return int(parts[0]) * 60 + float(parts[1])
return float(parts[0])
def _on_remove_click(self):
if self.on_remove:
self.on_remove(self)
self.destroy()
def _on_move_up_click(self):
if self.on_move_up:
self.on_move_up(self)
def _on_move_down_click(self):
if self.on_move_down:
self.on_move_down(self)
def get_duration(self):
return self._duration
def get_order(self):
raw = (self.order_var.get() or "").strip()
if not raw:
raise ValueError("Поле порядка не заполнено.")
order = int(raw)
if order <= 0:
raise ValueError("Порядок должен быть целым числом больше 0.")
return order
def set_order(self, value):
self.order_var.set(str(int(value)))
def set_media_info(self, duration=None, thumb_image=None, error_text=None):
if duration is not None and duration > 0:
self._duration = duration
if self._duration_label is not None:
self._duration_label.config(text=f"Длительность: {self.format_time(duration)}", fg=COLORS["subtext"])
elif error_text and self._duration_label is not None:
self._duration_label.config(text=error_text, fg=COLORS["red"])
if self._thumb_label is None:
return
if thumb_image is not None and _HAS_PIL:
try:
photo = ImageTk.PhotoImage(thumb_image)
self._thumbnail_photo = photo
self._thumb_label.config(image=photo, text="")
return
except Exception:
pass
if error_text:
self._thumb_label.config(text="Без превью")
def flash_reorder(self):
if self._card is None:
return
if self._flash_job is not None:
try:
self.after_cancel(self._flash_job)
except Exception:
pass
self._card.config(highlightbackground=COLORS["accent"], highlightthickness=2)
self._flash_job = self.after(450, self._restore_highlight)
def _restore_highlight(self):
self._flash_job = None
if self._card is None:
return
if self._hovered:
self._card.config(highlightbackground=COLORS["accent"], highlightthickness=2)
else:
self._card.config(highlightbackground=COLORS["border"], highlightthickness=1)
def _bind_hover(self, widget):
widget.bind("<Enter>", self._on_hover_enter, add="+")
widget.bind("<Leave>", self._on_hover_leave, add="+")
def _on_hover_enter(self, _event=None):
self._hovered = True
if self._card is not None:
self._card.config(highlightbackground=COLORS["accent"], highlightthickness=2)
def _on_hover_leave(self, _event=None):
self._hovered = False
self._restore_highlight()