diff --git a/config.json b/config.json new file mode 100644 index 0000000..6cd5b96 --- /dev/null +++ b/config.json @@ -0,0 +1,12 @@ +{ + "ignore_patterns": [ + "*.png", + "*.jpg", + "*.mp3", + "*/M/*", + "*/L/*", + "*/Ostatní/*", + "*.hidden*" + ], + "last_folder": "/media/veracrypt3" +} \ No newline at end of file diff --git a/src/core/config.py b/src/core/config.py new file mode 100644 index 0000000..734155f --- /dev/null +++ b/src/core/config.py @@ -0,0 +1,22 @@ +import json +from pathlib import Path + +CONFIG_FILE = Path("config.json") + +default_config = { + "ignore_patterns": [], + "last_folder": None +} + +def load_config(): + if CONFIG_FILE.exists(): + try: + with open(CONFIG_FILE, "r", encoding="utf-8") as f: + return json.load(f) + except Exception: + return default_config.copy() + return default_config.copy() + +def save_config(cfg: dict): + with open(CONFIG_FILE, "w", encoding="utf-8") as f: + json.dump(cfg, f, indent=2, ensure_ascii=False) diff --git a/src/core/file.py b/src/core/file.py index 13ed363..802daa2 100644 --- a/src/core/file.py +++ b/src/core/file.py @@ -21,6 +21,10 @@ class File: self.ignored = False self.tags = [] self.date = None + if self.tagmanager: + tag = self.tagmanager.add_tag("Stav", "Nové") + self.tags.append(tag) + self.save_metadata() else: self.load_metadata() @@ -93,4 +97,4 @@ class File: return if tag_obj in self.tags: self.tags.remove(tag_obj) - self.save_metadata() + self.save_metadata() \ No newline at end of file diff --git a/src/core/file_manager.py b/src/core/file_manager.py index 84b03de..5a5be7c 100644 --- a/src/core/file_manager.py +++ b/src/core/file_manager.py @@ -3,6 +3,8 @@ from .file import File from .tag_manager import TagManager from .utils import list_files from typing import Iterable +import fnmatch +from src.core.config import load_config, save_config class FileManager: def __init__(self, tagmanager: TagManager): @@ -10,18 +12,30 @@ class FileManager: self.folders: list[Path] = [] self.tagmanager = tagmanager self.on_files_changed = None # callback do GUI + self.config = load_config() def append(self, folder: Path) -> None: self.folders.append(folder) + self.config["last_folder"] = str(folder) + save_config(self.config) + + ignore_patterns = self.config.get("ignore_patterns", []) for each in list_files(folder): - if each.name.endswith(".!tag"): # ignoruj metadata soubory + if each.name.endswith(".!tag"): continue + + full_path = each.as_posix() # celá cesta jako string + + # kontrolujeme jméno i celou cestu + if any( + fnmatch.fnmatch(each.name, pat) or fnmatch.fnmatch(full_path, pat) + for pat in ignore_patterns + ): + continue + file_obj = File(each, self.tagmanager) self.filelist.append(file_obj) - if self.on_files_changed: - self.on_files_changed(self.filelist) - 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: @@ -87,4 +101,4 @@ class FileManager: 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 + return filtered \ No newline at end of file diff --git a/src/core/image.py b/src/core/image.py deleted file mode 100644 index d801316..0000000 --- a/src/core/image.py +++ /dev/null @@ -1,14 +0,0 @@ -# Module header -import sys - -if __name__ == "__main__": - sys.exit("This module is not intended to be executed as the main program.") - -# Imports -from PIL import Image, ImageTk - -# Functions -def load_icon(path) -> ImageTk.PhotoImage: - img = Image.open(path) - img = img.resize((16, 16), Image.Resampling.LANCZOS) - return ImageTk.PhotoImage(img) \ No newline at end of file diff --git a/src/core/list_manager.py b/src/core/list_manager.py index b1b3fea..341de49 100644 --- a/src/core/list_manager.py +++ b/src/core/list_manager.py @@ -17,4 +17,4 @@ class ListManager: # 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) + return sorted(files, key=date_key) \ No newline at end of file diff --git a/src/core/media_utils.py b/src/core/media_utils.py new file mode 100644 index 0000000..fde1a47 --- /dev/null +++ b/src/core/media_utils.py @@ -0,0 +1,42 @@ +# Module header +import sys +from .file import File +from .tag_manager import TagManager + +if __name__ == "__main__": + sys.exit("This module is not intended to be executed as the main program.") + +# Imports +from PIL import Image, ImageTk + +# Functions +def load_icon(path) -> ImageTk.PhotoImage: + img = Image.open(path) + img = img.resize((16, 16), Image.Resampling.LANCZOS) + return ImageTk.PhotoImage(img) + +def add_video_resolution_tag(file_obj: File, tagmanager: TagManager): + """ + Zjistí vertikální rozlišení videa a přiřadí tag Rozlišení/{výška}p. + Vyžaduje ffprobe (FFmpeg). + """ + path = str(file_obj.file_path) + try: + # ffprobe vrátí width a height ve formátu JSON + result = subprocess.run( + ["ffprobe", "-v", "error", "-select_streams", "v:0", + "-show_entries", "stream=width,height", "-of", "csv=p=0:s=x", path], + capture_output=True, + text=True, + check=True + ) + res = result.stdout.strip() # např. "1920x1080" + if "x" not in res: + return + width, height = map(int, res.split("x")) + tag_name = f"Rozlišení/{height}p" + tag_obj = tagmanager.add_tag("Rozlišení", f"{height}p") + file_obj.add_tag(tag_obj) + print(f"Přiřazen tag {tag_name} k {file_obj.filename}") + except Exception as e: + print(f"Chyba při získávání rozlišení videa {file_obj.filename}: {e}") \ No newline at end of file diff --git a/src/core/tag.py b/src/core/tag.py index 5316cb0..3858c22 100644 --- a/src/core/tag.py +++ b/src/core/tag.py @@ -19,4 +19,4 @@ class Tag: return False def __hash__(self): - return hash((self.category, self.name)) + return hash((self.category, self.name)) \ No newline at end of file diff --git a/src/core/tag_manager.py b/src/core/tag_manager.py index 8049747..4bd3ef2 100644 --- a/src/core/tag_manager.py +++ b/src/core/tag_manager.py @@ -33,4 +33,4 @@ class TagManager: return list(self.tags_by_category.keys()) def get_tags_in_category(self, category: str): - return list(self.tags_by_category.get(category, [])) + return list(self.tags_by_category.get(category, [])) \ No newline at end of file diff --git a/src/core/utils.py b/src/core/utils.py index 76d8dbb..98edf75 100644 --- a/src/core/utils.py +++ b/src/core/utils.py @@ -4,4 +4,4 @@ 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()] + return [file_path for file_path in folder.rglob("*") if file_path.is_file()] \ No newline at end of file diff --git a/src/ui/gui.py b/src/ui/gui.py index 48f09ff..7a529e0 100644 --- a/src/ui/gui.py +++ b/src/ui/gui.py @@ -6,13 +6,16 @@ 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.media_utils 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 +from src.core.config import save_config # <-- doplněno + + class TagSelectionDialog(tk.Toplevel): @@ -40,7 +43,7 @@ class TagSelectionDialog(tk.Toplevel): 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) + tk.Button(btn_frame, text="Zrušit", command=self.destroy).pack(side="left", padx=5) self.transient(parent) self.grab_set() @@ -127,7 +130,7 @@ class MultiFileTagAssignDialog(tk.Toplevel): class App: def __init__(self, filehandler: FileManager, tagmanager: TagManager): - self.states = {} # tree states (checkboxy) item_id -> bool + self.states = {} self.listbox_map: dict[int, list[File]] = {} self.selected_tree_item_for_context = None self.selected_list_index_for_context = None @@ -135,15 +138,48 @@ class App: self.tagmanager = tagmanager self.list_manager = ListManager() - # nové proměnné + # tady jen připravíme proměnnou, ale nevytváříme BooleanVar! + self.hide_ignored_var = None + 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 + def detect_video_resolution(self): + files = self.get_selected_files_objects() + if not files: + self.status_bar.config(text="Nebyly vybrány žádné soubory") + return + + 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}") + + self.update_files_from_manager(self.filehandler.filelist) + self.status_bar.config(text=f"Přiřazeno rozlišení tagů k {count} souborům") + + # ================================================== # MAIN GUI # ================================================== @@ -153,6 +189,16 @@ class App: root.geometry(APP_VIEWPORT) self.root = root + # teď už máme root, takže můžeme vytvořit BooleanVar + self.hide_ignored_var = tk.BooleanVar(value=False, master=root) + + last = self.filehandler.config.get("last_folder") + if last: + try: + self.filehandler.append(Path(last)) + except Exception: + pass + # ---- Ikony unchecked = load_icon("src/resources/images/32/32_unchecked.png") checked = load_icon("src/resources/images/32/32_checked.png") @@ -165,12 +211,27 @@ class App: # ---- 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_command(label="Nastavit ignorované vzory", command=self.set_ignore_patterns) file_menu.add_separator() file_menu.add_command(label="Exit", command=root.quit) - menu_bar.add_cascade(label="File", menu=file_menu) + + view_menu = tk.Menu(menu_bar, tearoff=0) + view_menu.add_checkbutton( + label="Skrýt ignorované", + variable=self.hide_ignored_var, + command=self.toggle_hide_ignored + ) + function_menu = tk.Menu(menu_bar, tearoff=0) + function_menu.add_command(label="Nastavit datum", command=self.set_date_for_selected) + function_menu.add_command(label="Detekovat rozlišení u videí", command=self.detect_video_resolution) + + + menu_bar.add_cascade(label="Soubor", menu=file_menu) + menu_bar.add_cascade(label="Pohled", menu=view_menu) + menu_bar.add_cascade(label="Funkce", menu=function_menu) main_frame = tk.Frame(root) main_frame.pack(fill="both", expand=True) @@ -192,7 +253,7 @@ class App: # 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.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) @@ -224,14 +285,14 @@ class App: # ---- 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.tree_menu.add_command(label="Nový štítek", command=self.tree_add_tag) + self.tree_menu.add_command(label="Smazat štítek", 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) - 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) + self.list_menu.add_command(label="Nastavit datum", command=self.set_date_for_selected) + self.list_menu.add_command(label="Přiřadit štítek", command=self.assign_tag_to_selected_bulk) # ---- Root node root_id = self.tree.insert("", "end", text="Štítky", image=self.icons["tag"]) @@ -244,9 +305,23 @@ class App: root.mainloop() + # ================================================== # FILTER + SORT TOGGLES # ================================================== + def set_ignore_patterns(self): + current = ", ".join(self.filehandler.config.get("ignore_patterns", [])) + s = simpledialog.askstring("Ignore patterns", "Zadej patterny oddělené čárkou (např. *.png, *.tmp):", initialvalue=current) + if s is None: + return + patterns = [p.strip() for p in s.split(",") if p.strip()] + self.filehandler.config["ignore_patterns"] = patterns + save_config(self.filehandler.config) + self.update_files_from_manager(self.filehandler.filelist) + + def toggle_hide_ignored(self): + self.update_files_from_manager(self.filehandler.filelist) + def on_filter_changed(self): self.filter_text = self.filter_entry.get().strip().lower() self.update_files_from_manager(self.filehandler.filelist) @@ -285,6 +360,14 @@ class App: (self.show_full_path and self.filter_text in str(f.file_path).lower()) ] + 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} + ] + + + # řazení reverse = (self.sort_order == "desc") if self.sort_mode == "name":