294 lines
12 KiB
Python
294 lines
12 KiB
Python
# -*- 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()
|