Browse Source

bug-fixes

master
Silberengel 2 weeks ago
parent
commit
02bd66e331
  1. 50
      src/imwald/core/author_html.py
  2. 14
      src/imwald/core/nostr_engine.py
  3. 42
      src/imwald/ui/feed_page.py
  4. 83
      src/imwald/ui/main_window.py
  5. 42
      src/imwald/ui/theme.py

50
src/imwald/core/author_html.py

@ -8,6 +8,15 @@ from imwald.core.kind0_profile import display_name_from_profile, display_name_fr
from imwald.core.nip05 import favicon_url_for_domain, parse_nip05_identifier from imwald.core.nip05 import favicon_url_for_domain, parse_nip05_identifier
def _npub_header_lines(npub_bech: str) -> tuple[str, str]:
"""Return (escaped one-line npub for display, escaped full npub for title/tooltip)."""
n = npub_bech.strip()
full_esc = html.escape(n, quote=True)
if len(n) <= 44:
return html.escape(n, quote=False), full_esc
return html.escape(n[:22] + "" + n[-18:], quote=False), full_esc
def safe_http_url(u: str | None) -> str | None: def safe_http_url(u: str | None) -> str | None:
if not u: if not u:
return None return None
@ -58,6 +67,8 @@ def format_nip05_chips_html(
dim: str, dim: str,
ok: str, ok: str,
bad: str, bad: str,
chip_bg: str | None = None,
chip_border: str | None = None,
) -> str: ) -> str:
""" """
Horizontal row of NIP-05 chips: favicon (per domain), identifier, verification mark. Horizontal row of NIP-05 chips: favicon (per domain), identifier, verification mark.
@ -83,15 +94,24 @@ def format_nip05_chips_html(
mark = f'<span style="color:{bad}" title="NIP-05 not verified">✗</span>' mark = f'<span style="color:{bad}" title="NIP-05 not verified">✗</span>'
else: else:
mark = f'<span style="color:{dim}" title="NIP-05 not verified yet">○</span>' mark = f'<span style="color:{dim}" title="NIP-05 not verified yet">○</span>'
if chip_bg:
brd = chip_border or dim
wrap = (
"display:inline-flex;align-items:center;gap:7px;padding:6px 12px;margin:8px 8px 0 0;"
f"border-radius:999px;background:{chip_bg};border:1px solid {brd}"
)
else:
wrap = "display:inline-flex;align-items:center;gap:5px;margin:4px 14px 0 0"
parts.append( parts.append(
f'<span style="display:inline-flex;align-items:center;gap:5px;margin:4px 14px 0 0">' f'<span style="{wrap}">'
f"{icon}" f"{icon}"
f'<span style="color:{muted};font-size:14px">{esc}</span>{mark}' f'<span style="color:{muted};font-size:13px">{esc}</span>{mark}'
f"</span>" f"</span>"
) )
if not parts: if not parts:
return "" return ""
return f'<div style="display:flex;flex-wrap:wrap;align-items:center;margin-top:4px">{"".join(parts)}</div>' mt = "12px" if chip_bg else "4px"
return f'<div style="display:flex;flex-wrap:wrap;align-items:center;margin-top:{mt}">{"".join(parts)}</div>'
def feed_op_author_compact_html( def feed_op_author_compact_html(
@ -103,21 +123,31 @@ def feed_op_author_compact_html(
nip05_chips_html: str, nip05_chips_html: str,
muted: str, muted: str,
border: str, border: str,
text: str,
dim: str,
) -> str: ) -> str:
"""Feed OP header: avatar, npub only, optional NIP-38 status (HTML), NIP-05 chips.""" """Feed OP header: larger avatar, display name, compact npub line, NIP-38 + NIP-05."""
pk_l = pubkey_hex.strip().lower() pk_l = pubkey_hex.strip().lower()
href = html.escape(f"imwald://pub/{pk_l}", quote=True) href = html.escape(f"imwald://pub/{pk_l}", quote=True)
av = avatar_img_or_placeholder(parsed, 52, border_hex=border, profile_href=href) av = avatar_img_or_placeholder(parsed, 64, border_hex=border, profile_href=href)
npub_e = html.escape(npub_bech) name_e = html.escape(display_name_from_profile_or_hex(parsed, pubkey_hex))
npub_line, npub_title = _npub_header_lines(npub_bech)
status_block = "" status_block = ""
if status_inner_html.strip(): if status_inner_html.strip():
status_block = f'<div style="margin-top:4px" class="md">{status_inner_html}</div>' status_block = (
f'<div style="margin-top:12px;padding-top:12px;border-top:1px solid {border}" class="md">'
f"{status_inner_html}</div>"
)
return ( return (
f'<div style="display:flex;align-items:flex-start;margin-bottom:12px;gap:12px">' f'<div style="display:flex;align-items:flex-start;gap:20px;margin:0 0 24px 0;'
f'padding:4px 4px 22px 2px;border-bottom:1px solid {border}">'
f"{av}" f"{av}"
f'<div style="flex:1;min-width:0">' f'<div style="flex:1;min-width:0;padding-top:2px">'
f'<a href="{href}" style="text-decoration:none;color:inherit" title="View profile">' f'<a href="{href}" style="text-decoration:none;color:inherit" title="View profile">'
f'<div style="color:{muted};font-size:15px;font-weight:500;word-break:break-all">{npub_e}</div>' f'<div style="color:{text};font-size:22px;font-weight:700;letter-spacing:-0.03em;line-height:1.2">'
f"{name_e}</div>"
f'<div style="margin-top:10px;font-size:13px;color:{dim};font-family:ui-monospace,Cascadia Mono,Consolas,'
f'monospace;line-height:1.45;word-break:break-all" title="{npub_title}">{npub_line}</div>'
f"</a>" f"</a>"
f"{status_block}" f"{status_block}"
f"{nip05_chips_html}" f"{nip05_chips_html}"

14
src/imwald/core/nostr_engine.py

@ -43,6 +43,7 @@ class NostrEngine(QObject):
event_ingested = Signal(str, object) event_ingested = Signal(str, object)
relay_status = Signal(str) relay_status = Signal(str)
relay_snapshot = Signal(object) relay_snapshot = Signal(object)
relay_connect_ready = Signal(int)
def __init__(self, db: Database) -> None: def __init__(self, db: Database) -> None:
super().__init__() super().__init__()
@ -55,6 +56,7 @@ class NostrEngine(QObject):
self._read_urls_snapshot: list[str] = [] self._read_urls_snapshot: list[str] = []
self._author_meta_pending: set[str] = set() self._author_meta_pending: set[str] = set()
self._author_meta_timer: asyncio.TimerHandle | None = None self._author_meta_timer: asyncio.TimerHandle | None = None
self._relay_connect_cookie = 0
def start_relays( def start_relays(
self, self,
@ -62,14 +64,18 @@ class NostrEngine(QObject):
*, *,
user_write_urls: list[str] | None = None, user_write_urls: list[str] | None = None,
list30000_owner: str | None = None, list30000_owner: str | None = None,
) -> None: ) -> int:
if self._thread and self._thread.is_alive(): if self._thread and self._thread.is_alive():
self.stop_relays() self.stop_relays()
self._relay_connect_cookie += 1
connect_cookie = self._relay_connect_cookie
urls = list(read_urls or default_feed_read_relays()) urls = list(read_urls or default_feed_read_relays())
aggr_writes = set(user_write_urls or list(DEFAULT_WRITE_RELAYS)) aggr_writes = set(user_write_urls or list(DEFAULT_WRITE_RELAYS))
k3000_owner = (list30000_owner or "").strip().lower() k3000_owner = (list30000_owner or "").strip().lower()
def runner() -> None: def runner() -> None:
cookie = connect_cookie
async def on_ev(url: str, ev: dict[str, Any]) -> None: async def on_ev(url: str, ev: dict[str, Any]) -> None:
self.event_ingested.emit(url, ev) self.event_ingested.emit(url, ev)
@ -116,8 +122,13 @@ class NostrEngine(QObject):
"relay worker: %d relay(s) registered, calling connect_all()", "relay worker: %d relay(s) registered, calling connect_all()",
len(mgr.all_relays()), len(mgr.all_relays()),
) )
try:
await mgr.connect_all() await mgr.connect_all()
log.info("relay worker: connect_all() returned (per-relay runners active)") log.info("relay worker: connect_all() returned (per-relay runners active)")
except Exception:
log.exception("relay worker: connect_all() failed")
finally:
self.relay_connect_ready.emit(cookie)
async def relay_pulse() -> None: async def relay_pulse() -> None:
while not app_stop.is_set(): while not app_stop.is_set():
@ -159,6 +170,7 @@ class NostrEngine(QObject):
self._thread = threading.Thread(target=runner, name="nostr-relay", daemon=True) self._thread = threading.Thread(target=runner, name="nostr-relay", daemon=True)
self._thread.start() self._thread.start()
log.info("relay worker thread started (%d read URL(s))", len(urls)) log.info("relay worker thread started (%d read URL(s))", len(urls))
return connect_cookie
def stop_relays(self) -> None: def stop_relays(self) -> None:
loop = self._loop loop = self._loop

42
src/imwald/ui/feed_page.py

@ -6,6 +6,7 @@ import html
import json import json
import re import re
from collections.abc import Sequence from collections.abc import Sequence
from datetime import datetime, timezone
from typing import Any, cast from typing import Any, cast
from PySide6.QtCore import QEvent, QObject, QRunnable, Qt, QThreadPool, QTimer, Signal, QUrl from PySide6.QtCore import QEvent, QObject, QRunnable, Qt, QThreadPool, QTimer, Signal, QUrl
@ -43,7 +44,7 @@ from imwald.core.nostr_engine import NostrEngine
from imwald.core.ranker import Ranker from imwald.core.ranker import Ranker
from imwald.ui.media_viewer_dialog import try_open_media_url_in_app from imwald.ui.media_viewer_dialog import try_open_media_url_in_app
from imwald.ui.note_text_browser import NoteTextBrowser from imwald.ui.note_text_browser import NoteTextBrowser
from imwald.ui.theme import ACCENT, BORDER, FEED_DOC_CSS, TEXT, TEXT_DIM, TEXT_MUTED from imwald.ui.theme import ACCENT, BG_CODE, BORDER, FEED_DOC_CSS, TEXT, TEXT_DIM, TEXT_MUTED
FEED_KINDS = (1, 20, 21, 30023, 9802, 11) FEED_KINDS = (1, 20, 21, 30023, 9802, 11)
@ -426,7 +427,13 @@ class FeedPage(QWidget):
vmap = {a.strip().lower(): b for a, b in nip05_verified} vmap = {a.strip().lower(): b for a, b in nip05_verified}
states = [(c, vmap.get(c.strip().lower(), False)) for c in candidates] states = [(c, vmap.get(c.strip().lower(), False)) for c in candidates]
chips = format_nip05_chips_html( chips = format_nip05_chips_html(
states, muted=TEXT_MUTED, dim=TEXT_DIM, ok=ACCENT, bad="#b86a6a" states,
muted=TEXT_MUTED,
dim=TEXT_DIM,
ok=ACCENT,
bad="#b86a6a",
chip_bg=BG_CODE,
chip_border=BORDER,
) )
st_ev = get_active_user_status_event(self._db, pk) st_ev = get_active_user_status_event(self._db, pk)
st_html = "" st_html = ""
@ -445,12 +452,28 @@ class FeedPage(QWidget):
nip05_chips_html=chips, nip05_chips_html=chips,
muted=TEXT_MUTED, muted=TEXT_MUTED,
border=BORDER, border=BORDER,
text=TEXT,
dim=TEXT_DIM,
) )
tr = "" tr = ""
sr = ev.get("source_relay") or "" sr = ev.get("source_relay") or ""
if sr and "nostrarchives.com" in sr: if sr and "nostrarchives.com" in sr:
tr = f"<div style='color:{TEXT_DIM};font-size:15px;margin:6px 0'><i>Trending slice (nostrarchives)</i></div>" tr = (
eid = html.escape(str(ev["id"])) f"<div style='color:{TEXT_MUTED};font-size:13px;margin:0 0 12px 0;font-style:italic'>"
f"Trending slice (nostrarchives)</div>"
)
try:
cts = int(ev["created_at"])
t_human = datetime.fromtimestamp(cts, tz=timezone.utc).strftime("%Y-%m-%d · %H:%M UTC")
except (TypeError, ValueError, OSError):
t_human = str(ev.get("created_at") or "")
t_human_e = html.escape(t_human)
eid_raw = str(ev["id"])
if len(eid_raw) > 52:
eid_display = html.escape(eid_raw[:22] + "" + eid_raw[-18:])
else:
eid_display = html.escape(eid_raw)
eid_title = html.escape(eid_raw, quote=True)
md_body = markdown_html_fragment( md_body = markdown_html_fragment(
ev.get("content") or "", ev.get("content") or "",
db=self._db, db=self._db,
@ -459,12 +482,15 @@ class FeedPage(QWidget):
) )
return ( return (
"<!DOCTYPE html><html><head><meta charset=\"utf-8\">" "<!DOCTYPE html><html><head><meta charset=\"utf-8\">"
f"{FEED_DOC_CSS}</head><body>" f"{FEED_DOC_CSS}</head><body style=\"padding:16px 22px 28px 22px\">"
f"{author_block}" f"{author_block}"
f"<div style='color:{TEXT_DIM};font-size:15px;margin-bottom:8px'>Kind {int(ev['kind'])} · {int(ev['created_at'])}</div>" f"<div style='color:{TEXT_DIM};font-size:14px;margin:0 0 16px 0;line-height:1.5'>"
f"<span style='color:{TEXT_MUTED};font-size:12px;font-weight:600'>Kind {int(ev['kind'])}</span>"
f"<span style='color:{TEXT_MUTED};margin:0 8px'>·</span>{t_human_e}</div>"
f"{tr}" f"{tr}"
f"<div class=\"md\">{md_body}</div>" f'<div class="md" style="margin-top:4px">{md_body}</div>'
f"<p style='color:{TEXT_DIM};font-size:14px;margin-top:14px'>{eid}</p>" f'<p style="color:{TEXT_DIM};font-size:12px;margin:22px 0 0 0;font-family:ui-monospace,monospace;'
f'line-height:1.4" title="{eid_title}">{eid_display}</p>'
"</body></html>" "</body></html>"
) )

