refactor: widget state
This commit is contained in:
parent
67d6381123
commit
9a8bb54e08
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -1032,6 +1032,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "egui-video"
|
name = "egui-video"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
|
source = "git+https://github.com/v0l/egui-video.git?rev=4766d939ce4d34b5a3a57b2fbe750ea10f389f39#4766d939ce4d34b5a3a57b2fbe750ea10f389f39"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"atomic",
|
"atomic",
|
||||||
|
@ -17,4 +17,4 @@ pretty_env_logger = "0.5.0"
|
|||||||
egui_inbox = "0.6.0"
|
egui_inbox = "0.6.0"
|
||||||
bech32 = "0.11.0"
|
bech32 = "0.11.0"
|
||||||
libc = "0.2.158"
|
libc = "0.2.158"
|
||||||
egui-video = { path = "../egui-video" }
|
egui-video = { git = "https://github.com/v0l/egui-video.git", rev = "4766d939ce4d34b5a3a57b2fbe750ea10f389f39" }
|
||||||
|
40
src/app.rs
40
src/app.rs
@ -1,15 +1,12 @@
|
|||||||
use crate::route::Router;
|
use crate::route::Router;
|
||||||
use eframe::{App, CreationContext, Frame};
|
use eframe::{App, CreationContext, Frame};
|
||||||
use egui::{Color32, Context, ScrollArea};
|
use egui::{Color32, Context};
|
||||||
use egui_inbox::UiInbox;
|
use nostr_sdk::database::MemoryDatabase;
|
||||||
use log::warn;
|
use nostr_sdk::{Client, RelayPoolNotification};
|
||||||
use nostr_sdk::database::{MemoryDatabase, MemoryDatabaseOptions};
|
|
||||||
use nostr_sdk::{Client, Filter, JsonUtil, Kind, RelayPoolNotification};
|
|
||||||
use nostrdb::{Config, Ndb};
|
use nostrdb::{Config, Ndb};
|
||||||
use tokio::sync::broadcast;
|
use tokio::sync::broadcast;
|
||||||
|
|
||||||
pub struct ZapStreamApp {
|
pub struct ZapStreamApp {
|
||||||
ndb: Ndb,
|
|
||||||
client: Client,
|
client: Client,
|
||||||
notifications: broadcast::Receiver<RelayPoolNotification>,
|
notifications: broadcast::Receiver<RelayPoolNotification>,
|
||||||
router: Router,
|
router: Router,
|
||||||
@ -17,10 +14,7 @@ pub struct ZapStreamApp {
|
|||||||
|
|
||||||
impl ZapStreamApp {
|
impl ZapStreamApp {
|
||||||
pub fn new(cc: &CreationContext) -> Self {
|
pub fn new(cc: &CreationContext) -> Self {
|
||||||
let client = Client::builder().database(MemoryDatabase::with_opts(MemoryDatabaseOptions {
|
let client = Client::builder().database(MemoryDatabase::with_opts(Default::default())).build();
|
||||||
events: true,
|
|
||||||
..Default::default()
|
|
||||||
})).build();
|
|
||||||
let notifications = client.notifications();
|
let notifications = client.notifications();
|
||||||
|
|
||||||
let ctx_clone = cc.egui_ctx.clone();
|
let ctx_clone = cc.egui_ctx.clone();
|
||||||
@ -28,10 +22,6 @@ impl ZapStreamApp {
|
|||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
client_clone.add_relay("wss://nos.lol").await.expect("Failed to add relay");
|
client_clone.add_relay("wss://nos.lol").await.expect("Failed to add relay");
|
||||||
client_clone.connect().await;
|
client_clone.connect().await;
|
||||||
client_clone.subscribe(vec![
|
|
||||||
Filter::new()
|
|
||||||
.kind(Kind::LiveEvent)
|
|
||||||
], None).await.expect("Failed to subscribe");
|
|
||||||
let mut notifications = client_clone.notifications();
|
let mut notifications = client_clone.notifications();
|
||||||
while let Ok(_) = notifications.recv().await {
|
while let Ok(_) = notifications.recv().await {
|
||||||
ctx_clone.request_repaint();
|
ctx_clone.request_repaint();
|
||||||
@ -39,36 +29,18 @@ 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();
|
let ndb = Ndb::new(".", &Config::default()).unwrap();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
ndb: ndb.clone(),
|
|
||||||
client: client.clone(),
|
client: client.clone(),
|
||||||
notifications,
|
notifications,
|
||||||
router: Router::new(inbox, cc.egui_ctx.clone(), client.clone(), ndb.clone()),
|
router: Router::new(cc.egui_ctx.clone(), client.clone(), ndb.clone()),
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn process_nostr(&mut self) {
|
|
||||||
while let Ok(msg) = self.notifications.try_recv() {
|
|
||||||
match msg {
|
|
||||||
RelayPoolNotification::Event { event, .. } => {
|
|
||||||
if let Err(e) = self.ndb.process_event(event.as_json().as_str()) {
|
|
||||||
warn!("Failed to process event: {:?}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
// dont care
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
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;
|
||||||
|
|
||||||
|
41
src/link.rs
41
src/link.rs
@ -5,22 +5,31 @@ use nostr_sdk::util::hex;
|
|||||||
use nostrdb::{Filter, Note};
|
use nostrdb::{Filter, Note};
|
||||||
use std::fmt::{Display, Formatter};
|
use std::fmt::{Display, Formatter};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Eq, PartialEq)]
|
||||||
pub struct NostrLink {
|
pub struct NostrLink {
|
||||||
hrp: NostrLinkType,
|
pub hrp: NostrLinkType,
|
||||||
id: IdOrStr,
|
pub id: IdOrStr,
|
||||||
kind: Option<u32>,
|
pub kind: Option<u32>,
|
||||||
author: Option<[u8; 32]>,
|
pub author: Option<[u8; 32]>,
|
||||||
relays: Vec<String>,
|
pub relays: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Eq, PartialEq)]
|
||||||
pub enum IdOrStr {
|
pub enum IdOrStr {
|
||||||
Id([u8; 32]),
|
Id([u8; 32]),
|
||||||
Str(String),
|
Str(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
impl Display for IdOrStr {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
IdOrStr::Id(id) => write!(f, "{}", hex::encode(id)),
|
||||||
|
IdOrStr::Str(str) => write!(f, "{}", str),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Eq, PartialEq)]
|
||||||
pub enum NostrLinkType {
|
pub enum NostrLinkType {
|
||||||
Note,
|
Note,
|
||||||
PublicKey,
|
PublicKey,
|
||||||
@ -62,6 +71,22 @@ impl NostrLink {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn to_tag(&self) -> Vec<String> {
|
||||||
|
if self.hrp == NostrLinkType::Coordinate {
|
||||||
|
vec!["a".to_string(), self.to_tag_value()]
|
||||||
|
} else {
|
||||||
|
vec!["e".to_string(), self.to_tag_value()]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_tag_value(&self) -> String {
|
||||||
|
if self.hrp == NostrLinkType::Coordinate {
|
||||||
|
format!("{}:{}:{}", self.kind.unwrap(), hex::encode(self.author.unwrap()), self.id)
|
||||||
|
} else {
|
||||||
|
self.id.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryInto<Filter> for &NostrLink {
|
impl TryInto<Filter> for &NostrLink {
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
use libc::{malloc, memcpy};
|
|
||||||
use nostr_sdk::util::hex;
|
use nostr_sdk::util::hex;
|
||||||
use nostrdb::{NdbStr, Note, Tag, Tags};
|
use nostrdb::{NdbStr, Note, Tag};
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
use std::mem::transmute;
|
|
||||||
use std::{mem, ptr};
|
|
||||||
|
|
||||||
pub trait NoteUtil {
|
pub trait NoteUtil {
|
||||||
fn id_hex(&self) -> String;
|
fn id_hex(&self) -> String;
|
||||||
@ -70,4 +67,5 @@ impl<'a> Iterator for TagIterBorrow<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct OwnedNote;
|
#[derive(Eq, PartialEq)]
|
||||||
|
pub struct OwnedNote(pub u64);
|
@ -1,27 +1,52 @@
|
|||||||
|
use crate::note_util::OwnedNote;
|
||||||
use crate::route::RouteServices;
|
use crate::route::RouteServices;
|
||||||
use egui::{Response, Ui, Widget};
|
use crate::services::ndb_wrapper::NDBWrapper;
|
||||||
use nostrdb::{Filter, Note};
|
|
||||||
use crate::widgets;
|
use crate::widgets;
|
||||||
|
use crate::widgets::NostrWidget;
|
||||||
|
use egui::{Response, Ui, Widget};
|
||||||
|
use log::{error, info};
|
||||||
|
use nostrdb::{Filter, Ndb, Note, NoteKey, Subscription, Transaction};
|
||||||
|
|
||||||
pub struct HomePage<'a> {
|
pub struct HomePage {
|
||||||
services: &'a RouteServices<'a>,
|
sub: Subscription,
|
||||||
|
events: Vec<OwnedNote>,
|
||||||
|
ndb: NDBWrapper,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> HomePage<'a> {
|
impl HomePage {
|
||||||
pub fn new(services: &'a RouteServices) -> Self {
|
pub fn new(ndb: &NDBWrapper, tx: &Transaction) -> Self {
|
||||||
Self { services }
|
let filter = [
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Widget for HomePage<'a> {
|
|
||||||
fn ui(self, ui: &mut Ui) -> Response {
|
|
||||||
let events = self.services.ndb.query(&self.services.tx, &[
|
|
||||||
Filter::new()
|
Filter::new()
|
||||||
.kinds([30_311])
|
.kinds([30_311])
|
||||||
.limit(10)
|
.limit(10)
|
||||||
.build()
|
.build()
|
||||||
], 10).unwrap();
|
];
|
||||||
let events: Vec<Note<'_>> = events.iter().map(|v| v.note.clone()).collect();
|
let (sub, events) = ndb.subscribe_with_results(&filter, tx, 100);
|
||||||
widgets::StreamList::new(&events, &self.services).ui(ui)
|
Self {
|
||||||
|
sub,
|
||||||
|
events: events.iter().map(|e| OwnedNote(e.note_key.as_u64())).collect(),
|
||||||
|
ndb: ndb.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for HomePage {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.ndb.unsubscribe(self.sub);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NostrWidget for HomePage {
|
||||||
|
fn render(&mut self, ui: &mut Ui, services: &RouteServices<'_>) -> Response {
|
||||||
|
let new_notes = services.ndb.poll(self.sub, 100);
|
||||||
|
new_notes.iter().for_each(|n| self.events.push(OwnedNote(n.as_u64())));
|
||||||
|
|
||||||
|
let events: Vec<Note<'_>> = self.events.iter()
|
||||||
|
.map(|n| services.ndb.get_note_by_key(services.tx, NoteKey::new(n.0)))
|
||||||
|
.map_while(|f| f.map_or(None, |f| Some(f)))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
info!("HomePage events: {}", events.len());
|
||||||
|
widgets::StreamList::new(&events, &services).ui(ui)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -3,18 +3,22 @@ use crate::note_util::OwnedNote;
|
|||||||
use crate::route;
|
use crate::route;
|
||||||
use crate::route::home::HomePage;
|
use crate::route::home::HomePage;
|
||||||
use crate::route::stream::StreamPage;
|
use crate::route::stream::StreamPage;
|
||||||
|
use crate::services::ndb_wrapper::NDBWrapper;
|
||||||
use crate::services::profile::ProfileService;
|
use crate::services::profile::ProfileService;
|
||||||
use crate::widgets::{Header, StreamList};
|
use crate::widgets::{Header, NostrWidget, StreamList};
|
||||||
use egui::{Context, Response, ScrollArea, Ui, Widget};
|
use egui::{Context, Response, ScrollArea, Ui, Widget};
|
||||||
use egui_inbox::{UiInbox, UiInboxSender};
|
use egui_inbox::{UiInbox, UiInboxSender};
|
||||||
use egui_video::Player;
|
use egui_video::{Player, PlayerState};
|
||||||
use log::{info, warn};
|
use log::{info, warn};
|
||||||
use nostr_sdk::Client;
|
use nostr_sdk::nips::nip01;
|
||||||
|
use nostr_sdk::{Client, Kind, PublicKey};
|
||||||
use nostrdb::{Filter, Ndb, Note, Transaction};
|
use nostrdb::{Filter, Ndb, Note, Transaction};
|
||||||
|
use std::borrow::Borrow;
|
||||||
|
|
||||||
mod stream;
|
mod stream;
|
||||||
mod home;
|
mod home;
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
pub enum Routes {
|
pub enum Routes {
|
||||||
HomePage,
|
HomePage,
|
||||||
Event {
|
Event {
|
||||||
@ -30,85 +34,90 @@ pub enum Routes {
|
|||||||
Action(RouteAction),
|
Action(RouteAction),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
pub enum RouteAction {
|
pub enum RouteAction {
|
||||||
Login([u8; 32]),
|
Login([u8; 32]),
|
||||||
StartPlayer(String),
|
|
||||||
PausePlayer,
|
|
||||||
SeekPlayer(f32),
|
|
||||||
StopPlayer,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Router {
|
pub struct Router {
|
||||||
current: Routes,
|
current: Routes,
|
||||||
|
current_widget: Option<Box<dyn NostrWidget>>,
|
||||||
router: UiInbox<Routes>,
|
router: UiInbox<Routes>,
|
||||||
|
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
profile_service: ProfileService,
|
profile_service: ProfileService,
|
||||||
ndb: Ndb,
|
ndb: NDBWrapper,
|
||||||
login: Option<[u8; 32]>,
|
login: Option<[u8; 32]>,
|
||||||
player: Option<Player>,
|
client: Client,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Router {
|
impl Router {
|
||||||
pub fn new(rx: UiInbox<Routes>, ctx: Context, client: Client, ndb: Ndb) -> Self {
|
pub fn new(ctx: Context, client: Client, ndb: Ndb) -> Self {
|
||||||
Self {
|
Self {
|
||||||
current: Routes::HomePage,
|
current: Routes::HomePage,
|
||||||
router: rx,
|
current_widget: None,
|
||||||
|
router: UiInbox::new(),
|
||||||
ctx: ctx.clone(),
|
ctx: ctx.clone(),
|
||||||
profile_service: ProfileService::new(client.clone(), ctx.clone()),
|
profile_service: ProfileService::new(client.clone(), ctx.clone()),
|
||||||
ndb,
|
ndb: NDBWrapper::new(ctx.clone(), ndb.clone(), client.clone()),
|
||||||
|
client,
|
||||||
login: None,
|
login: None,
|
||||||
player: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn load_widget(&mut self, route: Routes, tx: &Transaction) {
|
||||||
|
match &route {
|
||||||
|
Routes::HomePage => {
|
||||||
|
let w = HomePage::new(&self.ndb, tx);
|
||||||
|
self.current_widget = Some(Box::new(w));
|
||||||
|
}
|
||||||
|
Routes::Event { link, .. } => {
|
||||||
|
let w = StreamPage::new_from_link(&self.ndb, tx, link.clone());
|
||||||
|
self.current_widget = Some(Box::new(w));
|
||||||
|
}
|
||||||
|
_ => warn!("Not implemented")
|
||||||
|
}
|
||||||
|
self.current = route;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn show(&mut self, ui: &mut Ui) -> Response {
|
pub fn show(&mut self, ui: &mut Ui) -> Response {
|
||||||
let tx = Transaction::new(&self.ndb).unwrap();
|
let tx = self.ndb.start_transaction();
|
||||||
|
|
||||||
// handle app state changes
|
// handle app state changes
|
||||||
while let Some(r) = self.router.read(ui).next() {
|
while let Some(r) = self.router.read(ui).next() {
|
||||||
if let Routes::Action(a) = r {
|
if let Routes::Action(a) = &r {
|
||||||
match a {
|
match a {
|
||||||
RouteAction::Login(k) => {
|
RouteAction::Login(k) => {
|
||||||
self.login = Some(k)
|
self.login = Some(k.clone())
|
||||||
}
|
|
||||||
RouteAction::StartPlayer(u) => {
|
|
||||||
if self.player.is_none() {
|
|
||||||
if let Ok(p) = Player::new(&self.ctx, &u) {
|
|
||||||
self.player = Some(p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_ => info!("Not implemented")
|
_ => info!("Not implemented")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.current = r;
|
self.load_widget(r, &tx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut svc = RouteServices {
|
// load homepage on start
|
||||||
|
if self.current_widget.is_none() {
|
||||||
|
self.load_widget(Routes::HomePage, &tx);
|
||||||
|
}
|
||||||
|
|
||||||
|
let svc = RouteServices {
|
||||||
context: self.ctx.clone(),
|
context: self.ctx.clone(),
|
||||||
profile: &self.profile_service,
|
profile: &self.profile_service,
|
||||||
router: self.router.sender(),
|
router: self.router.sender(),
|
||||||
ndb: self.ndb.clone(),
|
ndb: self.ndb.clone(),
|
||||||
tx: &tx,
|
tx: &tx,
|
||||||
login: &self.login,
|
login: &self.login,
|
||||||
player: &mut self.player,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// display app
|
// display app
|
||||||
ScrollArea::vertical().show(ui, |ui| {
|
ScrollArea::vertical().show(ui, |ui| {
|
||||||
ui.add(Header::new(&svc));
|
ui.add(Header::new(&svc));
|
||||||
match &self.current {
|
if let Some(w) = self.current_widget.as_mut() {
|
||||||
Routes::HomePage => {
|
w.render(ui, &svc)
|
||||||
HomePage::new(&svc).ui(ui)
|
} else {
|
||||||
}
|
ui.label("No widget")
|
||||||
Routes::Event { link, event } => {
|
|
||||||
StreamPage::new(&mut svc, link, event)
|
|
||||||
.ui(ui)
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
ui.label("Not found")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}).inner
|
}).inner
|
||||||
}
|
}
|
||||||
@ -117,9 +126,8 @@ impl Router {
|
|||||||
pub struct RouteServices<'a> {
|
pub struct RouteServices<'a> {
|
||||||
pub context: Context, //cloned
|
pub context: Context, //cloned
|
||||||
pub router: UiInboxSender<Routes>, //cloned
|
pub router: UiInboxSender<Routes>, //cloned
|
||||||
pub ndb: Ndb, //cloned
|
pub ndb: NDBWrapper, //cloned
|
||||||
|
|
||||||
pub player: &'a mut Option<Player>,
|
|
||||||
pub profile: &'a ProfileService, //ref
|
pub profile: &'a ProfileService, //ref
|
||||||
pub tx: &'a Transaction, //ref
|
pub tx: &'a Transaction, //ref
|
||||||
pub login: &'a Option<[u8; 32]>, //ref
|
pub login: &'a Option<[u8; 32]>, //ref
|
||||||
|
@ -1,51 +1,83 @@
|
|||||||
use crate::link::NostrLink;
|
use crate::link::NostrLink;
|
||||||
use crate::note_util::{NoteUtil, OwnedNote};
|
use crate::note_util::{NoteUtil, OwnedNote};
|
||||||
use crate::route::{RouteAction, RouteServices, Routes};
|
use crate::route::RouteServices;
|
||||||
|
use crate::services::ndb_wrapper::NDBWrapper;
|
||||||
use crate::stream_info::StreamInfo;
|
use crate::stream_info::StreamInfo;
|
||||||
use crate::widgets::StreamPlayer;
|
use crate::widgets::{Chat, NostrWidget, StreamPlayer};
|
||||||
use egui::{Color32, Label, Response, RichText, TextWrapMode, Ui, Widget};
|
use egui::{Color32, Label, Response, RichText, TextWrapMode, Ui, Widget};
|
||||||
use nostrdb::Note;
|
use nostrdb::{Filter, NoteKey, Subscription, Transaction};
|
||||||
use std::ptr;
|
use std::borrow::Borrow;
|
||||||
|
|
||||||
pub struct StreamPage<'a> {
|
pub struct StreamPage {
|
||||||
services: &'a mut RouteServices<'a>,
|
link: NostrLink,
|
||||||
link: &'a NostrLink,
|
event: Option<OwnedNote>,
|
||||||
event: &'a Option<OwnedNote>,
|
player: Option<StreamPlayer>,
|
||||||
|
chat: Option<Chat>,
|
||||||
|
sub: Subscription,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> StreamPage<'a> {
|
impl StreamPage {
|
||||||
pub fn new(services: &'a mut RouteServices<'a>, link: &'a NostrLink, event: &'a Option<OwnedNote>) -> Self {
|
pub fn new_from_link(ndb: &NDBWrapper, tx: &Transaction, link: NostrLink) -> Self {
|
||||||
Self { services, link, event }
|
let f: Filter = link.borrow().try_into().unwrap();
|
||||||
|
let f = [
|
||||||
|
f.limit_mut(1)
|
||||||
|
];
|
||||||
|
let (sub, events) = ndb.subscribe_with_results(&f, tx, 1);
|
||||||
|
Self {
|
||||||
|
link,
|
||||||
|
sub,
|
||||||
|
event: events.first().map_or(None, |n| Some(OwnedNote(n.note_key.as_u64()))),
|
||||||
|
chat: None,
|
||||||
|
player: None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Widget for StreamPage<'a> {
|
impl NostrWidget for StreamPage {
|
||||||
fn ui(self, ui: &mut Ui) -> Response {
|
fn render(&mut self, ui: &mut Ui, services: &RouteServices<'_>) -> Response {
|
||||||
let event = if let Some(event) = self.event {
|
let poll = services.ndb.poll(self.sub, 1);
|
||||||
Note::Owned {
|
if let Some(k) = poll.first() {
|
||||||
ptr: ptr::null_mut(),
|
self.event = Some(OwnedNote(k.as_u64()))
|
||||||
size: 0,
|
}
|
||||||
|
|
||||||
|
let event = if let Some(k) = &self.event {
|
||||||
|
services.ndb.get_note_by_key(services.tx, NoteKey::new(k.0))
|
||||||
|
.map_or(None, |f| Some(f))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
if let Some(event) = event {
|
||||||
|
if let Some(stream) = event.stream() {
|
||||||
|
if self.player.is_none() {
|
||||||
|
let p = StreamPlayer::new(ui.ctx(), &stream);
|
||||||
|
self.player = Some(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(player) = &mut self.player {
|
||||||
|
player.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));
|
||||||
|
|
||||||
|
if self.chat.is_none() {
|
||||||
|
let chat = Chat::new(self.link.clone(), &services.ndb, services.tx);
|
||||||
|
self.chat = Some(chat);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(c) = self.chat.as_mut() {
|
||||||
|
c.render(ui, services)
|
||||||
|
} else {
|
||||||
|
ui.label("Loading..")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let mut q = self.services.ndb.query(self.services.tx, &[
|
ui.label("Loading..")
|
||||||
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 +1,2 @@
|
|||||||
pub mod profile;
|
pub mod profile;
|
||||||
|
pub mod ndb_wrapper;
|
82
src/services/ndb_wrapper.rs
Normal file
82
src/services/ndb_wrapper.rs
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
use egui::CursorIcon::Default;
|
||||||
|
use log::{info, warn};
|
||||||
|
use nostr_sdk::secp256k1::Context;
|
||||||
|
use nostr_sdk::{Client, JsonUtil, Kind, RelayPoolNotification};
|
||||||
|
use nostrdb::{Error, Filter, Ndb, Note, NoteKey, ProfileRecord, QueryResult, Subscription, Transaction};
|
||||||
|
use tokio::sync::mpsc::UnboundedSender;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct NDBWrapper {
|
||||||
|
ctx: egui::Context,
|
||||||
|
ndb: Ndb,
|
||||||
|
client: Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NDBWrapper {
|
||||||
|
pub fn new(ctx: egui::Context, ndb: Ndb, client: Client) -> Self {
|
||||||
|
let client_clone = client.clone();
|
||||||
|
let ndb_clone = ndb.clone();
|
||||||
|
let ctx_clone = ctx.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let mut notifications = client_clone.notifications();
|
||||||
|
while let Ok(e) = notifications.recv().await {
|
||||||
|
match e {
|
||||||
|
RelayPoolNotification::Event { event, .. } => {
|
||||||
|
if let Err(e) = ndb_clone.process_event(event.as_json().as_str()) {
|
||||||
|
warn!("Failed to process event: {:?}", e);
|
||||||
|
} else {
|
||||||
|
ctx_clone.request_repaint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// dont care
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Self { ctx, ndb, client }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start_transaction(&self) -> Transaction {
|
||||||
|
Transaction::new(&self.ndb).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn subscribe(&self, filters: &[Filter]) -> Subscription {
|
||||||
|
let sub = self.ndb.subscribe(filters).unwrap();
|
||||||
|
let c_clone = self.client.clone();
|
||||||
|
let filters = filters.iter().map(|f| nostr_sdk::Filter::from_json(f.json().unwrap()).unwrap()).collect();
|
||||||
|
let id_clone = sub.id();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let nostr_sub = c_clone.subscribe(filters, None).await.unwrap();
|
||||||
|
info!("Sub mapping {}->{}", id_clone, nostr_sub.id())
|
||||||
|
});
|
||||||
|
sub
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unsubscribe(&self, sub: Subscription) {
|
||||||
|
self.ndb.unsubscribe(sub).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn subscribe_with_results<'a>(&self, filters: &[Filter], tx: &'a Transaction, max_results: i32) -> (Subscription, Vec<QueryResult<'a>>) {
|
||||||
|
let sub = self.subscribe(filters);
|
||||||
|
let q = self.query(tx, filters, max_results);
|
||||||
|
(sub, q)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub fn query<'a>(&self, tx: &'a Transaction, filters: &[Filter], max_results: i32) -> Vec<QueryResult<'a>> {
|
||||||
|
self.ndb.query(tx, filters, max_results).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn poll(&self, sub: Subscription, max_results: u32) -> Vec<NoteKey> {
|
||||||
|
self.ndb.poll_for_notes(sub, max_results)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_note_by_key<'a>(&self, tx: &'a Transaction, key: NoteKey) -> Result<Note<'a>, Error> {
|
||||||
|
self.ndb.get_note_by_key(tx, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_profile_by_pubkey<'a>(&self, tx: &'a Transaction, pubkey: &[u8; 32]) -> Result<ProfileRecord<'a>, Error> {
|
||||||
|
self.ndb.get_profile_by_pubkey(tx, pubkey)
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
use egui::{Color32, Image, Rect, Response, Rounding, Sense, Ui, Vec2, Widget};
|
|
||||||
use crate::services::profile::ProfileService;
|
use crate::services::profile::ProfileService;
|
||||||
|
use egui::{Color32, Image, Rect, Response, Rounding, Sense, Ui, Vec2, Widget};
|
||||||
|
|
||||||
pub struct Avatar<'a> {
|
pub struct Avatar<'a> {
|
||||||
image: Option<Image<'a>>,
|
image: Option<Image<'a>>,
|
||||||
@ -10,6 +10,10 @@ impl<'a> Avatar<'a> {
|
|||||||
Self { image: Some(img) }
|
Self { image: Some(img) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn new_optional(img: Option<Image<'a>>) -> Self {
|
||||||
|
Self { image: img }
|
||||||
|
}
|
||||||
|
|
||||||
pub fn public_key(svc: &'a ProfileService, pk: &[u8; 32]) -> Self {
|
pub fn public_key(svc: &'a ProfileService, pk: &[u8; 32]) -> Self {
|
||||||
if let Some(meta) = svc.get_profile(pk) {
|
if let Some(meta) = svc.get_profile(pk) {
|
||||||
if let Some(img) = &meta.picture {
|
if let Some(img) = &meta.picture {
|
||||||
@ -45,8 +49,10 @@ impl<'a> Widget for Avatar<'a> {
|
|||||||
img.rounding(Rounding::same(ui.available_height())).ui(ui)
|
img.rounding(Rounding::same(ui.available_height())).ui(ui)
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
let (response, painter) = ui.allocate_painter(Vec2::new(32., 32.), Sense::hover());
|
let h = ui.available_height();
|
||||||
painter.rect_filled(Rect::EVERYTHING, Rounding::same(32.), Color32::from_rgb(200, 200, 200));
|
let rnd = Rounding::same(h);
|
||||||
|
let (response, painter) = ui.allocate_painter(Vec2::new(h,h), Sense::click());
|
||||||
|
painter.rect_filled(Rect::EVERYTHING, rnd, Color32::from_rgb(200, 200, 200));
|
||||||
response
|
response
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
54
src/widgets/chat.rs
Normal file
54
src/widgets/chat.rs
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
use crate::link::NostrLink;
|
||||||
|
use crate::note_util::OwnedNote;
|
||||||
|
use crate::route::RouteServices;
|
||||||
|
use crate::services::ndb_wrapper::NDBWrapper;
|
||||||
|
use crate::widgets::chat_message::ChatMessage;
|
||||||
|
use crate::widgets::NostrWidget;
|
||||||
|
use egui::{Response, ScrollArea, Ui, Widget};
|
||||||
|
use nostrdb::{Filter, Note, NoteKey, Subscription, Transaction};
|
||||||
|
use std::borrow::Borrow;
|
||||||
|
|
||||||
|
pub struct Chat {
|
||||||
|
link: NostrLink,
|
||||||
|
events: Vec<OwnedNote>,
|
||||||
|
sub: Subscription,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Chat {
|
||||||
|
pub fn new(link: NostrLink, ndb: &NDBWrapper, tx: &Transaction) -> Self {
|
||||||
|
let filter = Filter::new()
|
||||||
|
.kinds([1_311])
|
||||||
|
.tags([link.to_tag_value()], 'a')
|
||||||
|
.build();
|
||||||
|
let filter = [filter];
|
||||||
|
|
||||||
|
let (sub, events) = ndb.subscribe_with_results(&filter, tx, 500);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
link,
|
||||||
|
sub,
|
||||||
|
events: events.iter().map(|n| OwnedNote(n.note_key.as_u64())).collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NostrWidget for Chat {
|
||||||
|
fn render(&mut self, ui: &mut Ui, services: &RouteServices<'_>) -> Response {
|
||||||
|
let poll = services.ndb.poll(self.sub, 500);
|
||||||
|
poll.iter().for_each(|n| self.events.push(OwnedNote(n.as_u64())));
|
||||||
|
|
||||||
|
let events: Vec<Note> = self.events.iter().map_while(|n|
|
||||||
|
services.ndb
|
||||||
|
.get_note_by_key(services.tx, NoteKey::new(n.0))
|
||||||
|
.map_or(None, |n| Some(n))
|
||||||
|
).collect();
|
||||||
|
|
||||||
|
ScrollArea::vertical().show(ui, |ui| {
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
for ev in events {
|
||||||
|
ChatMessage::new(&ev, services).ui(ui);
|
||||||
|
}
|
||||||
|
}).response
|
||||||
|
}).inner
|
||||||
|
}
|
||||||
|
}
|
31
src/widgets/chat_message.rs
Normal file
31
src/widgets/chat_message.rs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
use crate::route::RouteServices;
|
||||||
|
use crate::widgets::Profile;
|
||||||
|
use eframe::epaint::Vec2;
|
||||||
|
use egui::{Response, Ui, Widget};
|
||||||
|
use nostrdb::Note;
|
||||||
|
|
||||||
|
pub struct ChatMessage<'a> {
|
||||||
|
ev: &'a Note<'a>,
|
||||||
|
services: &'a RouteServices<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ChatMessage<'a> {
|
||||||
|
pub fn new(ev: &'a Note<'a>, services: &'a RouteServices<'a>) -> ChatMessage<'a> {
|
||||||
|
ChatMessage { ev, services }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Widget for ChatMessage<'a> {
|
||||||
|
fn ui(self, ui: &mut Ui) -> Response {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.spacing_mut().item_spacing = Vec2::new(8., 2.);
|
||||||
|
let author = self.ev.pubkey();
|
||||||
|
Profile::new(author, self.services)
|
||||||
|
.size(24.)
|
||||||
|
.ui(ui);
|
||||||
|
|
||||||
|
let content = self.ev.content();
|
||||||
|
ui.label(content);
|
||||||
|
}).response
|
||||||
|
}
|
||||||
|
}
|
@ -4,8 +4,21 @@ mod stream_list;
|
|||||||
mod avatar;
|
mod avatar;
|
||||||
mod stream_player;
|
mod stream_player;
|
||||||
mod video_placeholder;
|
mod video_placeholder;
|
||||||
|
mod chat;
|
||||||
|
mod chat_message;
|
||||||
|
mod profile;
|
||||||
|
|
||||||
|
use egui::{Response, Ui};
|
||||||
|
use crate::route::RouteServices;
|
||||||
|
|
||||||
|
pub trait NostrWidget {
|
||||||
|
fn render(&mut self, ui: &mut Ui, services: &RouteServices<'_>) -> Response;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub use self::avatar::Avatar;
|
||||||
pub use self::header::Header;
|
pub use self::header::Header;
|
||||||
pub use self::stream_list::StreamList;
|
pub use self::stream_list::StreamList;
|
||||||
pub use self::video_placeholder::VideoPlaceholder;
|
pub use self::video_placeholder::VideoPlaceholder;
|
||||||
pub use self::stream_player::StreamPlayer;
|
pub use self::stream_player::StreamPlayer;
|
||||||
|
pub use self::profile::Profile;
|
||||||
|
pub use self::chat::Chat;
|
||||||
|
41
src/widgets/profile.rs
Normal file
41
src/widgets/profile.rs
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
use crate::route::RouteServices;
|
||||||
|
use crate::widgets::Avatar;
|
||||||
|
use egui::{Color32, Image, Label, Response, RichText, TextWrapMode, Ui, Widget};
|
||||||
|
use nostrdb::NdbProfile;
|
||||||
|
|
||||||
|
pub struct Profile<'a> {
|
||||||
|
size: f32,
|
||||||
|
pubkey: &'a [u8; 32],
|
||||||
|
profile: Option<NdbProfile<'a>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Profile<'a> {
|
||||||
|
pub fn new(pubkey: &'a [u8; 32], services: &'a RouteServices<'a>) -> Self {
|
||||||
|
let p = services.ndb.get_profile_by_pubkey(services.tx, &pubkey)
|
||||||
|
.map(|f| f.record().profile())
|
||||||
|
.map_or(None, |r| r);
|
||||||
|
|
||||||
|
Self { pubkey, size: 40., profile: p }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn size(self, size: f32) -> Self {
|
||||||
|
Self { size, ..self }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Widget for Profile<'a> {
|
||||||
|
fn ui(self, ui: &mut Ui) -> Response {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.spacing_mut().item_spacing.x = 8.;
|
||||||
|
|
||||||
|
let img = self.profile.map_or(None, |f| f.picture().map(|f| Image::from_uri(f)));
|
||||||
|
ui.add(Avatar::new_optional(img).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));
|
||||||
|
}).response
|
||||||
|
}
|
||||||
|
}
|
@ -21,7 +21,7 @@ impl Widget for StreamList<'_> {
|
|||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
ui.vertical(|ui| {
|
ui.vertical(|ui| {
|
||||||
ui.style_mut().spacing.item_spacing = egui::vec2(0., 20.0);
|
ui.style_mut().spacing.item_spacing = egui::vec2(0., 20.0);
|
||||||
for event in self.streams.iter().take(5) {
|
for event in self.streams {
|
||||||
ui.add(StreamEvent::new(event, self.services));
|
ui.add(StreamEvent::new(event, self.services));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -1,24 +1,29 @@
|
|||||||
use crate::route::RouteServices;
|
|
||||||
use crate::widgets::VideoPlaceholder;
|
use crate::widgets::VideoPlaceholder;
|
||||||
use egui::{Response, Ui, Vec2, Widget};
|
use egui::{Context, Response, Ui, Vec2, Widget};
|
||||||
|
use egui_video::Player;
|
||||||
|
|
||||||
pub struct StreamPlayer<'a> {
|
pub struct StreamPlayer {
|
||||||
services: &'a mut RouteServices<'a>,
|
player: Option<Player>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> StreamPlayer<'a> {
|
impl StreamPlayer {
|
||||||
pub fn new(services: &'a mut RouteServices<'a>) -> Self {
|
pub fn new(ctx: &Context, url: &String) -> Self {
|
||||||
Self { services }
|
Self {
|
||||||
|
player: Player::new(ctx, url).map_or(None, |mut f| {
|
||||||
|
f.start();
|
||||||
|
Some(f)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Widget for StreamPlayer<'a> {
|
impl Widget for &mut StreamPlayer {
|
||||||
fn ui(self, ui: &mut Ui) -> Response {
|
fn ui(self, ui: &mut Ui) -> Response {
|
||||||
let w = ui.available_width();
|
let w = ui.available_width();
|
||||||
let h = w / 16. * 9.;
|
let h = w / 16. * 9.;
|
||||||
let size = Vec2::new(w, h);
|
let size = Vec2::new(w, h);
|
||||||
|
|
||||||
if let Some(p) = self.services.player.as_mut() {
|
if let Some(mut p) = self.player.as_mut() {
|
||||||
p.ui(ui, size)
|
p.ui(ui, size)
|
||||||
} else {
|
} else {
|
||||||
VideoPlaceholder.ui(ui)
|
VideoPlaceholder.ui(ui)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user