diff --git a/config.py b/config.py
new file mode 100644
index 0000000..f1fab20
--- /dev/null
+++ b/config.py
@@ -0,0 +1,56 @@
+# config.py
+import sys
+import os
+
+# --- PFADE ---
+if os.name == 'nt':
+ base_dir = os.getenv('LOCALAPPDATA')
+else:
+ base_dir = os.path.join(os.path.expanduser("~"), ".local", "share")
+
+APP_DATA_DIR = os.path.join(base_dir, "UFF_Search")
+if not os.path.exists(APP_DATA_DIR):
+ os.makedirs(APP_DATA_DIR)
+
+DB_NAME = os.path.join(APP_DATA_DIR, "uff_index.db")
+LOG_FILE = os.path.join(APP_DATA_DIR, "uff.log")
+
+# --- LOGGING KLASSE ---
+class Logger(object):
+ def __init__(self):
+ self.log = open(LOG_FILE, "w", encoding="utf-8")
+
+ def write(self, message):
+ self.log.write(message)
+ self.log.flush()
+
+ def flush(self):
+ self.log.flush()
+
+# --- QT MESSAGE HANDLER (Filter) ---
+def qt_message_handler(mode, context, message):
+ msg_lower = message.lower()
+ ignore = ["qt.text.font", "qt.qpa.fonts", "opentype", "directwrite", "fontbbox", "script"]
+ if any(k in msg_lower for k in ignore): return
+ try:
+ sys.stdout.write(f"[Qt] {message}\n")
+ except: pass
+
+# --- STYLESHEET ---
+STYLESHEET = """
+QMainWindow { background-color: #f4f7f6; }
+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; font-size: 13px; }
+QListWidget::item { padding: 8px; border-bottom: 1px solid #2c3e50; }
+QListWidget::item:selected { background-color: #1abc9c; color: white; }
+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; }
+QLineEdit { padding: 10px; border: 1px solid #bdc3c7; border-radius: 20px; font-size: 14px; background-color: white; }
+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; }
+QScrollArea { border: none; background-color: transparent; }
+QWidget#ResultsContainer { background-color: transparent; }
+"""
\ No newline at end of file
diff --git a/database.py b/database.py
new file mode 100644
index 0000000..3c5256b
--- /dev/null
+++ b/database.py
@@ -0,0 +1,128 @@
+# database.py
+import sqlite3
+import os
+import numpy as np
+import traceback # WICHTIG: Damit wir den vollen Fehler sehen
+from sentence_transformers import util
+from rapidfuzz import fuzz
+from config import DB_NAME, APP_DATA_DIR
+
+class DatabaseHandler:
+ def __init__(self):
+ self.app_data_dir = APP_DATA_DIR
+ self.db_name = DB_NAME
+ self.model = None
+ self.init_db()
+
+ def init_db(self):
+ conn = sqlite3.connect(self.db_name)
+ cursor = conn.cursor()
+ cursor.execute("CREATE VIRTUAL TABLE IF NOT EXISTS documents USING fts5(filename, path, content);")
+ cursor.execute("CREATE TABLE IF NOT EXISTS folders (path TEXT PRIMARY KEY, alias TEXT);")
+ cursor.execute("CREATE TABLE IF NOT EXISTS embeddings (doc_id INTEGER PRIMARY KEY, vec BLOB);")
+ conn.commit()
+ 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)
+ cursor = conn.cursor()
+ cursor.execute("SELECT rowid FROM documents WHERE path LIKE ?", (f"{path}%",))
+ ids = [row[0] for row in cursor.fetchall()]
+ if ids:
+ cursor.execute("DELETE FROM documents WHERE path LIKE ?", (f"{path}%",))
+ cursor.execute(f"DELETE FROM embeddings WHERE doc_id IN ({','.join('?'*len(ids))})", ids)
+ cursor.execute("DELETE FROM folders WHERE path = ?", (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):
+ # Sicherheitscheck
+ if not query.strip() or not self.model:
+ return []
+
+ try:
+ # 1. Semantische Vorbereitung
+ q_vec = self.model.encode(query, convert_to_tensor=False)
+
+ conn = sqlite3.connect(self.db_name)
+ cursor = conn.cursor()
+
+ # Embeddings laden
+ cursor.execute("SELECT doc_id, vec FROM embeddings")
+ data = cursor.fetchall()
+ doc_ids = [d[0] for d in data]
+
+ if not doc_ids:
+ conn.close()
+ return []
+
+ # Umwandlung BLOB -> Numpy Array
+ # Hier knallt es oft, wenn die DB korrupt ist oder Dimensionen nicht passen
+ vecs = np.array([np.frombuffer(d[1], dtype=np.float32) for d in data])
+
+ # Cosine Similarity berechnen
+ scores = util.cos_sim(q_vec, vecs)[0].numpy()
+ scores = np.clip(scores, 0, 1)
+ sem_map = {did: float(s) for did, s in zip(doc_ids, scores)}
+
+ # 2. Lexikalische Suche (FTS)
+ words = query.replace('"', '').split()
+ if not words: words = [query]
+ fts_query = " OR ".join([f'"{w}"*' for w in words])
+
+ try:
+ fts_rows = cursor.execute("SELECT rowid, filename, content FROM documents WHERE documents MATCH ? LIMIT 100", (fts_query,)).fetchall()
+ except Exception as e:
+ print(f"FTS Fehler (ignoriert): {e}")
+ fts_rows = []
+
+ lex_map = {}
+ for did, fname, content in fts_rows:
+ r1 = fuzz.partial_ratio(query.lower(), fname.lower())
+ # Content kürzen für Performance
+ r2 = fuzz.partial_token_set_ratio(query.lower(), content[:5000].lower())
+ lex_map[did] = max(r1, r2) / 100.0
+
+ # 3. Hybrid Fusion
+ final = {}
+ ALPHA = 0.65
+ BETA = 0.35
+ for did, s_score in sem_map.items():
+ if s_score < 0.15 and did not in lex_map: continue
+ l_score = lex_map.get(did, 0.0)
+ h_score = (s_score * ALPHA) + (l_score * BETA)
+ # Kleiner Boost wenn beides passt
+ if s_score > 0.4 and l_score > 0.6: h_score += 0.1
+ final[did] = h_score
+
+ # 4. Ergebnisse holen
+ sorted_ids = sorted(final.keys(), key=lambda x: final[x], reverse=True)[:50]
+ results = []
+ for did in sorted_ids:
+ row = cursor.execute("SELECT filename, path, snippet(documents, 2, '', '', '...', 15) FROM documents WHERE rowid = ?", (did,)).fetchone()
+ if row: results.append(row)
+
+ conn.close()
+ return results
+
+ except Exception as e:
+ # DIESER TEIL IST NEU: Er schreibt den Fehler ins Logfile
+ print(f"!!! KRITISCHER FEHLER IN DER SUCHE !!!")
+ print(f"Fehler: {e}")
+ print(traceback.format_exc())
+ return []
\ No newline at end of file
diff --git a/indexer.py b/indexer.py
new file mode 100644
index 0000000..0329648
--- /dev/null
+++ b/indexer.py
@@ -0,0 +1,134 @@
+# indexer.py
+import os
+import sqlite3
+import pdfplumber
+import zipfile
+import io
+from PyQt6.QtCore import QThread, pyqtSignal
+
+# Importe optionaler Libraries
+try: import docx
+except ImportError: docx = None
+try: import openpyxl
+except ImportError: openpyxl = None
+try: from pptx import Presentation
+except ImportError: Presentation = None
+
+class IndexerThread(QThread):
+ progress_signal = pyqtSignal(str)
+ finished_signal = pyqtSignal(int, int, bool)
+
+ def __init__(self, folder, db_name, model):
+ super().__init__()
+ self.folder_path = folder
+ self.db_name = db_name
+ self.model = model
+ self.is_running = True
+
+ def stop(self): self.is_running = False
+
+ def _extract_text(self, stream, filename):
+ ext = os.path.splitext(filename)[1].lower()
+ text = ""
+ try:
+ if ext == ".pdf":
+ try:
+ with pdfplumber.open(stream) as pdf:
+ for p in pdf.pages:
+ if t := p.extract_text(): text += t + "\n"
+ except: pass
+
+ elif ext == ".docx" and docx:
+ try:
+ doc = docx.Document(stream)
+ for para in doc.paragraphs: text += para.text + "\n"
+ except: pass
+
+ elif ext == ".xlsx" and openpyxl:
+ 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:
+ 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 p in shape.text_frame.paragraphs:
+ for r in p.runs: text += r.text + " "
+ text += "\n"
+ except: pass
+
+ elif ext in [".txt", ".md", ".py", ".json", ".csv", ".html", ".log", ".ini", ".xml"]:
+ try:
+ content = stream.read()
+ if isinstance(content, str): text = content
+ else: text = content.decode('utf-8', errors='ignore')
+ except: pass
+ except: pass
+ return text
+
+ def run(self):
+ conn = sqlite3.connect(self.db_name)
+ cursor = conn.cursor()
+
+ # Cleanup old entries
+ cursor.execute("SELECT rowid FROM documents WHERE path LIKE ?", (f"{self.folder_path}%",))
+ ids = [r[0] for r in cursor.fetchall()]
+ if ids:
+ cursor.execute("DELETE FROM documents WHERE path LIKE ?", (f"{self.folder_path}%",))
+ cursor.execute(f"DELETE FROM embeddings WHERE doc_id IN ({','.join('?'*len(ids))})", ids)
+ conn.commit()
+
+ indexed = 0
+ skipped = 0
+ cancelled = False
+
+ for root, dirs, files in os.walk(self.folder_path):
+ if not self.is_running: cancelled = True; break
+ for file in files:
+ if not self.is_running: cancelled = True; break
+ path = os.path.join(root, file)
+ self.progress_signal.emit(f"Prüfe: {file}...")
+
+ if file.lower().endswith('.zip'):
+ try:
+ with zipfile.ZipFile(path, 'r') as z:
+ for zi in z.infolist():
+ if zi.is_dir(): continue
+ vpath = f"{path} :: {zi.filename}"
+ with z.open(zi) as zf:
+ content = self._extract_text(io.BytesIO(zf.read()), zi.filename)
+ if content and len(content.strip()) > 20:
+ self._save(cursor, zi.filename, vpath, content)
+ indexed += 1
+ except: skipped += 1
+ else:
+ 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()
+ conn.close()
+ self.finished_signal.emit(indexed, skipped, cancelled)
+
+ def _save(self, cursor, fname, path, content):
+ cursor.execute("INSERT INTO documents (filename, path, content) VALUES (?, ?, ?)", (fname, path, content))
+ did = cursor.lastrowid
+ vec = self.model.encode(content[:8000], convert_to_tensor=False).tobytes()
+ cursor.execute("INSERT INTO embeddings (doc_id, vec) VALUES (?, ?)", (did, vec))
\ No newline at end of file
diff --git a/main.py b/main.py
new file mode 100644
index 0000000..a2f2c3e
--- /dev/null
+++ b/main.py
@@ -0,0 +1,38 @@
+# main.py
+import sys
+import os
+from PyQt6.QtWidgets import QApplication, QSplashScreen
+from PyQt6.QtGui import QPixmap, QFont
+from PyQt6.QtCore import qInstallMessageHandler
+
+from config import Logger, qt_message_handler, LOG_FILE
+from ui import UffWindow
+
+# 1. Logging Setup
+sys.stdout = Logger()
+sys.stderr = sys.stdout
+print(f"--- APP START ---")
+print(f"Logfile: {LOG_FILE}")
+
+# 2. Filter für Qt Meldungen installieren
+qInstallMessageHandler(qt_message_handler)
+os.environ["QT_LOGGING_RULES"] = "qt.text.font.db=false;qt.qpa.fonts=false"
+
+if __name__ == "__main__":
+ app = QApplication(sys.argv)
+
+ # Globale Schriftart
+ app.setFont(QFont("Segoe UI", 10))
+
+ splash = None
+ if os.path.exists("assets/uff_banner.jpeg"):
+ try:
+ splash = QSplashScreen(QPixmap("assets/uff_banner.jpeg"))
+ splash.show()
+ except: pass
+
+ window = UffWindow(splash)
+ window.show()
+ window.start_model_loading()
+
+ sys.exit(app.exec())
\ No newline at end of file
diff --git a/uff_app.py b/uff_app.py
deleted file mode 100644
index 1c81d3a..0000000
--- a/uff_app.py
+++ /dev/null
@@ -1,716 +0,0 @@
-import sys
-import os
-import sqlite3
-import pdfplumber
-import numpy as np
-import zipfile
-import io
-import traceback
-
-# --- OPTIONALE IMPORTE ---
-try:
- import docx
-except ImportError:
- 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
-
-from PyQt6.QtCore import qInstallMessageHandler, QtMsgType, Qt, QThread, pyqtSignal, QUrl, QSize
-from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
- QHBoxLayout, QLineEdit, QPushButton, QLabel,
- QFileDialog, QProgressBar, QMessageBox,
- QListWidget, QListWidgetItem, QSplitter, QFrame,
- QSplashScreen, QScrollArea, QStyle, QGraphicsDropShadowEffect)
-from PyQt6.QtGui import QDesktopServices, QPixmap, QCursor, QAction, QColor, QPalette, QFont
-
-# --- 0. LOGGING & SETUP ---
-
-if os.name == 'nt':
- base_dir = os.getenv('LOCALAPPDATA')
-else:
- base_dir = os.path.join(os.path.expanduser("~"), ".local", "share")
-
-log_dir = os.path.join(base_dir, "UFF_Search")
-if not os.path.exists(log_dir):
- os.makedirs(log_dir)
-
-log_file_path = os.path.join(log_dir, "uff.log")
-
-class Logger(object):
- def __init__(self):
- self.log = open(log_file_path, "w", encoding="utf-8")
-
- def write(self, message):
- self.log.write(message)
- self.log.flush()
-
- def flush(self):
- self.log.flush()
-
-sys.stdout = Logger()
-sys.stderr = sys.stdout
-
-# --- STYLESHEET ---
-STYLESHEET = """
-QMainWindow {
- background-color: #f4f7f6;
-}
-
-/* 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()
- ignore_keywords = [
- "qt.text.font", "qt.qpa.fonts", "opentype", "directwrite",
- "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
-
-qInstallMessageHandler(qt_message_handler)
-os.environ["QT_LOGGING_RULES"] = "qt.text.font.db=false;qt.qpa.fonts=false"
-
-
-# --- WIDGET: Modernes Suchergebnis (Fixed Tooltips) ---
-class SearchResultItem(QFrame):
- 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: white;
- border: 1px solid #e0e0e0;
- border-radius: 8px;
- }
- SearchResultItem:hover {
- 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(15, 15, 15, 15)
- layout.setSpacing(5)
-
- # 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: 16px;
- color: #2c3e50;
- border: none;
- background: transparent;
- padding: 0px;
- }
- QPushButton:hover {
- color: #3498db;
- text-decoration: underline;
- }
- """)
- self.btn_title.clicked.connect(self.open_file)
-
- # 2. Snippet
- self.lbl_snippet = QLabel(snippet)
- self.lbl_snippet.setWordWrap(True)
- 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;")
-
- 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_path = self.filepath
- if " :: " in target_path:
- target_path = target_path.split(" :: ")[0]
- url = QUrl.fromLocalFile(target_path)
- QDesktopServices.openUrl(url)
-
-
-# --- 1. DATENBANK MANAGER ---
-
-class DatabaseHandler:
- def __init__(self):
- self.app_data_dir = log_dir
- self.db_name = os.path.join(self.app_data_dir, "uff_index.db")
- self.model = None
- self.init_db()
-
- def init_db(self):
- conn = sqlite3.connect(self.db_name)
- cursor = conn.cursor()
- cursor.execute("CREATE VIRTUAL TABLE IF NOT EXISTS documents USING fts5(filename, path, content);")
- cursor.execute("CREATE TABLE IF NOT EXISTS folders (path TEXT PRIMARY KEY, alias TEXT);")
- cursor.execute("CREATE TABLE IF NOT EXISTS embeddings (doc_id INTEGER PRIMARY KEY, vec BLOB);")
- conn.commit()
- 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)
- cursor = conn.cursor()
- cursor.execute("SELECT rowid FROM documents WHERE path LIKE ?", (f"{path}%",))
- ids = [row[0] for row in cursor.fetchall()]
- if ids:
- cursor.execute("DELETE FROM documents WHERE path LIKE ?", (f"{path}%",))
- cursor.execute(f"DELETE FROM embeddings WHERE doc_id IN ({','.join('?'*len(ids))})", ids)
- cursor.execute("DELETE FROM folders WHERE path = ?", (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):
- if not query.strip() or not self.model: return []
-
- q_vec = self.model.encode(query, convert_to_tensor=False)
- conn = sqlite3.connect(self.db_name)
- cursor = conn.cursor()
-
- cursor.execute("SELECT doc_id, vec FROM embeddings")
- data = cursor.fetchall()
- doc_ids = [d[0] for d in data]
- if not doc_ids:
- conn.close(); return []
-
- vecs = np.array([np.frombuffer(d[1], dtype=np.float32) for d in data])
- scores = util.cos_sim(q_vec, vecs)[0].numpy()
- scores = np.clip(scores, 0, 1)
- sem_map = {did: float(s) for did, s in zip(doc_ids, scores)}
-
- words = query.replace('"', '').split()
- if not words: words = [query]
- fts_query = " OR ".join([f'"{w}"*' for w in words])
-
- try:
- fts_rows = cursor.execute("SELECT rowid, filename, content FROM documents WHERE documents MATCH ? LIMIT 100", (fts_query,)).fetchall()
- except: fts_rows = []
-
- lex_map = {}
- for did, fname, content in fts_rows:
- r1 = fuzz.partial_ratio(query.lower(), fname.lower())
- r2 = fuzz.partial_token_set_ratio(query.lower(), content[:5000].lower())
- lex_map[did] = max(r1, r2) / 100.0
-
- final = {}
- ALPHA = 0.65
- BETA = 0.35
- for did, s_score in sem_map.items():
- if s_score < 0.15 and did not in lex_map: continue
- l_score = lex_map.get(did, 0.0)
- h_score = (s_score * ALPHA) + (l_score * BETA)
- if s_score > 0.4 and l_score > 0.6: h_score += 0.1
- final[did] = h_score
-
- sorted_ids = sorted(final.keys(), key=lambda x: final[x], reverse=True)[:50]
- results = []
- for did in sorted_ids:
- row = cursor.execute("SELECT filename, path, snippet(documents, 2, '', '', '...', 15) FROM documents WHERE rowid = ?", (did,)).fetchone()
- if row: results.append(row)
- conn.close()
- return results
-
-# --- 2. THREADS ---
-
-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)
-
-class IndexerThread(QThread):
- progress_signal = pyqtSignal(str)
- finished_signal = pyqtSignal(int, int, bool)
-
- def __init__(self, folder, db_name, model):
- super().__init__()
- self.folder_path = folder
- self.db_name = db_name
- self.model = model
- self.is_running = True
-
- def stop(self): self.is_running = False
-
- def _extract_text(self, stream, filename):
- ext = os.path.splitext(filename)[1].lower()
- text = ""
- try:
- if ext == ".pdf":
- try:
- with pdfplumber.open(stream) as pdf:
- for p in pdf.pages:
- if t := p.extract_text(): text += t + "\n"
- except: pass
-
- elif ext == ".docx" and docx is not None:
- try:
- doc = docx.Document(stream)
- 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
-
- elif ext in [".txt", ".md", ".py", ".json", ".csv", ".html", ".log", ".ini", ".xml"]:
- try:
- content = stream.read()
- if isinstance(content, str): text = content
- else: text = content.decode('utf-8', errors='ignore')
- except: pass
- except: pass
- return text
-
- def run(self):
- conn = sqlite3.connect(self.db_name)
- cursor = conn.cursor()
-
- cursor.execute("SELECT rowid FROM documents WHERE path LIKE ?", (f"{self.folder_path}%",))
- ids = [r[0] for r in cursor.fetchall()]
- if ids:
- cursor.execute("DELETE FROM documents WHERE path LIKE ?", (f"{self.folder_path}%",))
- cursor.execute(f"DELETE FROM embeddings WHERE doc_id IN ({','.join('?'*len(ids))})", ids)
- conn.commit()
-
- indexed = 0
- skipped = 0
- cancelled = False
-
- for root, dirs, files in os.walk(self.folder_path):
- if not self.is_running: cancelled = True; break
- for file in files:
- if not self.is_running: cancelled = True; break
- path = os.path.join(root, file)
- self.progress_signal.emit(f"Prüfe: {file}...")
-
- if file.lower().endswith('.zip'):
- try:
- with zipfile.ZipFile(path, 'r') as z:
- for zi in z.infolist():
- if zi.is_dir(): continue
- vpath = f"{path} :: {zi.filename}"
- with z.open(zi) as zf:
- content = self._extract_text(io.BytesIO(zf.read()), zi.filename)
- if content and len(content.strip()) > 20:
- self._save(cursor, zi.filename, vpath, content)
- indexed += 1
- except: skipped += 1
- else:
- 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()
- conn.close()
- self.finished_signal.emit(indexed, skipped, cancelled)
-
- def _save(self, cursor, fname, path, content):
- cursor.execute("INSERT INTO documents (filename, path, content) VALUES (?, ?, ?)", (fname, path, content))
- did = cursor.lastrowid
- vec = self.model.encode(content[:8000], convert_to_tensor=False).tobytes()
- cursor.execute("INSERT INTO embeddings (doc_id, vec) VALUES (?, ?)", (did, vec))
-
-# --- 3. UI MAIN WINDOW ---
-
-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 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)
-
- # -- SIDEBAR --
- left_panel = QFrame()
- 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()
-
- 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(" Ordner entfernen")
- btn_del.setObjectName("SidebarBtn")
- btn_del.setIcon(icon_del)
- btn_del.clicked.connect(self.delete_selected_folder)
-
- 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("STOPPEN")
- self.btn_cancel.setObjectName("CancelBtn")
- self.btn_cancel.setIcon(icon_stop)
- self.btn_cancel.clicked.connect(self.cancel_indexing)
- self.btn_cancel.hide()
-
- 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.addWidget(self.btn_rescan)
- left_layout.addWidget(self.btn_cancel)
-
- # -- 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)
-
- # Header
- search_box = QHBoxLayout()
- self.input_search = QLineEdit()
- self.input_search.setPlaceholderText("Wonach suchst du heute?")
- self.input_search.returnPressed.connect(self.perform_search)
-
- self.btn_go = QPushButton("Suchen")
- 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
- 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)
-
- # Ergebnisse
- self.scroll_area = QScrollArea()
- self.scroll_area.setWidgetResizable(True)
-
- self.results_container = QWidget()
- self.results_container.setObjectName("ResultsContainer")
- self.results_layout = QVBoxLayout(self.results_container)
- self.results_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
- self.results_layout.setSpacing(15)
- self.scroll_area.setWidget(self.results_container)
-
- right_layout.addLayout(search_box)
- right_layout.addLayout(status_box)
- right_layout.addWidget(self.scroll_area)
-
- main_layout.addWidget(left_panel)
- main_layout.addWidget(right_panel)
- self.set_ui_enabled(False)
-
- def set_ui_enabled(self, enabled):
- self.input_search.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 perform_search(self):
- query = self.input_search.text()
- if not query: return
- self.lbl_status.setText("Suche läuft...")
- QApplication.processEvents()
-
- while self.results_layout.count():
- child = self.results_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.results_layout.addWidget(lbl)
- else:
- for fname, fpath, snippet in results:
- self.results_layout.addWidget(SearchResultItem(fname, fpath, snippet))
- self.results_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_selected_folder(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.progress_bar.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_indexing(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.progress_bar.hide()
- 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()
- sys.exit(app.exec())
\ No newline at end of file
diff --git a/ui.py b/ui.py
new file mode 100644
index 0000000..ef2a590
--- /dev/null
+++ b/ui.py
@@ -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.")
\ No newline at end of file