setup routes
This commit is contained in:
parent
abfcbc8954
commit
67d6381123
986
Cargo.lock
generated
986
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
15
Cargo.toml
15
Cargo.toml
@ -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" }
|
||||
|
34
src/app.rs
34
src/app.rs
@ -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
119
src/link.rs
Normal 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!()
|
||||
}
|
||||
}
|
||||
}
|
10
src/main.rs
10
src/main.rs
@ -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
73
src/note_util.rs
Normal 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
27
src/route/home.rs
Normal 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
134
src/route/mod.rs
Normal 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
51
src/route/stream.rs
Normal 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))
|
||||
}
|
||||
}
|
@ -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()),
|
||||
}
|
||||
}
|
||||
}
|
@ -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
38
src/stream_info.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
use nostr_sdk::Filter;
|
||||
|
||||
pub trait NostrWidget {
|
||||
fn subscribe(&self) -> Vec<Filter>;
|
||||
}
|
@ -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())) };
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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));
|
||||
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
27
src/widgets/stream_player.rs
Normal file
27
src/widgets/stream_player.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
15
src/widgets/video_placeholder.rs
Normal file
15
src/widgets/video_placeholder.rs
Normal 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
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user