setup routes

This commit is contained in:
kieran 2024-10-10 20:40:25 +01:00
parent abfcbc8954
commit 67d6381123
No known key found for this signature in database
GPG Key ID: DE71CEB3925BE941
20 changed files with 1194 additions and 490 deletions

986
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -6,12 +6,15 @@ edition = "2021"
[dependencies]
tokio = "1.40.0"
egui = { git = "https://github.com/emilk/egui", rev = "fcb7764e48ce00f8f8e58da10f937410d65b0bfb" }
eframe = { git = "https://github.com/emilk/egui", rev = "fcb7764e48ce00f8f8e58da10f937410d65b0bfb", package = "eframe", default-features = false, features = ["wgpu", "wayland", "x11", "android-native-activity"] }
nostrdb = { git = "https://github.com/damus-io/nostrdb-rs", rev = "6d22af6d5159be4c9e4579f8c9d3af836e0d470a" }
nostr-sdk = { version = "0.34.0", features = ["all-nips"] }
egui_nav = { git = "https://github.com/damus-io/egui-nav", branch = "egui-0.28" }
egui_extras = { git = "https://github.com/emilk/egui", rev = "fcb7764e48ce00f8f8e58da10f937410d65b0bfb", package = "egui_extras", features = ["all_loaders"] }
egui = { version = "0.29.1" }
eframe = { version = "0.29.1", default-features = false, features = ["glow", "wgpu", "wayland", "x11", "android-native-activity"] }
nostrdb = { git = "https://github.com/damus-io/nostrdb-rs", version = "0.3.4" }
nostr-sdk = { version = "0.35.0", features = ["all-nips"] }
egui_extras = { version = "0.29.1", features = ["all_loaders"] }
image = { version = "0.25", features = ["jpeg", "png", "webp"] }
log = "0.4.22"
pretty_env_logger = "0.5.0"
egui_inbox = "0.6.0"
bech32 = "0.11.0"
libc = "0.2.158"
egui-video = { path = "../egui-video" }

View File

