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
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
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.
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
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
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
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
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
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
- 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.
- 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
- 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)
New setting "Record recent searches" (on by default). When disabled,
searches are not recorded and the Recent section is hidden from the
history dropdown. Saved searches are unaffected.
behavior change: opt-in setting, on by default (preserves existing behavior)
New setting "Remove bookmark when saved to library" (off by default).
When enabled, _maybe_unbookmark runs directly in each save callback
after save_post_file succeeds -- handles DB removal, grid dot, preview
state, popout sync, and bookmarks tab refresh. Wired into all 4 save
paths: save_to_library, bulk_save, save_as, batch_download_to.
behavior change: opt-in setting, off by default
New setting "Remove bookmark when saved to library" (off by default).
When enabled, saving a post to the library automatically removes its
bookmark. Handles both single saves (on_bookmark_done) and bulk saves
(on_batch_done). UI toggle in Settings > General.
behavior change: opt-in setting, off by default
CI installs httpx + Pillow + pytest but not PySide6. The Phase C
tests import pure functions from controller modules, which had
top-level PySide6 imports (QTimer, QPixmap, QApplication, QMessageBox).
Move these to lazy imports inside the methods that need them so the
module-level pure functions remain importable without Qt.