402 Commits

Author SHA1 Message Date
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
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
37f89c0bf8 search_controller: remove unused saved_dir import 2026-04-11 19:28:44 -05:00
pax
925e8c1001 sites: remove unused parse_qs import 2026-04-11 19:28:44 -05:00
pax
a760b39c07 dialogs: remove unused sys and Path imports 2026-04-11 19:28:44 -05:00
pax
77e49268ae settings: remove unused QProgressBar import 2026-04-11 19:28:44 -05:00
pax
e262a2d3bb grid: remove unused imports, stop animation before widget deletion
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.
2026-04-11 19:28:13 -05:00
pax
a51c9a1fda main_window: remove unused imports (os, sys, Path, field, is_cached) 2026-04-11 19:27:44 -05:00
pax
7249d57852 fix rubber band state getting stuck across interrupted drags
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
2026-04-11 18:04:55 -05:00
pax
e31ca07973 hide standard icon column from QMessageBox dialogs
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
2026-04-11 17:35:54 -05:00
pax
a849b8f900 force Fusion widgets when no custom.qss
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.
2026-04-11 17:23:05 -05:00
pax
5858c274c8 security: fix #2 — set lavf options on _MpvGLWidget after construction
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
2026-04-11 16:34:57 -05:00
pax
4db7943ac7 security: fix #2 — apply lavf protocol whitelist via property API
The previous attempt set ``demuxer_lavf_o`` as an init kwarg with a
comma-laden ``protocol_whitelist=file,http,https,tls,tcp`` value.
mpv rejected it with -7 OPT_FORMAT because python-mpv's init path
goes through ``mpv_set_option_string``, which routes through mpv's
keyvalue list parser — that parser splits on ``,`` to find entries,
shredding the protocol list into orphan tokens. Backslash-escaping
``\,`` did not unescape on this code path either.

Splits the option set into two helpers:

- ``build_mpv_kwargs`` — init kwargs only (ytdl=no, load_scripts=no,
  POSIX input_conf null, all the existing playback/audio/network
  tuning). The lavf option is intentionally absent.
- ``lavf_options`` — a dict applied post-construction via the
  python-mpv property API, which uses the node API and accepts
  dict values for keyvalue-list options without splitting on
  commas inside the value.

Tests cover both paths: that ``demuxer_lavf_o`` is NOT in the init
kwargs (regression guard), and that ``lavf_options`` returns the
expected protocol set.

Audit-Ref: SECURITY_AUDIT.md finding #2
Severity: High
2026-04-11 16:34:50 -05:00
pax
b8cb47badb security: fix #6 — escape source via build_source_html in InfoPanel
Replaces the inline f-string concatenation of post.source into the
RichText document with a call through build_source_html(), which
escapes both the href value and the visible display text.

Also escapes the filetype field for defense-in-depth — the value
comes from a parsed URL suffix (effectively booru-controlled) and
the previous code interpolated it raw.

Removes the dead duplicate setText() call that wrote a plain-text
version before being overwritten by the RichText version on the
next line.

Audit-Ref: SECURITY_AUDIT.md finding #6
Severity: Medium
2026-04-11 16:19:17 -05:00
pax
fa4f2cb270 security: fix #6 — add pure source HTML escape helper
Extracts the rich-text Source-line builder out of info_panel.py
into a Qt-free module so it can be unit-tested under CI (which
installs only httpx + Pillow + pytest, no PySide6).

The helper html.escape()s both the href and the visible display
text, and only emits an <a> tag for http(s) URLs — non-URL
sources (including javascript: and data: schemes) get rendered
as escaped plain text without a clickable anchor.

Not yet wired into InfoPanel.set_post; that lands in the next
commit.

Audit-Ref: SECURITY_AUDIT.md finding #6
Severity: Medium
2026-04-11 16:19:06 -05:00
pax
72803f0b14 security: fix #2 — wire hardened mpv options into _MpvGLWidget
Replaces the inline mpv.MPV(...) literal kwargs with a call through
build_mpv_kwargs(), which adds ytdl=no, load_scripts=no, a lavf
protocol whitelist (file,http,https,tls,tcp), and POSIX input_conf
lockdown. Closes the yt-dlp delegation surface (CVE-prone extractors
invoked on attacker-supplied URLs) and the concat:/subfile: local-
file-read gadget via ffmpeg's lavf demuxer.

