made modular
This commit is contained in:
266
ui.py
Normal file
266
ui.py
Normal file
@@ -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.")
|
||||
Reference in New Issue
Block a user