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 import asyncio
from pathlib import Path 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.QtGui import QPixmap
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QWidget, QWidget,
@ -80,15 +80,21 @@ class BookmarksView(QWidget):
top.addWidget(manage_btn) top.addWidget(manage_btn)
self._search_input = QLineEdit() 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) 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) 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) layout.addLayout(top)
# Count label # Count label

View File

@ -7,7 +7,7 @@ import os
import threading import threading
from pathlib import Path 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.QtGui import QPixmap
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QWidget, QWidget,
@ -89,8 +89,20 @@ class LibraryView(QWidget):
top.addWidget(refresh_btn) top.addWidget(refresh_btn)
self._search_input = QLineEdit() 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) 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) top.addWidget(self._search_input, stretch=1)
layout.addLayout(top) layout.addLayout(top)
@ -111,12 +123,28 @@ class LibraryView(QWidget):
# Public # 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: def refresh(self) -> None:
"""Scan the selected folder, sort, display thumbnails.""" """Scan the selected folder, sort, display thumbnails."""
root = saved_dir() root = saved_dir()
if not root.exists() or not os.access(root, os.R_OK): if not root.exists() or not os.access(root, os.R_OK):
self._count_label.setText("Library directory unreachable") self._set_count("Library directory unreachable", "error")
self._count_label.setStyleSheet("color: #ff4444; font-weight: bold;")
self._grid.set_posts(0) self._grid.set_posts(0)
self._files = [] self._files = []
return return
@ -134,11 +162,14 @@ class LibraryView(QWidget):
self._files = [] self._files = []
if self._files: if self._files:
self._count_label.setText(f"{len(self._files)} files") self._set_count(f"{len(self._files)} files", "normal")
self._count_label.setStyleSheet("") elif query:
# Search returned nothing — not an error, just no matches.
self._set_count("No items match search", "empty")
else: else:
self._count_label.setText("Library empty or directory unreachable") # The library is genuinely empty (the directory exists and is
self._count_label.setStyleSheet("color: #ff4444;") # 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)) thumbs = self._grid.set_posts(len(self._files))
lib_thumb_dir = thumbnails_dir() / "library" 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 ### Thumbnail Indicators and Selection Colors
```css ```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) ---------- */ /* ---------- Thumbnail dot indicators (Qt properties on ThumbnailWidget) ---------- */
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) ---------- */ /* ---------- Thumbnail dot indicators (Qt properties on ThumbnailWidget) ---------- */
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) ---------- */ /* ---------- Thumbnail dot indicators (Qt properties on ThumbnailWidget) ---------- */
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) ---------- */ /* ---------- Thumbnail dot indicators (Qt properties on ThumbnailWidget) ---------- */
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) ---------- */ /* ---------- Thumbnail dot indicators (Qt properties on ThumbnailWidget) ---------- */
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) ---------- */ /* ---------- Thumbnail dot indicators (Qt properties on ThumbnailWidget) ---------- */
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) ---------- */ /* ---------- Thumbnail dot indicators (Qt properties on ThumbnailWidget) ---------- */
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) ---------- */ /* ---------- Thumbnail dot indicators (Qt properties on ThumbnailWidget) ---------- */
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) ---------- */ /* ---------- Thumbnail dot indicators (Qt properties on ThumbnailWidget) ---------- */
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) ---------- */ /* ---------- Thumbnail dot indicators (Qt properties on ThumbnailWidget) ---------- */
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) ---------- */ /* ---------- Thumbnail dot indicators (Qt properties on ThumbnailWidget) ---------- */
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) ---------- */ /* ---------- Thumbnail dot indicators (Qt properties on ThumbnailWidget) ---------- */
ThumbnailWidget { ThumbnailWidget {