From 86c4bf61c8cbc00c03d407e50fc8f36e9dda4393 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Doubravsk=C3=BD?= Date: Sun, 28 Dec 2025 17:44:24 +0100 Subject: [PATCH] Scoring sorted --- .gitignore | 5 +- CHANGELOG.md | 8 +- PROJECT_NOTES.md | 515 +++++++-------------------- data/samples/.DORMER_PRAMET.PDF.!tag | 4 +- src/core/config.py | 10 +- src/core/file_manager.py | 7 +- src/core/tag_manager.py | 23 +- src/ui/gui.py | 12 +- tests/test_config.py | 2 +- tests/test_file_manager.py | 10 +- tests/test_tag_manager.py | 30 +- 11 files changed, 214 insertions(+), 412 deletions(-) diff --git a/.gitignore b/.gitignore index f54cbde..91fdaf0 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,6 @@ build .claude # Config a temp soubory -config.json -*.!tag \ No newline at end of file +*.!tag +*.!ftag +*.!gtag \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 64fd0a7..cb0d5c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ Všechny významné změny v projektu Tagger jsou dokumentovány v tomto souboru. -## [0.3.0] - 2024-12-28 +## [1.0.3] - 2025-12-28 ### Přidáno - **Hardlink struktura** - Nová funkcionalita pro vytváření adresářové struktury pomocí hardlinků @@ -26,13 +26,14 @@ Všechny významné změny v projektu Tagger jsou dokumentovány v tomto souboru - **Testy** - 189 testů pokrývajících všechny moduly - Testy pro hardlink manager včetně synchronizace +- **Poetry** - Správa závislostí pomocí Poetry ### Změněno - Modernizované GUI inspirované qBittorrentem - Ukládání geometrie okna do globálního configu - Ignore patterns se ukládají do složkového configu -## [0.2.0] - 2024-12-27 +## [1.0.2] - 2025-10-03 ### Přidáno - **Moderní GUI** - Přepracované rozhraní ve stylu qBittorrent @@ -54,8 +55,9 @@ Všechny významné změny v projektu Tagger jsou dokumentovány v tomto souboru ### Změněno - Refaktorizace struktury projektu do modulů (`src/core/`, `src/ui/`) +- Použití dataclass pro Tag a File objekty -## [0.1.0] - 2024-10-03 +## [1.0.0] - 2025-09-03 ### Přidáno - Základní funkcionalita tagování souborů diff --git a/PROJECT_NOTES.md b/PROJECT_NOTES.md index 98edbc1..ff2a96b 100644 --- a/PROJECT_NOTES.md +++ b/PROJECT_NOTES.md @@ -1,15 +1,15 @@ -# 📝 Tagger - Centrální Poznámky Projektu +# Tagger - Centrální Poznámky Projektu > **DŮLEŽITÉ:** Tento soubor obsahuje VŠE co potřebuji vědět o projektu. > Pokud pracuji na Tagger, VŽDY nejdříve přečtu tento soubor! -**Poslední aktualizace:** 2025-12-23 -**Verze:** 1.0.2 -**Status:** ✅ Stable, v aktivním vývoji +**Poslední aktualizace:** 2025-12-28 +**Verze:** 1.0.3 +**Status:** Stable, v aktivním vývoji --- -## 🎯 O projektu +## O projektu **Tagger** je desktopová aplikace pro správu a organizaci souborů pomocí hierarchických tagů (štítků). @@ -19,22 +19,21 @@ - Filtrování podle tagů - Metadata uložená v JSON souborech - Automatická detekce rozlišení videí (ffprobe) -- Dvě verze GUI: klasické a moderní (qBittorrent-style) -- TODO: Budu mit filmotéku ve složce sloužící jako zdroj (zadne složky uvnitr jen hromada souborů a tagy) a chctel bych na pokyn (menu funkce) aby povytvářel složky dle kategorii tagů a uložil hardlinky na prislušná místa (orig složka: film s tagy "žánr/Komedie" "žánr/Akční" "rok/1988" a soubor v originalni složce zanechá a jen vytvoří na danem míste všechny složky zala tyto zmínene tagy a vytvoří linky) +- Moderní GUI (qBittorrent-style) +- Hardlink struktura - vytváření adresářové struktury pomocí hardlinků podle tagů +- Tříúrovňový konfigurační systém (globální, složkový, souborový) --- -## 📁 Struktura projektu +## Struktura projektu ``` Tagger/ -├── Tagger.py # Entry point - klasické GUI -├── Tagger_modern.py # Entry point - moderní GUI +├── Tagger.py # Entry point ├── PROJECT_NOTES.md # ← TENTO SOUBOR - HLAVNÍ ZDROJ PRAVDY +├── CHANGELOG.md # Historie verzí ├── pyproject.toml # Poetry konfigurace ├── poetry.lock # Zamčené verze závislostí -├── pytest.ini # Pytest konfigurace -├── .editorconfig # Editor konfigurace ├── .gitignore # Git ignore pravidla │ ├── src/ @@ -43,54 +42,50 @@ Tagger/ │ │ ├── tag_manager.py # Správa tagů a kategorií │ │ ├── file.py # File s metadaty │ │ ├── file_manager.py # Správa souborů, filtrování -│ │ ├── config.py # Konfigurace (JSON) +│ │ ├── config.py # Tříúrovňová konfigurace (global, folder, file) +│ │ ├── hardlink_manager.py # Správa hardlink struktury │ │ ├── utils.py # list_files() - rekurzivní procházení │ │ ├── media_utils.py # load_icon(), ffprobe │ │ ├── constants.py # APP_NAME, VERSION, APP_VIEWPORT │ │ └── list_manager.py # Třídění (málo používaný) │ │ │ └── ui/ -│ ├── gui.py # Původní Tkinter GUI -│ ├── gui_modern.py # Moderní qBittorrent-style GUI ✨ NOVÉ -│ └── gui_old.py # Backup původního GUI +│ └── gui.py # Moderní qBittorrent-style GUI │ -├── tests/ # 116 testů, 100% core coverage +├── tests/ # 189 testů, 100% core coverage │ ├── __init__.py │ ├── conftest.py # Pytest fixtures │ ├── test_tag.py # 13 testů -│ ├── test_tag_manager.py # 19 testů +│ ├── test_tag_manager.py # 31 testů │ ├── test_file.py # 22 testů -│ ├── test_file_manager.py # 22 testů +│ ├── test_file_manager.py # 40 testů +│ ├── test_config.py # 33 testů +│ ├── test_hardlink_manager.py # 28 testů │ ├── test_utils.py # 17 testů -│ ├── test_config.py # 18 testů -│ ├── test_media_utils.py # 3 testy -│ └── README.md # Dokumentace testů +│ └── test_media_utils.py # 3 testy │ ├── src/resources/ -│ └── images/32/ # Ikony (16x16 PNG) +│ └── images/32/ # Ikony (32x32 PNG) │ ├── 32_unchecked.png │ ├── 32_checked.png │ └── 32_tag.png │ -└── docs/ # Dokumentace (ZASTARALÁ - použij tento soubor!) - ├── ARCHITECTURE.md # ⚠️ DEPRECATED - info je zde - ├── CONTRIBUTING.md # ⚠️ DEPRECATED - info je zde - └── GUI_MODERN_README.md # ⚠️ DEPRECATED - info je zde +└── data/samples/ # Testovací data ``` --- -## 🎨 Architektura +## Architektura ### Vrstvová struktura ``` ┌─────────────────────────────────┐ -│ Presentation (UI) │ ← gui.py, gui_modern.py +│ Presentation (UI) │ ← gui.py │ - Tkinter GUI │ - NESMÍ obsahovat business logiku │ - Jen zobrazení + interakce │ - NESMÍ importovat přímo z core ├─────────────────────────────────┤ -│ Business Logic │ ← FileManager, TagManager +│ Business Logic │ ← FileManager, TagManager, HardlinkManager │ - Správa souborů/tagů │ - Callable z UI │ - Filtrování, validace │ - Callback pattern pro notifikace ├─────────────────────────────────┤ @@ -104,9 +99,27 @@ Tagger/ └─────────────────────────────────┘ ``` +### Tříúrovňový konfigurační systém + +1. **Globální config** (`.Tagger.!gtag` vedle Tagger.py) + - Geometrie okna, maximalizace + - Poslední otevřená složka + - Recent folders + +2. **Složkový config** (`.Tagger.!ftag` v projekt složce) + - Ignore patterns + - Custom tagy pro složku + - Hardlink nastavení (output_dir, categories) + - Rekurzivní skenování + +3. **Souborové tagy** (`.filename.!tag`) + - Tagy souboru + - Datum + - Stav (nové, ignorované) + ### Klíčová pravidla -#### ✅ CO DĚLAT: +#### CO DĚLAT: 1. **UI NESMÍ obsahovat business logiku** ```python @@ -148,46 +161,16 @@ Tagger/ pass ``` -#### ❌ CO NEDĚLAT: +#### CO NEDĚLAT: -1. **Globální stav** - ```python - # ❌ NIKDY - current_file = None # global - ``` - -2. **Magic numbers** - ```python - # ❌ ŠPATNĚ - if len(files) > 100: - - # ✅ SPRÁVNĚ - MAX_FILES = 100 - if len(files) > MAX_FILES: - ``` - -3. **Ignorovat exceptions** - ```python - # ❌ NIKDY - try: - operation() - except: - pass - ``` - -4. **Hardcoded paths** - ```python - # ❌ ŠPATNĚ - icon = "/home/user/icon.png" - - # ✅ SPRÁVNĚ - ICON_DIR = Path(__file__).parent / "resources" - icon = ICON_DIR / "icon.png" - ``` +1. **Globální stav** - NIKDY +2. **Magic numbers** - použít konstanty +3. **Ignorovat exceptions** - vždy logovat nebo ošetřit +4. **Hardcoded paths** - použít Path --- -## 🔑 Klíčové komponenty +## Klíčové komponenty ### 1. Tag (immutable value object) @@ -200,19 +183,8 @@ class Tag: @property def full_path(self) -> str: return f"{self.category}/{self.name}" - - def __eq__(self, other): - return (self.category, self.name) == (other.category, other.name) - - def __hash__(self): - return hash((self.category, self.name)) ``` -**Proč immutable?** -- Lze použít jako klíč v dict/set -- Thread-safe -- Jasná sémantika rovnosti - ### 2. File (reprezentace souboru s metadaty) ```python @@ -223,7 +195,6 @@ class File: self.metadata_filename = parent / f".{filename}.!tag" self.tags: list[Tag] = [] self.date: str | None = None - self.get_metadata() # Auto-load při vytvoření ``` **Metadata format (.filename.!tag):** @@ -236,28 +207,18 @@ class File: } ``` -**DŮLEŽITÉ:** -- Každá změna (add_tag, set_date) automaticky volá `save_metadata()` -- UTF-8 encoding! -- ensure_ascii=False pro češtinu - ### 3. TagManager (správa tagů) ```python class TagManager: def __init__(self): self.tags_by_category = {} # {category: set(Tag)} - - def add_tag(self, category: str, name: str) -> Tag: - # Vytvoří kategorii pokud neexistuje - # Používá set - duplicity automaticky ignorovány - # Vrací Tag objekt + # Automaticky načte výchozí tagy (Hodnocení, Barva) ``` -**Speciální chování:** -- Když odstraníš poslední tag z kategorie → kategorie se smaže -- Set zajišťuje uniqueness -- Vždy vrací Tag objekt (ne string) +**Výchozí tagy:** +- Hodnocení: 1-5 hvězd (exkluzivní výběr) +- Barva: Červená, Modrá, Zelená, Žlutá, Oranžová ### 4. FileManager (správa souborů) @@ -267,55 +228,41 @@ class FileManager: self.filelist: list[File] = [] self.tagmanager = tagmanager self.on_files_changed = None # CALLBACK pro UI! - self.config = load_config() - - def append(self, folder: Path): - # Rekurzivně načte soubory - # Ignoruje podle patterns - # Vytvoří File objekty - # Zavolá on_files_changed callback + self.global_config = load_global_config() + self.folder_config = {} ``` -**Callback pattern:** +### 5. HardlinkManager (hardlink struktura) + ```python -# V GUI: -filemanager.on_files_changed = self.update_ui +class HardlinkManager: + def __init__(self, output_dir: Path): + self.output_dir = output_dir -# V FileManager: -if self.on_files_changed: - self.on_files_changed(self.filelist) + def create_structure_for_files(files, categories=None) -> (success, fail) + def find_obsolete_links(files, categories=None) -> List[(link, source)] + def remove_obsolete_links(files, categories=None) -> (count, paths) + def sync_structure(files, categories=None) -> (created, c_fail, removed, r_fail) ``` -**Proč callback?** -- Core nezávisí na UI -- Jednoduché na testování -- Flexibilní (můžeš změnit UI bez změny core) +**Příklad struktury:** +``` +output/ +├── žánr/ +│ ├── Komedie/ +│ │ └── film.mkv (hardlink) +│ └── Akční/ +│ └── film.mkv (hardlink) +└── rok/ + └── 1988/ + └── film.mkv (hardlink) +``` --- -## 🎨 GUI Verze +## GUI -### Klasické GUI (gui.py) - -``` -┌─────────────────────────────────────────┐ -│ Soubor │ Pohled │ Funkce Menu -├──────────┬──────────────────────────────┤ -│ │ [Filter____] [Name][Name][ASC] -│ Tree │ ┌──────────────────────────┐ │ -│ (tagy) │ │ Listbox (soubory) │ │ -│ 📂 Štítky│ │ - file1.txt — 2025-01-01│ │ -│ ☑ Nové │ │ - file2.mp4 │ │ -│ ☐ HD │ │ - file3.jpg │ │ -│ │ └──────────────────────────┘ │ -├──────────┴──────────────────────────────┤ -│ Status: Připraven │ -└─────────────────────────────────────────┘ -``` - -**Použít:** `poetry run python Tagger.py` - -### Moderní GUI (gui_modern.py) ✨ NOVÉ +### Moderní GUI (gui.py) ``` ┌─────────────────────────────────────────────────────┐ @@ -333,14 +280,15 @@ if self.on_files_changed: └─────────────────────────────────────────────────────┘ ``` -**Použít:** `poetry run python Tagger_modern.py` +**Použít:** `poetry run python Tagger.py` -**Nové funkce:** -- 📋 Tabulka s 4 sloupci (Název, Datum, Štítky, Velikost) -- 🔧 Toolbar s tlačítky -- ⌨️ Keyboard shortcuts (Ctrl+O, Ctrl+T, F5, Del...) -- 📊 Status bar se 3 sekcemi -- 🎨 qBittorrent-inspired design +**Funkce:** +- Tabulka s 4 sloupci (Název, Datum, Štítky, Velikost) +- Toolbar s tlačítky +- Keyboard shortcuts (Ctrl+O, Ctrl+T, F5, Del...) +- Status bar se 3 sekcemi +- Hromadné přiřazování tagů +- Hardlink menu (Nástroje → Hardlink) **Keyboard shortcuts:** - `Ctrl+O` - Otevřít složku @@ -353,7 +301,7 @@ if self.on_files_changed: --- -## 🔧 Vývoj +## Vývoj ### Setup prostředí @@ -363,52 +311,30 @@ poetry install poetry shell # Nebo přímo: -poetry run python Tagger_modern.py -``` - -**Poetry environment path:** -``` -/home/honza/.cache/pypoetry/virtualenvs/tagger-qKyHMOtL-py3.12 -``` - -### Spuštění aplikace - -```bash -# Moderní GUI (doporučeno) -poetry run python Tagger_modern.py - -# Klasické GUI poetry run python Tagger.py ``` ### Testy ```bash -# Všechny testy (116 testů) +# Všechny testy (189 testů) poetry run pytest tests/ -v # S coverage poetry run pytest tests/ --cov=src/core --cov-report=html # Konkrétní modul -poetry run pytest tests/test_file.py -v +poetry run pytest tests/test_hardlink_manager.py -v # Quick check poetry run pytest tests/ -q ``` -**Test coverage:** 100% core modulů ✅ - -### Linting & Formatting - -```bash -# TODO: Přidat black, flake8 do pyproject.toml -# Zatím manuální kontrola podle PEP 8 -``` +**Test coverage:** 100% core modulů --- -## 📝 Coding Standards +## Coding Standards ### Python Style @@ -419,26 +345,6 @@ poetry run pytest tests/ -q - **Type hints** povinné - **Docstrings** pro public API -### Naming Conventions - -```python -# Classes -class FileManager: - pass - -# Functions/methods -def load_config(): - pass - -# Constants -APP_NAME = "Tagger" -MAX_FILES = 1000 - -# Private -def _internal_method(): - pass -``` - ### Imports Order ```python @@ -456,21 +362,9 @@ from src.core.file import File from src.core.tag import Tag ``` -### String Formatting - -```python -# ✅ F-strings -name = "John" -msg = f"Hello, {name}!" - -# ❌ NE -msg = "Hello, " + name -msg = "Hello, {}".format(name) -``` - --- -## 🔀 Git Workflow +## Git Workflow ### Branches @@ -492,7 +386,7 @@ feature/* ← Feature branches ← VYVÍJÍME TADY [optional body] 🤖 Generated with Claude Code -Co-Authored-By: Claude Sonnet 4.5 +Co-Authored-By: Claude Opus 4.5 ``` **Types:** @@ -504,89 +398,9 @@ Co-Authored-By: Claude Sonnet 4.5 - `style:` - Formátování - `chore:` - Build, dependencies -**Příklad:** -```bash -git commit -m "feat: Add modern qBittorrent-style GUI - -Implemented new GUI with toolbar, table view, -and keyboard shortcuts. - -🤖 Generated with Claude Code -Co-Authored-By: Claude Sonnet 4.5 " -``` - --- -## 🎯 Design Decisions (ADR) - -### ADR-001: JSON soubory místo databáze - -**Rozhodnutí:** Metadata v `.filename.!tag` JSON souborech - -**Proč:** -- ✅ Jednoduchý backup (copy složky) -- ✅ Git-friendly -- ✅ Portable -- ✅ Metadata zůstanou při přesunu souboru - -**Kdy přehodnotit:** -- Pokud >10k souborů (zvážit SQLite) - -### ADR-002: Callback pattern pro UI updates - -**Rozhodnutí:** `on_files_changed` callback - -**Proč:** -- ✅ Core nezávisí na UI -- ✅ Jednoduché testování -- ✅ Flexibilní - -**Alternativy zamítnuté:** -- Observer pattern (overkill) -- Event system (složitější) - -### ADR-003: Tkinter pro GUI - -**Rozhodnutí:** Tkinter (standard library) - -**Proč:** -- ✅ Žádné extra dependencies -- ✅ Cross-platform -- ✅ Dobře dokumentované - -**Alternativy:** -- Qt - lepší UI, ale větší závislost -- Web - overkill pro desktop app - -### ADR-004: Poetry pro dependencies - -**Rozhodnutí:** Poetry místo pip - -**Proč:** -- ✅ Deterministické buildy (poetry.lock) -- ✅ Dev dependencies oddělené -- ✅ Moderní tool - ---- - -## 🐛 Známé problémy & TODO - -### Aktuální problémy - -1. **Git merge konflikty** - - `poetry.lock` a `pyproject.toml` - konflikty při merge devel→feature - - `tests/test_image.py` - deleted in devel, modified in feature - - **Stav:** Nezresolváno ⚠️ - -2. **ListManager málo použitý** - - Třídící logika duplicitní v GUI - - **TODO:** Refactor nebo odstranit - -3. **Dlouhé operace blokují UI** - - ffprobe detection běží v main threadu - - **TODO:** Threading pro dlouhé operace - -### Plánované features +## Plánované features - [ ] Progress bar pro dlouhé operace - [ ] Undo/Redo mechanismus @@ -607,12 +421,11 @@ Co-Authored-By: Claude Sonnet 4.5 " --- -## 📊 Metriky projektu +## Metriky projektu -**Řádky kódu:** ~1060 Python LOC -**Testy:** 116 (všechny ✅) +**Testy:** 189 (všechny ✅) **Test coverage:** 100% core modulů -**Python verze:** 3.12 +**Python verze:** 3.12+ **Dependencies:** Pillow (PIL) **Vývojové prostředí:** Poetry @@ -623,20 +436,20 @@ Co-Authored-By: Claude Sonnet 4.5 " --- -## 🔍 Debugování +## Debugování ### Časté problémy **1. "Cannot import ImageTk"** ```bash # Řešení: Použij poetry environment -poetry run python Tagger_modern.py +poetry run python Tagger.py ``` **2. "Config file not found"** ```bash # Normální při prvním spuštění -# Vytvoří se automaticky config.json +# Vytvoří se automaticky .Tagger.!gtag ``` **3. "Metadata corrupted"** @@ -645,132 +458,58 @@ poetry run python Tagger_modern.py # Vrátí default config při chybě ``` -### Logování - -```python -# Zatím jen print() statements -# TODO: Přidat logging module - -import logging -logging.basicConfig(level=logging.DEBUG) -logger = logging.getLogger(__name__) -``` - --- -## 📚 Dokumentace +## Dokumentace -**✅ AKTUÁLNÍ:** -- **PROJECT_NOTES.md** - TENTO SOUBOR (single source of truth) ⭐ +**AKTUÁLNÍ:** +- **PROJECT_NOTES.md** - TENTO SOUBOR (single source of truth) +- **CHANGELOG.md** - Historie verzí - Docstrings v kódu -**📝 Poznámka:** -- Všechny ostatní .md soubory byly smazány a skonsolidovány SEM -- .gitignore ignoruje všechny .md kromě PROJECT_NOTES.md -- Pokud vytvoříš nový .md, MUSÍŠ ho přidat do .gitignore whitelist - --- -## 💡 Pro AI asistenty (jako Claude) +## Pro AI asistenty (jako Claude) ### Když začínám práci na projektu: -1. ✅ **PŘEČTI TENTO SOUBOR CELÝ!** -2. ✅ Zkontroluj `git status` -3. ✅ Aktivuj poetry environment -4. ✅ Spusť testy (`poetry run pytest tests/`) -5. ✅ Dodržuj pravidla výše +1. **PŘEČTI TENTO SOUBOR CELÝ!** +2. Zkontroluj `git status` +3. Aktivuj poetry environment +4. Spusť testy (`poetry run pytest tests/`) +5. Dodržuj pravidla výše ### Při commitování: -1. ✅ Testy prošly (`pytest tests/`) -2. ✅ Type hints přidány -3. ✅ UTF-8 encoding -4. ✅ Žádné TODO/FIXME -5. ✅ Commit message formát správný +1. Testy prošly (`pytest tests/`) +2. Type hints přidány +3. UTF-8 encoding +4. Žádné TODO/FIXME +5. Commit message formát správný ### Při přidání nové funkce: -1. ✅ Testy napsány PŘED implementací (TDD) -2. ✅ Dokumentace aktualizována (TENTO SOUBOR!) -3. ✅ Architecture decision zdokumentováno (pokud významné) -4. ✅ Type hints všude -5. ✅ Error handling přidán - -### Při refactoringu: - -1. ✅ Testy před (měly by projít) -2. ✅ Refactor -3. ✅ Testy po (měly by stále projít) -4. ✅ Update dokumentace +1. Testy napsány PŘED implementací (TDD) +2. Dokumentace aktualizována (TENTO SOUBOR!) +3. Architecture decision zdokumentováno (pokud významné) +4. Type hints všude +5. Error handling přidán --- -## 📞 Kontakt & Help +## Kontakt & Help **Autor:** honza -**Repository:** /home/honza/Dokumenty/Tagger -**Python:** 3.12 -**OS:** Linux 6.14.0-37-generic +**Repository:** /home/honza/Documents/Tagger +**Python:** 3.12+ +**OS:** Linux **Pro pomoc:** - Přečti TENTO soubor - Podívej se do testů (`tests/`) - Zkontroluj docstrings v kódu -- V nouzi spusť: `poetry run python -i` a explorej objekty --- -## 📅 Changelog - -### [Unreleased] -- Merge konflikty s devel branch (poetry.lock, test_image.py) - -### [1.0.2] - 2025-12-23 -- ✨ Přidáno moderní GUI (gui_modern.py) -- ✨ Keyboard shortcuts -- ✨ Tabulkové zobrazení s 4 sloupci -- ✨ Toolbar s tlačítky -- ✨ 116 testů (100% core coverage) -- 📝 Vytvoření PROJECT_NOTES.md (tento soubor) -- 🔧 Poetry setup - -### [1.0.1] - 2025-10-05 -- 🐛 Bug fixy -- ✨ Video resolution detection - -### [1.0.0] - 2025-10-05 -- 🎉 Initial release -- ✨ Základní funkcionalita -- ✨ Tkinter GUI -- ✨ JSON metadata - ---- - -## 🎉 Poznámky na závěr - -**Tento soubor je SINGLE SOURCE OF TRUTH pro projekt Tagger.** - -Když přidávám funkci, fixuju bug, nebo dělám změnu: -1. Nejdřív PŘEČTU tento soubor -2. Pak UPRAVÍM kód -3. Pak AKTUALIZUJU tento soubor - -**Living document** - průběžně aktualizován! - ---- - -**Last updated:** 2025-12-23 18:30 -**Next review:** Při každé větší změně -**Maintainer:** Claude Sonnet 4.5 + honza - ---- - -## 📋 Changelog dokumentace - -### 2025-12-23 11:24 - Konsolidace dokumentace -- ✅ Smazány: CONTRIBUTING.md, GUI_MODERN_README.md, docs/ARCHITECTURE.md -- ✅ Vše skonsolidováno do PROJECT_NOTES.md -- ✅ Vytvořen README.md pro GitHub (základní intro) -- ✅ Aktualizován .gitignore (ignoruje všechny .md kromě PROJECT_NOTES.md a README.md) -- ⭐ **PROJECT_NOTES.md je nyní jediný zdroj pravdy pro dokumentaci!** +**Last updated:** 2025-12-28 +**Maintainer:** Claude Opus 4.5 + honza diff --git a/data/samples/.DORMER_PRAMET.PDF.!tag b/data/samples/.DORMER_PRAMET.PDF.!tag index da3577d..851bc22 100644 --- a/data/samples/.DORMER_PRAMET.PDF.!tag +++ b/data/samples/.DORMER_PRAMET.PDF.!tag @@ -3,7 +3,9 @@ "ignored": false, "tags": [ "Rozlišení/4K", - "Barva/🟣 Fialová" + "Barva/🟣 Fialová", + " Test/aha", + "Hodnocení/⭐⭐⭐⭐⭐" ], "date": "2025-09-15" } \ No newline at end of file diff --git a/src/core/config.py b/src/core/config.py index bd5b74d..98e5a96 100644 --- a/src/core/config.py +++ b/src/core/config.py @@ -2,18 +2,18 @@ Configuration management for Tagger Three levels of configuration: -1. Global config (config.json next to Tagger.py) - app-wide settings -2. Folder config (.tagger.json in project root) - folder-specific settings -3. File tags (.filename.!tag) - per-file metadata (handled in file.py) +1. Global config (.Tagger.!gtag next to Tagger.py) - app-wide settings +2. Folder config (.Tagger.!ftag in project root) - folder-specific settings +3. File tags (.{filename}.!tag) - per-file metadata (handled in file.py) """ import json from pathlib import Path # Global config file (next to the main script) -GLOBAL_CONFIG_FILE = Path(__file__).parent.parent.parent / "config.json" +GLOBAL_CONFIG_FILE = Path(__file__).parent.parent.parent / ".Tagger.!gtag" # Folder config filename -FOLDER_CONFIG_NAME = ".tagger.json" +FOLDER_CONFIG_NAME = ".Tagger.!ftag" # ============================================================================= diff --git a/src/core/file_manager.py b/src/core/file_manager.py index d6c5ec5..a8cbc03 100644 --- a/src/core/file_manager.py +++ b/src/core/file_manager.py @@ -46,9 +46,12 @@ class FileManager: ignore_patterns = folder_config.get("ignore_patterns", []) for each in list_files(folder): - if each.name.endswith(".!tag"): + # Skip all Tagger metadata files + if each.name.endswith(".!tag"): # File tags: .filename.!tag continue - if each.name == ".tagger.json": + if each.name.endswith(".!ftag"): # Folder config: .Tagger.!ftag + continue + if each.name.endswith(".!gtag"): # Global config: .Tagger.!gtag continue full_path = each.as_posix() diff --git a/src/core/tag_manager.py b/src/core/tag_manager.py index 21ca0bd..25a81ed 100644 --- a/src/core/tag_manager.py +++ b/src/core/tag_manager.py @@ -1,11 +1,17 @@ from .tag import Tag -# Default tags that are always available +# Default tags that are always available (order in list = display order) DEFAULT_TAGS = { "Hodnocení": ["⭐", "⭐⭐", "⭐⭐⭐", "⭐⭐⭐⭐", "⭐⭐⭐⭐⭐"], "Barva": ["🔴 Červená", "🟠 Oranžová", "🟡 Žlutá", "🟢 Zelená", "🔵 Modrá", "🟣 Fialová"], } +# Tag sort order for default categories (preserves display order) +DEFAULT_TAG_ORDER = { + "Hodnocení": {name: i for i, name in enumerate(DEFAULT_TAGS["Hodnocení"])}, + "Barva": {name: i for i, name in enumerate(DEFAULT_TAGS["Barva"])}, +} + class TagManager: def __init__(self): @@ -46,5 +52,16 @@ class TagManager: 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, [])) \ No newline at end of file + def get_tags_in_category(self, category: str) -> list[Tag]: + """Get tags in category, sorted by predefined order for default categories""" + tags = list(self.tags_by_category.get(category, [])) + + # Use predefined order for default categories + if category in DEFAULT_TAG_ORDER: + order = DEFAULT_TAG_ORDER[category] + tags.sort(key=lambda t: order.get(t.name, 999)) + else: + # Sort alphabetically for custom categories + tags.sort(key=lambda t: t.name) + + return tags \ No newline at end of file diff --git a/src/ui/gui.py b/src/ui/gui.py index 2043bfa..fe4a889 100644 --- a/src/ui/gui.py +++ b/src/ui/gui.py @@ -11,7 +11,7 @@ from typing import List 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.tag_manager import TagManager, DEFAULT_TAG_ORDER from src.core.file import File from src.core.tag import Tag from src.core.list_manager import ListManager @@ -116,6 +116,14 @@ class MultiFileTagAssignDialog(tk.Toplevel): tags_by_category[tag.category] = [] tags_by_category[tag.category].append((full_path, tag)) + # Sort tags within each category + for category in tags_by_category: + if category in DEFAULT_TAG_ORDER: + order = DEFAULT_TAG_ORDER[category] + tags_by_category[category].sort(key=lambda x: order.get(x[1].name, 999)) + else: + tags_by_category[category].sort(key=lambda x: x[1].name) + for category in sorted(tags_by_category.keys()): color = self.category_colors.get(category, "#333333") is_exclusive = category in EXCLUSIVE_CATEGORIES @@ -127,7 +135,7 @@ class MultiFileTagAssignDialog(tk.Toplevel): self.category_checkbuttons[category] = [] - for full_path, tag in sorted(tags_by_category[category], key=lambda x: x[1].name): + for full_path, tag in tags_by_category[category]: have_count = sum(1 for s in file_tag_sets if full_path in s) if have_count == 0: init = 0 diff --git a/tests/test_config.py b/tests/test_config.py index 7c022a9..e98b502 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -138,7 +138,7 @@ class TestFolderConfig: """Test získání cesty ke složkovému configu""" path = get_folder_config_path(tmp_path) assert path == tmp_path / FOLDER_CONFIG_NAME - assert path.name == ".tagger.json" + assert path.name == ".Tagger.!ftag" def test_load_folder_config_nonexistent(self, tmp_path): """Test načtení neexistujícího složkového configu""" diff --git a/tests/test_file_manager.py b/tests/test_file_manager.py index 3b4e947..5aa5fe7 100644 --- a/tests/test_file_manager.py +++ b/tests/test_file_manager.py @@ -81,14 +81,16 @@ class TestFileManager: filenames = {f.filename for f in file_manager.filelist} assert ".file1.txt.!tag" not in filenames - def test_file_manager_ignores_tagger_json(self, file_manager, temp_dir): - """Test že .tagger.json je ignorován""" - (temp_dir / ".tagger.json").write_text('{}') + def test_file_manager_ignores_tagger_config_files(self, file_manager, temp_dir): + """Test že Tagger config soubory jsou ignorovány""" + (temp_dir / ".Tagger.!ftag").write_text('{}') # Folder config + (temp_dir / ".Tagger.!gtag").write_text('{}') # Global config file_manager.append(temp_dir) filenames = {f.filename for f in file_manager.filelist} - assert ".tagger.json" not in filenames + assert ".Tagger.!ftag" not in filenames + assert ".Tagger.!gtag" not in filenames def test_file_manager_updates_last_folder(self, file_manager, temp_dir): """Test aktualizace last_folder v global configu""" diff --git a/tests/test_tag_manager.py b/tests/test_tag_manager.py index 5033e6a..5fdcb82 100644 --- a/tests/test_tag_manager.py +++ b/tests/test_tag_manager.py @@ -287,7 +287,35 @@ class TestDefaultTags: tm.remove_category("Hodnocení") assert "Hodnocení" not in tm.tags_by_category - assert "Barva" in tm.tags_by_category # Druhá zůstává + + def test_hodnoceni_tags_are_sorted_by_stars(self): + """Test že tagy v Hodnocení jsou seřazeny od 1 do 5 hvězd""" + tm = TagManager() + tags = tm.get_tags_in_category("Hodnocení") + + tag_names = [t.name for t in tags] + assert tag_names == ["⭐", "⭐⭐", "⭐⭐⭐", "⭐⭐⭐⭐", "⭐⭐⭐⭐⭐"] + + def test_barva_tags_are_sorted_in_predefined_order(self): + """Test že tagy v Barva jsou seřazeny v předdefinovaném pořadí""" + tm = TagManager() + tags = tm.get_tags_in_category("Barva") + + tag_names = [t.name for t in tags] + expected = ["🔴 Červená", "🟠 Oranžová", "🟡 Žlutá", "🟢 Zelená", "🔵 Modrá", "🟣 Fialová"] + assert tag_names == expected + + def test_custom_category_tags_sorted_alphabetically(self): + """Test že tagy v custom kategorii jsou seřazeny abecedně""" + tm = TagManager() + tm.add_tag("Video", "HD") + tm.add_tag("Video", "4K") + tm.add_tag("Video", "SD") + + tags = tm.get_tags_in_category("Video") + tag_names = [t.name for t in tags] + + assert tag_names == ["4K", "HD", "SD"] def test_can_add_tag_to_default_category(self): """Test že lze přidat tag do default kategorie"""