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-09-24 14:30:23 +02:00
|
|
|
from src.core.image import load_icon
|
2025-09-24 06:58:13 +02:00
|
|
|
from src.core.file_manager import FileManager
|
2025-09-24 14:30:23 +02:00
|
|
|
from src.core.tag_manager import TagManager
|
|
|
|
|
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-09-08 13:22:10 +02:00
|
|
|
|
2025-09-24 14:30:23 +02:00
|
|
|
class TagSelectionDialog(tk.Toplevel):
|
2025-09-28 13:00:39 +02:00
|
|
|
"""
|
|
|
|
|
Jednoduchý dialog pro výběr tagů (původní, používán jinde).
|
|
|
|
|
(tento třída zůstává pro jednobodové použití)
|
|
|
|
|
"""
|
2025-09-24 14:30:23 +02:00
|
|
|
def __init__(self, parent, tags: list[str]):
|
|
|
|
|
super().__init__(parent)
|
|
|
|
|
self.title("Vyber tagy")
|
|
|
|
|
self.selected_tags = []
|
|
|
|
|
self.vars = {}
|
|
|
|
|
|
|
|
|
|
tk.Label(self, text="Vyber tagy k přiřazení:").pack(pady=5)
|
|
|
|
|
|
|
|
|
|
frame = tk.Frame(self)
|
|
|
|
|
frame.pack(padx=10, pady=5)
|
|
|
|
|
|
|
|
|
|
for tag in tags:
|
|
|
|
|
var = tk.BooleanVar(value=False)
|
|
|
|
|
chk = tk.Checkbutton(frame, text=tag, variable=var)
|
|
|
|
|
chk.pack(anchor="w")
|
|
|
|
|
self.vars[tag] = var
|
|
|
|
|
|
|
|
|
|
btn_frame = tk.Frame(self)
|
|
|
|
|
btn_frame.pack(pady=5)
|
|
|
|
|
tk.Button(btn_frame, text="OK", command=self.on_ok).pack(side="left", padx=5)
|
|
|
|
|
tk.Button(btn_frame, text="Cancel", command=self.destroy).pack(side="left", padx=5)
|
|
|
|
|
|
|
|
|
|
self.transient(parent)
|
|
|
|
|
self.grab_set()
|
|
|
|
|
parent.wait_window(self)
|
|
|
|
|
|
|
|
|
|
def on_ok(self):
|
|
|
|
|
self.selected_tags = [tag for tag, var in self.vars.items() if var.get()]
|
|
|
|
|
self.destroy()
|
2025-09-17 06:46:05 +02:00
|
|
|
|
2025-09-28 13:00:39 +02:00
|
|
|
|
|
|
|
|
class MultiFileTagAssignDialog(tk.Toplevel):
|
|
|
|
|
def __init__(self, parent, all_tags: List[Tag], files: List[File]):
|
|
|
|
|
super().__init__(parent)
|
|
|
|
|
self.title("Přiřadit tagy k vybraným souborům")
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
tk.Label(self, text=f"Vybráno souborů: {len(files)}").pack(pady=5)
|
|
|
|
|
|
|
|
|
|
frame = tk.Frame(self)
|
|
|
|
|
frame.pack(padx=10, pady=5, fill="both", expand=True)
|
|
|
|
|
|
|
|
|
|
file_tag_sets = [{t.full_path for t in f.tags} for f in files]
|
|
|
|
|
|
|
|
|
|
for full_path, tag in sorted(self.tags_by_full.items()):
|
|
|
|
|
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=full_path, anchor="w")
|
|
|
|
|
cb.state_value = init
|
|
|
|
|
cb.full_path = full_path
|
|
|
|
|
cb.pack(fill="x", anchor="w")
|
|
|
|
|
cb.bind("<Button-1>", self._on_toggle)
|
|
|
|
|
|
|
|
|
|
self._update_checkbox_look(cb)
|
|
|
|
|
self.checkbuttons[full_path] = cb
|
|
|
|
|
self.vars[full_path] = init
|
|
|
|
|
|
|
|
|
|
btn_frame = tk.Frame(self)
|
|
|
|
|
btn_frame.pack(pady=5)
|
|
|
|
|
tk.Button(btn_frame, text="OK", command=self.on_ok).pack(side="left", padx=5)
|
|
|
|
|
tk.Button(btn_frame, text="Cancel", command=self.destroy).pack(side="left", padx=5)
|
|
|
|
|
|
|
|
|
|
self.transient(parent)
|
|
|
|
|
self.grab_set()
|
|
|
|
|
parent.wait_window(self)
|
|
|
|
|
|
|
|
|
|
def _on_toggle(self, event):
|
|
|
|
|
cb: tk.Checkbutton = event.widget
|
|
|
|
|
cur = cb.state_value
|
|
|
|
|
if cur == 0: # OFF → ON
|
|
|
|
|
cb.state_value = 1
|
|
|
|
|
elif cur == 1: # ON → OFF
|
|
|
|
|
cb.state_value = 0
|
|
|
|
|
elif cur == 2: # MIXED → ON
|
|
|
|
|
cb.state_value = 1
|
|
|
|
|
self._update_checkbox_look(cb)
|
|
|
|
|
return "break"
|
|
|
|
|
|
|
|
|
|
def _update_checkbox_look(self, cb: tk.Checkbutton):
|
|
|
|
|
"""Aktualizuje vizuál podle stavu."""
|
|
|
|
|
v = cb.state_value
|
|
|
|
|
if v == 0:
|
|
|
|
|
cb.deselect()
|
|
|
|
|
cb.config(fg="black")
|
|
|
|
|
elif v == 1:
|
|
|
|
|
cb.select()
|
|
|
|
|
cb.config(fg="blue")
|
|
|
|
|
elif v == 2:
|
|
|
|
|
cb.deselect() # mixed = nezaškrtnuté, ale červený text
|
|
|
|
|
cb.config(fg="red")
|
|
|
|
|
|
|
|
|
|
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-28 13:00:39 +02:00
|
|
|
self.states = {} # tree states (checkboxy) item_id -> bool
|
|
|
|
|
self.listbox_map: dict[int, list[File]] = {}
|
2025-09-21 19:28:17 +02:00
|
|
|
self.selected_tree_item_for_context = None
|
|
|
|
|
self.selected_list_index_for_context = None
|
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()
|
|
|
|
|
|
|
|
|
|
# nové proměnné
|
|
|
|
|
self.filter_text = ""
|
|
|
|
|
self.show_full_path = False
|
|
|
|
|
self.sort_mode = "name"
|
|
|
|
|
self.sort_order = "asc"
|
2025-09-24 14:30:23 +02:00
|
|
|
|
|
|
|
|
# callback z FileManageru
|
|
|
|
|
self.filehandler.on_files_changed = self.update_files_from_manager
|
2025-09-21 19:28:17 +02:00
|
|
|
|
|
|
|
|
# ==================================================
|
|
|
|
|
# MAIN GUI
|
|
|
|
|
# ==================================================
|
2025-09-24 14:30:23 +02:00
|
|
|
def main(self):
|
2025-09-17 06:46:05 +02:00
|
|
|
root = tk.Tk()
|
2025-09-28 13:00:39 +02:00
|
|
|
root.title(APP_NAME + " " + VERSION)
|
|
|
|
|
root.geometry(APP_VIEWPORT)
|
2025-09-21 19:28:17 +02:00
|
|
|
self.root = root
|
|
|
|
|
|
2025-09-24 14:30:23 +02:00
|
|
|
# ---- Ikony
|
2025-09-21 19:28:17 +02:00
|
|
|
unchecked = load_icon("src/resources/images/32/32_unchecked.png")
|
|
|
|
|
checked = load_icon("src/resources/images/32/32_checked.png")
|
2025-09-28 13:00:39 +02:00
|
|
|
tag_icon = load_icon("src/resources/images/32/32_tag.png")
|
|
|
|
|
self.icons = {"unchecked": unchecked, "checked": checked, "tag": tag_icon}
|
2025-09-21 19:28:17 +02:00
|
|
|
root.unchecked_img = unchecked
|
|
|
|
|
root.checked_img = checked
|
2025-09-28 13:00:39 +02:00
|
|
|
root.tag_img = tag_icon
|
2025-09-17 06:46:05 +02:00
|
|
|
|
2025-09-21 19:28:17 +02:00
|
|
|
# ---- Layout
|
2025-09-17 06:46:05 +02:00
|
|
|
menu_bar = tk.Menu(root)
|
|
|
|
|
root.config(menu=menu_bar)
|
|
|
|
|
file_menu = tk.Menu(menu_bar, tearoff=0)
|
2025-09-24 14:30:23 +02:00
|
|
|
file_menu.add_command(label="Open Folder...", command=self.open_folder_dialog)
|
2025-09-28 13:00:39 +02:00
|
|
|
file_menu.add_command(label="Set date for selected...", command=self.set_date_for_selected)
|
2025-09-24 14:30:23 +02:00
|
|
|
file_menu.add_separator()
|
2025-09-21 19:28:17 +02:00
|
|
|
file_menu.add_command(label="Exit", command=root.quit)
|
|
|
|
|
menu_bar.add_cascade(label="File", menu=file_menu)
|
2025-09-17 06:46:05 +02:00
|
|
|
|
|
|
|
|
main_frame = tk.Frame(root)
|
|
|
|
|
main_frame.pack(fill="both", expand=True)
|
|
|
|
|
main_frame.columnconfigure(0, weight=1)
|
|
|
|
|
main_frame.columnconfigure(1, weight=2)
|
|
|
|
|
main_frame.rowconfigure(0, weight=1)
|
|
|
|
|
|
2025-09-21 19:28:17 +02:00
|
|
|
# ---- Tree (left)
|
|
|
|
|
self.tree = ttk.Treeview(main_frame)
|
|
|
|
|
self.tree.grid(row=0, column=0, sticky="nsew", padx=4, pady=4)
|
|
|
|
|
self.tree.bind("<Button-1>", self.on_tree_left_click)
|
|
|
|
|
self.tree.bind("<Button-3>", self.on_tree_right_click)
|
|
|
|
|
|
2025-09-28 13:00:39 +02:00
|
|
|
# ---- Right side (filter + listbox)
|
|
|
|
|
right_frame = tk.Frame(main_frame)
|
|
|
|
|
right_frame.grid(row=0, column=1, sticky="nsew", padx=4, pady=4)
|
|
|
|
|
right_frame.rowconfigure(1, weight=1)
|
|
|
|
|
right_frame.columnconfigure(0, weight=1)
|
|
|
|
|
|
|
|
|
|
# Filter + buttons row
|
|
|
|
|
filter_frame = tk.Frame(right_frame)
|
|
|
|
|
filter_frame.grid(row=0, column=0, columnspan=2, sticky="ew", pady=(0,4))
|
|
|
|
|
filter_frame.columnconfigure(0, weight=1)
|
|
|
|
|
|
|
|
|
|
self.filter_entry = tk.Entry(filter_frame)
|
|
|
|
|
self.filter_entry.grid(row=0, column=0, sticky="ew")
|
|
|
|
|
self.filter_entry.bind("<KeyRelease>", lambda e: self.on_filter_changed())
|
|
|
|
|
|
|
|
|
|
self.btn_toggle_path = tk.Button(filter_frame, text="Name", width=6, command=self.toggle_show_path)
|
|
|
|
|
self.btn_toggle_path.grid(row=0, column=1, padx=2)
|
|
|
|
|
|
|
|
|
|
self.btn_toggle_sortmode = tk.Button(filter_frame, text="Sort:Name", width=8, command=self.toggle_sort_mode)
|
|
|
|
|
self.btn_toggle_sortmode.grid(row=0, column=2, padx=2)
|
|
|
|
|
|
|
|
|
|
self.btn_toggle_order = tk.Button(filter_frame, text="ASC", width=5, command=self.toggle_sort_order)
|
|
|
|
|
self.btn_toggle_order.grid(row=0, column=3, padx=2)
|
|
|
|
|
|
|
|
|
|
# Listbox + scrollbar
|
|
|
|
|
self.listbox = tk.Listbox(right_frame, selectmode="extended")
|
|
|
|
|
self.listbox.grid(row=1, column=0, sticky="nsew")
|
2025-09-21 19:28:17 +02:00
|
|
|
self.listbox.bind("<Double-1>", self.on_list_double)
|
|
|
|
|
self.listbox.bind("<Button-3>", self.on_list_right_click)
|
|
|
|
|
|
2025-09-28 13:00:39 +02:00
|
|
|
lb_scroll = tk.Scrollbar(right_frame, orient="vertical", command=self.listbox.yview)
|
|
|
|
|
lb_scroll.grid(row=1, column=1, sticky="ns")
|
|
|
|
|
self.listbox.config(yscrollcommand=lb_scroll.set)
|
|
|
|
|
|
2025-09-21 19:28:17 +02:00
|
|
|
# ---- Status bar
|
|
|
|
|
self.status_bar = tk.Label(root, text="Připraven", anchor="w", relief="sunken")
|
|
|
|
|
self.status_bar.pack(side="bottom", fill="x")
|
|
|
|
|
|
|
|
|
|
# ---- Context menus
|
|
|
|
|
self.tree_menu = tk.Menu(root, tearoff=0)
|
|
|
|
|
self.tree_menu.add_command(label="Nový tag zde", command=self.tree_add_tag)
|
|
|
|
|
self.tree_menu.add_command(label="Smazat tag", command=self.tree_delete_tag)
|
|
|
|
|
|
|
|
|
|
self.list_menu = tk.Menu(root, tearoff=0)
|
|
|
|
|
self.list_menu.add_command(label="Otevřít soubor", command=self.list_open_file)
|
|
|
|
|
self.list_menu.add_command(label="Smazat z indexu", command=self.list_remove_file)
|
2025-09-24 14:30:23 +02:00
|
|
|
self.list_menu.add_command(label="Assign Tag", command=self.assign_tag_to_selected)
|
2025-09-28 13:00:39 +02:00
|
|
|
self.list_menu.add_command(label="Assign Tag (advanced)...", command=self.assign_tag_to_selected_bulk)
|
2025-09-21 19:28:17 +02:00
|
|
|
|
2025-09-24 14:30:23 +02:00
|
|
|
# ---- Root node
|
2025-09-28 13:00:39 +02:00
|
|
|
root_id = self.tree.insert("", "end", text="Štítky", image=self.icons["tag"])
|
2025-09-21 19:28:17 +02:00
|
|
|
self.tree.item(root_id, open=True)
|
2025-09-24 14:30:23 +02:00
|
|
|
self.root_id = root_id
|
|
|
|
|
|
2025-09-28 13:00:39 +02:00
|
|
|
# ⚡ refresh při startu
|
|
|
|
|
self.refresh_tree_tags()
|
2025-09-24 14:30:23 +02:00
|
|
|
self.update_files_from_manager(self.filehandler.filelist)
|
2025-09-21 19:28:17 +02:00
|
|
|
|
|
|
|
|
root.mainloop()
|
|
|
|
|
|
2025-09-28 13:00:39 +02:00
|
|
|
# ==================================================
|
|
|
|
|
# FILTER + SORT TOGGLES
|
|
|
|
|
# ==================================================
|
|
|
|
|
def on_filter_changed(self):
|
|
|
|
|
self.filter_text = self.filter_entry.get().strip().lower()
|
|
|
|
|
self.update_files_from_manager(self.filehandler.filelist)
|
|
|
|
|
|
|
|
|
|
def toggle_show_path(self):
|
|
|
|
|
self.show_full_path = not self.show_full_path
|
|
|
|
|
self.btn_toggle_path.config(text="Path" if self.show_full_path else "Name")
|
|
|
|
|
self.update_files_from_manager(self.filehandler.filelist)
|
|
|
|
|
|
|
|
|
|
def toggle_sort_mode(self):
|
|
|
|
|
self.sort_mode = "date" if self.sort_mode == "name" else "name"
|
|
|
|
|
self.btn_toggle_sortmode.config(text=f"Sort:{self.sort_mode.capitalize()}")
|
|
|
|
|
self.update_files_from_manager(self.filehandler.filelist)
|
|
|
|
|
|
|
|
|
|
def toggle_sort_order(self):
|
|
|
|
|
self.sort_order = "desc" if self.sort_order == "asc" else "asc"
|
|
|
|
|
self.btn_toggle_order.config(text=self.sort_order.upper())
|
|
|
|
|
self.update_files_from_manager(self.filehandler.filelist)
|
|
|
|
|
|
2025-09-21 19:28:17 +02:00
|
|
|
# ==================================================
|
2025-09-24 14:30:23 +02:00
|
|
|
# FILE REFRESH + MAP
|
2025-09-21 19:28:17 +02:00
|
|
|
# ==================================================
|
2025-09-28 13:00:39 +02:00
|
|
|
def update_files_from_manager(self, filelist=None):
|
|
|
|
|
if filelist is None:
|
|
|
|
|
filelist = self.filehandler.filelist
|
|
|
|
|
|
|
|
|
|
# filtr tagy
|
|
|
|
|
checked_tags = self.get_checked_tags()
|
|
|
|
|
filtered_files = self.filehandler.filter_files_by_tags(checked_tags)
|
|
|
|
|
|
|
|
|
|
# filtr text
|
|
|
|
|
if self.filter_text:
|
|
|
|
|
filtered_files = [
|
|
|
|
|
f for f in filtered_files
|
|
|
|
|
if self.filter_text in f.filename.lower() or
|
|
|
|
|
(self.show_full_path and self.filter_text in str(f.file_path).lower())
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
# řazení
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
# naplníme listbox
|
2025-09-24 14:30:23 +02:00
|
|
|
self.listbox.delete(0, "end")
|
|
|
|
|
self.listbox_map = {}
|
2025-09-21 19:28:17 +02:00
|
|
|
|
2025-09-28 13:00:39 +02:00
|
|
|
for i, f in enumerate(filtered_files):
|
|
|
|
|
if self.show_full_path:
|
|
|
|
|
display = str(f.file_path)
|
|
|
|
|
else:
|
|
|
|
|
display = f.filename
|
|
|
|
|
if f.date:
|
|
|
|
|
display = f"{display} — {f.date}"
|
|
|
|
|
self.listbox.insert("end", display)
|
|
|
|
|
self.listbox_map[i] = [f]
|
|
|
|
|
|
2025-09-24 14:30:23 +02:00
|
|
|
self.status_bar.config(text=f"Zobrazeno {self.listbox.size()} položek")
|
|
|
|
|
|
2025-09-28 13:00:39 +02:00
|
|
|
# ==================================================
|
|
|
|
|
# GET SELECTED FILES
|
|
|
|
|
# ==================================================
|
2025-09-24 14:30:23 +02:00
|
|
|
def get_selected_files_objects(self):
|
|
|
|
|
indices = self.listbox.curselection()
|
|
|
|
|
files = []
|
|
|
|
|
for idx in indices:
|
2025-09-28 13:00:39 +02:00
|
|
|
files.extend(self.listbox_map.get(idx, []))
|
2025-09-24 14:30:23 +02:00
|
|
|
return files
|
|
|
|
|
|
|
|
|
|
# ==================================================
|
2025-09-28 13:00:39 +02:00
|
|
|
# ASSIGN TAG (jednoduchý)
|
2025-09-24 14:30:23 +02:00
|
|
|
# ==================================================
|
|
|
|
|
def assign_tag_to_selected(self):
|
|
|
|
|
files = self.get_selected_files_objects()
|
|
|
|
|
if not files:
|
|
|
|
|
self.status_bar.config(text="Nebyly vybrány žádné soubory")
|
|
|
|
|
return
|
|
|
|
|
|
2025-09-28 13:00:39 +02:00
|
|
|
all_tags: List[Tag] = []
|
2025-09-24 14:30:23 +02:00
|
|
|
for category in self.tagmanager.get_categories():
|
|
|
|
|
for tag in self.tagmanager.get_tags_in_category(category):
|
2025-09-28 13:00:39 +02:00
|
|
|
all_tags.append(tag)
|
2025-09-24 14:30:23 +02:00
|
|
|
|
|
|
|
|
if not all_tags:
|
|
|
|
|
messagebox.showwarning("Chyba", "Žádné tagy nejsou definovány")
|
|
|
|
|
return
|
|
|
|
|
|
2025-09-28 13:00:39 +02:00
|
|
|
tag_strings = [tag.full_path for tag in all_tags]
|
|
|
|
|
dialog = TagSelectionDialog(self.root, tag_strings)
|
|
|
|
|
selected_tag_strings = dialog.selected_tags
|
2025-09-24 14:30:23 +02:00
|
|
|
|
2025-09-28 13:00:39 +02:00
|
|
|
if not selected_tag_strings:
|
2025-09-24 14:30:23 +02:00
|
|
|
self.status_bar.config(text="Nebyl vybrán žádný tag")
|
|
|
|
|
return
|
|
|
|
|
|
2025-09-28 13:00:39 +02:00
|
|
|
selected_tags: list[Tag] = []
|
|
|
|
|
for full_tag in selected_tag_strings:
|
2025-09-24 14:30:23 +02:00
|
|
|
if "/" in full_tag:
|
|
|
|
|
category, name = full_tag.split("/", 1)
|
2025-09-28 13:00:39 +02:00
|
|
|
selected_tags.append(self.tagmanager.add_tag(category, name))
|
|
|
|
|
|
|
|
|
|
for tag in selected_tags:
|
|
|
|
|
self.filehandler.assign_tag_to_file_objects(files, tag)
|
2025-09-24 14:30:23 +02:00
|
|
|
|
|
|
|
|
self.update_files_from_manager(self.filehandler.filelist)
|
2025-09-28 13:00:39 +02:00
|
|
|
self.status_bar.config(text=f"Přiřazeny tagy: {', '.join(selected_tag_strings)}")
|
|
|
|
|
|
|
|
|
|
# ==================================================
|
|
|
|
|
# ASSIGN TAG (pokročilé pro více souborů - tri-state)
|
|
|
|
|
# ==================================================
|
|
|
|
|
def assign_tag_to_selected_bulk(self):
|
|
|
|
|
files = self.get_selected_files_objects()
|
|
|
|
|
if not files:
|
|
|
|
|
self.status_bar.config(text="Nebyly vybrány žádné soubory")
|
|
|
|
|
return
|
2025-09-24 14:30:23 +02:00
|
|
|
|
2025-09-28 13:00:39 +02:00
|
|
|
all_tags: List[Tag] = []
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
dialog = MultiFileTagAssignDialog(self.root, all_tags, files)
|
|
|
|
|
result = getattr(dialog, "result", None)
|
|
|
|
|
if result is None:
|
|
|
|
|
self.status_bar.config(text="Přiřazení zrušeno")
|
|
|
|
|
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)
|
|
|
|
|
from src.core.tag import Tag as TagClass
|
|
|
|
|
tag_obj = TagClass(category, name)
|
|
|
|
|
self.filehandler.remove_tag_from_file_objects(files, tag_obj)
|
|
|
|
|
else:
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
self.update_files_from_manager(self.filehandler.filelist)
|
|
|
|
|
self.status_bar.config(text="Hromadné přiřazení tagů dokončeno")
|
|
|
|
|
|
|
|
|
|
# ==================================================
|
|
|
|
|
# SET DATE FOR SELECTED FILES
|
|
|
|
|
# ==================================================
|
|
|
|
|
def set_date_for_selected(self):
|
|
|
|
|
files = self.get_selected_files_objects()
|
|
|
|
|
if not files:
|
|
|
|
|
self.status_bar.config(text="Nebyly vybrány žádné soubory")
|
|
|
|
|
return
|
|
|
|
|
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
|
|
|
|
|
for f in files:
|
|
|
|
|
f.set_date(date_str if date_str != "" else None)
|
|
|
|
|
self.update_files_from_manager(self.filehandler.filelist)
|
|
|
|
|
self.status_bar.config(text=f"Nastaveno datum pro {len(files)} soubor(ů)")
|
2025-09-24 14:30:23 +02:00
|
|
|
|
|
|
|
|
# ==================================================
|
|
|
|
|
# DOUBLE CLICK OPEN
|
|
|
|
|
# ==================================================
|
|
|
|
|
def on_list_double(self, event):
|
|
|
|
|
for f in self.get_selected_files_objects():
|
|
|
|
|
self.open_file(f.file_path)
|
|
|
|
|
|
|
|
|
|
# ==================================================
|
|
|
|
|
# OPEN FILE
|
|
|
|
|
# ==================================================
|
|
|
|
|
def open_file(self, path):
|
|
|
|
|
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_bar.config(text=f"Otevírám: {path}")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
messagebox.showerror("Chyba", f"Nelze otevřít {path}: {e}")
|
|
|
|
|
|
|
|
|
|
# ==================================================
|
|
|
|
|
# LIST CONTEXT MENU
|
|
|
|
|
# ==================================================
|
|
|
|
|
def on_list_right_click(self, event):
|
|
|
|
|
idx = self.listbox.nearest(event.y)
|
|
|
|
|
if idx is None:
|
|
|
|
|
return
|
2025-09-28 13:00:39 +02:00
|
|
|
|
|
|
|
|
# pokud položka není součástí aktuálního výběru, přidáme ji
|
|
|
|
|
if idx not in self.listbox.curselection():
|
|
|
|
|
self.listbox.selection_set(idx)
|
|
|
|
|
|
2025-09-24 14:30:23 +02:00
|
|
|
self.selected_list_index_for_context = idx
|
|
|
|
|
self.list_menu.tk_popup(event.x_root, event.y_root)
|
|
|
|
|
|
2025-09-28 13:00:39 +02:00
|
|
|
|
2025-09-24 14:30:23 +02:00
|
|
|
def list_open_file(self):
|
|
|
|
|
for f in self.get_selected_files_objects():
|
|
|
|
|
self.open_file(f.file_path)
|
|
|
|
|
|
|
|
|
|
def list_remove_file(self):
|
|
|
|
|
files = self.get_selected_files_objects()
|
|
|
|
|
if not files:
|
|
|
|
|
return
|
|
|
|
|
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_bar.config(text=f"Odstraněno {len(files)} souborů z indexu")
|
|
|
|
|
|
|
|
|
|
# ==================================================
|
|
|
|
|
# OPEN FOLDER
|
|
|
|
|
# ==================================================
|
|
|
|
|
def open_folder_dialog(self):
|
|
|
|
|
folder = filedialog.askdirectory(title="Vyber složku pro sledování")
|
|
|
|
|
if not folder:
|
|
|
|
|
return
|
|
|
|
|
folder_path = Path(folder)
|
|
|
|
|
try:
|
|
|
|
|
self.filehandler.append(folder_path)
|
|
|
|
|
for f in self.filehandler.filelist:
|
|
|
|
|
if f.tags and f.tagmanager:
|
2025-09-28 13:00:39 +02:00
|
|
|
for t in f.tags:
|
2025-09-24 14:30:23 +02:00
|
|
|
f.tagmanager.add_tag(t.category, t.name)
|
|
|
|
|
self.status_bar.config(text=f"Přidána složka: {folder_path}")
|
|
|
|
|
self.refresh_tree_tags()
|
2025-09-28 13:00:39 +02:00
|
|
|
self.update_files_from_manager(self.filehandler.filelist)
|
2025-09-24 14:30:23 +02:00
|
|
|
except Exception as e:
|
|
|
|
|
messagebox.showerror("Chyba", f"Nelze přidat složku {folder}: {e}")
|
2025-09-21 19:28:17 +02:00
|
|
|
|
|
|
|
|
# ==================================================
|
|
|
|
|
# TREE EVENTS
|
|
|
|
|
# ==================================================
|
|
|
|
|
def on_tree_left_click(self, event):
|
|
|
|
|
region = self.tree.identify("region", event.x, event.y)
|
2025-09-24 14:30:23 +02:00
|
|
|
if region not in ("tree", "icon"):
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
item_id = self.tree.identify_row(event.y)
|
|
|
|
|
if not item_id:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
parent_id = self.tree.parent(item_id)
|
2025-09-28 13:00:39 +02:00
|
|
|
if parent_id == "" or parent_id == self.root_id:
|
2025-09-24 14:30:23 +02:00
|
|
|
is_open = self.tree.item(item_id, "open")
|
|
|
|
|
self.tree.item(item_id, open=not is_open)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
self.states[item_id] = not self.states.get(item_id, False)
|
2025-09-28 13:00:39 +02:00
|
|
|
self.tree.item(
|
|
|
|
|
item_id,
|
|
|
|
|
image=self.icons["checked"] if self.states[item_id] else self.icons["unchecked"]
|
|
|
|
|
)
|
|
|
|
|
self.status_bar.config(
|
|
|
|
|
text=f"Tag {'checked' if self.states[item_id] else 'unchecked'}: {self.build_full_tag(item_id)}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
checked_tags = self.get_checked_tags()
|
|
|
|
|
filtered_files = self.filehandler.filter_files_by_tags(checked_tags)
|
|
|
|
|
self.update_files_from_manager(filtered_files)
|
2025-09-21 19:28:17 +02:00
|
|
|
|
|
|
|
|
def on_tree_right_click(self, event):
|
|
|
|
|
item_id = self.tree.identify_row(event.y)
|
|
|
|
|
if item_id:
|
|
|
|
|
self.selected_tree_item_for_context = item_id
|
|
|
|
|
self.tree.selection_set(item_id)
|
|
|
|
|
self.tree_menu.tk_popup(event.x_root, event.y_root)
|
|
|
|
|
else:
|
|
|
|
|
menu = tk.Menu(self.root, tearoff=0)
|
|
|
|
|
menu.add_command(label="Nový top-level tag", command=lambda: self.tree_add_tag(background=True))
|
|
|
|
|
menu.tk_popup(event.x_root, event.y_root)
|
|
|
|
|
|
2025-09-24 14:30:23 +02:00
|
|
|
# ==================================================
|
|
|
|
|
# TREE TAG CRUD
|
|
|
|
|
# ==================================================
|
2025-09-21 19:28:17 +02:00
|
|
|
def tree_add_tag(self, background=False):
|
|
|
|
|
name = simpledialog.askstring("Nový tag", "Název tagu:")
|
|
|
|
|
if not name:
|
|
|
|
|
return
|
2025-09-24 14:30:23 +02:00
|
|
|
parent = self.selected_tree_item_for_context if not background else self.root_id
|
2025-09-28 13:00:39 +02:00
|
|
|
new_id = self.tree.insert(parent, "end", text=name, image=self.icons["unchecked"])
|
2025-09-21 19:28:17 +02:00
|
|
|
self.states[new_id] = False
|
2025-09-24 14:30:23 +02:00
|
|
|
|
|
|
|
|
if parent == self.root_id:
|
|
|
|
|
category = name
|
|
|
|
|
self.tagmanager.add_category(category)
|
2025-09-28 13:00:39 +02:00
|
|
|
self.tree.item(new_id, image=self.icons["tag"])
|
2025-09-24 14:30:23 +02:00
|
|
|
else:
|
|
|
|
|
category = self.tree.item(parent, "text")
|
|
|
|
|
self.tagmanager.add_tag(category, name)
|
|
|
|
|
|
2025-09-21 19:28:17 +02:00
|
|
|
self.status_bar.config(text=f"Vytvořen tag: {self.build_full_tag(new_id)}")
|
|
|
|
|
|
|
|
|
|
def tree_delete_tag(self):
|
|
|
|
|
item = self.selected_tree_item_for_context
|
|
|
|
|
if not item:
|
|
|
|
|
return
|
|
|
|
|
full = self.build_full_tag(item)
|
|
|
|
|
ans = messagebox.askyesno("Smazat tag", f"Opravdu chcete smazat '{full}'?")
|
|
|
|
|
if not ans:
|
|
|
|
|
return
|
2025-09-24 14:30:23 +02:00
|
|
|
tag_name = self.tree.item(item, "text")
|
|
|
|
|
parent_id = self.tree.parent(item)
|
2025-09-21 19:28:17 +02:00
|
|
|
self.tree.delete(item)
|
|
|
|
|
self.states.pop(item, None)
|
2025-09-24 14:30:23 +02:00
|
|
|
|
|
|
|
|
if parent_id == self.root_id:
|
|
|
|
|
self.tagmanager.remove_category(tag_name)
|
|
|
|
|
else:
|
|
|
|
|
category = self.tree.item(parent_id, "text")
|
|
|
|
|
self.tagmanager.remove_tag(category, tag_name)
|
|
|
|
|
|
|
|
|
|
self.update_files_from_manager(self.filehandler.filelist)
|
2025-09-21 19:28:17 +02:00
|
|
|
self.status_bar.config(text=f"Smazán tag: {full}")
|
|
|
|
|
|
|
|
|
|
# ==================================================
|
2025-09-24 14:30:23 +02:00
|
|
|
# TREE HELPERS
|
2025-09-21 19:28:17 +02:00
|
|
|
# ==================================================
|
2025-09-24 14:30:23 +02:00
|
|
|
def build_full_tag(self, item_id):
|
|
|
|
|
parts = []
|
|
|
|
|
cur = item_id
|
2025-09-28 13:00:39 +02:00
|
|
|
while cur and cur != self.root_id:
|
2025-09-24 14:30:23 +02:00
|
|
|
parts.append(self.tree.item(cur, "text"))
|
|
|
|
|
cur = self.tree.parent(cur)
|
|
|
|
|
parts.reverse()
|
|
|
|
|
return "/".join(parts) if parts else ""
|
2025-09-21 19:28:17 +02:00
|
|
|
|
2025-09-24 14:30:23 +02:00
|
|
|
def get_checked_full_tags(self):
|
|
|
|
|
return {self.build_full_tag(i) for i, v in self.states.items() if v}
|
2025-09-21 19:28:17 +02:00
|
|
|
|
2025-09-24 14:30:23 +02:00
|
|
|
def refresh_tree_tags(self):
|
|
|
|
|
for child in self.tree.get_children(self.root_id):
|
|
|
|
|
self.tree.delete(child)
|
2025-09-21 19:28:17 +02:00
|
|
|
|
2025-09-24 14:30:23 +02:00
|
|
|
for category in self.tagmanager.get_categories():
|
2025-09-28 13:00:39 +02:00
|
|
|
cat_id = self.tree.insert(self.root_id, "end", text=category, image=self.icons["tag"])
|
2025-09-24 14:30:23 +02:00
|
|
|
self.states[cat_id] = False
|
|
|
|
|
for tag in self.tagmanager.get_tags_in_category(category):
|
|
|
|
|
tag_id = self.tree.insert(cat_id, "end", text=tag.name, image=self.icons["unchecked"])
|
|
|
|
|
self.states[tag_id] = False
|
2025-09-21 19:28:17 +02:00
|
|
|
|
2025-09-28 13:00:39 +02:00
|
|
|
self.tree.item(self.root_id, open=True)
|
|
|
|
|
|
|
|
|
|
def get_checked_tags(self) -> List[Tag]:
|
|
|
|
|
tags: List[Tag] = []
|
|
|
|
|
for item_id, checked in self.states.items():
|
|
|
|
|
if not checked:
|
|
|
|
|
continue
|
|
|
|
|
parent_id = self.tree.parent(item_id)
|
|
|
|
|
if parent_id == self.root_id:
|
|
|
|
|
continue
|
|
|
|
|
category = self.tree.item(parent_id, "text")
|
|
|
|
|
name = self.tree.item(item_id, "text")
|
|
|
|
|
tags.append(Tag(category, name))
|
|
|
|
|
return tags
|
|
|
|
|
|
|
|
|
|
def _get_checked_recursive(self, item):
|
|
|
|
|
tags = []
|
|
|
|
|
if self.states.get(item, False):
|
|
|
|
|
parent = self.tree.parent(item)
|
|
|
|
|
if parent and parent != self.root_id:
|
|
|
|
|
parent_text = self.tree.item(parent, "text")
|
|
|
|
|
text = self.tree.item(item, "text")
|
|
|
|
|
tags.append(f"{parent_text}/{text}")
|
|
|
|
|
for child in self.tree.get_children(item):
|
|
|
|
|
tags.extend(self._get_checked_recursive(child))
|
|
|
|
|
return tags
|