@@ -7,6 +7,7 @@ pub mod icons;
|
||||
pub mod images;
|
||||
pub mod media;
|
||||
pub mod mention;
|
||||
pub mod nip51_set;
|
||||
pub mod note;
|
||||
pub mod profile;
|
||||
mod username;
|
||||
|
||||
395
crates/notedeck_ui/src/nip51_set.rs
Normal file
395
crates/notedeck_ui/src/nip51_set.rs
Normal file
@@ -0,0 +1,395 @@
|
||||
use bitflags::bitflags;
|
||||
use egui::{vec2, Checkbox, CornerRadius, Layout, Margin, RichText, Sense, UiBuilder};
|
||||
use enostr::Pubkey;
|
||||
use hashbrown::{hash_map::RawEntryMut, HashMap};
|
||||
use nostrdb::{Ndb, ProfileRecord, Transaction};
|
||||
use notedeck::{
|
||||
fonts::get_font_size, get_profile_url, name::get_display_name, tr, Images, JobPool, JobsCache,
|
||||
Localization, Nip51Set, Nip51SetCache, NotedeckTextStyle,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
note::media::{render_media, ScaledTextureFlags},
|
||||
ProfilePic,
|
||||
};
|
||||
|
||||
pub struct Nip51SetWidget<'a> {
|
||||
state: &'a Nip51SetCache,
|
||||
ui_state: &'a mut Nip51SetUiCache,
|
||||
ndb: &'a Ndb,
|
||||
images: &'a mut Images,
|
||||
loc: &'a mut Localization,
|
||||
job_pool: &'a mut JobPool,
|
||||
jobs: &'a mut JobsCache,
|
||||
flags: Nip51SetWidgetFlags,
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
#[repr(transparent)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct Nip51SetWidgetFlags: u8 {
|
||||
const REQUIRES_TITLE = 1u8;
|
||||
const REQUIRES_IMAGE = 2u8;
|
||||
const REQUIRES_DESCRIPTION = 3u8;
|
||||
const NON_EMPTY_PKS = 4u8;
|
||||
const TRUST_IMAGES = 5u8;
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Nip51SetWidgetFlags {
|
||||
fn default() -> Self {
|
||||
Self::empty()
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Nip51SetWidgetResponse {
|
||||
ViewProfile(Pubkey),
|
||||
}
|
||||
|
||||
impl<'a> Nip51SetWidget<'a> {
|
||||
pub fn new(
|
||||
state: &'a Nip51SetCache,
|
||||
ui_state: &'a mut Nip51SetUiCache,
|
||||
ndb: &'a Ndb,
|
||||
loc: &'a mut Localization,
|
||||
images: &'a mut Images,
|
||||
job_pool: &'a mut JobPool,
|
||||
jobs: &'a mut JobsCache,
|
||||
) -> Self {
|
||||
Self {
|
||||
state,
|
||||
ui_state,
|
||||
ndb,
|
||||
loc,
|
||||
images,
|
||||
job_pool,
|
||||
jobs,
|
||||
flags: Nip51SetWidgetFlags::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_flags(mut self, flags: Nip51SetWidgetFlags) -> Self {
|
||||
self.flags = flags;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<Nip51SetWidgetResponse> {
|
||||
let mut resp = None;
|
||||
for pack in self.state.iter() {
|
||||
if should_skip(pack, &self.flags) {
|
||||
continue;
|
||||
}
|
||||
|
||||
egui::Frame::new()
|
||||
.corner_radius(CornerRadius::same(8))
|
||||
.fill(ui.visuals().extreme_bg_color)
|
||||
.inner_margin(Margin::same(8))
|
||||
.show(ui, |ui| {
|
||||
if let Some(cur_resp) = render_pack(
|
||||
ui,
|
||||
pack,
|
||||
self.ui_state,
|
||||
self.ndb,
|
||||
self.images,
|
||||
self.job_pool,
|
||||
self.jobs,
|
||||
self.loc,
|
||||
self.flags.contains(Nip51SetWidgetFlags::TRUST_IMAGES),
|
||||
) {
|
||||
resp = Some(cur_resp);
|
||||
}
|
||||
});
|
||||
|
||||
ui.add_space(8.0);
|
||||
}
|
||||
|
||||
resp
|
||||
}
|
||||
}
|
||||
|
||||
fn should_skip(set: &Nip51Set, required: &Nip51SetWidgetFlags) -> bool {
|
||||
(required.contains(Nip51SetWidgetFlags::REQUIRES_TITLE) && set.title.is_none())
|
||||
|| (required.contains(Nip51SetWidgetFlags::REQUIRES_IMAGE) && set.image.is_none())
|
||||
|| (required.contains(Nip51SetWidgetFlags::REQUIRES_DESCRIPTION)
|
||||
&& set.description.is_none())
|
||||
|| (required.contains(Nip51SetWidgetFlags::NON_EMPTY_PKS) && set.pks.is_empty())
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_pack(
|
||||
ui: &mut egui::Ui,
|
||||
pack: &Nip51Set,
|
||||
ui_state: &mut Nip51SetUiCache,
|
||||
ndb: &Ndb,
|
||||
images: &mut Images,
|
||||
job_pool: &mut JobPool,
|
||||
jobs: &mut JobsCache,
|
||||
loc: &mut Localization,
|
||||
image_trusted: bool,
|
||||
) -> Option<Nip51SetWidgetResponse> {
|
||||
let max_img_size = vec2(ui.available_width(), 200.0);
|
||||
|
||||
ui.allocate_new_ui(UiBuilder::new(), |ui| 's: {
|
||||
let Some(url) = &pack.image else {
|
||||
break 's;
|
||||
};
|
||||
let Some(media) = images.get_renderable_media(url) else {
|
||||
break 's;
|
||||
};
|
||||
|
||||
let media_rect = render_media(
|
||||
ui,
|
||||
images,
|
||||
job_pool,
|
||||
jobs,
|
||||
&media,
|
||||
image_trusted,
|
||||
loc,
|
||||
max_img_size,
|
||||
None,
|
||||
ScaledTextureFlags::RESPECT_MAX_DIMS,
|
||||
)
|
||||
.response
|
||||
.rect;
|
||||
|
||||
ui.advance_cursor_after_rect(media_rect);
|
||||
});
|
||||
|
||||
let (title_rect, _) =
|
||||
ui.allocate_at_least(vec2(ui.available_width(), 0.0), egui::Sense::hover());
|
||||
|
||||
let select_all_resp = ui
|
||||
.allocate_new_ui(
|
||||
UiBuilder::new()
|
||||
.max_rect(title_rect)
|
||||
.layout(Layout::top_down(egui::Align::Min)),
|
||||
|ui| {
|
||||
if let Some(title) = &pack.title {
|
||||
ui.add(egui::Label::new(egui::RichText::new(title).size(
|
||||
get_font_size(ui.ctx(), ¬edeck::NotedeckTextStyle::Heading),
|
||||
)));
|
||||
}
|
||||
if let Some(desc) = &pack.description {
|
||||
ui.add(egui::Label::new(egui::RichText::new(desc).size(
|
||||
get_font_size(ui.ctx(), ¬edeck::NotedeckTextStyle::Heading3),
|
||||
)));
|
||||
}
|
||||
let checked = ui.checkbox(
|
||||
ui_state.get_select_all_state(&pack.identifier),
|
||||
format!(
|
||||
"{} ({})",
|
||||
tr!(
|
||||
loc,
|
||||
"Select All",
|
||||
"Button to select all profiles in follow pack"
|
||||
),
|
||||
pack.pks.len()
|
||||
),
|
||||
);
|
||||
|
||||
checked
|
||||
},
|
||||
)
|
||||
.inner;
|
||||
|
||||
let new_select_all_state = if select_all_resp.clicked() {
|
||||
Some(*ui_state.get_select_all_state(&pack.identifier))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut resp = None;
|
||||
for pk in &pack.pks {
|
||||
let txn = Transaction::new(ndb).expect("txn");
|
||||
let m_profile = ndb.get_profile_by_pubkey(&txn, pk.bytes()).ok();
|
||||
|
||||
let cur_state = ui_state.get_pk_selected_state(&pack.identifier, pk);
|
||||
if let Some(use_state) = new_select_all_state {
|
||||
*cur_state = use_state;
|
||||
};
|
||||
|
||||
ui.separator();
|
||||
if render_profile_item(ui, images, m_profile.as_ref(), cur_state) {
|
||||
resp = Some(Nip51SetWidgetResponse::ViewProfile(*pk));
|
||||
}
|
||||
}
|
||||
|
||||
resp
|
||||
}
|
||||
|
||||
fn render_profile_item(
|
||||
ui: &mut egui::Ui,
|
||||
images: &mut Images,
|
||||
profile: Option<&ProfileRecord>,
|
||||
checked: &mut bool,
|
||||
) -> bool {
|
||||
let (card_rect, card_resp) =
|
||||
ui.allocate_exact_size(vec2(ui.available_width(), 48.0), egui::Sense::click());
|
||||
|
||||
let mut clicked_response = card_resp;
|
||||
|
||||
let checkbox_size = {
|
||||
let mut size = egui::Vec2::splat(ui.spacing().interact_size.y);
|
||||
size.y = size.y.max(ui.spacing().icon_width);
|
||||
size
|
||||
};
|
||||
|
||||
let (checkbox_section_rect, remaining_rect) =
|
||||
card_rect.split_left_right_at_x(card_rect.left() + checkbox_size.x + 8.0);
|
||||
|
||||
let checkbox_rect = egui::Rect::from_center_size(checkbox_section_rect.center(), checkbox_size);
|
||||
|
||||
let resp = ui.allocate_new_ui(UiBuilder::new().max_rect(checkbox_rect), |ui| {
|
||||
ui.add(Checkbox::without_text(checked));
|
||||
});
|
||||
ui.advance_cursor_after_rect(checkbox_rect);
|
||||
|
||||
clicked_response = clicked_response.union(resp.response);
|
||||
|
||||
let (pfp_rect, body_rect) = remaining_rect.split_left_right_at_x(remaining_rect.left() + 48.0);
|
||||
|
||||
let _ = ui.allocate_new_ui(UiBuilder::new().max_rect(pfp_rect), |ui| {
|
||||
let pfp_resp = ui.add(
|
||||
&mut ProfilePic::new(images, get_profile_url(profile))
|
||||
.sense(Sense::click())
|
||||
.size(48.0),
|
||||
);
|
||||
|
||||
clicked_response = clicked_response.union(pfp_resp);
|
||||
});
|
||||
ui.advance_cursor_after_rect(pfp_rect);
|
||||
|
||||
let (_, body_rect) = body_rect.split_left_right_at_x(body_rect.left() + 8.0);
|
||||
|
||||
let (name_rect, description_rect) = body_rect.split_top_bottom_at_fraction(0.5);
|
||||
|
||||
let resp = ui.allocate_new_ui(UiBuilder::new().max_rect(name_rect), |ui| {
|
||||
let name = get_display_name(profile);
|
||||
|
||||
let painter = ui.painter_at(name_rect);
|
||||
|
||||
let mut left_x_pos = name_rect.left();
|
||||
|
||||
if let Some(disp) = name.display_name {
|
||||
let galley = painter.layout_no_wrap(
|
||||
disp.to_owned(),
|
||||
NotedeckTextStyle::Heading3.get_font_id(ui.ctx()),
|
||||
ui.visuals().text_color(),
|
||||
);
|
||||
|
||||
left_x_pos += galley.rect.width() + 4.0;
|
||||
|
||||
painter.galley(name_rect.min, galley, ui.visuals().text_color());
|
||||
}
|
||||
|
||||
if let Some(username) = name.username {
|
||||
let galley = painter.layout_no_wrap(
|
||||
format!("@{username}"),
|
||||
NotedeckTextStyle::Body.get_font_id(ui.ctx()),
|
||||
crate::colors::MID_GRAY,
|
||||
);
|
||||
|
||||
let pos = {
|
||||
let mut pos = name_rect.min;
|
||||
pos.x = left_x_pos;
|
||||
|
||||
let padding = name_rect.height() - galley.rect.height();
|
||||
|
||||
pos.y += padding / 2.0;
|
||||
|
||||
pos
|
||||
};
|
||||
painter.galley(pos, galley, ui.visuals().text_color());
|
||||
}
|
||||
});
|
||||
ui.advance_cursor_after_rect(name_rect);
|
||||
clicked_response = clicked_response.union(resp.response);
|
||||
|
||||
let resp = ui.allocate_new_ui(UiBuilder::new().max_rect(description_rect), |ui| 's: {
|
||||
let Some(record) = profile else {
|
||||
break 's;
|
||||
};
|
||||
|
||||
let Some(ndb_profile) = record.record().profile() else {
|
||||
break 's;
|
||||
};
|
||||
|
||||
let Some(about) = ndb_profile.about() else {
|
||||
break 's;
|
||||
};
|
||||
|
||||
ui.add(
|
||||
egui::Label::new(
|
||||
RichText::new(about).size(get_font_size(ui.ctx(), &NotedeckTextStyle::Heading4)),
|
||||
)
|
||||
.selectable(false)
|
||||
.truncate(),
|
||||
);
|
||||
});
|
||||
|
||||
ui.advance_cursor_after_rect(description_rect);
|
||||
|
||||
clicked_response = clicked_response.union(resp.response);
|
||||
|
||||
clicked_response.clicked()
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Nip51SetUiCache {
|
||||
state: HashMap<String, Nip51SetUiState>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Nip51SetUiState {
|
||||
select_all: bool,
|
||||
select_pk: HashMap<Pubkey, bool>,
|
||||
}
|
||||
|
||||
impl Nip51SetUiCache {
|
||||
pub fn get_pk_selected_state(&mut self, identifier: &str, pk: &Pubkey) -> &mut bool {
|
||||
let pack_state = match self.state.raw_entry_mut().from_key(identifier) {
|
||||
RawEntryMut::Occupied(entry) => entry.into_mut(),
|
||||
RawEntryMut::Vacant(entry) => {
|
||||
let (_, pack_state) =
|
||||
entry.insert(identifier.to_owned(), Nip51SetUiState::default());
|
||||
|
||||
pack_state
|
||||
}
|
||||
};
|
||||
match pack_state.select_pk.raw_entry_mut().from_key(pk) {
|
||||
RawEntryMut::Occupied(entry) => entry.into_mut(),
|
||||
RawEntryMut::Vacant(entry) => {
|
||||
let (_, state) = entry.insert(*pk, false);
|
||||
state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_select_all_state(&mut self, identifier: &str) -> &mut bool {
|
||||
match self.state.raw_entry_mut().from_key(identifier) {
|
||||
RawEntryMut::Occupied(entry) => &mut entry.into_mut().select_all,
|
||||
RawEntryMut::Vacant(entry) => {
|
||||
let (_, pack_state) =
|
||||
entry.insert(identifier.to_owned(), Nip51SetUiState::default());
|
||||
|
||||
&mut pack_state.select_all
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_all_selected(&self) -> Vec<Pubkey> {
|
||||
let mut pks = Vec::new();
|
||||
|
||||
for pack in self.state.values() {
|
||||
for (pk, select_state) in &pack.select_pk {
|
||||
if !*select_state {
|
||||
continue;
|
||||
}
|
||||
|
||||
pks.push(*pk);
|
||||
}
|
||||
}
|
||||
|
||||
pks
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user