316 lines
12 KiB
Python
316 lines
12 KiB
Python
# ui.py
|
|
import os
|
|
from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
|
QLineEdit, QPushButton, QLabel, QFileDialog,
|
|
QProgressBar, QMessageBox, QListWidget, QListWidgetItem,
|
|
QSplitter, QFrame, QScrollArea, QStyle, QGraphicsDropShadowEffect,
|
|
QSplashScreen) # QSplashScreen hier wichtig
|
|
from PyQt6.QtCore import Qt, QUrl, QThread, pyqtSignal, QRect
|
|
from PyQt6.QtGui import QDesktopServices, QColor, QFont, QPainter, QIcon, QPixmap # Painter & Icon neu
|
|
from sentence_transformers import SentenceTransformer
|
|
|
|
from database import DatabaseHandler
|
|
from indexer import IndexerThread
|
|
from config import STYLESHEET
|
|
|
|
# --- NEU: Ein moderner Splash Screen mit Ladebalken ---
|
|
class ModernSplashScreen(QSplashScreen):
|
|
def __init__(self, pixmap):
|
|
super().__init__(pixmap)
|
|
self.progress = 0
|
|
self.message = "Initialisiere..."
|
|
# Schriftart für den Ladetext
|
|
self.font = QFont("Segoe UI", 10, QFont.Weight.Bold)
|
|
|
|
def set_progress(self, value, text):
|
|
self.progress = value
|
|
self.message = text
|
|
self.repaint() # Erzwingt neuzeichnen
|
|
|
|
def drawContents(self, painter):
|
|
# 1. Das normale Bild zeichnen
|
|
super().drawContents(painter)
|
|
|
|
# 2. Ladebalken-Hintergrund (unten)
|
|
# Wir malen direkt auf das Bild
|
|
bg_rect = self.rect()
|
|
bar_height = 20
|
|
# Position: Ganz unten am Bild
|
|
bar_rect = QRect(0, bg_rect.height() - bar_height, bg_rect.width(), bar_height)
|
|
|
|
# Hintergrund des Balkens (dunkelgrau)
|
|
painter.setPen(Qt.PenStyle.NoPen)
|
|
painter.setBrush(QColor(50, 50, 50))
|
|
painter.drawRect(bar_rect)
|
|
|
|
# 3. Der Fortschritt (türkis/blau)
|
|
# Breite basierend auf % berechnen
|
|
progress_width = int(bg_rect.width() * (self.progress / 100))
|
|
prog_rect = QRect(0, bg_rect.height() - bar_height, progress_width, bar_height)
|
|
|
|
painter.setBrush(QColor("#3498db")) # UFF-Blau
|
|
painter.drawRect(prog_rect)
|
|
|
|
# 4. Text zeichnen (zentriert über dem Balken oder darin)
|
|
painter.setPen(QColor("white"))
|
|
painter.setFont(self.font)
|
|
# Text etwas oberhalb des Balkens zeichnen
|
|
text_rect = QRect(0, bg_rect.height() - bar_height - 30, bg_rect.width(), 25)
|
|
painter.drawText(text_rect, Qt.AlignmentFlag.AlignCenter, self.message)
|
|
|
|
# --- Thread zum Laden des Modells ---
|
|
class ModelLoaderThread(QThread):
|
|
model_loaded = pyqtSignal(object)
|
|
|
|
def run(self):
|
|
try:
|
|
# Das ist der schwere Teil, der dauert
|
|
model = SentenceTransformer('all-MiniLM-L6-v2')
|
|
self.model_loaded.emit(model)
|
|
except:
|
|
self.model_loaded.emit(None)
|
|
|
|
# --- SearchResultItem (Unverändert, aber der Vollständigkeit halber hier) ---
|
|
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))
|
|
|
|
# --- Das Hauptfenster ---
|
|
class UffWindow(QMainWindow):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.db = DatabaseHandler()
|
|
self.initUI()
|
|
|
|
|
|
self.load_saved_folders()
|
|
|
|
def initUI(self):
|
|
self.setWindowTitle("UFF Search v1.0")
|
|
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("Bereit.")
|
|
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)
|
|
|
|
# Methoden für Model Loading (wird jetzt von main gesteuert)
|
|
def on_model_loaded(self, model):
|
|
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)
|
|
|
|
# ... RESTLICHE METHODEN (search, add_folder etc.) bleiben gleich wie vorher ...
|
|
# (Kopiere hier einfach die Methoden aus deiner alten ui.py rein,
|
|
# search, load_saved_folders, add_new_folder, delete_selected_folder, rescan, start_idx, cancel_idx, idx_done)
|
|
|
|
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.") |