fix recent uploads gallery
This commit is contained in:
298
gui.py
298
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()
|
||||
|
||||
# ==========================================
|
||||
|
||||
Reference in New Issue
Block a user