From 6936f376c31a25f852a2e5783247b1dc7a640fda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Doubravsk=C3=BD?= Date: Sun, 28 Sep 2025 13:00:39 +0200 Subject: [PATCH] WIP --- data/samples/.50.png.!tag | 9 + data/samples/.DORMER_PRAMET.PDF.!tag | 3 +- src/core/constants.py | 4 + src/core/file.py | 50 +++- src/core/file_manager.py | 61 ++++- src/core/list_manager.py | 20 ++ src/ui/gui.py | 372 ++++++++++++++++++++++----- 7 files changed, 454 insertions(+), 65 deletions(-) create mode 100644 data/samples/.50.png.!tag create mode 100644 src/core/constants.py create mode 100644 src/core/list_manager.py diff --git a/data/samples/.50.png.!tag b/data/samples/.50.png.!tag new file mode 100644 index 0000000..bc1eb9a --- /dev/null +++ b/data/samples/.50.png.!tag @@ -0,0 +1,9 @@ +{ + "new": true, + "ignored": false, + "tags": [ + "Rozlišení/4K", + "Rozlišení/FullHD" + ], + "date": null +} \ No newline at end of file diff --git a/data/samples/.DORMER_PRAMET.PDF.!tag b/data/samples/.DORMER_PRAMET.PDF.!tag index 88dcf81..f0e3d74 100644 --- a/data/samples/.DORMER_PRAMET.PDF.!tag +++ b/data/samples/.DORMER_PRAMET.PDF.!tag @@ -3,5 +3,6 @@ "ignored": false, "tags": [ "Rozlišení/4K" - ] + ], + "date": "2025-09-15" } \ No newline at end of file diff --git a/src/core/constants.py b/src/core/constants.py new file mode 100644 index 0000000..17ea52f --- /dev/null +++ b/src/core/constants.py @@ -0,0 +1,4 @@ +# src/core/constants.py +VERSION = "v1.0.1" +APP_NAME = "Tagger" +APP_VIEWPORT = "1000x700" \ No newline at end of file diff --git a/src/core/file.py b/src/core/file.py index 768dc93..13ed363 100644 --- a/src/core/file.py +++ b/src/core/file.py @@ -9,8 +9,10 @@ class File: self.metadata_filename = self.file_path.parent / f".{self.filename}.!tag" self.new = True self.ignored = False - self.tags = [] + self.tags: list[Tag] = [] self.tagmanager = tagmanager + # new: optional date string "YYYY-MM-DD" (assigned manually) + self.date: str | None = None self.get_metadata() def get_metadata(self) -> None: @@ -18,6 +20,7 @@ class File: self.new = True self.ignored = False self.tags = [] + self.date = None else: self.load_metadata() @@ -27,6 +30,8 @@ class File: "ignored": self.ignored, # ukládáme full_path tagů "tags": [tag.full_path if isinstance(tag, Tag) else tag for tag in self.tags], + # date může být None + "date": self.date, } with open(self.metadata_filename, "w", encoding="utf-8") as f: json.dump(data, f, indent=2, ensure_ascii=False) @@ -37,6 +42,7 @@ class File: self.new = data.get("new", True) self.ignored = data.get("ignored", False) self.tags = [] + self.date = data.get("date", None) if not self.tagmanager: return @@ -46,3 +52,45 @@ class File: category, name = tag_str.split("/", 1) tag = self.tagmanager.add_tag(category, name) self.tags.append(tag) + + def set_date(self, date_str: str | None): + """Nastaví datum (např. '2025-09-25') nebo None pro smazání.""" + if date_str is None or date_str == "": + self.date = None + else: + # neprvádíme složitou validaci zde; očekáváme 'YYYY-MM-DD' + self.date = date_str + self.save_metadata() + + def add_tag(self, tag): + # tag může být Tag nebo string + from .tag import Tag as TagClass + if isinstance(tag, str): + if "/" in tag and self.tagmanager: + category, name = tag.split("/", 1) + tag_obj = self.tagmanager.add_tag(category, name) + else: + tag_obj = TagClass("default", tag) + elif isinstance(tag, TagClass): + tag_obj = tag + else: + return + if tag_obj not in self.tags: + self.tags.append(tag_obj) + self.save_metadata() + + def remove_tag(self, tag): + # tag může být Tag nebo string (full_path) + if isinstance(tag, str): + if "/" in tag: + category, name = tag.split("/", 1) + tag_obj = Tag(category, name) + else: + tag_obj = Tag("default", tag) + elif isinstance(tag, Tag): + tag_obj = tag + else: + return + if tag_obj in self.tags: + self.tags.remove(tag_obj) + self.save_metadata() diff --git a/src/core/file_manager.py b/src/core/file_manager.py index d309e6c..84b03de 100644 --- a/src/core/file_manager.py +++ b/src/core/file_manager.py @@ -2,11 +2,12 @@ from pathlib import Path from .file import File from .tag_manager import TagManager from .utils import list_files +from typing import Iterable class FileManager: def __init__(self, tagmanager: TagManager): - self.filelist = [] - self.folders = [] + self.filelist: list[File] = [] + self.folders: list[Path] = [] self.tagmanager = tagmanager self.on_files_changed = None # callback do GUI @@ -21,15 +22,16 @@ class FileManager: if self.on_files_changed: self.on_files_changed(self.filelist) - def assign_tag_to_file_objects(self, files_objs: list[File], tag: str): + def assign_tag_to_file_objects(self, files_objs: list[File], tag): + """Přiřadí tag (Tag nebo 'category/name' string) ke každému souboru v seznamu.""" for f in files_objs: - # tag může být string nebo Tag if isinstance(tag, str): if "/" in tag: category, name = tag.split("/", 1) tag_obj = self.tagmanager.add_tag(category, name) else: - tag_obj = tag + # pokud není uvedena kategorie, zařadíme pod "default" + tag_obj = self.tagmanager.add_tag("default", tag) else: tag_obj = tag if tag_obj not in f.tags: @@ -37,3 +39,52 @@ class FileManager: f.save_metadata() if self.on_files_changed: self.on_files_changed(self.filelist) + + def remove_tag_from_file_objects(self, files_objs: list[File], tag): + """Odebere tag (Tag nebo 'category/name') ze všech uvedených souborů.""" + for f in files_objs: + if isinstance(tag, str): + if "/" in tag: + category, name = tag.split("/", 1) + tag_obj = File.__module__ # dummy to satisfy typing (we create Tag below) + # use Tag class directly + from .tag import Tag as TagClass + tag_obj = TagClass(category, name) + else: + from .tag import Tag as TagClass + tag_obj = TagClass("default", tag) + else: + tag_obj = tag + if tag_obj in f.tags: + f.tags.remove(tag_obj) + f.save_metadata() + if self.on_files_changed: + self.on_files_changed(self.filelist) + + def filter_files_by_tags(self, tags: Iterable): + """ + Vrátí jen soubory, které obsahují všechny zadané tagy. + 'tags' může být iterace Tag objektů nebo stringů 'category/name'. + """ + tags_list = list(tags) if tags is not None else [] + if not tags_list: + return self.filelist + + # normalizuj cílové tagy na full_path stringy + target_full_paths = set() + from .tag import Tag as TagClass + for t in tags_list: + if isinstance(t, TagClass): + target_full_paths.add(t.full_path) + elif isinstance(t, str): + target_full_paths.add(t) + else: + # neznámý typ: ignorovat + continue + + filtered = [] + for f in self.filelist: + file_tags = {t.full_path for t in f.tags} + if all(tag in file_tags for tag in target_full_paths): + filtered.append(f) + return filtered diff --git a/src/core/list_manager.py b/src/core/list_manager.py new file mode 100644 index 0000000..b1b3fea --- /dev/null +++ b/src/core/list_manager.py @@ -0,0 +1,20 @@ +from typing import List +from .file import File + +class ListManager: + def __init__(self): + # 'name' nebo 'date' + self.sort_mode = "name" + + def set_sort(self, mode: str): + if mode in ("name", "date"): + self.sort_mode = mode + + def sort_files(self, files: List[File]) -> List[File]: + if self.sort_mode == "name": + return sorted(files, key=lambda f: f.filename.lower()) + else: + # sort by date (None last) — nejnovější nahoře? Zde dávám None jako "" + def date_key(f): + return (f.date is None, f.date or "") + return sorted(files, key=date_key) diff --git a/src/ui/gui.py b/src/ui/gui.py index 6b5eaf2..48f09ff 100644 --- a/src/ui/gui.py +++ b/src/ui/gui.py @@ -4,14 +4,22 @@ import subprocess import tkinter as tk from tkinter import ttk, simpledialog, messagebox, filedialog from pathlib import Path +from typing import List from src.core.image import load_icon from src.core.file_manager import FileManager from src.core.tag_manager import TagManager from src.core.file import File from src.core.tag import Tag +from src.core.list_manager import ListManager +from src.core.constants import APP_NAME, VERSION, APP_VIEWPORT + class TagSelectionDialog(tk.Toplevel): + """ + Jednoduchý dialog pro výběr tagů (původní, používán jinde). + (tento třída zůstává pro jednobodové použití) + """ def __init__(self, parent, tags: list[str]): super().__init__(parent) self.title("Vyber tagy") @@ -42,14 +50,96 @@ class TagSelectionDialog(tk.Toplevel): self.selected_tags = [tag for tag, var in self.vars.items() if var.get()] self.destroy() + +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("", 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() + + class App: def __init__(self, filehandler: FileManager, tagmanager: TagManager): - self.states = {} # tree states (checkboxy) - self.listbox_map = {} # filename -> list[File] + self.states = {} # tree states (checkboxy) item_id -> bool + self.listbox_map: dict[int, list[File]] = {} self.selected_tree_item_for_context = None self.selected_list_index_for_context = None self.filehandler = filehandler self.tagmanager = tagmanager + self.list_manager = ListManager() + + # nové proměnné + self.filter_text = "" + self.show_full_path = False + self.sort_mode = "name" + self.sort_order = "asc" # callback z FileManageru self.filehandler.on_files_changed = self.update_files_from_manager @@ -59,22 +149,25 @@ class App: # ================================================== def main(self): root = tk.Tk() - root.title("Tagger") - root.geometry("900x600") + root.title(APP_NAME + " " + VERSION) + root.geometry(APP_VIEWPORT) self.root = root # ---- Ikony unchecked = load_icon("src/resources/images/32/32_unchecked.png") checked = load_icon("src/resources/images/32/32_checked.png") - self.icons = {"unchecked": unchecked, "checked": checked} + tag_icon = load_icon("src/resources/images/32/32_tag.png") + self.icons = {"unchecked": unchecked, "checked": checked, "tag": tag_icon} root.unchecked_img = unchecked root.checked_img = checked + root.tag_img = tag_icon # ---- Layout menu_bar = tk.Menu(root) root.config(menu=menu_bar) file_menu = tk.Menu(menu_bar, tearoff=0) file_menu.add_command(label="Open Folder...", command=self.open_folder_dialog) + file_menu.add_command(label="Set date for selected...", command=self.set_date_for_selected) file_menu.add_separator() file_menu.add_command(label="Exit", command=root.quit) menu_bar.add_cascade(label="File", menu=file_menu) @@ -91,12 +184,40 @@ class App: self.tree.bind("", self.on_tree_left_click) self.tree.bind("", self.on_tree_right_click) - # ---- Listbox (right) - self.listbox = tk.Listbox(main_frame, selectmode="extended") - self.listbox.grid(row=0, column=1, sticky="nsew", padx=4, pady=4) + # ---- 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("", 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") self.listbox.bind("", self.on_list_double) self.listbox.bind("", self.on_list_right_click) + 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) + # ---- Status bar self.status_bar = tk.Label(root, text="Připraven", anchor="w", relief="sunken") self.status_bar.pack(side="bottom", fill="x") @@ -110,48 +231,95 @@ class App: 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) self.list_menu.add_command(label="Assign Tag", command=self.assign_tag_to_selected) + self.list_menu.add_command(label="Assign Tag (advanced)...", command=self.assign_tag_to_selected_bulk) # ---- Root node - root_id = self.tree.insert("", "end", text="Štítky") - self.states[root_id] = False + root_id = self.tree.insert("", "end", text="Štítky", image=self.icons["tag"]) self.tree.item(root_id, open=True) self.root_id = root_id - # ⚡ refresh listbox při startu + # ⚡ refresh při startu + self.refresh_tree_tags() self.update_files_from_manager(self.filehandler.filelist) root.mainloop() + # ================================================== + # 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) + # ================================================== # FILE REFRESH + MAP # ================================================== - def update_files_from_manager(self, filelist): + 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 self.listbox.delete(0, "end") self.listbox_map = {} - checked_tags = self.get_checked_full_tags() - for f in filelist: - # ignoruj metadata soubory - if f.file_path.name.endswith(".!tag") or f.file_path.name.startswith("."): - continue - if not checked_tags or checked_tags.issubset(set(f.tags)): - filename = f.file_path.name - self.listbox.insert("end", filename) - if filename not in self.listbox_map: - self.listbox_map[filename] = [] - self.listbox_map[filename].append(f) + 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] + self.status_bar.config(text=f"Zobrazeno {self.listbox.size()} položek") + # ================================================== + # GET SELECTED FILES + # ================================================== def get_selected_files_objects(self): indices = self.listbox.curselection() files = [] for idx in indices: - filename = self.listbox.get(idx) - files.extend(self.listbox_map.get(filename, [])) + files.extend(self.listbox_map.get(idx, [])) return files # ================================================== - # ASSIGN TAG + # ASSIGN TAG (jednoduchý) # ================================================== def assign_tag_to_selected(self): files = self.get_selected_files_objects() @@ -159,34 +327,93 @@ class App: self.status_bar.config(text="Nebyly vybrány žádné soubory") return - # generujeme seznam tagů přímo z TagManageru (aktuální) - all_tags = [] + all_tags: List[Tag] = [] for category in self.tagmanager.get_categories(): for tag in self.tagmanager.get_tags_in_category(category): - all_tags.append(f"{category}/{tag.name}") # vždy aktuální seznam + all_tags.append(tag) if not all_tags: messagebox.showwarning("Chyba", "Žádné tagy nejsou definovány") return - # vytvoříme dialog **po načtení všech tagů** - dialog = TagSelectionDialog(self.root, all_tags) - selected_tags = dialog.selected_tags + tag_strings = [tag.full_path for tag in all_tags] + dialog = TagSelectionDialog(self.root, tag_strings) + selected_tag_strings = dialog.selected_tags - if not selected_tags: + if not selected_tag_strings: self.status_bar.config(text="Nebyl vybrán žádný tag") return - # přiřazení tagů souborům - for full_tag in selected_tags: + selected_tags: list[Tag] = [] + for full_tag in selected_tag_strings: if "/" in full_tag: category, name = full_tag.split("/", 1) - self.filehandler.assign_tag_to_file_objects(files, Tag(category, name)) + selected_tags.append(self.tagmanager.add_tag(category, name)) + + for tag in selected_tags: + self.filehandler.assign_tag_to_file_objects(files, tag) self.update_files_from_manager(self.filehandler.filelist) - self.status_bar.config(text=f"Přiřazeny tagy: {', '.join(selected_tags)}") + 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 + 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) + + 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(ů)") # ================================================== # DOUBLE CLICK OPEN @@ -217,11 +444,15 @@ class App: idx = self.listbox.nearest(event.y) if idx is None: return + + # 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) + self.selected_list_index_for_context = idx - self.listbox.selection_clear(0, "end") - self.listbox.selection_set(idx) self.list_menu.tk_popup(event.x_root, event.y_root) + def list_open_file(self): for f in self.get_selected_files_objects(): self.open_file(f.file_path) @@ -248,13 +479,13 @@ class App: folder_path = Path(folder) try: self.filehandler.append(folder_path) - # po načtení propíš tagy z metadat do TagManageru for f in self.filehandler.filelist: if f.tags and f.tagmanager: - for t in f.tags: # t je Tag + for t in f.tags: 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() + self.update_files_from_manager(self.filehandler.filelist) except Exception as e: messagebox.showerror("Chyba", f"Nelze přidat složku {folder}: {e}") @@ -270,19 +501,24 @@ class App: if not item_id: return - item_text = self.tree.item(item_id, "text") parent_id = self.tree.parent(item_id) - if parent_id == "": - # root nebo kategorie → jen toggle open/close + if parent_id == "" or parent_id == self.root_id: is_open = self.tree.item(item_id, "open") self.tree.item(item_id, open=not is_open) return - # pokud je to tag → toggle checkbox self.states[item_id] = not self.states.get(item_id, False) - 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)}") - self.update_files_from_manager(self.filehandler.filelist) + 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) def on_tree_right_click(self, event): item_id = self.tree.identify_row(event.y) @@ -303,15 +539,14 @@ class App: if not name: return parent = self.selected_tree_item_for_context if not background else self.root_id - new_id = self.tree.insert(parent, "end", text=name) + new_id = self.tree.insert(parent, "end", text=name, image=self.icons["unchecked"]) self.states[new_id] = False - # ⚡ aktualizace TagManageru if parent == self.root_id: category = name self.tagmanager.add_category(category) + self.tree.item(new_id, image=self.icons["tag"]) else: - # tag pod existující kategorií category = self.tree.item(parent, "text") self.tagmanager.add_tag(category, name) @@ -330,7 +565,6 @@ class App: self.tree.delete(item) self.states.pop(item, None) - # odebrání z TagManageru if parent_id == self.root_id: self.tagmanager.remove_category(tag_name) else: @@ -346,7 +580,7 @@ class App: def build_full_tag(self, item_id): parts = [] cur = item_id - while cur: + while cur and cur != self.root_id: parts.append(self.tree.item(cur, "text")) cur = self.tree.parent(cur) parts.reverse() @@ -356,17 +590,39 @@ class App: return {self.build_full_tag(i) for i, v in self.states.items() if v} def refresh_tree_tags(self): - # smažeme všechny pod root for child in self.tree.get_children(self.root_id): self.tree.delete(child) - # projdeme všechny kategorie a tagy for category in self.tagmanager.get_categories(): - cat_id = self.tree.insert(self.root_id, "end", text=category) + cat_id = self.tree.insert(self.root_id, "end", text=category, image=self.icons["tag"]) 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 - # otevřeme root - self.tree.item(self.root_id, open=True) \ No newline at end of file + 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