Files
notedeck/src/ui/profile/preview.rs
William Casarin be3edc02a4 nav: refactor title rendering for flexibility
Updated navigation to use a custom title renderer for more flexible
rendering of navigation titles. This change decouples the rendering
logic from predefined formats, enabling dynamic title compositions
based on application context and data.

This includes:
- Refactoring `NavResponse` to introduce `NotedeckNavResponse` for
  handling unified navigation response data.
- Adding `NavTitle` in `ui/column/header.rs` to handle rendering
  of navigation titles and profile images dynamically.
- Updating route and timeline logic to support new rendering pipeline.
- Replacing hardcoded title rendering with data-driven approaches.

Benefits:
- Simplifies navigation handling by consolidating title and action
  management.
- Improves scalability for new navigation features without modifying
  core logic.
- Enhances visual customization capabilities.

Signed-off-by: William Casarin <jb55@jb55.com>
2024-12-05 10:58:59 -08:00

323 lines
9.3 KiB
Rust

use crate::app_style::{get_font_size, NotedeckTextStyle};
use crate::imgcache::ImageCache;
use crate::storage::{DataPath, DataPathType};
use crate::ui::ProfilePic;
use crate::user_account::UserAccount;
use crate::{colors, images, DisplayName};
use egui::load::TexturePoll;
use egui::{Frame, Label, RichText, Sense, Widget};
use egui_extras::Size;
use enostr::{NoteId, Pubkey};
use nostrdb::{Ndb, ProfileRecord, Transaction};
pub struct ProfilePreview<'a, 'cache> {
profile: &'a ProfileRecord<'a>,
cache: &'cache mut ImageCache,
banner_height: Size,
}
impl<'a, 'cache> ProfilePreview<'a, 'cache> {
pub fn new(profile: &'a ProfileRecord<'a>, cache: &'cache mut ImageCache) -> Self {
let banner_height = Size::exact(80.0);
ProfilePreview {
profile,
cache,
banner_height,
}
}
pub fn banner_height(&mut self, size: Size) {
self.banner_height = size;
}
fn banner_texture(
ui: &mut egui::Ui,
profile: &ProfileRecord<'_>,
) -> Option<egui::load::SizedTexture> {
// TODO: cache banner
let banner = profile.record().profile().and_then(|p| p.banner());
if let Some(banner) = banner {
let texture_load_res =
egui::Image::new(banner).load_for_size(ui.ctx(), ui.available_size());
if let Ok(texture_poll) = texture_load_res {
match texture_poll {
TexturePoll::Pending { .. } => {}
TexturePoll::Ready { texture, .. } => return Some(texture),
}
}
}
None
}
fn banner(ui: &mut egui::Ui, profile: &ProfileRecord<'_>) -> egui::Response {
if let Some(texture) = Self::banner_texture(ui, profile) {
images::aspect_fill(
ui,
Sense::hover(),
texture.id,
texture.size.x / texture.size.y,
)
} else {
// TODO: default banner texture
ui.label("")
}
}
fn body(self, ui: &mut egui::Ui) {
crate::ui::padding(12.0, ui, |ui| {
ui.add(ProfilePic::new(self.cache, get_profile_url(Some(self.profile))).size(80.0));
ui.add(display_name_widget(
get_display_name(Some(self.profile)),
false,
));
ui.add(about_section_widget(self.profile));
});
}
}
impl egui::Widget for ProfilePreview<'_, '_> {
fn ui(self, ui: &mut egui::Ui) -> egui::Response {
ui.vertical(|ui| {
ui.add_sized([ui.available_size().x, 80.0], |ui: &mut egui::Ui| {
ProfilePreview::banner(ui, self.profile)
});
self.body(ui);
})
.response
}
}
pub struct SimpleProfilePreview<'a, 'cache> {
profile: Option<&'a ProfileRecord<'a>>,
cache: &'cache mut ImageCache,
is_nsec: bool,
}
impl<'a, 'cache> SimpleProfilePreview<'a, 'cache> {
pub fn new(
profile: Option<&'a ProfileRecord<'a>>,
cache: &'cache mut ImageCache,
is_nsec: bool,
) -> Self {
SimpleProfilePreview {
profile,
cache,
is_nsec,
}
}
}
impl egui::Widget for SimpleProfilePreview<'_, '_> {
fn ui(self, ui: &mut egui::Ui) -> egui::Response {
Frame::none()
.show(ui, |ui| {
ui.add(ProfilePic::new(self.cache, get_profile_url(self.profile)).size(48.0));
ui.vertical(|ui| {
ui.add(display_name_widget(get_display_name(self.profile), true));
if !self.is_nsec {
ui.add(
Label::new(
RichText::new("Read only")
.size(get_font_size(ui.ctx(), &NotedeckTextStyle::Tiny))
.color(ui.visuals().warn_fg_color),
)
.selectable(false),
);
}
});
})
.response
}
}
mod previews {
use super::*;
use crate::test_data::test_profile_record;
use crate::ui::{Preview, PreviewConfig, View};
pub struct ProfilePreviewPreview<'a> {
profile: ProfileRecord<'a>,
cache: ImageCache,
}
impl ProfilePreviewPreview<'_> {
pub fn new() -> Self {
let profile = test_profile_record();
let path = DataPath::new("previews")
.path(DataPathType::Cache)
.join(ImageCache::rel_dir());
let cache = ImageCache::new(path);
ProfilePreviewPreview { profile, cache }
}
}
impl Default for ProfilePreviewPreview<'_> {
fn default() -> Self {
ProfilePreviewPreview::new()
}
}
impl View for ProfilePreviewPreview<'_> {
fn ui(&mut self, ui: &mut egui::Ui) {
ProfilePreview::new(&self.profile, &mut self.cache).ui(ui);
}
}
impl<'a> Preview for ProfilePreview<'a, '_> {
/// A preview of the profile preview :D
type Prev = ProfilePreviewPreview<'a>;
fn preview(_cfg: PreviewConfig) -> Self::Prev {
ProfilePreviewPreview::new()
}
}
}
pub fn get_display_name<'a>(profile: Option<&ProfileRecord<'a>>) -> DisplayName<'a> {
if let Some(name) = profile.and_then(|p| crate::profile::get_profile_name(p)) {
name
} else {
DisplayName::One("??")
}
}
pub fn get_profile_url<'a>(profile: Option<&ProfileRecord<'a>>) -> &'a str {
if let Some(url) = profile.and_then(|pr| pr.record().profile().and_then(|p| p.picture())) {
url
} else {
ProfilePic::no_pfp_url()
}
}
pub fn get_profile_url_owned(profile: Option<ProfileRecord<'_>>) -> &str {
if let Some(url) = profile.and_then(|pr| pr.record().profile().and_then(|p| p.picture())) {
url
} else {
ProfilePic::no_pfp_url()
}
}
pub fn get_account_url<'a>(
txn: &'a nostrdb::Transaction,
ndb: &nostrdb::Ndb,
account: Option<&UserAccount>,
) -> &'a str {
if let Some(selected_account) = account {
if let Ok(profile) = ndb.get_profile_by_pubkey(txn, selected_account.pubkey.bytes()) {
get_profile_url_owned(Some(profile))
} else {
get_profile_url_owned(None)
}
} else {
get_profile_url(None)
}
}
fn display_name_widget(
display_name: DisplayName<'_>,
add_placeholder_space: bool,
) -> impl egui::Widget + '_ {
move |ui: &mut egui::Ui| match display_name {
DisplayName::One(n) => {
let name_response = ui.add(
Label::new(RichText::new(n).text_style(NotedeckTextStyle::Heading3.text_style()))
.selectable(false),
);
if add_placeholder_space {
ui.add_space(16.0);
}
name_response
}
DisplayName::Both {
display_name,
username,
} => {
ui.add(
Label::new(
RichText::new(display_name)
.text_style(NotedeckTextStyle::Heading3.text_style()),
)
.selectable(false),
);
ui.add(
Label::new(
RichText::new(format!("@{}", username))
.size(12.0)
.color(colors::MID_GRAY),
)
.selectable(false),
)
}
}
}
pub fn one_line_display_name_widget(
display_name: DisplayName<'_>,
style: NotedeckTextStyle,
) -> impl egui::Widget + '_ {
let text_style = style.text_style();
move |ui: &mut egui::Ui| match display_name {
DisplayName::One(n) => ui.label(
RichText::new(n)
.text_style(text_style)
.color(colors::GRAY_SECONDARY),
),
DisplayName::Both {
display_name,
username: _,
} => ui.label(
RichText::new(display_name)
.text_style(text_style)
.color(colors::GRAY_SECONDARY),
),
}
}
fn about_section_widget<'a, 'b>(profile: &'b ProfileRecord<'a>) -> impl egui::Widget + 'b
where
'b: 'a,
{
move |ui: &mut egui::Ui| {
if let Some(about) = profile.record().profile().and_then(|p| p.about()) {
ui.label(about)
} else {
// need any Response so we dont need an Option
ui.allocate_response(egui::Vec2::ZERO, egui::Sense::hover())
}
}
}
fn get_display_name_as_string<'a>(profile: Option<&ProfileRecord<'a>>) -> &'a str {
let display_name = get_display_name(profile);
match display_name {
DisplayName::One(n) => n,
DisplayName::Both { display_name, .. } => display_name,
}
}
pub fn get_profile_displayname_string<'a>(txn: &'a Transaction, ndb: &Ndb, pk: &Pubkey) -> &'a str {
let profile = ndb.get_profile_by_pubkey(txn, pk.bytes()).ok();
get_display_name_as_string(profile.as_ref())
}
pub fn get_note_users_displayname_string<'a>(
txn: &'a Transaction,
ndb: &Ndb,
id: &NoteId,
) -> &'a str {
let note = ndb.get_note_by_id(txn, id.bytes());
let profile = if let Ok(note) = note {
ndb.get_profile_by_pubkey(txn, note.pubkey()).ok()
} else {
None
};
get_display_name_as_string(profile.as_ref())
}