# 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.")