diff --git a/android.sh b/android.sh index 73f6e0e..13de478 100755 --- a/android.sh +++ b/android.sh @@ -2,15 +2,15 @@ git clone https://github.com/v0l/ffmpeg-kit.git export ANDROID_SDK_ROOT=$ANDROID_HOME -cd ffmpeg-kit && ./android.sh \ - --disable-x86 \ - --disable-x86-64 \ - --disable-arm-v7a \ - --disable-arm-v7a-neon \ - --enable-openssl \ - --api-level=28 \ - --no-ffmpeg-kit-protocols \ - --no-archive +#cd ffmpeg-kit && ./android.sh \ +# --disable-x86 \ +# --disable-x86-64 \ +# --disable-arm-v7a \ +# --disable-arm-v7a-neon \ +# --enable-openssl \ +# --api-level=28 \ +# --no-ffmpeg-kit-protocols \ +# --no-archive NDK_VER="28.0.12433566" ARCH="arm64" @@ -20,6 +20,8 @@ export FFMPEG_DIR="$(pwd)/ffmpeg-kit/prebuilt/$PLATFORM-$ARCH/ffmpeg" export PKG_CONFIG_SYSROOT_DIR="$(pwd)/ffmpeg-kit/prebuilt/$PLATFORM-$ARCH/pkgconfig" # DIRTY HACK !! +mkdir -p ./target/x/debug/android/$ARCH/cargo/$TRIPLET/release/deps +mkdir -p ./target/x/release/android/$ARCH/cargo/$TRIPLET/release/deps cp "$ANDROID_HOME/ndk/$NDK_VER/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/$TRIPLET/35/libcamera2ndk.so" \ ./target/x/debug/android/$ARCH/cargo/$TRIPLET/debug/deps cp "$ANDROID_HOME/ndk/$NDK_VER/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/$TRIPLET/35/libcamera2ndk.so" \ diff --git a/src/app.rs b/src/app.rs index 72bcf57..a549329 100644 --- a/src/app.rs +++ b/src/app.rs @@ -60,6 +60,7 @@ impl App for ZapStreamApp { egui::CentralPanel::default() .frame(app_frame) .show(ctx, |ui| { + ui.visuals_mut().override_text_color = Some(Color32::WHITE); self.router.show(ui); }); } diff --git a/src/lib.rs b/src/lib.rs index d9c2ff4..49c5457 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,8 +8,10 @@ mod note_util; mod route; mod services; mod stream_info; -pub mod widgets; -pub mod theme; +mod widgets; +mod theme; +mod note_store; + #[cfg(target_os = "android")] use winit::platform::android::activity::AndroidApp; @@ -31,7 +33,7 @@ pub async fn android_main(app: AndroidApp) { builder.with_android_app(app_clone_for_event_loop); })); - let external_data_path = app + let data_path = app .external_data_path() .expect("external data path") .to_path_buf(); @@ -39,6 +41,6 @@ pub async fn android_main(app: AndroidApp) { let _res = eframe::run_native( "zap.stream", options, - Box::new(move |cc| Ok(Box::new(ZapStreamApp::new(cc, external_data_path)))), + Box::new(move |cc| Ok(Box::new(ZapStreamApp::new(cc, data_path)))), ); } \ No newline at end of file diff --git a/src/note_store.rs b/src/note_store.rs new file mode 100644 index 0000000..ff98785 --- /dev/null +++ b/src/note_store.rs @@ -0,0 +1,48 @@ +use crate::link::NostrLink; +use nostrdb::Note; +use std::borrow::Borrow; +use std::collections::HashMap; + +pub struct NoteStore<'a> { + events: HashMap>, +} + +impl<'a> NoteStore<'a> { + pub fn new() -> Self { + Self { + events: HashMap::new() + } + } + + pub fn from_vec(events: Vec>) -> Self { + let mut store = Self::new(); + for note in events { + store.add(note); + } + store + } + + pub fn add(&mut self, note: Note<'a>) -> Option> { + let k = Self::key(¬e); + if let Some(v) = self.events.get(&k) { + if v.created_at() < note.created_at() { + return self.events.insert(k, note); + } + } + self.events.insert(k, note) + } + + pub fn remove(&mut self, note: &Note<'a>) -> Option> { + self.events.remove(&Self::key(note)) + } + + pub fn key(note: &Note<'a>) -> String { + NostrLink::from_note(note) + .to_tag_value() + } + + pub fn iter(&self) -> impl Iterator> { + self.events.values() + } +} + diff --git a/src/route/home.rs b/src/route/home.rs index e16e820..a73dd43 100644 --- a/src/route/home.rs +++ b/src/route/home.rs @@ -1,3 +1,4 @@ +use crate::note_store::NoteStore; use crate::note_util::OwnedNote; use crate::route::RouteServices; use crate::services::ndb_wrapper::{NDBWrapper, SubWrapper}; @@ -39,6 +40,7 @@ impl NostrWidget for HomePage { .map_while(|f| f.map_or(None, |f| Some(f))) .collect(); + let events = NoteStore::from_vec(events); ScrollArea::vertical() .show(ui, |ui| { widgets::StreamList::new(&events, &services).ui(ui) diff --git a/src/route/login.rs b/src/route/login.rs new file mode 100644 index 0000000..090a638 --- /dev/null +++ b/src/route/login.rs @@ -0,0 +1,46 @@ +use crate::route::{RouteAction, RouteServices, Routes}; +use crate::widgets::{Button, NostrWidget}; +use egui::{Color32, Response, RichText, Ui}; +use nostr_sdk::util::hex; + +pub struct LoginPage { + key: String, + error: Option, +} + +impl LoginPage { + pub fn new() -> Self { + Self { + key: String::new(), + error: None, + } + } +} + +impl NostrWidget for LoginPage { + fn render(&mut self, ui: &mut Ui, services: &RouteServices<'_>) -> Response { + ui.vertical_centered(|ui| { + ui.spacing_mut().item_spacing.y = 8.; + + ui.label(RichText::new("Login").size(32.)); + ui.label("Pubkey"); + ui.text_edit_singleline(&mut self.key); + if Button::new() + .show(ui, |ui| { + ui.label("Login") + }).clicked() { + if let Ok(pk) = hex::decode(&self.key) { + if let Ok(pk) = pk.as_slice().try_into() { + services.action(RouteAction::LoginPubkey(pk)); + services.navigate(Routes::HomePage); + return; + } + } + self.error = Some("Invalid pubkey".to_string()); + } + if let Some(e) = &self.error { + ui.label(RichText::new(e).color(Color32::RED)); + } + }).response + } +} \ No newline at end of file diff --git a/src/route/mod.rs b/src/route/mod.rs index 4e4e8bd..406f8fe 100644 --- a/src/route/mod.rs +++ b/src/route/mod.rs @@ -2,6 +2,7 @@ use crate::link::NostrLink; use crate::note_util::OwnedNote; use crate::route; use crate::route::home::HomePage; +use crate::route::login::LoginPage; use crate::route::stream::StreamPage; use crate::services::image_cache::ImageCache; use crate::services::ndb_wrapper::NDBWrapper; @@ -18,11 +19,12 @@ use std::path::PathBuf; mod home; mod stream; +mod login; #[derive(PartialEq)] pub enum Routes { HomePage, - Event { + EventPage { link: NostrLink, event: Option, }, @@ -30,6 +32,7 @@ pub enum Routes { link: NostrLink, profile: Option, }, + LoginPage, // special kind for modifying route state Action(RouteAction), @@ -37,7 +40,8 @@ pub enum Routes { #[derive(PartialEq)] pub enum RouteAction { - Login([u8; 32]), + /// Login with public key + LoginPubkey([u8; 32]), } pub struct Router { @@ -72,10 +76,14 @@ impl Router { let w = HomePage::new(&self.ndb, tx); self.current_widget = Some(Box::new(w)); } - Routes::Event { link, .. } => { + Routes::EventPage { link, .. } => { let w = StreamPage::new_from_link(&self.ndb, tx, link.clone()); self.current_widget = Some(Box::new(w)); } + Routes::LoginPage => { + let w = LoginPage::new(); + self.current_widget = Some(Box::new(w)); + } _ => warn!("Not implemented"), } self.current = route; @@ -85,10 +93,11 @@ impl Router { let tx = self.ndb.start_transaction(); // handle app state changes - while let Some(r) = self.router.read(ui).next() { + let mut q = self.router.read(ui); + while let Some(r) = q.next() { if let Routes::Action(a) = &r { match a { - RouteAction::Login(k) => self.login = Some(k.clone()), + RouteAction::LoginPubkey(k) => self.login = Some(k.clone()), _ => info!("Not implemented"), } } else { @@ -138,4 +147,10 @@ impl<'a> RouteServices<'a> { warn!("Failed to navigate"); } } + + pub fn action(&self, route: RouteAction) { + if let Err(e) = self.router.send(Routes::Action(route)) { + warn!("Failed to navigate"); + } + } } diff --git a/src/route/stream.rs b/src/route/stream.rs index 0c35383..b57073d 100644 --- a/src/route/stream.rs +++ b/src/route/stream.rs @@ -74,10 +74,12 @@ impl NostrWidget for StreamPage { let h = ui.available_height(); ui.allocate_ui(Vec2::new(w, h - chat_h), |ui| { if let Some(c) = self.chat.as_mut() { - c.render(ui, services) + c.render(ui, services); } else { - ui.label("Loading..") + ui.label("Loading.."); } + // consume rest of space + ui.add_space(ui.available_height()); }); ui.allocate_ui(Vec2::new(w, chat_h), |ui| { self.new_msg.render(ui, services) diff --git a/src/services/ndb_wrapper.rs b/src/services/ndb_wrapper.rs index 1993c11..11a52fe 100644 --- a/src/services/ndb_wrapper.rs +++ b/src/services/ndb_wrapper.rs @@ -7,7 +7,8 @@ use nostrdb::{ Error, Filter, Ndb, NdbProfile, Note, NoteKey, ProfileRecord, QueryResult, Subscription, Transaction, }; -use std::sync::{Arc, RwLock}; +use std::collections::HashSet; +use std::sync::{Arc, Mutex, RwLock}; use tokio::sync::mpsc::UnboundedSender; pub struct NDBWrapper { @@ -15,6 +16,7 @@ pub struct NDBWrapper { ndb: Ndb, client: Client, query_manager: QueryManager, + profiles: Mutex>, } /// Automatic cleanup for subscriptions @@ -70,6 +72,7 @@ impl NDBWrapper { ndb, client, query_manager: qm, + profiles: Mutex::new(HashSet::new()), } } @@ -144,11 +147,13 @@ impl NDBWrapper { // TODO: fix this shit if p.is_none() { - self.query_manager.queue_query("profile", &[ - nostr::Filter::new() - .kinds([Kind::Metadata]) - .authors([PublicKey::from_slice(pubkey).unwrap()]) - ]) + if self.profiles.lock().unwrap().insert(*pubkey) { + self.query_manager.queue_query("profile", &[ + nostr::Filter::new() + .kinds([Kind::Metadata]) + .authors([PublicKey::from_slice(pubkey).unwrap()]) + ]) + } } let sub = None; (p, sub) diff --git a/src/services/query.rs b/src/services/query.rs index 7ad686b..bba4a6e 100644 --- a/src/services/query.rs +++ b/src/services/query.rs @@ -64,6 +64,12 @@ impl Query { let now = Utc::now(); let id = Uuid::new_v4(); + // remove filters already sent + next = next + .into_iter() + .filter(|f| self.traces.len() == 0 || !self.traces.iter().all(|y| y.filters.iter().any(|z| z == f))) + .collect(); + // force profile queries into single filter if next.iter().all(|f| if let Some(k) = &f.kinds { k.len() == 1 && k.first().unwrap().as_u16() == 0 @@ -76,11 +82,6 @@ impl Query { ] } - // remove filters already sent - next = next - .into_iter() - .filter(|f| !self.traces.iter().any(|y| y.filters.iter().any(|z| z.eq(f)))) - .collect(); if next.len() == 0 { return None; diff --git a/src/stream_info.rs b/src/stream_info.rs index f303c36..9a6b159 100644 --- a/src/stream_info.rs +++ b/src/stream_info.rs @@ -11,6 +11,12 @@ pub trait StreamInfo { fn stream(&self) -> Option<&str>; fn starts(&self) -> u64; + + fn image(&self) -> Option<&str>; + + fn status(&self) -> Option<&str>; + + fn viewers(&self) -> Option; } impl<'a> StreamInfo for Note<'a> { @@ -50,6 +56,7 @@ impl<'a> StreamInfo for Note<'a> { } } + fn starts(&self) -> u64 { if let Some(s) = self.get_tag_value("starts") { s.variant().str() @@ -58,4 +65,29 @@ impl<'a> StreamInfo for Note<'a> { self.created_at() } } + + fn image(&self) -> Option<&str> { + if let Some(s) = self.get_tag_value("image") { + s.variant().str() + } else { + None + } + } + + fn status(&self) -> Option<&str> { + if let Some(s) = self.get_tag_value("status") { + s.variant().str() + } else { + None + } + } + + fn viewers(&self) -> Option { + if let Some(s) = self.get_tag_value("current_participants") { + s.variant().str() + .map_or(None, |v| Some(v.parse::().unwrap_or(0))) + } else { + None + } + } } diff --git a/src/theme.rs b/src/theme.rs index 2106f7f..0a26a26 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -1,5 +1,7 @@ use egui::Color32; +pub const FONT_SIZE: f32 = 13.0; pub const PRIMARY: Color32 = Color32::from_rgb(248, 56, 217); pub const NEUTRAL_500: Color32 = Color32::from_rgb(115, 115, 115); +pub const NEUTRAL_800: Color32 = Color32::from_rgb(38, 38, 38); pub const NEUTRAL_900: Color32 = Color32::from_rgb(23, 23, 23); \ No newline at end of file diff --git a/src/widgets/avatar.rs b/src/widgets/avatar.rs index e48305c..f011109 100644 --- a/src/widgets/avatar.rs +++ b/src/widgets/avatar.rs @@ -27,7 +27,7 @@ impl<'a> Avatar<'a> { } } - pub fn from_profile(p: Option>, svc: &'a ImageCache) -> Self { + pub fn from_profile(p: &'a Option>, svc: &'a ImageCache) -> Self { let img = p .map_or(None, |f| f.picture().map(|f| svc.load(f))); Self { diff --git a/src/widgets/button.rs b/src/widgets/button.rs new file mode 100644 index 0000000..bf82494 --- /dev/null +++ b/src/widgets/button.rs @@ -0,0 +1,32 @@ +use crate::theme::NEUTRAL_800; +use egui::{Color32, CursorIcon, Frame, Margin, Response, Sense, Ui}; + +pub struct Button { + color: Color32, +} + +impl Button { + pub fn new() -> Self { + Self { + color: NEUTRAL_800 + } + } + + pub fn show(self, ui: &mut Ui, add_contents: F) -> Response + where + F: FnOnce(&mut Ui) -> Response, + { + let r = Frame::none() + .inner_margin(Margin::symmetric(12., 8.)) + .fill(self.color) + .rounding(12.) + .show(ui, add_contents); + + let id = r.response.id; + ui.interact( + r.response.on_hover_and_drag_cursor(CursorIcon::PointingHand).rect, + id, + Sense::click(), + ) + } +} \ No newline at end of file diff --git a/src/widgets/chat_message.rs b/src/widgets/chat_message.rs index ca74db2..ea7db81 100644 --- a/src/widgets/chat_message.rs +++ b/src/widgets/chat_message.rs @@ -25,6 +25,8 @@ impl<'a> Widget for ChatMessage<'a> { fn ui(self, ui: &mut Ui) -> Response { ui.horizontal_wrapped(|ui| { let mut job = LayoutJob::default(); + // TODO: avoid this somehow + job.wrap.break_anywhere = true; let is_host = self.stream.host().eq(self.ev.pubkey()); let profile = self.services.ndb.get_profile_by_pubkey(self.services.tx, self.ev.pubkey()) @@ -48,7 +50,7 @@ impl<'a> Widget for ChatMessage<'a> { format.color = Color32::WHITE; job.append(self.ev.content(), 5.0, format.clone()); - ui.add(Avatar::from_profile(profile ,self.services.img_cache).size(24.)); + ui.add(Avatar::from_profile(&profile ,self.services.img_cache).size(24.)); ui.add(Label::new(job) .wrap_mode(TextWrapMode::Wrap) ); diff --git a/src/widgets/header.rs b/src/widgets/header.rs index 1a86421..fddf8c2 100644 --- a/src/widgets/header.rs +++ b/src/widgets/header.rs @@ -1,9 +1,9 @@ use crate::route::{RouteServices, Routes}; use crate::widgets::avatar::Avatar; -use crate::widgets::NostrWidget; +use crate::widgets::{Button, NostrWidget}; use eframe::emath::Align; use eframe::epaint::Vec2; -use egui::{Frame, Image, Layout, Margin, Response, Sense, Ui, Widget}; +use egui::{CursorIcon, Frame, Image, Layout, Margin, Response, Sense, Ui, Widget}; pub struct Header; @@ -28,13 +28,24 @@ impl NostrWidget for Header { .max_height(24.) .sense(Sense::click()) .ui(ui) + .on_hover_and_drag_cursor(CursorIcon::PointingHand) .clicked() { services.navigate(Routes::HomePage); } - if let Some(pk) = services.login { - //ui.add(Avatar::pubkey(pk, services)); - } + + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + if let Some(pk) = services.login { + ui.add(Avatar::pubkey(pk, services)); + } else { + if Button::new() + .show(ui, |ui| { + ui.label("Login") + }).clicked() { + services.navigate(Routes::LoginPage); + } + } + }); }, ) }) diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index f511e8b..ef894f9 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -9,6 +9,8 @@ mod stream_player; mod video_placeholder; mod stream_title; mod write_chat; +mod username; +mod button; use crate::route::RouteServices; use egui::{Response, Ui}; @@ -26,3 +28,5 @@ pub use self::stream_player::StreamPlayer; pub use self::video_placeholder::VideoPlaceholder; pub use self::stream_title::StreamTitle; pub use self::write_chat::WriteChat; +pub use self::username::Username; +pub use self::button::Button; \ No newline at end of file diff --git a/src/widgets/profile.rs b/src/widgets/profile.rs index f6f59c9..3196761 100644 --- a/src/widgets/profile.rs +++ b/src/widgets/profile.rs @@ -1,9 +1,10 @@ use crate::route::RouteServices; use crate::services::image_cache::ImageCache; use crate::services::ndb_wrapper::SubWrapper; -use crate::widgets::Avatar; -use egui::{Color32, Label, Response, RichText, TextWrapMode, Ui, Widget}; +use crate::widgets::{Avatar, Username}; +use egui::{Response, Ui, Widget}; use nostrdb::NdbProfile; +use crate::theme::FONT_SIZE; pub struct Profile<'a> { size: f32, @@ -36,13 +37,8 @@ impl<'a> Widget for Profile<'a> { ui.horizontal(|ui| { ui.spacing_mut().item_spacing.x = 8.; - ui.add(Avatar::from_profile(self.profile, self.img_cache).size(self.size)); - - let name = self - .profile - .map_or("Nostrich", |f| f.name().map_or("Nostrich", |f| f)); - let name = RichText::new(name).size(13.).color(Color32::WHITE); - ui.add(Label::new(name).wrap_mode(TextWrapMode::Truncate)); + ui.add(Avatar::from_profile(&self.profile, self.img_cache).size(self.size)); + ui.add(Username::new(&self.profile, FONT_SIZE)) }).response } } diff --git a/src/widgets/stream_list.rs b/src/widgets/stream_list.rs index a0af011..93167d9 100644 --- a/src/widgets/stream_list.rs +++ b/src/widgets/stream_list.rs @@ -1,15 +1,17 @@ +use crate::note_store::NoteStore; use crate::route::RouteServices; +use crate::stream_info::StreamInfo; use crate::widgets::stream_tile::StreamEvent; use egui::{Frame, Margin, Response, Ui, Widget}; -use nostrdb::Note; +use itertools::Itertools; pub struct StreamList<'a> { - streams: &'a Vec>, + streams: &'a NoteStore<'a>, services: &'a RouteServices<'a>, } impl<'a> StreamList<'a> { - pub fn new(streams: &'a Vec>, services: &'a RouteServices) -> Self { + pub fn new(streams: &'a NoteStore<'a>, services: &'a RouteServices) -> Self { Self { streams, services } } } @@ -21,7 +23,10 @@ impl Widget for StreamList<'_> { .show(ui, |ui| { ui.vertical(|ui| { ui.style_mut().spacing.item_spacing = egui::vec2(0., 20.0); - for event in self.streams { + for event in self.streams.iter() + .sorted_by(|a, b| { + a.starts().cmp(&b.starts()) + }) { ui.add(StreamEvent::new(event, self.services)); } }) diff --git a/src/widgets/stream_tile.rs b/src/widgets/stream_tile.rs index 5d08896..3f743bd 100644 --- a/src/widgets/stream_tile.rs +++ b/src/widgets/stream_tile.rs @@ -1,32 +1,24 @@ use crate::link::NostrLink; -use crate::note_util::NoteUtil; use crate::route::{RouteServices, Routes}; use crate::stream_info::StreamInfo; +use crate::theme::{NEUTRAL_500, NEUTRAL_900, PRIMARY}; use crate::widgets::avatar::Avatar; -use crate::widgets::VideoPlaceholder; -use eframe::epaint::Vec2; -use egui::{Color32, Image, Label, Response, RichText, Rounding, Sense, TextWrapMode, Ui, Widget}; -use nostrdb::{NdbProfile, Note}; +use eframe::epaint::{Rounding, Vec2}; +use egui::epaint::RectShape; +use egui::load::TexturePoll; +use egui::{vec2, Color32, CursorIcon, FontId, Label, Pos2, Rect, Response, RichText, Sense, TextWrapMode, Ui, Widget}; +use image::Pixel; +use nostrdb::Note; pub struct StreamEvent<'a> { event: &'a Note<'a>, - picture: Option>, services: &'a RouteServices<'a>, } impl<'a> StreamEvent<'a> { pub fn new(event: &'a Note<'a>, services: &'a RouteServices) -> Self { - let image = event.get_tag_value("image"); - let cover = match image { - Some(i) => match i.variant().str() { - Some(i) => Some(services.img_cache.load(i)), - None => None, - }, - None => None, - }; Self { event, - picture: cover, services, } } @@ -41,24 +33,65 @@ impl Widget for StreamEvent<'_> { let w = ui.available_width(); let h = (w / 16.0) * 9.0; - let img_size = Vec2::new(w, h); + let cover = self.event.image() + .map(|p| self.services.img_cache.load(p)); - let img = match self.picture { - Some(picture) => picture - .fit_to_exact_size(img_size) - .rounding(Rounding::same(12.)) - .sense(Sense::click()) - .ui(ui), - None => VideoPlaceholder.ui(ui), + let (response, painter) = ui.allocate_painter(Vec2::new(w, h), Sense::click()); + + if let Some(cover) = cover.map(|c| + c.rounding(Rounding::same(12.)) + .load_for_size(painter.ctx(), Vec2::new(w, h))) { + match cover { + Ok(TexturePoll::Ready { texture }) => { + painter.add(RectShape { + rect: response.rect, + rounding: Rounding::same(12.), + fill: Color32::WHITE, + stroke: Default::default(), + blur_width: 0.0, + fill_texture_id: texture.id, + uv: Rect::from_min_max(Pos2::new(0.0, 0.0), Pos2::new(1.0, 1.0)), + }); + } + _ => { + painter.rect_filled(response.rect, 12., NEUTRAL_500); + } + } + } else { + painter.rect_filled(response.rect, 12., NEUTRAL_500); + } + + let overlay_label_pad = Vec2::new(5., 5.); + let live_label_text = self.event.status().unwrap_or("live").to_string().to_uppercase(); + let live_label_color = if live_label_text == "LIVE" { + PRIMARY + } else { + NEUTRAL_900 }; - if img.clicked() { - self.services.navigate(Routes::Event { + let live_label = painter.layout_no_wrap(live_label_text, FontId::default(), Color32::WHITE); + + let overlay_react = response.rect.shrink(8.0); + let live_label_pos = overlay_react.min + vec2(overlay_react.width() - live_label.rect.width() - (overlay_label_pad.x * 2.), 0.0); + let live_label_background = Rect::from_two_pos(live_label_pos, live_label_pos + live_label.size() + (overlay_label_pad * 2.)); + painter.rect_filled(live_label_background, 8., live_label_color); + painter.galley(live_label_pos + overlay_label_pad, live_label, Color32::PLACEHOLDER); + + if let Some(viewers) = self.event.viewers() { + let viewers_label = painter.layout_no_wrap(format!("{} viewers", viewers), FontId::default(), Color32::WHITE); + let rect_start = overlay_react.max - viewers_label.size() - (overlay_label_pad * 2.0); + let pos = Rect::from_two_pos(rect_start, overlay_react.max); + painter.rect_filled(pos, 8., NEUTRAL_900); + painter.galley(rect_start + overlay_label_pad, viewers_label, Color32::PLACEHOLDER); + } + let response = response.on_hover_and_drag_cursor(CursorIcon::PointingHand); + if response.clicked() { + self.services.navigate(Routes::EventPage { link: NostrLink::from_note(&self.event), event: None, }); } ui.horizontal(|ui| { - ui.add(Avatar::from_profile(host_profile, self.services.img_cache).size(40.)); + ui.add(Avatar::from_profile(&host_profile, self.services.img_cache).size(40.)); let title = RichText::new(self.event.title().unwrap_or("Untitled")) .size(16.) .color(Color32::WHITE); diff --git a/src/widgets/username.rs b/src/widgets/username.rs new file mode 100644 index 0000000..f3d4be1 --- /dev/null +++ b/src/widgets/username.rs @@ -0,0 +1,23 @@ +use egui::{Color32, Label, Response, RichText, TextWrapMode, Ui, Widget}; +use nostrdb::NdbProfile; + +pub struct Username<'a> { + profile: &'a Option>, + size: f32, +} + +impl<'a> Username<'a> { + pub fn new(profile: &'a Option>, size: f32) -> Self { + Self { profile, size } + } +} + +impl<'a> Widget for Username<'a> { + fn ui(self, ui: &mut Ui) -> Response { + let name = self + .profile + .map_or("Nostrich", |f| f.name().map_or("Nostrich", |f| f)); + let name = RichText::new(name).size(self.size).color(Color32::WHITE); + ui.add(Label::new(name).wrap_mode(TextWrapMode::Truncate)) + } +} \ No newline at end of file diff --git a/src/widgets/video_placeholder.rs b/src/widgets/video_placeholder.rs index 9165f20..c511dfa 100644 --- a/src/widgets/video_placeholder.rs +++ b/src/widgets/video_placeholder.rs @@ -1,4 +1,4 @@ -use egui::{Color32, Rect, Response, Rounding, Sense, Ui, Vec2, Widget}; +use egui::{Color32, Response, Rounding, Sense, Ui, Vec2, Widget}; pub struct VideoPlaceholder; @@ -10,7 +10,7 @@ impl Widget for VideoPlaceholder { let (response, painter) = ui.allocate_painter(img_size, Sense::click()); painter.rect_filled( - Rect::EVERYTHING, + response.rect, Rounding::same(12.), Color32::from_rgb(200, 200, 200), );