Refactor UI components and improve logging; enhance search functionality with better error handling and folder management
This commit is contained in:
684
uff_app.py
684
uff_app.py
@@ -10,15 +10,15 @@ import traceback
|
|||||||
from sentence_transformers import SentenceTransformer, util
|
from sentence_transformers import SentenceTransformer, util
|
||||||
from rapidfuzz import process, fuzz
|
from rapidfuzz import process, fuzz
|
||||||
|
|
||||||
# Wichtige Importe für UI und Signale
|
from PyQt6.QtCore import qInstallMessageHandler, QtMsgType, Qt, QThread, pyqtSignal, QUrl, QSize
|
||||||
from PyQt6.QtCore import qInstallMessageHandler, QtMsgType, Qt, QThread, pyqtSignal, QUrl
|
|
||||||
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, QProgressBar, QMessageBox,
|
||||||
QListWidget, QListWidgetItem, QSplitter, QFrame, QSplashScreen)
|
QListWidget, QListWidgetItem, QSplitter, QFrame,
|
||||||
from PyQt6.QtGui import QDesktopServices, QPixmap
|
QSplashScreen, QScrollArea, QStyle)
|
||||||
|
from PyQt6.QtGui import QDesktopServices, QPixmap, QCursor, QAction
|
||||||
|
|
||||||
# --- 0. LOGGING & SYSTEM-SETUP ---
|
# --- 0. LOGGING & SETUP ---
|
||||||
|
|
||||||
if os.name == 'nt':
|
if os.name == 'nt':
|
||||||
base_dir = os.getenv('LOCALAPPDATA')
|
base_dir = os.getenv('LOCALAPPDATA')
|
||||||
@@ -31,7 +31,6 @@ if not os.path.exists(log_dir):
|
|||||||
|
|
||||||
log_file_path = os.path.join(log_dir, "uff.log")
|
log_file_path = os.path.join(log_dir, "uff.log")
|
||||||
|
|
||||||
# Logger-Klasse
|
|
||||||
class Logger(object):
|
class Logger(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.log = open(log_file_path, "w", encoding="utf-8")
|
self.log = open(log_file_path, "w", encoding="utf-8")
|
||||||
@@ -43,97 +42,120 @@ class Logger(object):
|
|||||||
def flush(self):
|
def flush(self):
|
||||||
self.log.flush()
|
self.log.flush()
|
||||||
|
|
||||||
# stdout und stderr umleiten
|
|
||||||
sys.stdout = Logger()
|
sys.stdout = Logger()
|
||||||
sys.stderr = sys.stdout
|
sys.stderr = sys.stdout
|
||||||
|
|
||||||
print(f"--- START LOGGING ---")
|
print(f"--- START LOGGING ---")
|
||||||
print(f"Logfile liegt hier: {log_file_path}")
|
print(f"Logfile: {log_file_path}")
|
||||||
|
|
||||||
# --- QT MESSAGE HANDLER (Der Filter für C++ Errors) ---
|
|
||||||
def qt_message_handler(mode, context, message):
|
def qt_message_handler(mode, context, message):
|
||||||
"""
|
|
||||||
Fängt interne Qt-Nachrichten ab und filtert Font-Fehler heraus.
|
|
||||||
"""
|
|
||||||
msg_lower = message.lower()
|
msg_lower = message.lower()
|
||||||
|
|
||||||
# FILTER-LISTE: Erweitert basierend auf deinen Logs
|
|
||||||
ignore_keywords = [
|
ignore_keywords = [
|
||||||
"qt.text.font",
|
"qt.text.font", "qt.qpa.fonts", "opentype", "directwrite",
|
||||||
"qt.qpa.fonts",
|
"unable to create font", "fontbbox", "script"
|
||||||
"opentype support missing",
|
|
||||||
"directwrite",
|
|
||||||
"unable to create font",
|
|
||||||
"fontbbox",
|
|
||||||
"script 66",
|
|
||||||
"script 9",
|
|
||||||
"script 10",
|
|
||||||
"script 20",
|
|
||||||
"script 32"
|
|
||||||
]
|
]
|
||||||
|
if any(k in msg_lower for k in ignore_keywords): return
|
||||||
|
|
||||||
# Wenn eines der Keywords vorkommt -> Nachricht ignorieren (return)
|
|
||||||
if any(keyword in msg_lower for keyword in ignore_keywords):
|
|
||||||
return
|
|
||||||
|
|
||||||
# Formatierung für das Logfile
|
|
||||||
mode_str = "INFO"
|
|
||||||
if mode == QtMsgType.QtWarningMsg: mode_str = "WARNING"
|
|
||||||
elif mode == QtMsgType.QtCriticalMsg: mode_str = "CRITICAL"
|
|
||||||
elif mode == QtMsgType.QtFatalMsg: mode_str = "FATAL"
|
|
||||||
|
|
||||||
# Nur relevante Nachrichten ins Log schreiben
|
|
||||||
try:
|
try:
|
||||||
sys.stdout.write(f"[Qt {mode_str}] {message}\n")
|
sys.stdout.write(f"[Qt] {message}\n")
|
||||||
except:
|
except: pass
|
||||||
pass
|
|
||||||
|
|
||||||
# Handler installieren (Muss VOR der App-Erstellung passieren)
|
|
||||||
qInstallMessageHandler(qt_message_handler)
|
qInstallMessageHandler(qt_message_handler)
|
||||||
|
|
||||||
# Zusätzlich Environment Variable setzen
|
|
||||||
os.environ["QT_LOGGING_RULES"] = "qt.text.font.db=false;qt.qpa.fonts=false"
|
os.environ["QT_LOGGING_RULES"] = "qt.text.font.db=false;qt.qpa.fonts=false"
|
||||||
|
|
||||||
|
|
||||||
|
# --- NEUE KOMPONENTE: Ein einzelnes Suchergebnis als Widget ---
|
||||||
|
class SearchResultItem(QFrame):
|
||||||
|
"""
|
||||||
|
Stellt ein einzelnes Suchergebnis als 'Karte' dar.
|
||||||
|
"""
|
||||||
|
def __init__(self, filename, filepath, snippet, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.filepath = filepath
|
||||||
|
|
||||||
|
# Optik der Karte
|
||||||
|
self.setFrameShape(QFrame.Shape.StyledPanel)
|
||||||
|
self.setStyleSheet("""
|
||||||
|
SearchResultItem {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
SearchResultItem:hover {
|
||||||
|
background-color: #f0f8ff;
|
||||||
|
border: 1px solid #2980b9;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
layout.setContentsMargins(10, 10, 10, 10)
|
||||||
|
|
||||||
|
# 1. Dateiname (Sieht aus wie ein Link, ist aber ein Button)
|
||||||
|
self.btn_title = QPushButton(filename)
|
||||||
|
self.btn_title.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||||
|
self.btn_title.setStyleSheet("""
|
||||||
|
QPushButton {
|
||||||
|
text-align: left;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 14pt;
|
||||||
|
color: #2980b9;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
QPushButton:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
self.btn_title.clicked.connect(self.open_file)
|
||||||
|
|
||||||
|
# 2. Snippet (Textvorschau)
|
||||||
|
# Wir nutzen QLabel mit WordWrap. HTML für Fettung ist okay.
|
||||||
|
self.lbl_snippet = QLabel(snippet)
|
||||||
|
self.lbl_snippet.setWordWrap(True)
|
||||||
|
self.lbl_snippet.setStyleSheet("color: #444; font-size: 10pt; margin-top: 5px;")
|
||||||
|
|
||||||
|
# 3. Pfad (Grau und klein)
|
||||||
|
self.lbl_path = QLabel(filepath)
|
||||||
|
self.lbl_path.setStyleSheet("color: #888; font-size: 8pt; margin-top: 5px;")
|
||||||
|
|
||||||
|
layout.addWidget(self.btn_title)
|
||||||
|
layout.addWidget(self.lbl_snippet)
|
||||||
|
layout.addWidget(self.lbl_path)
|
||||||
|
|
||||||
|
def open_file(self):
|
||||||
|
"""
|
||||||
|
Öffnet die Datei direkt über QDesktopServices.
|
||||||
|
"""
|
||||||
|
print(f"Öffne Datei: {self.filepath}")
|
||||||
|
|
||||||
|
target_path = self.filepath
|
||||||
|
# Falls es ein ZIP-Pfad ist (erkennbar am Trenner " :: ")
|
||||||
|
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.")
|
||||||
|
|
||||||
|
|
||||||
# --- 1. DATENBANK MANAGER ---
|
# --- 1. DATENBANK MANAGER ---
|
||||||
|
|
||||||
class DatabaseHandler:
|
class DatabaseHandler:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
if os.name == 'nt':
|
self.app_data_dir = log_dir
|
||||||
base_dir = os.getenv('LOCALAPPDATA')
|
|
||||||
else:
|
|
||||||
base_dir = os.path.join(os.path.expanduser("~"), ".local", "share")
|
|
||||||
|
|
||||||
self.app_data_dir = os.path.join(base_dir, "UFF_Search")
|
|
||||||
|
|
||||||
if not os.path.exists(self.app_data_dir):
|
|
||||||
os.makedirs(self.app_data_dir)
|
|
||||||
|
|
||||||
self.db_name = os.path.join(self.app_data_dir, "uff_index.db")
|
self.db_name = os.path.join(self.app_data_dir, "uff_index.db")
|
||||||
print(f"Datenbank Pfad: {self.db_name}")
|
|
||||||
self.model = None
|
self.model = None
|
||||||
|
|
||||||
self.init_db()
|
self.init_db()
|
||||||
|
|
||||||
def init_db(self):
|
def init_db(self):
|
||||||
conn = sqlite3.connect(self.db_name)
|
conn = sqlite3.connect(self.db_name)
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute("""
|
cursor.execute("CREATE VIRTUAL TABLE IF NOT EXISTS documents USING fts5(filename, path, content);")
|
||||||
CREATE VIRTUAL TABLE IF NOT EXISTS documents
|
cursor.execute("CREATE TABLE IF NOT EXISTS folders (path TEXT PRIMARY KEY, alias TEXT);")
|
||||||
USING fts5(filename, path, content);
|
cursor.execute("CREATE TABLE IF NOT EXISTS embeddings (doc_id INTEGER PRIMARY KEY, vec BLOB);")
|
||||||
""")
|
|
||||||
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.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
@@ -143,19 +165,17 @@ class DatabaseHandler:
|
|||||||
conn.execute("INSERT OR IGNORE INTO folders (path, alias) VALUES (?, ?)", (path, os.path.basename(path)))
|
conn.execute("INSERT OR IGNORE INTO folders (path, alias) VALUES (?, ?)", (path, os.path.basename(path)))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return True
|
return True
|
||||||
except:
|
except: return False
|
||||||
return False
|
finally: conn.close()
|
||||||
finally:
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
def remove_folder(self, path):
|
def remove_folder(self, path):
|
||||||
conn = sqlite3.connect(self.db_name)
|
conn = sqlite3.connect(self.db_name)
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute("SELECT rowid FROM documents WHERE path LIKE ?", (f"{path}%",))
|
cursor.execute("SELECT rowid FROM documents WHERE path LIKE ?", (f"{path}%",))
|
||||||
ids_to_delete = [row[0] for row in cursor.fetchall()]
|
ids = [row[0] for row in cursor.fetchall()]
|
||||||
if ids_to_delete:
|
if ids:
|
||||||
cursor.execute("DELETE FROM documents WHERE path LIKE ?", (f"{path}%",))
|
cursor.execute("DELETE FROM documents WHERE path LIKE ?", (f"{path}%",))
|
||||||
cursor.execute(f"DELETE FROM embeddings WHERE doc_id IN ({','.join('?'*len(ids_to_delete))})", ids_to_delete)
|
cursor.execute(f"DELETE FROM embeddings WHERE doc_id IN ({','.join('?'*len(ids))})", ids)
|
||||||
cursor.execute("DELETE FROM folders WHERE path = ?", (path,))
|
cursor.execute("DELETE FROM folders WHERE path = ?", (path,))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
@@ -169,455 +189,351 @@ class DatabaseHandler:
|
|||||||
def search(self, query):
|
def search(self, query):
|
||||||
if not query.strip() or not self.model: return []
|
if not query.strip() or not self.model: return []
|
||||||
|
|
||||||
# PHASE 1: SEMANTIK
|
# 1. Semantik
|
||||||
query_embedding = self.model.encode(query, convert_to_tensor=False)
|
q_vec = self.model.encode(query, convert_to_tensor=False)
|
||||||
conn = sqlite3.connect(self.db_name)
|
conn = sqlite3.connect(self.db_name)
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute("SELECT doc_id, vec FROM embeddings")
|
|
||||||
all_embeddings_data = cursor.fetchall()
|
|
||||||
doc_ids = [item[0] for item in all_embeddings_data]
|
|
||||||
|
|
||||||
|
cursor.execute("SELECT doc_id, vec FROM embeddings")
|
||||||
|
data = cursor.fetchall()
|
||||||
|
doc_ids = [d[0] for d in data]
|
||||||
if not doc_ids:
|
if not doc_ids:
|
||||||
conn.close()
|
conn.close(); return []
|
||||||
return []
|
|
||||||
|
|
||||||
all_embeddings = np.array([np.frombuffer(item[1], dtype=np.float32) for item in all_embeddings_data])
|
vecs = np.array([np.frombuffer(d[1], dtype=np.float32) for d in data])
|
||||||
cos_scores = util.cos_sim(query_embedding, all_embeddings)[0].numpy()
|
scores = util.cos_sim(q_vec, vecs)[0].numpy()
|
||||||
cos_scores = np.clip(cos_scores, 0, 1)
|
scores = np.clip(scores, 0, 1)
|
||||||
semantic_map = {doc_id: float(score) for doc_id, score in zip(doc_ids, cos_scores)}
|
sem_map = {did: float(s) for did, s in zip(doc_ids, scores)}
|
||||||
|
|
||||||
# PHASE 2: LEXIKALISCH
|
# 2. Lexikalisch
|
||||||
words = query.replace('"', '').split()
|
words = query.replace('"', '').split()
|
||||||
if not words: words = [query]
|
if not words: words = [query]
|
||||||
sql_query_parts = [f'"{w}"*' for w in words]
|
fts_query = " OR ".join([f'"{w}"*' for w in words])
|
||||||
sql_query_string = " OR ".join(sql_query_parts)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
fts_rows = cursor.execute("""
|
fts_rows = cursor.execute("SELECT rowid, filename, content FROM documents WHERE documents MATCH ? LIMIT 100", (fts_query,)).fetchall()
|
||||||
SELECT rowid, filename, content
|
except: fts_rows = []
|
||||||
FROM documents
|
|
||||||
WHERE documents MATCH ?
|
|
||||||
LIMIT 100
|
|
||||||
""", (sql_query_string,)).fetchall()
|
|
||||||
except:
|
|
||||||
fts_rows = []
|
|
||||||
|
|
||||||
lexical_map = {}
|
lex_map = {}
|
||||||
for doc_id, filename, content in fts_rows:
|
for did, fname, content in fts_rows:
|
||||||
ratio_name = fuzz.partial_ratio(query.lower(), filename.lower())
|
r1 = fuzz.partial_ratio(query.lower(), fname.lower())
|
||||||
ratio_content = fuzz.partial_token_set_ratio(query.lower(), content[:5000].lower())
|
r2 = fuzz.partial_token_set_ratio(query.lower(), content[:5000].lower())
|
||||||
best_ratio = max(ratio_name, ratio_content)
|
lex_map[did] = max(r1, r2) / 100.0
|
||||||
lexical_map[doc_id] = best_ratio / 100.0
|
|
||||||
|
|
||||||
# PHASE 3: HYBRID
|
# 3. Hybrid
|
||||||
final_scores = {}
|
final = {}
|
||||||
ALPHA = 0.65
|
ALPHA = 0.65
|
||||||
BETA = 0.35
|
BETA = 0.35
|
||||||
for doc_id, sem_score in semantic_map.items():
|
for did, s_score in sem_map.items():
|
||||||
if sem_score < 0.15 and doc_id not in lexical_map:
|
if s_score < 0.15 and did not in lex_map: continue
|
||||||
continue
|
l_score = lex_map.get(did, 0.0)
|
||||||
lex_score = lexical_map.get(doc_id, 0.0)
|
h_score = (s_score * ALPHA) + (l_score * BETA)
|
||||||
hybrid_score = (sem_score * ALPHA) + (lex_score * BETA)
|
if s_score > 0.4 and l_score > 0.6: h_score += 0.1
|
||||||
if sem_score > 0.4 and lex_score > 0.6:
|
final[did] = h_score
|
||||||
hybrid_score += 0.1
|
|
||||||
final_scores[doc_id] = hybrid_score
|
|
||||||
|
|
||||||
# PHASE 4: SORT
|
# 4. Fetch
|
||||||
sorted_ids = sorted(final_scores.keys(), key=lambda x: final_scores[x], reverse=True)
|
sorted_ids = sorted(final.keys(), key=lambda x: final[x], reverse=True)[:50]
|
||||||
results = []
|
results = []
|
||||||
for doc_id in sorted_ids[:50]:
|
for did in sorted_ids:
|
||||||
row = cursor.execute(
|
row = cursor.execute("SELECT filename, path, snippet(documents, 2, '<b>', '</b>', '...', 15) FROM documents WHERE rowid = ?", (did,)).fetchone()
|
||||||
"SELECT filename, path, snippet(documents, 2, '<b>', '</b>', '...', 15) FROM documents WHERE rowid = ?",
|
if row: results.append(row)
|
||||||
(doc_id,)
|
|
||||||
).fetchone()
|
|
||||||
if row:
|
|
||||||
results.append(row)
|
|
||||||
conn.close()
|
conn.close()
|
||||||
return results
|
return results
|
||||||
|
|
||||||
# --- 2. MODEL LOADER ---
|
# --- 2. THREADS ---
|
||||||
|
|
||||||
class ModelLoaderThread(QThread):
|
class ModelLoaderThread(QThread):
|
||||||
model_loaded = pyqtSignal(object)
|
model_loaded = pyqtSignal(object)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
print("Lade das semantische Modell (all-MiniLM-L6-v2)...")
|
|
||||||
try:
|
try:
|
||||||
model = SentenceTransformer('all-MiniLM-L6-v2')
|
model = SentenceTransformer('all-MiniLM-L6-v2')
|
||||||
print("Modell geladen.")
|
|
||||||
self.model_loaded.emit(model)
|
self.model_loaded.emit(model)
|
||||||
except Exception as e:
|
except: self.model_loaded.emit(None)
|
||||||
print(f"Fehler beim Laden des Modells: {e}")
|
|
||||||
self.model_loaded.emit(None)
|
|
||||||
|
|
||||||
# --- 3. INDEXER ---
|
|
||||||
class IndexerThread(QThread):
|
class IndexerThread(QThread):
|
||||||
progress_signal = pyqtSignal(str)
|
progress_signal = pyqtSignal(str)
|
||||||
finished_signal = pyqtSignal(int, int, bool)
|
finished_signal = pyqtSignal(int, int, bool)
|
||||||
|
|
||||||
def __init__(self, folder_path, db_name, model):
|
def __init__(self, folder, db_name, model):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.folder_path = folder_path
|
self.folder_path = folder
|
||||||
self.db_name = db_name
|
self.db_name = db_name
|
||||||
self.model = model
|
self.model = model
|
||||||
self.is_running = True
|
self.is_running = True
|
||||||
|
|
||||||
def stop(self):
|
def stop(self): self.is_running = False
|
||||||
self.is_running = False
|
|
||||||
|
|
||||||
def _extract_text_from_stream(self, file_stream, filename):
|
def _extract_text(self, stream, filename):
|
||||||
ext = os.path.splitext(filename)[1].lower()
|
ext = os.path.splitext(filename)[1].lower()
|
||||||
text = ""
|
text = ""
|
||||||
try:
|
try:
|
||||||
if ext == ".pdf":
|
if ext == ".pdf":
|
||||||
try:
|
try:
|
||||||
with pdfplumber.open(file_stream) as pdf:
|
with pdfplumber.open(stream) as pdf:
|
||||||
for page in pdf.pages:
|
for p in pdf.pages:
|
||||||
try:
|
if t := p.extract_text(): text += t + "\n"
|
||||||
if page_text := page.extract_text():
|
except: pass
|
||||||
text += page_text + "\n"
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Warnung: Konnte eine Seite in '{filename}' nicht lesen. Fehler: {e}")
|
|
||||||
continue
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Warnung: PDF '{filename}' defekt. Fehler: {e}")
|
|
||||||
return None
|
|
||||||
elif ext in [".txt", ".md", ".py", ".json", ".csv", ".html", ".log", ".ini", ".xml"]:
|
elif ext in [".txt", ".md", ".py", ".json", ".csv", ".html", ".log", ".ini", ".xml"]:
|
||||||
if hasattr(file_stream, 'read'):
|
try:
|
||||||
content_bytes = file_stream.read()
|
content = stream.read()
|
||||||
if isinstance(content_bytes, str):
|
if isinstance(content, str): text = content
|
||||||
with open(file_stream, 'r', encoding='utf-8', errors='ignore') as f:
|
else: text = content.decode('utf-8', errors='ignore')
|
||||||
text = f.read()
|
except: pass
|
||||||
else:
|
except: pass
|
||||||
text = content_bytes.decode('utf-8', errors='ignore')
|
|
||||||
else:
|
|
||||||
with open(file_stream, "r", encoding="utf-8", errors="ignore") as f:
|
|
||||||
text = f.read()
|
|
||||||
except Exception as e:
|
|
||||||
return None
|
|
||||||
return text
|
return text
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
conn = sqlite3.connect(self.db_name)
|
conn = sqlite3.connect(self.db_name)
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
cursor.execute("SELECT rowid FROM documents WHERE path LIKE ?", (f"{self.folder_path}%",))
|
cursor.execute("SELECT rowid FROM documents WHERE path LIKE ?", (f"{self.folder_path}%",))
|
||||||
ids_to_delete = [row[0] for row in cursor.fetchall()]
|
ids = [r[0] for r in cursor.fetchall()]
|
||||||
if ids_to_delete:
|
if ids:
|
||||||
cursor.execute("DELETE FROM documents WHERE path LIKE ?", (f"{self.folder_path}%",))
|
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_to_delete))})", ids_to_delete)
|
cursor.execute(f"DELETE FROM embeddings WHERE doc_id IN ({','.join('?'*len(ids))})", ids)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
indexed = 0
|
indexed = 0
|
||||||
skipped = 0
|
skipped = 0
|
||||||
was_cancelled = False
|
cancelled = False
|
||||||
|
|
||||||
for root, dirs, files in os.walk(self.folder_path):
|
for root, dirs, files in os.walk(self.folder_path):
|
||||||
if not self.is_running:
|
if not self.is_running: cancelled = True; break
|
||||||
was_cancelled = True
|
|
||||||
break
|
|
||||||
|
|
||||||
for file in files:
|
for file in files:
|
||||||
if not self.is_running:
|
if not self.is_running: cancelled = True; break
|
||||||
was_cancelled = True
|
path = os.path.join(root, file)
|
||||||
break
|
|
||||||
|
|
||||||
file_path = os.path.join(root, file)
|
|
||||||
self.progress_signal.emit(f"Prüfe: {file}...")
|
self.progress_signal.emit(f"Prüfe: {file}...")
|
||||||
|
|
||||||
if file.lower().endswith('.zip'):
|
if file.lower().endswith('.zip'):
|
||||||
try:
|
try:
|
||||||
with zipfile.ZipFile(file_path, 'r') as z:
|
with zipfile.ZipFile(path, 'r') as z:
|
||||||
for z_info in z.infolist():
|
for zi in z.infolist():
|
||||||
if z_info.is_dir(): continue
|
if zi.is_dir(): continue
|
||||||
virtual_path = f"{file_path} :: {z_info.filename}"
|
vpath = f"{path} :: {zi.filename}"
|
||||||
with z.open(z_info) as z_file:
|
with z.open(zi) as zf:
|
||||||
file_in_memory = io.BytesIO(z_file.read())
|
content = self._extract_text(io.BytesIO(zf.read()), zi.filename)
|
||||||
content = self._extract_text_from_stream(file_in_memory, z_info.filename)
|
|
||||||
if content and len(content.strip()) > 20:
|
if content and len(content.strip()) > 20:
|
||||||
self._save_to_db(cursor, z_info.filename, virtual_path, content)
|
self._save(cursor, zi.filename, vpath, content)
|
||||||
indexed += 1
|
indexed += 1
|
||||||
except Exception as e:
|
except: skipped += 1
|
||||||
print(f"Zip Error {file}: {e}")
|
|
||||||
skipped += 1
|
|
||||||
else:
|
else:
|
||||||
content = self._extract_text_from_stream(file_path, file)
|
with open(path, "rb") as f:
|
||||||
|
content = self._extract_text(f, file)
|
||||||
if content and len(content.strip()) > 20:
|
if content and len(content.strip()) > 20:
|
||||||
self._save_to_db(cursor, file, file_path, content)
|
self._save(cursor, file, path, content)
|
||||||
indexed += 1
|
indexed += 1
|
||||||
else:
|
else: skipped += 1
|
||||||
skipped += 1
|
if cancelled: break
|
||||||
|
|
||||||
if was_cancelled: break
|
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
self.finished_signal.emit(indexed, skipped, was_cancelled)
|
self.finished_signal.emit(indexed, skipped, cancelled)
|
||||||
|
|
||||||
def _save_to_db(self, cursor, filename, path, content):
|
def _save(self, cursor, fname, path, content):
|
||||||
cursor.execute("INSERT INTO documents (filename, path, content) VALUES (?, ?, ?)", (filename, path, content))
|
cursor.execute("INSERT INTO documents (filename, path, content) VALUES (?, ?, ?)", (fname, path, content))
|
||||||
doc_id = cursor.lastrowid
|
did = cursor.lastrowid
|
||||||
embedding = self.model.encode(content[:8000], convert_to_tensor=False)
|
vec = self.model.encode(content[:8000], convert_to_tensor=False).tobytes()
|
||||||
embedding_blob = embedding.tobytes()
|
cursor.execute("INSERT INTO embeddings (doc_id, vec) VALUES (?, ?)", (did, vec))
|
||||||
cursor.execute("INSERT INTO embeddings (doc_id, vec) VALUES (?, ?)", (doc_id, embedding_blob))
|
|
||||||
|
|
||||||
|
# --- 3. UI ---
|
||||||
# --- 4. UI ---
|
|
||||||
|
|
||||||
class UffWindow(QMainWindow):
|
class UffWindow(QMainWindow):
|
||||||
def __init__(self, splash=None):
|
def __init__(self, splash=None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.splash = splash
|
self.splash = splash
|
||||||
self.db = DatabaseHandler()
|
self.db = DatabaseHandler()
|
||||||
self.indexer_thread = None
|
|
||||||
self.initUI()
|
self.initUI()
|
||||||
self.load_saved_folders()
|
self.load_saved_folders()
|
||||||
|
|
||||||
def initUI(self):
|
def initUI(self):
|
||||||
self.setWindowTitle("UFF Text Search")
|
self.setWindowTitle("UFF Search v6.0 (Widget List)")
|
||||||
self.resize(1000, 700)
|
self.resize(1000, 700)
|
||||||
|
|
||||||
central = QWidget()
|
central = QWidget()
|
||||||
self.setCentralWidget(central)
|
self.setCentralWidget(central)
|
||||||
main_layout = QHBoxLayout(central)
|
main_layout = QHBoxLayout(central)
|
||||||
|
|
||||||
# LINKS
|
# -- LINKS (Sidebar) --
|
||||||
left_panel = QFrame()
|
left_panel = QFrame()
|
||||||
left_panel.setFixedWidth(250)
|
left_panel.setFixedWidth(250)
|
||||||
|
left_panel.setStyleSheet("background-color: #f0f0f0; border-right: 1px solid #ccc;")
|
||||||
left_layout = QVBoxLayout(left_panel)
|
left_layout = QVBoxLayout(left_panel)
|
||||||
left_layout.setContentsMargins(0, 0, 0, 0)
|
|
||||||
lbl_folders = QLabel("📂 Meine Ordner")
|
|
||||||
lbl_folders.setStyleSheet("font-weight: bold; font-size: 14px;")
|
|
||||||
|
|
||||||
self.folder_list = QListWidget()
|
self.folder_list = QListWidget()
|
||||||
self.folder_list.setSelectionMode(QListWidget.SelectionMode.SingleSelection)
|
self.folder_list.setStyleSheet("border: 1px solid #ddd; background: white;")
|
||||||
|
|
||||||
self.btn_add = QPushButton(" + Hinzufügen")
|
btn_add = QPushButton(" + Ordner")
|
||||||
self.btn_add.clicked.connect(self.add_new_folder)
|
btn_add.clicked.connect(self.add_new_folder)
|
||||||
self.btn_remove = QPushButton(" - Entfernen")
|
btn_del = QPushButton(" - Löschen")
|
||||||
self.btn_remove.clicked.connect(self.delete_selected_folder)
|
btn_del.clicked.connect(self.delete_selected_folder)
|
||||||
self.btn_rescan = QPushButton(" ↻ Neu scannen")
|
self.btn_rescan = QPushButton(" ↻ Scan")
|
||||||
self.btn_rescan.clicked.connect(self.rescan_selected_folder)
|
self.btn_rescan.clicked.connect(self.rescan_selected_folder)
|
||||||
self.btn_cancel = QPushButton("🛑 Abbrechen")
|
self.btn_cancel = QPushButton("🛑 Stop")
|
||||||
self.btn_cancel.setStyleSheet("background-color: #ffcccc; color: #cc0000; font-weight: bold;")
|
|
||||||
self.btn_cancel.clicked.connect(self.cancel_indexing)
|
self.btn_cancel.clicked.connect(self.cancel_indexing)
|
||||||
self.btn_cancel.hide()
|
self.btn_cancel.hide()
|
||||||
|
self.btn_cancel.setStyleSheet("background-color: #ffcccc; color: red;")
|
||||||
|
|
||||||
left_layout.addWidget(lbl_folders)
|
left_layout.addWidget(QLabel("<b>📂 Indizierte Ordner</b>"))
|
||||||
left_layout.addWidget(self.folder_list)
|
left_layout.addWidget(self.folder_list)
|
||||||
left_layout.addWidget(self.btn_add)
|
left_layout.addWidget(btn_add)
|
||||||
left_layout.addWidget(self.btn_remove)
|
left_layout.addWidget(btn_del)
|
||||||
left_layout.addStretch()
|
left_layout.addStretch()
|
||||||
left_layout.addWidget(self.btn_rescan)
|
left_layout.addWidget(self.btn_rescan)
|
||||||
left_layout.addWidget(self.btn_cancel)
|
left_layout.addWidget(self.btn_cancel)
|
||||||
|
|
||||||
# RECHTS
|
# -- RECHTS (Suche & Ergebnisse) --
|
||||||
right_panel = QWidget()
|
right_panel = QWidget()
|
||||||
right_layout = QVBoxLayout(right_panel)
|
right_layout = QVBoxLayout(right_panel)
|
||||||
|
|
||||||
search_container = QHBoxLayout()
|
# Suchleiste
|
||||||
|
search_box = QHBoxLayout()
|
||||||
self.input_search = QLineEdit()
|
self.input_search = QLineEdit()
|
||||||
self.input_search.setPlaceholderText("Suche... (Hybrid: Inhalt + Keywords)")
|
self.input_search.setPlaceholderText("Suchbegriff eingeben...")
|
||||||
self.input_search.returnPressed.connect(self.perform_search)
|
|
||||||
self.input_search.setStyleSheet("padding: 8px; font-size: 14px;")
|
self.input_search.setStyleSheet("padding: 8px; font-size: 14px;")
|
||||||
|
self.input_search.returnPressed.connect(self.perform_search)
|
||||||
|
|
||||||
self.btn_go = QPushButton("Suchen")
|
self.btn_go = QPushButton("Suchen")
|
||||||
self.btn_go.setFixedWidth(100)
|
self.btn_go.setFixedWidth(100)
|
||||||
|
self.btn_go.setStyleSheet("background-color: #2980b9; color: white; padding: 8px; font-weight: bold;")
|
||||||
self.btn_go.clicked.connect(self.perform_search)
|
self.btn_go.clicked.connect(self.perform_search)
|
||||||
|
|
||||||
search_container.addWidget(self.input_search)
|
search_box.addWidget(self.input_search)
|
||||||
search_container.addWidget(self.btn_go)
|
search_box.addWidget(self.btn_go)
|
||||||
|
|
||||||
self.lbl_status = QLabel("Initialisiere...")
|
# Status & Progress
|
||||||
self.lbl_status.setStyleSheet("color: #666;")
|
self.lbl_status = QLabel("Warte auf Modell...")
|
||||||
self.progress_bar = QProgressBar()
|
self.progress_bar = QProgressBar()
|
||||||
self.progress_bar.hide()
|
self.progress_bar.hide()
|
||||||
|
|
||||||
# STANDARD BROWSER MIT RICHTIGEN EINSTELLUNGEN
|
# ERGEBNIS-BEREICH (QScrollArea statt QTextBrowser)
|
||||||
self.result_browser = QTextBrowser()
|
self.scroll_area = QScrollArea()
|
||||||
# WICHTIG: Interne Links deaktivieren, damit wir sie abfangen können
|
self.scroll_area.setWidgetResizable(True)
|
||||||
self.result_browser.setOpenExternalLinks(False)
|
self.scroll_area.setStyleSheet("background-color: #fafafa; border: none;")
|
||||||
# Wenn wir darauf klicken, wird unser Slot aufgerufen
|
|
||||||
self.result_browser.anchorClicked.connect(self.link_clicked)
|
|
||||||
|
|
||||||
self.result_browser.setStyleSheet("background-color: white; border: 1px solid #ccc;")
|
# Container Widget für die Ergebnisse
|
||||||
|
self.results_container = QWidget()
|
||||||
|
self.results_container.setStyleSheet("background-color: transparent;")
|
||||||
|
self.results_layout = QVBoxLayout(self.results_container)
|
||||||
|
self.results_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||||
|
self.results_layout.setSpacing(10)
|
||||||
|
|
||||||
|
self.scroll_area.setWidget(self.results_container)
|
||||||
|
|
||||||
right_layout.addLayout(search_container)
|
right_layout.addLayout(search_box)
|
||||||
right_layout.addWidget(self.lbl_status)
|
right_layout.addWidget(self.lbl_status)
|
||||||
right_layout.addWidget(self.progress_bar)
|
right_layout.addWidget(self.progress_bar)
|
||||||
right_layout.addWidget(self.result_browser)
|
right_layout.addWidget(self.scroll_area)
|
||||||
|
|
||||||
splitter = QSplitter(Qt.Orientation.Horizontal)
|
# Splitter
|
||||||
|
splitter = QSplitter()
|
||||||
splitter.addWidget(left_panel)
|
splitter.addWidget(left_panel)
|
||||||
splitter.addWidget(right_panel)
|
splitter.addWidget(right_panel)
|
||||||
splitter.setSizes([250, 750])
|
splitter.setSizes([250, 750])
|
||||||
|
|
||||||
main_layout.addWidget(splitter)
|
main_layout.addWidget(splitter)
|
||||||
self.set_main_ui_enabled(False)
|
self.set_ui_enabled(False)
|
||||||
|
|
||||||
def set_main_ui_enabled(self, enabled):
|
def set_ui_enabled(self, enabled):
|
||||||
self.input_search.setEnabled(enabled)
|
self.input_search.setEnabled(enabled)
|
||||||
self.btn_go.setEnabled(enabled)
|
self.btn_go.setEnabled(enabled)
|
||||||
self.folder_list.setEnabled(enabled)
|
self.folder_list.setEnabled(enabled)
|
||||||
self.btn_add.setEnabled(enabled)
|
|
||||||
self.btn_remove.setEnabled(enabled)
|
|
||||||
self.btn_rescan.setEnabled(enabled)
|
|
||||||
|
|
||||||
def start_model_loading(self):
|
def start_model_loading(self):
|
||||||
if self.splash:
|
if self.splash: self.splash.showMessage("Lade KI-Modell...", Qt.AlignmentFlag.AlignBottom, Qt.GlobalColor.white)
|
||||||
self.splash.showMessage("Lade semantisches Modell...", Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignHCenter, Qt.GlobalColor.white)
|
self.loader = ModelLoaderThread()
|
||||||
self.model_loader = ModelLoaderThread()
|
self.loader.model_loaded.connect(self.on_model_loaded)
|
||||||
self.model_loader.model_loaded.connect(self.on_model_loaded)
|
self.loader.start()
|
||||||
self.model_loader.start()
|
|
||||||
|
|
||||||
def on_model_loaded(self, model):
|
def on_model_loaded(self, model):
|
||||||
if self.splash:
|
if self.splash: self.splash.finish(self)
|
||||||
self.splash.showMessage("Modell geladen. Starte Benutzeroberfläche...", Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignHCenter, Qt.GlobalColor.white)
|
if not model:
|
||||||
|
QMessageBox.critical(self, "Fehler", "Modell konnte nicht geladen werden.")
|
||||||
if model is None:
|
|
||||||
self.lbl_status.setText("Fehler: Modell konnte nicht geladen werden.")
|
|
||||||
QMessageBox.critical(self, "Kritischer Fehler", "Das semantische Modell konnte nicht geladen werden.")
|
|
||||||
self.close()
|
|
||||||
else:
|
|
||||||
self.db.model = model
|
|
||||||
self.lbl_status.setText("Bereit. Hybrid-Modell geladen.")
|
|
||||||
self.set_main_ui_enabled(True)
|
|
||||||
|
|
||||||
if self.splash:
|
|
||||||
self.splash.finish(self)
|
|
||||||
|
|
||||||
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")
|
|
||||||
if folder:
|
|
||||||
if self.db.add_folder(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
|
return
|
||||||
self.start_indexing(item.text())
|
self.db.model = model
|
||||||
|
self.lbl_status.setText("Bereit.")
|
||||||
def start_indexing(self, folder):
|
self.set_ui_enabled(True)
|
||||||
if not self.db.model:
|
|
||||||
QMessageBox.warning(self, "Bitte warten", "Das Suchmodell wird noch geladen.")
|
|
||||||
return
|
|
||||||
|
|
||||||
self.set_ui_busy(True)
|
|
||||||
self.lbl_status.setText(f"Starte... {os.path.basename(folder)}")
|
|
||||||
|
|
||||||
self.indexer_thread = IndexerThread(folder, db_name=self.db.db_name, model=self.db.model)
|
|
||||||
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.start()
|
|
||||||
|
|
||||||
def cancel_indexing(self):
|
|
||||||
if self.indexer_thread and self.indexer_thread.isRunning():
|
|
||||||
self.lbl_status.setText("Breche ab...")
|
|
||||||
self.indexer_thread.stop()
|
|
||||||
|
|
||||||
def indexing_finished(self, indexed, skipped, was_cancelled):
|
|
||||||
self.set_ui_busy(False)
|
|
||||||
if was_cancelled:
|
|
||||||
self.lbl_status.setText(f"Abgebrochen. ({indexed} indiziert).")
|
|
||||||
QMessageBox.information(self, "Abbruch", f"Vorgang abgebrochen.\nBis dahin indiziert: {indexed}")
|
|
||||||
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):
|
|
||||||
self.input_search.setEnabled(not busy)
|
|
||||||
self.folder_list.setEnabled(not busy)
|
|
||||||
self.btn_add.setEnabled(not busy)
|
|
||||||
self.btn_remove.setEnabled(not busy)
|
|
||||||
self.btn_go.setEnabled(not busy)
|
|
||||||
self.btn_rescan.setVisible(not busy)
|
|
||||||
self.btn_cancel.setVisible(busy)
|
|
||||||
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
|
||||||
|
|
||||||
self.lbl_status.setText("Suche läuft...")
|
self.lbl_status.setText("Suche läuft...")
|
||||||
QApplication.processEvents()
|
QApplication.processEvents() # UI Update erzwingen
|
||||||
|
|
||||||
|
# 1. Alte Ergebnisse löschen
|
||||||
|
while self.results_layout.count():
|
||||||
|
child = self.results_layout.takeAt(0)
|
||||||
|
if child.widget():
|
||||||
|
child.widget().deleteLater()
|
||||||
|
|
||||||
|
# 2. Suchen
|
||||||
results = self.db.search(query)
|
results = self.db.search(query)
|
||||||
self.lbl_status.setText(f"{len(results)} relevante Treffer.")
|
self.lbl_status.setText(f"{len(results)} Treffer.")
|
||||||
|
|
||||||
html = ""
|
|
||||||
if not results:
|
|
||||||
html = "<h3 style='color: gray; text-align: center; margin-top: 20px;'>Nichts gefunden.</h3>"
|
|
||||||
|
|
||||||
for filename, filepath, snippet in results:
|
|
||||||
if " :: " in filepath:
|
|
||||||
real_path = filepath.split(" :: ")[0]
|
|
||||||
display_path = filepath
|
|
||||||
else:
|
|
||||||
real_path = filepath
|
|
||||||
display_path = filepath
|
|
||||||
|
|
||||||
# Link für QTextBrowser
|
|
||||||
file_url = QUrl.fromLocalFile(real_path).toString()
|
|
||||||
|
|
||||||
html += f"""
|
|
||||||
<div style='margin-bottom: 10px; padding: 10px; background-color: #f9f9f9; border-left: 4px solid #2980b9;'>
|
|
||||||
<a href="{file_url}" style='font-size: 16px; font-weight: bold; color: #2980b9; text-decoration: none;'>
|
|
||||||
{filename}
|
|
||||||
</a>
|
|
||||||
<div style='color: #333; margin-top: 5px; font-family: sans-serif; font-size: 13px;'>{snippet}</div>
|
|
||||||
<div style='color: #999; font-size: 11px; margin-top: 4px;'>{display_path}</div>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
self.result_browser.setHtml(html)
|
|
||||||
|
|
||||||
# --- DIE FUNKTION ZUM ÖFFNEN DER LINKS ---
|
# 3. Neue Ergebnisse als Widgets hinzufügen
|
||||||
def link_clicked(self, url):
|
if not results:
|
||||||
print(f"Versuche zu öffnen: {url.toString()}")
|
lbl = QLabel("Keine Ergebnisse gefunden.")
|
||||||
QDesktopServices.openUrl(url)
|
lbl.setStyleSheet("color: #777; font-size: 14pt; margin-top: 20px;")
|
||||||
|
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)
|
||||||
|
|
||||||
|
# Stretch am Ende, damit alles oben bleibt
|
||||||
|
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))
|
||||||
|
|
||||||
|
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 "Fertig"
|
||||||
|
self.lbl_status.setText(f"{msg}: {n} neu, {s} übersprungen.")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
|
|
||||||
splash = None
|
splash = None
|
||||||
try:
|
try:
|
||||||
pixmap = QPixmap("assets/uff_banner.jpeg")
|
if os.path.exists("assets/uff_banner.jpeg"):
|
||||||
splash = QSplashScreen(pixmap)
|
splash = QSplashScreen(QPixmap("assets/uff_banner.jpeg"))
|
||||||
splash.show()
|
splash.show()
|
||||||
splash.showMessage("Initialisiere Anwendung...", Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignHCenter, Qt.GlobalColor.white)
|
except: pass
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
app.processEvents()
|
w = UffWindow(splash)
|
||||||
|
w.show()
|
||||||
window = UffWindow(splash)
|
w.start_model_loading()
|
||||||
window.show()
|
|
||||||
window.start_model_loading()
|
|
||||||
|
|
||||||
sys.exit(app.exec())
|
sys.exit(app.exec())
|
||||||
Reference in New Issue
Block a user