Scoring sorted
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -5,5 +5,6 @@ build
|
||||
.claude
|
||||
|
||||
# Config a temp soubory
|
||||
config.json
|
||||
*.!tag
|
||||
*.!tag
|
||||
*.!ftag
|
||||
*.!gtag
|
||||
@@ -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ů
|
||||
|
||||
515
PROJECT_NOTES.md
515
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 <noreply@anthropic.com>
|
||||
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
||||
```
|
||||
|
||||
**Types:**
|
||||
@@ -504,89 +398,9 @@ Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
|
||||
- `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 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 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 <noreply@anthropic.com>"
|
||||
|
||||
---
|
||||
|
||||
## 📊 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 <noreply@anthropic.com>"
|
||||
|
||||
---
|
||||
|
||||
## 🔍 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
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
"ignored": false,
|
||||
"tags": [
|
||||
"Rozlišení/4K",
|
||||
"Barva/🟣 Fialová"
|
||||
"Barva/🟣 Fialová",
|
||||
" Test/aha",
|
||||
"Hodnocení/⭐⭐⭐⭐⭐"
|
||||
],
|
||||
"date": "2025-09-15"
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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, []))
|
||||
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
|
||||
@@ -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
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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"""
|
||||
|
||||
Reference in New Issue
Block a user