@ -1,20 +1,18 @@
use crate::services::Services;
use crate::widget::NostrWidget;
use crate::widgets::header::Header;
use crate::widgets::stream_list::StreamList;
use crate::route::Router;
use eframe::{App, CreationContext, Frame};
use egui::{Color32, Context, ScrollArea};
use egui_inbox::UiInbox;
use log::warn;
use nostr_sdk::database::{MemoryDatabase, MemoryDatabaseOptions};
use nostr_sdk::{Client, Event, Filter, Kind, RelayPoolNotification};
use nostrdb::{Config, Ndb, Transaction};
use nostr_sdk::{Client, Filter, JsonUtil, Kind, RelayPoolNotification};
use nostrdb::{Config, Ndb};
use tokio::sync::broadcast;
pub struct ZapStreamApp {
ndb: Ndb,
client: Client,
notifications: broadcast::Receiver<RelayPoolNotification>,
events: Vec<Event>,
services: Services,
router: Router,
}
impl ZapStreamApp {
@ -41,12 +39,13 @@ impl ZapStreamApp {
});
egui_extras::install_image_loaders(&cc.egui_ctx);
let inbox = UiInbox::new();
let ndb = Ndb::new(".", &Config::default()).unwrap();
Self {
ndb: Ndb::new(".", &Config::default()).unwrap(),
ndb: ndb.clone(),
client: client.clone(),
notifications,
services: Services::new(client, cc.egui_ctx.clone()),
events: vec![],
router: Router::new(inbox, cc.egui_ctx.clone(), client.clone(), ndb.clone()),
}
}
@ -54,7 +53,9 @@ impl ZapStreamApp {
while let Ok(msg) = self.notifications.try_recv() {
match msg {
RelayPoolNotification::Event { event, .. } => {
self.events.push(*event);
if let Err(e) = self.ndb.process_event(event.as_json().as_str()) {
warn!("Failed to process event: {:?}", e);
}
}
_ => {
// dont care
@ -66,20 +67,17 @@ impl ZapStreamApp {
impl App for ZapStreamApp {
fn update(&mut self, ctx: &Context, frame: &mut Frame) {
let txn = Transaction::new(&self.ndb).expect("txn");
self.process_nostr();
let mut app_frame = egui::containers::Frame::default();
app_frame.stroke.color = Color32::BLACK;
ctx.set_debug_on_hover(true);
//ctx.set_debug_on_hover(true);
egui::CentralPanel::default()
.frame(app_frame)
.show(ctx, |ui| {
ScrollArea::vertical().show(ui, |ui| {
ui.add(Header::new(&self.services));
ui.add(StreamList::new(&self.events, &self.services));
});
self.router.show(ui)
});
}
}

119
src/link.rs Normal file
View File

@ -0,0 +1,119 @@
use crate::note_util::NoteUtil;
use bech32::{Hrp, NoChecksum};
use egui::TextBuffer;
use nostr_sdk::util::hex;
use nostrdb::{Filter, Note};
use std::fmt::{Display, Formatter};
#[derive(Clone)]
pub struct NostrLink {
hrp: NostrLinkType,
id: IdOrStr,
kind: Option<u32>,
author: Option<[u8; 32]>,
relays: Vec<String>,
}
#[derive(Clone)]
pub enum IdOrStr {
Id([u8; 32]),
Str(String),
}
#[derive(Clone)]
pub enum NostrLinkType {
Note,
PublicKey,
PrivateKey,
// TLV kinds
Event,
Profile,
Coordinate,
}
impl NostrLink {
pub fn new(hrp: NostrLinkType, id: IdOrStr, kind: Option<u32>, author: Option<[u8; 32]>, relays: Vec<String>) -> Self {
Self {
hrp,
id,
kind,
author,
relays,
}
}
pub fn from_note(note: &Note<'_>) -> Self {
if note.kind() >= 30_000 && note.kind() < 40_000 {
Self {
hrp: NostrLinkType::Coordinate,
id: IdOrStr::Str(note.get_tag_value("d").unwrap().variant().str().unwrap().to_string()),
kind: Some(note.kind()),
author: Some(note.pubkey().clone()),
relays: vec![],
}
} else {
Self {
hrp: NostrLinkType::Event,
id: IdOrStr::Id(note.id().clone()),
kind: Some(note.kind()),
author: Some(note.pubkey().clone()),
relays: vec![],
}
}
}
}
impl TryInto<Filter> for &NostrLink {
type Error = ();
fn try_into(self) -> Result<Filter, Self::Error> {
match self.hrp {
NostrLinkType::Coordinate => {
Ok(Filter::new()
.tags([match self.id {
IdOrStr::Str(ref s) => s.to_owned(),
IdOrStr::Id(ref i) => hex::encode(i)
}], 'd')
.build())
}
NostrLinkType::Event | NostrLinkType::Note => {
Ok(Filter::new().build())
}
_ => Err(())
}
}
}
impl Display for NostrLinkType {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Note => write!(f, "note"),
Self::PublicKey => write!(f, "npub"),
Self::PrivateKey => write!(f, "nsec"),
Self::Event => write!(f, "nevent"),
Self::Profile => write!(f, "nprofile"),
Self::Coordinate => write!(f, "naddr"),
}
}
}
impl NostrLinkType {
pub fn to_hrp(&self) -> Hrp {
let str = self.to_string();
Hrp::parse(str.as_str()).unwrap()
}
}
impl Display for NostrLink {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self.hrp {
NostrLinkType::Note | NostrLinkType::PrivateKey | NostrLinkType::PublicKey => {
Ok(bech32::encode_to_fmt::<NoChecksum, Formatter>(f, self.hrp.to_hrp(), match &self.id {
IdOrStr::Str(s) => s.as_bytes(),
IdOrStr::Id(i) => i
}).map_err(|e| std::fmt::Error)?)
}
NostrLinkType::Event | NostrLinkType::Profile | NostrLinkType::Coordinate => todo!()
}
}
}

View File

@ -1,16 +1,22 @@
use eframe::Renderer;
use crate::app::ZapStreamApp;
use egui::Vec2;
use nostrdb::Note;
mod app;
mod widget;
mod widgets;
pub mod widgets;
mod services;
mod route;
mod note_util;
mod link;
mod stream_info;
#[tokio::main]
async fn main() {
pretty_env_logger::init();
let mut options = eframe::NativeOptions::default();
options.renderer = Renderer::Glow;
options.viewport = options.viewport
.with_inner_size(Vec2::new(360., 720.));

73
src/note_util.rs Normal file
View File

@ -0,0 +1,73 @@
use libc::{malloc, memcpy};
use nostr_sdk::util::hex;
use nostrdb::{NdbStr, Note, Tag, Tags};
use std::fmt::Display;
use std::mem::transmute;
use std::{mem, ptr};
pub trait NoteUtil {
fn id_hex(&self) -> String;
fn get_tag_value(&self, key: &str) -> Option<NdbStr>;
fn find_tag_value<F>(&self, fx: F) -> Option<NdbStr>
where
F: Fn(Vec<NdbStr>) -> bool;
}
impl<'a> NoteUtil for Note<'a> {
fn id_hex(&self) -> String {
hex::encode(self.id())
}
fn get_tag_value(&self, key: &str) -> Option<NdbStr> {
self.find_tag_value(|t| t[0].variant().str() == Some(key))
}
fn find_tag_value<F>(&self, fx: F) -> Option<NdbStr>
where
F: Fn(Vec<NdbStr>) -> bool,
{
let tag = self.tags().iter().find(|t| {
let tag_vec = TagIterBorrow::new(t).collect();
fx(tag_vec)
});
if let Some(t) = tag {
t.get(1)
} else {
None
}
}
}
#[derive(Debug, Clone)]
pub struct TagIterBorrow<'a> {
tag: &'a Tag<'a>,
index: u16,
}
impl<'a> TagIterBorrow<'a> {
pub fn new(tag: &'a Tag<'a>) -> Self {
let index = 0;
TagIterBorrow { tag, index }
}
pub fn done(&self) -> bool {
self.index >= self.tag.count()
}
}
impl<'a> Iterator for TagIterBorrow<'a> {
type Item = NdbStr<'a>;
fn next(&mut self) -> Option<NdbStr<'a>> {
let tag = self.tag.get(self.index);
if tag.is_some() {
self.index += 1;
tag
} else {
None
}
}
}
pub struct OwnedNote;

27
src/route/home.rs Normal file
View File

@ -0,0 +1,27 @@
use crate::route::RouteServices;
use egui::{Response, Ui, Widget};
use nostrdb::{Filter, Note};
use crate::widgets;
pub struct HomePage<'a> {
services: &'a RouteServices<'a>,
}
impl<'a> HomePage<'a> {
pub fn new(services: &'a RouteServices) -> Self {
Self { services }
}
}
impl<'a> Widget for HomePage<'a> {
fn ui(self, ui: &mut Ui) -> Response {
let events = self.services.ndb.query(&self.services.tx, &[
Filter::new()
.kinds([30_311])
.limit(10)
.build()
], 10).unwrap();
let events: Vec<Note<'_>> = events.iter().map(|v| v.note.clone()).collect();
widgets::StreamList::new(&events, &self.services).ui(ui)
}
}

