fix recent uploads gallery

This commit is contained in:
2026-08-13 19:38:12 +02:00
parent 971f9d647e
commit acc859919e

298
gui.py
View File

@@ -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()
# ==========================================