Files
catalog_doc/catalogizer/ui/main_window.py
Andrey Kruchinkin 49f91247c7 Каталогизатор документов: разбор папки, тезисы, поиск, GUI
Программа принимает папку с документами, извлекает из них текст и
метаданные и строит каталог: краткое описание, тезисы и ключевые слова
для каждого файла плюс поиск по всему собранному.

Ядро:
- extract: PDF, DJVU, DOC(X), RTF, ODT, CHM, EPUB, FB2, PPT(X), XLS(X), TXT;
  PDF передаётся MuPDF потоком в память (работают длинные пути) и ограничен
  по времени — повреждённый файл иначе чинится минутами;
- summarize: экстрактивное реферирование по TF-IDF, посчитанному на самой
  библиотеке, с лёгким стеммером для русского и английского;
- db: SQLite с полнотекстовым индексом FTS5, морфологический поиск с BM25;
- scanner: инкрементальный многопоточный обход, счётчик попыток защищает
  от файла, обрывающего разбор;
- report: автономный HTML с поиском, Markdown, JSON, CSV.

Интерфейс:
- окно PySide6 в тёмной теме: вкладки разбора и поиска, фоновый поток,
  карточка документа, правка своих ключевых слов;
- командная строка: build, scan, analyze, report, search, show, tag, stats;
- у каждой папки свой каталог, последняя обработанная запоминается.

Сборка: scripts/build_exe.py даёт Catalogizer.exe в Windows и
Catalogizer.app в macOS, иконки .ico и .icns рисуются кодом.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 19:20:29 +03:00

125 lines
4.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""@file main_window.py
@brief Главное окно каталогизатора: шапка, вкладки разбора и поиска.
Обе вкладки работают с одной и той же папкой: смена папки на вкладке
разбора сразу переключает каталог, по которому идёт поиск.
"""
from __future__ import annotations
from pathlib import Path
from PySide6.QtWidgets import (
QFrame,
QHBoxLayout,
QLabel,
QMainWindow,
QTabWidget,
QVBoxLayout,
QWidget,
)
from .. import config, db
from .catalog_tab import CatalogTab
from .scan_tab import ScanTab
from .theme import APP_STYLESHEET
class MainWindow(QMainWindow):
"""@brief Окно с вкладками «Каталог и поиск» и «Разбор папки»."""
def __init__(self, root: Path | None = None,
parent: QWidget | None = None) -> None:
super().__init__(parent)
self.setWindowTitle("Каталогизатор документов")
self.resize(1280, 820)
self.setMinimumSize(1000, 640)
self.setStyleSheet(APP_STYLESHEET)
self.catalog_tab = CatalogTab()
self.scan_tab = ScanTab()
central = QWidget()
layout = QVBoxLayout(central)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
layout.addWidget(self._build_header())
layout.addWidget(self._build_tabs(), 1)
self.setCentralWidget(central)
self.scan_tab.catalog_ready.connect(self._on_catalog_ready)
self.scan_tab.root_changed.connect(self._on_root_changed)
self.set_root(Path(root) if root else config.current_root())
# ------------------------------------------------------------------
def _build_header(self) -> QFrame:
header = QFrame()
header.setObjectName("header")
row = QHBoxLayout(header)
row.setContentsMargins(18, 12, 18, 12)
titles = QVBoxLayout()
title = QLabel("Каталогизатор")
title.setProperty("title", True)
subtitle = QLabel("Описания, тезисы и поиск по папке с документами")
subtitle.setProperty("muted", True)
titles.addWidget(title)
titles.addWidget(subtitle)
row.addLayout(titles)
row.addStretch(1)
self.summary = QLabel("")
self.summary.setProperty("muted", True)
row.addWidget(self.summary)
return header
def _build_tabs(self) -> QTabWidget:
self.tabs = QTabWidget()
self.tabs.addTab(self.catalog_tab, "Каталог и поиск")
self.tabs.addTab(self.scan_tab, "Разбор папки")
return self.tabs
# ------------------------------------------------------------------
def set_root(self, root: Path) -> None:
"""@brief Переключает обе вкладки на указанную папку."""
self.scan_tab.set_root(root)
self.catalog_tab.set_root(root)
self._update_summary(root)
def _on_root_changed(self, text: str) -> None:
root = Path(text.strip('" '))
self.catalog_tab.set_root(root)
self._update_summary(root)
def _on_catalog_ready(self, text: str) -> None:
root = Path(text)
self.catalog_tab.set_root(root)
self._update_summary(root)
self.tabs.setCurrentWidget(self.catalog_tab)
def _update_summary(self, root: Path) -> None:
"""@brief Показывает в шапке, что уже есть в каталоге этой папки."""
try:
db_path, _ = config.paths_for(root)
con = db.connect(db_path, create=False)
total = con.execute("SELECT COUNT(*) c FROM docs").fetchone()["c"]
scans = con.execute(
"SELECT COUNT(*) c FROM docs WHERE scanned = 1").fetchone()["c"]
when = db.get_meta(con, "last_scan")
con.close()
except Exception: # noqa: BLE001 — база может быть ещё не создана
self.summary.setText("")
return
if not total:
self.summary.setText("Каталог пуст — постройте его на вкладке «Разбор папки»")
return
self.summary.setText(
"В каталоге %d документов, из них %d сканов · обновлён %s"
% (total, scans, when or ""))
def closeEvent(self, event) -> None: # noqa: N802 — имя из Qt
if self.scan_tab.busy():
self.scan_tab.stop_build()
event.accept()