made modular

This commit is contained in:
2026-01-10 13:07:42 +01:00
parent afd3ae1cc4
commit 74fc2faa82
6 changed files with 622 additions and 716 deletions

266
ui.py Normal file
View File

@@ -0,0 +1,266 @@
# ui.py
from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QLineEdit, QPushButton, QLabel, QFileDialog,
QProgressBar, QMessageBox, QListWidget, QListWidgetItem,
QSplitter, QFrame, QScrollArea, QStyle, QGraphicsDropShadowEffect)
from PyQt6.QtCore import Qt, QUrl, QThread, pyqtSignal
from PyQt6.QtGui import QDesktopServices, QColor, QFont
from sentence_transformers import SentenceTransformer
from database import DatabaseHandler
from indexer import IndexerThread
from config import STYLESHEET
# Thread zum Laden des Modells (UI-Helper)
class ModelLoaderThread(QThread):
model_loaded = pyqtSignal(object)
def run(self):
try:
model = SentenceTransformer('all-MiniLM-L6-v2')
self.model_loaded.emit(model)
except: self.model_loaded.emit(None)
# Widget für einzelne Ergebnisse
class SearchResultItem(QFrame):
def __init__(self, filename, filepath, snippet, parent=None):
super().__init__(parent)
self.filepath = filepath
self.setToolTip(filepath)
self.setFrameShape(QFrame.Shape.StyledPanel)
self.setStyleSheet("""
SearchResultItem { background-color: white; border: 1px solid #e0e0e0; border-radius: 8px; }
SearchResultItem:hover { border: 1px solid #3498db; background-color: #fbfbfb; }
""")
shadow = QGraphicsDropShadowEffect(self)
shadow.setBlurRadius(10)
shadow.setXOffset(0)
shadow.setYOffset(2)
shadow.setColor(QColor(0, 0, 0, 30))
self.setGraphicsEffect(shadow)
layout = QVBoxLayout(self)
layout.setContentsMargins(15, 15, 15, 15)
layout.setSpacing(5)
self.btn_title = QPushButton(filename)
self.btn_title.setCursor(Qt.CursorShape.PointingHandCursor)
self.btn_title.setMouseTracking(True)
self.btn_title.setStyleSheet("""
QPushButton { text-align: left; font-weight: bold; font-size: 16px; color: #2c3e50; border: none; background: transparent; padding: 0px; }
QPushButton:hover { color: #3498db; text-decoration: underline; }
""")
self.btn_title.clicked.connect(self.open_file)
self.lbl_snippet = QLabel(snippet)
self.lbl_snippet.setWordWrap(True)
self.lbl_snippet.setStyleSheet("color: #555; font-size: 13px; line-height: 1.4;")
path_layout = QHBoxLayout()
lbl_icon = QLabel("📄")
lbl_icon.setStyleSheet("font-size: 10px; color: #95a5a6;")
self.lbl_path = QLabel(filepath)
self.lbl_path.setStyleSheet("color: #95a5a6; font-size: 11px;")
path_layout.addWidget(lbl_icon)
path_layout.addWidget(self.lbl_path)
path_layout.addStretch()
layout.addWidget(self.btn_title)
layout.addWidget(self.lbl_snippet)
layout.addLayout(path_layout)
def open_file(self):
target = self.filepath.split(" :: ")[0] if " :: " in self.filepath else self.filepath
QDesktopServices.openUrl(QUrl.fromLocalFile(target))
class UffWindow(QMainWindow):
def __init__(self, splash=None):
super().__init__()
self.splash = splash
self.db = DatabaseHandler()
self.initUI()
self.load_saved_folders()
def initUI(self):
self.setWindowTitle("UFF Search v8.0 (Modular)")
self.resize(1100, 750)
self.setStyleSheet(STYLESHEET)
central = QWidget()
self.setCentralWidget(central)
main_layout = QHBoxLayout(central)
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.setSpacing(0)
# -- SIDEBAR --
left_panel = QFrame()
left_panel.setObjectName("Sidebar")
left_panel.setFixedWidth(260)
left = QVBoxLayout(left_panel)
left.setContentsMargins(0, 20, 0, 20)
lbl_title = QLabel(" UFF SEARCH")
lbl_title.setObjectName("SidebarTitle")
self.folder_list = QListWidget()
btn_add = QPushButton(" Ordner hinzufügen")
btn_add.setObjectName("SidebarBtn")
btn_add.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_FileDialogNewFolder))
btn_add.clicked.connect(self.add_new_folder)
btn_del = QPushButton(" Ordner entfernen")
btn_del.setObjectName("SidebarBtn")
btn_del.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_TrashIcon))
btn_del.clicked.connect(self.delete_selected_folder)
self.btn_rescan = QPushButton(" Neu scannen")
self.btn_rescan.setObjectName("SidebarBtn")
self.btn_rescan.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_BrowserReload))
self.btn_rescan.clicked.connect(self.rescan)
self.btn_cancel = QPushButton("STOPPEN")
self.btn_cancel.setObjectName("CancelBtn")
self.btn_cancel.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_DialogCancelButton))
self.btn_cancel.clicked.connect(self.cancel_idx)
self.btn_cancel.hide()
left.addWidget(lbl_title)
left.addSpacing(10)
left.addWidget(self.folder_list)
left.addSpacing(10)
left.addWidget(btn_add)
left.addWidget(btn_del)
left.addWidget(self.btn_rescan)
left.addWidget(self.btn_cancel)
# -- MAIN AREA --
right_panel = QWidget()
right_panel.setObjectName("MainArea")
right = QVBoxLayout(right_panel)
right.setContentsMargins(30, 30, 30, 30)
right.setSpacing(15)
search_box = QHBoxLayout()
self.input = QLineEdit()
self.input.setPlaceholderText("Wonach suchst du heute?")
self.input.returnPressed.connect(self.search)
self.btn_go = QPushButton("Suchen")
self.btn_go.setObjectName("SearchBtn")
self.btn_go.setCursor(Qt.CursorShape.PointingHandCursor)
self.btn_go.clicked.connect(self.search)
search_box.addWidget(self.input)
search_box.addWidget(self.btn_go)
status_box = QHBoxLayout()
self.lbl_status = QLabel("Modell wird geladen...")
self.lbl_status.setObjectName("StatusLabel")
self.prog = QProgressBar()
self.prog.hide()
status_box.addWidget(self.lbl_status)
status_box.addWidget(self.prog)
self.scroll = QScrollArea()
self.scroll.setWidgetResizable(True)
self.res_cont = QWidget()
self.res_cont.setObjectName("ResultsContainer")
self.res_layout = QVBoxLayout(self.res_cont)
self.res_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
self.res_layout.setSpacing(15)
self.scroll.setWidget(self.res_cont)
right.addLayout(search_box)
right.addLayout(status_box)
right.addWidget(self.scroll)
main_layout.addWidget(left_panel)
main_layout.addWidget(right_panel)
self.set_ui_enabled(False)
def set_ui_enabled(self, enabled):
self.input.setEnabled(enabled)
self.btn_go.setEnabled(enabled)
self.folder_list.setEnabled(enabled)
def start_model_loading(self):
if self.splash: self.splash.showMessage("Lade KI-Modell...", Qt.AlignmentFlag.AlignBottom, Qt.GlobalColor.white)
self.loader = ModelLoaderThread()
self.loader.model_loaded.connect(self.on_model_loaded)
self.loader.start()
def on_model_loaded(self, model):
if self.splash: self.splash.finish(self)
if not model:
QMessageBox.critical(self, "Fehler", "Modell konnte nicht geladen werden.")
return
self.db.model = model
self.lbl_status.setText("Bereit für deine Suche.")
self.set_ui_enabled(True)
def search(self):
query = self.input.text()
if not query: return
self.lbl_status.setText("Suche läuft...")
QApplication.processEvents()
while self.res_layout.count():
child = self.res_layout.takeAt(0)
if child.widget(): child.widget().deleteLater()
results = self.db.search(query)
self.lbl_status.setText(f"{len(results)} Treffer gefunden.")
if not results:
lbl = QLabel("Leider keine Ergebnisse.")
lbl.setStyleSheet("color: #95a5a6; font-size: 18px; margin-top: 40px;")
lbl.setAlignment(Qt.AlignmentFlag.AlignHCenter)
self.res_layout.addWidget(lbl)
else:
for fname, fpath, snippet in results:
self.res_layout.addWidget(SearchResultItem(fname, fpath, snippet))
self.res_layout.addStretch()
def load_saved_folders(self):
self.folder_list.clear()
for f in self.db.get_folders():
item = QListWidgetItem(self.style().standardIcon(QStyle.StandardPixmap.SP_DirIcon), f)
item.setToolTip(f)
self.folder_list.addItem(item)
def add_new_folder(self):
f = QFileDialog.getExistingDirectory(self, "Ordner wählen")
if f and self.db.add_folder(f):
self.load_saved_folders()
self.start_idx(f)
def delete_selected_folder(self):
item = self.folder_list.currentItem()
if item and QMessageBox.question(self, "Löschen", f"Weg damit?\n{item.text()}", QMessageBox.StandardButton.Yes|QMessageBox.StandardButton.No) == QMessageBox.StandardButton.Yes:
self.db.remove_folder(item.text())
self.load_saved_folders()
def rescan(self):
if item := self.folder_list.currentItem(): self.start_idx(item.text())
def start_idx(self, folder):
if not self.db.model: return
self.set_ui_enabled(False)
self.btn_cancel.show(); self.btn_rescan.hide(); self.prog.show()
self.idx_thread = IndexerThread(folder, self.db.db_name, self.db.model)
self.idx_thread.progress_signal.connect(self.lbl_status.setText)
self.idx_thread.finished_signal.connect(self.idx_done)
self.idx_thread.start()
def cancel_idx(self):
if self.idx_thread: self.idx_thread.stop()
def idx_done(self, n, s, c):
self.set_ui_enabled(True)
self.btn_cancel.hide(); self.btn_rescan.show(); self.prog.hide()
msg = "Abgebrochen" if c else "Indexierung fertig"
self.lbl_status.setText(f"{msg}: {n} neu, {s} übersprungen.")