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]
|
[dependencies]
|
||||||
tokio = "1.40.0"
|
tokio = "1.40.0"
|
||||||
|
|
||||||
egui = { git = "https://github.com/emilk/egui", rev = "fcb7764e48ce00f8f8e58da10f937410d65b0bfb" }
|
egui = { version = "0.29.1" }
|
||||||
eframe = { git = "https://github.com/emilk/egui", rev = "fcb7764e48ce00f8f8e58da10f937410d65b0bfb", package = "eframe", default-features = false, features = ["wgpu", "wayland", "x11", "android-native-activity"] }
|
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", rev = "6d22af6d5159be4c9e4579f8c9d3af836e0d470a" }
|
nostrdb = { git = "https://github.com/damus-io/nostrdb-rs", version = "0.3.4" }
|
||||||
nostr-sdk = { version = "0.34.0", features = ["all-nips"] }
|
nostr-sdk = { version = "0.35.0", features = ["all-nips"] }
|
||||||
egui_nav = { git = "https://github.com/damus-io/egui-nav", branch = "egui-0.28" }
|
egui_extras = { version = "0.29.1", features = ["all_loaders"] }
|
||||||
egui_extras = { git = "https://github.com/emilk/egui", rev = "fcb7764e48ce00f8f8e58da10f937410d65b0bfb", package = "egui_extras", features = ["all_loaders"] }
|
|
||||||
image = { version = "0.25", features = ["jpeg", "png", "webp"] }
|
image = { version = "0.25", features = ["jpeg", "png", "webp"] }
|
||||||
log = "0.4.22"
|
log = "0.4.22"
|
||||||
pretty_env_logger = "0.5.0"
|
pretty_env_logger = "0.5.0"
|
||||||
|
egui_inbox = "0.6.0"
|
||||||
|
bech32 = "0.11.0"
|
||||||
|
libc = "0.2.158"
|
||||||
|
egui-video = { path = "../egui-video" }
|
||||||
|
36
src/app.rs
36
src/app.rs
@ -1,20 +1,18 @@
|
|||||||
use crate::services::Services;
|
use crate::route::Router;
|
||||||
use crate::widget::NostrWidget;
|
|
||||||
use crate::widgets::header::Header;
|
|
||||||
use crate::widgets::stream_list::StreamList;
|
|
||||||
use eframe::{App, CreationContext, Frame};
|
use eframe::{App, CreationContext, Frame};
|
||||||
use egui::{Color32, Context, ScrollArea};
|
use egui::{Color32, Context, ScrollArea};
|
||||||
|
use egui_inbox::UiInbox;
|
||||||
|
use log::warn;
|
||||||
use nostr_sdk::database::{MemoryDatabase, MemoryDatabaseOptions};
|
use nostr_sdk::database::{MemoryDatabase, MemoryDatabaseOptions};
|
||||||
use nostr_sdk::{Client, Event, Filter, Kind, RelayPoolNotification};
|
use nostr_sdk::{Client, Filter, JsonUtil, Kind, RelayPoolNotification};
|
||||||
use nostrdb::{Config, Ndb, Transaction};
|
use nostrdb::{Config, Ndb};
|
||||||
use tokio::sync::broadcast;
|
use tokio::sync::broadcast;
|
||||||
|
|
||||||
pub struct ZapStreamApp {
|
pub struct ZapStreamApp {
|
||||||
ndb: Ndb,
|
ndb: Ndb,
|
||||||
client: Client,
|
client: Client,
|
||||||
notifications: broadcast::Receiver<RelayPoolNotification>,
|
notifications: broadcast::Receiver<RelayPoolNotification>,
|
||||||
events: Vec<Event>,
|
router: Router,
|
||||||
services: Services,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ZapStreamApp {
|
impl ZapStreamApp {
|
||||||
@ -41,12 +39,13 @@ impl ZapStreamApp {
|
|||||||
});
|
});
|
||||||
egui_extras::install_image_loaders(&cc.egui_ctx);
|
egui_extras::install_image_loaders(&cc.egui_ctx);
|
||||||
|
|
||||||
|
let inbox = UiInbox::new();
|
||||||
|
let ndb = Ndb::new(".", &Config::default()).unwrap();
|
||||||
Self {
|
Self {
|
||||||
ndb: Ndb::new(".", &Config::default()).unwrap(),
|
ndb: ndb.clone(),
|
||||||
client: client.clone(),
|
client: client.clone(),
|
||||||
notifications,
|
notifications,
|
||||||
services: Services::new(client, cc.egui_ctx.clone()),
|
router: Router::new(inbox, cc.egui_ctx.clone(), client.clone(), ndb.clone()),
|
||||||
events: vec![],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,7 +53,9 @@ impl ZapStreamApp {
|
|||||||
while let Ok(msg) = self.notifications.try_recv() {
|
while let Ok(msg) = self.notifications.try_recv() {
|
||||||
match msg {
|
match msg {
|
||||||
RelayPoolNotification::Event { event, .. } => {
|
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
|
// dont care
|
||||||
@ -66,20 +67,17 @@ impl ZapStreamApp {
|
|||||||
|
|
||||||
impl App for ZapStreamApp {
|
impl App for ZapStreamApp {
|
||||||
fn update(&mut self, ctx: &Context, frame: &mut Frame) {
|
fn update(&mut self, ctx: &Context, frame: &mut Frame) {
|
||||||
let txn = Transaction::new(&self.ndb).expect("txn");
|
|
||||||
self.process_nostr();
|
self.process_nostr();
|
||||||
|
|
||||||
let mut app_frame = egui::containers::Frame::default();
|
let mut app_frame = egui::containers::Frame::default();
|
||||||
app_frame.stroke.color = Color32::BLACK;
|
app_frame.stroke.color = Color32::BLACK;
|
||||||
|
|
||||||
ctx.set_debug_on_hover(true);
|
//ctx.set_debug_on_hover(true);
|
||||||
|
|
||||||
egui::CentralPanel::default()
|
egui::CentralPanel::default()
|
||||||
.frame(app_frame)
|
.frame(app_frame)
|
||||||
.show(ctx, |ui| {
|
.show(ctx, |ui| {
|
||||||
ScrollArea::vertical().show(ui, |ui| {
|
self.router.show(ui)
|
||||||
ui.add(Header::new(&self.services));
|
|
||||||
ui.add(StreamList::new(&self.events, &self.services));
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
src/main.rs
12
src/main.rs
@ -1,16 +1,22 @@
|
|||||||
|
use eframe::Renderer;
|
||||||
use crate::app::ZapStreamApp;
|
use crate::app::ZapStreamApp;
|
||||||
use egui::Vec2;
|
use egui::Vec2;
|
||||||
|
use nostrdb::Note;
|
||||||
|
|
||||||
mod app;
|
mod app;
|
||||||
mod widget;
|
pub mod widgets;
|
||||||
mod widgets;
|
|
||||||
mod services;
|
mod services;
|
||||||
|
mod route;
|
||||||
|
mod note_util;
|
||||||
|
mod link;
|
||||||
|
mod stream_info;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
pretty_env_logger::init();
|
pretty_env_logger::init();
|
||||||
|
|
||||||
let mut options = eframe::NativeOptions::default();
|
let mut options = eframe::NativeOptions::default();
|
||||||
|
options.renderer = Renderer::Glow;
|
||||||
options.viewport = options.viewport
|
options.viewport = options.viewport
|
||||||
.with_inner_size(Vec2::new(360., 720.));
|
.with_inner_size(Vec2::new(360., 720.));
|
||||||
|
|
||||||
@ -19,4 +25,4 @@ async fn main() {
|
|||||||
options,
|
options,
|
||||||
Box::new(move |cc| Ok(Box::new(ZapStreamApp::new(cc)))),
|
Box::new(move |cc| Ok(Box::new(ZapStreamApp::new(cc)))),
|
||||||
);
|
);
|
||||||
}
|
}
|
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;
|
pub mod profile;
|
||||||
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 egui::load::BytesLoader;
|
||||||
use log::{info, warn};
|
use log::{info, warn};
|
||||||
|
use nostr_sdk::prelude::hex;
|
||||||
use nostr_sdk::{Client, Metadata, PublicKey};
|
use nostr_sdk::{Client, Metadata, PublicKey};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender};
|
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender};
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
pub struct ProfileService {
|
pub struct ProfileService {
|
||||||
client: Client,
|
client: Client,
|
||||||
pub profiles: Arc<Mutex<HashMap<PublicKey, Option<Metadata>>>>,
|
pub profiles: Arc<Mutex<HashMap<[u8; 32], Option<Metadata>>>>,
|
||||||
ctx: egui::Context,
|
ctx: egui::Context,
|
||||||
request_profile: UnboundedSender<PublicKey>,
|
request_profile: UnboundedSender<[u8; 32]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Loader {
|
struct Loader {
|
||||||
client: Client,
|
client: Client,
|
||||||
ctx: egui::Context,
|
ctx: egui::Context,
|
||||||
profiles: Arc<Mutex<HashMap<PublicKey, Option<Metadata>>>>,
|
profiles: Arc<Mutex<HashMap<[u8; 32], Option<Metadata>>>>,
|
||||||
queue: UnboundedReceiver<PublicKey>,
|
queue: UnboundedReceiver<[u8; 32]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Loader {
|
impl Loader {
|
||||||
@ -25,8 +27,9 @@ impl Loader {
|
|||||||
while let Some(key) = self.queue.recv().await {
|
while let Some(key) = self.queue.recv().await {
|
||||||
let mut profiles = self.profiles.lock().await;
|
let mut profiles = self.profiles.lock().await;
|
||||||
if !profiles.contains_key(&key) {
|
if !profiles.contains_key(&key) {
|
||||||
info!("Requesting profile {}", key);
|
info!("Requesting profile {}", hex::encode(key));
|
||||||
match self.client.metadata(key).await {
|
match self.client.fetch_metadata(PublicKey::from_slice(&key).unwrap(),
|
||||||
|
Some(Duration::from_secs(3))).await {
|
||||||
Ok(meta) => {
|
Ok(meta) => {
|
||||||
profiles.insert(key, Some(meta));
|
profiles.insert(key, Some(meta));
|
||||||
self.ctx.request_repaint();
|
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() {
|
if let Ok(profiles) = self.profiles.try_lock() {
|
||||||
return if let Some(p) = profiles.get(public_key) {
|
return if let Some(p) = profiles.get(public_key) {
|
||||||
if let Some(p) = p {
|
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 egui::{Color32, Image, Rect, Response, Rounding, Sense, Ui, Vec2, Widget};
|
||||||
use nostr_sdk::PublicKey;
|
use crate::services::profile::ProfileService;
|
||||||
|
|
||||||
pub struct Avatar<'a> {
|
pub struct Avatar<'a> {
|
||||||
image: Option<Image<'a>>,
|
image: Option<Image<'a>>,
|
||||||
@ -11,8 +10,8 @@ impl<'a> Avatar<'a> {
|
|||||||
Self { image: Some(img) }
|
Self { image: Some(img) }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn public_key(svc: &'a Services, pk: &PublicKey) -> Self {
|
pub fn public_key(svc: &'a ProfileService, pk: &[u8; 32]) -> Self {
|
||||||
if let Some(meta) = svc.profile.get_profile(pk) {
|
if let Some(meta) = svc.get_profile(pk) {
|
||||||
if let Some(img) = &meta.picture {
|
if let Some(img) = &meta.picture {
|
||||||
return Self { image: Some(Image::from_uri(img.clone())) };
|
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 crate::widgets::avatar::Avatar;
|
||||||
use eframe::emath::Align;
|
use eframe::emath::Align;
|
||||||
use eframe::epaint::Vec2;
|
use eframe::epaint::Vec2;
|
||||||
use egui::{Frame, Image, Layout, Margin, Response, Ui, Widget};
|
use egui::{Frame, Image, Layout, Margin, Response, Sense, Ui, Widget};
|
||||||
use nostr_sdk::PublicKey;
|
use nostr_sdk::util::hex;
|
||||||
|
|
||||||
pub struct Header<'a> {
|
pub struct Header<'a> {
|
||||||
services: &'a Services,
|
services: &'a RouteServices<'a>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Header<'a> {
|
impl<'a> Header<'a> {
|
||||||
pub fn new(services: &'a Services) -> Self {
|
pub fn new(services: &'a RouteServices) -> Self {
|
||||||
Self { services }
|
Self { services }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Header<'_> {
|
impl Widget for Header<'_> {
|
||||||
fn ui(self, ui: &mut Ui) -> Response {
|
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");
|
let logo_bytes = include_bytes!("../logo.svg");
|
||||||
Frame::none()
|
Frame::none()
|
||||||
.outer_margin(Margin::symmetric(16., 8.))
|
.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.allocate_ui_with_layout(Vec2::new(ui.available_width(), 32.), Layout::left_to_right(Align::Center), |ui| {
|
||||||
ui.style_mut()
|
ui.style_mut()
|
||||||
.spacing.item_spacing.x = 16.;
|
.spacing.item_spacing.x = 16.;
|
||||||
Image::from_bytes("logo.svg", logo_bytes)
|
if Image::from_bytes("logo.svg", logo_bytes)
|
||||||
.max_height(22.62).ui(ui);
|
.max_height(22.62)
|
||||||
ui.add(Avatar::public_key(&self.services, &login));
|
.sense(Sense::click())
|
||||||
|
.ui(ui).clicked() {
|
||||||
|
self.services.navigate(Routes::HomePage);
|
||||||
|
}
|
||||||
|
ui.add(Avatar::public_key(&self.services.profile, &login));
|
||||||
})
|
})
|
||||||
}).response
|
}).response
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,11 @@
|
|||||||
pub mod header;
|
mod header;
|
||||||
pub mod stream;
|
mod stream;
|
||||||
pub mod stream_list;
|
mod stream_list;
|
||||||
mod avatar;
|
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,21 +1,26 @@
|
|||||||
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::avatar::Avatar;
|
||||||
|
use crate::widgets::VideoPlaceholder;
|
||||||
use eframe::epaint::Vec2;
|
use eframe::epaint::Vec2;
|
||||||
use egui::{Color32, Image, Label, Rect, Response, RichText, Rounding, Sense, TextWrapMode, Ui, Widget};
|
use egui::{Color32, Image, Label, Response, RichText, Rounding, Sense, TextWrapMode, Ui, Widget};
|
||||||
use log::info;
|
use nostrdb::{NdbStrVariant, Note};
|
||||||
use nostr_sdk::{Alphabet, Event, PublicKey, SingleLetterTag, TagKind};
|
|
||||||
|
|
||||||
pub struct StreamEvent<'a> {
|
pub struct StreamEvent<'a> {
|
||||||
event: &'a Event,
|
event: &'a Note<'a>,
|
||||||
picture: Option<Image<'a>>,
|
picture: Option<Image<'a>>,
|
||||||
services: &'a Services,
|
services: &'a RouteServices<'a>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> StreamEvent<'a> {
|
impl<'a> StreamEvent<'a> {
|
||||||
pub fn new(event: &'a Event, services: &'a Services) -> Self {
|
pub fn new(event: &'a Note<'a>, services: &'a RouteServices) -> Self {
|
||||||
let image = event.get_tag_content(TagKind::Image);
|
let image = event.get_tag_value("image");
|
||||||
let cover = match image {
|
let cover = match image {
|
||||||
Some(i) => Some(Image::from_uri(i)),
|
Some(i) => match i.variant().str() {
|
||||||
|
Some(i) => Some(Image::from_uri(i)),
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
Self { event, picture: cover, services }
|
Self { event, picture: cover, services }
|
||||||
@ -27,28 +32,39 @@ impl Widget for StreamEvent<'_> {
|
|||||||
ui.style_mut()
|
ui.style_mut()
|
||||||
.spacing.item_spacing = Vec2::new(12., 16.);
|
.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") {
|
let host = match self.event.find_tag_value(|t| t[0].variant().str() == Some("p") && t[3].variant().str() == Some("host")) {
|
||||||
Some(t) => PublicKey::from_hex(t.as_vec().get(1).unwrap()).unwrap(),
|
Some(t) => match t.variant() {
|
||||||
None => self.event.author()
|
NdbStrVariant::Id(i) => i,
|
||||||
|
NdbStrVariant::Str(s) => self.event.pubkey(),
|
||||||
|
}
|
||||||
|
None => self.event.pubkey()
|
||||||
};
|
};
|
||||||
let w = ui.available_width();
|
let w = ui.available_width();
|
||||||
let h = (w / 16.0) * 9.0;
|
let h = (w / 16.0) * 9.0;
|
||||||
let img_size = Vec2::new(w, h);
|
let img_size = Vec2::new(w, h);
|
||||||
|
|
||||||
let img = match self.picture {
|
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 => {
|
None => {
|
||||||
let (response, painter) = ui.allocate_painter(img_size, Sense::click());
|
VideoPlaceholder.ui(ui)
|
||||||
painter.rect_filled(Rect::EVERYTHING, Rounding::same(12.), Color32::from_rgb(200, 200, 200));
|
|
||||||
response
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if img.clicked() {
|
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.horizontal(|ui| {
|
||||||
ui.add(Avatar::public_key(self.services, &host).size(40.));
|
ui.add(Avatar::public_key(self.services.profile, &host).size(40.));
|
||||||
let title = RichText::new(self.event.get_tag_content(TagKind::Title).unwrap_or("Unknown"))
|
let title = RichText::new(match self.event.get_tag_value("title") {
|
||||||
|
Some(s) => s.variant().str().unwrap_or("Unknown"),
|
||||||
|
None => "Unknown",
|
||||||
|
})
|
||||||
.size(16.)
|
.size(16.)
|
||||||
.color(Color32::WHITE);
|
.color(Color32::WHITE);
|
||||||
ui.add(Label::new(title).wrap_mode(TextWrapMode::Truncate));
|
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 crate::widgets::stream::StreamEvent;
|
||||||
use egui::{Frame, Margin, Response, Ui, Widget};
|
use egui::{Frame, Margin, Response, Ui, Widget};
|
||||||
use egui_extras::Column;
|
use nostrdb::Note;
|
||||||
use nostr_sdk::Event;
|
|
||||||
|
|
||||||
pub struct StreamList<'a> {
|
pub struct StreamList<'a> {
|
||||||
streams: &'a Vec<Event>,
|
streams: &'a Vec<Note<'a>>,
|
||||||
services: &'a Services,
|
services: &'a RouteServices<'a>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> StreamList<'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 }
|
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