From 81ce926c88478b6918c3fc99b4306c80bbf84769 Mon Sep 17 00:00:00 2001 From: pax Date: Tue, 7 Apr 2026 15:26:00 -0500 Subject: [PATCH] Live search in bookmarks/library (debounced) + 3-state library count label with QSS-targetable libraryCountState property --- booru_viewer/gui/bookmarks.py | 20 +++++++----- booru_viewer/gui/library.py | 47 ++++++++++++++++++++++++----- themes/README.md | 16 ++++++++++ themes/catppuccin-mocha-rounded.qss | 21 +++++++++++++ themes/catppuccin-mocha-square.qss | 21 +++++++++++++ themes/everforest-rounded.qss | 21 +++++++++++++ themes/everforest-square.qss | 21 +++++++++++++ themes/gruvbox-rounded.qss | 21 +++++++++++++ themes/gruvbox-square.qss | 21 +++++++++++++ themes/nord-rounded.qss | 21 +++++++++++++ themes/nord-square.qss | 21 +++++++++++++ themes/solarized-dark-rounded.qss | 21 +++++++++++++ themes/solarized-dark-square.qss | 21 +++++++++++++ themes/tokyo-night-rounded.qss | 21 +++++++++++++ themes/tokyo-night-square.qss | 21 +++++++++++++ 15 files changed, 320 insertions(+), 15 deletions(-) diff --git a/booru_viewer/gui/bookmarks.py b/booru_viewer/gui/bookmarks.py index babbca7..dc6c671 100644 --- a/booru_viewer/gui/bookmarks.py +++ b/booru_viewer/gui/bookmarks.py @@ -7,7 +7,7 @@ import threading import asyncio from pathlib import Path -from PySide6.QtCore import Qt, Signal, QObject +from PySide6.QtCore import Qt, Signal, QObject, QTimer from PySide6.QtGui import QPixmap from PySide6.QtWidgets import ( QWidget, @@ -80,15 +80,21 @@ class BookmarksView(QWidget): top.addWidget(manage_btn) self._search_input = QLineEdit() - self._search_input.setPlaceholderText("Search bookmarks by tag...") + self._search_input.setPlaceholderText("Search bookmarks by tag (live, Enter to commit)") + # Enter still triggers an immediate search. self._search_input.returnPressed.connect(self._do_search) + # Live search via debounced timer: every keystroke restarts a + # 150ms one-shot, when the user stops typing the search runs. + # Cheap enough since each search is just one SQLite query. + self._search_debounce = QTimer(self) + self._search_debounce.setSingleShot(True) + self._search_debounce.setInterval(150) + self._search_debounce.timeout.connect(self._do_search) + self._search_input.textChanged.connect( + lambda _: self._search_debounce.start() + ) top.addWidget(self._search_input, stretch=1) - search_btn = QPushButton("Search") - search_btn.setStyleSheet(_btn_style) - search_btn.clicked.connect(self._do_search) - top.addWidget(search_btn) - layout.addLayout(top) # Count label diff --git a/booru_viewer/gui/library.py b/booru_viewer/gui/library.py index 69e4d99..43bf07c 100644 --- a/booru_viewer/gui/library.py +++ b/booru_viewer/gui/library.py @@ -7,7 +7,7 @@ import os import threading from pathlib import Path -from PySide6.QtCore import Qt, Signal, QObject +from PySide6.QtCore import Qt, Signal, QObject, QTimer from PySide6.QtGui import QPixmap from PySide6.QtWidgets import ( QWidget, @@ -89,8 +89,20 @@ class LibraryView(QWidget): top.addWidget(refresh_btn) self._search_input = QLineEdit() - self._search_input.setPlaceholderText("Search tags...") + self._search_input.setPlaceholderText("Search tags (live, Enter to commit)") + # Enter still triggers an immediate refresh. self._search_input.returnPressed.connect(self.refresh) + # Live search via debounced timer. Library refresh is heavier + # than bookmarks (filesystem scan + DB query + thumbnail repop) + # so use a slightly longer 250ms debounce so the user has to pause + # a bit more between keystrokes before the work happens. + self._search_debounce = QTimer(self) + self._search_debounce.setSingleShot(True) + self._search_debounce.setInterval(250) + self._search_debounce.timeout.connect(self.refresh) + self._search_input.textChanged.connect( + lambda _: self._search_debounce.start() + ) top.addWidget(self._search_input, stretch=1) layout.addLayout(top) @@ -111,12 +123,28 @@ class LibraryView(QWidget): # Public # ------------------------------------------------------------------ + def _set_count(self, text: str, state: str = "normal") -> None: + """Update the count label's text and visual state. + + state ∈ {normal, empty, error}. The state is exposed as a Qt + dynamic property `libraryCountState` so themes can target it via + `QLabel[libraryCountState="error"]` selectors. Re-polishes the + widget so a property change at runtime takes effect immediately. + """ + self._count_label.setText(text) + # Clear any inline stylesheet from earlier code paths so the + # theme's QSS rules can take over. + self._count_label.setStyleSheet("") + self._count_label.setProperty("libraryCountState", state) + st = self._count_label.style() + st.unpolish(self._count_label) + st.polish(self._count_label) + def refresh(self) -> None: """Scan the selected folder, sort, display thumbnails.""" root = saved_dir() if not root.exists() or not os.access(root, os.R_OK): - self._count_label.setText("Library directory unreachable") - self._count_label.setStyleSheet("color: #ff4444; font-weight: bold;") + self._set_count("Library directory unreachable", "error") self._grid.set_posts(0) self._files = [] return @@ -134,11 +162,14 @@ class LibraryView(QWidget): self._files = [] if self._files: - self._count_label.setText(f"{len(self._files)} files") - self._count_label.setStyleSheet("") + self._set_count(f"{len(self._files)} files", "normal") + elif query: + # Search returned nothing — not an error, just no matches. + self._set_count("No items match search", "empty") else: - self._count_label.setText("Library empty or directory unreachable") - self._count_label.setStyleSheet("color: #ff4444;") + # The library is genuinely empty (the directory exists and is + # readable, it just has no files in this folder selection). + self._set_count("Library is empty", "empty") thumbs = self._grid.set_posts(len(self._files)) lib_thumb_dir = thumbnails_dir() / "library" diff --git a/themes/README.md b/themes/README.md index 1a24125..ac4acb8 100644 --- a/themes/README.md +++ b/themes/README.md @@ -354,6 +354,22 @@ QRubberBand { } ``` +### Library Count Label States + +The library tab's count label switches between three visual states depending on what `refresh()` finds. The state is exposed as a Qt dynamic property `libraryCountState` so themes target it via attribute selectors: + +```css +QLabel[libraryCountState="empty"] { + color: #a6adc8; /* dim text — search miss or empty folder */ +} +QLabel[libraryCountState="error"] { + color: #f38ba8; /* danger color — directory unreachable */ + font-weight: bold; +} +``` + +The `normal` state (`N files`) inherits the panel's default text color — no rule needed. + ### Thumbnail Indicators and Selection Colors ```css diff --git a/themes/catppuccin-mocha-rounded.qss b/themes/catppuccin-mocha-rounded.qss index 72fbba9..7ec3b1a 100644 --- a/themes/catppuccin-mocha-rounded.qss +++ b/themes/catppuccin-mocha-rounded.qss @@ -510,6 +510,27 @@ QRubberBand { + +/* ---------- Library count label states ---------- */ +/* + * The library tab's count label switches between three visual states + * depending on what refresh() found. The state is exposed as a Qt + * dynamic property `libraryCountState` so users can override these + * rules in their custom.qss without touching the Python. + * + * normal N files — default text color, no rule needed + * empty no items — dim text (no items found, search miss) + * error bad/unreachable — danger color + bold (real error) + */ + +QLabel[libraryCountState="empty"] { + color: ${text_dim}; +} +QLabel[libraryCountState="error"] { + color: ${danger}; + font-weight: bold; +} + /* ---------- Thumbnail dot indicators (Qt properties on ThumbnailWidget) ---------- */ ThumbnailWidget { diff --git a/themes/catppuccin-mocha-square.qss b/themes/catppuccin-mocha-square.qss index 054e00e..09b0123 100644 --- a/themes/catppuccin-mocha-square.qss +++ b/themes/catppuccin-mocha-square.qss @@ -497,6 +497,27 @@ QRubberBand { + +/* ---------- Library count label states ---------- */ +/* + * The library tab's count label switches between three visual states + * depending on what refresh() found. The state is exposed as a Qt + * dynamic property `libraryCountState` so users can override these + * rules in their custom.qss without touching the Python. + * + * normal N files — default text color, no rule needed + * empty no items — dim text (no items found, search miss) + * error bad/unreachable — danger color + bold (real error) + */ + +QLabel[libraryCountState="empty"] { + color: ${text_dim}; +} +QLabel[libraryCountState="error"] { + color: ${danger}; + font-weight: bold; +} + /* ---------- Thumbnail dot indicators (Qt properties on ThumbnailWidget) ---------- */ ThumbnailWidget { diff --git a/themes/everforest-rounded.qss b/themes/everforest-rounded.qss index 3a7215b..603ea50 100644 --- a/themes/everforest-rounded.qss +++ b/themes/everforest-rounded.qss @@ -510,6 +510,27 @@ QRubberBand { + +/* ---------- Library count label states ---------- */ +/* + * The library tab's count label switches between three visual states + * depending on what refresh() found. The state is exposed as a Qt + * dynamic property `libraryCountState` so users can override these + * rules in their custom.qss without touching the Python. + * + * normal N files — default text color, no rule needed + * empty no items — dim text (no items found, search miss) + * error bad/unreachable — danger color + bold (real error) + */ + +QLabel[libraryCountState="empty"] { + color: ${text_dim}; +} +QLabel[libraryCountState="error"] { + color: ${danger}; + font-weight: bold; +} + /* ---------- Thumbnail dot indicators (Qt properties on ThumbnailWidget) ---------- */ ThumbnailWidget { diff --git a/themes/everforest-square.qss b/themes/everforest-square.qss index e59289c..9a38a18 100644 --- a/themes/everforest-square.qss +++ b/themes/everforest-square.qss @@ -497,6 +497,27 @@ QRubberBand { + +/* ---------- Library count label states ---------- */ +/* + * The library tab's count label switches between three visual states + * depending on what refresh() found. The state is exposed as a Qt + * dynamic property `libraryCountState` so users can override these + * rules in their custom.qss without touching the Python. + * + * normal N files — default text color, no rule needed + * empty no items — dim text (no items found, search miss) + * error bad/unreachable — danger color + bold (real error) + */ + +QLabel[libraryCountState="empty"] { + color: ${text_dim}; +} +QLabel[libraryCountState="error"] { + color: ${danger}; + font-weight: bold; +} + /* ---------- Thumbnail dot indicators (Qt properties on ThumbnailWidget) ---------- */ ThumbnailWidget { diff --git a/themes/gruvbox-rounded.qss b/themes/gruvbox-rounded.qss index be578d6..b5f24dd 100644 --- a/themes/gruvbox-rounded.qss +++ b/themes/gruvbox-rounded.qss @@ -510,6 +510,27 @@ QRubberBand { + +/* ---------- Library count label states ---------- */ +/* + * The library tab's count label switches between three visual states + * depending on what refresh() found. The state is exposed as a Qt + * dynamic property `libraryCountState` so users can override these + * rules in their custom.qss without touching the Python. + * + * normal N files — default text color, no rule needed + * empty no items — dim text (no items found, search miss) + * error bad/unreachable — danger color + bold (real error) + */ + +QLabel[libraryCountState="empty"] { + color: ${text_dim}; +} +QLabel[libraryCountState="error"] { + color: ${danger}; + font-weight: bold; +} + /* ---------- Thumbnail dot indicators (Qt properties on ThumbnailWidget) ---------- */ ThumbnailWidget { diff --git a/themes/gruvbox-square.qss b/themes/gruvbox-square.qss index ce02e96..fe2976a 100644 --- a/themes/gruvbox-square.qss +++ b/themes/gruvbox-square.qss @@ -497,6 +497,27 @@ QRubberBand { + +/* ---------- Library count label states ---------- */ +/* + * The library tab's count label switches between three visual states + * depending on what refresh() found. The state is exposed as a Qt + * dynamic property `libraryCountState` so users can override these + * rules in their custom.qss without touching the Python. + * + * normal N files — default text color, no rule needed + * empty no items — dim text (no items found, search miss) + * error bad/unreachable — danger color + bold (real error) + */ + +QLabel[libraryCountState="empty"] { + color: ${text_dim}; +} +QLabel[libraryCountState="error"] { + color: ${danger}; + font-weight: bold; +} + /* ---------- Thumbnail dot indicators (Qt properties on ThumbnailWidget) ---------- */ ThumbnailWidget { diff --git a/themes/nord-rounded.qss b/themes/nord-rounded.qss index 5d0e7e5..d2d4d0c 100644 --- a/themes/nord-rounded.qss +++ b/themes/nord-rounded.qss @@ -510,6 +510,27 @@ QRubberBand { + +/* ---------- Library count label states ---------- */ +/* + * The library tab's count label switches between three visual states + * depending on what refresh() found. The state is exposed as a Qt + * dynamic property `libraryCountState` so users can override these + * rules in their custom.qss without touching the Python. + * + * normal N files — default text color, no rule needed + * empty no items — dim text (no items found, search miss) + * error bad/unreachable — danger color + bold (real error) + */ + +QLabel[libraryCountState="empty"] { + color: ${text_dim}; +} +QLabel[libraryCountState="error"] { + color: ${danger}; + font-weight: bold; +} + /* ---------- Thumbnail dot indicators (Qt properties on ThumbnailWidget) ---------- */ ThumbnailWidget { diff --git a/themes/nord-square.qss b/themes/nord-square.qss index dba2ad9..4ec3400 100644 --- a/themes/nord-square.qss +++ b/themes/nord-square.qss @@ -497,6 +497,27 @@ QRubberBand { + +/* ---------- Library count label states ---------- */ +/* + * The library tab's count label switches between three visual states + * depending on what refresh() found. The state is exposed as a Qt + * dynamic property `libraryCountState` so users can override these + * rules in their custom.qss without touching the Python. + * + * normal N files — default text color, no rule needed + * empty no items — dim text (no items found, search miss) + * error bad/unreachable — danger color + bold (real error) + */ + +QLabel[libraryCountState="empty"] { + color: ${text_dim}; +} +QLabel[libraryCountState="error"] { + color: ${danger}; + font-weight: bold; +} + /* ---------- Thumbnail dot indicators (Qt properties on ThumbnailWidget) ---------- */ ThumbnailWidget { diff --git a/themes/solarized-dark-rounded.qss b/themes/solarized-dark-rounded.qss index c1086e5..015f810 100644 --- a/themes/solarized-dark-rounded.qss +++ b/themes/solarized-dark-rounded.qss @@ -510,6 +510,27 @@ QRubberBand { + +/* ---------- Library count label states ---------- */ +/* + * The library tab's count label switches between three visual states + * depending on what refresh() found. The state is exposed as a Qt + * dynamic property `libraryCountState` so users can override these + * rules in their custom.qss without touching the Python. + * + * normal N files — default text color, no rule needed + * empty no items — dim text (no items found, search miss) + * error bad/unreachable — danger color + bold (real error) + */ + +QLabel[libraryCountState="empty"] { + color: ${text_dim}; +} +QLabel[libraryCountState="error"] { + color: ${danger}; + font-weight: bold; +} + /* ---------- Thumbnail dot indicators (Qt properties on ThumbnailWidget) ---------- */ ThumbnailWidget { diff --git a/themes/solarized-dark-square.qss b/themes/solarized-dark-square.qss index 1662351..b83f4d0 100644 --- a/themes/solarized-dark-square.qss +++ b/themes/solarized-dark-square.qss @@ -497,6 +497,27 @@ QRubberBand { + +/* ---------- Library count label states ---------- */ +/* + * The library tab's count label switches between three visual states + * depending on what refresh() found. The state is exposed as a Qt + * dynamic property `libraryCountState` so users can override these + * rules in their custom.qss without touching the Python. + * + * normal N files — default text color, no rule needed + * empty no items — dim text (no items found, search miss) + * error bad/unreachable — danger color + bold (real error) + */ + +QLabel[libraryCountState="empty"] { + color: ${text_dim}; +} +QLabel[libraryCountState="error"] { + color: ${danger}; + font-weight: bold; +} + /* ---------- Thumbnail dot indicators (Qt properties on ThumbnailWidget) ---------- */ ThumbnailWidget { diff --git a/themes/tokyo-night-rounded.qss b/themes/tokyo-night-rounded.qss index 4665fb3..8ea1239 100644 --- a/themes/tokyo-night-rounded.qss +++ b/themes/tokyo-night-rounded.qss @@ -510,6 +510,27 @@ QRubberBand { + +/* ---------- Library count label states ---------- */ +/* + * The library tab's count label switches between three visual states + * depending on what refresh() found. The state is exposed as a Qt + * dynamic property `libraryCountState` so users can override these + * rules in their custom.qss without touching the Python. + * + * normal N files — default text color, no rule needed + * empty no items — dim text (no items found, search miss) + * error bad/unreachable — danger color + bold (real error) + */ + +QLabel[libraryCountState="empty"] { + color: ${text_dim}; +} +QLabel[libraryCountState="error"] { + color: ${danger}; + font-weight: bold; +} + /* ---------- Thumbnail dot indicators (Qt properties on ThumbnailWidget) ---------- */ ThumbnailWidget { diff --git a/themes/tokyo-night-square.qss b/themes/tokyo-night-square.qss index 49ab3a6..d107304 100644 --- a/themes/tokyo-night-square.qss +++ b/themes/tokyo-night-square.qss @@ -497,6 +497,27 @@ QRubberBand { + +/* ---------- Library count label states ---------- */ +/* + * The library tab's count label switches between three visual states + * depending on what refresh() found. The state is exposed as a Qt + * dynamic property `libraryCountState` so users can override these + * rules in their custom.qss without touching the Python. + * + * normal N files — default text color, no rule needed + * empty no items — dim text (no items found, search miss) + * error bad/unreachable — danger color + bold (real error) + */ + +QLabel[libraryCountState="empty"] { + color: ${text_dim}; +} +QLabel[libraryCountState="error"] { + color: ${danger}; + font-weight: bold; +} + /* ---------- Thumbnail dot indicators (Qt properties on ThumbnailWidget) ---------- */ ThumbnailWidget {