Files

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