behavior change from v0.2.5: any file_url whose host is only
handled by yt-dlp (youtube.com, reddit.com, etc.) will no longer
play. Boorus do not legitimately return such URLs, so in practice
this only affects hostile responses. Cached local files and direct
https .mp4/.webm/.mkv continue to work.

Manually smoke tested: played a cached local .mp4 from the library
(file: protocol) and a fresh network .webm from a danbooru search
(https: protocol) — both work.

Audit-Ref: SECURITY_AUDIT.md finding #2
Severity: High
2026-04-11 16:07:33 -05:00
pax
22744c48af security: fix #2 — add pure mpv options builder helper
Extracts the mpv.MPV() kwargs into a Qt-free pure function so the
security-relevant options can be unit-tested on CI (which lacks
PySide6 and libmpv). The builder embeds the audit #2 hardening —
ytdl="no", load_scripts="no", and a lavf protocol whitelist of
file,http,https,tls,tcp — alongside the existing playback tuning.
Not yet wired into _MpvGLWidget; that lands in the next commit.

Audit-Ref: SECURITY_AUDIT.md finding #2
Severity: High
2026-04-11 16:06:33 -05:00
pax
10c2dcb8aa fix popout menu flash on wrong monitor and preview unsave button
- preview_pane: unsave button now checks self._is_saved instead of
  self._save_btn.text() == "Unsave", which stopped matching after the
  button text became a Unicode icon (✕ / ⤓)
- popout: new _exec_menu_at_button helper uses menu.popup() +
  QEventLoop blocked on aboutToHide instead of menu.exec(globalPos).
  On Hyprland the popout gets moved via hyprctl after Qt maps it and
  Qt's window-position tracking stays stale, so exec(btn.mapToGlobal)
  resolved to a global point on the wrong monitor, flashing the menu
  there before the compositor corrected it. popup() routes through a
  different positioning path that anchors correctly.
2026-04-10 22:10:27 -05:00
pax
35135c9a5b video controls: 1x icon, responsive layout, EOF replay, autoplay icon fix
- Render "Once" loop icon as bold "1×" text via QPainter drawText
  instead of the hand-drawn line art
- Responsive controls bar: hide volume slider below 320px, duration
  label below 240px, current time label below 200px
- _toggle_play seeks to 0 if paused at EOF so pressing play replays
  the video in Once mode instead of doing nothing
- Fix stray "Auto" text leaking through the autoplay icon — the
  autoplay property setter was still calling setText
2026-04-10 21:09:49 -05:00
pax
fa9fcc3db0 rubber band from cell padding with 30px drag threshold
- ThumbnailWidget detects clicks outside the pixmap and calls
  grid.on_padding_click() via parent walk (signals + event filters
  both failed on Wayland/QScrollArea)
- Grid tracks a pending rubber band origin; only activates past 30px
  manhattan distance so small clicks deselect cleanly
- Move/release events forwarded from ThumbnailWidget to grid for both
  the pending-drag check and the active rubber band drag
- Fixed mapFrom/mapTo direction (mapFrom's first arg must be a parent)
2026-04-10 20:54:37 -05:00
pax
c440065513 install event filter on each ThumbnailWidget for reliable padding detection 2026-04-10 20:36:54 -05:00
pax
00b8e352ea use viewport event filter for cell padding detection instead of signals 2026-04-10 20:34:36 -05:00
pax
c8b21305ba fix padding click: pass no args through signal, just deselect 2026-04-10 20:31:56 -05:00
pax
9081208170 cell padding clicks deselect via signal instead of broken event propagation 2026-04-10 20:27:54 -05:00
pax
b541f64374 fix cell padding hit-test: use mapFrom instead of broken mapToGlobal on Wayland 2026-04-10 20:25:00 -05:00
pax
9c42b4fdd7 fix coordinate mapping for cell padding hit-test in grid 2026-04-10 20:23:36 -05:00
pax
a1ea2b8727 remove dead enterEvent, reset cursor in leaveEvent 2026-04-10 20:22:17 -05:00
pax
4ba9990f3a pixmap-aware double-click and dynamic cursor on hover 2026-04-10 20:21:58 -05:00
pax
868b1a7708 cell padding starts rubber band and deselects, not just flow gaps 2026-04-10 20:20:23 -05:00
pax
09fadcf3c2 hover only when cursor is over the pixmap, not cell padding 2026-04-10 20:18:49 -05:00