134
src/route/mod.rs Normal file
View File

@ -0,0 +1,134 @@
use crate::link::NostrLink;
use crate::note_util::OwnedNote;
use crate::route;
use crate::route::home::HomePage;
use crate::route::stream::StreamPage;
use crate::services::profile::ProfileService;
use crate::widgets::{Header, StreamList};
use egui::{Context, Response, ScrollArea, Ui, Widget};
use egui_inbox::{UiInbox, UiInboxSender};
use egui_video::Player;
use log::{info, warn};
use nostr_sdk::Client;
use nostrdb::{Filter, Ndb, Note, Transaction};
mod stream;
mod home;
pub enum Routes {
HomePage,
Event {
link: NostrLink,
event: Option<OwnedNote>,
},
ProfilePage {
link: NostrLink,
profile: Option<OwnedNote>,
},
// special kind for modifying route state
Action(RouteAction),
}
pub enum RouteAction {
Login([u8; 32]),
StartPlayer(String),
PausePlayer,
SeekPlayer(f32),
StopPlayer,
}
pub struct Router {
current: Routes,
router: UiInbox<Routes>,
ctx: Context,
profile_service: ProfileService,
ndb: Ndb,
login: Option<[u8; 32]>,
player: Option<Player>,
}
impl Router {
pub fn new(rx: UiInbox<Routes>, ctx: Context, client: Client, ndb: Ndb) -> Self {
Self {
current: Routes::HomePage,
router: rx,
ctx: ctx.clone(),
profile_service: ProfileService::new(client.clone(), ctx.clone()),
ndb,
login: None,
player: None,
}
}
pub fn show(&mut self, ui: &mut Ui) -> Response {
let tx = Transaction::new(&self.ndb).unwrap();
// handle app state changes
while let Some(r) = self.router.read(ui).next() {
if let Routes::Action(a) = r {
match a {
RouteAction::Login(k) => {
self.login = Some(k)
}
RouteAction::StartPlayer(u) => {
if self.player.is_none() {
if let Ok(p) = Player::new(&self.ctx, &u) {
self.player = Some(p)
}
}
}
_ => info!("Not implemented")
}
} else {
self.current = r;
}
}
let mut svc = RouteServices {
context: self.ctx.clone(),
profile: &self.profile_service,
router: self.router.sender(),
ndb: self.ndb.clone(),
tx: &tx,
login: &self.login,
player: &mut self.player,
};
// display app
ScrollArea::vertical().show(ui, |ui| {
ui.add(Header::new(&svc));
match &self.current {
Routes::HomePage => {
HomePage::new(&svc).ui(ui)
}
Routes::Event { link, event } => {
StreamPage::new(&mut svc, link, event)
.ui(ui)
}
_ => {
ui.label("Not found")
}
}
}).inner
}
}
pub struct RouteServices<'a> {
pub context: Context, //cloned
pub router: UiInboxSender<Routes>, //cloned
pub ndb: Ndb, //cloned
pub player: &'a mut Option<Player>,
pub profile: &'a ProfileService, //ref
pub tx: &'a Transaction, //ref
pub login: &'a Option<[u8; 32]>, //ref
}
impl<'a> RouteServices<'a> {
pub fn navigate(&self, route: Routes) {
if let Err(e) = self.router.send(route) {
warn!("Failed to navigate");
}
}
}

