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
|
|
|
|
2025-12-30 07:54:30 +01:00
|
|
|
from src.ui.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-12-30 07:54:30 +01:00
|
|
|
# ListManager removed - sorting implemented directly in GUI
|
2025-09-28 13:00:39 +02:00
|
|
|
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-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-12-30 07:54:30 +01:00
|
|
|
self.show_csfd_column = True # CSFD column visibility
|
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-10-03 17:29:54 +02:00
|
|
|
|
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-10-03 17:29:54 +02:00
|
|
|
|
2025-12-28 16:05:34 +01:00
|
|
|
save_global_config(self.filehandler.global_config)
|
|
|
|
|
self.root.destroy()
|
2025-10-03 17:29:54 +02:00
|
|
|
|
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)
|
|
|
|
|
|
2025-10-03 17:29:54 +02:00
|
|
|
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")
|
2025-10-03 17:29:54 +02:00
|
|
|
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-10-03 17:29:54 +02:00
|
|
|
|
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)
|
2025-12-30 07:54:30 +01:00
|
|
|
file_menu.add_command(label="Zavřít složku (Ctrl+W)", command=self.close_folder)
|
2025-10-03 17:29:54 +02:00
|
|
|
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-10-03 17:29:54 +02:00
|
|
|
|
2025-12-28 16:05:34 +01:00
|
|
|
# View menu
|
2025-10-03 17:29:54 +02:00
|
|
|
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-30 07:54:30 +01:00
|
|
|
self.show_csfd_var = tk.BooleanVar(value=True, master=self.root)
|
|
|
|
|
view_menu.add_checkbutton(
|
|
|
|
|
label="Zobrazit CSFD sloupec",
|
|
|
|
|
variable=self.show_csfd_var,
|
|
|
|
|
command=self.toggle_csfd_column
|
|
|
|
|
)
|
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()
|
2025-12-30 07:54:30 +01:00
|
|
|
tools_menu.add_command(label="Nastavit CSFD URL...", command=self.set_csfd_url_for_selected)
|
|
|
|
|
tools_menu.add_command(label="Načíst tagy z CSFD", command=self.apply_csfd_tags_for_selected)
|
|
|
|
|
tools_menu.add_separator()
|
2025-12-28 16:05:34 +01:00
|
|
|
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)
|
2025-10-03 17:29:54 +02:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
2025-12-30 07:54:30 +01:00
|
|
|
# Define columns (including CSFD)
|
|
|
|
|
columns = ("name", "date", "tags", "csfd", "size")
|
2025-12-28 16:05:34 +01:00
|
|
|
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")
|
2025-12-30 07:54:30 +01:00
|
|
|
self.file_table.heading("csfd", text="🎬 CSFD")
|
2025-12-28 16:05:34 +01:00
|
|
|
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)
|
2025-12-30 07:54:30 +01:00
|
|
|
self.file_table.column("csfd", width=50)
|
2025-12-28 16:05:34 +01:00
|
|
|
self.file_table.column("size", width=80)
|
|
|
|
|
|
2025-12-30 07:54:30 +01:00
|
|
|
# Load CSFD column visibility from folder config
|
|
|
|
|
self._update_csfd_column_visibility()
|
|
|
|
|
|
2025-12-28 16:05:34 +01:00
|
|
|
# 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)
|
2025-12-30 07:54:30 +01:00
|
|
|
self.tag_menu.add_command(label="Přejmenovat štítek", command=self.tree_rename_tag)
|
2025-12-28 16:05:34 +01:00
|
|
|
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()
|
2025-12-30 07:54:30 +01:00
|
|
|
self.file_menu.add_command(label="Nastavit CSFD URL...", command=self.set_csfd_url_for_selected)
|
|
|
|
|
self.file_menu.add_command(label="Načíst tagy z CSFD", command=self.apply_csfd_tags_for_selected)
|
|
|
|
|
self.file_menu.add_separator()
|
2025-12-28 16:05:34 +01:00
|
|
|
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())
|
2025-12-30 07:54:30 +01:00
|
|
|
self.root.bind("<Control-w>", lambda e: self.close_folder())
|
2025-12-28 16:05:34 +01:00
|
|
|
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-10-03 17:29:54 +02:00
|
|
|
|
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)
|
|
|
|
|
|
2025-12-30 07:54:30 +01:00
|
|
|
# Force tree update
|
|
|
|
|
self.tag_tree.update_idletasks()
|
|
|
|
|
|
2025-12-28 16:05:34 +01:00
|
|
|
def update_tag_counts(self, filtered_files):
|
|
|
|
|
"""Update tag counts in sidebar based on filtered files"""
|
|
|
|
|
if not hasattr(self, 'tag_tree_items'):
|
2025-10-03 17:29:54 +02:00
|
|
|
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-10-03 17:29:54 +02:00
|
|
|
|
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}")
|
|
|
|
|
|
2025-12-30 07:54:30 +01:00
|
|
|
def tree_rename_tag(self):
|
|
|
|
|
"""Rename selected tag or category"""
|
|
|
|
|
item = self.selected_tree_item_for_context
|
|
|
|
|
if not item:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Don't allow renaming root
|
|
|
|
|
if item == self.root_tag_id:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
parent_id = self.tag_tree.parent(item)
|
|
|
|
|
current_text = self.tag_tree.item(item, "text").strip()
|
|
|
|
|
|
|
|
|
|
# Check if this is a category (parent is root) or a tag
|
|
|
|
|
is_category = (parent_id == self.root_tag_id)
|
|
|
|
|
|
|
|
|
|
if is_category:
|
|
|
|
|
# Renaming a category
|
|
|
|
|
current_name = current_text.replace("📁 ", "")
|
|
|
|
|
new_name = simpledialog.askstring(
|
|
|
|
|
"Přejmenovat kategorii",
|
|
|
|
|
f"Nový název kategorie '{current_name}':",
|
|
|
|
|
initialvalue=current_name
|
|
|
|
|
)
|
|
|
|
|
if not new_name or new_name == current_name:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Check if new name already exists - offer merge
|
|
|
|
|
if new_name in self.tagmanager.get_categories():
|
|
|
|
|
merge = messagebox.askyesno(
|
|
|
|
|
"Kategorie existuje",
|
|
|
|
|
f"Kategorie '{new_name}' již existuje.\n\n"
|
|
|
|
|
f"Chcete sloučit kategorii '{current_name}' do '{new_name}'?\n\n"
|
|
|
|
|
f"Všechny štítky z '{current_name}' budou přesunuty do '{new_name}'.",
|
|
|
|
|
icon="question"
|
|
|
|
|
)
|
|
|
|
|
if not merge:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Merge category in all files
|
|
|
|
|
updated_count = self.filehandler.merge_category_in_files(current_name, new_name)
|
|
|
|
|
|
|
|
|
|
# Refresh sidebar
|
|
|
|
|
self.refresh_sidebar()
|
|
|
|
|
self.root.update_idletasks()
|
|
|
|
|
self.update_files_from_manager(self.filehandler.filelist)
|
|
|
|
|
|
|
|
|
|
self.status_label.config(
|
|
|
|
|
text=f"Kategorie sloučena: {current_name} → {new_name} ({updated_count} souborů)"
|
|
|
|
|
)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Rename category in all files
|
|
|
|
|
updated_count = self.filehandler.rename_category_in_files(current_name, new_name)
|
|
|
|
|
|
|
|
|
|
# Refresh sidebar
|
|
|
|
|
self.refresh_sidebar()
|
|
|
|
|
self.root.update_idletasks()
|
|
|
|
|
self.update_files_from_manager(self.filehandler.filelist)
|
|
|
|
|
|
|
|
|
|
self.status_label.config(
|
|
|
|
|
text=f"Kategorie přejmenována: {current_name} → {new_name} ({updated_count} souborů)"
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
# Renaming a tag
|
|
|
|
|
# Get tag name (without count suffix)
|
|
|
|
|
# Find the tag name from the mapping
|
|
|
|
|
tag_name = None
|
|
|
|
|
for full_path, (item_id, name) in self.tag_tree_items.items():
|
|
|
|
|
if item_id == item:
|
|
|
|
|
tag_name = name
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
if tag_name is None:
|
|
|
|
|
# Fallback: parse from text (remove leading spaces and count)
|
|
|
|
|
tag_name = current_text.lstrip()
|
|
|
|
|
# Remove count suffix like " (5)"
|
|
|
|
|
import re
|
|
|
|
|
tag_name = re.sub(r'\s*\(\d+\)\s*$', '', tag_name)
|
|
|
|
|
|
|
|
|
|
category = self.tag_tree.item(parent_id, "text").replace("📁 ", "")
|
|
|
|
|
|
|
|
|
|
new_name = simpledialog.askstring(
|
|
|
|
|
"Přejmenovat štítek",
|
|
|
|
|
f"Nový název štítku '{tag_name}':",
|
|
|
|
|
initialvalue=tag_name
|
|
|
|
|
)
|
|
|
|
|
if not new_name or new_name == tag_name:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Check if new name already exists in this category - offer merge
|
|
|
|
|
existing_tags = [t.name for t in self.tagmanager.get_tags_in_category(category)]
|
|
|
|
|
if new_name in existing_tags:
|
|
|
|
|
merge = messagebox.askyesno(
|
|
|
|
|
"Štítek existuje",
|
|
|
|
|
f"Štítek '{new_name}' v kategorii '{category}' již existuje.\n\n"
|
|
|
|
|
f"Chcete sloučit '{tag_name}' do '{new_name}'?\n\n"
|
|
|
|
|
f"Všechny soubory s '{tag_name}' budou mít tento štítek nahrazen za '{new_name}'.",
|
|
|
|
|
icon="question"
|
|
|
|
|
)
|
|
|
|
|
if not merge:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Merge tag in all files
|
|
|
|
|
updated_count = self.filehandler.merge_tag_in_files(category, tag_name, new_name)
|
|
|
|
|
|
|
|
|
|
# Refresh sidebar
|
|
|
|
|
self.refresh_sidebar()
|
|
|
|
|
self.root.update_idletasks()
|
|
|
|
|
self.update_files_from_manager(self.filehandler.filelist)
|
|
|
|
|
|
|
|
|
|
self.status_label.config(
|
|
|
|
|
text=f"Štítek sloučen: {category}/{tag_name} → {category}/{new_name} ({updated_count} souborů)"
|
|
|
|
|
)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Rename tag in all files
|
|
|
|
|
updated_count = self.filehandler.rename_tag_in_files(category, tag_name, new_name)
|
|
|
|
|
|
|
|
|
|
# Refresh sidebar
|
|
|
|
|
self.refresh_sidebar()
|
|
|
|
|
self.root.update_idletasks()
|
|
|
|
|
self.update_files_from_manager(self.filehandler.filelist)
|
|
|
|
|
|
|
|
|
|
self.status_label.config(
|
|
|
|
|
text=f"Štítek přejmenován: {category}/{tag_name} → {category}/{new_name} ({updated_count} souborů)"
|
|
|
|
|
)
|
|
|
|
|
|
2025-12-28 16:05:34 +01:00
|
|
|
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
|
2025-10-03 17:29:54 +02:00
|
|
|
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-30 07:54:30 +01:00
|
|
|
# CSFD indicator
|
|
|
|
|
csfd = "✓" if f.csfd_url else ""
|
|
|
|
|
|
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 = "?"
|
|
|
|
|
|
2025-12-30 07:54:30 +01:00
|
|
|
item_id = self.file_table.insert("", "end", values=(name, date, tags, csfd, size_str))
|
2025-12-28 16:05:34 +01:00
|
|
|
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}")
|
2025-12-30 07:54:30 +01:00
|
|
|
self._update_csfd_column_visibility() # Load CSFD column setting for new folder
|
2025-12-28 16:05:34 +01:00
|
|
|
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-30 07:54:30 +01:00
|
|
|
def close_folder(self):
|
|
|
|
|
"""Close current folder safely"""
|
|
|
|
|
if not self.filehandler.current_folder:
|
|
|
|
|
self.status_label.config(text="Žádná složka není otevřena")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
folder_name = self.filehandler.current_folder.name
|
|
|
|
|
|
|
|
|
|
# Close folder (saves metadata and clears state)
|
|
|
|
|
self.filehandler.close_folder()
|
|
|
|
|
|
|
|
|
|
# Refresh UI
|
|
|
|
|
self.refresh_sidebar()
|
|
|
|
|
self.status_label.config(text=f"Složka zavřena: {folder_name}")
|
|
|
|
|
|
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-30 07:54:30 +01:00
|
|
|
def toggle_csfd_column(self):
|
|
|
|
|
"""Toggle CSFD column visibility"""
|
|
|
|
|
self.show_csfd_column = self.show_csfd_var.get()
|
|
|
|
|
self._update_csfd_column_visibility()
|
|
|
|
|
|
|
|
|
|
# Save to folder config
|
|
|
|
|
if self.filehandler.current_folder:
|
|
|
|
|
folder_config = self.filehandler.get_folder_config()
|
|
|
|
|
folder_config["show_csfd_column"] = self.show_csfd_column
|
|
|
|
|
self.filehandler.save_folder_config(config=folder_config)
|
|
|
|
|
|
|
|
|
|
def _update_csfd_column_visibility(self):
|
|
|
|
|
"""Update CSFD column width based on visibility setting"""
|
|
|
|
|
# Load from folder config if available
|
|
|
|
|
if self.filehandler.current_folder:
|
|
|
|
|
folder_config = self.filehandler.get_folder_config()
|
|
|
|
|
self.show_csfd_column = folder_config.get("show_csfd_column", True)
|
|
|
|
|
if hasattr(self, 'show_csfd_var'):
|
|
|
|
|
self.show_csfd_var.set(self.show_csfd_column)
|
|
|
|
|
|
|
|
|
|
# Update column width
|
|
|
|
|
if hasattr(self, 'file_table'):
|
|
|
|
|
if self.show_csfd_column:
|
|
|
|
|
self.file_table.column("csfd", width=50)
|
|
|
|
|
else:
|
|
|
|
|
self.file_table.column("csfd", width=0)
|
|
|
|
|
|
|
|
|
|
def set_csfd_url_for_selected(self):
|
|
|
|
|
"""Set CSFD URL for selected files"""
|
|
|
|
|
files = self.get_selected_files()
|
|
|
|
|
if not files:
|
|
|
|
|
self.status_label.config(text="Nebyly vybrány žádné soubory")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Get current URL from first file
|
|
|
|
|
current_url = files[0].csfd_url or ""
|
|
|
|
|
|
|
|
|
|
prompt = "Zadej CSFD URL (např. https://www.csfd.cz/film/9423-pane-vy-jste-vdova/):"
|
|
|
|
|
url = simpledialog.askstring("Nastavit CSFD URL", prompt, initialvalue=current_url, parent=self.root)
|
|
|
|
|
if url is None:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
for f in files:
|
|
|
|
|
f.set_csfd_url(url if url != "" else None)
|
|
|
|
|
|
|
|
|
|
self.update_files_from_manager(self.filehandler.filelist)
|
|
|
|
|
self.status_label.config(text=f"CSFD URL nastaveno pro {len(files)} soubor(ů)")
|
|
|
|
|
|
|
|
|
|
def apply_csfd_tags_for_selected(self):
|
|
|
|
|
"""Load tags from CSFD for selected files"""
|
|
|
|
|
files = self.get_selected_files()
|
|
|
|
|
if not files:
|
|
|
|
|
self.status_label.config(text="Nebyly vybrány žádné soubory")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Filter files with CSFD URL
|
|
|
|
|
files_with_url = [f for f in files if f.csfd_url]
|
|
|
|
|
if not files_with_url:
|
|
|
|
|
messagebox.showwarning("Upozornění", "Žádný z vybraných souborů nemá nastavenou CSFD URL")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
self.status_label.config(text=f"Načítám tagy z CSFD pro {len(files_with_url)} souborů...")
|
|
|
|
|
self.root.update()
|
|
|
|
|
|
|
|
|
|
success_count = 0
|
|
|
|
|
error_count = 0
|
|
|
|
|
all_tags_added = []
|
|
|
|
|
|
|
|
|
|
for f in files_with_url:
|
|
|
|
|
result = f.apply_csfd_tags()
|
|
|
|
|
if result["success"]:
|
|
|
|
|
success_count += 1
|
|
|
|
|
all_tags_added.extend(result["tags_added"])
|
|
|
|
|
else:
|
|
|
|
|
error_count += 1
|
|
|
|
|
|
|
|
|
|
# Refresh sidebar to show new categories
|
|
|
|
|
self.refresh_sidebar()
|
|
|
|
|
self.root.update_idletasks() # Force UI refresh
|
|
|
|
|
self.update_files_from_manager(self.filehandler.filelist)
|
|
|
|
|
|
|
|
|
|
# Show result
|
|
|
|
|
if error_count > 0:
|
|
|
|
|
messagebox.showwarning("Dokončeno s chybami",
|
|
|
|
|
f"Úspěšně: {success_count}, Chyby: {error_count}\n"
|
|
|
|
|
f"Přidáno {len(all_tags_added)} tagů")
|
|
|
|
|
else:
|
|
|
|
|
self.status_label.config(
|
|
|
|
|
text=f"Načteno z CSFD: {success_count} souborů, přidáno {len(all_tags_added)} tagů")
|
|
|
|
|
|
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"]
|