From acc859919e25db75d14a8fe49bfe2f3ce0d988c2 Mon Sep 17 00:00:00 2001 From: Kibi Kelburton Date: Thu, 13 Aug 2026 19:38:12 +0200 Subject: [PATCH] fix recent uploads gallery --- gui.py | 298 ++++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 261 insertions(+), 37 deletions(-) diff --git a/gui.py b/gui.py index 58c3983..f0d8b16 100644 --- a/gui.py +++ b/gui.py @@ -17,7 +17,7 @@ import threading import signal from http.server import HTTPServer, BaseHTTPRequestHandler -from PySide6.QtCore import Qt, QThread, QObject, Signal, QUrl, QFile, QIODevice, QTimer, QMimeData, QProcess +from PySide6.QtCore import Qt, QThread, QObject, Signal, QUrl, QFile, QIODevice, QTimer, QMimeData, QProcess, QRunnable, QThreadPool from PySide6.QtGui import QIcon, QAction, QActionGroup, QPixmap, QPainter, QColor, QFont, QPen, QImage, QDesktopServices, QDrag, QClipboard from PySide6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QHttpMultiPart, QHttpPart, QNetworkReply, QLocalServer, QLocalSocket from PySide6.QtWidgets import ( @@ -215,11 +215,20 @@ def save_history(history_list): try: os.makedirs(CONFIG_DIR, exist_ok=True) with open(HISTORY_PATH, "w", encoding="utf-8") as f: - json.dump(history_list[:100], f, indent=2) + json.dump(history_list[:500], f, indent=2) except Exception as e: print(f"Error saving history: {e}") def add_history_entry(entry): + if isinstance(entry, dict) and "instance_name" not in entry: + config = get_env_config() + instances = config.get("instances", []) + idx = config.get("active_instance_index", 0) + if 0 <= idx < len(instances): + entry["instance_name"] = instances[idx].get("name", "Default Instance") + else: + entry["instance_name"] = "Default Instance" + history = load_history() history = [h for h in history if h.get("url") != entry.get("url")] history.insert(0, entry) @@ -916,13 +925,17 @@ class SettingsDialog(QDialog): else: QMessageBox.critical(self, "Error", "Failed to save configuration file.") +# ========================================== +# Draggable File Label Widget +# ========================================== # ========================================== # Draggable File Label Widget # ========================================== class DraggableImageLabel(QLabel): - def __init__(self, file_path, parent=None): + def __init__(self, item, parent=None): super().__init__(parent) - self.file_path = file_path + self.item = item if isinstance(item, dict) else {"file_path": str(item)} + self.file_path = self.item.get("file_path", "") self.drag_start_pos = None def mousePressEvent(self, event): @@ -930,6 +943,19 @@ class DraggableImageLabel(QLabel): self.drag_start_pos = event.position().toPoint() super().mousePressEvent(event) + def mouseReleaseEvent(self, event): + if event.button() == Qt.MouseButton.LeftButton and self.drag_start_pos is not None: + delta = (event.position().toPoint() - self.drag_start_pos).manhattanLength() + if delta < QApplication.startDragDistance(): + file_path = self.item.get("file_path", "") + url = self.item.get("url", "") + if file_path and os.path.exists(file_path): + QDesktopServices.openUrl(QUrl.fromLocalFile(file_path)) + elif url: + QDesktopServices.openUrl(QUrl(url)) + self.drag_start_pos = None + super().mouseReleaseEvent(event) + def mouseMoveEvent(self, event): if not (event.buttons() & Qt.MouseButton.LeftButton): return @@ -948,15 +974,52 @@ class DraggableImageLabel(QLabel): drag.setPixmap(self.pixmap().scaled(80, 80, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)) drag.exec(Qt.DropAction.CopyAction) + self.drag_start_pos = None # ========================================== # Upload Gallery Window & Components # ========================================== +THUMBNAIL_CACHE = {} + +class ThumbnailWorkerSignals(QObject): + finished = Signal(dict, QImage) + +class ThumbnailWorker(QRunnable): + def __init__(self, item, callback): + super().__init__() + self.item = item + self.signals = ThumbnailWorkerSignals() + self.signals.finished.connect(callback) + + def run(self): + try: + thumb_path = self.item.get("thumbnail_path", "") + file_path = self.item.get("file_path", "") + qimg = None + if thumb_path and os.path.exists(thumb_path): + qimg = QImage(thumb_path) + elif file_path and os.path.exists(file_path): + gen_thumb = generate_thumbnail(file_path) + if gen_thumb and os.path.exists(gen_thumb): + qimg = QImage(gen_thumb) + self.item["thumbnail_path"] = gen_thumb + + if qimg is None or qimg.isNull(): + self.signals.finished.emit(self.item, QImage()) + else: + self.signals.finished.emit(self.item, qimg) + except Exception: + try: + self.signals.finished.emit(self.item, QImage()) + except Exception: + pass + class GalleryCard(QFrame): def __init__(self, item, parent=None, on_delete=None): super().__init__(parent) self.item = item self.on_delete = on_delete + self._is_destroyed = False self.setFrameShape(QFrame.StyledPanel) self.setFixedHeight(190) self.setMinimumWidth(160) @@ -977,35 +1040,15 @@ class GalleryCard(QFrame): layout.setContentsMargins(8, 8, 8, 8) layout.setSpacing(6) - # Thumbnail with Drag & Drop support - file_path = item.get("file_path", "") - self.thumb_label = DraggableImageLabel(file_path) + # Thumbnail with Drag & Drop & Click support + self.thumb_label = DraggableImageLabel(item) self.thumb_label.setFixedHeight(95) self.thumb_label.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) self.thumb_label.setAlignment(Qt.AlignmentFlag.AlignCenter) - self.thumb_label.setCursor(Qt.CursorShape.OpenHandCursor) - self.thumb_label.setToolTip("Click & Drag to drop file into other apps") + self.thumb_label.setCursor(Qt.CursorShape.PointingHandCursor) + self.thumb_label.setToolTip("Click to open file | Drag to drop into other apps") self.thumb_label.setStyleSheet("background-color: #121216; border-radius: 4px;") - thumb_path = item.get("thumbnail_path", "") - file_path = item.get("file_path", "") - pixmap = None - if thumb_path and os.path.exists(thumb_path): - pixmap = QPixmap(thumb_path) - elif file_path and os.path.exists(file_path): - gen_thumb = generate_thumbnail(file_path) - if gen_thumb and os.path.exists(gen_thumb): - pixmap = QPixmap(gen_thumb) - - if pixmap and not pixmap.isNull(): - scaled = pixmap.scaled(160, 100, Qt.AspectRatioMode.KeepAspectRatioByExpanding, Qt.TransformationMode.SmoothTransformation) - x = max(0, (scaled.width() - 160) // 2) - y = max(0, (scaled.height() - 100) // 2) - self.thumb_label.setPixmap(scaled.copy(x, y, 160, 100)) - else: - self.thumb_label.setText("No Preview") - self.thumb_label.setStyleSheet("background-color: #121216; color: #666; font-size: 11px;") - layout.addWidget(self.thumb_label) # Filename @@ -1015,12 +1058,37 @@ class GalleryCard(QFrame): lbl_name.setToolTip(item.get("filename", "")) layout.addWidget(lbl_name) + # Metadata Row: Filetype badge + Timestamp + Instance badge + meta_layout = QHBoxLayout() + meta_layout.setContentsMargins(0, 0, 0, 0) + meta_layout.setSpacing(4) + + # Filetype badge + ext = os.path.splitext(item.get("filename", "") or item.get("file_path", "") or item.get("url", ""))[1].replace(".", "").upper() + if not ext: + ext = "FILE" + lbl_ext = QLabel(ext) + lbl_ext.setStyleSheet("background-color: #2a2a38; color: #7f9cf5; border-radius: 3px; font-weight: bold; font-size: 9px; padding: 1px 4px;") + meta_layout.addWidget(lbl_ext) + # Timestamp ts = item.get("timestamp", 0) dt_str = time.strftime("%Y-%m-%d %H:%M", time.localtime(ts)) if ts else "" lbl_time = QLabel(dt_str) lbl_time.setStyleSheet("font-size: 9px; color: #8888a0;") - layout.addWidget(lbl_time) + meta_layout.addWidget(lbl_time) + + meta_layout.addStretch() + + # Instance badge if available + inst_name = item.get("instance_name", "") + if inst_name: + lbl_inst = QLabel(inst_name) + lbl_inst.setStyleSheet("background-color: #262630; color: #a0a0b0; border-radius: 3px; font-size: 9px; padding: 1px 4px;") + lbl_inst.setToolTip(f"Instance: {inst_name}") + meta_layout.addWidget(lbl_inst) + + layout.addLayout(meta_layout) # Action Buttons btn_layout = QHBoxLayout() @@ -1047,6 +1115,49 @@ class GalleryCard(QFrame): layout.addLayout(btn_layout) + self.load_thumbnail_async() + + def load_thumbnail_async(self): + cache_key = self.item.get("url") or self.item.get("file_path") or self.item.get("thumbnail_path") + if cache_key and cache_key in THUMBNAIL_CACHE: + self.thumb_label.setPixmap(THUMBNAIL_CACHE[cache_key]) + else: + self.thumb_label.setText("Loading...") + self.thumb_label.setStyleSheet("background-color: #121216; color: #666; font-size: 11px;") + worker = ThumbnailWorker(self.item, self.on_thumbnail_loaded) + QThreadPool.globalInstance().start(worker) + + def on_thumbnail_loaded(self, item, qimg): + try: + if getattr(self, "_is_destroyed", False): + return + if not qimg or qimg.isNull(): + self.thumb_label.setText("No Preview") + self.thumb_label.setStyleSheet("background-color: #121216; color: #666; font-size: 11px;") + return + + scaled = qimg.scaled(160, 100, Qt.AspectRatioMode.KeepAspectRatioByExpanding, Qt.TransformationMode.SmoothTransformation) + x = max(0, (scaled.width() - 160) // 2) + y = max(0, (scaled.height() - 100) // 2) + cropped = scaled.copy(x, y, 160, 100) + pixmap = QPixmap.fromImage(cropped) + + cache_key = item.get("url") or item.get("file_path") or item.get("thumbnail_path") + if cache_key: + THUMBNAIL_CACHE[cache_key] = pixmap + + self.thumb_label.setPixmap(pixmap) + except Exception: + try: + self.thumb_label.setText("No Preview") + self.thumb_label.setStyleSheet("background-color: #121216; color: #666; font-size: 11px;") + except Exception: + pass + + def deleteLater(self): + self._is_destroyed = True + super().deleteLater() + def on_copy_link(self): url = self.item.get("url", "") if url: @@ -1062,10 +1173,18 @@ class GalleryCard(QFrame): self.on_delete(self.item) class GalleryWindow(QDialog): + BATCH_SIZE = 16 + def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle("f0ckm Upload Gallery") - self.resize(760, 560) + self.resize(780, 580) + self.filtered_history = [] + self.loaded_count = 0 + self.cards = [] + self.current_cols = 0 + self.is_loading_batch = False + app_icon = get_app_icon() if not app_icon.isNull(): self.setWindowIcon(app_icon) @@ -1086,9 +1205,17 @@ class GalleryWindow(QDialog): header_layout.addStretch() + # Instance Filter Dropdown + self.cb_instance = QComboBox() + self.cb_instance.setToolTip("Filter uploads by target instance") + self.cb_instance.setStyleSheet("padding: 3px 6px;") + self.cb_instance.currentIndexChanged.connect(self.load_items) + header_layout.addWidget(self.cb_instance) + + # Search bar self.txt_search = QLineEdit() self.txt_search.setPlaceholderText("Search uploads...") - self.txt_search.setFixedWidth(200) + self.txt_search.setFixedWidth(180) self.txt_search.textChanged.connect(self.load_items) header_layout.addWidget(self.txt_search) @@ -1114,9 +1241,29 @@ class GalleryWindow(QDialog): self.scroll_area.setWidget(self.scroll_content) main_layout.addWidget(self.scroll_area) - self.current_cols = 0 + # Connect scroll listener for lazy loading next batch + self.scroll_area.verticalScrollBar().valueChanged.connect(self.on_scroll) + + self.populate_instance_filter() self.load_items() + def populate_instance_filter(self): + prev_signal_state = self.cb_instance.blockSignals(True) + self.cb_instance.clear() + self.cb_instance.addItem("All Instances") + + config = get_env_config() + configured = [inst.get("name") for inst in config.get("instances", []) if inst.get("name")] + + history = load_history() + from_history = [h.get("instance_name") for h in history if h.get("instance_name")] + + all_names = sorted(list(set([n for n in (configured + from_history) if n]))) + for name in all_names: + self.cb_instance.addItem(name) + + self.cb_instance.blockSignals(prev_signal_state) + def get_column_count(self): w = self.scroll_area.viewport().width() if w <= 0: @@ -1128,6 +1275,11 @@ class GalleryWindow(QDialog): return cols def load_items(self): + # Clear existing cards + for card in self.cards: + card.deleteLater() + self.cards.clear() + while self.grid_layout.count(): child = self.grid_layout.takeAt(0) if child.widget(): @@ -1138,9 +1290,25 @@ class GalleryWindow(QDialog): self.grid_layout.setColumnStretch(c, 0) history = load_history() + + # Filter by selected Instance + selected_instance = self.cb_instance.currentText() + if selected_instance and selected_instance != "All Instances": + history = [ + h for h in history + if h.get("instance_name", "Default Instance") == selected_instance or selected_instance.lower() in h.get("url", "").lower() + ] + + # Filter by Search Query query = self.txt_search.text().strip().lower() if query: - history = [h for h in history if query in h.get("filename", "").lower() or query in h.get("url", "").lower()] + history = [ + h for h in history + if query in h.get("filename", "").lower() or query in h.get("url", "").lower() or query in h.get("instance_name", "").lower() + ] + + self.filtered_history = history + self.loaded_count = 0 if not history: lbl_empty = QLabel("No uploads found.") @@ -1155,8 +1323,57 @@ class GalleryWindow(QDialog): for c in range(cols): self.grid_layout.setColumnStretch(c, 1) - for idx, item in enumerate(history): - card = GalleryCard(item, on_delete=self.delete_item) + self.load_next_batch() + + def load_next_batch(self): + if self.is_loading_batch or self.loaded_count >= len(self.filtered_history): + return + + self.is_loading_batch = True + try: + cols = self.get_column_count() + self.current_cols = cols + + start_idx = self.loaded_count + end_idx = min(len(self.filtered_history), start_idx + self.BATCH_SIZE) + + for idx in range(start_idx, end_idx): + item = self.filtered_history[idx] + card = GalleryCard(item, on_delete=self.delete_item) + self.cards.append(card) + row = idx // cols + col = idx % cols + self.grid_layout.addWidget(card, row, col) + + self.loaded_count = end_idx + finally: + self.is_loading_batch = False + + QTimer.singleShot(50, self.check_fill_viewport) + + def check_fill_viewport(self): + vbar = self.scroll_area.verticalScrollBar() + if not vbar.isVisible() and self.loaded_count < len(self.filtered_history): + self.load_next_batch() + + def on_scroll(self, value): + vbar = self.scroll_area.verticalScrollBar() + if value >= vbar.maximum() - 150: + self.load_next_batch() + + def relayout_cards(self): + cols = self.get_column_count() + self.current_cols = cols + + for c in range(20): + self.grid_layout.setColumnStretch(c, 1 if c < cols else 0) + + # Remove cards from grid without destroying them + for card in self.cards: + self.grid_layout.removeWidget(card) + + # Re-add cards at new grid positions + for idx, card in enumerate(self.cards): row = idx // cols col = idx % cols self.grid_layout.addWidget(card, row, col) @@ -1164,18 +1381,25 @@ class GalleryWindow(QDialog): def resizeEvent(self, event): super().resizeEvent(event) cols = self.get_column_count() - if cols != getattr(self, "current_cols", 0): - self.load_items() + if cols != getattr(self, "current_cols", 0) and self.cards: + self.relayout_cards() def delete_item(self, item): + cache_key = item.get("url") or item.get("file_path") or item.get("thumbnail_path") + if cache_key and cache_key in THUMBNAIL_CACHE: + del THUMBNAIL_CACHE[cache_key] + history = load_history() history = [h for h in history if h.get("url") != item.get("url")] save_history(history) + self.populate_instance_filter() self.load_items() def on_clear_history(self): if QMessageBox.question(self, "Clear History", "Are you sure you want to clear your upload history?", QMessageBox.Yes | QMessageBox.No) == QMessageBox.Yes: + THUMBNAIL_CACHE.clear() save_history([]) + self.populate_instance_filter() self.load_items() # ==========================================