Browse Source

implement favicon and banner

master
Silberengel 2 weeks ago
parent
commit
ab3bce931b
  1. 2
      pyproject.toml
  2. 6
      src/imwald/app.py
  3. 19
      src/imwald/ui/asset_paths.py
  4. BIN
      src/imwald/ui/assets/banner.png
  5. BIN
      src/imwald/ui/assets/favicon.png
  6. 82
      src/imwald/ui/profile_page.py

2
pyproject.toml

@ -34,7 +34,7 @@ where = ["src"]
"" = "src" "" = "src"
[tool.setuptools.package-data] [tool.setuptools.package-data]
imwald = ["ui/assets/vendor/*.js"] imwald = ["ui/assets/vendor/*.js", "ui/assets/*.png"]
[tool.pytest.ini_options] [tool.pytest.ini_options]
pythonpath = ["src"] pythonpath = ["src"]

6
src/imwald/app.py

@ -5,12 +5,13 @@ from __future__ import annotations
import logging import logging
import sys import sys
from PySide6.QtGui import QFont from PySide6.QtGui import QFont, QIcon, QPixmap
from PySide6.QtWidgets import QApplication from PySide6.QtWidgets import QApplication
from imwald.config import db_path from imwald.config import db_path
from imwald.core.database import Database from imwald.core.database import Database
from imwald.core.nostr_engine import NostrEngine from imwald.core.nostr_engine import NostrEngine
from imwald.ui.asset_paths import favicon_png_path
from imwald.ui.main_window import MainWindow from imwald.ui.main_window import MainWindow
from imwald.ui.theme import apply_application_theme from imwald.ui.theme import apply_application_theme
@ -35,6 +36,9 @@ def main() -> None:
app = QApplication(sys.argv) app = QApplication(sys.argv)
app.setApplicationName("imwald") app.setApplicationName("imwald")
app.setOrganizationName("imwald") app.setOrganizationName("imwald")
fav_pm = QPixmap(str(favicon_png_path()))
if not fav_pm.isNull():
app.setWindowIcon(QIcon(fav_pm))
apply_application_theme(app) apply_application_theme(app)
_set_comfortable_default_font(app) _set_comfortable_default_font(app)

19
src/imwald/ui/asset_paths.py

@ -0,0 +1,19 @@
"""Filesystem paths to shipped assets under ``imwald/ui/assets``."""
from __future__ import annotations
from pathlib import Path
_UI_DIR = Path(__file__).resolve().parent
def ui_assets_dir() -> Path:
return _UI_DIR / "assets"
def favicon_png_path() -> Path:
return ui_assets_dir() / "favicon.png"
def default_banner_png_path() -> Path:
return ui_assets_dir() / "banner.png"

BIN
src/imwald/ui/assets/banner.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

BIN
src/imwald/ui/assets/favicon.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

82
src/imwald/ui/profile_page.py

