Infinite scroll mode — toggle in Settings > General

When enabled, hides prev/next buttons and loads more posts
automatically when scrolling to the bottom. Posts appended
to the grid, deduped against already-shown posts. Restart
required to toggle.
This commit is contained in:
pax 2026-04-05 13:37:38 -05:00
parent 78b7215467
commit 7115d34504
4 changed files with 110 additions and 2 deletions

View File

@ -94,6 +94,7 @@ _DEFAULTS = {
"clear_cache_on_exit": "0",
"slideshow_monitor": "",
"library_dir": "",
"infinite_scroll": "0",
}

View File

@ -75,6 +75,7 @@ class LogHandler(logging.Handler, QObject):
class AsyncSignals(QObject):
"""Signals for async worker results."""
search_done = Signal(list)
search_append = Signal(list)
search_error = Signal(str)
thumb_done = Signal(int, str)
image_done = Signal(str, str)
@ -233,6 +234,7 @@ class BooruApp(QMainWindow):
Q = Qt.ConnectionType.QueuedConnection
s = self._signals
s.search_done.connect(self._on_search_done, Q)
s.search_append.connect(self._on_search_append, Q)
s.search_error.connect(self._on_search_error, Q)
s.thumb_done.connect(self._on_thumb_done, Q)
s.image_done.connect(self._on_image_done, Q)
@ -398,7 +400,9 @@ class BooruApp(QMainWindow):
layout.addWidget(self._splitter, stretch=1)
# Bottom page nav (centered)
bottom_nav = QHBoxLayout()
self._bottom_nav = QWidget()
bottom_nav = QHBoxLayout(self._bottom_nav)
bottom_nav.setContentsMargins(0, 4, 0, 4)
bottom_nav.addStretch()
self._page_label = QLabel("Page 1")
bottom_nav.addWidget(self._page_label)
@ -411,7 +415,13 @@ class BooruApp(QMainWindow):
bottom_next.clicked.connect(self._next_page)
bottom_nav.addWidget(bottom_next)
bottom_nav.addStretch()
layout.addLayout(bottom_nav)
layout.addWidget(self._bottom_nav)
# Infinite scroll
self._infinite_scroll = self._db.get_setting_bool("infinite_scroll")
if self._infinite_scroll:
self._bottom_nav.hide()
self._grid.reached_bottom.connect(self._on_reached_bottom)
# Log panel
self._log_text = QTextEdit()
@ -567,6 +577,50 @@ class BooruApp(QMainWindow):
self._nav_page_turn = "last"
self._prev_page()
def _on_reached_bottom(self) -> None:
if not self._infinite_scroll or self._loading:
return
self._loading = True
self._current_page += 1
search_tags = self._build_search_tags()
page = self._current_page
limit = self._db.get_setting_int("page_size") or 40
bl_tags = set()
if self._db.get_setting_bool("blacklist_enabled"):
bl_tags = set(self._db.get_blacklisted_tags())
bl_posts = self._db.get_blacklisted_posts()
shown_ids = getattr(self, '_shown_post_ids', set()).copy()
def _filter(posts):
if bl_tags:
posts = [p for p in posts if not bl_tags.intersection(p.tag_list)]
if bl_posts:
posts = [p for p in posts if p.file_url not in bl_posts]
posts = [p for p in posts if p.id not in shown_ids]
return posts
async def _search():
client = self._make_client()
try:
collected = []
current_page = page
for _ in range(5):
batch = await client.search(tags=search_tags, page=current_page, limit=limit)
filtered = _filter(batch)
collected.extend(filtered)
if len(collected) >= limit or len(batch) < limit:
break
current_page += 1
self._signals.search_append.emit(collected[:limit])
except Exception as e:
log.warning(f"Operation failed: {e}")
finally:
await client.close()
self._run_async(_search)
def _scroll_next_page(self) -> None:
if self._loading:
return
@ -753,6 +807,39 @@ class BooruApp(QMainWindow):
if self._db.get_setting_bool("prefetch_adjacent") and posts:
self._prefetch_adjacent(0)
def _on_search_append(self, posts: list) -> None:
"""Append more posts to the grid (infinite scroll)."""
if not posts:
self._loading = False
return
self._shown_post_ids.update(p.id for p in posts)
start = len(self._posts)
self._posts.extend(posts)
self._page_label.setText(f"Page {self._current_page}")
self._status.showMessage(f"{len(self._posts)} results")
thumbs = self._grid.append_posts(len(posts))
QTimer.singleShot(100, self._clear_loading)
from ..core.config import saved_dir, saved_folder_dir
site_id = self._site_combo.currentData()
_sd = saved_dir()
_saved_ids: set[int] = set()
if _sd.exists():
_saved_ids = {int(f.stem) for f in _sd.iterdir() if f.is_file() and f.stem.isdigit()}
for i, (post, thumb) in enumerate(zip(posts, thumbs)):
if site_id and self._db.is_bookmarked(site_id, post.id):
thumb.set_bookmarked(True)
saved = post.id in _saved_ids
thumb.set_saved_locally(saved)
from ..core.cache import cached_path_for
cached = cached_path_for(post.file_url)
if cached.exists():
thumb._cached_path = str(cached)
if post.preview_url:
self._fetch_thumbnail(start + i, post.preview_url)
def _fetch_thumbnail(self, index: int, url: str) -> None:
async def _download():
try:

View File

@ -319,6 +319,20 @@ class ThumbnailGrid(QScrollArea):
return self._thumbs
def append_posts(self, count: int) -> list[ThumbnailWidget]:
"""Add more thumbnails to the existing grid."""
start = len(self._thumbs)
new_thumbs = []
for i in range(start, start + count):
thumb = ThumbnailWidget(i)
thumb.clicked.connect(self._on_thumb_click)
thumb.double_clicked.connect(self._on_thumb_double_click)
thumb.right_clicked.connect(self._on_thumb_right_click)
self._flow.add_widget(thumb)
self._thumbs.append(thumb)
new_thumbs.append(thumb)
return new_thumbs
def _clear_multi(self) -> None:
for idx in self._multi_selected:
if 0 <= idx < len(self._thumbs):

View File

@ -115,6 +115,11 @@ class SettingsDialog(QDialog):
self._prefetch.setChecked(self._db.get_setting_bool("prefetch_adjacent"))
form.addRow("", self._prefetch)
# Infinite scroll
self._infinite_scroll = QCheckBox("Infinite scroll (replaces page buttons)")
self._infinite_scroll.setChecked(self._db.get_setting_bool("infinite_scroll"))
form.addRow("", self._infinite_scroll)
# Slideshow monitor
from PySide6.QtWidgets import QApplication
self._monitor_combo = QComboBox()
@ -677,6 +682,7 @@ class SettingsDialog(QDialog):
self._db.set_setting("default_score", str(self._default_score.value()))
self._db.set_setting("preload_thumbnails", "1" if self._preload.isChecked() else "0")
self._db.set_setting("prefetch_adjacent", "1" if self._prefetch.isChecked() else "0")
self._db.set_setting("infinite_scroll", "1" if self._infinite_scroll.isChecked() else "0")
self._db.set_setting("slideshow_monitor", self._monitor_combo.currentText())
self._db.set_setting("library_dir", self._library_dir.text().strip())
self._db.set_setting("max_cache_mb", str(self._max_cache.value()))