-
v0.2.5 Stable
released this
2026-04-11 02:25:40 +00:00 | 158 commits to main since this releasev0.2.5
Full UI overhaul (icon buttons, compact top bar, responsive video controls), popout resize-pivot anchor, layout flip, and the main_window.py controller decomposition.
Changes since v0.2.4
Refactor: main_window.py controller decomposition
main_window.pywent from a 3,318-line god-class to a 1,164-line coordinator plus 7 controller modules. Every other subsystem in the codebase had already been decomposed (popout state machine, library save, category fetcher) — BooruApp was the last monolith. 11 commits, pure refactor, no behavior change. Design doc atdocs/MAIN_WINDOW_REFACTOR.md.- New
gui/window_state.py(293 lines) — geometry persistence, Hyprland IPC, splitter savers. - New
gui/privacy.py(66 lines) — privacy overlay toggle + popout coordination. - New
gui/search_controller.py(572 lines) — search orchestration, infinite scroll, backfill, blacklist filtering, tag building, autocomplete, thumbnail fetching. - New
gui/media_controller.py(273 lines) — image/video loading, prefetch, download progress, video streaming fast-path, cache eviction. - New
gui/popout_controller.py(204 lines) — popout lifecycle (open/close), state sync, geometry persistence, navigation delegation. - New
gui/post_actions.py(561 lines) — bookmarks, save/library, batch download, unsave, bulk ops, blacklist actions from popout. - New
gui/context_menus.py(246 lines) — single-post and multi-select context menu building + dispatch. - Controller-pattern: each takes
app: BooruAppvia constructor, accesses app internals as trusted collaborator viaself._app. No mixins, no ABC, no dependency injection — just plain classes with one reference each.TYPE_CHECKINGimport forBooruAppavoids circular imports at runtime. - Cleaned up 14 dead imports from
main_window.py. - The
_fullscreen_windowreference (52 sites across the codebase) was fully consolidated intoPopoutController.window. No file outsidepopout_controller.pytouches_fullscreen_windowdirectly anymore.
New: Phase 2 test suite (64 tests for extracted pure functions)
Each controller extraction also pulled decision-making code out into standalone module-level functions that take plain data in and return plain data out. Controllers call those functions; tests import them directly. Same structural forcing function as the popout state machine tests — the test files fail to collect if anyone adds a Qt import to a tested module.
tests/gui/test_search_controller.py(24 tests):build_search_tagsrating/score/media filter mapping per API type,filter_postsblacklist/dedup/seen-ids interaction,should_backfilltermination conditions.tests/gui/test_window_state.py(16 tests):parse_geometry/format_geometryround-trip,parse_splitter_sizesvalidation edge cases,build_hyprctl_restore_cmdsfor every floating/tiled permutation including the no_anim priming path.tests/gui/test_media_controller.py(9 tests):compute_prefetch_orderfor Nearby (cardinals) and Aggressive (ring expansion) modes, including bounds, cap, and dedup invariants.tests/gui/test_post_actions.py(10 tests):is_batch_messageprogress-pattern detection,is_in_librarypath-containment check.tests/gui/test_popout_controller.py(3 tests):build_video_sync_dictshape.- Total suite: 186 tests (57 core + 65 popout state machine + 64 new controller pure functions), ~0.3s runtime, all import-pure.
- PySide6 imports in controller modules were made lazy (inside method bodies) so the Phase 2 tests can collect on CI, which only installs
httpx,Pillow, andpytest.
UI overhaul: icon buttons and responsive layout
Toolbar and video controls moved from fixed-width text buttons to 24x24 icon buttons. Preview toolbar uses Unicode symbols (☆/★ bookmark, ↓/✕ save, ⊘ blacklist tag, ⊗ blacklist post, ⧉ popout) — both the embedded preview and the popout toolbar share the same object names (
#_tb_bookmark,#_tb_save,#_tb_bl_tag,#_tb_bl_post,#_tb_popout) so one QSS rule styles both. Video controls (play/pause, mute, loop, autoplay) render via QPainter using the palette'sbuttonTextcolor so they match any theme automatically, with1×as bold text for the Once loop state.- Responsive video controls bar: hides volume slider below 320px, duration label below 240px, current time label below 200px. Play/pause/seek/mute/loop always visible.
- Compact top bar: combos use
AdjustToContents, 3px spacing, top/nav bars wrapped in#_top_bar/#_nav_barnamed containers for theme targeting. - Main window minimum size dropped from 900x600 to 740x400 — the hard floor was blocking Hyprland's keyboard resize mode on narrow floating windows.
- Preview pane minimum width dropped from 380 to 200.
- Info panel title + details use
QSizePolicy.Ignoredhorizontally so long source URLs wrap within the splitter instead of pushing it wider.
New: popout anchor setting (resize pivot)
Combo in Settings > General. Controls which point of the popout window stays fixed across navigations as the aspect ratio changes:
Center(default, pins window center), or one of the four corners (pins that corner, window grows/shrinks from the opposite corner). The user can still drag the window anywhere — the anchor only controls the resize direction, not the screen position. Works on all platforms; on Hyprland the hyprctl dispatch path is used, elsewhere Qt'ssetGeometryfallback handles the same math.Viewport.center_x/center_yrepurposed as anchor point coordinates — in center mode it's the window center, in corner modes it's the pinned corner. Newanchor_point()helper inviewport.pyextracts the right point from a window rect based on mode._compute_window_rectbranches on anchor: center mode keeps the existing symmetric math, corner modes derive position from the anchor point + the new size.- Hyprland monitor reserved-area handling: reads
reservedfromhyprctl monitors -jso window positioning respects Waybar's exclusive zone (Qt'sscreen.availableGeometry()doesn't see layer-shell reservations on Wayland).
New: layout flip setting
Checkbox in Settings > General (restart required). Swaps the main splitter — preview+info panel on the left, grid on the right. Useful for left-handed workflows or multi-monitor setups where you want the preview closer to your other reference windows.
New: thumbnail fade-in animation
Thumbnails animate from 0 to 1 opacity over 200ms (OutCubic easing) as they load. Uses a
QPropertyAnimationon athumbOpacityQt Property applied inpaintEvent. The animation is stored on the widget instance to prevent Python garbage collection before the Qt event loop runs it.New: B / F / S keyboard shortcuts
BorF— toggle bookmark on the selected post (works in main grid and popout).S— toggle save to library (Unfiled). If already saved, unsaves. Works in main grid and popout.- The popout gained a new
toggle_save_requestedsignal that routes to a sharedPostActionsController.toggle_save_from_previewso both paths use the same toggle logic.
UX: grid click behavior
- Clicking empty grid space (blue area around thumbnails, cell padding outside the pixmap, or the 2px gaps between cells) deselects everything. Cell padding clicks work via a direct parent-walk from
ThumbnailWidget.mousePressEventto the grid — Qt event propagation throughQScrollAreaswallows events too aggressively to rely on. - Rubber band drag selection now works from any empty space — not just the 2px gaps. 30px manhattan threshold gates activation so single clicks on padding just deselect without flashing a zero-size rubber band.
- Hover highlight only appears when the cursor is actually over the pixmap, not the cell padding. Uses the same
_hit_pixmaphit-test as clicks. Cursor swaps between pointing-hand (over pixmap) and arrow (over padding) viamouseMoveEventtracking. - Clicking an already-showing post no longer restarts the video (fixes the click-to-drag case where the drag-start click was restarting mpv).
- Escape clears the grid selection.
- Stuck forbidden cursor after cancelled drag-and-drop is reset on mouse release. Stuck hover states on Wayland fast-exits are force-cleared in
ThumbnailGrid.leaveEvent.
Themes
All 12 bundled QSS themes were trimmed and regenerated:
- Removed 12 dead selector groups that the app never instantiates:
QRadioButton,QToolButton,QToolBar,QDockWidget,QTreeView/QTreeWidget,QTableView/QTableWidget,QHeaderView,QDoubleSpinBox,QPlainTextEdit,QFrame. - Popout overlay buttons now use
font-size: 15px; font-weight: boldso the icon symbols read well against the translucent-black overlay. themes/README.mddocuments the new#_tb_*toolbar button object names and the popout overlay styling. Removed the old Nerd Font remapping note — QSS can't change button text, so that claim was incorrect.
Downloads
- New