Enhance database schema and UI functionality for folder management and indexing

This commit is contained in:
2026-01-08 17:02:41 +01:00
parent 1821d51fcf
commit ae90ade6b8
2 changed files with 211 additions and 100 deletions

View File

@@ -3,14 +3,14 @@ import os
import sqlite3 import sqlite3
from pypdf import PdfReader from pypdf import PdfReader
# PyQt6 Module
from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QLineEdit, QPushButton, QLabel, QHBoxLayout, QLineEdit, QPushButton, QLabel,
QFileDialog, QTextBrowser, QProgressBar, QMessageBox) QFileDialog, QTextBrowser, QProgressBar, QMessageBox,
QListWidget, QListWidgetItem, QSplitter, QFrame)
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QUrl from PyQt6.QtCore import Qt, QThread, pyqtSignal, QUrl
from PyQt6.QtGui import QDesktopServices, QIcon from PyQt6.QtGui import QDesktopServices
# --- 1. BACKEND (Unverändert, nur ausgelagert) --- # --- 1. DATENBANK MANAGER ---
class DatabaseHandler: class DatabaseHandler:
def __init__(self, db_name="uff_index.db"): def __init__(self, db_name="uff_index.db"):
@@ -24,36 +24,70 @@ class DatabaseHandler:
CREATE VIRTUAL TABLE IF NOT EXISTS documents CREATE VIRTUAL TABLE IF NOT EXISTS documents
USING fts5(filename, path, content); USING fts5(filename, path, content);
""") """)
cursor.execute("""
CREATE TABLE IF NOT EXISTS folders (
path TEXT PRIMARY KEY,
alias TEXT
);
""")
conn.commit() conn.commit()
conn.close() conn.close()
def add_folder(self, path):
conn = sqlite3.connect(self.db_name)
try:
conn.execute("INSERT OR IGNORE INTO folders (path, alias) VALUES (?, ?)", (path, os.path.basename(path)))
conn.commit()
return True
except:
return False
finally:
conn.close()
def remove_folder(self, path):
conn = sqlite3.connect(self.db_name)
conn.execute("DELETE FROM folders WHERE path = ?", (path,))
conn.execute("DELETE FROM documents WHERE path LIKE ?", (f"{path}%",))
conn.commit()
conn.close()
def get_folders(self):
conn = sqlite3.connect(self.db_name)
rows = conn.execute("SELECT path FROM folders").fetchall()
conn.close()
return [r[0] for r in rows]
def search(self, query): def search(self, query):
conn = sqlite3.connect(self.db_name) conn = sqlite3.connect(self.db_name)
cursor = conn.cursor()
safe_query = query.replace('"', '""') safe_query = query.replace('"', '""')
sql = """ sql = """
SELECT filename, path, snippet(documents, 2, '<b>', '</b>', '...', 15) SELECT filename, path, snippet(documents, 2, '<b>', '</b>', '...', 15)
FROM documents FROM documents
WHERE documents MATCH ? WHERE documents MATCH ?
ORDER BY rank LIMIT 50 ORDER BY rank LIMIT 100
""" """
try: try:
results = cursor.execute(sql, (f"{safe_query}*",)).fetchall() results = conn.execute(sql, (f"{safe_query}*",)).fetchall()
except sqlite3.OperationalError: except:
results = [] results = []
conn.close() conn.close()
return results return results
# --- 2. WORKER THREAD (Damit die UI beim Scannen nicht einfriert) --- # --- 2. INDEXER (Mit Stop-Funktion) ---
class IndexerThread(QThread): class IndexerThread(QThread):
progress_signal = pyqtSignal(str) # Sendet Text an UI progress_signal = pyqtSignal(str)
finished_signal = pyqtSignal(int, int) # Sendet Statistiken (indexed, skipped) finished_signal = pyqtSignal(int, int, bool) # bool = Wurde abgebrochen?
def __init__(self, folder_path, db_name="uff_index.db"): def __init__(self, folder_path, db_name="uff_index.db"):
super().__init__() super().__init__()
self.folder_path = folder_path self.folder_path = folder_path
self.db_name = db_name self.db_name = db_name
self.is_running = True # Flag zum Steuern
def stop(self):
"""Setzt das Flag, damit der Loop stoppt."""
self.is_running = False
def _extract_text(self, filepath): def _extract_text(self, filepath):
ext = os.path.splitext(filepath)[1].lower() ext = os.path.splitext(filepath)[1].lower()
@@ -62,175 +96,252 @@ class IndexerThread(QThread):
reader = PdfReader(filepath) reader = PdfReader(filepath)
text = "" text = ""
for page in reader.pages: for page in reader.pages:
text_page = page.extract_text() if page_text := page.extract_text(): text += page_text + "\n"
if text_page: text += text_page + "\n"
return text return text
elif ext in [".txt", ".md", ".py", ".json", ".csv", ".html", ".log", ".ini"]: elif ext in [".txt", ".md", ".py", ".json", ".csv", ".html", ".log", ".ini", ".xml"]:
with open(filepath, "r", encoding="utf-8", errors="ignore") as f: with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
return f.read() return f.read()
return None return None
except Exception: except:
return None return None
def run(self): def run(self):
conn = sqlite3.connect(self.db_name) conn = sqlite3.connect(self.db_name)
cursor = conn.cursor()
# Alten Index leeren # Alten Inhalt des Ordners löschen
cursor.execute("DELETE FROM documents") conn.execute("DELETE FROM documents WHERE path LIKE ?", (f"{self.folder_path}%",))
conn.commit() conn.commit()
indexed = 0 indexed = 0
skipped = 0 skipped = 0
was_cancelled = False
for root, dirs, files in os.walk(self.folder_path): for root, dirs, files in os.walk(self.folder_path):
for file in files: # Check 1: Wurde Stop gedrückt?
self.progress_signal.emit(f"Scanne: {file}...") if not self.is_running:
path = os.path.join(root, file) was_cancelled = True
break
for file in files:
# Check 2: Auch innerhalb der Dateien prüfen für schnellere Reaktion
if not self.is_running:
was_cancelled = True
break
self.progress_signal.emit(f"Lese: {file}...")
path = os.path.join(root, file)
content = self._extract_text(path) content = self._extract_text(path)
if content and len(content.strip()) > 0: if content and len(content.strip()) > 0:
cursor.execute( conn.execute(
"INSERT INTO documents (filename, path, content) VALUES (?, ?, ?)", "INSERT INTO documents (filename, path, content) VALUES (?, ?, ?)",
(file, path, content) (file, path, content)
) )
indexed += 1 indexed += 1
else: else:
skipped += 1 skipped += 1
if was_cancelled:
break
conn.commit() conn.commit() # Wir speichern, was wir bis zum Abbruch geschafft haben
conn.close() conn.close()
self.finished_signal.emit(indexed, skipped) self.finished_signal.emit(indexed, skipped, was_cancelled)
# --- 3. FRONTEND (Das PyQt Fenster) --- # --- 3. UI ---
class UffWindow(QMainWindow): class UffWindow(QMainWindow):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.db = DatabaseHandler() self.db = DatabaseHandler()
self.indexer_thread = None
self.initUI() self.initUI()
self.load_saved_folders()
def initUI(self): def initUI(self):
self.setWindowTitle("UFF Text Search - PyQt Edition") self.setWindowTitle("UFF Text Search v2.1")
self.resize(800, 600) self.resize(1000, 700)
# Haupt-Container central = QWidget()
central_widget = QWidget() self.setCentralWidget(central)
self.setCentralWidget(central_widget) main_layout = QHBoxLayout(central)
layout = QVBoxLayout(central_widget)
# --- Header --- # --- LINKS ---
title = QLabel("UFF Text Search") left_panel = QFrame()
title.setStyleSheet("font-size: 24px; font-weight: bold; color: #333;") left_panel.setFixedWidth(250)
layout.addWidget(title) left_layout = QVBoxLayout(left_panel)
left_layout.setContentsMargins(0, 0, 0, 0)
# --- Ordner Auswahl --- lbl_folders = QLabel("📂 Meine Ordner")
folder_layout = QHBoxLayout() lbl_folders.setStyleSheet("font-weight: bold; font-size: 14px;")
self.btn_folder = QPushButton("Ordner wählen")
self.btn_folder.setCursor(Qt.CursorShape.PointingHandCursor)
self.btn_folder.clicked.connect(self.select_folder)
self.lbl_folder = QLabel("Kein Ordner gewählt") self.folder_list = QListWidget()
self.lbl_folder.setStyleSheet("color: gray; font-style: italic;") self.folder_list.setSelectionMode(QListWidget.SelectionMode.SingleSelection)
folder_layout.addWidget(self.btn_folder)
folder_layout.addWidget(self.lbl_folder)
folder_layout.addStretch()
layout.addLayout(folder_layout)
# --- Suche --- btn_add = QPushButton(" + Hinzufügen")
search_layout = QHBoxLayout() btn_add.clicked.connect(self.add_new_folder)
btn_remove = QPushButton(" - Entfernen")
btn_remove.clicked.connect(self.delete_selected_folder)
self.btn_rescan = QPushButton(" ↻ Neu scannen")
self.btn_rescan.clicked.connect(self.rescan_selected_folder)
# Der neue Abbrechen-Button (Standardmäßig unsichtbar)
self.btn_cancel = QPushButton("🛑 Abbrechen")
self.btn_cancel.setStyleSheet("background-color: #ffcccc; color: #cc0000; font-weight: bold;")
self.btn_cancel.clicked.connect(self.cancel_indexing)
self.btn_cancel.hide()
left_layout.addWidget(lbl_folders)
left_layout.addWidget(self.folder_list)
left_layout.addWidget(btn_add)
left_layout.addWidget(btn_remove)
left_layout.addStretch() # Spacer
left_layout.addWidget(self.btn_rescan)
left_layout.addWidget(self.btn_cancel) # Wird eingeblendet beim Scan
# --- RECHTS ---
right_panel = QWidget()
right_layout = QVBoxLayout(right_panel)
search_container = QHBoxLayout()
self.input_search = QLineEdit() self.input_search = QLineEdit()
self.input_search.setPlaceholderText("Suchbegriff eingeben und Enter drücken...") self.input_search.setPlaceholderText("Suchbegriff eingeben...")
self.input_search.returnPressed.connect(self.perform_search) self.input_search.returnPressed.connect(self.perform_search)
self.input_search.setStyleSheet("padding: 8px; font-size: 14px;")
self.btn_search = QPushButton("Suchen") btn_go = QPushButton("Suchen")
self.btn_search.clicked.connect(self.perform_search) btn_go.setFixedWidth(100)
btn_go.clicked.connect(self.perform_search)
search_container.addWidget(self.input_search)
search_container.addWidget(btn_go)
search_layout.addWidget(self.input_search)
search_layout.addWidget(self.btn_search)
layout.addLayout(search_layout)
# --- Status & Progress ---
self.lbl_status = QLabel("Bereit.") self.lbl_status = QLabel("Bereit.")
self.lbl_status.setStyleSheet("color: #666;")
self.progress_bar = QProgressBar() self.progress_bar = QProgressBar()
self.progress_bar.setRange(0, 0) # Infinite Loading Animation
self.progress_bar.hide() self.progress_bar.hide()
layout.addWidget(self.lbl_status)
layout.addWidget(self.progress_bar)
# --- Ergebnisse (Browser Engine für HTML Support) ---
self.result_browser = QTextBrowser() self.result_browser = QTextBrowser()
self.result_browser.setOpenExternalLinks(False) # Wir handeln Links selbst self.result_browser.setOpenExternalLinks(False)
self.result_browser.anchorClicked.connect(self.link_clicked) self.result_browser.anchorClicked.connect(self.link_clicked)
self.result_browser.setStyleSheet("background-color: white; padding: 10px; font-size: 14px;") self.result_browser.setStyleSheet("background-color: white; border: 1px solid #ccc;")
layout.addWidget(self.result_browser)
def select_folder(self): right_layout.addLayout(search_container)
right_layout.addWidget(self.lbl_status)
right_layout.addWidget(self.progress_bar)
right_layout.addWidget(self.result_browser)
splitter = QSplitter(Qt.Orientation.Horizontal)
splitter.addWidget(left_panel)
splitter.addWidget(right_panel)
splitter.setSizes([250, 750])
main_layout.addWidget(splitter)
# --- LOGIK ---
def load_saved_folders(self):
self.folder_list.clear()
folders = self.db.get_folders()
for f in folders:
item = QListWidgetItem(f)
item.setToolTip(f)
self.folder_list.addItem(item)
def add_new_folder(self):
folder = QFileDialog.getExistingDirectory(self, "Ordner wählen") folder = QFileDialog.getExistingDirectory(self, "Ordner wählen")
if folder: if folder:
self.lbl_folder.setText(folder) if self.db.add_folder(folder):
self.start_indexing(folder) self.load_saved_folders()
self.start_indexing(folder)
else:
QMessageBox.warning(self, "Info", "Ordner ist bereits vorhanden.")
def delete_selected_folder(self):
item = self.folder_list.currentItem()
if not item: return
path = item.text()
if QMessageBox.question(self, "Löschen", f"Ordner entfernen?\n{path}",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) == QMessageBox.StandardButton.Yes:
self.db.remove_folder(path)
self.load_saved_folders()
self.result_browser.clear()
self.lbl_status.setText("Ordner entfernt.")
def rescan_selected_folder(self):
item = self.folder_list.currentItem()
if not item:
QMessageBox.information(self, "Info", "Bitte Ordner links auswählen.")
return
self.start_indexing(item.text())
def start_indexing(self, folder): def start_indexing(self, folder):
self.btn_folder.setEnabled(False) self.set_ui_busy(True)
self.input_search.setEnabled(False) self.lbl_status.setText(f"Starte... {os.path.basename(folder)}")
self.progress_bar.show()
# Thread starten
self.indexer_thread = IndexerThread(folder) self.indexer_thread = IndexerThread(folder)
self.indexer_thread.progress_signal.connect(self.update_status) self.indexer_thread.progress_signal.connect(lambda msg: self.lbl_status.setText(msg))
self.indexer_thread.finished_signal.connect(self.indexing_finished) self.indexer_thread.finished_signal.connect(self.indexing_finished)
self.indexer_thread.start() self.indexer_thread.start()
def update_status(self, msg): def cancel_indexing(self):
self.lbl_status.setText(msg) if self.indexer_thread and self.indexer_thread.isRunning():
self.lbl_status.setText("Breche ab... Bitte warten...")
self.indexer_thread.stop()
# Wir warten nicht auf den Thread hier (non-blocking),
# das finished_signal kümmert sich um den Rest.
def indexing_finished(self, indexed, skipped): def indexing_finished(self, indexed, skipped, was_cancelled):
self.progress_bar.hide() self.set_ui_busy(False)
self.btn_folder.setEnabled(True) if was_cancelled:
self.input_search.setEnabled(True) self.lbl_status.setText(f"Abgebrochen. ({indexed} indiziert).")
self.lbl_status.setText(f"Fertig! {indexed} Dateien indiziert ({skipped} übersprungen).") QMessageBox.information(self, "Abbruch", f"Vorgang vom Benutzer abgebrochen.\nBis dahin indiziert: {indexed}")
QMessageBox.information(self, "Scan beendet", f"{indexed} Dateien wurden erfolgreich indiziert.") else:
self.lbl_status.setText(f"Fertig. {indexed} neu, {skipped} übersprungen.")
QMessageBox.information(self, "Fertig", f"Scan abgeschlossen!\n{indexed} Dateien im Index.")
def set_ui_busy(self, busy):
# Steuert die Buttons während des Scans
self.input_search.setEnabled(not busy)
self.folder_list.setEnabled(not busy)
self.btn_rescan.setVisible(not busy) # Rescan verstecken
self.btn_cancel.setVisible(busy) # Abbrechen zeigen
if busy:
self.progress_bar.setRange(0, 0)
self.progress_bar.show()
else:
self.progress_bar.hide()
def perform_search(self): def perform_search(self):
query = self.input_search.text() query = self.input_search.text()
if not query: return if not query: return
results = self.db.search(query) results = self.db.search(query)
self.lbl_status.setText(f"{len(results)} Treffer gefunden.") self.lbl_status.setText(f"{len(results)} Treffer.")
# HTML bauen für die Anzeige html = ""
html_content = ""
if not results: if not results:
html_content = "<p style='color: gray;'>Keine Ergebnisse gefunden.</p>" html = "<h3 style='color: gray; text-align: center; margin-top: 20px;'>Nichts gefunden.</h3>"
for filename, filepath, snippet in results: for filename, filepath, snippet in results:
# Wir nutzen den Dateipfad als Link-URL
file_url = QUrl.fromLocalFile(filepath).toString() file_url = QUrl.fromLocalFile(filepath).toString()
html += f"""
html_content += f""" <div style='margin-bottom: 10px; padding: 10px; background-color: #f9f9f9; border-left: 4px solid #2980b9;'>
<div style='margin-bottom: 15px; border-bottom: 1px solid #ddd; padding-bottom: 5px;'>
<a href="{file_url}" style='font-size: 16px; font-weight: bold; color: #2980b9; text-decoration: none;'> <a href="{file_url}" style='font-size: 16px; font-weight: bold; color: #2980b9; text-decoration: none;'>
📄 {filename} {filename}
</a> </a>
<div style='color: #444; margin-top: 5px; font-family: sans-serif;'> <div style='color: #333; margin-top: 5px; font-family: sans-serif; font-size: 13px;'>{snippet}</div>
...{snippet}... <div style='color: #999; font-size: 11px; margin-top: 4px;'>{filepath}</div>
</div>
<div style='color: #888; font-size: 10px; margin-top: 2px;'>
{filepath}
</div>
</div> </div>
""" """
self.result_browser.setHtml(html)
self.result_browser.setHtml(html_content)
def link_clicked(self, url): def link_clicked(self, url):
# Öffnet die Datei mit dem Standard-Programm des Betriebssystems
QDesktopServices.openUrl(url) QDesktopServices.openUrl(url)
# --- APP START ---
if __name__ == "__main__": if __name__ == "__main__":
app = QApplication(sys.argv) app = QApplication(sys.argv)
window = UffWindow() window = UffWindow()

Binary file not shown.