• v0.2.5 5bf85f223b

    v0.2.5 Stable

    pax released this 2026-04-11 02:25:40 +00:00 | 158 commits to main since this release

    v0.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.py went 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 at docs/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: BooruApp via constructor, accesses app internals as trusted collaborator via self._app. No mixins, no ABC, no dependency injection — just plain classes with one reference each. TYPE_CHECKING import for BooruApp avoids circular imports at runtime.
    • Cleaned up 14 dead imports from main_window.py.
    • The _fullscreen_window reference (52 sites across the codebase) was fully consolidated into PopoutController.window. No file outside popout_controller.py touches _fullscreen_window directly 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_tags rating/score/media filter mapping per API type, filter_posts blacklist/dedup/seen-ids interaction, should_backfill termination conditions.
    • tests/gui/test_window_state.py (16 tests): parse_geometry / format_geometry round-trip, parse_splitter_sizes validation edge cases, build_hyprctl_restore_cmds for every floating/tiled permutation including the no_anim priming path.
    • tests/gui/test_media_controller.py (9 tests): compute_prefetch_order for Nearby (cardinals) and Aggressive (ring expansion) modes, including bounds, cap, and dedup invariants.
    • tests/gui/test_post_actions.py (10 tests): is_batch_message progress-pattern detection, is_in_library path-containment check.
    • tests/gui/test_popout_controller.py (3 tests): build_video_sync_dict shape.
    • 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, and pytest.

    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's buttonText color so they match any theme automatically, with 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_bar named 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.Ignored horizontally 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's setGeometry fallback handles the same math.

    • Viewport.center_x/center_y repurposed as anchor point coordinates — in center mode it's the window center, in corner modes it's the pinned corner. New anchor_point() helper in viewport.py extracts the right point from a window rect based on mode.
    • _compute_window_rect branches 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 reserved from hyprctl monitors -j so window positioning respects Waybar's exclusive zone (Qt's screen.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 QPropertyAnimation on a thumbOpacity Qt Property applied in paintEvent. 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

    • B or F — 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_requested signal that routes to a shared PostActionsController.toggle_save_from_preview so 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.mousePressEvent to the grid — Qt event propagation through QScrollArea swallows 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_pixmap hit-test as clicks. Cursor swaps between pointing-hand (over pixmap) and arrow (over padding) via mouseMoveEvent tracking.
    • 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: bold so the icon symbols read well against the translucent-black overlay.
    • themes/README.md documents 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