51
src/route/stream.rs Normal file
View File

@ -0,0 +1,51 @@
use crate::link::NostrLink;
use crate::note_util::{NoteUtil, OwnedNote};
use crate::route::{RouteAction, RouteServices, Routes};
use crate::stream_info::StreamInfo;
use crate::widgets::StreamPlayer;
use egui::{Color32, Label, Response, RichText, TextWrapMode, Ui, Widget};
use nostrdb::Note;
use std::ptr;
pub struct StreamPage<'a> {
services: &'a mut RouteServices<'a>,
link: &'a NostrLink,
event: &'a Option<OwnedNote>,
}
impl<'a> StreamPage<'a> {
pub fn new(services: &'a mut RouteServices<'a>, link: &'a NostrLink, event: &'a Option<OwnedNote>) -> Self {
Self { services, link, event }
}
}
impl<'a> Widget for StreamPage<'a> {
fn ui(self, ui: &mut Ui) -> Response {
let event = if let Some(event) = self.event {
Note::Owned {
ptr: ptr::null_mut(),
size: 0,
}
} else {
let mut q = self.services.ndb.query(self.services.tx, &[
self.link.try_into().unwrap()
], 1).unwrap();
let [e] = q.try_into().unwrap();
e.note
};
if let Some(stream) = event.stream() {
if self.services.player.is_none() {
self.services.navigate(Routes::Action(RouteAction::StartPlayer(stream)));
}
}
StreamPlayer::new(self.services).ui(ui);
let title = RichText::new(match event.get_tag_value("title") {
Some(s) => s.variant().str().unwrap_or("Unknown"),
None => "Unknown",
})
.size(16.)
.color(Color32::WHITE);
ui.add(Label::new(title).wrap_mode(TextWrapMode::Truncate))
}
}