83
src/imwald/ui/main_window.py

@ -7,10 +7,12 @@ from typing import Any, cast
from PySide6.QtCore import QObject, QRunnable, QSize, QThreadPool, Qt, QTimer, Signal from PySide6.QtCore import QObject, QRunnable, QSize, QThreadPool, Qt, QTimer, Signal
from PySide6.QtGui import QAction, QCloseEvent, QColor, QIcon, QPainter, QPen, QPixmap from PySide6.QtGui import QAction, QCloseEvent, QColor, QIcon, QPainter, QPen, QPixmap
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QApplication,
QComboBox, QComboBox,
QDialog, QDialog,
QDialogButtonBox, QDialogButtonBox,
QFormLayout, QFormLayout,
QHBoxLayout,
QInputDialog, QInputDialog,
QLabel, QLabel,
QLineEdit, QLineEdit,
@ -18,6 +20,7 @@ from PySide6.QtWidgets import (
QListWidgetItem, QListWidgetItem,
QMainWindow, QMainWindow,
QMessageBox, QMessageBox,
QProgressBar,
QSpinBox, QSpinBox,
QSplitter, QSplitter,
QStackedWidget, QStackedWidget,
@ -139,6 +142,30 @@ class MainWindow(QMainWindow):
split.setSizes([920, 280]) split.setSizes([920, 280])
self.setCentralWidget(split) self.setCentralWidget(split)
self._busy_depth = 0
self._awaiting_relay_cookie: int | None = None
self._busy_wrap = QWidget()
self._busy_wrap.setObjectName("BusyWrap")
bh = QHBoxLayout(self._busy_wrap)
bh.setContentsMargins(0, 0, 8, 0)
bh.setSpacing(10)
self._busy_label = QLabel("")
self._busy_label.setStyleSheet(f"color: {TEXT_MUTED}; font-size: 13px;")
self._busy_bar = QProgressBar()
self._busy_bar.setObjectName("BusyBar")
self._busy_bar.setRange(0, 0)
self._busy_bar.setTextVisible(False)
self._busy_bar.setFixedSize(132, 10)
self._busy_bar.setMaximumHeight(12)
bh.addWidget(self._busy_label)
bh.addWidget(self._busy_bar)
self._busy_wrap.hide()
self.statusBar().addPermanentWidget(self._busy_wrap)
self._relay_connect_timer = QTimer(self)
self._relay_connect_timer.setSingleShot(True)
self._relay_connect_timer.setInterval(45_000)
self._relay_connect_timer.timeout.connect(self._on_relay_connect_timeout)
self._acct_combo = QComboBox() self._acct_combo = QComboBox()
self._acct_combo.setMinimumWidth(220) self._acct_combo.setMinimumWidth(220)
self._acct_combo.setIconSize(QSize(22, 22)) self._acct_combo.setIconSize(QSize(22, 22))
@ -172,8 +199,12 @@ class MainWindow(QMainWindow):
self._on_account_changed() self._on_account_changed()
if not self._db.get_setting("onboarding_done") and not self._accounts: if not self._db.get_setting("onboarding_done") and not self._accounts:
self.push_busy("Opening wizard…")
try:
if run_onboarding_wizard(self, db=self._db, engine=self._engine, existing_accounts=self._accounts): if run_onboarding_wizard(self, db=self._db, engine=self._engine, existing_accounts=self._accounts):
self._db.set_setting("onboarding_done", "1") self._db.set_setting("onboarding_done", "1")
finally:
self.pop_busy()
self._accounts = load_accounts() self._accounts = load_accounts()
self._reload_account_combo() self._reload_account_combo()
self._notif.set_accounts(self._accounts) self._notif.set_accounts(self._accounts)
@ -283,15 +314,52 @@ class MainWindow(QMainWindow):
self._feed.reload_queue() self._feed.reload_queue()
self._restart_relays() self._restart_relays()
def push_busy(self, message: str = "Working…") -> None:
"""Show the status-bar spinner (supports nested push/pop)."""
self._busy_depth += 1
if self._busy_depth == 1:
self._busy_label.setText(message)
self._busy_wrap.show()
self.setCursor(Qt.CursorShape.WaitCursor)
QApplication.processEvents()
def pop_busy(self) -> None:
if self._busy_depth <= 0:
return
self._busy_depth -= 1
if self._busy_depth == 0:
self._busy_wrap.hide()
self.unsetCursor()
def _on_relay_connect_ready(self, cookie: int) -> None:
if self._awaiting_relay_cookie != cookie:
return
self._awaiting_relay_cookie = None
self._relay_connect_timer.stop()
self.pop_busy()
def _on_relay_connect_timeout(self) -> None:
if self._awaiting_relay_cookie is None:
return
self._awaiting_relay_cookie = None
self.pop_busy()
def _restart_relays(self) -> None: def _restart_relays(self) -> None:
self.push_busy("Connecting to relays…")
pk = self._current_pubkey() pk = self._current_pubkey()
resolved = resolve_for_account(self._db, pk) resolved = resolve_for_account(self._db, pk)
reads = augment_feed_with_trending(resolved.read_urls) reads = augment_feed_with_trending(resolved.read_urls)
self._engine.start_relays(
def _go() -> None:
ck = self._engine.start_relays(
read_urls=reads, read_urls=reads,
user_write_urls=resolved.write_urls, user_write_urls=resolved.write_urls,
list30000_owner=self.list_owner_pubkey_for_relays(), list30000_owner=self.list_owner_pubkey_for_relays(),
) )
self._awaiting_relay_cookie = ck
self._relay_connect_timer.start()
QTimer.singleShot(0, _go)
self._author_bootstrap_timer.start() self._author_bootstrap_timer.start()
def _bootstrap_author_metadata_queue(self) -> None: def _bootstrap_author_metadata_queue(self) -> None:
@ -395,6 +463,7 @@ class MainWindow(QMainWindow):
self._engine.event_ingested.connect(self._on_event_ingested) self._engine.event_ingested.connect(self._on_event_ingested)
self._engine.relay_status.connect(self._relay_status_message) self._engine.relay_status.connect(self._relay_status_message)
self._engine.relay_status.connect(self._relay_panel.log_line.emit) self._engine.relay_status.connect(self._relay_panel.log_line.emit)
self._engine.relay_connect_ready.connect(self._on_relay_connect_ready)
def _relay_status_message(self, s: str) -> None: def _relay_status_message(self, s: str) -> None:
self.statusBar().showMessage(s, 8000) self.statusBar().showMessage(s, 8000)
@ -515,7 +584,11 @@ class MainWindow(QMainWindow):
return return
if QMessageBox.question(self, "NIP-09", f"Publish deletion for {event_id[:16]}…?") != QMessageBox.StandardButton.Yes: if QMessageBox.question(self, "NIP-09", f"Publish deletion for {event_id[:16]}…?") != QMessageBox.StandardButton.Yes:
return return
self.push_busy("Publishing deletion…")
try:
self._engine.publish_nip09_deletion(acc, pw, event_id) self._engine.publish_nip09_deletion(acc, pw, event_id)
finally:
self.pop_busy()
QMessageBox.information(self, "NIP-09", "Deletion request published to your write relays.") QMessageBox.information(self, "NIP-09", "Deletion request published to your write relays.")
def _edit_current(self) -> None: def _edit_current(self) -> None:
@ -581,7 +654,11 @@ class MainWindow(QMainWindow):
def _onboarding_again(self) -> None: def _onboarding_again(self) -> None:
self._accounts = load_accounts() self._accounts = load_accounts()
self.push_busy("Opening wizard…")
try:
run_onboarding_wizard(self, db=self._db, engine=self._engine, existing_accounts=self._accounts) run_onboarding_wizard(self, db=self._db, engine=self._engine, existing_accounts=self._accounts)
finally:
self.pop_busy()
self._accounts = load_accounts() self._accounts = load_accounts()
self._reload_account_combo() self._reload_account_combo()
self._notif.set_accounts(self._accounts) self._notif.set_accounts(self._accounts)
@ -604,6 +681,10 @@ class MainWindow(QMainWindow):
return acc, self._password_for(pk) return acc, self._password_for(pk)
def closeEvent(self, event: QCloseEvent) -> None: # noqa: N802 def closeEvent(self, event: QCloseEvent) -> None: # noqa: N802
self._relay_connect_timer.stop()
self._awaiting_relay_cookie = None
while self._busy_depth > 0:
self.pop_busy()
self._relay_panel.shutdown_logging() self._relay_panel.shutdown_logging()
self._engine.stop_relays() self._engine.stop_relays()
super().closeEvent(event) super().closeEvent(event)

42
src/imwald/ui/theme.py

@ -2,6 +2,8 @@
from __future__ import annotations from __future__ import annotations
import base64
from PySide6.QtWidgets import QApplication from PySide6.QtWidgets import QApplication
from imwald.core.display_constants import IMAGE_DISPLAY_MAX_WIDTH_PX from imwald.core.display_constants import IMAGE_DISPLAY_MAX_WIDTH_PX
@ -19,6 +21,14 @@ BG_CARD = "#151f1a"
BORDER = "#2a3d34" BORDER = "#2a3d34"
BG_CODE = "#0a100d" BG_CODE = "#0a100d"
# Fusion’s default tab-close pixmap is nearly invisible on our dark tabs; use an explicit “×”.
_TAB_CLOSE_ICON_B64 = base64.standard_b64encode(
b'<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14">'
b'<path d="M3 3l8 8M11 3l-8 8" stroke="#c5e0d4" stroke-width="2" stroke-linecap="round" fill="none"/>'
b"</svg>"
).decode("ascii")
_TAB_CLOSE_ICON_URL = f'url("data:image/svg+xml;base64,{_TAB_CLOSE_ICON_B64}")'
_W = IMAGE_DISPLAY_MAX_WIDTH_PX _W = IMAGE_DISPLAY_MAX_WIDTH_PX
FEED_DOC_CSS = f""" FEED_DOC_CSS = f"""
<style> <style>
@ -63,6 +73,16 @@ QStatusBar {{
color: {TEXT_MUTED}; color: {TEXT_MUTED};
border-top: 1px solid {BORDER}; border-top: 1px solid {BORDER};
}} }}
QProgressBar#BusyBar {{
background-color: {BG_WINDOW};
border: 1px solid {BORDER};
border-radius: 5px;
padding: 1px;
}}
QProgressBar#BusyBar::chunk {{
background-color: {ACCENT_SOFT};
border-radius: 3px;
}}
QPushButton {{ QPushButton {{
background-color: {BG_CARD}; background-color: {BG_CARD};
color: {TEXT}; color: {TEXT};
@ -127,7 +147,7 @@ QTabBar::tab {{
color: {TEXT_DIM}; color: {TEXT_DIM};
font-size: 15px; font-size: 15px;
min-height: 22px; min-height: 22px;
padding: 10px 22px 11px 20px; padding: 10px 36px 11px 18px;
margin: 8px 5px 0 0; margin: 8px 5px 0 0;
border: 1px solid {BORDER}; border: 1px solid {BORDER};
border-bottom: none; border-bottom: none;
@ -138,7 +158,7 @@ QTabBar::tab:selected {{
background-color: {BG_FIELD}; background-color: {BG_FIELD};
color: {TEXT}; color: {TEXT};
font-weight: 600; font-weight: 600;
padding-bottom: 12px; padding: 10px 36px 12px 18px;
margin-bottom: -1px; margin-bottom: -1px;
border-color: {BORDER}; border-color: {BORDER};
border-bottom-color: {BG_FIELD}; border-bottom-color: {BG_FIELD};
@ -150,18 +170,22 @@ QTabBar::tab:!selected:hover {{
QTabBar::close-button {{ QTabBar::close-button {{
subcontrol-origin: padding; subcontrol-origin: padding;
subcontrol-position: right; subcontrol-position: right;
width: 22px; image: {_TAB_CLOSE_ICON_URL};
height: 22px; width: 26px;
margin: 0 2px 0 8px; height: 26px;
padding: 4px; margin: 0 4px 0 6px;
border-radius: 6px; padding: 5px;
border-radius: 8px;
background-color: {BG_CODE};
border: 1px solid {BORDER};
}} }}
QTabBar::close-button:hover {{ QTabBar::close-button:hover {{
background-color: {BG_CARD}; background-color: {BG_CARD};
border: 1px solid {ACCENT_SOFT}; border: 1px solid {ACCENT};
}} }}
QTabBar::close-button:pressed {{ QTabBar::close-button:pressed {{
background-color: {BG_CODE}; background-color: {BG_FIELD};
border: 1px solid {ACCENT_SOFT};
}} }}
QComboBox {{ QComboBox {{
background-color: {BG_FIELD}; background-color: {BG_FIELD};

Loading…
Cancel
Save