1589 lines
57 KiB
Python
1589 lines
57 KiB
Python
"""
|
|
Modern PySide6/Qt6 GUI for Tagger
|
|
"""
|
|
import os
|
|
import sys
|
|
import subprocess
|
|
import re
|
|
from pathlib import Path
|
|
from typing import List
|
|
|
|
from PySide6.QtWidgets import (
|
|
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
|
QSplitter, QTreeWidget, QTreeWidgetItem, QTableWidget, QTableWidgetItem,
|
|
QHeaderView, QMenu, QMenuBar, QToolBar, QStatusBar, QLabel,
|
|
QPushButton, QLineEdit, QCheckBox, QDialog, QDialogButtonBox,
|
|
QScrollArea, QFrame, QMessageBox, QInputDialog, QFileDialog,
|
|
QAbstractItemView, QSizePolicy
|
|
)
|
|
from PySide6.QtCore import Qt, QSize
|
|
from PySide6.QtGui import QAction, QIcon, QPixmap, QFont, QColor
|
|
|
|
from src.core.file_manager import FileManager
|
|
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.constants import APP_NAME, APP_VIEWPORT
|
|
from src.core.config import save_global_config
|
|
from src.core.hardlink_manager import HardlinkManager
|
|
|
|
|
|
# Color scheme
|
|
COLORS = {
|
|
"bg": "#ffffff",
|
|
"sidebar_bg": "#f5f5f5",
|
|
"toolbar_bg": "#f0f0f0",
|
|
"selected": "#0078d7",
|
|
"selected_text": "#ffffff",
|
|
"border": "#d0d0d0",
|
|
"status_bg": "#f8f8f8",
|
|
"text": "#000000",
|
|
}
|
|
|
|
# Tag category colors
|
|
TAG_COLORS = [
|
|
"#e74c3c", # red
|
|
"#3498db", # blue
|
|
"#2ecc71", # green
|
|
"#f39c12", # orange
|
|
"#9b59b6", # purple
|
|
"#1abc9c", # teal
|
|
"#e91e63", # pink
|
|
"#00bcd4", # cyan
|
|
]
|
|
|
|
# Fixed colors for default categories
|
|
DEFAULT_CATEGORY_COLORS = {
|
|
"Hodnocení": "#f1c40f", # gold/yellow for stars
|
|
"Barva": "#95a5a6", # gray for color category
|
|
}
|
|
|
|
# Categories where only one tag can be selected (exclusive/radio behavior)
|
|
EXCLUSIVE_CATEGORIES = {"Hodnocení"}
|
|
|
|
|
|
class MultiFileTagAssignDialog(QDialog):
|
|
"""Dialog for bulk tag assignment to multiple files"""
|
|
|
|
def __init__(self, parent, all_tags: List[Tag], files: List[File],
|
|
category_colors: dict = None):
|
|
super().__init__(parent)
|
|
self.setWindowTitle("Přiřadit tagy k vybraným souborům")
|
|
self.setMinimumSize(500, 600)
|
|
self.result = None
|
|
self.tags_by_full = {t.full_path: t for t in all_tags}
|
|
self.files = files
|
|
self.category_colors = category_colors or {}
|
|
self.checkboxes: dict[str, QCheckBox] = {}
|
|
self.category_checkboxes: dict[str, list] = {}
|
|
|
|
self._setup_ui()
|
|
|
|
def _setup_ui(self):
|
|
layout = QVBoxLayout(self)
|
|
|
|
# Header
|
|
header = QLabel(f"Vybráno souborů: {len(self.files)}")
|
|
header.setFont(QFont("Arial", 11, QFont.Bold))
|
|
header.setAlignment(Qt.AlignCenter)
|
|
layout.addWidget(header)
|
|
|
|
# Scrollable content
|
|
scroll = QScrollArea()
|
|
scroll.setWidgetResizable(True)
|
|
scroll.setFrameShape(QFrame.NoFrame)
|
|
|
|
content = QWidget()
|
|
content_layout = QVBoxLayout(content)
|
|
content_layout.setSpacing(2)
|
|
|
|
# Calculate tag states
|
|
file_tag_sets = [{t.full_path for t in f.tags} for f in self.files]
|
|
|
|
# Group by category
|
|
tags_by_category = {}
|
|
for full_path, tag in self.tags_by_full.items():
|
|
if tag.category not in tags_by_category:
|
|
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)
|
|
|
|
# Create category sections
|
|
for category in sorted(tags_by_category.keys()):
|
|
color = self.category_colors.get(category, "#333333")
|
|
is_exclusive = category in EXCLUSIVE_CATEGORIES
|
|
exclusive_note = " (pouze jedno)" if is_exclusive else ""
|
|
|
|
# Category header
|
|
cat_label = QLabel(f"▸ {category}{exclusive_note}")
|
|
cat_label.setFont(QFont("Arial", 10, QFont.Bold))
|
|
cat_label.setStyleSheet(f"color: {color}; margin-top: 12px;")
|
|
content_layout.addWidget(cat_label)
|
|
|
|
self.category_checkboxes[category] = []
|
|
|
|
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_state = Qt.Unchecked
|
|
elif have_count == len(self.files):
|
|
init_state = Qt.Checked
|
|
else:
|
|
init_state = Qt.PartiallyChecked
|
|
|
|
cb = QCheckBox(f" {tag.name}")
|
|
cb.setTristate(True)
|
|
cb.setCheckState(init_state)
|
|
cb.setProperty("full_path", full_path)
|
|
cb.setProperty("category", category)
|
|
cb.setProperty("tag_color", color)
|
|
|
|
# Style based on state
|
|
self._update_checkbox_style(cb)
|
|
cb.stateChanged.connect(lambda state, c=cb: self._on_state_changed(c))
|
|
|
|
content_layout.addWidget(cb)
|
|
self.checkboxes[full_path] = cb
|
|
self.category_checkboxes[category].append(cb)
|
|
|
|
content_layout.addStretch()
|
|
scroll.setWidget(content)
|
|
layout.addWidget(scroll)
|
|
|
|
# Buttons
|
|
button_box = QDialogButtonBox(
|
|
QDialogButtonBox.Ok | QDialogButtonBox.Cancel
|
|
)
|
|
button_box.accepted.connect(self._on_ok)
|
|
button_box.rejected.connect(self.reject)
|
|
layout.addWidget(button_box)
|
|
|
|
def _update_checkbox_style(self, cb: QCheckBox):
|
|
state = cb.checkState()
|
|
color = cb.property("tag_color") or "#333333"
|
|
|
|
if state == Qt.Unchecked:
|
|
cb.setStyleSheet("color: #666666;")
|
|
elif state == Qt.Checked:
|
|
cb.setStyleSheet(f"color: {color};")
|
|
else: # PartiallyChecked
|
|
cb.setStyleSheet("color: #cc6600;")
|
|
|
|
def _on_state_changed(self, cb: QCheckBox):
|
|
category = cb.property("category")
|
|
|
|
# Handle exclusive categories
|
|
if category in EXCLUSIVE_CATEGORIES:
|
|
if cb.checkState() == Qt.Checked:
|
|
# Uncheck all others in this category
|
|
for other_cb in self.category_checkboxes.get(category, []):
|
|
if other_cb != cb:
|
|
other_cb.blockSignals(True)
|
|
other_cb.setCheckState(Qt.Unchecked)
|
|
self._update_checkbox_style(other_cb)
|
|
other_cb.blockSignals(False)
|
|
|
|
self._update_checkbox_style(cb)
|
|
|
|
def _on_ok(self):
|
|
self.result = {}
|
|
for full_path, cb in self.checkboxes.items():
|
|
state = cb.checkState()
|
|
if state == Qt.Checked:
|
|
self.result[full_path] = 1
|
|
elif state == Qt.Unchecked:
|
|
self.result[full_path] = 0
|
|
else:
|
|
self.result[full_path] = 2 # mixed - don't change
|
|
self.accept()
|
|
|
|
|
|
class CategorySelectionDialog(QDialog):
|
|
"""Dialog for selecting categories for hardlink structure"""
|
|
|
|
def __init__(self, parent, categories: List[str], category_colors: dict,
|
|
preselected: List[str] | None = None):
|
|
super().__init__(parent)
|
|
self.setWindowTitle("Vybrat kategorie")
|
|
self.setMinimumSize(350, 400)
|
|
self.categories = categories
|
|
self.category_colors = category_colors
|
|
self.preselected = preselected
|
|
self.result = None
|
|
self.checkboxes: dict[str, QCheckBox] = {}
|
|
|
|
self._setup_ui()
|
|
|
|
def _setup_ui(self):
|
|
layout = QVBoxLayout(self)
|
|
|
|
# Header
|
|
header = QLabel("Vyberte kategorie pro vytvoření struktury:")
|
|
header.setFont(QFont("Arial", 10, QFont.Bold))
|
|
layout.addWidget(header)
|
|
|
|
# Scrollable content
|
|
scroll = QScrollArea()
|
|
scroll.setWidgetResizable(True)
|
|
|
|
content = QWidget()
|
|
content_layout = QVBoxLayout(content)
|
|
|
|
for category in sorted(self.categories):
|
|
initial_value = (self.preselected is None or
|
|
category in self.preselected)
|
|
color = self.category_colors.get(category, "#333333")
|
|
|
|
cb = QCheckBox(category)
|
|
cb.setChecked(initial_value)
|
|
cb.setStyleSheet(f"color: {color};")
|
|
content_layout.addWidget(cb)
|
|
self.checkboxes[category] = cb
|
|
|
|
content_layout.addStretch()
|
|
scroll.setWidget(content)
|
|
layout.addWidget(scroll)
|
|
|
|
# Selection buttons
|
|
sel_layout = QHBoxLayout()
|
|
btn_all = QPushButton("Všechny")
|
|
btn_all.clicked.connect(self._select_all)
|
|
btn_none = QPushButton("Žádné")
|
|
btn_none.clicked.connect(self._select_none)
|
|
sel_layout.addWidget(btn_all)
|
|
sel_layout.addWidget(btn_none)
|
|
sel_layout.addStretch()
|
|
layout.addLayout(sel_layout)
|
|
|
|
# Dialog buttons
|
|
button_box = QDialogButtonBox(
|
|
QDialogButtonBox.Ok | QDialogButtonBox.Cancel
|
|
)
|
|
button_box.accepted.connect(self._on_ok)
|
|
button_box.rejected.connect(self.reject)
|
|
layout.addWidget(button_box)
|
|
|
|
def _select_all(self):
|
|
for cb in self.checkboxes.values():
|
|
cb.setChecked(True)
|
|
|
|
def _select_none(self):
|
|
for cb in self.checkboxes.values():
|
|
cb.setChecked(False)
|
|
|
|
def _on_ok(self):
|
|
self.result = [cat for cat, cb in self.checkboxes.items() if cb.isChecked()]
|
|
self.accept()
|
|
|
|
|
|
class MainWindow(QMainWindow):
|
|
"""Main application window"""
|
|
|
|
def __init__(self, filehandler: FileManager, tagmanager: TagManager):
|
|
super().__init__()
|
|
self.filehandler = filehandler
|
|
self.tagmanager = tagmanager
|
|
|
|
# State
|
|
self.tag_states: dict[str, bool] = {} # tag full_path -> checked
|
|
self.file_items: dict[int, File] = {} # row -> File mapping
|
|
self.tag_tree_items: dict[str, tuple] = {} # full_path -> (item, name)
|
|
self.filter_text = ""
|
|
self.show_full_path = False
|
|
self.sort_column = 0
|
|
self.sort_order = Qt.AscendingOrder
|
|
self.category_colors: dict[str, str] = {}
|
|
self.show_csfd_column = True
|
|
self.hide_ignored = False
|
|
|
|
self.filehandler.on_files_changed = self.update_files_from_manager
|
|
|
|
self._setup_window()
|
|
self._create_menu()
|
|
self._create_toolbar()
|
|
self._create_main_layout()
|
|
self._create_status_bar()
|
|
self._setup_shortcuts()
|
|
|
|
# Load last folder
|
|
last = self.filehandler.global_config.get("last_folder")
|
|
if last:
|
|
try:
|
|
self.filehandler.append(Path(last))
|
|
except Exception:
|
|
pass
|
|
|
|
# Initial refresh
|
|
self.refresh_sidebar()
|
|
self.update_files_from_manager(self.filehandler.filelist)
|
|
|
|
def _setup_window(self):
|
|
self.setWindowTitle(APP_NAME)
|
|
|
|
# Parse viewport size
|
|
try:
|
|
w, h = APP_VIEWPORT.split("x")
|
|
self.resize(int(w), int(h))
|
|
except:
|
|
self.resize(1000, 700)
|
|
|
|
# Load saved geometry
|
|
geometry = self.filehandler.global_config.get("window_geometry")
|
|
if geometry:
|
|
try:
|
|
parts = geometry.split("x")
|
|
if len(parts) >= 2:
|
|
w = int(parts[0])
|
|
h_pos = parts[1].split("+")
|
|
h = int(h_pos[0])
|
|
self.resize(w, h)
|
|
if len(h_pos) >= 3:
|
|
self.move(int(h_pos[1]), int(h_pos[2]))
|
|
except:
|
|
pass
|
|
|
|
if self.filehandler.global_config.get("window_maximized", False):
|
|
self.showMaximized()
|
|
|
|
def _create_menu(self):
|
|
menubar = self.menuBar()
|
|
|
|
# File menu
|
|
file_menu = menubar.addMenu("Soubor")
|
|
|
|
open_action = QAction("Otevřít složku... (Ctrl+O)", self)
|
|
open_action.triggered.connect(self.open_folder_dialog)
|
|
file_menu.addAction(open_action)
|
|
|
|
close_action = QAction("Zavřít složku (Ctrl+W)", self)
|
|
close_action.triggered.connect(self.close_folder)
|
|
file_menu.addAction(close_action)
|
|
|
|
ignore_action = QAction("Nastavit ignorované vzory", self)
|
|
ignore_action.triggered.connect(self.set_ignore_patterns)
|
|
file_menu.addAction(ignore_action)
|
|
|
|
file_menu.addSeparator()
|
|
|
|
exit_action = QAction("Ukončit (Ctrl+Q)", self)
|
|
exit_action.triggered.connect(self.close)
|
|
file_menu.addAction(exit_action)
|
|
|
|
# View menu
|
|
view_menu = menubar.addMenu("Pohled")
|
|
|
|
self.hide_ignored_action = QAction("Skrýt ignorované", self)
|
|
self.hide_ignored_action.setCheckable(True)
|
|
self.hide_ignored_action.triggered.connect(self.toggle_hide_ignored)
|
|
view_menu.addAction(self.hide_ignored_action)
|
|
|
|
self.csfd_column_action = QAction("Zobrazit CSFD sloupec", self)
|
|
self.csfd_column_action.setCheckable(True)
|
|
self.csfd_column_action.setChecked(True)
|
|
self.csfd_column_action.triggered.connect(self.toggle_csfd_column)
|
|
view_menu.addAction(self.csfd_column_action)
|
|
|
|
refresh_action = QAction("Obnovit (F5)", self)
|
|
refresh_action.triggered.connect(self.refresh_all)
|
|
view_menu.addAction(refresh_action)
|
|
|
|
# Tools menu
|
|
tools_menu = menubar.addMenu("Nástroje")
|
|
|
|
date_action = QAction("Nastavit datum (Ctrl+D)", self)
|
|
date_action.triggered.connect(self.set_date_for_selected)
|
|
tools_menu.addAction(date_action)
|
|
|
|
resolution_action = QAction("Detekovat rozlišení videí", self)
|
|
resolution_action.triggered.connect(self.detect_video_resolution)
|
|
tools_menu.addAction(resolution_action)
|
|
|
|
tags_action = QAction("Přiřadit tagy (Ctrl+T)", self)
|
|
tags_action.triggered.connect(self.assign_tag_to_selected_bulk)
|
|
tools_menu.addAction(tags_action)
|
|
|
|
tools_menu.addSeparator()
|
|
|
|
csfd_url_action = QAction("Nastavit CSFD URL...", self)
|
|
csfd_url_action.triggered.connect(self.set_csfd_url_for_selected)
|
|
tools_menu.addAction(csfd_url_action)
|
|
|
|
csfd_tags_action = QAction("Načíst tagy z CSFD", self)
|
|
csfd_tags_action.triggered.connect(self.apply_csfd_tags_for_selected)
|
|
tools_menu.addAction(csfd_tags_action)
|
|
|
|
tools_menu.addSeparator()
|
|
|
|
hardlink_config_action = QAction("Nastavit hardlink složku...", self)
|
|
hardlink_config_action.triggered.connect(self.configure_hardlink_folder)
|
|
tools_menu.addAction(hardlink_config_action)
|
|
|
|
hardlink_update_action = QAction("Aktualizovat hardlink strukturu", self)
|
|
hardlink_update_action.triggered.connect(self.update_hardlink_structure)
|
|
tools_menu.addAction(hardlink_update_action)
|
|
|
|
hardlink_create_action = QAction("Vytvořit hardlink strukturu...", self)
|
|
hardlink_create_action.triggered.connect(self.create_hardlink_structure)
|
|
tools_menu.addAction(hardlink_create_action)
|
|
|
|
def _create_toolbar(self):
|
|
toolbar = QToolBar()
|
|
toolbar.setMovable(False)
|
|
self.addToolBar(toolbar)
|
|
|
|
# Open folder button
|
|
open_btn = QPushButton("📁 Otevřít složku")
|
|
open_btn.setFlat(True)
|
|
open_btn.clicked.connect(self.open_folder_dialog)
|
|
toolbar.addWidget(open_btn)
|
|
|
|
# Refresh button
|
|
refresh_btn = QPushButton("🔄 Obnovit")
|
|
refresh_btn.setFlat(True)
|
|
refresh_btn.clicked.connect(self.refresh_all)
|
|
toolbar.addWidget(refresh_btn)
|
|
|
|
toolbar.addSeparator()
|
|
|
|
# New tag button
|
|
tag_btn = QPushButton("🏷️ Nový tag")
|
|
tag_btn.setFlat(True)
|
|
tag_btn.clicked.connect(lambda: self.tree_add_tag(background=True))
|
|
toolbar.addWidget(tag_btn)
|
|
|
|
# Set date button
|
|
date_btn = QPushButton("📅 Nastavit datum")
|
|
date_btn.setFlat(True)
|
|
date_btn.clicked.connect(self.set_date_for_selected)
|
|
toolbar.addWidget(date_btn)
|
|
|
|
toolbar.addSeparator()
|
|
|
|
# Spacer
|
|
spacer = QWidget()
|
|
spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
|
|
toolbar.addWidget(spacer)
|
|
|
|
# Search
|
|
search_label = QLabel("🔍 ")
|
|
toolbar.addWidget(search_label)
|
|
|
|
self.search_input = QLineEdit()
|
|
self.search_input.setPlaceholderText("Hledat...")
|
|
self.search_input.setFixedWidth(200)
|
|
self.search_input.textChanged.connect(self.on_filter_changed)
|
|
toolbar.addWidget(self.search_input)
|
|
|
|
def _create_main_layout(self):
|
|
central = QWidget()
|
|
self.setCentralWidget(central)
|
|
|
|
layout = QHBoxLayout(central)
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
# Splitter for sidebar and main content
|
|
splitter = QSplitter(Qt.Horizontal)
|
|
|
|
# Left sidebar
|
|
sidebar = self._create_sidebar()
|
|
splitter.addWidget(sidebar)
|
|
|
|
# Right panel (file table)
|
|
file_panel = self._create_file_panel()
|
|
splitter.addWidget(file_panel)
|
|
|
|
# Set initial sizes
|
|
splitter.setSizes([250, 750])
|
|
|
|
layout.addWidget(splitter)
|
|
|
|
def _create_sidebar(self) -> QWidget:
|
|
sidebar = QWidget()
|
|
sidebar.setMinimumWidth(200)
|
|
sidebar.setMaximumWidth(400)
|
|
|
|
layout = QVBoxLayout(sidebar)
|
|
layout.setContentsMargins(5, 5, 5, 5)
|
|
|
|
# Header
|
|
header = QLabel("📂 Štítky")
|
|
header.setFont(QFont("Arial", 10, QFont.Bold))
|
|
layout.addWidget(header)
|
|
|
|
# Tag tree
|
|
self.tag_tree = QTreeWidget()
|
|
self.tag_tree.setHeaderHidden(True)
|
|
self.tag_tree.setSelectionMode(QAbstractItemView.SingleSelection)
|
|
self.tag_tree.itemClicked.connect(self.on_tree_item_clicked)
|
|
self.tag_tree.setContextMenuPolicy(Qt.CustomContextMenu)
|
|
self.tag_tree.customContextMenuRequested.connect(self.on_tree_context_menu)
|
|
|
|
layout.addWidget(self.tag_tree)
|
|
|
|
return sidebar
|
|
|
|
def _create_file_panel(self) -> QWidget:
|
|
panel = QWidget()
|
|
layout = QVBoxLayout(panel)
|
|
layout.setContentsMargins(5, 5, 5, 5)
|
|
|
|
# Control panel
|
|
control_layout = QHBoxLayout()
|
|
|
|
self.full_path_cb = QCheckBox("Plná cesta")
|
|
self.full_path_cb.toggled.connect(self.toggle_show_path)
|
|
control_layout.addWidget(self.full_path_cb)
|
|
|
|
control_layout.addStretch()
|
|
layout.addLayout(control_layout)
|
|
|
|
# File table
|
|
self.file_table = QTableWidget()
|
|
self.file_table.setColumnCount(5)
|
|
self.file_table.setHorizontalHeaderLabels(
|
|
["📄 Název", "📅 Datum", "🏷️ Štítky", "🎬 CSFD", "💾 Velikost"]
|
|
)
|
|
self.file_table.setSelectionBehavior(QAbstractItemView.SelectRows)
|
|
self.file_table.setSelectionMode(QAbstractItemView.ExtendedSelection)
|
|
self.file_table.setEditTriggers(QAbstractItemView.NoEditTriggers)
|
|
self.file_table.setSortingEnabled(True)
|
|
self.file_table.horizontalHeader().setSectionResizeMode(
|
|
0, QHeaderView.Stretch
|
|
)
|
|
self.file_table.horizontalHeader().setSectionResizeMode(
|
|
1, QHeaderView.ResizeToContents
|
|
)
|
|
self.file_table.horizontalHeader().setSectionResizeMode(
|
|
2, QHeaderView.ResizeToContents
|
|
)
|
|
self.file_table.horizontalHeader().setSectionResizeMode(
|
|
3, QHeaderView.ResizeToContents
|
|
)
|
|
self.file_table.horizontalHeader().setSectionResizeMode(
|
|
4, QHeaderView.ResizeToContents
|
|
)
|
|
self.file_table.verticalHeader().setVisible(False)
|
|
|
|
self.file_table.doubleClicked.connect(self.on_file_double_click)
|
|
self.file_table.setContextMenuPolicy(Qt.CustomContextMenu)
|
|
self.file_table.customContextMenuRequested.connect(self.on_file_context_menu)
|
|
self.file_table.selectionModel().selectionChanged.connect(
|
|
self.on_selection_changed
|
|
)
|
|
self.file_table.horizontalHeader().sortIndicatorChanged.connect(
|
|
self.on_sort_changed
|
|
)
|
|
|
|
layout.addWidget(self.file_table)
|
|
|
|
return panel
|
|
|
|
def _create_status_bar(self):
|
|
self.status_bar = QStatusBar()
|
|
self.setStatusBar(self.status_bar)
|
|
|
|
self.status_label = QLabel("Připraven")
|
|
self.status_bar.addWidget(self.status_label, 1)
|
|
|
|
self.selected_count_label = QLabel("")
|
|
self.status_bar.addPermanentWidget(self.selected_count_label)
|
|
|
|
self.selected_size_label = QLabel("")
|
|
self.status_bar.addPermanentWidget(self.selected_size_label)
|
|
|
|
self.file_count_label = QLabel("0 souborů")
|
|
self.status_bar.addPermanentWidget(self.file_count_label)
|
|
|
|
def _setup_shortcuts(self):
|
|
from PySide6.QtGui import QShortcut, QKeySequence
|
|
|
|
QShortcut(QKeySequence("Ctrl+O"), self, self.open_folder_dialog)
|
|
QShortcut(QKeySequence("Ctrl+W"), self, self.close_folder)
|
|
QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
|
|
QShortcut(QKeySequence("Ctrl+T"), self, self.assign_tag_to_selected_bulk)
|
|
QShortcut(QKeySequence("Ctrl+D"), self, self.set_date_for_selected)
|
|
QShortcut(QKeySequence("F5"), self, self.refresh_all)
|
|
QShortcut(QKeySequence("Delete"), self, self.remove_selected_files)
|
|
|
|
# ==================================================
|
|
# SIDEBAR / TAG TREE METHODS
|
|
# ==================================================
|
|
|
|
def refresh_sidebar(self):
|
|
"""Refresh tag tree in sidebar"""
|
|
self.tag_tree.clear()
|
|
self.tag_tree_items.clear()
|
|
self.tag_states.clear()
|
|
|
|
# Count files per tag
|
|
tag_counts = {}
|
|
for f in self.filehandler.filelist:
|
|
for t in f.tags:
|
|
tag_counts[t.full_path] = tag_counts.get(t.full_path, 0) + 1
|
|
|
|
# Root item
|
|
total_files = len(self.filehandler.filelist)
|
|
root = QTreeWidgetItem(self.tag_tree)
|
|
root.setText(0, f"📂 Všechny soubory ({total_files})")
|
|
root.setExpanded(True)
|
|
self.root_item = root
|
|
|
|
# Assign colors to categories
|
|
categories = self.tagmanager.get_categories()
|
|
color_index = 0
|
|
for category in categories:
|
|
if category not in self.category_colors:
|
|
if category in DEFAULT_CATEGORY_COLORS:
|
|
self.category_colors[category] = DEFAULT_CATEGORY_COLORS[category]
|
|
else:
|
|
self.category_colors[category] = TAG_COLORS[
|
|
color_index % len(TAG_COLORS)
|
|
]
|
|
color_index += 1
|
|
|
|
# Add categories and tags
|
|
for category in categories:
|
|
color = self.category_colors.get(category, "#333333")
|
|
|
|
cat_item = QTreeWidgetItem(root)
|
|
cat_item.setText(0, f"📁 {category}")
|
|
cat_item.setForeground(0, QColor(color))
|
|
cat_item.setData(0, Qt.UserRole, {"type": "category", "name": category})
|
|
|
|
for tag in self.tagmanager.get_tags_in_category(category):
|
|
count = tag_counts.get(tag.full_path, 0)
|
|
count_str = f" ({count})" if count > 0 else ""
|
|
|
|
tag_item = QTreeWidgetItem(cat_item)
|
|
tag_item.setText(0, f"☐ {tag.name}{count_str}")
|
|
tag_item.setForeground(0, QColor(color))
|
|
tag_item.setData(0, Qt.UserRole, {
|
|
"type": "tag",
|
|
"full_path": tag.full_path,
|
|
"name": tag.name,
|
|
"category": category
|
|
})
|
|
|
|
self.tag_tree_items[tag.full_path] = (tag_item, tag.name)
|
|
self.tag_states[tag.full_path] = False
|
|
|
|
def update_tag_counts(self, filtered_files: List[File]):
|
|
"""Update tag counts in sidebar"""
|
|
tag_counts = {}
|
|
for f in filtered_files:
|
|
for t in f.tags:
|
|
tag_counts[t.full_path] = tag_counts.get(t.full_path, 0) + 1
|
|
|
|
for full_path, (item, tag_name) in self.tag_tree_items.items():
|
|
count = tag_counts.get(full_path, 0)
|
|
count_str = f" ({count})" if count > 0 else ""
|
|
checked = "☑" if self.tag_states.get(full_path, False) else "☐"
|
|
item.setText(0, f"{checked} {tag_name}{count_str}")
|
|
|
|
# Update root count
|
|
total = len(filtered_files)
|
|
self.root_item.setText(0, f"📂 Všechny soubory ({total})")
|
|
|
|
def on_tree_item_clicked(self, item: QTreeWidgetItem, column: int):
|
|
"""Handle click on tag tree item"""
|
|
data = item.data(0, Qt.UserRole)
|
|
if not data:
|
|
return
|
|
|
|
if data.get("type") == "tag":
|
|
full_path = data["full_path"]
|
|
self.tag_states[full_path] = not self.tag_states.get(full_path, False)
|
|
|
|
# Update checkbox visual
|
|
tag_name = data["name"]
|
|
count_match = re.search(r'\((\d+)\)$', item.text(0))
|
|
count_str = f" ({count_match.group(1)})" if count_match else ""
|
|
checked = "☑" if self.tag_states[full_path] else "☐"
|
|
item.setText(0, f"{checked} {tag_name}{count_str}")
|
|
|
|
self.update_files_from_manager(self.filehandler.filelist)
|
|
|
|
def on_tree_context_menu(self, pos):
|
|
"""Show context menu for tag tree"""
|
|
item = self.tag_tree.itemAt(pos)
|
|
if not item:
|
|
return
|
|
|
|
self.selected_tree_item = item
|
|
data = item.data(0, Qt.UserRole)
|
|
|
|
menu = QMenu(self)
|
|
|
|
add_action = QAction("Nový štítek", self)
|
|
add_action.triggered.connect(self.tree_add_tag)
|
|
menu.addAction(add_action)
|
|
|
|
if data and data.get("type") in ("tag", "category"):
|
|
rename_action = QAction("Přejmenovat", self)
|
|
rename_action.triggered.connect(self.tree_rename_tag)
|
|
menu.addAction(rename_action)
|
|
|
|
delete_action = QAction("Smazat", self)
|
|
delete_action.triggered.connect(self.tree_delete_tag)
|
|
menu.addAction(delete_action)
|
|
|
|
menu.exec(self.tag_tree.mapToGlobal(pos))
|
|
|
|
def tree_add_tag(self, background=False):
|
|
"""Add new tag"""
|
|
name, ok = QInputDialog.getText(self, "Nový tag", "Název tagu:")
|
|
if not ok or not name:
|
|
return
|
|
|
|
item = getattr(self, 'selected_tree_item', None) if not background else None
|
|
data = item.data(0, Qt.UserRole) if item else None
|
|
|
|
if data and data.get("type") == "category":
|
|
category = data["name"]
|
|
self.tagmanager.add_tag(category, name)
|
|
else:
|
|
self.tagmanager.add_category(name)
|
|
|
|
self.refresh_sidebar()
|
|
self.status_label.setText(f"Vytvořen tag: {name}")
|
|
|
|
def tree_delete_tag(self):
|
|
"""Delete selected tag"""
|
|
item = getattr(self, 'selected_tree_item', None)
|
|
if not item:
|
|
return
|
|
|
|
data = item.data(0, Qt.UserRole)
|
|
if not data:
|
|
return
|
|
|
|
if data.get("type") == "category":
|
|
name = data["name"]
|
|
reply = QMessageBox.question(
|
|
self, "Smazat kategorii",
|
|
f"Opravdu chcete smazat kategorii '{name}'?"
|
|
)
|
|
if reply == QMessageBox.Yes:
|
|
self.tagmanager.remove_category(name)
|
|
elif data.get("type") == "tag":
|
|
name = data["name"]
|
|
category = data["category"]
|
|
reply = QMessageBox.question(
|
|
self, "Smazat štítek",
|
|
f"Opravdu chcete smazat štítek '{name}'?"
|
|
)
|
|
if reply == QMessageBox.Yes:
|
|
self.tagmanager.remove_tag(category, name)
|
|
|
|
self.refresh_sidebar()
|
|
self.update_files_from_manager(self.filehandler.filelist)
|
|
|
|
def tree_rename_tag(self):
|
|
"""Rename selected tag or category"""
|
|
item = getattr(self, 'selected_tree_item', None)
|
|
if not item:
|
|
return
|
|
|
|
data = item.data(0, Qt.UserRole)
|
|
if not data:
|
|
return
|
|
|
|
if data.get("type") == "category":
|
|
current_name = data["name"]
|
|
new_name, ok = QInputDialog.getText(
|
|
self, "Přejmenovat kategorii",
|
|
f"Nový název kategorie '{current_name}':",
|
|
text=current_name
|
|
)
|
|
if not ok or not new_name or new_name == current_name:
|
|
return
|
|
|
|
# Check for existing category - offer merge
|
|
if new_name in self.tagmanager.get_categories():
|
|
reply = QMessageBox.question(
|
|
self, "Kategorie existuje",
|
|
f"Kategorie '{new_name}' již existuje.\n\n"
|
|
f"Chcete sloučit '{current_name}' do '{new_name}'?"
|
|
)
|
|
if reply != QMessageBox.Yes:
|
|
return
|
|
updated = self.filehandler.merge_category_in_files(
|
|
current_name, new_name
|
|
)
|
|
self.status_label.setText(
|
|
f"Kategorie sloučena: {current_name} → {new_name} "
|
|
f"({updated} souborů)"
|
|
)
|
|
else:
|
|
updated = self.filehandler.rename_category_in_files(
|
|
current_name, new_name
|
|
)
|
|
self.status_label.setText(
|
|
f"Kategorie přejmenována: {current_name} → {new_name} "
|
|
f"({updated} souborů)"
|
|
)
|
|
|
|
elif data.get("type") == "tag":
|
|
current_name = data["name"]
|
|
category = data["category"]
|
|
new_name, ok = QInputDialog.getText(
|
|
self, "Přejmenovat štítek",
|
|
f"Nový název štítku '{current_name}':",
|
|
text=current_name
|
|
)
|
|
if not ok or not new_name or new_name == current_name:
|
|
return
|
|
|
|
# Check for existing tag - offer merge
|
|
existing = [t.name for t in
|
|
self.tagmanager.get_tags_in_category(category)]
|
|
if new_name in existing:
|
|
reply = QMessageBox.question(
|
|
self, "Štítek existuje",
|
|
f"Štítek '{new_name}' v kategorii '{category}' již existuje.\n\n"
|
|
f"Chcete sloučit '{current_name}' do '{new_name}'?"
|
|
)
|
|
if reply != QMessageBox.Yes:
|
|
return
|
|
updated = self.filehandler.merge_tag_in_files(
|
|
category, current_name, new_name
|
|
)
|
|
self.status_label.setText(
|
|
f"Štítek sloučen: {category}/{current_name} → "
|
|
f"{category}/{new_name} ({updated} souborů)"
|
|
)
|
|
else:
|
|
updated = self.filehandler.rename_tag_in_files(
|
|
category, current_name, new_name
|
|
)
|
|
self.status_label.setText(
|
|
f"Štítek přejmenován: {category}/{current_name} → "
|
|
f"{category}/{new_name} ({updated} souborů)"
|
|
)
|
|
|
|
self.refresh_sidebar()
|
|
self.update_files_from_manager(self.filehandler.filelist)
|
|
|
|
def get_checked_tags(self) -> List[Tag]:
|
|
"""Get list of checked tags"""
|
|
tags = []
|
|
for full_path, checked in self.tag_states.items():
|
|
if checked and full_path in self.tag_tree_items:
|
|
item, name = self.tag_tree_items[full_path]
|
|
data = item.data(0, Qt.UserRole)
|
|
if data and data.get("type") == "tag":
|
|
tags.append(Tag(data["category"], data["name"]))
|
|
return tags
|
|
|
|
# ==================================================
|
|
# FILE TABLE METHODS
|
|
# ==================================================
|
|
|
|
def update_files_from_manager(self, filelist=None):
|
|
"""Update file table"""
|
|
if filelist is None:
|
|
filelist = self.filehandler.filelist
|
|
|
|
# Filter by checked tags
|
|
checked_tags = self.get_checked_tags()
|
|
filtered_files = self.filehandler.filter_files_by_tags(checked_tags)
|
|
|
|
# Filter by search text
|
|
search_text = self.search_input.text().lower() if hasattr(self, 'search_input') else ""
|
|
if search_text:
|
|
filtered_files = [
|
|
f for f in filtered_files
|
|
if search_text in f.filename.lower() or
|
|
(self.show_full_path and search_text in str(f.file_path).lower())
|
|
]
|
|
|
|
# Filter ignored
|
|
if self.hide_ignored:
|
|
filtered_files = [
|
|
f for f in filtered_files
|
|
if "Stav/Ignorované" not in {t.full_path for t in f.tags}
|
|
]
|
|
|
|
# Clear table
|
|
self.file_table.setSortingEnabled(False)
|
|
self.file_table.setRowCount(0)
|
|
self.file_items.clear()
|
|
|
|
# Populate table
|
|
for row, f in enumerate(filtered_files):
|
|
self.file_table.insertRow(row)
|
|
|
|
name = str(f.file_path) if self.show_full_path else f.filename
|
|
date = f.date or ""
|
|
tags = ", ".join([t.name for t in f.tags[:3]])
|
|
if len(f.tags) > 3:
|
|
tags += f" +{len(f.tags) - 3}"
|
|
csfd = "✓" if f.csfd_url else ""
|
|
|
|
try:
|
|
size = f.file_path.stat().st_size
|
|
size_str = self._format_size(size)
|
|
except:
|
|
size_str = "?"
|
|
|
|
self.file_table.setItem(row, 0, QTableWidgetItem(name))
|
|
self.file_table.setItem(row, 1, QTableWidgetItem(date))
|
|
self.file_table.setItem(row, 2, QTableWidgetItem(tags))
|
|
self.file_table.setItem(row, 3, QTableWidgetItem(csfd))
|
|
self.file_table.setItem(row, 4, QTableWidgetItem(size_str))
|
|
|
|
self.file_items[row] = f
|
|
|
|
self.file_table.setSortingEnabled(True)
|
|
|
|
# Update CSFD column visibility
|
|
self.file_table.setColumnHidden(3, not self.show_csfd_column)
|
|
|
|
# Update status
|
|
self.file_count_label.setText(f"{len(filtered_files)} souborů")
|
|
self.status_label.setText(f"Zobrazeno {len(filtered_files)} souborů")
|
|
|
|
# Update tag counts
|
|
self.update_tag_counts(filtered_files)
|
|
|
|
def _format_size(self, size_bytes: int) -> str:
|
|
"""Format file size"""
|
|
for unit in ['B', 'KB', 'MB', 'GB']:
|
|
if size_bytes < 1024.0:
|
|
return f"{size_bytes:.1f} {unit}"
|
|
size_bytes /= 1024.0
|
|
return f"{size_bytes:.1f} TB"
|
|
|
|
def get_selected_files(self) -> List[File]:
|
|
"""Get selected files from table"""
|
|
files = []
|
|
for index in self.file_table.selectionModel().selectedRows():
|
|
row = index.row()
|
|
# Find the file by matching the filename in the visible row
|
|
name_item = self.file_table.item(row, 0)
|
|
if name_item:
|
|
name = name_item.text()
|
|
for f in self.filehandler.filelist:
|
|
display_name = str(f.file_path) if self.show_full_path else f.filename
|
|
if display_name == name:
|
|
files.append(f)
|
|
break
|
|
return files
|
|
|
|
def on_selection_changed(self):
|
|
"""Update status bar when selection changes"""
|
|
files = self.get_selected_files()
|
|
count = len(files)
|
|
|
|
if count == 0:
|
|
self.selected_count_label.setText("")
|
|
self.selected_size_label.setText("")
|
|
else:
|
|
self.selected_count_label.setText(f"{count} vybráno")
|
|
total_size = 0
|
|
for f in files:
|
|
try:
|
|
total_size += f.file_path.stat().st_size
|
|
except:
|
|
pass
|
|
self.selected_size_label.setText(f"[{self._format_size(total_size)}]")
|
|
|
|
def on_file_double_click(self, index):
|
|
"""Handle double click on file"""
|
|
files = self.get_selected_files()
|
|
for f in files:
|
|
self.open_file(f.file_path)
|
|
|
|
def on_file_context_menu(self, pos):
|
|
"""Show context menu for files"""
|
|
menu = QMenu(self)
|
|
|
|
open_action = QAction("Otevřít soubor", self)
|
|
open_action.triggered.connect(self.open_selected_files)
|
|
menu.addAction(open_action)
|
|
|
|
tags_action = QAction("Přiřadit štítky (Ctrl+T)", self)
|
|
tags_action.triggered.connect(self.assign_tag_to_selected_bulk)
|
|
menu.addAction(tags_action)
|
|
|
|
date_action = QAction("Nastavit datum (Ctrl+D)", self)
|
|
date_action.triggered.connect(self.set_date_for_selected)
|
|
menu.addAction(date_action)
|
|
|
|
menu.addSeparator()
|
|
|
|
csfd_url_action = QAction("Nastavit CSFD URL...", self)
|
|
csfd_url_action.triggered.connect(self.set_csfd_url_for_selected)
|
|
menu.addAction(csfd_url_action)
|
|
|
|
csfd_tags_action = QAction("Načíst tagy z CSFD", self)
|
|
csfd_tags_action.triggered.connect(self.apply_csfd_tags_for_selected)
|
|
menu.addAction(csfd_tags_action)
|
|
|
|
menu.addSeparator()
|
|
|
|
remove_action = QAction("Smazat z indexu (Del)", self)
|
|
remove_action.triggered.connect(self.remove_selected_files)
|
|
menu.addAction(remove_action)
|
|
|
|
menu.exec(self.file_table.mapToGlobal(pos))
|
|
|
|
def on_sort_changed(self, column: int, order: Qt.SortOrder):
|
|
"""Handle sort indicator change"""
|
|
self.sort_column = column
|
|
self.sort_order = order
|
|
|
|
def open_file(self, path: Path):
|
|
"""Open file with default application"""
|
|
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_label.setText(f"Otevírám: {path.name}")
|
|
except Exception as e:
|
|
QMessageBox.critical(self, "Chyba", f"Nelze otevřít {path}: {e}")
|
|
|
|
# ==================================================
|
|
# ACTIONS
|
|
# ==================================================
|
|
|
|
def open_folder_dialog(self):
|
|
"""Open folder selection dialog"""
|
|
folder = QFileDialog.getExistingDirectory(
|
|
self, "Vyber složku pro sledování"
|
|
)
|
|
if not folder:
|
|
return
|
|
|
|
folder_path = Path(folder)
|
|
try:
|
|
self.filehandler.append(folder_path)
|
|
for f in self.filehandler.filelist:
|
|
if f.tags and f.tagmanager:
|
|
for t in f.tags:
|
|
f.tagmanager.add_tag(t.category, t.name)
|
|
|
|
self.status_label.setText(f"Přidána složka: {folder_path}")
|
|
self._update_csfd_column_visibility()
|
|
self.refresh_sidebar()
|
|
self.update_files_from_manager(self.filehandler.filelist)
|
|
except Exception as e:
|
|
QMessageBox.critical(self, "Chyba", f"Nelze přidat složku {folder}: {e}")
|
|
|
|
def close_folder(self):
|
|
"""Close current folder safely"""
|
|
if not self.filehandler.current_folder:
|
|
self.status_label.setText("Žádná složka není otevřena")
|
|
return
|
|
|
|
folder_name = self.filehandler.current_folder.name
|
|
self.filehandler.close_folder()
|
|
self.refresh_sidebar()
|
|
self.status_label.setText(f"Složka zavřena: {folder_name}")
|
|
|
|
def open_selected_files(self):
|
|
"""Open selected files"""
|
|
for f in self.get_selected_files():
|
|
self.open_file(f.file_path)
|
|
|
|
def remove_selected_files(self):
|
|
"""Remove selected files from index"""
|
|
files = self.get_selected_files()
|
|
if not files:
|
|
return
|
|
|
|
reply = QMessageBox.question(
|
|
self, "Smazat z indexu",
|
|
f"Odstranit {len(files)} souborů z indexu?"
|
|
)
|
|
if reply == QMessageBox.Yes:
|
|
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_label.setText(f"Odstraněno {len(files)} souborů z indexu")
|
|
|
|
def assign_tag_to_selected_bulk(self):
|
|
"""Assign tags to selected files"""
|
|
files = self.get_selected_files()
|
|
if not files:
|
|
self.status_label.setText("Nebyly vybrány žádné soubory")
|
|
return
|
|
|
|
all_tags = []
|
|
for category in self.tagmanager.get_categories():
|
|
for tag in self.tagmanager.get_tags_in_category(category):
|
|
all_tags.append(tag)
|
|
|
|
if not all_tags:
|
|
QMessageBox.warning(self, "Chyba", "Žádné tagy nejsou definovány")
|
|
return
|
|
|
|
dialog = MultiFileTagAssignDialog(
|
|
self, all_tags, files, self.category_colors
|
|
)
|
|
if dialog.exec() != QDialog.Accepted or dialog.result is None:
|
|
self.status_label.setText("Přiřazení zrušeno")
|
|
return
|
|
|
|
for full_path, state in dialog.result.items():
|
|
if state == 1:
|
|
if "/" in full_path:
|
|
category, name = full_path.split("/", 1)
|
|
tag_obj = self.tagmanager.add_tag(category, name)
|
|
self.filehandler.assign_tag_to_file_objects(files, tag_obj)
|
|
elif state == 0:
|
|
if "/" in full_path:
|
|
category, name = full_path.split("/", 1)
|
|
tag_obj = Tag(category, name)
|
|
self.filehandler.remove_tag_from_file_objects(files, tag_obj)
|
|
|
|
self.update_files_from_manager(self.filehandler.filelist)
|
|
self.status_label.setText("Hromadné přiřazení tagů dokončeno")
|
|
|
|
def set_date_for_selected(self):
|
|
"""Set date for selected files"""
|
|
files = self.get_selected_files()
|
|
if not files:
|
|
self.status_label.setText("Nebyly vybrány žádné soubory")
|
|
return
|
|
|
|
date_str, ok = QInputDialog.getText(
|
|
self, "Nastavit datum",
|
|
"Zadej datum ve formátu YYYY-MM-DD (nebo prázdné pro smazání):"
|
|
)
|
|
if not ok:
|
|
return
|
|
|
|
for f in files:
|
|
f.set_date(date_str if date_str != "" else None)
|
|
|
|
self.update_files_from_manager(self.filehandler.filelist)
|
|
self.status_label.setText(f"Nastaveno datum pro {len(files)} soubor(ů)")
|
|
|
|
def detect_video_resolution(self):
|
|
"""Detect video resolution using ffprobe"""
|
|
files = self.get_selected_files()
|
|
if not files:
|
|
self.status_label.setText("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:
|
|
pass
|
|
|
|
self.update_files_from_manager(self.filehandler.filelist)
|
|
self.status_label.setText(f"Přiřazeno rozlišení tagů k {count} souborům")
|
|
|
|
def set_ignore_patterns(self):
|
|
"""Set ignore patterns"""
|
|
current = ", ".join(self.filehandler.get_ignore_patterns())
|
|
patterns, ok = QInputDialog.getText(
|
|
self, "Ignore patterns",
|
|
"Zadej patterny oddělené čárkou (např. *.png, *.tmp):",
|
|
text=current
|
|
)
|
|
if not ok:
|
|
return
|
|
|
|
pattern_list = [p.strip() for p in patterns.split(",") if p.strip()]
|
|
self.filehandler.set_ignore_patterns(pattern_list)
|
|
self.update_files_from_manager(self.filehandler.filelist)
|
|
self.status_label.setText("Ignore patterns aktualizovány")
|
|
|
|
def toggle_hide_ignored(self):
|
|
"""Toggle hiding ignored files"""
|
|
self.hide_ignored = self.hide_ignored_action.isChecked()
|
|
self.update_files_from_manager(self.filehandler.filelist)
|
|
|
|
def toggle_show_path(self):
|
|
"""Toggle showing full path"""
|
|
self.show_full_path = self.full_path_cb.isChecked()
|
|
self.update_files_from_manager(self.filehandler.filelist)
|
|
|
|
def toggle_csfd_column(self):
|
|
"""Toggle CSFD column visibility"""
|
|
self.show_csfd_column = self.csfd_column_action.isChecked()
|
|
self.file_table.setColumnHidden(3, not self.show_csfd_column)
|
|
|
|
if self.filehandler.current_folder:
|
|
folder_config = self.filehandler.get_folder_config()
|
|
folder_config["show_csfd_column"] = self.show_csfd_column
|
|
self.filehandler.save_folder_config(config=folder_config)
|
|
|
|
def _update_csfd_column_visibility(self):
|
|
"""Update CSFD column from folder config"""
|
|
if self.filehandler.current_folder:
|
|
folder_config = self.filehandler.get_folder_config()
|
|
self.show_csfd_column = folder_config.get("show_csfd_column", True)
|
|
self.csfd_column_action.setChecked(self.show_csfd_column)
|
|
self.file_table.setColumnHidden(3, not self.show_csfd_column)
|
|
|
|
def set_csfd_url_for_selected(self):
|
|
"""Set CSFD URL for selected files"""
|
|
files = self.get_selected_files()
|
|
if not files:
|
|
self.status_label.setText("Nebyly vybrány žádné soubory")
|
|
return
|
|
|
|
current_url = files[0].csfd_url or ""
|
|
url, ok = QInputDialog.getText(
|
|
self, "Nastavit CSFD URL",
|
|
"Zadej CSFD URL:",
|
|
text=current_url
|
|
)
|
|
if not ok:
|
|
return
|
|
|
|
for f in files:
|
|
f.set_csfd_url(url if url != "" else None)
|
|
|
|
self.update_files_from_manager(self.filehandler.filelist)
|
|
self.status_label.setText(f"CSFD URL nastaveno pro {len(files)} soubor(ů)")
|
|
|
|
def apply_csfd_tags_for_selected(self):
|
|
"""Load tags from CSFD"""
|
|
files = self.get_selected_files()
|
|
if not files:
|
|
self.status_label.setText("Nebyly vybrány žádné soubory")
|
|
return
|
|
|
|
files_with_url = [f for f in files if f.csfd_url]
|
|
if not files_with_url:
|
|
QMessageBox.warning(
|
|
self, "Upozornění",
|
|
"Žádný z vybraných souborů nemá nastavenou CSFD URL"
|
|
)
|
|
return
|
|
|
|
self.status_label.setText(
|
|
f"Načítám tagy z CSFD pro {len(files_with_url)} souborů..."
|
|
)
|
|
QApplication.processEvents()
|
|
|
|
success_count = 0
|
|
error_count = 0
|
|
all_tags_added = []
|
|
|
|
for f in files_with_url:
|
|
result = f.apply_csfd_tags()
|
|
if result["success"]:
|
|
success_count += 1
|
|
all_tags_added.extend(result["tags_added"])
|
|
else:
|
|
error_count += 1
|
|
|
|
self.refresh_sidebar()
|
|
self.update_files_from_manager(self.filehandler.filelist)
|
|
|
|
if error_count > 0:
|
|
QMessageBox.warning(
|
|
self, "Dokončeno s chybami",
|
|
f"Úspěšně: {success_count}, Chyby: {error_count}\n"
|
|
f"Přidáno {len(all_tags_added)} tagů"
|
|
)
|
|
else:
|
|
self.status_label.setText(
|
|
f"Načteno z CSFD: {success_count} souborů, "
|
|
f"přidáno {len(all_tags_added)} tagů"
|
|
)
|
|
|
|
def on_filter_changed(self):
|
|
"""Handle search/filter change"""
|
|
self.update_files_from_manager(self.filehandler.filelist)
|
|
|
|
def refresh_all(self):
|
|
"""Refresh everything"""
|
|
self.refresh_sidebar()
|
|
self.update_files_from_manager(self.filehandler.filelist)
|
|
self.status_label.setText("Obnoveno")
|
|
|
|
def configure_hardlink_folder(self):
|
|
"""Configure hardlink output folder"""
|
|
if not self.filehandler.current_folder:
|
|
QMessageBox.warning(self, "Upozornění", "Nejprve otevřete složku")
|
|
return
|
|
|
|
folder_config = self.filehandler.get_folder_config()
|
|
current_dir = folder_config.get("hardlink_output_dir")
|
|
current_categories = folder_config.get("hardlink_categories")
|
|
|
|
initial_dir = current_dir if current_dir else str(self.filehandler.current_folder)
|
|
output_dir = QFileDialog.getExistingDirectory(
|
|
self, "Vyber cílovou složku pro hardlink strukturu",
|
|
initial_dir
|
|
)
|
|
if not output_dir:
|
|
return
|
|
|
|
categories = self.tagmanager.get_categories()
|
|
if not categories:
|
|
QMessageBox.warning(self, "Upozornění", "Žádné kategorie tagů")
|
|
return
|
|
|
|
dialog = CategorySelectionDialog(
|
|
self, categories, self.category_colors, current_categories
|
|
)
|
|
if dialog.exec() != QDialog.Accepted:
|
|
return
|
|
|
|
folder_config["hardlink_output_dir"] = output_dir
|
|
folder_config["hardlink_categories"] = dialog.result if dialog.result else None
|
|
self.filehandler.save_folder_config(config=folder_config)
|
|
|
|
QMessageBox.information(
|
|
self, "Hotovo", f"Hardlink složka nastavena:\n{output_dir}"
|
|
)
|
|
self.status_label.setText(f"Hardlink složka nastavena: {output_dir}")
|
|
|
|
def update_hardlink_structure(self):
|
|
"""Quick update hardlink structure"""
|
|
if not self.filehandler.current_folder:
|
|
QMessageBox.warning(self, "Upozornění", "Nejprve otevřete složku")
|
|
return
|
|
|
|
folder_config = self.filehandler.get_folder_config()
|
|
output_dir = folder_config.get("hardlink_output_dir")
|
|
saved_categories = folder_config.get("hardlink_categories")
|
|
|
|
if not output_dir:
|
|
QMessageBox.information(
|
|
self, "Info",
|
|
"Hardlink složka není nastavena.\n"
|
|
"Použijte 'Nastavit hardlink složku...' pro konfiguraci."
|
|
)
|
|
return
|
|
|
|
output_path = Path(output_dir)
|
|
files = self.filehandler.filelist
|
|
|
|
if not files:
|
|
QMessageBox.warning(self, "Upozornění", "Žádné soubory k zpracování")
|
|
return
|
|
|
|
manager = HardlinkManager(output_path)
|
|
preview_create = manager.get_preview(files, saved_categories)
|
|
obsolete = manager.find_obsolete_links(files, saved_categories)
|
|
|
|
to_create = []
|
|
for source, target in preview_create:
|
|
if not target.exists():
|
|
to_create.append((source, target))
|
|
elif not manager._is_same_file(source, target):
|
|
to_create.append((source, target))
|
|
|
|
if not to_create and not obsolete:
|
|
QMessageBox.information(
|
|
self, "Info",
|
|
"Struktura je již synchronizovaná, žádné změny nejsou potřeba"
|
|
)
|
|
return
|
|
|
|
confirm_lines = []
|
|
if to_create:
|
|
confirm_lines.append(f"Vytvořit: {len(to_create)} hardlinků")
|
|
if obsolete:
|
|
confirm_lines.append(f"Odebrat: {len(obsolete)} zastaralých hardlinků")
|
|
confirm_lines.append(f"\nCílová složka: {output_path}")
|
|
confirm_lines.append("\nPokračovat?")
|
|
|
|
reply = QMessageBox.question(
|
|
self, "Potvrdit aktualizaci", "\n".join(confirm_lines)
|
|
)
|
|
if reply != QMessageBox.Yes:
|
|
return
|
|
|
|
self.status_label.setText("Aktualizuji hardlink strukturu...")
|
|
QApplication.processEvents()
|
|
|
|
created, create_fail, removed, remove_fail = manager.sync_structure(
|
|
files, saved_categories
|
|
)
|
|
|
|
result_lines = []
|
|
if created > 0:
|
|
result_lines.append(f"Vytvořeno: {created} hardlinků")
|
|
if removed > 0:
|
|
result_lines.append(f"Odebráno: {removed} zastaralých hardlinků")
|
|
|
|
if create_fail > 0 or remove_fail > 0:
|
|
if create_fail > 0:
|
|
result_lines.append(f"Selhalo vytvoření: {create_fail}")
|
|
if remove_fail > 0:
|
|
result_lines.append(f"Selhalo odebrání: {remove_fail}")
|
|
QMessageBox.warning(
|
|
self, "Dokončeno s chybami", "\n".join(result_lines)
|
|
)
|
|
else:
|
|
QMessageBox.information(
|
|
self, "Hotovo",
|
|
"\n".join(result_lines) if result_lines else "Žádné změny"
|
|
)
|
|
|
|
self.status_label.setText(
|
|
f"Hardlink struktura aktualizována "
|
|
f"(vytvořeno: {created}, odebráno: {removed})"
|
|
)
|
|
|
|
def create_hardlink_structure(self):
|
|
"""Create hardlink structure with manual selection"""
|
|
files = self.filehandler.filelist
|
|
if not files:
|
|
QMessageBox.warning(self, "Upozornění", "Žádné soubory k zpracování")
|
|
return
|
|
|
|
output_dir = QFileDialog.getExistingDirectory(
|
|
self, "Vyber cílovou složku pro hardlink strukturu"
|
|
)
|
|
if not output_dir:
|
|
return
|
|
|
|
output_path = Path(output_dir)
|
|
|
|
categories = self.tagmanager.get_categories()
|
|
if not categories:
|
|
QMessageBox.warning(self, "Upozornění", "Žádné kategorie tagů")
|
|
return
|
|
|
|
dialog = CategorySelectionDialog(
|
|
self, categories, self.category_colors
|
|
)
|
|
if dialog.exec() != QDialog.Accepted:
|
|
return
|
|
|
|
cat_filter = dialog.result if dialog.result else None
|
|
|
|
manager = HardlinkManager(output_path)
|
|
preview_create = manager.get_preview(files, cat_filter)
|
|
obsolete = manager.find_obsolete_links(files, cat_filter)
|
|
|
|
to_create = []
|
|
for source, target in preview_create:
|
|
if not target.exists():
|
|
to_create.append((source, target))
|
|
elif not manager._is_same_file(source, target):
|
|
to_create.append((source, target))
|
|
|
|
if not to_create and not obsolete:
|
|
QMessageBox.information(
|
|
self, "Info",
|
|
"Struktura je již synchronizovaná, žádné změny nejsou potřeba"
|
|
)
|
|
return
|
|
|
|
confirm_lines = []
|
|
if to_create:
|
|
confirm_lines.append(f"Vytvořit: {len(to_create)} hardlinků")
|
|
if obsolete:
|
|
confirm_lines.append(f"Odebrat: {len(obsolete)} zastaralých hardlinků")
|
|
confirm_lines.append(f"\nCílová složka: {output_path}")
|
|
confirm_lines.append("\nPokračovat?")
|
|
|
|
reply = QMessageBox.question(
|
|
self, "Potvrdit synchronizaci", "\n".join(confirm_lines)
|
|
)
|
|
if reply != QMessageBox.Yes:
|
|
return
|
|
|
|
self.status_label.setText("Synchronizuji hardlink strukturu...")
|
|
QApplication.processEvents()
|
|
|
|
created, create_fail, removed, remove_fail = manager.sync_structure(
|
|
files, cat_filter
|
|
)
|
|
|
|
result_lines = []
|
|
if created > 0 or create_fail > 0:
|
|
result_lines.append(f"Vytvořeno: {created} hardlinků")
|
|
if create_fail > 0:
|
|
result_lines.append(f"Selhalo vytvoření: {create_fail}")
|
|
if removed > 0 or remove_fail > 0:
|
|
result_lines.append(f"Odebráno: {removed} zastaralých hardlinků")
|
|
if remove_fail > 0:
|
|
result_lines.append(f"Selhalo odebrání: {remove_fail}")
|
|
|
|
if create_fail > 0 or remove_fail > 0:
|
|
if manager.errors:
|
|
result_lines.append("\nChyby:")
|
|
for path, err in manager.errors[:5]:
|
|
result_lines.append(f"- {path.name}: {err}")
|
|
if len(manager.errors) > 5:
|
|
result_lines.append(
|
|
f"... a dalších {len(manager.errors) - 5} chyb"
|
|
)
|
|
QMessageBox.warning(
|
|
self, "Dokončeno s chybami", "\n".join(result_lines)
|
|
)
|
|
else:
|
|
QMessageBox.information(
|
|
self, "Hotovo",
|
|
"\n".join(result_lines) if result_lines else "Žádné změny"
|
|
)
|
|
|
|
self.status_label.setText(
|
|
f"Hardlink struktura synchronizována "
|
|
f"(vytvořeno: {created}, odebráno: {removed})"
|
|
)
|
|
|
|
def closeEvent(self, event):
|
|
"""Save window state on close"""
|
|
is_maximized = self.isMaximized()
|
|
self.filehandler.global_config["window_maximized"] = is_maximized
|
|
|
|
if not is_maximized:
|
|
geo = self.geometry()
|
|
self.filehandler.global_config["window_geometry"] = (
|
|
f"{geo.width()}x{geo.height()}+{geo.x()}+{geo.y()}"
|
|
)
|
|
|
|
save_global_config(self.filehandler.global_config)
|
|
event.accept()
|
|
|
|
|
|
class App:
|
|
"""Application wrapper for compatibility with existing entry point"""
|
|
|
|
def __init__(self, filehandler: FileManager, tagmanager: TagManager):
|
|
self.filehandler = filehandler
|
|
self.tagmanager = tagmanager
|
|
|
|
def main(self):
|
|
app = QApplication.instance()
|
|
if app is None:
|
|
app = QApplication(sys.argv)
|
|
|
|
window = MainWindow(self.filehandler, self.tagmanager)
|
|
window.show()
|
|
|
|
app.exec()
|