From afd3ae1cc442e9220eb3fee4d7ab98e00f7185d9 Mon Sep 17 00:00:00 2001 From: Konstantin Date: Sat, 10 Jan 2026 12:58:18 +0100 Subject: [PATCH] make it beauty and shiny --- requirements.txt | 4 +- uff_app.py | 375 +++++++++++++++++++++++++++++++++-------------- 2 files changed, 272 insertions(+), 107 deletions(-) diff --git a/requirements.txt b/requirements.txt index 120d033..4cdff9b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,4 +6,6 @@ sentence-transformers==2.2.2 transformers==4.28.1 torch==1.13.1 numpy==1.24.2 -python-docx \ No newline at end of file +python-docx +openpyxl +python-pptx \ No newline at end of file diff --git a/uff_app.py b/uff_app.py index 4c7b3b2..1c81d3a 100644 --- a/uff_app.py +++ b/uff_app.py @@ -7,11 +7,21 @@ import zipfile import io import traceback -# NEU: Word Support +# --- OPTIONALE IMPORTE --- try: import docx except ImportError: - docx = None # Fallback, falls nicht installiert + docx = None + +try: + import openpyxl +except ImportError: + openpyxl = None + +try: + from pptx import Presentation +except ImportError: + Presentation = None from sentence_transformers import SentenceTransformer, util from rapidfuzz import process, fuzz @@ -21,8 +31,8 @@ from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLineEdit, QPushButton, QLabel, QFileDialog, QProgressBar, QMessageBox, QListWidget, QListWidgetItem, QSplitter, QFrame, - QSplashScreen, QScrollArea, QStyle) -from PyQt6.QtGui import QDesktopServices, QPixmap, QCursor, QAction + QSplashScreen, QScrollArea, QStyle, QGraphicsDropShadowEffect) +from PyQt6.QtGui import QDesktopServices, QPixmap, QCursor, QAction, QColor, QPalette, QFont # --- 0. LOGGING & SETUP --- @@ -51,12 +61,117 @@ class Logger(object): sys.stdout = Logger() sys.stderr = sys.stdout -print(f"--- START LOGGING ---") -print(f"Logfile: {log_file_path}") +# --- STYLESHEET --- +STYLESHEET = """ +QMainWindow { + background-color: #f4f7f6; +} -# Check ob docx installiert ist und warnen im Log -if docx is None: - print("WARNUNG: 'python-docx' ist nicht installiert. .docx Dateien werden ignoriert.") +/* Sidebar Styles */ +QFrame#Sidebar { + background-color: #2c3e50; + border: none; +} +QLabel#SidebarTitle { + color: #ecf0f1; + font-weight: bold; + font-size: 16px; + padding: 10px; +} +QListWidget { + background-color: #34495e; + color: #ecf0f1; + border: none; + outline: none; + font-size: 13px; +} +QListWidget::item { + padding: 8px; + border-bottom: 1px solid #2c3e50; +} +QListWidget::item:selected { + background-color: #1abc9c; + color: white; +} +QListWidget::item:hover { + background-color: #16a085; +} + +/* Sidebar Buttons */ +QPushButton#SidebarBtn { + background-color: #34495e; + color: #bdc3c7; + border: 1px solid #2c3e50; + padding: 8px; + text-align: left; + border-radius: 4px; + margin: 2px 10px; +} +QPushButton#SidebarBtn:hover { + background-color: #1abc9c; + color: white; + border: 1px solid #16a085; +} +QPushButton#CancelBtn { + background-color: #e74c3c; + color: white; + font-weight: bold; + border-radius: 4px; + margin: 10px; + padding: 8px; +} + +/* Main Area */ +QLineEdit { + padding: 10px; + border: 1px solid #bdc3c7; + border-radius: 20px; + font-size: 14px; + background-color: white; + selection-background-color: #3498db; +} +QLineEdit:focus { + border: 2px solid #3498db; +} + +QPushButton#SearchBtn { + background-color: #3498db; + color: white; + font-weight: bold; + border-radius: 20px; + padding: 10px 20px; + font-size: 14px; +} +QPushButton#SearchBtn:hover { + background-color: #2980b9; +} +QPushButton#SearchBtn:pressed { + background-color: #1f618d; +} + +/* Scroll Area & Results */ +QScrollArea { + border: none; + background-color: transparent; +} +QWidget#ResultsContainer { + background-color: transparent; +} +QLabel#StatusLabel { + color: #7f8c8d; + font-size: 12px; + margin-left: 10px; +} +QProgressBar { + border: none; + background-color: #ecf0f1; + height: 4px; + text-align: center; +} +QProgressBar::chunk { + background-color: #1abc9c; +} +""" def qt_message_handler(mode, context, message): msg_lower = message.lower() @@ -65,7 +180,6 @@ def qt_message_handler(mode, context, message): "unable to create font", "fontbbox", "script" ] if any(k in msg_lower for k in ignore_keywords): return - try: sys.stdout.write(f"[Qt] {message}\n") except: pass @@ -74,45 +188,58 @@ qInstallMessageHandler(qt_message_handler) os.environ["QT_LOGGING_RULES"] = "qt.text.font.db=false;qt.qpa.fonts=false" -# --- NEUE KOMPONENTE: Ein einzelnes Suchergebnis als Widget --- +# --- WIDGET: Modernes Suchergebnis (Fixed Tooltips) --- class SearchResultItem(QFrame): - """ - Stellt ein einzelnes Suchergebnis als 'Karte' dar. - """ def __init__(self, filename, filepath, snippet, parent=None): super().__init__(parent) self.filepath = filepath + # WICHTIG: Tooltip auf das gesamte Frame setzen, nicht nur auf Kinder + self.setToolTip(filepath) + + # Design der Karte self.setFrameShape(QFrame.Shape.StyledPanel) self.setStyleSheet(""" SearchResultItem { - background-color: #ffffff; - border: 1px solid #ddd; - border-radius: 5px; - margin-bottom: 5px; + background-color: white; + border: 1px solid #e0e0e0; + border-radius: 8px; } SearchResultItem:hover { - background-color: #f0f8ff; - border: 1px solid #2980b9; + border: 1px solid #3498db; + background-color: #fbfbfb; } """) + + # Schatten-Effekt + 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(10, 10, 10, 10) + layout.setContentsMargins(15, 15, 15, 15) + layout.setSpacing(5) - # 1. Dateiname + # 1. Titel (Dateiname) self.btn_title = QPushButton(filename) self.btn_title.setCursor(Qt.CursorShape.PointingHandCursor) + # MouseTracking aktivieren hilft manchmal bei schnellen Bewegungen + self.btn_title.setMouseTracking(True) self.btn_title.setStyleSheet(""" QPushButton { text-align: left; font-weight: bold; - font-size: 14pt; - color: #2980b9; + font-size: 16px; + color: #2c3e50; border: none; background: transparent; + padding: 0px; } QPushButton:hover { + color: #3498db; text-decoration: underline; } """) @@ -121,26 +248,30 @@ class SearchResultItem(QFrame): # 2. Snippet self.lbl_snippet = QLabel(snippet) self.lbl_snippet.setWordWrap(True) - self.lbl_snippet.setStyleSheet("color: #444; font-size: 10pt; margin-top: 5px;") + self.lbl_snippet.setStyleSheet("color: #555; font-size: 13px; line-height: 1.4;") + + # 3. Pfad (unten, klein) + path_layout = QHBoxLayout() + lbl_icon = QLabel("📄") + lbl_icon.setStyleSheet("font-size: 10px; color: #95a5a6;") - # 3. Pfad self.lbl_path = QLabel(filepath) - self.lbl_path.setStyleSheet("color: #888; font-size: 8pt; margin-top: 5px;") + 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.addWidget(self.lbl_path) + layout.addLayout(path_layout) def open_file(self): - print(f"Öffne Datei: {self.filepath}") target_path = self.filepath if " :: " in target_path: target_path = target_path.split(" :: ")[0] - url = QUrl.fromLocalFile(target_path) - success = QDesktopServices.openUrl(url) - if not success: - print("Fehler: Konnte Datei nicht öffnen.") + QDesktopServices.openUrl(url) # --- 1. DATENBANK MANAGER --- @@ -191,7 +322,6 @@ class DatabaseHandler: def search(self, query): if not query.strip() or not self.model: return [] - # 1. Semantik q_vec = self.model.encode(query, convert_to_tensor=False) conn = sqlite3.connect(self.db_name) cursor = conn.cursor() @@ -207,7 +337,6 @@ class DatabaseHandler: scores = np.clip(scores, 0, 1) sem_map = {did: float(s) for did, s in zip(doc_ids, scores)} - # 2. Lexikalisch words = query.replace('"', '').split() if not words: words = [query] fts_query = " OR ".join([f'"{w}"*' for w in words]) @@ -222,7 +351,6 @@ class DatabaseHandler: r2 = fuzz.partial_token_set_ratio(query.lower(), content[:5000].lower()) lex_map[did] = max(r1, r2) / 100.0 - # 3. Hybrid final = {} ALPHA = 0.65 BETA = 0.35 @@ -233,7 +361,6 @@ class DatabaseHandler: if s_score > 0.4 and l_score > 0.6: h_score += 0.1 final[did] = h_score - # 4. Fetch sorted_ids = sorted(final.keys(), key=lambda x: final[x], reverse=True)[:50] results = [] for did in sorted_ids: @@ -269,7 +396,6 @@ class IndexerThread(QThread): ext = os.path.splitext(filename)[1].lower() text = "" try: - # --- PDF --- if ext == ".pdf": try: with pdfplumber.open(stream) as pdf: @@ -277,17 +403,34 @@ class IndexerThread(QThread): if t := p.extract_text(): text += t + "\n" except: pass - # --- WORD / DOCX --- elif ext == ".docx" and docx is not None: try: - # python-docx kann file-like objects (BytesIO oder file) lesen doc = docx.Document(stream) - for para in doc.paragraphs: - text += para.text + "\n" - except Exception as e: - print(f"Docx Error {filename}: {e}") + for para in doc.paragraphs: text += para.text + "\n" + except: pass + + elif ext == ".xlsx" and openpyxl is not None: + try: + wb = openpyxl.load_workbook(stream, data_only=True, read_only=True) + for sheet in wb.worksheets: + text += f"\n--- {sheet.title} ---\n" + for row in sheet.iter_rows(values_only=True): + row_text = " ".join([str(c) for c in row if c is not None]) + if row_text.strip(): text += row_text + "\n" + except: pass + + elif ext == ".pptx" and Presentation is not None: + try: + prs = Presentation(stream) + for i, slide in enumerate(prs.slides): + text += f"\n--- Folie {i+1} ---\n" + for shape in slide.shapes: + if shape.has_text_frame: + for paragraph in shape.text_frame.paragraphs: + for run in paragraph.runs: text += run.text + " " + text += "\n" + except: pass - # --- PLAIN TEXT --- elif ext in [".txt", ".md", ".py", ".json", ".csv", ".html", ".log", ".ini", ".xml"]: try: content = stream.read() @@ -301,7 +444,6 @@ class IndexerThread(QThread): conn = sqlite3.connect(self.db_name) cursor = conn.cursor() - # Cleanup cursor.execute("SELECT rowid FROM documents WHERE path LIKE ?", (f"{self.folder_path}%",)) ids = [r[0] for r in cursor.fetchall()] if ids: @@ -333,12 +475,16 @@ class IndexerThread(QThread): indexed += 1 except: skipped += 1 else: - with open(path, "rb") as f: - content = self._extract_text(f, file) - if content and len(content.strip()) > 20: - self._save(cursor, file, path, content) - indexed += 1 - else: skipped += 1 + try: + with open(path, "rb") as f: + file_content = io.BytesIO(f.read()) + content = self._extract_text(file_content, file) + if content and len(content.strip()) > 20: + self._save(cursor, file, path, content) + indexed += 1 + else: skipped += 1 + except: skipped += 1 + if cancelled: break conn.commit() @@ -351,7 +497,7 @@ class IndexerThread(QThread): vec = self.model.encode(content[:8000], convert_to_tensor=False).tobytes() cursor.execute("INSERT INTO embeddings (doc_id, vec) VALUES (?, ?)", (did, vec)) -# --- 3. UI --- +# --- 3. UI MAIN WINDOW --- class UffWindow(QMainWindow): def __init__(self, splash=None): @@ -362,90 +508,111 @@ class UffWindow(QMainWindow): self.load_saved_folders() def initUI(self): - self.setWindowTitle("UFF Search") - self.resize(1000, 700) + self.setWindowTitle("UFF Search v7.2 (Stable Tooltips)") + 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) - # -- LINKS (Sidebar) -- + # -- SIDEBAR -- left_panel = QFrame() - left_panel.setFixedWidth(250) - left_panel.setStyleSheet("background-color: #f0f0f0; border-right: 1px solid #ccc;") + left_panel.setObjectName("Sidebar") + left_panel.setFixedWidth(260) left_layout = QVBoxLayout(left_panel) + left_layout.setContentsMargins(0, 20, 0, 20) + + lbl_title = QLabel(" UFF SEARCH") + lbl_title.setObjectName("SidebarTitle") self.folder_list = QListWidget() - self.folder_list.setStyleSheet("border: 1px solid #ddd; background: white;") - btn_add = QPushButton(" + Ordner") + icon_add = self.style().standardIcon(QStyle.StandardPixmap.SP_FileDialogNewFolder) + icon_del = self.style().standardIcon(QStyle.StandardPixmap.SP_TrashIcon) + icon_refresh = self.style().standardIcon(QStyle.StandardPixmap.SP_BrowserReload) + icon_stop = self.style().standardIcon(QStyle.StandardPixmap.SP_DialogCancelButton) + + btn_add = QPushButton(" Ordner hinzufügen") + btn_add.setObjectName("SidebarBtn") + btn_add.setIcon(icon_add) btn_add.clicked.connect(self.add_new_folder) - btn_del = QPushButton(" - Löschen") + + btn_del = QPushButton(" Ordner entfernen") + btn_del.setObjectName("SidebarBtn") + btn_del.setIcon(icon_del) btn_del.clicked.connect(self.delete_selected_folder) - self.btn_rescan = QPushButton(" ↻ Scan") + + self.btn_rescan = QPushButton(" Neu scannen") + self.btn_rescan.setObjectName("SidebarBtn") + self.btn_rescan.setIcon(icon_refresh) self.btn_rescan.clicked.connect(self.rescan_selected_folder) - self.btn_cancel = QPushButton("🛑 Stop") + + self.btn_cancel = QPushButton("STOPPEN") + self.btn_cancel.setObjectName("CancelBtn") + self.btn_cancel.setIcon(icon_stop) self.btn_cancel.clicked.connect(self.cancel_indexing) self.btn_cancel.hide() - self.btn_cancel.setStyleSheet("background-color: #ffcccc; color: red;") - left_layout.addWidget(QLabel("📂 Indizierte Ordner")) + left_layout.addWidget(lbl_title) + left_layout.addSpacing(10) left_layout.addWidget(self.folder_list) + left_layout.addSpacing(10) left_layout.addWidget(btn_add) left_layout.addWidget(btn_del) - left_layout.addStretch() left_layout.addWidget(self.btn_rescan) left_layout.addWidget(self.btn_cancel) - # -- RECHTS (Suche & Ergebnisse) -- + # -- RECHTS (Hauptbereich) -- right_panel = QWidget() + right_panel.setObjectName("MainArea") right_layout = QVBoxLayout(right_panel) + right_layout.setContentsMargins(30, 30, 30, 30) + right_layout.setSpacing(15) - # Suchleiste + # Header search_box = QHBoxLayout() self.input_search = QLineEdit() - self.input_search.setPlaceholderText("Suchbegriff eingeben...") - self.input_search.setStyleSheet("padding: 8px; font-size: 14px;") + self.input_search.setPlaceholderText("Wonach suchst du heute?") self.input_search.returnPressed.connect(self.perform_search) self.btn_go = QPushButton("Suchen") - self.btn_go.setFixedWidth(100) - self.btn_go.setStyleSheet("background-color: #2980b9; color: white; padding: 8px; font-weight: bold;") + self.btn_go.setObjectName("SearchBtn") + self.btn_go.setCursor(Qt.CursorShape.PointingHandCursor) self.btn_go.clicked.connect(self.perform_search) search_box.addWidget(self.input_search) search_box.addWidget(self.btn_go) - # Status & Progress - self.lbl_status = QLabel("Warte auf Modell...") + # Status + status_box = QHBoxLayout() + self.lbl_status = QLabel("Modell wird geladen...") + self.lbl_status.setObjectName("StatusLabel") self.progress_bar = QProgressBar() self.progress_bar.hide() + status_box.addWidget(self.lbl_status) + status_box.addWidget(self.progress_bar) - # ERGEBNIS-BEREICH (QScrollArea statt QTextBrowser) + # Ergebnisse self.scroll_area = QScrollArea() self.scroll_area.setWidgetResizable(True) - self.scroll_area.setStyleSheet("background-color: #fafafa; border: none;") - # Container Widget für die Ergebnisse self.results_container = QWidget() - self.results_container.setStyleSheet("background-color: transparent;") + self.results_container.setObjectName("ResultsContainer") self.results_layout = QVBoxLayout(self.results_container) self.results_layout.setAlignment(Qt.AlignmentFlag.AlignTop) - self.results_layout.setSpacing(10) - + self.results_layout.setSpacing(15) self.scroll_area.setWidget(self.results_container) right_layout.addLayout(search_box) - right_layout.addWidget(self.lbl_status) - right_layout.addWidget(self.progress_bar) + right_layout.addLayout(status_box) right_layout.addWidget(self.scroll_area) - # Splitter - splitter = QSplitter() - splitter.addWidget(left_panel) - splitter.addWidget(right_panel) - splitter.setSizes([250, 750]) - - main_layout.addWidget(splitter) + main_layout.addWidget(left_panel) + main_layout.addWidget(right_panel) self.set_ui_enabled(False) def set_ui_enabled(self, enabled): @@ -465,44 +632,38 @@ class UffWindow(QMainWindow): QMessageBox.critical(self, "Fehler", "Modell konnte nicht geladen werden.") return self.db.model = model - self.lbl_status.setText("Bereit.") + self.lbl_status.setText("Bereit für deine Suche.") self.set_ui_enabled(True) def perform_search(self): query = self.input_search.text() if not query: return - self.lbl_status.setText("Suche läuft...") QApplication.processEvents() - # 1. Alte Ergebnisse löschen while self.results_layout.count(): child = self.results_layout.takeAt(0) - if child.widget(): - child.widget().deleteLater() + if child.widget(): child.widget().deleteLater() - # 2. Suchen results = self.db.search(query) - self.lbl_status.setText(f"{len(results)} Treffer.") + self.lbl_status.setText(f"{len(results)} Treffer gefunden.") - # 3. Neue Ergebnisse als Widgets hinzufügen if not results: - lbl = QLabel("Keine Ergebnisse gefunden.") - lbl.setStyleSheet("color: #777; font-size: 14pt; margin-top: 20px;") + lbl = QLabel("Leider keine Ergebnisse.") + lbl.setStyleSheet("color: #95a5a6; font-size: 18px; margin-top: 40px;") lbl.setAlignment(Qt.AlignmentFlag.AlignHCenter) self.results_layout.addWidget(lbl) else: for fname, fpath, snippet in results: - item = SearchResultItem(fname, fpath, snippet) - self.results_layout.addWidget(item) - + self.results_layout.addWidget(SearchResultItem(fname, fpath, snippet)) self.results_layout.addStretch() - # --- Folder Management --- def load_saved_folders(self): self.folder_list.clear() for f in self.db.get_folders(): - self.folder_list.addItem(QListWidgetItem(f)) + 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") @@ -534,19 +695,21 @@ class UffWindow(QMainWindow): def idx_done(self, n, s, c): self.set_ui_enabled(True) self.btn_cancel.hide(); self.btn_rescan.show(); self.progress_bar.hide() - msg = "Abgebrochen" if c else "Fertig" + msg = "Abgebrochen" if c else "Indexierung fertig" self.lbl_status.setText(f"{msg}: {n} neu, {s} übersprungen.") if __name__ == "__main__": app = QApplication(sys.argv) + font = QFont("Segoe UI", 10) + app.setFont(font) + splash = None try: if os.path.exists("assets/uff_banner.jpeg"): splash = QSplashScreen(QPixmap("assets/uff_banner.jpeg")) splash.show() except: pass - w = UffWindow(splash) w.show() w.start_model_loading()