Files
Tagger/src/ui/gui.py

1297 lines
51 KiB
Python
Raw Normal View History

2025-12-28 16:05:34 +01:00
"""
Modern qBittorrent-style GUI for Tagger
"""
2025-09-21 19:28:17 +02:00
import os
2025-09-11 18:59:22 +02:00
import sys
2025-09-21 19:28:17 +02:00
import subprocess
2025-09-08 13:22:10 +02:00
import tkinter as tk
2025-09-24 14:30:23 +02:00
from tkinter import ttk, simpledialog, messagebox, filedialog
from pathlib import Path
2025-09-28 13:00:39 +02:00
from typing import List
2025-09-21 19:28:17 +02:00
from src.core.media_utils import load_icon
2025-09-24 06:58:13 +02:00
from src.core.file_manager import FileManager
2025-12-28 17:44:24 +01:00
from src.core.tag_manager import TagManager, DEFAULT_TAG_ORDER
2025-09-24 14:30:23 +02:00
from src.core.file import File
from src.core.tag import Tag
2025-09-28 13:00:39 +02:00
from src.core.list_manager import ListManager
from src.core.constants import APP_NAME, VERSION, APP_VIEWPORT
2025-12-28 16:05:34 +01:00
from src.core.config import save_global_config
from src.core.hardlink_manager import HardlinkManager
# qBittorrent-inspired color scheme
COLORS = {
"bg": "#ffffff",
"sidebar_bg": "#f5f5f5",
"toolbar_bg": "#f0f0f0",
"selected": "#0078d7",
"selected_text": "#ffffff",
"border": "#d0d0d0",
"status_bg": "#f8f8f8",
"text": "#000000",
}
# Tag category colors
TAG_COLORS = [
"#e74c3c", # red
"#3498db", # blue
"#2ecc71", # green
"#f39c12", # orange
"#9b59b6", # purple
"#1abc9c", # teal
"#e91e63", # pink
"#00bcd4", # cyan
]
# Fixed colors for default categories
DEFAULT_CATEGORY_COLORS = {
"Hodnocení": "#f1c40f", # gold/yellow for stars
"Barva": "#95a5a6", # gray for color category
}
# Categories where only one tag can be selected (exclusive/radio behavior)
EXCLUSIVE_CATEGORIES = {"Hodnocení"}
2025-09-17 06:46:05 +02:00
2025-09-28 13:00:39 +02:00
class MultiFileTagAssignDialog(tk.Toplevel):
2025-12-28 16:05:34 +01:00
"""Dialog for bulk tag assignment to multiple files"""
def __init__(self, parent, all_tags: List[Tag], files: List[File], category_colors: dict = None):
2025-09-28 13:00:39 +02:00
super().__init__(parent)
self.title("Přiřadit tagy k vybraným souborům")
2025-12-28 16:05:34 +01:00
self.result = None
2025-09-28 13:00:39 +02:00
self.vars: dict[str, int] = {}
self.checkbuttons: dict[str, tk.Checkbutton] = {}
self.tags_by_full = {t.full_path: t for t in all_tags}
self.files = files
2025-12-28 16:05:34 +01:00
self.category_colors = category_colors or {}
self.category_checkbuttons: dict[str, list] = {} # category -> list of checkbuttons
2025-09-28 13:00:39 +02:00
2025-12-28 16:05:34 +01:00
self.geometry("500x600")
self.minsize(400, 400)
self.configure(bg=COLORS["bg"])
2025-09-28 13:00:39 +02:00
2025-12-28 16:05:34 +01:00
tk.Label(self, text=f"Vybráno souborů: {len(files)}",
bg=COLORS["bg"], font=("Arial", 11, "bold")).pack(pady=10)
2025-09-28 13:00:39 +02:00
2025-12-28 16:05:34 +01:00
# Scrollable frame
canvas = tk.Canvas(self, bg=COLORS["bg"])
scrollbar = ttk.Scrollbar(self, orient="vertical", command=canvas.yview)
frame = tk.Frame(canvas, bg=COLORS["bg"])
2025-09-28 13:00:39 +02:00
2025-12-28 16:05:34 +01:00
frame.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
canvas.create_window((0, 0), window=frame, anchor="nw")
canvas.configure(yscrollcommand=scrollbar.set)
2025-09-28 13:00:39 +02:00
2025-12-28 16:05:34 +01:00
canvas.pack(side="left", fill="both", expand=True, padx=10)
scrollbar.pack(side="right", fill="y")
2025-09-28 13:00:39 +02:00
2025-12-28 16:05:34 +01:00
# Enable mousewheel scrolling (only when dialog is active)
def on_mousewheel(event):
if canvas.winfo_exists():
canvas.yview_scroll(int(-1*(event.delta/120)), "units")
2025-09-28 13:00:39 +02:00
2025-12-28 16:05:34 +01:00
def on_scroll_up(event):
if canvas.winfo_exists():
canvas.yview_scroll(-1, "units")
def on_scroll_down(event):
if canvas.winfo_exists():
canvas.yview_scroll(1, "units")
canvas.bind("<MouseWheel>", on_mousewheel)
canvas.bind("<Button-4>", on_scroll_up)
canvas.bind("<Button-5>", on_scroll_down)
frame.bind("<MouseWheel>", on_mousewheel)
frame.bind("<Button-4>", on_scroll_up)
frame.bind("<Button-5>", on_scroll_down)
file_tag_sets = [{t.full_path for t in f.tags} for f in files]
# Group by category
tags_by_category = {}
for full_path, tag in self.tags_by_full.items():
if tag.category not in tags_by_category:
tags_by_category[tag.category] = []
tags_by_category[tag.category].append((full_path, tag))
2025-12-28 17:44:24 +01:00
# Sort tags within each category
for category in tags_by_category:
if category in DEFAULT_TAG_ORDER:
order = DEFAULT_TAG_ORDER[category]
tags_by_category[category].sort(key=lambda x: order.get(x[1].name, 999))
else:
tags_by_category[category].sort(key=lambda x: x[1].name)
2025-12-28 16:05:34 +01:00
for category in sorted(tags_by_category.keys()):
color = self.category_colors.get(category, "#333333")
is_exclusive = category in EXCLUSIVE_CATEGORIES
exclusive_note = " (pouze jedno)" if is_exclusive else ""
cat_label = tk.Label(frame, text=f"{category}{exclusive_note}", bg=COLORS["bg"],
fg=color, font=("Arial", 10, "bold"))
cat_label.pack(fill="x", anchor="w", pady=(12, 4))
self.category_checkbuttons[category] = []
2025-12-28 17:44:24 +01:00
for full_path, tag in tags_by_category[category]:
2025-12-28 16:05:34 +01:00
have_count = sum(1 for s in file_tag_sets if full_path in s)
if have_count == 0:
init = 0
elif have_count == len(files):
init = 1
else:
init = 2 # mixed
cb = tk.Checkbutton(frame, text=f" {tag.name}", anchor="w", bg=COLORS["bg"],
font=("Arial", 10))
cb.state_value = init
cb.full_path = full_path
cb.tag_color = color
cb.category = category
cb.pack(fill="x", anchor="w", padx=20)
cb.bind("<Button-1>", self._on_toggle)
self._update_checkbox_look(cb)
self.checkbuttons[full_path] = cb
self.vars[full_path] = init
self.category_checkbuttons[category].append(cb)
btn_frame = tk.Frame(self, bg=COLORS["bg"])
btn_frame.pack(pady=15)
tk.Button(btn_frame, text="OK", command=self.on_ok, width=12,
font=("Arial", 10)).pack(side="left", padx=5)
tk.Button(btn_frame, text="Zrušit", command=self.destroy, width=12,
font=("Arial", 10)).pack(side="left", padx=5)
2025-09-28 13:00:39 +02:00
self.transient(parent)
self.grab_set()
parent.wait_window(self)
def _on_toggle(self, event):
cb: tk.Checkbutton = event.widget
2025-12-28 16:05:34 +01:00
category = cb.category
2025-09-28 13:00:39 +02:00
cur = cb.state_value
2025-12-28 16:05:34 +01:00
# For exclusive categories, uncheck others first
if category in EXCLUSIVE_CATEGORIES:
if cur == 0 or cur == 2: # turning on
# Uncheck all others in this category
for other_cb in self.category_checkbuttons.get(category, []):
if other_cb != cb and other_cb.state_value != 0:
other_cb.state_value = 0
self._update_checkbox_look(other_cb)
cb.state_value = 1
else: # turning off
cb.state_value = 0
else:
# Normal toggle behavior
if cur == 0:
cb.state_value = 1
elif cur == 1:
cb.state_value = 0
elif cur == 2:
cb.state_value = 1
2025-09-28 13:00:39 +02:00
self._update_checkbox_look(cb)
return "break"
def _update_checkbox_look(self, cb: tk.Checkbutton):
v = cb.state_value
2025-12-28 16:05:34 +01:00
color = getattr(cb, 'tag_color', '#333333')
2025-09-28 13:00:39 +02:00
if v == 0:
cb.deselect()
2025-12-28 16:05:34 +01:00
cb.config(fg="#666666")
2025-09-28 13:00:39 +02:00
elif v == 1:
cb.select()
2025-12-28 16:05:34 +01:00
cb.config(fg=color)
2025-09-28 13:00:39 +02:00
elif v == 2:
2025-12-28 16:05:34 +01:00
cb.deselect()
cb.config(fg="#cc6600") # orange for mixed
2025-09-28 13:00:39 +02:00
def on_ok(self):
self.result = {full: cb.state_value for full, cb in self.checkbuttons.items()}
self.destroy()
2025-09-21 19:28:17 +02:00
class App:
2025-09-24 14:30:23 +02:00
def __init__(self, filehandler: FileManager, tagmanager: TagManager):
2025-09-24 06:58:13 +02:00
self.filehandler = filehandler
2025-09-24 14:30:23 +02:00
self.tagmanager = tagmanager
2025-09-28 13:00:39 +02:00
self.list_manager = ListManager()
2025-12-28 16:05:34 +01:00
# State
self.states = {}
self.file_items = {} # Treeview item_id -> File object mapping
self.selected_tree_item_for_context = None
self.hide_ignored_var = None
2025-09-28 13:00:39 +02:00
self.filter_text = ""
self.show_full_path = False
self.sort_mode = "name"
self.sort_order = "asc"
2025-12-28 16:05:34 +01:00
self.category_colors = {} # category -> color mapping
2025-09-24 14:30:23 +02:00
self.filehandler.on_files_changed = self.update_files_from_manager
2025-09-21 19:28:17 +02:00
2025-12-28 16:05:34 +01:00
def _on_close(self):
"""Save window geometry and close"""
# Check if maximized
is_maximized = self.root.state() == 'zoomed'
self.filehandler.global_config["window_maximized"] = is_maximized
2025-12-28 16:05:34 +01:00
# Save geometry only when not maximized
if not is_maximized:
self.filehandler.global_config["window_geometry"] = self.root.geometry()
2025-12-28 16:05:34 +01:00
save_global_config(self.filehandler.global_config)
self.root.destroy()
2025-09-24 14:30:23 +02:00
def main(self):
2025-09-17 06:46:05 +02:00
root = tk.Tk()
2025-12-28 16:05:34 +01:00
root.title(f"{APP_NAME} {VERSION}")
# Load window geometry from global config
geometry = self.filehandler.global_config.get("window_geometry", APP_VIEWPORT)
root.geometry(geometry)
if self.filehandler.global_config.get("window_maximized", False):
root.state('zoomed')
root.configure(bg=COLORS["bg"])
2025-09-21 19:28:17 +02:00
self.root = root
2025-12-28 16:05:34 +01:00
# Bind window close to save geometry
root.protocol("WM_DELETE_WINDOW", self._on_close)
self.hide_ignored_var = tk.BooleanVar(value=False, master=root)
2025-12-28 16:05:34 +01:00
# Load last folder
last = self.filehandler.global_config.get("last_folder")
if last:
try:
self.filehandler.append(Path(last))
except Exception:
pass
2025-12-28 16:05:34 +01:00
# Load icons
self._load_icons()
# Build UI
self._create_menu()
self._create_toolbar()
self._create_main_layout()
self._create_status_bar()
self._create_context_menus()
self._bind_shortcuts()
# Initial refresh
self.refresh_sidebar()
self.update_files_from_manager(self.filehandler.filelist)
root.mainloop()
def _load_icons(self):
"""Load application icons"""
try:
unchecked = load_icon("src/resources/images/32/32_unchecked.png")
checked = load_icon("src/resources/images/32/32_checked.png")
tag_icon = load_icon("src/resources/images/32/32_tag.png")
self.icons = {"unchecked": unchecked, "checked": checked, "tag": tag_icon}
self.root.unchecked_img = unchecked
self.root.checked_img = checked
self.root.tag_img = tag_icon
except Exception as e:
print(f"Warning: Could not load icons: {e}")
self.icons = {"unchecked": None, "checked": None, "tag": None}
2025-09-17 06:46:05 +02:00
2025-12-28 16:05:34 +01:00
def _create_menu(self):
"""Create menu bar"""
menu_bar = tk.Menu(self.root)
self.root.config(menu=menu_bar)
2025-12-28 16:05:34 +01:00
# File menu
2025-09-17 06:46:05 +02:00
file_menu = tk.Menu(menu_bar, tearoff=0)
2025-12-28 16:05:34 +01:00
file_menu.add_command(label="Open Folder... (Ctrl+O)", command=self.open_folder_dialog)
file_menu.add_command(label="Nastavit ignorované vzory", command=self.set_ignore_patterns)
2025-09-24 14:30:23 +02:00
file_menu.add_separator()
2025-12-28 16:05:34 +01:00
file_menu.add_command(label="Exit (Ctrl+Q)", command=self.root.quit)
2025-12-28 16:05:34 +01:00
# View menu
view_menu = tk.Menu(menu_bar, tearoff=0)
view_menu.add_checkbutton(
label="Skrýt ignorované",
variable=self.hide_ignored_var,
command=self.toggle_hide_ignored
)
2025-12-28 16:05:34 +01:00
view_menu.add_command(label="Refresh (F5)", command=self.refresh_all)
# Tools menu
tools_menu = tk.Menu(menu_bar, tearoff=0)
tools_menu.add_command(label="Nastavit datum (Ctrl+D)", command=self.set_date_for_selected)
tools_menu.add_command(label="Detekovat rozlišení videí", command=self.detect_video_resolution)
tools_menu.add_command(label="Přiřadit tagy (Ctrl+T)", command=self.assign_tag_to_selected_bulk)
tools_menu.add_separator()
tools_menu.add_command(label="Nastavit hardlink složku...", command=self.configure_hardlink_folder)
tools_menu.add_command(label="Aktualizovat hardlink strukturu", command=self.update_hardlink_structure)
tools_menu.add_command(label="Vytvořit hardlink strukturu...", command=self.create_hardlink_structure)
menu_bar.add_cascade(label="Soubor", menu=file_menu)
menu_bar.add_cascade(label="Pohled", menu=view_menu)
2025-12-28 16:05:34 +01:00
menu_bar.add_cascade(label="Nástroje", menu=tools_menu)
2025-09-21 19:28:17 +02:00
2025-12-28 16:05:34 +01:00
def _create_toolbar(self):
"""Create toolbar with buttons"""
toolbar = tk.Frame(self.root, bg=COLORS["toolbar_bg"], height=40, relief=tk.RAISED, bd=1)
toolbar.pack(side=tk.TOP, fill=tk.X)
# Buttons
tk.Button(toolbar, text="📁 Otevřít složku", command=self.open_folder_dialog,
relief=tk.FLAT, bg=COLORS["toolbar_bg"]).pack(side=tk.LEFT, padx=5, pady=5)
tk.Button(toolbar, text="🔄 Obnovit", command=self.refresh_all,
relief=tk.FLAT, bg=COLORS["toolbar_bg"]).pack(side=tk.LEFT, padx=2, pady=5)
ttk.Separator(toolbar, orient=tk.VERTICAL).pack(side=tk.LEFT, fill=tk.Y, padx=5)
tk.Button(toolbar, text="🏷️ Nový tag", command=lambda: self.tree_add_tag(background=True),
relief=tk.FLAT, bg=COLORS["toolbar_bg"]).pack(side=tk.LEFT, padx=2, pady=5)
tk.Button(toolbar, text="📅 Nastavit datum", command=self.set_date_for_selected,
relief=tk.FLAT, bg=COLORS["toolbar_bg"]).pack(side=tk.LEFT, padx=2, pady=5)
ttk.Separator(toolbar, orient=tk.VERTICAL).pack(side=tk.LEFT, fill=tk.Y, padx=5)
# Search box
search_frame = tk.Frame(toolbar, bg=COLORS["toolbar_bg"])
search_frame.pack(side=tk.RIGHT, padx=10, pady=5)
tk.Label(search_frame, text="🔍", bg=COLORS["toolbar_bg"]).pack(side=tk.LEFT)
self.search_var = tk.StringVar()
self.search_var.trace('w', lambda *args: self.on_filter_changed())
search_entry = tk.Entry(search_frame, textvariable=self.search_var, width=25)
search_entry.pack(side=tk.LEFT, padx=5)
def _create_main_layout(self):
"""Create main split layout"""
# Main container
main_container = tk.PanedWindow(self.root, orient=tk.HORIZONTAL, sashwidth=5, bg=COLORS["border"])
main_container.pack(fill=tk.BOTH, expand=True)
# Left sidebar (tags)
self._create_sidebar(main_container)
2025-09-21 19:28:17 +02:00
2025-12-28 16:05:34 +01:00
# Right panel (files table)
self._create_file_panel(main_container)
def _create_sidebar(self, parent):
"""Create left sidebar with tag tree"""
sidebar_frame = tk.Frame(parent, bg=COLORS["sidebar_bg"], width=250)
# Sidebar header
header = tk.Frame(sidebar_frame, bg=COLORS["sidebar_bg"])
header.pack(fill=tk.X, padx=5, pady=5)
tk.Label(header, text="📂 Štítky", font=("Arial", 10, "bold"),
bg=COLORS["sidebar_bg"]).pack(side=tk.LEFT)
# Tag tree
tree_frame = tk.Frame(sidebar_frame)
tree_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
self.tag_tree = ttk.Treeview(tree_frame, selectmode="browse", show="tree")
self.tag_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
tree_scroll = ttk.Scrollbar(tree_frame, orient=tk.VERTICAL, command=self.tag_tree.yview)
tree_scroll.pack(side=tk.RIGHT, fill=tk.Y)
self.tag_tree.config(yscrollcommand=tree_scroll.set)
# Bind events
self.tag_tree.bind("<Button-1>", self.on_tree_left_click)
self.tag_tree.bind("<Button-3>", self.on_tree_right_click)
parent.add(sidebar_frame)
def _create_file_panel(self, parent):
"""Create right panel with file table"""
file_frame = tk.Frame(parent, bg=COLORS["bg"])
# Control panel
control_frame = tk.Frame(file_frame, bg=COLORS["bg"])
control_frame.pack(fill=tk.X, padx=5, pady=5)
# View options
tk.Checkbutton(control_frame, text="Plná cesta", variable=tk.BooleanVar(),
command=self.toggle_show_path, bg=COLORS["bg"]).pack(side=tk.LEFT, padx=5)
# File table
table_frame = tk.Frame(file_frame)
table_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
# Define columns
columns = ("name", "date", "tags", "size")
self.file_table = ttk.Treeview(table_frame, columns=columns, show="headings", selectmode="extended")
# Column headers with sort commands
self.file_table.heading("name", text="📄 Název ▲", command=lambda: self.sort_by_column("name"))
self.file_table.heading("date", text="📅 Datum", command=lambda: self.sort_by_column("date"))
self.file_table.heading("tags", text="🏷️ Štítky")
self.file_table.heading("size", text="💾 Velikost", command=lambda: self.sort_by_column("size"))
# Column widths
self.file_table.column("name", width=300)
self.file_table.column("date", width=100)
self.file_table.column("tags", width=200)
self.file_table.column("size", width=80)
# Scrollbars
vsb = ttk.Scrollbar(table_frame, orient=tk.VERTICAL, command=self.file_table.yview)
hsb = ttk.Scrollbar(table_frame, orient=tk.HORIZONTAL, command=self.file_table.xview)
self.file_table.configure(yscrollcommand=vsb.set, xscrollcommand=hsb.set)
self.file_table.grid(row=0, column=0, sticky="nsew")
vsb.grid(row=0, column=1, sticky="ns")
hsb.grid(row=1, column=0, sticky="ew")
table_frame.grid_rowconfigure(0, weight=1)
table_frame.grid_columnconfigure(0, weight=1)
# Bind events
self.file_table.bind("<Double-1>", self.on_file_double_click)
self.file_table.bind("<Button-3>", self.on_file_right_click)
self.file_table.bind("<<TreeviewSelect>>", self.on_selection_changed)
parent.add(file_frame)
def _create_status_bar(self):
"""Create status bar at bottom"""
status_frame = tk.Frame(self.root, bg=COLORS["status_bg"], relief=tk.SUNKEN, bd=1)
status_frame.pack(side=tk.BOTTOM, fill=tk.X)
# Left side - status message
self.status_label = tk.Label(status_frame, text="Připraven", anchor=tk.W,
bg=COLORS["status_bg"], padx=10)
self.status_label.pack(side=tk.LEFT, fill=tk.X, expand=True)
# Right side - file count
self.file_count_label = tk.Label(status_frame, text="0 souborů", anchor=tk.E,
bg=COLORS["status_bg"], padx=10)
self.file_count_label.pack(side=tk.RIGHT)
# Selected size
self.selected_size_label = tk.Label(status_frame, text="", anchor=tk.E,
bg=COLORS["status_bg"], padx=10)
self.selected_size_label.pack(side=tk.RIGHT)
# Selected count
self.selected_count_label = tk.Label(status_frame, text="", anchor=tk.E,
bg=COLORS["status_bg"], padx=10)
self.selected_count_label.pack(side=tk.RIGHT)
def _create_context_menus(self):
"""Create context menus"""
# Tag context menu
self.tag_menu = tk.Menu(self.root, tearoff=0)
self.tag_menu.add_command(label="Nový štítek", command=self.tree_add_tag)
self.tag_menu.add_command(label="Smazat štítek", command=self.tree_delete_tag)
# File context menu
self.file_menu = tk.Menu(self.root, tearoff=0)
self.file_menu.add_command(label="Otevřít soubor", command=self.open_selected_files)
self.file_menu.add_command(label="Přiřadit štítky (Ctrl+T)", command=self.assign_tag_to_selected_bulk)
self.file_menu.add_command(label="Nastavit datum (Ctrl+D)", command=self.set_date_for_selected)
self.file_menu.add_separator()
self.file_menu.add_command(label="Smazat z indexu (Del)", command=self.remove_selected_files)
def _bind_shortcuts(self):
"""Bind keyboard shortcuts"""
self.root.bind("<Control-o>", lambda e: self.open_folder_dialog())
self.root.bind("<Control-q>", lambda e: self.root.quit())
self.root.bind("<Control-t>", lambda e: self.assign_tag_to_selected_bulk())
self.root.bind("<Control-d>", lambda e: self.set_date_for_selected())
self.root.bind("<Control-f>", lambda e: self.search_var.get()) # Focus search
self.root.bind("<F5>", lambda e: self.refresh_all())
self.root.bind("<Delete>", lambda e: self.remove_selected_files())
2025-09-28 13:00:39 +02:00
# ==================================================
2025-12-28 16:05:34 +01:00
# SIDEBAR / TAG TREE METHODS
2025-09-28 13:00:39 +02:00
# ==================================================
2025-12-28 16:05:34 +01:00
def refresh_sidebar(self):
"""Refresh tag tree in sidebar"""
# Clear tree
for item in self.tag_tree.get_children():
self.tag_tree.delete(item)
# Reset tag item mapping
self.tag_tree_items = {} # full_path -> tree item_id
# Count files per tag (from all files)
tag_counts = {}
for f in self.filehandler.filelist:
for t in f.tags:
tag_counts[t.full_path] = tag_counts.get(t.full_path, 0) + 1
# Add root
total_files = len(self.filehandler.filelist)
root_id = self.tag_tree.insert("", "end", text=f"📂 Všechny soubory ({total_files})", image=self.icons.get("tag"))
self.tag_tree.item(root_id, open=True)
self.root_tag_id = root_id
# Assign colors to categories
categories = self.tagmanager.get_categories()
color_index = 0
for category in categories:
if category not in self.category_colors:
# Use predefined color for default categories, otherwise cycle through TAG_COLORS
if category in DEFAULT_CATEGORY_COLORS:
self.category_colors[category] = DEFAULT_CATEGORY_COLORS[category]
else:
self.category_colors[category] = TAG_COLORS[color_index % len(TAG_COLORS)]
color_index += 1
# Add categories and tags
for category in categories:
color = self.category_colors.get(category, "#333333")
cat_id = self.tag_tree.insert(root_id, "end", text=f"📁 {category}", image=self.icons.get("tag"),
tags=(f"cat_{category}",))
self.states[cat_id] = False
for tag in self.tagmanager.get_tags_in_category(category):
count = tag_counts.get(tag.full_path, 0)
count_str = f" ({count})" if count > 0 else ""
tag_id = self.tag_tree.insert(cat_id, "end", text=f" {tag.name}{count_str}",
image=self.icons.get("unchecked"),
tags=(f"tag_{category}",))
self.states[tag_id] = False
self.tag_tree_items[tag.full_path] = (tag_id, tag.name)
# Apply color to category tags
self.tag_tree.tag_configure(f"cat_{category}", foreground=color)
self.tag_tree.tag_configure(f"tag_{category}", foreground=color)
def update_tag_counts(self, filtered_files):
"""Update tag counts in sidebar based on filtered files"""
if not hasattr(self, 'tag_tree_items'):
return
2025-12-28 16:05:34 +01:00
# Count files per tag from filtered files
tag_counts = {}
for f in filtered_files:
for t in f.tags:
tag_counts[t.full_path] = tag_counts.get(t.full_path, 0) + 1
2025-12-28 16:05:34 +01:00
# Update each tag item text
for full_path, (item_id, tag_name) in self.tag_tree_items.items():
count = tag_counts.get(full_path, 0)
count_str = f" ({count})" if count > 0 else ""
# Preserve the checkbox state
current_text = f" {tag_name}{count_str}"
self.tag_tree.item(item_id, text=current_text)
2025-09-28 13:00:39 +02:00
2025-12-28 16:05:34 +01:00
# Update root count
total = len(filtered_files)
self.tag_tree.item(self.root_tag_id, text=f"📂 Všechny soubory ({total})")
def on_tree_left_click(self, event):
"""Handle left click on tag tree"""
region = self.tag_tree.identify("region", event.x, event.y)
if region not in ("tree", "icon"):
return
item_id = self.tag_tree.identify_row(event.y)
if not item_id:
return
parent_id = self.tag_tree.parent(item_id)
# Toggle folder open/close
if parent_id == "" or parent_id == self.root_tag_id:
is_open = self.tag_tree.item(item_id, "open")
self.tag_tree.item(item_id, open=not is_open)
return
# Toggle tag checkbox
self.states[item_id] = not self.states.get(item_id, False)
self.tag_tree.item(item_id, image=self.icons["checked"] if self.states[item_id] else self.icons["unchecked"])
2025-09-28 13:00:39 +02:00
2025-12-28 16:05:34 +01:00
# Update file list
2025-09-28 13:00:39 +02:00
self.update_files_from_manager(self.filehandler.filelist)
2025-12-28 16:05:34 +01:00
def on_tree_right_click(self, event):
"""Handle right click on tag tree"""
item_id = self.tag_tree.identify_row(event.y)
if item_id:
self.selected_tree_item_for_context = item_id
self.tag_tree.selection_set(item_id)
self.tag_menu.tk_popup(event.x_root, event.y_root)
def tree_add_tag(self, background=False):
"""Add new tag"""
name = simpledialog.askstring("Nový tag", "Název tagu:")
if not name:
return
parent = self.selected_tree_item_for_context if not background else self.root_tag_id
new_id = self.tag_tree.insert(parent, "end", text=f" {name}", image=self.icons["unchecked"])
self.states[new_id] = False
if parent == self.root_tag_id:
self.tagmanager.add_category(name)
self.tag_tree.item(new_id, image=self.icons["tag"])
else:
category = self.tag_tree.item(parent, "text").replace("📁 ", "")
self.tagmanager.add_tag(category, name)
self.status_label.config(text=f"Vytvořen tag: {name}")
def tree_delete_tag(self):
"""Delete selected tag"""
item = self.selected_tree_item_for_context
if not item:
return
name = self.tag_tree.item(item, "text").strip()
ans = messagebox.askyesno("Smazat tag", f"Opravdu chcete smazat '{name}'?")
if not ans:
return
parent_id = self.tag_tree.parent(item)
self.tag_tree.delete(item)
self.states.pop(item, None)
if parent_id == self.root_tag_id:
self.tagmanager.remove_category(name.replace("📁 ", ""))
else:
category = self.tag_tree.item(parent_id, "text").replace("📁 ", "")
self.tagmanager.remove_tag(category, name)
2025-09-28 13:00:39 +02:00
self.update_files_from_manager(self.filehandler.filelist)
2025-12-28 16:05:34 +01:00
self.status_label.config(text=f"Smazán tag: {name}")
def get_checked_tags(self) -> List[Tag]:
"""Get list of checked tags"""
tags = []
for item_id, checked in self.states.items():
if not checked:
continue
parent_id = self.tag_tree.parent(item_id)
if parent_id == "" or parent_id == self.root_tag_id:
continue
category = self.tag_tree.item(parent_id, "text").replace("📁 ", "")
# Get tag name from stored mapping (not from text which includes count)
tag_name = None
for full_path, (stored_id, stored_name) in self.tag_tree_items.items():
if stored_id == item_id:
tag_name = stored_name
break
if tag_name:
tags.append(Tag(category, tag_name))
return tags
2025-09-28 13:00:39 +02:00
2025-09-21 19:28:17 +02:00
# ==================================================
2025-12-28 16:05:34 +01:00
# FILE TABLE METHODS
2025-09-21 19:28:17 +02:00
# ==================================================
2025-12-28 16:05:34 +01:00
2025-09-28 13:00:39 +02:00
def update_files_from_manager(self, filelist=None):
2025-12-28 16:05:34 +01:00
"""Update file table"""
2025-09-28 13:00:39 +02:00
if filelist is None:
filelist = self.filehandler.filelist
2025-12-28 16:05:34 +01:00
# Filter by checked tags
2025-09-28 13:00:39 +02:00
checked_tags = self.get_checked_tags()
filtered_files = self.filehandler.filter_files_by_tags(checked_tags)
2025-12-28 16:05:34 +01:00
# Filter by search text
search_text = self.search_var.get().lower() if hasattr(self, 'search_var') else ""
if search_text:
2025-09-28 13:00:39 +02:00
filtered_files = [
f for f in filtered_files
2025-12-28 16:05:34 +01:00
if search_text in f.filename.lower() or
(self.show_full_path and search_text in str(f.file_path).lower())
2025-09-28 13:00:39 +02:00
]
2025-12-28 16:05:34 +01:00
# Filter ignored
if self.hide_ignored_var and self.hide_ignored_var.get():
filtered_files = [
f for f in filtered_files
if "Stav/Ignorované" not in {t.full_path for t in f.tags}
]
2025-12-28 16:05:34 +01:00
# Sort
2025-09-28 13:00:39 +02:00
reverse = (self.sort_order == "desc")
if self.sort_mode == "name":
filtered_files.sort(key=lambda f: f.filename.lower(), reverse=reverse)
elif self.sort_mode == "date":
filtered_files.sort(key=lambda f: (f.date or ""), reverse=reverse)
2025-12-28 16:05:34 +01:00
elif self.sort_mode == "size":
filtered_files.sort(key=lambda f: f.file_path.stat().st_size if f.file_path.exists() else 0, reverse=reverse)
# Clear table
for item in self.file_table.get_children():
self.file_table.delete(item)
self.file_items.clear()
# Populate table
for f in filtered_files:
name = str(f.file_path) if self.show_full_path else f.filename
date = f.date or ""
tags = ", ".join([t.name for t in f.tags[:3]]) # Show first 3 tags
if len(f.tags) > 3:
tags += f" +{len(f.tags) - 3}"
2025-09-28 13:00:39 +02:00
2025-12-28 16:05:34 +01:00
try:
size = f.file_path.stat().st_size
size_str = self._format_size(size)
except:
size_str = "?"
item_id = self.file_table.insert("", "end", values=(name, date, tags, size_str))
self.file_items[item_id] = f
# Update status
self.file_count_label.config(text=f"{len(filtered_files)} souborů")
self.status_label.config(text=f"Zobrazeno {len(filtered_files)} souborů")
# Update tag counts in sidebar
self.update_tag_counts(filtered_files)
def _format_size(self, size_bytes):
"""Format file size"""
for unit in ['B', 'KB', 'MB', 'GB']:
if size_bytes < 1024.0:
return f"{size_bytes:.1f} {unit}"
size_bytes /= 1024.0
return f"{size_bytes:.1f} TB"
def get_selected_files(self) -> List[File]:
"""Get selected files from table"""
selected_items = self.file_table.selection()
return [self.file_items[item] for item in selected_items if item in self.file_items]
def on_selection_changed(self, event=None):
"""Update status bar when selection changes"""
files = self.get_selected_files()
count = len(files)
if count == 0:
self.selected_count_label.config(text="")
self.selected_size_label.config(text="")
else:
self.selected_count_label.config(text=f"{count} vybráno")
total_size = 0
for f in files:
try:
total_size += f.file_path.stat().st_size
except:
pass
self.selected_size_label.config(text=f"[{self._format_size(total_size)}]")
def on_file_double_click(self, event):
"""Handle double click on file"""
files = self.get_selected_files()
for f in files:
self.open_file(f.file_path)
2025-09-21 19:28:17 +02:00
2025-12-28 16:05:34 +01:00
def on_file_right_click(self, event):
"""Handle right click on file"""
# Select item under cursor if not selected
item = self.file_table.identify_row(event.y)
if item and item not in self.file_table.selection():
self.file_table.selection_set(item)
2025-09-28 13:00:39 +02:00
2025-12-28 16:05:34 +01:00
# Update selected count
count = len(self.file_table.selection())
self.selected_count_label.config(text=f"{count} vybráno" if count > 0 else "")
2025-09-24 14:30:23 +02:00
2025-12-28 16:05:34 +01:00
self.file_menu.tk_popup(event.x_root, event.y_root)
def open_file(self, path):
"""Open file with default application"""
try:
if sys.platform.startswith("win"):
os.startfile(path)
elif sys.platform.startswith("darwin"):
subprocess.call(["open", path])
else:
subprocess.call(["xdg-open", path])
self.status_label.config(text=f"Otevírám: {path.name}")
except Exception as e:
messagebox.showerror("Chyba", f"Nelze otevřít {path}: {e}")
2025-09-24 14:30:23 +02:00
# ==================================================
2025-12-28 16:05:34 +01:00
# ACTIONS
2025-09-24 14:30:23 +02:00
# ==================================================
2025-12-28 16:05:34 +01:00
def open_folder_dialog(self):
"""Open folder selection dialog"""
folder = filedialog.askdirectory(title="Vyber složku pro sledování")
if not folder:
2025-09-24 14:30:23 +02:00
return
2025-12-28 16:05:34 +01:00
folder_path = Path(folder)
try:
self.filehandler.append(folder_path)
for f in self.filehandler.filelist:
if f.tags and f.tagmanager:
for t in f.tags:
f.tagmanager.add_tag(t.category, t.name)
2025-09-24 14:30:23 +02:00
2025-12-28 16:05:34 +01:00
self.status_label.config(text=f"Přidána složka: {folder_path}")
self.refresh_sidebar()
self.update_files_from_manager(self.filehandler.filelist)
except Exception as e:
messagebox.showerror("Chyba", f"Nelze přidat složku {folder}: {e}")
2025-09-24 14:30:23 +02:00
2025-12-28 16:05:34 +01:00
def open_selected_files(self):
"""Open selected files"""
files = self.get_selected_files()
for f in files:
self.open_file(f.file_path)
2025-09-28 13:00:39 +02:00
2025-12-28 16:05:34 +01:00
def remove_selected_files(self):
"""Remove selected files from index"""
files = self.get_selected_files()
if not files:
return
2025-09-24 14:30:23 +02:00
2025-12-28 16:05:34 +01:00
ans = messagebox.askyesno("Smazat z indexu", f"Odstranit {len(files)} souborů z indexu?")
if ans:
for f in files:
if f in self.filehandler.filelist:
self.filehandler.filelist.remove(f)
self.update_files_from_manager(self.filehandler.filelist)
self.status_label.config(text=f"Odstraněno {len(files)} souborů z indexu")
2025-09-28 13:00:39 +02:00
def assign_tag_to_selected_bulk(self):
2025-12-28 16:05:34 +01:00
"""Assign tags to selected files (bulk mode)"""
files = self.get_selected_files()
2025-09-28 13:00:39 +02:00
if not files:
2025-12-28 16:05:34 +01:00
self.status_label.config(text="Nebyly vybrány žádné soubory")
2025-09-28 13:00:39 +02:00
return
2025-09-24 14:30:23 +02:00
2025-12-28 16:05:34 +01:00
all_tags = []
2025-09-28 13:00:39 +02:00
for category in self.tagmanager.get_categories():
for tag in self.tagmanager.get_tags_in_category(category):
all_tags.append(tag)
2025-09-24 14:30:23 +02:00
2025-09-28 13:00:39 +02:00
if not all_tags:
messagebox.showwarning("Chyba", "Žádné tagy nejsou definovány")
return
2025-12-28 16:05:34 +01:00
dialog = MultiFileTagAssignDialog(self.root, all_tags, files, self.category_colors)
result = dialog.result
2025-09-28 13:00:39 +02:00
if result is None:
2025-12-28 16:05:34 +01:00
self.status_label.config(text="Přiřazení zrušeno")
2025-09-28 13:00:39 +02:00
return
for full_path, state in result.items():
if state == 1:
if "/" in full_path:
category, name = full_path.split("/", 1)
tag_obj = self.tagmanager.add_tag(category, name)
self.filehandler.assign_tag_to_file_objects(files, tag_obj)
elif state == 0:
if "/" in full_path:
category, name = full_path.split("/", 1)
2025-12-28 16:05:34 +01:00
tag_obj = Tag(category, name)
2025-09-28 13:00:39 +02:00
self.filehandler.remove_tag_from_file_objects(files, tag_obj)
self.update_files_from_manager(self.filehandler.filelist)
2025-12-28 16:05:34 +01:00
self.status_label.config(text="Hromadné přiřazení tagů dokončeno")
2025-09-28 13:00:39 +02:00
def set_date_for_selected(self):
2025-12-28 16:05:34 +01:00
"""Set date for selected files"""
files = self.get_selected_files()
2025-09-28 13:00:39 +02:00
if not files:
2025-12-28 16:05:34 +01:00
self.status_label.config(text="Nebyly vybrány žádné soubory")
2025-09-28 13:00:39 +02:00
return
2025-12-28 16:05:34 +01:00
2025-09-28 13:00:39 +02:00
prompt = "Zadej datum ve formátu YYYY-MM-DD (nebo prázdné pro smazání):"
date_str = simpledialog.askstring("Nastavit datum", prompt, parent=self.root)
if date_str is None:
return
2025-12-28 16:05:34 +01:00
2025-09-28 13:00:39 +02:00
for f in files:
f.set_date(date_str if date_str != "" else None)
2025-12-28 16:05:34 +01:00
2025-09-28 13:00:39 +02:00
self.update_files_from_manager(self.filehandler.filelist)
2025-12-28 16:05:34 +01:00
self.status_label.config(text=f"Nastaveno datum pro {len(files)} soubor(ů)")
2025-09-24 14:30:23 +02:00
2025-12-28 16:05:34 +01:00
def detect_video_resolution(self):
"""Detect video resolution using ffprobe"""
files = self.get_selected_files()
if not files:
self.status_label.config(text="Nebyly vybrány žádné soubory")
return
2025-09-24 14:30:23 +02:00
2025-12-28 16:05:34 +01:00
count = 0
for f in files:
try:
path = str(f.file_path)
result = subprocess.run(
["ffprobe", "-v", "error", "-select_streams", "v:0",
"-show_entries", "stream=height", "-of", "csv=p=0", path],
capture_output=True,
text=True,
check=True
)
height_str = result.stdout.strip()
if not height_str.isdigit():
continue
height = int(height_str)
tag_name = f"{height}p"
tag_obj = self.tagmanager.add_tag("Rozlišení", tag_name)
f.add_tag(tag_obj)
count += 1
except Exception as e:
print(f"Chyba u {f.filename}: {e}")
2025-09-24 14:30:23 +02:00
2025-12-28 16:05:34 +01:00
self.update_files_from_manager(self.filehandler.filelist)
self.status_label.config(text=f"Přiřazeno rozlišení tagů k {count} souborům")
def set_ignore_patterns(self):
"""Set ignore patterns for current folder"""
current = ", ".join(self.filehandler.get_ignore_patterns())
s = simpledialog.askstring("Ignore patterns",
"Zadej patterny oddělené čárkou (např. *.png, *.tmp):",
initialvalue=current)
if s is None:
2025-09-24 14:30:23 +02:00
return
2025-09-28 13:00:39 +02:00
2025-12-28 16:05:34 +01:00
patterns = [p.strip() for p in s.split(",") if p.strip()]
self.filehandler.set_ignore_patterns(patterns)
self.update_files_from_manager(self.filehandler.filelist)
self.status_label.config(text="Ignore patterns aktualizovány")
2025-09-28 13:00:39 +02:00
2025-12-28 16:05:34 +01:00
def toggle_hide_ignored(self):
"""Toggle hiding ignored files"""
self.update_files_from_manager(self.filehandler.filelist)
2025-09-24 14:30:23 +02:00
2025-12-28 16:05:34 +01:00
def toggle_show_path(self):
"""Toggle showing full path"""
self.show_full_path = not self.show_full_path
self.update_files_from_manager(self.filehandler.filelist)
2025-09-28 13:00:39 +02:00
2025-12-28 16:05:34 +01:00
def sort_by_column(self, column: str):
"""Sort by column header click"""
if self.sort_mode == column:
self.sort_order = "desc" if self.sort_order == "asc" else "asc"
else:
self.sort_mode = column
self.sort_order = "asc"
2025-09-24 14:30:23 +02:00
2025-12-28 16:05:34 +01:00
self._update_sort_indicators()
self.update_files_from_manager(self.filehandler.filelist)
def _update_sort_indicators(self):
"""Update column header sort indicators"""
arrow = "" if self.sort_order == "asc" else ""
headers = {
"name": "📄 Název",
"date": "📅 Datum",
"size": "💾 Velikost"
}
for col, base_text in headers.items():
if col == self.sort_mode:
self.file_table.heading(col, text=f"{base_text} {arrow}")
else:
self.file_table.heading(col, text=base_text)
def on_filter_changed(self):
"""Handle search/filter change"""
self.update_files_from_manager(self.filehandler.filelist)
def refresh_all(self):
"""Refresh everything"""
self.refresh_sidebar()
self.update_files_from_manager(self.filehandler.filelist)
self.status_label.config(text="Obnoveno")
def configure_hardlink_folder(self):
"""Configure hardlink output folder for current project"""
if not self.filehandler.current_folder:
messagebox.showwarning("Upozornění", "Nejprve otevřete složku")
2025-09-24 14:30:23 +02:00
return
2025-12-28 16:05:34 +01:00
# Get current settings
folder_config = self.filehandler.get_folder_config()
current_dir = folder_config.get("hardlink_output_dir")
current_categories = folder_config.get("hardlink_categories")
# Ask for output directory
initial_dir = current_dir if current_dir else str(self.filehandler.current_folder)
output_dir = filedialog.askdirectory(
title="Vyber cílovou složku pro hardlink strukturu",
initialdir=initial_dir,
mustexist=False
)
if not output_dir:
2025-09-24 14:30:23 +02:00
return
2025-09-21 19:28:17 +02:00
2025-12-28 16:05:34 +01:00
# Get available categories
categories = self.tagmanager.get_categories()
if not categories:
messagebox.showwarning("Upozornění", "Žádné kategorie tagů")
2025-09-24 14:30:23 +02:00
return
2025-12-28 16:05:34 +01:00
# Show category selection dialog
selected_categories = self._show_category_selection_dialog(
categories,
preselected=current_categories
)
if selected_categories is None:
return # Cancelled
# Save to folder config
folder_config["hardlink_output_dir"] = output_dir
folder_config["hardlink_categories"] = selected_categories if selected_categories else None
self.filehandler.save_folder_config(config=folder_config)
messagebox.showinfo("Hotovo", f"Hardlink složka nastavena:\n{output_dir}")
self.status_label.config(text=f"Hardlink složka nastavena: {output_dir}")
def update_hardlink_structure(self):
"""Quick update hardlink structure using saved settings"""
if not self.filehandler.current_folder:
messagebox.showwarning("Upozornění", "Nejprve otevřete složku")
2025-09-24 14:30:23 +02:00
return
2025-12-28 16:05:34 +01:00
# Get saved settings
folder_config = self.filehandler.get_folder_config()
output_dir = folder_config.get("hardlink_output_dir")
saved_categories = folder_config.get("hardlink_categories")
if not output_dir:
messagebox.showinfo("Info", "Hardlink složka není nastavena.\nPoužijte 'Nastavit hardlink složku...' pro konfiguraci.")
2025-09-24 14:30:23 +02:00
return
2025-12-28 16:05:34 +01:00
output_path = Path(output_dir)
files = self.filehandler.filelist
2025-09-28 13:00:39 +02:00
2025-12-28 16:05:34 +01:00
if not files:
messagebox.showwarning("Upozornění", "Žádné soubory k zpracování")
return
2025-09-21 19:28:17 +02:00
2025-12-28 16:05:34 +01:00
# Create manager and analyze
manager = HardlinkManager(output_path)
2025-09-21 19:28:17 +02:00
2025-12-28 16:05:34 +01:00
# Find what needs to be created and removed
preview_create = manager.get_preview(files, saved_categories)
obsolete = manager.find_obsolete_links(files, saved_categories)
# Filter out already existing links from preview
to_create = []
for source, target in preview_create:
if not target.exists():
to_create.append((source, target))
elif not manager._is_same_file(source, target):
to_create.append((source, target))
if not to_create and not obsolete:
messagebox.showinfo("Info", "Struktura je již synchronizovaná, žádné změny nejsou potřeba")
2025-09-21 19:28:17 +02:00
return
2025-09-24 14:30:23 +02:00
2025-12-28 16:05:34 +01:00
# Build confirmation message
confirm_lines = []
if to_create:
confirm_lines.append(f"Vytvořit: {len(to_create)} hardlinků")
if obsolete:
confirm_lines.append(f"Odebrat: {len(obsolete)} zastaralých hardlinků")
confirm_lines.append(f"\nCílová složka: {output_path}")
confirm_lines.append("\nPokračovat?")
if not messagebox.askyesno("Potvrdit aktualizaci", "\n".join(confirm_lines)):
return
# Perform sync
self.status_label.config(text="Aktualizuji hardlink strukturu...")
self.root.update()
created, create_fail, removed, remove_fail = manager.sync_structure(files, saved_categories)
# Show result
result_lines = []
if created > 0:
result_lines.append(f"Vytvořeno: {created} hardlinků")
if removed > 0:
result_lines.append(f"Odebráno: {removed} zastaralých hardlinků")
if create_fail > 0 or remove_fail > 0:
if create_fail > 0:
result_lines.append(f"Selhalo vytvoření: {create_fail}")
if remove_fail > 0:
result_lines.append(f"Selhalo odebrání: {remove_fail}")
messagebox.showwarning("Dokončeno s chybami", "\n".join(result_lines))
2025-09-24 14:30:23 +02:00
else:
2025-12-28 16:05:34 +01:00
messagebox.showinfo("Hotovo", "\n".join(result_lines) if result_lines else "Žádné změny")
2025-09-24 14:30:23 +02:00
2025-12-28 16:05:34 +01:00
self.status_label.config(text=f"Hardlink struktura aktualizována (vytvořeno: {created}, odebráno: {removed})")
2025-09-21 19:28:17 +02:00
2025-12-28 16:05:34 +01:00
def create_hardlink_structure(self):
"""Create hardlink directory structure based on file tags (manual selection)"""
files = self.filehandler.filelist
if not files:
messagebox.showwarning("Upozornění", "Žádné soubory k zpracování")
2025-09-21 19:28:17 +02:00
return
2025-12-28 16:05:34 +01:00
# Ask for output directory
output_dir = filedialog.askdirectory(
title="Vyber cílovou složku pro hardlink strukturu",
mustexist=False
)
if not output_dir:
2025-09-21 19:28:17 +02:00
return
2025-09-24 14:30:23 +02:00
2025-12-28 16:05:34 +01:00
output_path = Path(output_dir)
2025-09-24 14:30:23 +02:00
2025-12-28 16:05:34 +01:00
# Get available categories
categories = self.tagmanager.get_categories()
if not categories:
messagebox.showwarning("Upozornění", "Žádné kategorie tagů")
return
2025-09-21 19:28:17 +02:00
2025-12-28 16:05:34 +01:00
# Show category selection dialog
selected_categories = self._show_category_selection_dialog(categories)
if selected_categories is None:
return # Cancelled
2025-09-21 19:28:17 +02:00
2025-12-28 16:05:34 +01:00
cat_filter = selected_categories if selected_categories else None
2025-09-21 19:28:17 +02:00
2025-12-28 16:05:34 +01:00
# Create manager and analyze
manager = HardlinkManager(output_path)
2025-09-28 13:00:39 +02:00
2025-12-28 16:05:34 +01:00
# Find what needs to be created and removed
preview_create = manager.get_preview(files, cat_filter)
obsolete = manager.find_obsolete_links(files, cat_filter)
2025-09-28 13:00:39 +02:00
2025-12-28 16:05:34 +01:00
# Filter out already existing links from preview
to_create = []
for source, target in preview_create:
if not target.exists():
to_create.append((source, target))
elif not manager._is_same_file(source, target):
to_create.append((source, target))
if not to_create and not obsolete:
messagebox.showinfo("Info", "Struktura je již synchronizovaná, žádné změny nejsou potřeba")
return
# Build confirmation message
confirm_lines = []
if to_create:
confirm_lines.append(f"Vytvořit: {len(to_create)} hardlinků")
if obsolete:
confirm_lines.append(f"Odebrat: {len(obsolete)} zastaralých hardlinků")
confirm_lines.append(f"\nCílová složka: {output_path}")
confirm_lines.append("\nPokračovat?")
if not messagebox.askyesno("Potvrdit synchronizaci", "\n".join(confirm_lines)):
return
# Perform sync
self.status_label.config(text="Synchronizuji hardlink strukturu...")
self.root.update()
created, create_fail, removed, remove_fail = manager.sync_structure(files, cat_filter)
# Show result
result_lines = []
if created > 0 or create_fail > 0:
result_lines.append(f"Vytvořeno: {created} hardlinků")
if create_fail > 0:
result_lines.append(f"Selhalo vytvoření: {create_fail}")
if removed > 0 or remove_fail > 0:
result_lines.append(f"Odebráno: {removed} zastaralých hardlinků")
if remove_fail > 0:
result_lines.append(f"Selhalo odebrání: {remove_fail}")
if create_fail > 0 or remove_fail > 0:
if manager.errors:
result_lines.append("\nChyby:")
for path, err in manager.errors[:5]:
result_lines.append(f"- {path.name}: {err}")
if len(manager.errors) > 5:
result_lines.append(f"... a dalších {len(manager.errors) - 5} chyb")
messagebox.showwarning("Dokončeno s chybami", "\n".join(result_lines))
else:
messagebox.showinfo("Hotovo", "\n".join(result_lines) if result_lines else "Žádné změny")
self.status_label.config(text=f"Hardlink struktura synchronizována (vytvořeno: {created}, odebráno: {removed})")
def _show_category_selection_dialog(self, categories: List[str], preselected: List[str] | None = None) -> List[str] | None:
"""Show dialog to select which categories to include in hardlink structure
Args:
categories: List of available category names
preselected: Optional list of categories to pre-check (None = all checked)
"""
dialog = tk.Toplevel(self.root)
dialog.title("Vybrat kategorie")
dialog.geometry("350x400")
dialog.transient(self.root)
dialog.grab_set()
result = {"categories": None}
tk.Label(dialog, text="Vyberte kategorie pro vytvoření struktury:",
font=("Arial", 10, "bold")).pack(pady=10)
# Scrollable frame for checkboxes
frame = tk.Frame(dialog)
frame.pack(fill=tk.BOTH, expand=True, padx=10)
canvas = tk.Canvas(frame)
scrollbar = ttk.Scrollbar(frame, orient="vertical", command=canvas.yview)
scrollable_frame = tk.Frame(canvas)
scrollable_frame.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
canvas.configure(yscrollcommand=scrollbar.set)
canvas.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
# Category checkboxes
category_vars = {}
for category in sorted(categories):
# If preselected is None, check all; otherwise check only those in preselected
initial_value = preselected is None or category in preselected
var = tk.BooleanVar(value=initial_value)
category_vars[category] = var
color = self.category_colors.get(category, "#333333")
cb = tk.Checkbutton(scrollable_frame, text=category, variable=var,
fg=color, font=("Arial", 10), anchor="w")
cb.pack(fill="x", pady=2)
# Buttons
btn_frame = tk.Frame(dialog)
btn_frame.pack(pady=10)
def on_ok():
result["categories"] = [cat for cat, var in category_vars.items() if var.get()]
dialog.destroy()
def on_cancel():
result["categories"] = None
dialog.destroy()
def select_all():
for var in category_vars.values():
var.set(True)
def select_none():
for var in category_vars.values():
var.set(False)
tk.Button(btn_frame, text="Všechny", command=select_all, width=8).pack(side=tk.LEFT, padx=2)
tk.Button(btn_frame, text="Žádné", command=select_none, width=8).pack(side=tk.LEFT, padx=2)
tk.Button(btn_frame, text="OK", command=on_ok, width=10).pack(side=tk.LEFT, padx=10)
tk.Button(btn_frame, text="Zrušit", command=on_cancel, width=10).pack(side=tk.LEFT, padx=2)
self.root.wait_window(dialog)
return result["categories"]