Optional prefetch with progress bar, post blacklist, theme template link
- Prefetch adjacent posts is now a toggle in Settings > General (off by default) - Prefetch progress bar on thumbnails shows download state - Blacklist Post: right-click to hide a specific post by URL - "Create from Template" opens themes reference on git.pax.moe and spawns the default text editor with custom.qss
This commit is contained in:
parent
5d48581f52
commit
f311326e73
@ -54,6 +54,11 @@ CREATE TABLE IF NOT EXISTS blacklisted_tags (
|
|||||||
tag TEXT NOT NULL UNIQUE
|
tag TEXT NOT NULL UNIQUE
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS blacklisted_posts (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
url TEXT NOT NULL UNIQUE
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS settings (
|
CREATE TABLE IF NOT EXISTS settings (
|
||||||
key TEXT PRIMARY KEY,
|
key TEXT PRIMARY KEY,
|
||||||
value TEXT NOT NULL
|
value TEXT NOT NULL
|
||||||
@ -384,6 +389,20 @@ class Database:
|
|||||||
rows = self.conn.execute("SELECT tag FROM blacklisted_tags ORDER BY tag").fetchall()
|
rows = self.conn.execute("SELECT tag FROM blacklisted_tags ORDER BY tag").fetchall()
|
||||||
return [r["tag"] for r in rows]
|
return [r["tag"] for r in rows]
|
||||||
|
|
||||||
|
# -- Blacklisted Posts --
|
||||||
|
|
||||||
|
def add_blacklisted_post(self, url: str) -> None:
|
||||||
|
self.conn.execute("INSERT OR IGNORE INTO blacklisted_posts (url) VALUES (?)", (url,))
|
||||||
|
self.conn.commit()
|
||||||
|
|
||||||
|
def remove_blacklisted_post(self, url: str) -> None:
|
||||||
|
self.conn.execute("DELETE FROM blacklisted_posts WHERE url = ?", (url,))
|
||||||
|
self.conn.commit()
|
||||||
|
|
||||||
|
def get_blacklisted_posts(self) -> set[str]:
|
||||||
|
rows = self.conn.execute("SELECT url FROM blacklisted_posts").fetchall()
|
||||||
|
return {r["url"] for r in rows}
|
||||||
|
|
||||||
# -- Settings --
|
# -- Settings --
|
||||||
|
|
||||||
def get_setting(self, key: str) -> str:
|
def get_setting(self, key: str) -> str:
|
||||||
|
|||||||
@ -84,6 +84,7 @@ class AsyncSignals(QObject):
|
|||||||
batch_progress = Signal(int, int) # current, total
|
batch_progress = Signal(int, int) # current, total
|
||||||
batch_done = Signal(str)
|
batch_done = Signal(str)
|
||||||
download_progress = Signal(int, int) # bytes_downloaded, total_bytes
|
download_progress = Signal(int, int) # bytes_downloaded, total_bytes
|
||||||
|
prefetch_progress = Signal(int, float) # index, progress (0-1 or -1 to hide)
|
||||||
|
|
||||||
|
|
||||||
# -- Info Panel --
|
# -- Info Panel --
|
||||||
@ -236,6 +237,11 @@ class BooruApp(QMainWindow):
|
|||||||
s.batch_progress.connect(self._on_batch_progress, Q)
|
s.batch_progress.connect(self._on_batch_progress, Q)
|
||||||
s.batch_done.connect(lambda m: self._status.showMessage(m), Q)
|
s.batch_done.connect(lambda m: self._status.showMessage(m), Q)
|
||||||
s.download_progress.connect(self._on_download_progress, Q)
|
s.download_progress.connect(self._on_download_progress, Q)
|
||||||
|
s.prefetch_progress.connect(self._on_prefetch_progress, Q)
|
||||||
|
|
||||||
|
def _on_prefetch_progress(self, index: int, progress: float) -> None:
|
||||||
|
if 0 <= index < len(self._grid._thumbs):
|
||||||
|
self._grid._thumbs[index].set_prefetch_progress(progress)
|
||||||
|
|
||||||
def _clear_loading(self) -> None:
|
def _clear_loading(self) -> None:
|
||||||
self._loading = False
|
self._loading = False
|
||||||
@ -628,6 +634,10 @@ class BooruApp(QMainWindow):
|
|||||||
bl_tags = set(self._db.get_blacklisted_tags())
|
bl_tags = set(self._db.get_blacklisted_tags())
|
||||||
if bl_tags:
|
if bl_tags:
|
||||||
posts = [p for p in posts if not bl_tags.intersection(p.tag_list)]
|
posts = [p for p in posts if not bl_tags.intersection(p.tag_list)]
|
||||||
|
# Filter blacklisted posts by URL
|
||||||
|
bl_posts = self._db.get_blacklisted_posts()
|
||||||
|
if bl_posts:
|
||||||
|
posts = [p for p in posts if p.file_url not in bl_posts]
|
||||||
self._posts = posts
|
self._posts = posts
|
||||||
self._status.showMessage(f"{len(posts)} results")
|
self._status.showMessage(f"{len(posts)} results")
|
||||||
thumbs = self._grid.set_posts(len(posts))
|
thumbs = self._grid.set_posts(len(posts))
|
||||||
@ -762,6 +772,7 @@ class BooruApp(QMainWindow):
|
|||||||
self._run_async(_load)
|
self._run_async(_load)
|
||||||
|
|
||||||
# Prefetch adjacent posts
|
# Prefetch adjacent posts
|
||||||
|
if self._db.get_setting_bool("prefetch_adjacent"):
|
||||||
self._prefetch_adjacent(index)
|
self._prefetch_adjacent(index)
|
||||||
|
|
||||||
def _prefetch_adjacent(self, index: int) -> None:
|
def _prefetch_adjacent(self, index: int) -> None:
|
||||||
@ -772,10 +783,15 @@ class BooruApp(QMainWindow):
|
|||||||
for offset in offsets:
|
for offset in offsets:
|
||||||
adj = index + offset
|
adj = index + offset
|
||||||
if 0 <= adj < len(self._posts) and self._posts[adj].file_url:
|
if 0 <= adj < len(self._posts) and self._posts[adj].file_url:
|
||||||
|
self._signals.prefetch_progress.emit(adj, 0.0)
|
||||||
try:
|
try:
|
||||||
await download_image(self._posts[adj].file_url)
|
def _progress(dl, total, idx=adj):
|
||||||
|
if total > 0:
|
||||||
|
self._signals.prefetch_progress.emit(idx, dl / total)
|
||||||
|
await download_image(self._posts[adj].file_url, progress_callback=_progress)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
self._signals.prefetch_progress.emit(adj, -1)
|
||||||
self._run_async(_prefetch_batch)
|
self._run_async(_prefetch_batch)
|
||||||
|
|
||||||
def _on_download_progress(self, downloaded: int, total: int) -> None:
|
def _on_download_progress(self, downloaded: int, total: int) -> None:
|
||||||
@ -1054,6 +1070,7 @@ class BooruApp(QMainWindow):
|
|||||||
else:
|
else:
|
||||||
for tag in post.tag_list[:30]:
|
for tag in post.tag_list[:30]:
|
||||||
bl_menu.addAction(tag)
|
bl_menu.addAction(tag)
|
||||||
|
bl_post_action = menu.addAction("Blacklist Post")
|
||||||
|
|
||||||
action = menu.exec(pos)
|
action = menu.exec(pos)
|
||||||
if not action:
|
if not action:
|
||||||
@ -1105,6 +1122,10 @@ class BooruApp(QMainWindow):
|
|||||||
self._db.set_setting("blacklist_enabled", "1")
|
self._db.set_setting("blacklist_enabled", "1")
|
||||||
self._status.showMessage(f"Blacklisted: {tag}")
|
self._status.showMessage(f"Blacklisted: {tag}")
|
||||||
self._do_search()
|
self._do_search()
|
||||||
|
elif action == bl_post_action:
|
||||||
|
self._db.add_blacklisted_post(post.file_url)
|
||||||
|
self._status.showMessage(f"Post #{post.id} blacklisted")
|
||||||
|
self._do_search()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _is_child_of_menu(action, menu) -> bool:
|
def _is_child_of_menu(action, menu) -> bool:
|
||||||
|
|||||||
@ -38,6 +38,7 @@ class ThumbnailWidget(QWidget):
|
|||||||
self._hover = False
|
self._hover = False
|
||||||
self._drag_start: QPoint | None = None
|
self._drag_start: QPoint | None = None
|
||||||
self._cached_path: str | None = None
|
self._cached_path: str | None = None
|
||||||
|
self._prefetch_progress: float = -1 # -1 = not prefetching, 0-1 = progress
|
||||||
self.setFixedSize(THUMB_SIZE, THUMB_SIZE)
|
self.setFixedSize(THUMB_SIZE, THUMB_SIZE)
|
||||||
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||||
self.setMouseTracking(True)
|
self.setMouseTracking(True)
|
||||||
@ -66,6 +67,11 @@ class ThumbnailWidget(QWidget):
|
|||||||
self._saved_locally = saved
|
self._saved_locally = saved
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
|
def set_prefetch_progress(self, progress: float) -> None:
|
||||||
|
"""Set prefetch progress: -1 = hide, 0.0-1.0 = progress."""
|
||||||
|
self._prefetch_progress = progress
|
||||||
|
self.update()
|
||||||
|
|
||||||
def paintEvent(self, event) -> None:
|
def paintEvent(self, event) -> None:
|
||||||
# Ensure QSS is applied so palette picks up custom colors
|
# Ensure QSS is applied so palette picks up custom colors
|
||||||
self.ensurePolished()
|
self.ensurePolished()
|
||||||
@ -120,6 +126,17 @@ class ThumbnailWidget(QWidget):
|
|||||||
p.drawLine(7, 10, 9, 13)
|
p.drawLine(7, 10, 9, 13)
|
||||||
p.drawLine(9, 13, 14, 7)
|
p.drawLine(9, 13, 14, 7)
|
||||||
|
|
||||||
|
# Prefetch progress bar
|
||||||
|
if self._prefetch_progress >= 0:
|
||||||
|
bar_h = 3
|
||||||
|
bar_y = self.height() - bar_h - 2
|
||||||
|
bar_w = int((self.width() - 8) * self._prefetch_progress)
|
||||||
|
p.setPen(Qt.PenStyle.NoPen)
|
||||||
|
p.setBrush(QColor(100, 100, 100, 120))
|
||||||
|
p.drawRect(4, bar_y, self.width() - 8, bar_h)
|
||||||
|
p.setBrush(highlight)
|
||||||
|
p.drawRect(4, bar_y, bar_w, bar_h)
|
||||||
|
|
||||||
p.end()
|
p.end()
|
||||||
|
|
||||||
def enterEvent(self, event) -> None:
|
def enterEvent(self, event) -> None:
|
||||||
|
|||||||
@ -110,6 +110,11 @@ class SettingsDialog(QDialog):
|
|||||||
self._preload.setChecked(self._db.get_setting_bool("preload_thumbnails"))
|
self._preload.setChecked(self._db.get_setting_bool("preload_thumbnails"))
|
||||||
form.addRow("", self._preload)
|
form.addRow("", self._preload)
|
||||||
|
|
||||||
|
# Prefetch adjacent posts
|
||||||
|
self._prefetch = QCheckBox("Prefetch adjacent posts for faster navigation")
|
||||||
|
self._prefetch.setChecked(self._db.get_setting_bool("prefetch_adjacent"))
|
||||||
|
form.addRow("", self._prefetch)
|
||||||
|
|
||||||
# File dialog platform (Linux only)
|
# File dialog platform (Linux only)
|
||||||
self._file_dialog_combo = None
|
self._file_dialog_combo = None
|
||||||
if not IS_WINDOWS:
|
if not IS_WINDOWS:
|
||||||
@ -372,37 +377,14 @@ class SettingsDialog(QDialog):
|
|||||||
QDesktopServices.openUrl(QUrl.fromLocalFile(str(css_path)))
|
QDesktopServices.openUrl(QUrl.fromLocalFile(str(css_path)))
|
||||||
|
|
||||||
def _create_css_template(self) -> None:
|
def _create_css_template(self) -> None:
|
||||||
|
from PySide6.QtGui import QDesktopServices
|
||||||
|
from PySide6.QtCore import QUrl
|
||||||
|
# Open themes reference online and create a blank custom.qss for editing
|
||||||
|
QDesktopServices.openUrl(QUrl("https://git.pax.moe/pax/booru-viewer/src/branch/main/themes"))
|
||||||
css_path = data_dir() / "custom.qss"
|
css_path = data_dir() / "custom.qss"
|
||||||
if css_path.exists():
|
if not css_path.exists():
|
||||||
reply = QMessageBox.question(
|
css_path.write_text("/* booru-viewer custom stylesheet */\n/* See themes reference for examples */\n\n")
|
||||||
self, "Confirm", "Overwrite existing custom.qss with template?",
|
QDesktopServices.openUrl(QUrl.fromLocalFile(str(css_path)))
|
||||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
||||||
)
|
|
||||||
if reply != QMessageBox.StandardButton.Yes:
|
|
||||||
return
|
|
||||||
template = (
|
|
||||||
"/* booru-viewer custom stylesheet */\n"
|
|
||||||
"/* Edit and restart the app to apply changes */\n\n"
|
|
||||||
"/* -- Accent color -- */\n"
|
|
||||||
"/* QWidget { color: #00ff00; } */\n\n"
|
|
||||||
"/* -- Background -- */\n"
|
|
||||||
"/* QWidget { background-color: #000000; } */\n\n"
|
|
||||||
"/* -- Font -- */\n"
|
|
||||||
"/* QWidget { font-family: monospace; font-size: 13px; } */\n\n"
|
|
||||||
"/* -- Buttons -- */\n"
|
|
||||||
"/* QPushButton { padding: 6px 16px; border-radius: 4px; } */\n"
|
|
||||||
"/* QPushButton:hover { border-color: #00ff00; } */\n\n"
|
|
||||||
"/* -- Inputs -- */\n"
|
|
||||||
"/* QLineEdit { padding: 6px 10px; border-radius: 4px; } */\n"
|
|
||||||
"/* QLineEdit:focus { border-color: #00ff00; } */\n\n"
|
|
||||||
"/* -- Scrollbar -- */\n"
|
|
||||||
"/* QScrollBar:vertical { width: 10px; } */\n\n"
|
|
||||||
"/* -- Video seek bar -- */\n"
|
|
||||||
"/* QSlider::groove:horizontal { background: #333; height: 6px; } */\n"
|
|
||||||
"/* QSlider::handle:horizontal { background: #00ff00; width: 14px; } */\n"
|
|
||||||
)
|
|
||||||
css_path.write_text(template)
|
|
||||||
QMessageBox.information(self, "Done", f"Template created at:\n{css_path}")
|
|
||||||
|
|
||||||
def _view_css_guide(self) -> None:
|
def _view_css_guide(self) -> None:
|
||||||
from PySide6.QtGui import QDesktopServices
|
from PySide6.QtGui import QDesktopServices
|
||||||
@ -611,6 +593,7 @@ class SettingsDialog(QDialog):
|
|||||||
self._db.set_setting("default_rating", self._default_rating.currentText())
|
self._db.set_setting("default_rating", self._default_rating.currentText())
|
||||||
self._db.set_setting("default_score", str(self._default_score.value()))
|
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("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("max_cache_mb", str(self._max_cache.value()))
|
self._db.set_setting("max_cache_mb", str(self._max_cache.value()))
|
||||||
self._db.set_setting("auto_evict", "1" if self._auto_evict.isChecked() else "0")
|
self._db.set_setting("auto_evict", "1" if self._auto_evict.isChecked() else "0")
|
||||||
self._db.set_setting("clear_cache_on_exit", "1" if self._clear_on_exit.isChecked() else "0")
|
self._db.set_setting("clear_cache_on_exit", "1" if self._clear_on_exit.isChecked() else "0")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user