@ -7,6 +7,7 @@ import json
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import cast from typing import cast
from PySide6 import Shiboken
from PySide6.QtCore import QObject, QRunnable, QSize, Qt, QThreadPool, Signal, QUrl from PySide6.QtCore import QObject, QRunnable, QSize, Qt, QThreadPool, Signal, QUrl
from PySide6.QtGui import QDesktopServices, QMouseEvent, QPainter, QPainterPath, QPixmap, QResizeEvent from PySide6.QtGui import QDesktopServices, QMouseEvent, QPainter, QPainterPath, QPixmap, QResizeEvent
from PySide6.QtWidgets import QFrame, QLabel, QSizePolicy, QSplitter, QTabWidget, QVBoxLayout, QWidget from PySide6.QtWidgets import QFrame, QLabel, QSizePolicy, QSplitter, QTabWidget, QVBoxLayout, QWidget
@ -19,6 +20,7 @@ from imwald.core.md_render import markdown_html_fragment, markdown_plain_summary
from imwald.core.nip19 import encode_npub from imwald.core.nip19 import encode_npub
from imwald.core.nostr_engine import NostrEngine from imwald.core.nostr_engine import NostrEngine
from imwald.core.relay_list import parse_kind10002_tags from imwald.core.relay_list import parse_kind10002_tags
from imwald.ui.asset_paths import default_banner_png_path
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_SOFT, BG_CARD, BG_CODE, BORDER, FEED_DOC_CSS, TEXT, TEXT_DIM, TEXT_MUTED from imwald.ui.theme import ACCENT_SOFT, BG_CARD, BG_CODE, BORDER, FEED_DOC_CSS, TEXT, TEXT_DIM, TEXT_MUTED
@ -210,6 +212,11 @@ class _CoverImageSignals(QObject):
avatar_ready = Signal(object, int) avatar_ready = Signal(object, int)
def _qobject_alive(obj: QObject) -> bool:
"""False after the wrapped Qt object has been destroyed (e.g. profile tab closed)."""
return Shiboken.isValid(obj)
class _ProfileHttpImageRunnable(QRunnable): class _ProfileHttpImageRunnable(QRunnable):
def __init__(self, url: str, gen: int, mode: str, out: _CoverImageSignals) -> None: def __init__(self, url: str, gen: int, mode: str, out: _CoverImageSignals) -> None:
super().__init__() super().__init__()
@ -233,6 +240,8 @@ class _ProfileHttpImageRunnable(QRunnable):
except OSError: except OSError:
self._emit_fail() self._emit_fail()
return return
if not _qobject_alive(self._out):
return
if self._mode == "banner": if self._mode == "banner":
self._out.banner_ready.emit(pm, self._gen) self._out.banner_ready.emit(pm, self._gen)
elif self._mode == "avatar": elif self._mode == "avatar":
@ -252,6 +261,8 @@ class _ProfileHttpImageRunnable(QRunnable):
self._emit_fail() self._emit_fail()
def _emit_fail(self) -> None: def _emit_fail(self) -> None:
if not _qobject_alive(self._out):
return
if self._mode == "banner": if self._mode == "banner":
self._out.banner_ready.emit(QPixmap(), self._gen) self._out.banner_ready.emit(QPixmap(), self._gen)
else: else:
@ -291,7 +302,8 @@ class _ProfileLnurlRunnable(QRunnable):
def run(self) -> None: def run(self) -> None:
html = build_merged_lnurl_pay_section(self._urls) html = build_merged_lnurl_pay_section(self._urls)
self._out.finished.emit(html, self._gen) if _qobject_alive(self._out):
self._out.finished.emit(html, self._gen)
class ProfilePage(QWidget): class ProfilePage(QWidget):
@ -337,9 +349,11 @@ class ProfilePage(QWidget):
split.setChildrenCollapsible(False) split.setChildrenCollapsible(False)
split.addWidget(self._left_column) split.addWidget(self._left_column)
split.addWidget(self._feed_body) split.addWidget(self._feed_body)
split.setStretchFactor(0, 100) # Narrower metadata column, wider posts — inverse of FeedPage (thread wide, OP narrower).
split.setStretchFactor(1, 58) split.setStretchFactor(0, 2)
split.setSizes([720, 400]) split.setStretchFactor(1, 3)
split.setSizes([400, 780])
self._left_column.setMinimumWidth(300)
self._feed_body.setMinimumWidth(260) self._feed_body.setMinimumWidth(260)
lay = QVBoxLayout(self) lay = QVBoxLayout(self)
lay.setContentsMargins(0, 0, 0, 0) lay.setContentsMargins(0, 0, 0, 0)
@ -356,6 +370,10 @@ class ProfilePage(QWidget):
self._cov_sigs.avatar_ready.connect(self._on_cover_avatar_http) self._cov_sigs.avatar_ready.connect(self._on_cover_avatar_http)
self._cov_pool = QThreadPool(self) self._cov_pool = QThreadPool(self)
self._cov_pool.setMaxThreadCount(3) self._cov_pool.setMaxThreadCount(3)
# Last banner/picture HTTPS URLs we applied to the hero (including ""). Used to avoid
# resetting placeholders + bumping fetch generations on every refresh() while ingest runs.
self._cover_banner_http_key: str | None = None
self._cover_avatar_http_key: str | None = None
self.refresh() self.refresh()
def tab_title(self) -> str: def tab_title(self) -> str:
@ -437,28 +455,40 @@ class ProfilePage(QWidget):
else "" else ""
) )
forest_seed = int(pk[:16], 16) % (2**31) if len(pk) >= 16 else 0 forest_seed = int(pk[:16], 16) % (2**31) if len(pk) >= 16 else 0
ph_b = QPixmap() b_key = banner_https or ""
bh_bytes = build_forest_banner_png( banner_dirty = self._cover_banner_http_key is None or self._cover_banner_http_key != b_key
width=2000, height=ProfileHeroFrame.BANNER_H + 48, seed=forest_seed p_key = picture_https or ""
) avatar_dirty = self._cover_avatar_http_key is None or self._cover_avatar_http_key != p_key
if not ph_b.loadFromData(bh_bytes):
ph_b.loadFromData( if banner_dirty:
build_forest_banner_png(width=2000, height=ProfileHeroFrame.BANNER_H + 48, seed=42) self._cover_banner_http_key = b_key
) ph_b = QPixmap(str(default_banner_png_path()))
self._hero.set_banner_source(ph_b if not ph_b.isNull() else QPixmap()) if ph_b.isNull():
ph_a = QPixmap() bh_bytes = build_forest_banner_png(
if not ph_a.loadFromData(build_forest_avatar_png(size=384, seed=forest_seed)): width=2000, height=ProfileHeroFrame.BANNER_H + 48, seed=forest_seed
ph_a.loadFromData(build_forest_avatar_png(size=384, seed=42)) )
av_circ = _circle_pixmap(ph_a, ProfileHeroFrame.AVATAR) if not ph_b.loadFromData(bh_bytes):
self._hero.set_avatar_pixmap(av_circ) ph_b.loadFromData(
self._cover_banner_gen += 1 build_forest_banner_png(width=2000, height=ProfileHeroFrame.BANNER_H + 48, seed=42)
b_gen = self._cover_banner_gen )
self._cover_avatar_gen += 1 self._hero.set_banner_source(ph_b if not ph_b.isNull() else QPixmap())
a_gen = self._cover_avatar_gen self._cover_banner_gen += 1
if banner_https: b_gen = self._cover_banner_gen
self._cov_pool.start(_ProfileHttpImageRunnable(banner_https, b_gen, "banner", self._cov_sigs)) if banner_https:
if picture_https: self._cov_pool.start(_ProfileHttpImageRunnable(banner_https, b_gen, "banner", self._cov_sigs))
self._cov_pool.start(_ProfileHttpImageRunnable(picture_https, a_gen, "avatar", self._cov_sigs))
if avatar_dirty:
self._cover_avatar_http_key = p_key
ph_a = QPixmap()
if not ph_a.loadFromData(build_forest_avatar_png(size=384, seed=forest_seed)):
ph_a.loadFromData(build_forest_avatar_png(size=384, seed=42))
av_circ = _circle_pixmap(ph_a, ProfileHeroFrame.AVATAR)
self._hero.set_avatar_pixmap(av_circ)
self._cover_avatar_gen += 1
a_gen = self._cover_avatar_gen
if picture_https:
self._cov_pool.start(_ProfileHttpImageRunnable(picture_https, a_gen, "avatar", self._cov_sigs))
self._hero.set_banner_open_url(banner_https or None) self._hero.set_banner_open_url(banner_https or None)
k0_meta_plain = "" k0_meta_plain = ""
if k0_ev: if k0_ev:

Loading…
Cancel
Save