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>
323 lines
9.3 KiB
Rust
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())
|
|
}
|