649 Commits

Author SHA1 Message Date
pax
738e1329b8 main_window: read image dimensions for bookmark popout aspect lock
Bookmark Post objects have no width/height (the DB doesn't store
them). When opening a bookmark in the popout, read the actual
dimensions from the cached file via QImageReader so the popout
can set keep_aspect_ratio correctly. Previously images from the
bookmarks tab always got 0x0, skipping the aspect lock.
2026-04-11 23:18:06 -05:00
pax
a3cb563ae0 grid: shorten thumbnail fade-in from 200ms to 80ms 2026-04-11 23:18:06 -05:00
pax
60cf4e0beb grid: fix fade animation cleanup crashing FlowLayout.clear
The previous deleteLater on the QPropertyAnimation left a dangling
self._fade_anim reference to a dead C++ object. When the next
search called FlowLayout.clear(), calling .stop() on the dead
animation threw RuntimeError and aborted widget cleanup, leaving
stale thumbnails in the grid.

Now the finished callback nulls self._fade_anim before scheduling
deletion, so clear() never touches a dead object.
2026-04-11 23:10:54 -05:00
pax
692a0c1569 grid: clean up QPropertyAnimation after fade completes
Connect finished signal to deleteLater so the animation object
is freed instead of being retained on the widget indefinitely.
2026-04-11 23:01:45 -05:00
pax
b964a77688 cache: single-pass directory walk in eviction functions
evict_oldest and evict_oldest_thumbnails now collect paths, stats,
and sizes in one iterdir() pass instead of separate passes for
sorting, sizing, and deleting. evict_oldest also accepts a
current_bytes arg to skip a redundant cache_size_bytes() call.
2026-04-11 23:01:44 -05:00
pax
10f1b3fd10 test_mpv_options: update demuxer_max_bytes assertion to 50MiB 2026-04-11 23:01:41 -05:00
pax
5564f4cf0a video_player: pass 150MiB demuxer cap for streaming URLs
Per-file override so network video buffering stays at the
previous level despite the lower default in _mpv_options.
2026-04-11 23:01:40 -05:00
pax
b055cdd1a2 _mpv_options: reduce default demuxer buffer from 150MiB to 50MiB
150MiB is excessive for local cached file playback. Network
streaming URLs get the 150MiB cap via a per-file override in
play_file() so the fast-path buffering is unaffected.

behavior change: mpv allocates less demuxer memory for local files.
2026-04-11 23:01:38 -05:00
pax
45b87adb33 media_controller: cancel stale prefetch spirals on new click
Each prefetch_adjacent() call now bumps a generation counter.
Running spirals check the counter at each iteration and exit
when superseded. Previously, rapid clicks between posts stacked
up concurrent download loops that never cancelled, accumulating
HTTP connections and response buffers.

Also incrementally updates the search controller's cached-names
set when a download completes, avoiding a full directory rescan.

behavior change: only the most recent click's prefetch spiral
runs; older ones exit at their next iteration.
2026-04-11 23:01:35 -05:00
pax
c11cca1134 settings: remove stale restart-required label from flip layout
The setting now applies live — the "(restart required)" label was
left over from before the live-apply change.
2026-04-11 22:54:04 -05:00
pax
fa8c5b84cf media_controller: throttle auto_evict_cache to once per 30s
cache_size_bytes() does a full stat() of every file in the cache
directory. It was called on every image load and every infinite
scroll drain. Now skipped if less than 30 seconds since the last
check.

Also replace QPixmap with QImageReader in image_dimensions() to
read width/height from the file header without decoding the full
image into memory.

behavior change: cache eviction checks run at most once per 30s
instead of on every image load. Library image dimensions are read
via QImageReader (header-only) instead of QPixmap (full decode).
2026-04-11 22:49:00 -05:00
pax
c3258c1d53 post_actions: invalidate search lookup caches on bookmark/save 2026-04-11 22:48:55 -05:00
pax
3a95b6817d search_controller: cache lookup sets across infinite scroll appends
Build the cache-dir listing, bookmark ID set, and saved-post ID
set once in on_search_done and reuse in _drain_append_queue.
Previously these were rebuilt from scratch on every infinite
scroll append — a full directory listing and two DB queries per
page load.

Caches are invalidated on new search, site change, and
bookmark/save operations via invalidate_lookup_caches().
2026-04-11 22:48:54 -05:00
pax
b00f3ff95c grid: recycle decoded pixmaps for off-screen thumbnails
Release _pixmap for ThumbnailWidgets outside the visible viewport
plus a 5-row buffer zone. Re-decode from the on-disk thumbnail
cache (_source_path) when they scroll back into view. Caps decoded
thumbnail memory to the visible area instead of growing unboundedly
during infinite scroll.

