# -*- 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("", self._on_hover_enter, add="+") widget.bind("", 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()