From 531b05cd033dd2f7b48d4d48369137dc2a125a57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Doubravsk=C3=BD?= Date: Wed, 24 Sep 2025 14:30:23 +0200 Subject: [PATCH] Tags and files fuctions --- .gitignore | 3 +- .vscode/settings.json | 9 +- Test.db3 | Bin 28672 -> 0 bytes data/samples/.DORMER_PRAMET.PDF.!tag | 7 + data/samples/50.png | Bin 0 -> 9053 bytes data/samples/DORMER_PRAMET.PDF | 210 ++++++++++++++ src/core/file.py | 30 +- src/core/file_manager.py | 43 +-- src/core/tag.py | 22 ++ src/core/tag_manager.py | 37 ++- src/core/utils.py | 7 + src/ui/gui.py | 401 +++++++++++++++++++-------- test/SQL_handler_test.py | 21 -- tests/test_image.py | 40 +++ 14 files changed, 661 insertions(+), 169 deletions(-) delete mode 100644 Test.db3 create mode 100644 data/samples/.DORMER_PRAMET.PDF.!tag create mode 100644 data/samples/50.png create mode 100644 data/samples/DORMER_PRAMET.PDF create mode 100644 src/core/utils.py delete mode 100644 test/SQL_handler_test.py create mode 100644 tests/test_image.py diff --git a/.gitignore b/.gitignore index 0e5ac79..652d35d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .venv -__pycache__ \ No newline at end of file +__pycache__ +.pytest_cache \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 3905ae4..6d3c95f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,10 @@ { - "editor.rulers": [80] + "editor.rulers": [ + 80 + ], + "python.testing.pytestArgs": [ + "tests" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true } \ No newline at end of file diff --git a/Test.db3 b/Test.db3 deleted file mode 100644 index 581b170aa74c18ff72723150d4c62bff55c28405..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28672 zcmeI(!D`zu0LF2tP3=*Vr7(hz)g`QhF&I1SuuHwhYfW7zu^m`oU^13LA#3TDLfKAP z&wY?SOW$bDQi^@0gb+1d zZC&$X&P~mW%nw$)*2Jry7mem0VbyO%bH91_bkew~-!_sJ_wW`12q1s}0tg_000Id7 z?*g;EE!%0gp?jjo^gbpk?CLrQ0yPNz{=h5T@#3vzImc}&PS3uKuYO!i z^ad*(T~5!l^K|h$rLPyy?AfQ9?d8aJW?8_ z*)a_T0R#|0009ILKmY**5I_I{1ga{qZGy4?uj=%Y3jqWWKmY**5I_I{1Q0*~flPqk z|5*qKAbWTbAb3=?5sU~J8Fb`J1#c2+1T%1_J8 zNo8Qrm{>c}*5j~)%+dJZrHiKWl?e!*36MR~w4!5i*g_6bueBFeHJ@A>e_@HDXJMg8 z3^!k$PMB=WoYK+)QI>=J0`BVSuHu>>>pd5`_OKuSTT%YL@H?NttJGz)WFI%IigeSG z6E?mXYWSgxp-teBMwLq1tKyR>O!FSUmb2S;&$V*e_nr3c>n9mYJ`nCqpP3hNMEc0( z+0)Kd&bK(zHOZ;Q&nE2xYp489z1@F~?R>D|fbn5BD^q_NH8Jj7o$ZF&Qd%EH3S9zC zPl+4pY0ueivHGc2_Pxpb4Q5;po3_)hSfEfRiPN)VLZB;WYDCASRTGnxTz?z#|8IEg zKgnAyoU=!Udxb;mlOG5FKPcaxR#$Y$m+M%=Iq}>7B-ogCJ1kDSegF8jyB3Up&L5b1 z?Nj)x183AenVz|m6g$=@4J%P#ouIi2U^#6KpjHF6d9i}O!0)oyqAKI?XL zvHWs=hgrM4?l7>oAMj}0blvve-K+O*?@yog_b)?5VbRLN1`fr*c;PJYhy>Dl${@^G zvDCf{D9B#o>Fdh=j9s2zTh4HU-$kI1WQl7;iF1B#Zfaf$gL6@8Vo7R>LV0FMhJw4N zZ$Nk>pEv`90)wZEV@SoVH#ZC!85DR97_`-&Kg{I6-^JQrve;o^u zWMB~C$pF(rY$jlulUWKxH?%PFf@p_B4QwD0q>ZFxor-rGwGt!D#aUm<&dn2cyjcP;o$|=D~VdYcY^<5>N7NyJ}knbyy(7yWEgr z-2+aL;oJ=ZkYU_WY4B+GXt0bX2T-mV%_ZQ;p3%}_w9o|RhS5@Sv{W1|6^B+f2vp6l l0j~700<8rYEyTzu#F%(ia~JSEG!+D;d{0+Dmvv4FO#u7Fip~H4 literal 0 HcmV?d00001 diff --git a/data/samples/DORMER_PRAMET.PDF b/data/samples/DORMER_PRAMET.PDF new file mode 100644 index 0000000..df832a2 --- /dev/null +++ b/data/samples/DORMER_PRAMET.PDF @@ -0,0 +1,210 @@ +%PDF-1.6 % +1 0 obj <>/OCGs[5 0 R]>>/Pages 3 0 R/Type/Catalog>> endobj 2 0 obj <>stream + + + + + application/pdf + + + DORMER PRAMET TM logo black + + + + + Pavel Remeš + Pavel Remes + + + + + Marketing Dpt. 787 53 Sumperk, Czech Republic + + + Adobe Illustrator 27.6 (Windows) + 2024-11-19T12:48:57+02:00 + 2024-11-19T12:48:57+01:00 + 2024-11-19T12:48:57+01:00 + + + + 256 + 72 + JPEG + /9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgASAEAAwER AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE 1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp 0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo +DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A8z5JodirsVdirsVdirsV dir6V/5wq/47Hmn/AJh7T/icmAs4Pbvz7/8AJPeaf+YP/mYmAM5cn5+ZJpZB5U8/ecfKd2l15f1a 4sWRgzQo5ML0NaSQtWNx/rLikF9wfkt+bNl+Y/lc33prbaxYssOrWaklVkYVWSOu/pyUPGu4oRvS pi2g28Z/5yO/5yB1mHWbryZ5Su3sorImLV9TgYrM8w+1BE43RY+jsNy23QbkMZSfNL3E7zGd5Hac nkZWYl+XjyO9cLW98/5x5/P7XtM8wWflXzPfSX+h6hItvaXVy5eW0mc8Y/3jmpiY0Uqx+HqKCoIZ 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 6UuDxcFTT0oexwFnHk9swMnYq7FX/9k= + + + + xmp.did:d8460b81-f0b4-b54f-90cb-f1169ae4002d + uuid:04d3f7ee-b4f3-4080-b4f4-fc39258cc8b8 + proof:pdf + uuid:6CC3FC99C2BFDE11AFC3CA46B930BAE5 + + uuid:ae1ad9e8-25ce-45bc-851b-942f5049d62f + xmp.did:351d9a2d-71f1-3d42-ad11-b0eef367c74f + uuid:6CC3FC99C2BFDE11AFC3CA46B930BAE5 + default + + + + + saved + xmp.iid:5872C63803E8E411A4A09BB2B09E70BA + 2015-04-21T11:05:19+02:00 + Adobe Illustrator CS6 (Windows) + / + + + saved + xmp.iid:5FBFBBE305E8E41193D7858916604D92 + 2015-04-21T11:07:52+02:00 + Adobe Bridge CS6 (Windows) + /metadata + + + saved + xmp.iid:D0FCA8F16226E711A2EEF3A1EB1A0515 + 2017-04-21T09:20:06+02:00 + Adobe Illustrator CS6 (Windows) + / + + + saved + xmp.iid:eafbe254-adf0-4c49-ad3a-1734bef02370 + 2019-01-02T14:50:24+01:00 + Adobe Illustrator CC 23.0 (Windows) + / + + + saved + xmp.iid:d4d72b2d-9304-034c-9ced-4fba2904b342 + 2019-01-02T15:06:17+01:00 + Adobe Bridge CC 2017 (Windows) + /metadata + + + saved + xmp.iid:4ccde57e-9c46-2043-a80e-a8cb4f87ab08 + 2019-04-09T13:49:07+02:00 + Adobe Illustrator CC 23.0 (Windows) + / + + + saved + xmp.iid:d8460b81-f0b4-b54f-90cb-f1169ae4002d + 2024-11-19T12:48:09+01:00 + Adobe Illustrator 27.6 (Windows) + / + + + + 1 + False + True + + 106.814775 + 46.000000 + Millimeters + + + + Cyan + Magenta + Yellow + Black + + + + + + Výchozí skupina vzorků + 0 + + + + Adobe PDF library 17.00 + Chief Graphic Designer + Adobe Illustrator + + Unicovska 7 + Šumperk + Czech Republic + 787 53 + +420 583 381 553 + pavel.remes@dormerpramet.com + Czech Republic + + + + + + + + + + + + + + + + + + + + + + + + + + +endstream endobj 3 0 obj <> endobj 7 0 obj <>/Properties<>>>/TrimBox[0.0 0.0 302.782 130.394]/Type/Page>> endobj 8 0 obj <>stream +HlWK$ )֏0f1x݀_ 0GR^ѯ,)з?=yy3ʨg9tc9#vP:s|o?aEKgjFv~<<,G'+?R38%IJiP o+,٭bsR豟G>4[-ā$BU*IτQˆ(BL!fABld4e!4`/?qP +l 56/J[ (| xaIaJ[k篢ON(bG +-:l异Q* GVPGyZӽxcP+wѾ@{U׽v>{?cW<tEoH#sg9{Kj,~peQ/|SVa/J,!$_ eYCAjz,7IxS Efm?nj؈Šj2+??s%N#/NEZf9 +x ZЂPb/ 6M%DJ<uA(:Jea; +2ﵮ$Ke!Q;P*oȅ]d#/jm#H?giD!WLd +:"H-0XWVq9lXёh ,8PS"V$- ylՈVҞ19ڃa +jN8T56Qq03g`0+X`W@%;o.lLG! ?v>cOuXS1xYJ<;[[7<1xW,9b1 +am?lOPz$v0m).QHOjܴ܎&le֏GTu}qŎ}+#vձnzVY-g8%tj9rat"vq^ʀU5.'^UM>*3.:N'{7 +Jѱl@5-sv;:hVxDvڊH/eW?¼P5-/l o#h dZ1g@_[\Mhpy6zg^PU5g,kG #Dzl"yWm뮗+FU[yB[&"q3ԂC$[܊{~ls#/VZ}g͕ꣶlNU$>l궠:|4ZxAZy7B5}!M 'jWec``1`Ir)Tnc:+#ɘ>QbƛTI#%2QBkݝ>@uL“[ɕkl;[ynh +܀bޏFhǴIvZˡS`bѤh!1r2ûQ5oNVo ^wgq9mkN]9HoO&aTdOl6% A'2inr(W5]i3v TYҪA.g89W{vc7:G +bۤztx&A. tJ-aod078)IaU;Ku$i =1_#l:> endobj 10 0 obj [/View/Design] endobj 11 0 obj <>>> endobj 9 0 obj <> endobj 6 0 obj [5 0 R] endobj 12 0 obj <> endobj xref +0 13 +0000000000 65535 f +0000000016 00000 n +0000000144 00000 n +0000021179 00000 n +0000000000 00000 f +0000024127 00000 n +0000024423 00000 n +0000021230 00000 n +0000021563 00000 n +0000024313 00000 n +0000024197 00000 n +0000024228 00000 n +0000024446 00000 n +trailer +<<8886194BA5A7D34E82FD2511260ACF59>]>> +startxref +24726 +%%EOF diff --git a/src/core/file.py b/src/core/file.py index 7a4780a..768dc93 100644 --- a/src/core/file.py +++ b/src/core/file.py @@ -1,21 +1,18 @@ from pathlib import Path import json +from .tag import Tag -class File(): - def __init__(self, file_path: Path, tagmanager = None) -> None: +class File: + def __init__(self, file_path: Path, tagmanager=None) -> None: self.file_path = file_path - self.metadata_filename = self.get_metadata_filename(self.file_path) + self.filename = file_path.name + self.metadata_filename = self.file_path.parent / f".{self.filename}.!tag" self.new = True self.ignored = False self.tags = [] self.tagmanager = tagmanager self.get_metadata() - def get_metadata_filename(self, file_path: Path) -> Path: - file_name = file_path.name - metadata_filename =Path(f".{file_name}.!tag") - return metadata_filename - def get_metadata(self) -> None: if not self.metadata_filename.exists(): self.new = True @@ -24,12 +21,12 @@ class File(): else: self.load_metadata() - def save_metadata(self) -> None: - """Save object state into metadata file as JSON.""" + def save_metadata(self): data = { "new": self.new, "ignored": self.ignored, - "tags": self.tags, + # ukládáme full_path tagů + "tags": [tag.full_path if isinstance(tag, Tag) else tag for tag in self.tags], } with open(self.metadata_filename, "w", encoding="utf-8") as f: json.dump(data, f, indent=2, ensure_ascii=False) @@ -39,4 +36,13 @@ class File(): data = json.load(f) self.new = data.get("new", True) self.ignored = data.get("ignored", False) - self.tags = data.get("tags", []) + self.tags = [] + + if not self.tagmanager: + return + + for tag_str in data.get("tags", []): + if "/" in tag_str: + category, name = tag_str.split("/", 1) + tag = self.tagmanager.add_tag(category, name) + self.tags.append(tag) diff --git a/src/core/file_manager.py b/src/core/file_manager.py index da02c3c..d309e6c 100644 --- a/src/core/file_manager.py +++ b/src/core/file_manager.py @@ -1,30 +1,39 @@ -# Imports from pathlib import Path from .file import File from .tag_manager import TagManager +from .utils import list_files - -class FileManager(): - def __init__(self, tagmanager: TagManager) -> None: +class FileManager: + def __init__(self, tagmanager: TagManager): self.filelist = [] self.folders = [] self.tagmanager = tagmanager + self.on_files_changed = None # callback do GUI def append(self, folder: Path) -> None: self.folders.append(folder) for each in list_files(folder): - file = File(each, self.tagmanager) - self.filelist.append(file) - -def list_files(folder_path: str | Path) -> list[Path]: - """ - Vrátí seznam Path objektů všech souborů uvnitř složky (rekurzivně). + if each.name.endswith(".!tag"): # ignoruj metadata soubory + continue + file_obj = File(each, self.tagmanager) + self.filelist.append(file_obj) - :param folder_path: cesta ke složce (string nebo Path) - :return: list objektů Path - """ - folder = Path(folder_path) - if not folder.is_dir(): - raise NotADirectoryError(f"{folder} není platná složka.") + if self.on_files_changed: + self.on_files_changed(self.filelist) - return [file_path for file_path in folder.rglob("*") if file_path.is_file()] \ No newline at end of file + def assign_tag_to_file_objects(self, files_objs: list[File], tag: str): + 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 + else: + tag_obj = tag + if tag_obj not in f.tags: + f.tags.append(tag_obj) + f.save_metadata() + if self.on_files_changed: + self.on_files_changed(self.filelist) diff --git a/src/core/tag.py b/src/core/tag.py index e69de29..5316cb0 100644 --- a/src/core/tag.py +++ b/src/core/tag.py @@ -0,0 +1,22 @@ +class Tag: + def __init__(self, category: str, name: str): + self.category = category + self.name = name + + @property + def full_path(self): + return f"{self.category}/{self.name}" + + def __str__(self): + return self.full_path + + def __repr__(self): + return f"Tag({self.full_path})" + + def __eq__(self, other): + if isinstance(other, Tag): + return self.category == other.category and self.name == other.name + return False + + def __hash__(self): + return hash((self.category, self.name)) diff --git a/src/core/tag_manager.py b/src/core/tag_manager.py index 5a0d59e..8049747 100644 --- a/src/core/tag_manager.py +++ b/src/core/tag_manager.py @@ -1,3 +1,36 @@ -class TagManager(): +from .tag import Tag + +class TagManager: def __init__(self): - pass \ No newline at end of file + self.tags_by_category = {} # {category: set(Tag)} + + def add_category(self, category: str): + if category not in self.tags_by_category: + self.tags_by_category[category] = set() + + def remove_category(self, category: str): + if category in self.tags_by_category: + del self.tags_by_category[category] + + def add_tag(self, category: str, name: str) -> Tag: + self.add_category(category) + tag = Tag(category, name) + self.tags_by_category[category].add(tag) + return tag + + def remove_tag(self, category: str, name: str): + if category in self.tags_by_category: + tag = Tag(category, name) + self.tags_by_category[category].discard(tag) + if not self.tags_by_category[category]: + self.remove_category(category) + + def get_all_tags(self): + """Vrací list všech tagů full_path""" + return [tag.full_path for tags in self.tags_by_category.values() for tag in tags] + + def get_categories(self): + return list(self.tags_by_category.keys()) + + def get_tags_in_category(self, category: str): + return list(self.tags_by_category.get(category, [])) diff --git a/src/core/utils.py b/src/core/utils.py new file mode 100644 index 0000000..76d8dbb --- /dev/null +++ b/src/core/utils.py @@ -0,0 +1,7 @@ +from pathlib import Path + +def list_files(folder_path: str | Path) -> list[Path]: + folder = Path(folder_path) + if not folder.is_dir(): + raise NotADirectoryError(f"{folder} není platná složka.") + return [file_path for file_path in folder.rglob("*") if file_path.is_file()] diff --git a/src/ui/gui.py b/src/ui/gui.py index 0ec9514..6b5eaf2 100644 --- a/src/ui/gui.py +++ b/src/ui/gui.py @@ -1,32 +1,69 @@ -# src/gui/gui.py import os import sys import subprocess import tkinter as tk -from tkinter import ttk, simpledialog, messagebox +from tkinter import ttk, simpledialog, messagebox, filedialog +from pathlib import Path -from src.core.image_handler import load_icon +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 +class TagSelectionDialog(tk.Toplevel): + 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() class App: - def __init__(self, filehandler: FileManager): - self.states = {} # Tree states (checkbox on/off) - self.files = {} # Path -> set(tags), napojíš na SQLite3FileHandler + def __init__(self, filehandler: FileManager, tagmanager: TagManager): + self.states = {} # tree states (checkboxy) + self.listbox_map = {} # filename -> list[File] self.selected_tree_item_for_context = None self.selected_list_index_for_context = None self.filehandler = filehandler + self.tagmanager = tagmanager + + # callback z FileManageru + self.filehandler.on_files_changed = self.update_files_from_manager # ================================================== # MAIN GUI # ================================================== - def main(self) -> None: + def main(self): root = tk.Tk() root.title("Tagger") root.geometry("900x600") self.root = root - # ---- Ikony (už připravené) + # ---- 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} @@ -37,6 +74,8 @@ class App: 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_separator() file_menu.add_command(label="Exit", command=root.quit) menu_bar.add_cascade(label="File", menu=file_menu) @@ -53,7 +92,7 @@ class App: self.tree.bind("", self.on_tree_right_click) # ---- Listbox (right) - self.listbox = tk.Listbox(main_frame) + self.listbox = tk.Listbox(main_frame, selectmode="extended") self.listbox.grid(row=0, column=1, sticky="nsew", padx=4, pady=4) self.listbox.bind("", self.on_list_double) self.listbox.bind("", self.on_list_right_click) @@ -70,15 +109,237 @@ class App: 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) + self.list_menu.add_command(label="Assign Tag", command=self.assign_tag_to_selected) - # ---- Sample root node - root_id = self.tree.insert("", "end", text="Root", image=self.icons["unchecked"]) + # ---- Root node + root_id = self.tree.insert("", "end", text="Štítky") self.states[root_id] = False self.tree.item(root_id, open=True) + self.root_id = root_id + + # ⚡ refresh listbox při startu + self.update_files_from_manager(self.filehandler.filelist) - self.refresh_list() root.mainloop() + # ================================================== + # FILE REFRESH + MAP + # ================================================== + def update_files_from_manager(self, filelist): + 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) + self.status_bar.config(text=f"Zobrazeno {self.listbox.size()} položek") + + 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, [])) + return files + + # ================================================== + # ASSIGN TAG + # ================================================== + 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 + + # generujeme seznam tagů přímo z TagManageru (aktuální) + all_tags = [] + 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 + + 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 + + if not selected_tags: + self.status_bar.config(text="Nebyl vybrán žádný tag") + return + + # přiřazení tagů souborům + for full_tag in selected_tags: + if "/" in full_tag: + category, name = full_tag.split("/", 1) + self.filehandler.assign_tag_to_file_objects(files, Tag(category, name)) + + self.update_files_from_manager(self.filehandler.filelist) + self.status_bar.config(text=f"Přiřazeny tagy: {', '.join(selected_tags)}") + + + + # ================================================== + # 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 + 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) + + 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) + # 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 + 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() + except Exception as e: + messagebox.showerror("Chyba", f"Nelze přidat složku {folder}: {e}") + + # ================================================== + # TREE EVENTS + # ================================================== + def on_tree_left_click(self, event): + region = self.tree.identify("region", event.x, event.y) + if region not in ("tree", "icon"): + return + + item_id = self.tree.identify_row(event.y) + 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 + 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) + + 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) + + # ================================================== + # TREE TAG CRUD + # ================================================== + def tree_add_tag(self, background=False): + 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_id + new_id = self.tree.insert(parent, "end", text=name) + self.states[new_id] = False + + # ⚡ aktualizace TagManageru + if parent == self.root_id: + category = name + self.tagmanager.add_category(category) + else: + # tag pod existující kategorií + category = self.tree.item(parent, "text") + self.tagmanager.add_tag(category, name) + + 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 + tag_name = self.tree.item(item, "text") + parent_id = self.tree.parent(item) + 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: + category = self.tree.item(parent_id, "text") + self.tagmanager.remove_tag(category, tag_name) + + self.update_files_from_manager(self.filehandler.filelist) + self.status_bar.config(text=f"Smazán tag: {full}") + # ================================================== # TREE HELPERS # ================================================== @@ -94,108 +355,18 @@ class App: def get_checked_full_tags(self): return {self.build_full_tag(i) for i, v in self.states.items() if v} - # ================================================== - # TREE EVENTS - # ================================================== - def on_tree_left_click(self, event): - region = self.tree.identify("region", event.x, event.y) - if region in ("tree", "icon"): - item_id = self.tree.identify_row(event.y) - if item_id: - 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.refresh_list() + 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) - 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: - # klik do prázdna = nabídka top-level tagu - self.selected_tree_item_for_context = None - 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) + # projdeme všechny kategorie a tagy + for category in self.tagmanager.get_categories(): + cat_id = self.tree.insert(self.root_id, "end", text=category) + 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 - def tree_add_tag(self, background=False): - name = simpledialog.askstring("Nový tag", "Název tagu:") - if not name: - return - parent = self.selected_tree_item_for_context if not background else "" - new_id = self.tree.insert(parent, "end", text=name, image=self.icons["unchecked"]) - self.states[new_id] = False - 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 - self.tree.delete(item) - self.states.pop(item, None) - self.refresh_list() - self.status_bar.config(text=f"Smazán tag: {full}") - - # ================================================== - # LIST EVENTS - # ================================================== - def on_list_double(self, event): - index = self.listbox.curselection() - if index: - self.open_file(self.listbox.get(index[0])) - - def on_list_right_click(self, event): - idx = self.listbox.nearest(event.y) - if idx is None: - return - 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): - idx = self.selected_list_index_for_context - if idx is not None: - self.open_file(self.listbox.get(idx)) - - def list_remove_file(self): - idx = self.selected_list_index_for_context - if idx is not None: - path = self.listbox.get(idx) - ans = messagebox.askyesno("Smazat z indexu", f"Odstranit '{path}' z indexu?") - if ans and path in self.files: - del self.files[path] - self.refresh_list() - self.status_bar.config(text=f"Odstraněno z indexu: {path}") - - # ================================================== - # FILE LIST REFRESH - # ================================================== - def refresh_list(self): - checked = self.get_checked_full_tags() - self.listbox.delete(0, "end") - for path, tags in self.files.items(): - if not checked or checked.issubset(tags): - self.listbox.insert("end", path) - self.status_bar.config(text=f"Zobrazeno {self.listbox.size()} položek") - - # ================================================== - # 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}") + # otevřeme root + self.tree.item(self.root_id, open=True) \ No newline at end of file diff --git a/test/SQL_handler_test.py b/test/SQL_handler_test.py deleted file mode 100644 index 23877cf..0000000 --- a/test/SQL_handler_test.py +++ /dev/null @@ -1,21 +0,0 @@ -import sys, os -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) - -from src.core.SQL_handler import SQLite3FileHandler -from pathlib import Path - -db_path = Path("Test.db3") - -test = SQLite3FileHandler(db_path=db_path) -try: - test.insert_tag("test") -except: - pass -try: - test.insert_category("Test") -except: - pass -test.insert_relation_tag_cat(4, 7) -print(test.fetch_all("RELATIONS")) - -test.close() \ No newline at end of file diff --git a/tests/test_image.py b/tests/test_image.py new file mode 100644 index 0000000..3ce500e --- /dev/null +++ b/tests/test_image.py @@ -0,0 +1,40 @@ +import sys, os +import tempfile +from pathlib import Path +import pytest + +# přidáme src do sys.path (pokud nespouštíš pytest s -m nebo PYTHONPATH=src) +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src"))) + +from core.image import load_icon +from PIL import Image, ImageTk +import tkinter as tk + + +@pytest.fixture(scope="module") +def tk_root(): + """Fixture pro inicializaci Tkinteru (nutné pro ImageTk).""" + root = tk.Tk() + yield root + root.destroy() + + +def test_load_icon_returns_photoimage(tk_root): + # vytvoříme dočasný obrázek + with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp: + tmp_path = Path(tmp.name) + try: + # vytvoříme 100x100 červený obrázek + img = Image.new("RGB", (100, 100), color="red") + img.save(tmp_path) + + icon = load_icon(tmp_path) + + # musí být PhotoImage + assert isinstance(icon, ImageTk.PhotoImage) + + # ověříme velikost 16x16 + assert icon.width() == 16 + assert icon.height() == 16 + finally: + tmp_path.unlink(missing_ok=True) \ No newline at end of file