Improve history loading and thumbnail caching UX
Refactors the history loading process to emit entries as soon as they are loaded, allowing the UI to display immediately. Thumbnail downloads are now handled in the background, with widgets updated via a new signal when thumbnails are cached. This prevents UI freezes and improves responsiveness by showing placeholders until thumbnails are available.
This commit is contained in:
@@ -42,14 +42,18 @@ if TYPE_CHECKING:
|
|||||||
class HistoryLoaderThread(QThread):
|
class HistoryLoaderThread(QThread):
|
||||||
"""Thread to load history and pre-fetch thumbnails."""
|
"""Thread to load history and pre-fetch thumbnails."""
|
||||||
|
|
||||||
finished = Signal(list)
|
entries_loaded = Signal(list)
|
||||||
|
thumbnail_loaded = Signal(str)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
try:
|
try:
|
||||||
# HistoryManager uses get_all_entries, not get_all
|
# HistoryManager uses get_all_entries, not get_all
|
||||||
entries = HistoryManager.get_all_entries()
|
entries = HistoryManager.get_all_entries()
|
||||||
|
|
||||||
# Pre-fetch thumbnails so UI doesn't freeze
|
# Emit entries immediately so UI shows up
|
||||||
|
self.entries_loaded.emit(entries)
|
||||||
|
|
||||||
|
# Pre-fetch thumbnails in background
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
thumbnail_url = entry.get("thumbnail_url")
|
thumbnail_url = entry.get("thumbnail_url")
|
||||||
entry_id = entry.get("id", "")
|
entry_id = entry.get("id", "")
|
||||||
@@ -69,14 +73,13 @@ class HistoryLoaderThread(QThread):
|
|||||||
|
|
||||||
image = Image.open(BytesIO(response.content))
|
image = Image.open(BytesIO(response.content))
|
||||||
image.save(thumbnail_path, "JPEG", quality=95, optimize=True)
|
image.save(thumbnail_path, "JPEG", quality=95, optimize=True)
|
||||||
|
self.thumbnail_loaded.emit(entry_id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f"Error caching thumbnail in background: {e}")
|
logger.debug(f"Error caching thumbnail in background: {e}")
|
||||||
|
|
||||||
self.finished.emit(entries)
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error loading history: {e}")
|
logger.error(f"Error loading history: {e}")
|
||||||
self.finished.emit([])
|
self.entries_loaded.emit([])
|
||||||
|
|
||||||
|
|
||||||
class HistoryEntryWidget(QFrame):
|
class HistoryEntryWidget(QFrame):
|
||||||
@@ -246,40 +249,13 @@ class HistoryEntryWidget(QFrame):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f"Error loading cached thumbnail: {e}")
|
logger.debug(f"Error loading cached thumbnail: {e}")
|
||||||
|
|
||||||
# Download thumbnail
|
# If not in cache, just set placeholder.
|
||||||
try:
|
# Background thread will download it and notify parent to reload.
|
||||||
response = requests.get(thumbnail_url, timeout=5)
|
self.set_placeholder_thumbnail()
|
||||||
response.raise_for_status()
|
|
||||||
|
|
||||||
image = Image.open(BytesIO(response.content))
|
def reload_thumbnail(self):
|
||||||
|
"""Reload thumbnail from cache (called when background download finishes)."""
|
||||||
# Don't resize, keep original quality and just save at higher quality
|
self.load_thumbnail()
|
||||||
# The QPixmap scaling will handle the display size with high quality
|
|
||||||
|
|
||||||
# Save to cache with higher quality
|
|
||||||
try:
|
|
||||||
APP_THUMBNAILS_DIR.mkdir(parents=True, exist_ok=True)
|
|
||||||
image.save(thumbnail_path, "JPEG", quality=95, optimize=True)
|
|
||||||
except Exception as e:
|
|
||||||
logger.debug(f"Error caching thumbnail: {e}")
|
|
||||||
|
|
||||||
# Convert to QPixmap
|
|
||||||
image_bytes = BytesIO()
|
|
||||||
image.save(image_bytes, format="JPEG", quality=95)
|
|
||||||
image_bytes.seek(0)
|
|
||||||
|
|
||||||
pixmap = QPixmap()
|
|
||||||
pixmap.loadFromData(image_bytes.read())
|
|
||||||
|
|
||||||
if not pixmap.isNull():
|
|
||||||
# Don't scale here, let setScaledContents handle it
|
|
||||||
self.thumbnail_label.setPixmap(pixmap)
|
|
||||||
else:
|
|
||||||
self.set_placeholder_thumbnail()
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.debug(f"Error downloading thumbnail: {e}")
|
|
||||||
self.set_placeholder_thumbnail()
|
|
||||||
|
|
||||||
def set_placeholder_thumbnail(self):
|
def set_placeholder_thumbnail(self):
|
||||||
"""Set a placeholder when thumbnail is not available."""
|
"""Set a placeholder when thumbnail is not available."""
|
||||||
@@ -401,6 +377,7 @@ class HistoryDialog(QDialog):
|
|||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.parent_app = parent
|
self.parent_app = parent
|
||||||
self.entry_widgets = []
|
self.entry_widgets = []
|
||||||
|
self.entry_widgets_map = {} # Map entry_id -> widget
|
||||||
|
|
||||||
self.setup_ui()
|
self.setup_ui()
|
||||||
|
|
||||||
@@ -414,13 +391,19 @@ class HistoryDialog(QDialog):
|
|||||||
def start_loading_history(self):
|
def start_loading_history(self):
|
||||||
"""Start the background thread to load history."""
|
"""Start the background thread to load history."""
|
||||||
self.loader_thread = HistoryLoaderThread()
|
self.loader_thread = HistoryLoaderThread()
|
||||||
self.loader_thread.finished.connect(self.on_history_loaded)
|
self.loader_thread.entries_loaded.connect(self.on_history_loaded)
|
||||||
|
self.loader_thread.thumbnail_loaded.connect(self.update_entry_thumbnail)
|
||||||
self.loader_thread.start()
|
self.loader_thread.start()
|
||||||
|
|
||||||
def on_history_loaded(self, entries):
|
def on_history_loaded(self, entries):
|
||||||
"""Called when history is loaded from background thread."""
|
"""Called when history is loaded from background thread."""
|
||||||
self.load_history_entries(entries)
|
self.load_history_entries(entries)
|
||||||
|
|
||||||
|
def update_entry_thumbnail(self, entry_id: str):
|
||||||
|
"""Called when a thumbnail is downloaded in the background."""
|
||||||
|
if entry_id in self.entry_widgets_map:
|
||||||
|
self.entry_widgets_map[entry_id].reload_thumbnail()
|
||||||
|
|
||||||
def setup_ui(self):
|
def setup_ui(self):
|
||||||
"""Setup the dialog UI."""
|
"""Setup the dialog UI."""
|
||||||
self.setWindowTitle(_("history.title"))
|
self.setWindowTitle(_("history.title"))
|
||||||
@@ -528,6 +511,7 @@ class HistoryDialog(QDialog):
|
|||||||
for widget in self.entry_widgets:
|
for widget in self.entry_widgets:
|
||||||
widget.deleteLater()
|
widget.deleteLater()
|
||||||
self.entry_widgets.clear()
|
self.entry_widgets.clear()
|
||||||
|
self.entry_widgets_map.clear()
|
||||||
|
|
||||||
def show_loading_state(self):
|
def show_loading_state(self):
|
||||||
"""Show loading indicator."""
|
"""Show loading indicator."""
|
||||||
@@ -572,6 +556,11 @@ class HistoryDialog(QDialog):
|
|||||||
widget.redownload_requested.connect(self.handle_redownload)
|
widget.redownload_requested.connect(self.handle_redownload)
|
||||||
|
|
||||||
self.history_layout.addWidget(widget)
|
self.history_layout.addWidget(widget)
|
||||||
|
|
||||||
|
# Map widget by ID for updates
|
||||||
|
entry_id = entry.get("id")
|
||||||
|
if entry_id:
|
||||||
|
self.entry_widgets_map[entry_id] = widget
|
||||||
self.entry_widgets.append(widget)
|
self.entry_widgets.append(widget)
|
||||||
|
|
||||||
# Update status
|
# Update status
|
||||||
|
|||||||
Reference in New Issue
Block a user