Программа принимает папку с документами, извлекает из них текст и метаданные и строит каталог: краткое описание, тезисы и ключевые слова для каждого файла плюс поиск по всему собранному. Ядро: - 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>
125 lines
4.6 KiB
Python
125 lines
4.6 KiB
Python
"""@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()
|