Three fixes that all surfaced from the bookmark/library decoupling
shake-out:
- Popout first-image aspect-lock race: _fit_to_content used to call
_is_hypr_floating which returned None for both "not Hyprland" and
"Hyprland but the window isn't visible to hyprctl yet". The latter
happens on the very first popout open because the wm:openWindow
event hasn't been processed when set_media fires. The method then
fell through to a plain Qt resize and skipped the
keep_aspect_ratio setprop, so the first image always opened
unlocked and only subsequent navigations got the right shape. Now
we inline the env-var check, distinguish the two None cases, and
retry on Hyprland with a 40ms backoff (capped at 5 attempts /
200ms total) when the window isn't registered yet.
- Image fill in popout (and embedded preview): ImageViewer._fit_to_view
used min(scale_w, scale_h, 1.0) which clamped the zoom at native
pixel size, so a smaller image in a larger window centered with
letterbox space around it. Dropped the 1.0 cap so images scale up
to fill the available view, matching how the video player fills
its widget. Combined with the popout's keep_aspect_ratio, the
window matches the image's aspect AND the image fills it cleanly.
Tiled popouts with mismatched aspect still letterbox (intentional —
the layout owns the window shape).
- Combo + button padding tightening across all 12 bundled themes
and Library sort combo: QPushButton padding 2px 8px → 2px 6px,
QComboBox padding 2px 6px → 2px 4px, QComboBox::drop-down width
18px → 14px. Saves 8px non-text width per combo and 4px per
button, so the new "Post ID" sort entry fits in 75px instead of
needing 90. Library sort combo bumped from "Name" (lexicographic)
to "Post ID" with a numeric stem sort that handles non-digit
stems gracefully.
610 lines
14 KiB
CSS
610 lines
14 KiB
CSS
/* booru-viewer — Solarized Dark
|
|
*
|
|
* Edit the @palette block below to recolor this square variant. The body uses
|
|
* ${...} placeholders that the app's _load_user_qss preprocessor
|
|
* substitutes at load time. See themes/README.md for the full list of
|
|
* placeholder names and what each one is used for.
|
|
*
|
|
* The same dialect works in any custom.qss you put in your data dir.
|
|
*/
|
|
|
|
/* @palette
|
|
bg: #002b36
|
|
bg_alt: #001f27
|
|
bg_subtle: #073642
|
|
bg_hover: #0d4654
|
|
bg_active: #586e75
|
|
text: #93a1a1
|
|
text_dim: #839496
|
|
text_disabled: #586e75
|
|
border: #073642
|
|
border_strong: #0d4654
|
|
accent: #268bd2
|
|
accent_text: #002b36
|
|
accent_dim: #2aa198
|
|
link: #2aa198
|
|
danger: #dc322f
|
|
success: #859900
|
|
warning: #b58900
|
|
overlay_bg: rgba(0, 43, 54, 200)
|
|
*/
|
|
|
|
/* ---------- Base ---------- */
|
|
|
|
QWidget {
|
|
background-color: ${bg};
|
|
color: ${text};
|
|
font-size: 13px;
|
|
selection-background-color: ${accent};
|
|
selection-color: ${accent_text};
|
|
}
|
|
|
|
QWidget:disabled {
|
|
color: ${text_disabled};
|
|
}
|
|
|
|
/* Labels should never paint an opaque background — they sit on top of
|
|
* other widgets in many places (toolbars, info panels, overlays). */
|
|
QLabel {
|
|
background: transparent;
|
|
}
|
|
|
|
QMainWindow, QDialog {
|
|
background-color: ${bg};
|
|
}
|
|
|
|
/* ---------- Buttons ---------- */
|
|
|
|
QPushButton {
|
|
background-color: ${bg_subtle};
|
|
color: ${text};
|
|
border: 1px solid ${border_strong};
|
|
padding: 2px 6px;
|
|
min-height: 17px;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: ${bg_hover};
|
|
border-color: ${accent};
|
|
}
|
|
QPushButton:pressed {
|
|
background-color: ${bg_active};
|
|
}
|
|
QPushButton:checked {
|
|
background-color: ${accent};
|
|
color: ${accent_text};
|
|
border-color: ${accent};
|
|
}
|
|
QPushButton:checked:hover {
|
|
background-color: ${accent_dim};
|
|
border-color: ${accent_dim};
|
|
}
|
|
QPushButton:disabled {
|
|
background-color: ${bg_alt};
|
|
color: ${text_disabled};
|
|
border-color: ${border};
|
|
}
|
|
QPushButton:flat {
|
|
background: transparent;
|
|
border: none;
|
|
}
|
|
QPushButton:flat:hover {
|
|
background-color: ${bg_hover};
|
|
}
|
|
|
|
QToolButton {
|
|
background-color: transparent;
|
|
color: ${text};
|
|
border: 1px solid transparent;
|
|
padding: 4px;
|
|
}
|
|
QToolButton:hover {
|
|
background-color: ${bg_hover};
|
|
border-color: ${border_strong};
|
|
}
|
|
QToolButton:pressed, QToolButton:checked {
|
|
background-color: ${bg_active};
|
|
}
|
|
|
|
/* ---------- Inputs ---------- */
|
|
|
|
QLineEdit, QSpinBox, QDoubleSpinBox, QTextEdit, QPlainTextEdit {
|
|
background-color: ${bg_subtle};
|
|
color: ${text};
|
|
border: 1px solid ${border_strong};
|
|
padding: 2px 6px;
|
|
/* min-height ensures the painted text fits inside the widget bounds
|
|
* even when a parent layout (e.g. QFormLayout inside a QGroupBox)
|
|
* compresses the natural sizeHint. Without this, spinboxes in dense
|
|
* forms render with the top of the value text clipped. */
|
|
min-height: 16px;
|
|
selection-background-color: ${accent};
|
|
selection-color: ${accent_text};
|
|
}
|
|
QLineEdit:focus,
|
|
QSpinBox:focus,
|
|
QDoubleSpinBox:focus,
|
|
QTextEdit:focus,
|
|
QPlainTextEdit:focus {
|
|
border-color: ${accent};
|
|
}
|
|
QLineEdit:disabled,
|
|
QSpinBox:disabled,
|
|
QDoubleSpinBox:disabled,
|
|
QTextEdit:disabled,
|
|
QPlainTextEdit:disabled {
|
|
background-color: ${bg_alt};
|
|
color: ${text_disabled};
|
|
border-color: ${border};
|
|
}
|
|
|
|
QComboBox {
|
|
background-color: ${bg_subtle};
|
|
color: ${text};
|
|
border: 1px solid ${border_strong};
|
|
padding: 2px 4px;
|
|
min-height: 16px;
|
|
}
|
|
QComboBox:hover {
|
|
border-color: ${accent};
|
|
}
|
|
QComboBox:focus {
|
|
border-color: ${accent};
|
|
}
|
|
QComboBox::drop-down {
|
|
border: none;
|
|
width: 14px;
|
|
}
|
|
QComboBox QAbstractItemView {
|
|
background-color: ${bg_subtle};
|
|
color: ${text};
|
|
border: 1px solid ${border_strong};
|
|
selection-background-color: ${accent};
|
|
selection-color: ${accent_text};
|
|
outline: none;
|
|
padding: 2px;
|
|
}
|
|
|
|
/* ---------- Scrollbars ---------- */
|
|
|
|
QScrollBar:vertical {
|
|
background: ${bg};
|
|
width: 10px;
|
|
border: none;
|
|
margin: 0;
|
|
}
|
|
QScrollBar::handle:vertical {
|
|
background: ${bg_hover};
|
|
min-height: 24px;
|
|
margin: 1px;
|
|
}
|
|
QScrollBar::handle:vertical:hover {
|
|
background: ${bg_active};
|
|
}
|
|
QScrollBar::add-line:vertical,
|
|
QScrollBar::sub-line:vertical {
|
|
height: 0;
|
|
border: none;
|
|
}
|
|
QScrollBar::add-page:vertical,
|
|
QScrollBar::sub-page:vertical {
|
|
background: transparent;
|
|
}
|
|
|
|
QScrollBar:horizontal {
|
|
background: ${bg};
|
|
height: 10px;
|
|
border: none;
|
|
margin: 0;
|
|
}
|
|
QScrollBar::handle:horizontal {
|
|
background: ${bg_hover};
|
|
min-width: 24px;
|
|
margin: 1px;
|
|
}
|
|
QScrollBar::handle:horizontal:hover {
|
|
background: ${bg_active};
|
|
}
|
|
QScrollBar::add-line:horizontal,
|
|
QScrollBar::sub-line:horizontal {
|
|
width: 0;
|
|
border: none;
|
|
}
|
|
QScrollBar::add-page:horizontal,
|
|
QScrollBar::sub-page:horizontal {
|
|
background: transparent;
|
|
}
|
|
|
|
QScrollArea {
|
|
background: transparent;
|
|
border: none;
|
|
}
|
|
|
|
/* ---------- Menus ---------- */
|
|
|
|
QMenuBar {
|
|
background-color: ${bg};
|
|
color: ${text};
|
|
border-bottom: 1px solid ${border};
|
|
}
|
|
QMenuBar::item {
|
|
background: transparent;
|
|
padding: 4px 10px;
|
|
}
|
|
QMenuBar::item:selected {
|
|
background-color: ${bg_hover};
|
|
color: ${text};
|
|
}
|
|
QMenuBar::item:pressed {
|
|
background-color: ${bg_active};
|
|
}
|
|
|
|
QMenu {
|
|
background-color: ${bg_subtle};
|
|
color: ${text};
|
|
border: 1px solid ${border_strong};
|
|
padding: 4px 0;
|
|
}
|
|
QMenu::item {
|
|
background: transparent;
|
|
padding: 5px 24px 5px 24px;
|
|
}
|
|
QMenu::item:selected {
|
|
background-color: ${accent};
|
|
color: ${accent_text};
|
|
}
|
|
QMenu::item:disabled {
|
|
color: ${text_disabled};
|
|
}
|
|
QMenu::separator {
|
|
height: 1px;
|
|
background: ${border};
|
|
margin: 4px 8px;
|
|
}
|
|
QMenu::icon {
|
|
padding-left: 6px;
|
|
}
|
|
|
|
/* ---------- Status bar ---------- */
|
|
|
|
QStatusBar {
|
|
background-color: ${bg};
|
|
color: ${text_dim};
|
|
border-top: 1px solid ${border};
|
|
}
|
|
QStatusBar::item {
|
|
border: none;
|
|
}
|
|
|
|
/* ---------- Splitters ---------- */
|
|
|
|
QSplitter::handle {
|
|
background: ${border};
|
|
}
|
|
QSplitter::handle:horizontal {
|
|
width: 2px;
|
|
}
|
|
QSplitter::handle:vertical {
|
|
height: 2px;
|
|
}
|
|
QSplitter::handle:hover {
|
|
background: ${accent};
|
|
}
|
|
|
|
/* ---------- Sliders ---------- */
|
|
|
|
QSlider::groove:horizontal {
|
|
background: ${bg_subtle};
|
|
height: 4px;
|
|
}
|
|
QSlider::sub-page:horizontal {
|
|
background: ${accent};
|
|
}
|
|
QSlider::handle:horizontal {
|
|
background: ${accent};
|
|
width: 12px;
|
|
height: 12px;
|
|
margin: -5px 0;
|
|
}
|
|
QSlider::handle:horizontal:hover {
|
|
background: ${accent_dim};
|
|
}
|
|
|
|
QSlider::groove:vertical {
|
|
background: ${bg_subtle};
|
|
width: 4px;
|
|
}
|
|
QSlider::handle:vertical {
|
|
background: ${accent};
|
|
width: 12px;
|
|
height: 12px;
|
|
margin: 0 -5px;
|
|
}
|
|
|
|
/* ---------- Progress ---------- */
|
|
|
|
QProgressBar {
|
|
background-color: ${bg_subtle};
|
|
color: ${text};
|
|
border: 1px solid ${border};
|
|
text-align: center;
|
|
height: 6px;
|
|
}
|
|
QProgressBar::chunk {
|
|
background-color: ${accent};
|
|
}
|
|
|
|
/* ---------- Checkboxes & radio buttons ---------- */
|
|
|
|
QCheckBox, QRadioButton {
|
|
background: transparent;
|
|
color: ${text};
|
|
spacing: 6px;
|
|
}
|
|
QCheckBox::indicator, QRadioButton::indicator {
|
|
width: 14px;
|
|
height: 14px;
|
|
background-color: ${bg_subtle};
|
|
border: 1px solid ${border_strong};
|
|
}
|
|
QCheckBox::indicator {
|
|
}
|
|
QRadioButton::indicator {
|
|
border-radius: 7px;
|
|
}
|
|
QCheckBox::indicator:hover, QRadioButton::indicator:hover {
|
|
border-color: ${accent};
|
|
}
|
|
QCheckBox::indicator:checked, QRadioButton::indicator:checked {
|
|
background-color: ${accent};
|
|
border-color: ${accent};
|
|
}
|
|
QCheckBox::indicator:disabled, QRadioButton::indicator:disabled {
|
|
background-color: ${bg_alt};
|
|
border-color: ${border};
|
|
}
|
|
|
|
/* ---------- Tooltips ---------- */
|
|
|
|
QToolTip {
|
|
background-color: ${bg_subtle};
|
|
color: ${text};
|
|
border: 1px solid ${border_strong};
|
|
padding: 4px 6px;
|
|
}
|
|
|
|
/* ---------- Item views (lists, trees, tables) ---------- */
|
|
|
|
QListView, QListWidget, QTreeView, QTreeWidget, QTableView, QTableWidget {
|
|
background-color: ${bg};
|
|
alternate-background-color: ${bg_alt};
|
|
color: ${text};
|
|
border: 1px solid ${border};
|
|
selection-background-color: ${accent};
|
|
selection-color: ${accent_text};
|
|
outline: none;
|
|
}
|
|
QListView::item, QListWidget::item,
|
|
QTreeView::item, QTreeWidget::item,
|
|
QTableView::item, QTableWidget::item {
|
|
padding: 4px;
|
|
}
|
|
QListView::item:hover, QListWidget::item:hover,
|
|
QTreeView::item:hover, QTreeWidget::item:hover,
|
|
QTableView::item:hover, QTableWidget::item:hover {
|
|
background-color: ${bg_hover};
|
|
}
|
|
QListView::item:selected, QListWidget::item:selected,
|
|
QTreeView::item:selected, QTreeWidget::item:selected,
|
|
QTableView::item:selected, QTableWidget::item:selected {
|
|
background-color: ${accent};
|
|
color: ${accent_text};
|
|
}
|
|
|
|
QHeaderView::section {
|
|
background-color: ${bg_subtle};
|
|
color: ${text};
|
|
border: none;
|
|
border-right: 1px solid ${border};
|
|
padding: 4px 8px;
|
|
}
|
|
QHeaderView::section:hover {
|
|
background-color: ${bg_hover};
|
|
}
|
|
|
|
/* ---------- Tabs ---------- */
|
|
|
|
QTabWidget::pane {
|
|
border: 1px solid ${border};
|
|
top: -1px;
|
|
}
|
|
QTabBar::tab {
|
|
background: ${bg_subtle};
|
|
color: ${text_dim};
|
|
border: 1px solid ${border};
|
|
border-bottom: none;
|
|
padding: 6px 14px;
|
|
border-top-left-radius: 4px;
|
|
border-top-right-radius: 4px;
|
|
}
|
|
QTabBar::tab:selected {
|
|
background: ${bg};
|
|
color: ${text};
|
|
border-color: ${border_strong};
|
|
}
|
|
QTabBar::tab:hover:!selected {
|
|
background: ${bg_hover};
|
|
color: ${text};
|
|
}
|
|
|
|
/* ---------- Group boxes ---------- */
|
|
|
|
QGroupBox {
|
|
background: transparent;
|
|
color: ${text};
|
|
border: 1px solid ${border};
|
|
margin-top: 10px;
|
|
padding-top: 8px;
|
|
}
|
|
QGroupBox::title {
|
|
subcontrol-origin: margin;
|
|
subcontrol-position: top left;
|
|
padding: 0 6px;
|
|
color: ${text_dim};
|
|
}
|
|
|
|
/* ---------- Frames ---------- */
|
|
|
|
QFrame[frameShape="4"], /* HLine */
|
|
QFrame[frameShape="5"] /* VLine */ {
|
|
background: ${border};
|
|
color: ${border};
|
|
}
|
|
|
|
/* ---------- Toolbars ---------- */
|
|
|
|
QToolBar {
|
|
background: ${bg};
|
|
border: none;
|
|
spacing: 4px;
|
|
padding: 2px;
|
|
}
|
|
QToolBar::separator {
|
|
background: ${border};
|
|
width: 1px;
|
|
margin: 4px 4px;
|
|
}
|
|
|
|
/* ---------- Dock widgets ---------- */
|
|
|
|
QDockWidget {
|
|
color: ${text};
|
|
titlebar-close-icon: none;
|
|
}
|
|
QDockWidget::title {
|
|
background: ${bg_subtle};
|
|
padding: 4px;
|
|
border: 1px solid ${border};
|
|
}
|
|
|
|
/* ---------- Rubber band (multi-select drag rectangle) ---------- */
|
|
|
|
QRubberBand {
|
|
background: ${accent};
|
|
border: 1px solid ${accent};
|
|
/* Qt blends rubber band at ~30% so this reads as a translucent
|
|
* accent-tinted rectangle without needing rgba(). */
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ---------- Library count label states ---------- */
|
|
/*
|
|
* The library tab's count label switches between three visual states
|
|
* depending on what refresh() found. The state is exposed as a Qt
|
|
* dynamic property `libraryCountState` so users can override these
|
|
* rules in their custom.qss without touching the Python.
|
|
*
|
|
* normal N files — default text color, no rule needed
|
|
* empty no items — dim text (no items found, search miss)
|
|
* error bad/unreachable — danger color + bold (real error)
|
|
*/
|
|
|
|
QLabel[libraryCountState="empty"] {
|
|
color: ${text_dim};
|
|
}
|
|
QLabel[libraryCountState="error"] {
|
|
color: ${danger};
|
|
font-weight: bold;
|
|
}
|
|
|
|
/* ---------- Thumbnail dot indicators (Qt properties on ThumbnailWidget) ---------- */
|
|
|
|
ThumbnailWidget {
|
|
qproperty-savedColor: #22cc22; /* green dot: saved to library — universal "confirmed" feel */
|
|
qproperty-bookmarkedColor: #ffcc00; /* yellow star: bookmarked */
|
|
qproperty-selectionColor: ${accent};
|
|
qproperty-multiSelectColor: ${accent_dim};
|
|
qproperty-hoverColor: ${accent};
|
|
qproperty-idleColor: ${border_strong};
|
|
}
|
|
|
|
/* ---------- Info panel tag category colors ---------- */
|
|
|
|
InfoPanel {
|
|
qproperty-tagArtistColor: ${warning};
|
|
qproperty-tagCharacterColor: ${success};
|
|
qproperty-tagCopyrightColor: ${accent};
|
|
qproperty-tagSpeciesColor: ${danger};
|
|
qproperty-tagMetaColor: ${text_dim};
|
|
qproperty-tagLoreColor: ${text_dim};
|
|
}
|
|
|
|
/* ---------- Video player letterbox / pillarbox color (mpv background) ---------- */
|
|
|
|
VideoPlayer {
|
|
qproperty-letterboxColor: ${bg};
|
|
}
|
|
|
|
/* ---------- Popout overlay bars (slideshow toolbar + slideshow controls + embedded preview controls) ---------- */
|
|
/*
|
|
* The popout window's translucent toolbar (top) and transport controls
|
|
* (bottom) float over the video content. The bg color comes from the
|
|
* @palette overlay_bg slot. Children get the classic overlay treatment:
|
|
* transparent backgrounds, near-white text, hairline borders.
|
|
*/
|
|
|
|
QWidget#_slideshow_toolbar,
|
|
QWidget#_slideshow_controls,
|
|
QWidget#_preview_controls {
|
|
background: ${overlay_bg};
|
|
}
|
|
|
|
QWidget#_slideshow_toolbar *,
|
|
QWidget#_slideshow_controls *,
|
|
QWidget#_preview_controls * {
|
|
background: transparent;
|
|
color: white;
|
|
border: none;
|
|
}
|
|
|
|
QWidget#_slideshow_toolbar QPushButton,
|
|
QWidget#_slideshow_controls QPushButton,
|
|
QWidget#_preview_controls QPushButton {
|
|
background: transparent;
|
|
color: white;
|
|
border: 1px solid rgba(255, 255, 255, 80);
|
|
padding: 2px 6px;
|
|
}
|
|
QWidget#_slideshow_toolbar QPushButton:hover,
|
|
QWidget#_slideshow_controls QPushButton:hover,
|
|
QWidget#_preview_controls QPushButton:hover {
|
|
background: rgba(255, 255, 255, 30);
|
|
}
|
|
|
|
QWidget#_slideshow_toolbar QSlider::groove:horizontal,
|
|
QWidget#_slideshow_controls QSlider::groove:horizontal,
|
|
QWidget#_preview_controls QSlider::groove:horizontal {
|
|
background: rgba(255, 255, 255, 40);
|
|
height: 4px;
|
|
}
|
|
QWidget#_slideshow_toolbar QSlider::handle:horizontal,
|
|
QWidget#_slideshow_controls QSlider::handle:horizontal,
|
|
QWidget#_preview_controls QSlider::handle:horizontal {
|
|
background: ${accent};
|
|
width: 10px;
|
|
margin: -4px 0;
|
|
}
|
|
QWidget#_slideshow_toolbar QSlider::sub-page:horizontal,
|
|
QWidget#_slideshow_controls QSlider::sub-page:horizontal,
|
|
QWidget#_preview_controls QSlider::sub-page:horizontal {
|
|
background: ${accent};
|
|
}
|
|
|
|
QWidget#_slideshow_toolbar QLabel,
|
|
QWidget#_slideshow_controls QLabel,
|
|
QWidget#_preview_controls QLabel {
|
|
background: transparent;
|
|
color: white;
|
|
}
|