|
|
|
|
@ -4,10 +4,10 @@ from __future__ import annotations
@@ -4,10 +4,10 @@ from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import html |
|
|
|
|
import json |
|
|
|
|
from typing import Any |
|
|
|
|
from typing import Any, cast |
|
|
|
|
|
|
|
|
|
from PySide6.QtCore import Qt |
|
|
|
|
from PySide6.QtGui import QFont, QTextOption |
|
|
|
|
from PySide6.QtCore import QEvent, QObject, Qt, QTimer |
|
|
|
|
from PySide6.QtGui import QKeyEvent, QTextOption |
|
|
|
|
from PySide6.QtWidgets import ( |
|
|
|
|
QFrame, |
|
|
|
|
QHBoxLayout, |
|
|
|
|
@ -21,37 +21,34 @@ from PySide6.QtWidgets import (
@@ -21,37 +21,34 @@ from PySide6.QtWidgets import (
|
|
|
|
|
QWidget, |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
from imwald.core.author_html import feed_op_author_block_html, thread_reply_author_row_html |
|
|
|
|
from imwald.core.database import Database, THREAD_REPLY_KINDS |
|
|
|
|
from imwald.core.kind0_profile import display_name_from_profile, parse_kind0_profile |
|
|
|
|
from imwald.core.kind0_profile import parse_kind0_profile |
|
|
|
|
from imwald.core.md_render import markdown_html_fragment, markdown_to_plain_text |
|
|
|
|
from imwald.core.nip19 import encode_npub |
|
|
|
|
from imwald.core.nostr_engine import NostrEngine |
|
|
|
|
from imwald.core.ranker import Ranker |
|
|
|
|
from imwald.ui.note_text_browser import NoteTextBrowser |
|
|
|
|
from imwald.ui.theme import BORDER, FEED_DOC_CSS, TEXT, TEXT_DIM, TEXT_MUTED |
|
|
|
|
|
|
|
|
|
FEED_KINDS = (1, 20, 21, 30023, 9802, 11) |
|
|
|
|
|
|
|
|
|
_FEED_DOC_CSS = """ |
|
|
|
|
<style> |
|
|
|
|
body { font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; font-size: 17px; |
|
|
|
|
margin: 0; padding: 0; line-height: 1.5; color: #1e1b16; background: transparent; } |
|
|
|
|
a { color: #2563eb; } |
|
|
|
|
pre, code { font-family: ui-monospace, "Cascadia Code", Consolas, monospace; font-size: 15px; } |
|
|
|
|
pre { background: #f0ebe3; padding: 10px; border-radius: 8px; overflow-x: auto; } |
|
|
|
|
blockquote { border-left: 3px solid #c4b8a8; margin: 8px 0; padding: 4px 0 4px 12px; color: #4a4236; } |
|
|
|
|
.md img { max-width: min(100%, 400px); height: auto; border-radius: 8px; margin: 6px 0; } |
|
|
|
|
.md p { margin: 0.45em 0; } |
|
|
|
|
</style> |
|
|
|
|
""" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _safe_http_url(u: str | None) -> str | None: |
|
|
|
|
if not u or not isinstance(u, str): |
|
|
|
|
return None |
|
|
|
|
u = u.strip() |
|
|
|
|
if u.startswith("https://") or u.startswith("http://"): |
|
|
|
|
return html.escape(u, quote=True) |
|
|
|
|
return None |
|
|
|
|
|
|
|
|
|
def _set_plain_height_to_content(te: QPlainTextEdit) -> None: |
|
|
|
|
doc = te.document() |
|
|
|
|
lay = doc.documentLayout() |
|
|
|
|
if lay is None: |
|
|
|
|
return |
|
|
|
|
vw = te.viewport().width() |
|
|
|
|
if vw < 50: |
|
|
|
|
outer = max(te.width(), 120) |
|
|
|
|
vw = outer - te.frameWidth() * 2 - 4 |
|
|
|
|
doc.setTextWidth(float(max(vw, 80))) |
|
|
|
|
h = lay.documentSize().height() |
|
|
|
|
m = te.contentsMargins() |
|
|
|
|
margins = m.top() + m.bottom() + int(doc.documentMargin()) * 2 |
|
|
|
|
fr = te.frameWidth() * 2 |
|
|
|
|
te.setFixedHeight(int(max(h + margins + fr, 44))) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _format_engagement_html(stats: dict[str, Any]) -> str: |
|
|
|
|
@ -82,37 +79,20 @@ def _format_engagement_html(stats: dict[str, Any]) -> str:
@@ -82,37 +79,20 @@ def _format_engagement_html(stats: dict[str, Any]) -> str:
|
|
|
|
|
em_row = " ".join(emoji_bits) if emoji_bits else "" |
|
|
|
|
head = " · ".join(parts) if parts else "no engagement in local DB yet" |
|
|
|
|
if em_row: |
|
|
|
|
return f"<div style='margin-bottom:6px'>{head}</div><div>{em_row}</div>" |
|
|
|
|
return f"<div>{head}</div>" |
|
|
|
|
inner = f"<div style='margin-bottom:6px'>{head}</div><div>{em_row}</div>" |
|
|
|
|
else: |
|
|
|
|
inner = f"<div>{head}</div>" |
|
|
|
|
return f'<div style="color:{TEXT}">{inner}</div>' |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class FeedPage(QWidget): |
|
|
|
|
def __init__(self, db: Database, engine: NostrEngine, parent=None) -> None: |
|
|
|
|
super().__init__(parent) |
|
|
|
|
self.setObjectName("FeedPage") |
|
|
|
|
self.setStyleSheet( |
|
|
|
|
""" |
|
|
|
|
QWidget#FeedPage { background: #ebe6dc; } |
|
|
|
|
QFrame#EngagementBar { |
|
|
|
|
background: #faf7f2; border: 1px solid #d9d0c3; border-radius: 10px; |
|
|
|
|
padding: 8px 12px; margin-bottom: 8px; |
|
|
|
|
} |
|
|
|
|
QFrame#OpCard { |
|
|
|
|
background: #fffcf7; border: 1px solid #d9d0c3; border-radius: 12px; |
|
|
|
|
} |
|
|
|
|
QScrollArea#ThreadScroll { border: 1px solid #d9d0c3; border-radius: 10px; background: #faf7f2; } |
|
|
|
|
QFrame#ReplyCard { |
|
|
|
|
background: #fffcf7; border: 1px solid #e5ddd0; border-radius: 8px; margin: 2px 0; |
|
|
|
|
} |
|
|
|
|
QLabel#ThreadTitle { font-weight: 600; color: #3d3428; padding: 4px 2px; } |
|
|
|
|
QPlainTextEdit#ReplyBody { |
|
|
|
|
border: none; background: transparent; font-size: 16px; color: #2a241c; |
|
|
|
|
} |
|
|
|
|
""" |
|
|
|
|
) |
|
|
|
|
self._db = db |
|
|
|
|
self._engine = engine |
|
|
|
|
self._ranker = Ranker(db) |
|
|
|
|
self._page_nav_widgets: set[QObject] = set() |
|
|
|
|
self._queue: list[dict[str, Any]] = [] |
|
|
|
|
self._index = 0 |
|
|
|
|
self._my_pubkey: str | None = None |
|
|
|
|
@ -127,6 +107,8 @@ class FeedPage(QWidget):
@@ -127,6 +107,8 @@ class FeedPage(QWidget):
|
|
|
|
|
self._engagement_label.setTextFormat(Qt.TextFormat.RichText) |
|
|
|
|
self._engagement_label.setWordWrap(True) |
|
|
|
|
eng_layout.addWidget(self._engagement_label) |
|
|
|
|
self._engagement_label.installEventFilter(self) |
|
|
|
|
self._page_nav_widgets.add(self._engagement_label) |
|
|
|
|
|
|
|
|
|
self._op_card = QFrame() |
|
|
|
|
self._op_card.setObjectName("OpCard") |
|
|
|
|
@ -137,10 +119,12 @@ class FeedPage(QWidget):
@@ -137,10 +119,12 @@ class FeedPage(QWidget):
|
|
|
|
|
self._op.setOpenExternalLinks(True) |
|
|
|
|
self._op.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) |
|
|
|
|
self._op.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) |
|
|
|
|
self._op.installEventFilter(self) |
|
|
|
|
self._page_nav_widgets.add(self._op) |
|
|
|
|
op_card_lay.addWidget(self._op, stretch=1) |
|
|
|
|
|
|
|
|
|
self._why = QLabel("") |
|
|
|
|
self._why.setStyleSheet("color: #6b5f4f; font-size: 14px;") |
|
|
|
|
self._why.setStyleSheet(f"color: {TEXT_MUTED}; font-size: 14px;") |
|
|
|
|
self._why.setWordWrap(True) |
|
|
|
|
|
|
|
|
|
self._thread_title = QLabel( |
|
|
|
|
@ -156,6 +140,7 @@ class FeedPage(QWidget):
@@ -156,6 +140,7 @@ class FeedPage(QWidget):
|
|
|
|
|
self._thread_scroll.setWidgetResizable(True) |
|
|
|
|
self._thread_scroll.setWidget(self._thread_host) |
|
|
|
|
self._thread_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) |
|
|
|
|
self._thread_scroll.viewport().installEventFilter(self) |
|
|
|
|
|
|
|
|
|
prev = QPushButton("◀ Previous") |
|
|
|
|
next_ = QPushButton("Next ▶") |
|
|
|
|
@ -173,6 +158,9 @@ class FeedPage(QWidget):
@@ -173,6 +158,9 @@ class FeedPage(QWidget):
|
|
|
|
|
nav.addWidget(down) |
|
|
|
|
nav.addStretch() |
|
|
|
|
nav.addWidget(self._why) |
|
|
|
|
for w in (prev, next_, up, down, self._why): |
|
|
|
|
w.installEventFilter(self) |
|
|
|
|
self._page_nav_widgets.add(w) |
|
|
|
|
|
|
|
|
|
left = QVBoxLayout() |
|
|
|
|
left.setSpacing(8) |
|
|
|
|
@ -192,6 +180,9 @@ class FeedPage(QWidget):
@@ -192,6 +180,9 @@ class FeedPage(QWidget):
|
|
|
|
|
split = QSplitter(Qt.Orientation.Horizontal) |
|
|
|
|
split.addWidget(lw) |
|
|
|
|
split.addWidget(rw) |
|
|
|
|
for w in (self._engagement, self._engagement_label, self._op_card, self._thread_title, lw, rw, split): |
|
|
|
|
w.installEventFilter(self) |
|
|
|
|
self._page_nav_widgets.add(w) |
|
|
|
|
split.setStretchFactor(0, 3) |
|
|
|
|
split.setStretchFactor(1, 2) |
|
|
|
|
split.setSizes([780, 420]) |
|
|
|
|
@ -200,6 +191,34 @@ class FeedPage(QWidget):
@@ -200,6 +191,34 @@ class FeedPage(QWidget):
|
|
|
|
|
outer.setContentsMargins(10, 8, 10, 8) |
|
|
|
|
outer.addWidget(split) |
|
|
|
|
|
|
|
|
|
def eventFilter(self, obj: QObject, event: QEvent) -> bool: # noqa: N802 |
|
|
|
|
if event.type() == QEvent.Type.KeyPress and isinstance(event, QKeyEvent): |
|
|
|
|
nav_ok = obj in self._page_nav_widgets or ( |
|
|
|
|
isinstance(obj, QPlainTextEdit) and obj.objectName() == "ReplyBody" |
|
|
|
|
) |
|
|
|
|
if nav_ok: |
|
|
|
|
if event.key() == Qt.Key.Key_PageDown: |
|
|
|
|
self._next() |
|
|
|
|
return True |
|
|
|
|
if event.key() == Qt.Key.Key_PageUp: |
|
|
|
|
self._prev() |
|
|
|
|
return True |
|
|
|
|
if obj is self._thread_scroll.viewport() and event.type() == QEvent.Type.Resize: |
|
|
|
|
self._refit_thread_reply_sizes() |
|
|
|
|
return super().eventFilter(obj, event) |
|
|
|
|
|
|
|
|
|
def _refit_thread_reply_sizes(self) -> None: |
|
|
|
|
for i in range(self._thread_layout.count()): |
|
|
|
|
item = self._thread_layout.itemAt(i) |
|
|
|
|
if item is None: |
|
|
|
|
continue |
|
|
|
|
w = item.widget() |
|
|
|
|
if w is None: |
|
|
|
|
continue |
|
|
|
|
te = w.findChild(QPlainTextEdit, "ReplyBody") |
|
|
|
|
if te is not None: |
|
|
|
|
_set_plain_height_to_content(te) |
|
|
|
|
|
|
|
|
|
def set_context( |
|
|
|
|
self, |
|
|
|
|
my_pubkey: str | None, |
|
|
|
|
@ -237,7 +256,7 @@ class FeedPage(QWidget):
@@ -237,7 +256,7 @@ class FeedPage(QWidget):
|
|
|
|
|
if not ev: |
|
|
|
|
self._op.setPlainText(f"(not in local DB yet) {event_id}") |
|
|
|
|
return |
|
|
|
|
self._queue = [ev] |
|
|
|
|
self._queue = [cast(dict[str, Any], ev)] |
|
|
|
|
self._index = 0 |
|
|
|
|
self._show_current() |
|
|
|
|
if not ev.get("deleted"): |
|
|
|
|
@ -263,8 +282,10 @@ class FeedPage(QWidget):
@@ -263,8 +282,10 @@ class FeedPage(QWidget):
|
|
|
|
|
if ev.get("deleted"): |
|
|
|
|
raw = html.escape(ev.get("content") or "") |
|
|
|
|
self._op.setHtml( |
|
|
|
|
f"<p><i>Marked deleted locally</i></p><pre>{raw}</pre>" |
|
|
|
|
f"<p style='color:gray'>{html.escape(ev['id'])}</p>" |
|
|
|
|
f"<body style=\"color:{TEXT};background:transparent\">" |
|
|
|
|
f"<p><i>Marked deleted locally</i></p>" |
|
|
|
|
f"<pre style=\"color:{TEXT_MUTED}\">{raw}</pre>" |
|
|
|
|
f"<p style=\"color:{TEXT_DIM}\">{html.escape(ev['id'])}</p></body>" |
|
|
|
|
) |
|
|
|
|
self._clear_thread_rows() |
|
|
|
|
self._why.setText("") |
|
|
|
|
@ -284,41 +305,42 @@ class FeedPage(QWidget):
@@ -284,41 +305,42 @@ class FeedPage(QWidget):
|
|
|
|
|
|
|
|
|
|
pk = ev["pubkey"] |
|
|
|
|
prof_row = self._db.get_latest_kind0_profile(pk) |
|
|
|
|
parsed = parse_kind0_profile((prof_row or {}).get("content") or "") |
|
|
|
|
disp = html.escape(display_name_from_profile(parsed)) |
|
|
|
|
parsed = parse_kind0_profile(prof_row["content"] if prof_row else "") |
|
|
|
|
npub = encode_npub(pk) |
|
|
|
|
npub_e = html.escape(npub) |
|
|
|
|
pk_short = html.escape(pk[:12] + "…") |
|
|
|
|
pic_url = _safe_http_url(parsed.get("picture")) |
|
|
|
|
pk_short = pk[:12] + "…" |
|
|
|
|
nip05 = html.escape((parsed.get("nip05") or "").strip()) if parsed.get("nip05") else "" |
|
|
|
|
about = html.escape((parsed.get("about") or "")[:280]) if parsed.get("about") else "" |
|
|
|
|
avatar_html = ( |
|
|
|
|
f'<img src="{pic_url}" width="52" height="52" style="border-radius:10px;object-fit:cover;vertical-align:middle;margin-right:10px"/>' |
|
|
|
|
if pic_url |
|
|
|
|
else '<span style="display:inline-block;width:52px;height:52px;border-radius:10px;background:#d9d0c3;margin-right:10px;vertical-align:middle"></span>' |
|
|
|
|
nip_line = ( |
|
|
|
|
f"<div style='color:{TEXT_MUTED};font-size:15px;margin-top:4px'>{nip05}</div>" if nip05 else "" |
|
|
|
|
) |
|
|
|
|
about_line = f"<div style='color:{TEXT_DIM};font-size:15px;margin-top:6px'>{about}</div>" if about else "" |
|
|
|
|
author_block = feed_op_author_block_html( |
|
|
|
|
parsed, |
|
|
|
|
npub, |
|
|
|
|
pk_short, |
|
|
|
|
nip_line, |
|
|
|
|
about_line, |
|
|
|
|
text=TEXT, |
|
|
|
|
muted=TEXT_MUTED, |
|
|
|
|
dim=TEXT_DIM, |
|
|
|
|
border=BORDER, |
|
|
|
|
) |
|
|
|
|
nip_line = f"<div style='color:#6b5f4f;font-size:15px;margin-top:4px'>{nip05}</div>" if nip05 else "" |
|
|
|
|
about_line = f"<div style='color:#5c5246;font-size:15px;margin-top:6px'>{about}</div>" if about else "" |
|
|
|
|
|
|
|
|
|
tr = "" |
|
|
|
|
sr = ev.get("source_relay") or "" |
|
|
|
|
if sr and "nostrarchives.com" in sr: |
|
|
|
|
tr = "<div style='color:#7a6b55;font-size:15px;margin:6px 0'><i>Trending slice (nostrarchives)</i></div>" |
|
|
|
|
tr = f"<div style='color:{TEXT_DIM};font-size:15px;margin:6px 0'><i>Trending slice (nostrarchives)</i></div>" |
|
|
|
|
|
|
|
|
|
eid = html.escape(ev["id"]) |
|
|
|
|
md_body = markdown_html_fragment(ev.get("content") or "") |
|
|
|
|
md_body = markdown_html_fragment(ev.get("content") or "", db=self._db) |
|
|
|
|
body = ( |
|
|
|
|
"<!DOCTYPE html><html><head><meta charset=\"utf-8\">" |
|
|
|
|
f"{_FEED_DOC_CSS}</head><body>" |
|
|
|
|
f"<div style='display:flex;align-items:flex-start;margin-bottom:12px'>" |
|
|
|
|
f"{avatar_html}" |
|
|
|
|
f"<div style='flex:1'><div style='font-size:21px;font-weight:600'>{disp}</div>" |
|
|
|
|
f"<div style='color:#6b5f4f;font-size:15px'>{npub_e} · {pk_short}</div>" |
|
|
|
|
f"{nip_line}{about_line}</div></div>" |
|
|
|
|
f"<div style='color:#7a6b55;font-size:15px;margin-bottom:8px'>Kind {int(ev['kind'])} · {int(ev['created_at'])}</div>" |
|
|
|
|
f"{FEED_DOC_CSS}</head><body>" |
|
|
|
|
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"{tr}" |
|
|
|
|
f"<div class=\"md\">{md_body}</div>" |
|
|
|
|
f"<p style='color:#9a8b78;font-size:14px;margin-top:14px'>{eid}</p>" |
|
|
|
|
f"<p style='color:{TEXT_DIM};font-size:14px;margin-top:14px'>{eid}</p>" |
|
|
|
|
"</body></html>" |
|
|
|
|
) |
|
|
|
|
self._op.setHtml(body) |
|
|
|
|
@ -329,41 +351,49 @@ class FeedPage(QWidget):
@@ -329,41 +351,49 @@ class FeedPage(QWidget):
|
|
|
|
|
profiles = self._db.get_latest_kind0_profiles(pubkeys) |
|
|
|
|
for r in replies: |
|
|
|
|
rpk = str(r["pubkey"]).lower() |
|
|
|
|
pr = profiles.get(rpk) or {} |
|
|
|
|
rp = parse_kind0_profile(pr.get("content") or "") |
|
|
|
|
rname = html.escape(display_name_from_profile(rp)) |
|
|
|
|
rnpub = html.escape(encode_npub(rpk)) |
|
|
|
|
plain = markdown_to_plain_text(r.get("content") or "") |
|
|
|
|
pr = profiles.get(rpk) |
|
|
|
|
rp = parse_kind0_profile(pr["content"] if pr else "") |
|
|
|
|
plain = markdown_to_plain_text(r.get("content") or "", db=self._db) |
|
|
|
|
|
|
|
|
|
card = QFrame() |
|
|
|
|
card.setObjectName("ReplyCard") |
|
|
|
|
vl = QVBoxLayout(card) |
|
|
|
|
vl.setContentsMargins(8, 6, 8, 8) |
|
|
|
|
rk = int(r["kind"]) |
|
|
|
|
head = QLabel( |
|
|
|
|
f"<span style='color:#8a7b6a;font-size:13px'>k{rk}</span> " |
|
|
|
|
f"<b>{rname}</b> <span style='color:#7a6b55;font-size:14px'>{rnpub}</span>" |
|
|
|
|
head_b = NoteTextBrowser(self) |
|
|
|
|
head_b.setObjectName("ReplyHead") |
|
|
|
|
head_b.setOpenExternalLinks(False) |
|
|
|
|
head_b.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) |
|
|
|
|
head_b.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) |
|
|
|
|
head_b.setFrameShape(QFrame.Shape.NoFrame) |
|
|
|
|
head_b.document().setDocumentMargin(2) |
|
|
|
|
head_b.setFixedHeight(56) |
|
|
|
|
row_html = thread_reply_author_row_html( |
|
|
|
|
rp, rk, encode_npub(rpk), text=TEXT, muted=TEXT_MUTED, dim=TEXT_DIM, border=BORDER |
|
|
|
|
) |
|
|
|
|
head_b.setHtml( |
|
|
|
|
"<!DOCTYPE html><html><head><meta charset=\"utf-8\">" |
|
|
|
|
f"{FEED_DOC_CSS}</head><body style=\"margin:0;padding:0\">" |
|
|
|
|
f"{row_html}</body></html>" |
|
|
|
|
) |
|
|
|
|
head.setTextFormat(Qt.TextFormat.RichText) |
|
|
|
|
head.setWordWrap(True) |
|
|
|
|
f_small = QFont() |
|
|
|
|
f_small.setPointSize(13) |
|
|
|
|
head.setFont(f_small) |
|
|
|
|
head_b.installEventFilter(self) |
|
|
|
|
self._page_nav_widgets.add(head_b) |
|
|
|
|
body_te = QPlainTextEdit() |
|
|
|
|
body_te.setObjectName("ReplyBody") |
|
|
|
|
body_te.setReadOnly(True) |
|
|
|
|
body_te.setPlainText(plain or "(empty)") |
|
|
|
|
body_te.setWordWrapMode(QTextOption.WrapMode.WrapAtWordBoundaryOrAnywhere) |
|
|
|
|
body_te.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) |
|
|
|
|
body_te.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) |
|
|
|
|
body_te.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) |
|
|
|
|
body_te.setFrameShape(QFrame.Shape.NoFrame) |
|
|
|
|
body_te.document().setDocumentMargin(0) |
|
|
|
|
body_te.setMinimumHeight(96) |
|
|
|
|
body_te.setMaximumHeight(440) |
|
|
|
|
vl.addWidget(head) |
|
|
|
|
body_te.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) |
|
|
|
|
body_te.installEventFilter(self) |
|
|
|
|
vl.addWidget(head_b) |
|
|
|
|
vl.addWidget(body_te) |
|
|
|
|
self._thread_layout.addWidget(card) |
|
|
|
|
self._thread_layout.addStretch(1) |
|
|
|
|
QTimer.singleShot(0, self._refit_thread_reply_sizes) |
|
|
|
|
|
|
|
|
|
def _prev(self) -> None: |
|
|
|
|
if self._queue: |
|
|
|
|
|