View File

@ -1,19 +1 @@
use crate::services::profile::ProfileService;
use egui::Context;
use nostr_sdk::Client;
pub mod profile;
pub struct Services {
pub context: Context,
pub profile: ProfileService,
}
impl Services {
pub fn new(client: Client, context: Context) -> Self {
Self {
context: context.clone(),
profile: ProfileService::new(client.clone(), context.clone()),
}
}
}

View File

@ -1,23 +1,25 @@
use egui::load::BytesLoader;
use log::{info, warn};
use nostr_sdk::prelude::hex;
use nostr_sdk::{Client, Metadata, PublicKey};
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender};
use tokio::sync::Mutex;
pub struct ProfileService {
client: Client,
pub profiles: Arc<Mutex<HashMap<PublicKey, Option<Metadata>>>>,
pub profiles: Arc<Mutex<HashMap<[u8; 32], Option<Metadata>>>>,
ctx: egui::Context,
request_profile: UnboundedSender<PublicKey>,
request_profile: UnboundedSender<[u8; 32]>,
}
struct Loader {
client: Client,
ctx: egui::Context,
profiles: Arc<Mutex<HashMap<PublicKey, Option<Metadata>>>>,
queue: UnboundedReceiver<PublicKey>,
profiles: Arc<Mutex<HashMap<[u8; 32], Option<Metadata>>>>,
queue: UnboundedReceiver<[u8; 32]>,
}
impl Loader {
@ -25,8 +27,9 @@ impl Loader {
while let Some(key) = self.queue.recv().await {
let mut profiles = self.profiles.lock().await;
if !profiles.contains_key(&key) {
info!("Requesting profile {}", key);
match self.client.metadata(key).await {
info!("Requesting profile {}", hex::encode(key));
match self.client.fetch_metadata(PublicKey::from_slice(&key).unwrap(),
Some(Duration::from_secs(3))).await {
Ok(meta) => {
profiles.insert(key, Some(meta));
self.ctx.request_repaint();
@ -64,7 +67,7 @@ impl ProfileService {
}
}
pub fn get_profile(&self, public_key: &PublicKey) -> Option<Metadata> {
pub fn get_profile(&self, public_key: &[u8; 32]) -> Option<Metadata> {
if let Ok(profiles) = self.profiles.try_lock() {
return if let Some(p) = profiles.get(public_key) {
if let Some(p) = p {

38
src/stream_info.rs Normal file
View File

@ -0,0 +1,38 @@
use crate::note_util::NoteUtil;
use nostrdb::Note;
pub trait StreamInfo {
fn title(&self) -> Option<String>;
fn summary(&self) -> Option<String>;
fn host(&self) -> [u8; 32];
fn stream(&self) -> Option<String>;
}
impl<'a> StreamInfo for Note<'a> {
fn title(&self) -> Option<String> {
if let Some(s) = self.get_tag_value("title") {
s.variant().str().map(ToString::to_string)
} else {
None
}
}
fn summary(&self) -> Option<String> {
todo!()
}
fn host(&self) -> [u8; 32] {
todo!()
}
fn stream(&self) -> Option<String> {
if let Some(s) = self.get_tag_value("streaming") {
s.variant().str().map(ToString::to_string)
} else {
None
}
}
}

View File

@ -1,5 +0,0 @@
use nostr_sdk::Filter;
pub trait NostrWidget {
fn subscribe(&self) -> Vec<Filter>;
}

View File

@ -1,6 +1,5 @@
use crate::services::Services;
use egui::{Color32, Image, Rect, Response, Rounding, Sense, Ui, Vec2, Widget};
use nostr_sdk::PublicKey;
use crate::services::profile::ProfileService;
pub struct Avatar<'a> {
image: Option<Image<'a>>,
@ -11,8 +10,8 @@ impl<'a> Avatar<'a> {
Self { image: Some(img) }
}
pub fn public_key(svc: &'a Services, pk: &PublicKey) -> Self {
if let Some(meta) = svc.profile.get_profile(pk) {
pub fn public_key(svc: &'a ProfileService, pk: &[u8; 32]) -> Self {
if let Some(meta) = svc.get_profile(pk) {
if let Some(img) = &meta.picture {
return Self { image: Some(Image::from_uri(img.clone())) };
}

View File

@ -1,23 +1,23 @@
use crate::services::Services;
use crate::route::{RouteServices, Routes};
use crate::widgets::avatar::Avatar;
use eframe::emath::Align;
use eframe::epaint::Vec2;
use egui::{Frame, Image, Layout, Margin, Response, Ui, Widget};
use nostr_sdk::PublicKey;
use egui::{Frame, Image, Layout, Margin, Response, Sense, Ui, Widget};
use nostr_sdk::util::hex;
pub struct Header<'a> {
services: &'a Services,
services: &'a RouteServices<'a>,
}
impl<'a> Header<'a> {
pub fn new(services: &'a Services) -> Self {
pub fn new(services: &'a RouteServices) -> Self {
Self { services }
}
}
impl Widget for Header<'_> {
fn ui(self, ui: &mut Ui) -> Response {
let login = PublicKey::from_hex("63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed").unwrap();
let login: [u8; 32] = hex::decode("63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed").unwrap().try_into().unwrap();
let logo_bytes = include_bytes!("../logo.svg");
Frame::none()
.outer_margin(Margin::symmetric(16., 8.))
@ -25,9 +25,13 @@ impl Widget for Header<'_> {
ui.allocate_ui_with_layout(Vec2::new(ui.available_width(), 32.), Layout::left_to_right(Align::Center), |ui| {
ui.style_mut()
.spacing.item_spacing.x = 16.;
Image::from_bytes("logo.svg", logo_bytes)
.max_height(22.62).ui(ui);
ui.add(Avatar::public_key(&self.services, &login));
if Image::from_bytes("logo.svg", logo_bytes)
.max_height(22.62)
.sense(Sense::click())
.ui(ui).clicked() {
self.services.navigate(Routes::HomePage);
}
ui.add(Avatar::public_key(&self.services.profile, &login));
})
}).response
}

View File

@ -1,4 +1,11 @@
pub mod header;
pub mod stream;
pub mod stream_list;
mod header;
mod stream;
mod stream_list;
mod avatar;
mod stream_player;
mod video_placeholder;
pub use self::header::Header;
pub use self::stream_list::StreamList;
pub use self::video_placeholder::VideoPlaceholder;
pub use self::stream_player::StreamPlayer;

View File

@ -1,22 +1,27 @@
use crate::services::Services;
use crate::link::NostrLink;
use crate::note_util::NoteUtil;
use crate::route::{RouteServices, Routes};
use crate::widgets::avatar::Avatar;
use crate::widgets::VideoPlaceholder;
use eframe::epaint::Vec2;
use egui::{Color32, Image, Label, Rect, Response, RichText, Rounding, Sense, TextWrapMode, Ui, Widget};
use log::info;
use nostr_sdk::{Alphabet, Event, PublicKey, SingleLetterTag, TagKind};
use egui::{Color32, Image, Label, Response, RichText, Rounding, Sense, TextWrapMode, Ui, Widget};
use nostrdb::{NdbStrVariant, Note};
pub struct StreamEvent<'a> {
event: &'a Event,
event: &'a Note<'a>,
picture: Option<Image<'a>>,
services: &'a Services,
services: &'a RouteServices<'a>,
}
impl<'a> StreamEvent<'a> {
pub fn new(event: &'a Event, services: &'a Services) -> Self {
let image = event.get_tag_content(TagKind::Image);
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(Image::from_uri(i)),
None => None,
}
None => None,
};
Self { event, picture: cover, services }
}
@ -27,28 +32,39 @@ impl Widget for StreamEvent<'_> {
ui.style_mut()
.spacing.item_spacing = Vec2::new(12., 16.);
let host = match self.event.tags.iter().find(|t| t.kind() == TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::P)) && t.as_vec()[3] == "host") {
Some(t) => PublicKey::from_hex(t.as_vec().get(1).unwrap()).unwrap(),
None => self.event.author()
let host = match self.event.find_tag_value(|t| t[0].variant().str() == Some("p") && t[3].variant().str() == Some("host")) {
Some(t) => match t.variant() {
NdbStrVariant::Id(i) => i,
NdbStrVariant::Str(s) => self.event.pubkey(),
}
None => self.event.pubkey()
};
let w = ui.available_width();
let h = (w / 16.0) * 9.0;
let img_size = Vec2::new(w, h);
let img = match self.picture {
Some(picture) => picture.fit_to_exact_size(img_size).rounding(Rounding::same(12.)).sense(Sense::click()).ui(ui),
Some(picture) => picture
.fit_to_exact_size(img_size)
.rounding(Rounding::same(12.))
.sense(Sense::click())
.ui(ui),
None => {
let (response, painter) = ui.allocate_painter(img_size, Sense::click());
painter.rect_filled(Rect::EVERYTHING, Rounding::same(12.), Color32::from_rgb(200, 200, 200));
response
VideoPlaceholder.ui(ui)
}
};
if img.clicked() {
info!("Navigating to {}", self.event.id);
self.services.navigate(Routes::Event {
link: NostrLink::from_note(&self.event),
event: None,
});
}
ui.horizontal(|ui| {
ui.add(Avatar::public_key(self.services, &host).size(40.));
let title = RichText::new(self.event.get_tag_content(TagKind::Title).unwrap_or("Unknown"))
ui.add(Avatar::public_key(self.services.profile, &host).size(40.));
let title = RichText::new(match self.event.get_tag_value("title") {
Some(s) => s.variant().str().unwrap_or("Unknown"),
None => "Unknown",
})
.size(16.)
.color(Color32::WHITE);
ui.add(Label::new(title).wrap_mode(TextWrapMode::Truncate));

View File

@ -1,16 +1,15 @@
use crate::services::Services;
use crate::route::RouteServices;
use crate::widgets::stream::StreamEvent;
use egui::{Frame, Margin, Response, Ui, Widget};
use egui_extras::Column;
use nostr_sdk::Event;
use nostrdb::Note;
pub struct StreamList<'a> {
streams: &'a Vec<Event>,
services: &'a Services,
streams: &'a Vec<Note<'a>>,
services: &'a RouteServices<'a>,
}
impl<'a> StreamList<'a> {
pub fn new(streams: &'a Vec<Event>, services: &'a Services) -> Self {
pub fn new(streams: &'a Vec<Note<'a>>, services: &'a RouteServices) -> Self {
Self { streams, services }
}
}

View File

@ -0,0 +1,27 @@
use crate::route::RouteServices;
use crate::widgets::VideoPlaceholder;
use egui::{Response, Ui, Vec2, Widget};
pub struct StreamPlayer<'a> {
services: &'a mut RouteServices<'a>,
}
impl<'a> StreamPlayer<'a> {
pub fn new(services: &'a mut RouteServices<'a>) -> Self {
Self { services }
}
}
impl<'a> Widget for StreamPlayer<'a> {
fn ui(self, ui: &mut Ui) -> Response {
let w = ui.available_width();
let h = w / 16. * 9.;
let size = Vec2::new(w, h);
if let Some(p) = self.services.player.as_mut() {
p.ui(ui, size)
} else {
VideoPlaceholder.ui(ui)
}
}
}

View File

@ -0,0 +1,15 @@
use egui::{Color32, Rect, Response, Rounding, Sense, Ui, Vec2, Widget};
pub struct VideoPlaceholder;
impl Widget for VideoPlaceholder {
fn ui(self, ui: &mut Ui) -> Response {
let w = ui.available_width();
let h = (w / 16.0) * 9.0;
let img_size = Vec2::new(w, h);
let (response, painter) = ui.allocate_painter(img_size, Sense::click());
painter.rect_filled(Rect::EVERYTHING, Rounding::same(12.), Color32::from_rgb(200, 200, 200));
response
}
}