Live search in bookmarks/library (debounced) + 3-state library count label with QSS-targetable libraryCountState property

This commit is contained in:
pax 2026-04-07 15:26:00 -05:00
parent 2dfeb4e46c
commit 81ce926c88
15 changed files with 320 additions and 15 deletions

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {