Store the on-disk thumbnail path instead of a second decoded QPixmap
per ThumbnailWidget. Saves ~90 KB per widget in decoded pixel memory.
The source pixmap was only needed for the settings thumbnail-resize
path, which now re-decodes from disk (rare operation).
behavior change: thumbnail resize in settings re-reads from disk
instead of scaling from a held pixmap. No visual difference.
on_search previously read the page spin value, so a stale page
number from a previous search carried over. Now resets the spin
to 1 on every new search.
behavior change: new searches always start from page 1.
QCompleter previously replaced the entire search bar text when
accepting a suggestion, wiping all previous tags. Added _TagCompleter
subclass that overrides splitPath (match against last tag only) and
pathFromIndex (prepend existing tags). Accepting a suggestion now
replaces only the last tag.
Space clears the suggestion popup so stale completions from the
previous tag don't linger when starting a new tag.
behavior change: autocomplete preserves existing tags in multi-tag
search; suggestions reset on space.
Thumbnail size change now resizes all existing thumbnails from their
source pixmap and reflows all three grids immediately. No restart
needed.
Flip layout change now swaps the splitter widget order live.
behavior change: thumbnail size and preview-on-left settings apply
instantly via Apply/Save instead of requiring a restart.
set_pixmap now keeps the original pixmap alongside the scaled display
copy. Used by live thumbnail resize in settings — re-scales from the
source instead of the already-scaled pixmap, preventing quality
degradation when changing sizes up and down.
Extracted save logic into _apply() method. Apply writes settings
and emits settings_changed without closing the dialog. Save calls
Apply then closes. Lets users preview setting changes before
committing.
behavior change: settings dialog now has Apply | Save | Cancel.
Tab switch previously cleared all grid selections and nulled
_current_post, losing the user's place and leaving toolbar actions
dead. Now only clears the other tabs' selections — the target tab
keeps its selection so switching back and forth preserves state.
behavior change: switching tabs no longer clears the current tab's
grid selection or preview post.
_use_gtk() created a fresh Database instance on every file dialog
open just to read one setting. Now caches the result at module level
after first check. reset_gtk_cache() clears it when the setting
changes.
Ctrl+S (Manage Sites) and Ctrl+D (Batch Download) violate platform
conventions where these keys mean Save and Bookmark respectively.
Menu items remain accessible via File menu.
behavior change: Ctrl+S and Ctrl+D no longer trigger actions.
Categorized tags were capped at 50 per category and flat tags at
100. Tags area is already inside a QScrollArea so there's no layout
reason for the limit. All tags now render.
behavior change: posts with 50+ tags per category now show all of
them instead of silently truncating.
S key (toggle save) previously checked _preview._current_post which
could be stale after tab switches or right-clicks. Now uses the same
guard as B/F: requires posts loaded and a valid grid selection index.
Previously privacy dismiss unconditionally resumed the embedded
preview video, overriding a manual pause. Now captures whether
the video was playing before privacy activated and only resumes
if it was.
behavior change: manually paused videos stay paused after
privacy screen dismiss.
Context menu now shows either Save to Library or Unsave from Library
based on saved state, never both.
behavior change: popout context menu shows either Save or Unsave.
Context menu now shows either Save to Library or Unsave from Library
based on saved state, never both.
behavior change: preview context menu shows either Save or Unsave.
Previously both Save to Library submenu and Unsave from Library
showed simultaneously for saved posts. Now only the relevant action
appears based on whether the post is already in the library.
Also removed stale _current_post override on unsave — get_preview_post
already resolves the right-clicked post via grid selection index.
behavior change: browse grid context menu shows either Save or
Unsave, never both.
Save to Library and Unsave from Library are now mutually exclusive
in both single and multi-select context menus (previously both
showed simultaneously).
Replaced full grid refresh() after save/unsave with targeted dot
updates — save_done signal fires per-post after async save completes
and lights the saved dot on just that thumbnail. Unsave clears the
dot inline. Eliminates the visible flash from grid rebuild.
behavior change: context menus show either Save or Unsave, never
both. Saved dots appear without grid flash.
Post ID sort used filepath.stem which sorted templated filenames
like artist_12345.jpg alphabetically instead of by post ID. Now
resolves post_id via library_meta DB lookup, falls back to digit-stem
for legacy files, unknowns sort to the end.
Single-delete and multi-delete used filepath.stem for the thumbnail
path, but library thumbnails are keyed by post_id. Templated filenames
like artist_12345.jpg would look for thumbnails/library/artist_12345.jpg
instead of thumbnails/library/12345.jpg, leaving orphan thumbnails.
Now uses the resolved post_id when available, falls back to stem for
legacy digit-stem files.
Fixed self._state → self._state_machine (latent AttributeError when
copying video to clipboard from popout context menu).
Rewrote copy logic to use QMimeData with file URL + image data,
matching main_window's Ctrl+C. For streaming URLs, resolves to the
cached local file. Added Copy Image URL entry for the source URL.
behavior change: clipboard copy now includes file URL; new context
menu entry for URL copy; video copy no longer crashes.
Copy File to Clipboard now sets QMimeData with both the file URL
and image data, matching main_window's Ctrl+C behavior. Previously
it only called setPixmap which didn't work in file managers.
Added Copy Image URL context menu entry that copies the booru CDN
URL as text.
behavior change: clipboard copy now includes file URL for paste
into file managers; new context menu entry for URL copy.
Same hi-res scroll fix — accumulate angleDelta to ±120 boundaries
before applying a zoom step. Uses 1.15^steps so multi-step scrolls
on standard mice still feel the same.
behavior change
Hi-res scroll mice (e.g. G502) send many small angleDelta events
per physical notch instead of one ±120. Without accumulation, each
micro-event triggered a ±5 volume jump, making volume unusable on
hi-res hardware. Now accumulates to ±120 boundaries before firing.
behavior change
The cache_pause=yes change (ac3939e) broke first-click popout
playback — mpv paused indefinitely waiting for cache fill on
uncached videos. Reverted to cache_pause=no.
Kept the demuxer_max_bytes bump (50→150MiB) which reduces stutter
on network streams by giving mpv more buffer headroom without
changing the pause/play behavior.
behavior change
cache_pause_initial=yes made mpv wait for a full buffer before
showing the first frame on uncached videos, which looked like the
popout was broken on first click. Removing it restores immediate
playback start — cache_pause=yes still handles mid-playback
underruns.
behavior change
cache_pause=no caused frame-wait-frame-wait on uncached videos
because mpv kept playing through buffer underruns instead of
pausing to refill. Flip to cache_pause=yes with a 2s resume
threshold so playback is smooth after the initial buffer fill.
Also: bump demuxer buffers (50→150MiB forward, add 75MiB back for
backward seek without refetch), increase stream_buffer_size from
default 128KiB to 4MiB to reduce syscall overhead, extend network
timeout (10→30s) for slow CDNs, and set a browser-like user agent
to avoid 403s from boorus that block mpv's default UA.
behavior change
Same fix as audit #5 applied to get_bookmarks (lines 490-499) but
missed here. Without ESCAPE, searching 'cat_ear' also matches
'catxear' because _ is a SQL LIKE wildcard that matches any single
character.
Unused: Path, Post, QPainterPath, QMenu, QApplication.
FlowLayout.clear() now stops any in-flight fade animation before
calling deleteLater() on thumbnails. Without this, a mid-flight
QPropertyAnimation can fire property updates on a widget that's
queued for deletion.
Two fixes:
1. Stale state cleanup. If a rubber band drag is interrupted without a
matching release event (Wayland focus steal, drag outside window,
tab switch, alt-tab), _rb_origin and the rubber band widget stay
stuck. The next click then reuses the stale origin and rubber band
stops working until the app is restarted. New _clear_stale_rubber_band
helper is called at the top of every mouse press entry point
(Grid.mousePressEvent, on_padding_click, ThumbnailWidget pixmap
press) so the next interaction starts from a clean slate.
2. Scroll offset sign error in _rb_drag. The intersection test
translated thumb geometry by +vp_offset, but thumb.geometry() is in
widget coords and rb_rect is in viewport coords — the translation
needs to convert between them. Switched to translating rb_rect into
widget coords (rb_widget = rb_rect.translated(vp_offset)) before the
intersection test, which is the mathematically correct direction.
Rubber band selection now tracks the visible band when scrolled.
behavior change: rubber band stays responsive after interrupted drags
Targets the internal qt_msgboxex_icon_label by objectName via the
base stylesheet, so confirm/warn/info dialogs across all 36+ call
sites render text-only without per-call setIcon plumbing.
behavior change
Both follow-ups (lock file, dead code in core/images.py) are
resolved or explicitly out of scope. The lock file item was
declined as not worth the dev tooling overhead; the dead code
was just removed in 2186f50.
make_thumbnail and image_dimensions were both unreferenced. The
library's actual thumbnailing happens inline in gui/library.py
(PIL for stills, ffmpeg subprocess for videos), and the live
image_dimensions used by main_window.py is the static method on
gui/media_controller.py — not the standalone function this file
exposed. Audit finding #15 follow-up.
The audit #8 explanation no longer needs to name core.images as the
example case — the invariant holds for any submodule, and core.images
is about to be removed entirely as dead code.
The 'audit #8 invariant' the test was anchored on (core.images
imported without core.cache first) is about to become moot when
images.py is removed in a follow-up commit. Swap to core.config
to keep the same coverage shape: any non-cache submodule import
must still trigger __init__.py and install the PIL cap.
Distro pyside6 builds linked against system Qt pick up the system
platform theme plugin (Breeze on KDE, Adwaita-ish on GNOME, etc.),
which gave AUR users a different widget style than the source-from-pip
build that uses bundled Qt. Force Fusion in the no-custom.qss path so
both routes render identically.
The inherited palette is intentionally untouched: KDE writes
~/.config/Trolltech.conf which every Qt app reads, so KDE users
still get their color scheme — just under Fusion widgets instead
of Breeze.
The two validate_public_request hook tests used @pytest.mark.asyncio
which requires pytest-asyncio at collection time. CI only installs
httpx + Pillow + pytest, so the marker decoded as PytestUnknownMark
and the test bodies failed with "async def functions are not
natively supported."
Switches both to plain sync tests that drive the coroutine via
asyncio.run(), matching the pattern already used in test_cache.py
for the same reason.
Audit-Ref: SECURITY_AUDIT.md finding #1 (test infrastructure)
Calls lavf_options() post mpv.MPV() init and writes each entry into
the demuxer-lavf-o property. This is the consumer side of the split
helpers introduced in the previous commit. Verified end-to-end by
launching the GUI: mpv constructs cleanly and m['demuxer-lavf-o']
reads back as {'protocol_whitelist': 'file,http,https,tls,tcp'}.
Audit-Ref: SECURITY_AUDIT.md finding #2
Severity: High