behavior change: off-screen thumbnails release their decoded
pixmaps and re-decode on scroll-back. No visual difference —
the buffer zone prevents flicker.
2026-04-11 22:48:49 -05:00
pax
172fae9583 main_window: re-decode thumbnails from disk on size change
The settings thumbnail-resize path now loads from _source_path
instead of scaling from a held _source_pixmap (which no longer
exists after the grid.py change).
2026-04-11 22:40:56 -05:00
pax
12ec94b4b1 library: pass thumbnail path to set_pixmap 2026-04-11 22:40:55 -05:00
pax
f83435904a bookmarks: pass thumbnail path to set_pixmap 2026-04-11 22:40:54 -05:00
pax
a73c2d6b02 search_controller: pass thumbnail path to set_pixmap 2026-04-11 22:40:53 -05:00
pax
738ece9cd5 grid: replace _source_pixmap with _source_path
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.
2026-04-11 22:40:49 -05:00
pax
3d288a909f search_controller: reset page to 1 on new search
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.
2026-04-11 22:30:23 -05:00
pax
a8dfff90c5 search: fix autocomplete for multi-tag queries
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.
2026-04-11 22:30:21 -05:00
pax
14033b57b5 main_window: live-apply thumbnail size and flip layout
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.
2026-04-11 22:26:31 -05:00
pax
9592830e67 grid: store source pixmap for lossless re-scaling
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.
2026-04-11 22:26:28 -05:00
pax
d895c28608 settings: add Apply button
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.
2026-04-11 22:23:46 -05:00
pax
53a8622020 main_window: preserve tab selection on switch
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.
2026-04-11 22:20:46 -05:00
pax
88f6d769c8 settings: reset dialog platform cache on save
Calls reset_gtk_cache() after writing file_dialog_platform so the
next dialog open picks up the new value without restarting.
2026-04-11 22:19:38 -05:00
pax
5812f54877 dialogs: cache _use_gtk result instead of creating Database per call
_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.
2026-04-11 22:19:36 -05:00
pax
0a046bf936 main_window: remove Ctrl+S and Ctrl+D menu shortcuts
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.
2026-04-11 22:18:34 -05:00
pax
0c0dd55907 popout: increase overlay hover zone
Fixed 40px hover zone was too small on high-DPI monitors. Now scales
to ~10% of window height with a 60px floor.
2026-04-11 22:17:32 -05:00
pax
710839387a info_panel: remove tag count limits
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.
2026-04-11 22:16:00 -05:00
pax
d355f24394 main_window: make S key guard consistent with B/F
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.
2026-04-11 22:15:07 -05:00
pax
f687141f80 privacy: preserve video pause state across privacy toggle
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.
2026-04-11 22:14:15 -05:00
pax
d64b1d6465 popout: make Save/Unsave from Library mutually exclusive
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.
2026-04-11 22:13:23 -05:00
pax
558c19bdb5 preview_pane: make Save/Unsave from Library mutually exclusive
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.
2026-04-11 22:13:23 -05:00
pax
4bcff35708 context_menus: make Save/Unsave from Library mutually exclusive
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.
2026-04-11 22:13:21 -05:00
pax
79419794f6 bookmarks: fix save/unsave UX — no flash, correct dot indicators
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.
2026-04-11 22:13:06 -05:00
pax
5e8035cb1d library: fix Post ID sort for templated filenames
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.
2026-04-11 21:59:20 -05:00
pax
52b76dfc83 library: fix thumbnail cleanup for templated filenames
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.
2026-04-11 21:57:18 -05:00
pax
c210c4b44a popout: fix Copy File to Clipboard, add Copy Image URL
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.
2026-04-11 21:55:07 -05:00
pax
fd21f735fb preview_pane: fix Copy File to Clipboard, add Copy Image URL
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.
2026-04-11 21:55:04 -05:00
pax
e9d1ca7b3a image_viewer: accumulate scroll delta for zoom
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
2026-04-11 20:06:31 -05:00
pax
21f2fa1513 popout: accumulate scroll delta for volume control
Same hi-res scroll fix as preview_pane — accumulate angleDelta to
±120 boundaries before triggering a volume step.

behavior change
2026-04-11 20:06:26 -05:00
pax
ebaacb8a25 preview_pane: accumulate scroll delta for volume control
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
2026-04-11 20:06:22 -05:00
pax
553734fe79 test_mpv_options: update demuxer_max_bytes assertion (50→150MiB) 2026-04-11 20:01:29 -05:00
pax
c1af3f2e02 mpv: revert cache_pause changes, keep larger demuxer buffer
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
2026-04-11 20:00:27 -05:00
pax
7046f9b94e mpv: drop cache_pause_initial (blocks first frame)
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
2026-04-11 19:53:20 -05:00
pax
ac3939ef61 mpv: fix video stutter on network streams
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
2026-04-11 19:51:56 -05:00
pax
e939085ac9 main_window: restore Path import (used at line 69)
Erroneously removed in a51c9a1 — Path is used in __init__ for
set_library_dir(Path(lib_dir)). The dead-code scan missed it.
2026-04-11 19:30:19 -05:00
pax
b28cc0d104 db: escape LIKE wildcards in search_library_meta
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.
2026-04-11 19:28:59 -05:00
pax
37f89c0bf8 search_controller: remove unused saved_dir import 2026-04-11 19:28:44 -05:00