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 import signal
from http.server import HTTPServer, BaseHTTPRequestHandler 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.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.QtNetwork import QNetworkAccessManager, QNetworkRequest, QHttpMultiPart, QHttpPart, QNetworkReply, QLocalServer, QLocalSocket
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
@@ -215,11 +215,20 @@ def save_history(history_list):
try: try:
os.makedirs(CONFIG_DIR, exist_ok=True) os.makedirs(CONFIG_DIR, exist_ok=True)
with open(HISTORY_PATH, "w", encoding="utf-8") as f: 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: except Exception as e:
print(f"Error saving history: {e}") print(f"Error saving history: {e}")
def add_history_entry(entry): 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 = load_history()
history = [h for h in history if h.get("url") != entry.get("url")] history = [h for h in history if h.get("url") != entry.get("url")]
history.insert(0, entry) history.insert(0, entry)
@@ -916,13 +925,17 @@ class SettingsDialog(QDialog):
else: else:
QMessageBox.critical(self, "Error", "Failed to save configuration file.") QMessageBox.critical(self, "Error", "Failed to save configuration file.")
# ==========================================
# Draggable File Label Widget
# ==========================================
# ========================================== # ==========================================
# Draggable File Label Widget # Draggable File Label Widget
# ========================================== # ==========================================
class DraggableImageLabel(QLabel): class DraggableImageLabel(QLabel):
def __init__(self, file_path, parent=None): def __init__(self, item, parent=None):
super().__init__(parent) 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 self.drag_start_pos = None
def mousePressEvent(self, event): def mousePressEvent(self, event):
@@ -930,6 +943,19 @@ class DraggableImageLabel(QLabel):
self.drag_start_pos = event.position().toPoint() self.drag_start_pos = event.position().toPoint()
super().mousePressEvent(event) 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): def mouseMoveEvent(self, event):
if not (event.buttons() & Qt.MouseButton.LeftButton): if not (event.buttons() & Qt.MouseButton.LeftButton):
return return
@@ -948,15 +974,52 @@ class DraggableImageLabel(QLabel):
drag.setPixmap(self.pixmap().scaled(80, 80, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)) drag.setPixmap(self.pixmap().scaled(80, 80, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation))
drag.exec(Qt.DropAction.CopyAction) drag.exec(Qt.DropAction.CopyAction)
self.drag_start_pos = None
# ========================================== # ==========================================
# Upload Gallery Window & Components # 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): class GalleryCard(QFrame):
def __init__(self, item, parent=None, on_delete=None): def __init__(self, item, parent=None, on_delete=None):
super().__init__(parent) super().__init__(parent)
self.item = item self.item = item
self.on_delete = on_delete self.on_delete = on_delete
self._is_destroyed = False
self.setFrameShape(QFrame.StyledPanel) self.setFrameShape(QFrame.StyledPanel)
self.setFixedHeight(190) self.setFixedHeight(190)
self.setMinimumWidth(160) self.setMinimumWidth(160)
@@ -977,35 +1040,15 @@ class GalleryCard(QFrame):
layout.setContentsMargins(8, 8, 8, 8) layout.setContentsMargins(8, 8, 8, 8)
layout.setSpacing(6) layout.setSpacing(6)
# Thumbnail with Drag & Drop support # Thumbnail with Drag & Drop & Click support
file_path = item.get("file_path", "") self.thumb_label = DraggableImageLabel(item)
self.thumb_label = DraggableImageLabel(file_path)
self.thumb_label.setFixedHeight(95) self.thumb_label.setFixedHeight(95)
self.thumb_label.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) self.thumb_label.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
self.thumb_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.thumb_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.thumb_label.setCursor(Qt.CursorShape.OpenHandCursor) self.thumb_label.setCursor(Qt.CursorShape.PointingHandCursor)
self.thumb_label.setToolTip("Click & Drag to drop file into other apps") self.thumb_label.setToolTip("Click to open file | Drag to drop into other apps")
self.thumb_label.setStyleSheet("background-color: #121216; border-radius: 4px;") 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) layout.addWidget(self.thumb_label)
# Filename # Filename
@@ -1015,12 +1058,37 @@ class GalleryCard(QFrame):
lbl_name.setToolTip(item.get("filename", "")) lbl_name.setToolTip(item.get("filename", ""))
layout.addWidget(lbl_name) 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 # Timestamp
ts = item.get("timestamp", 0) ts = item.get("timestamp", 0)
dt_str = time.strftime("%Y-%m-%d %H:%M", time.localtime(ts)) if ts else "" dt_str = time.strftime("%Y-%m-%d %H:%M", time.localtime(ts)) if ts else ""
lbl_time = QLabel(dt_str) lbl_time = QLabel(dt_str)
lbl_time.setStyleSheet("font-size: 9px; color: #8888a0;") 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 # Action Buttons
btn_layout = QHBoxLayout() btn_layout = QHBoxLayout()
@@ -1047,6 +1115,49 @@ class GalleryCard(QFrame):
layout.addLayout(btn_layout) 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): def on_copy_link(self):
url = self.item.get("url", "") url = self.item.get("url", "")
if url: if url:
@@ -1062,10 +1173,18 @@ class GalleryCard(QFrame):
self.on_delete(self.item) self.on_delete(self.item)
class GalleryWindow(QDialog): class GalleryWindow(QDialog):
BATCH_SIZE = 16
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self.setWindowTitle("f0ckm Upload Gallery") 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() app_icon = get_app_icon()
if not app_icon.isNull(): if not app_icon.isNull():
self.setWindowIcon(app_icon) self.setWindowIcon(app_icon)
@@ -1086,9 +1205,17 @@ class GalleryWindow(QDialog):
header_layout.addStretch() 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 = QLineEdit()
self.txt_search.setPlaceholderText("Search uploads...") self.txt_search.setPlaceholderText("Search uploads...")
self.txt_search.setFixedWidth(200) self.txt_search.setFixedWidth(180)
self.txt_search.textChanged.connect(self.load_items) self.txt_search.textChanged.connect(self.load_items)
header_layout.addWidget(self.txt_search) header_layout.addWidget(self.txt_search)
@@ -1114,9 +1241,29 @@ class GalleryWindow(QDialog):
self.scroll_area.setWidget(self.scroll_content) self.scroll_area.setWidget(self.scroll_content)
main_layout.addWidget(self.scroll_area) 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() 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): def get_column_count(self):
w = self.scroll_area.viewport().width() w = self.scroll_area.viewport().width()
if w <= 0: if w <= 0:
@@ -1128,6 +1275,11 @@ class GalleryWindow(QDialog):
return cols return cols
def load_items(self): def load_items(self):
# Clear existing cards
for card in self.cards:
card.deleteLater()
self.cards.clear()
while self.grid_layout.count(): while self.grid_layout.count():
child = self.grid_layout.takeAt(0) child = self.grid_layout.takeAt(0)
if child.widget(): if child.widget():
@@ -1138,9 +1290,25 @@ class GalleryWindow(QDialog):
self.grid_layout.setColumnStretch(c, 0) self.grid_layout.setColumnStretch(c, 0)
history = load_history() 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() query = self.txt_search.text().strip().lower()
if query: 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: if not history:
lbl_empty = QLabel("No uploads found.") lbl_empty = QLabel("No uploads found.")
@@ -1155,8 +1323,57 @@ class GalleryWindow(QDialog):
for c in range(cols): for c in range(cols):
self.grid_layout.setColumnStretch(c, 1) self.grid_layout.setColumnStretch(c, 1)
for idx, item in enumerate(history): self.load_next_batch()
card = GalleryCard(item, on_delete=self.delete_item)
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 row = idx // cols
col = idx % cols col = idx % cols
self.grid_layout.addWidget(card, row, col) self.grid_layout.addWidget(card, row, col)
@@ -1164,18 +1381,25 @@ class GalleryWindow(QDialog):
def resizeEvent(self, event): def resizeEvent(self, event):
super().resizeEvent(event) super().resizeEvent(event)
cols = self.get_column_count() cols = self.get_column_count()
if cols != getattr(self, "current_cols", 0): if cols != getattr(self, "current_cols", 0) and self.cards:
self.load_items() self.relayout_cards()
def delete_item(self, item): 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 = load_history()
history = [h for h in history if h.get("url") != item.get("url")] history = [h for h in history if h.get("url") != item.get("url")]
save_history(history) save_history(history)
self.populate_instance_filter()
self.load_items() self.load_items()
def on_clear_history(self): 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: 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([]) save_history([])
self.populate_instance_filter()
self.load_items() self.load_items()
# ========================================== # ==========================================