feat: ffmpeg image loader
This commit is contained in:
parent
d21a45c941
commit
c62fbfe510
763
Cargo.lock
generated
763
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -12,8 +12,6 @@ egui = { version = "0.29.1" }
|
|||||||
eframe = { version = "0.29.1", default-features = false, features = ["glow", "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", version = "0.3.4" }
|
nostrdb = { git = "https://github.com/damus-io/nostrdb-rs", version = "0.3.4" }
|
||||||
nostr-sdk = { version = "0.35.0", features = ["all-nips"] }
|
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"
|
log = "0.4.22"
|
||||||
pretty_env_logger = "0.5.0"
|
pretty_env_logger = "0.5.0"
|
||||||
egui_inbox = "0.6.0"
|
egui_inbox = "0.6.0"
|
||||||
@ -26,11 +24,12 @@ async-trait = "0.1.83"
|
|||||||
sha2 = "0.10.8"
|
sha2 = "0.10.8"
|
||||||
reqwest = { version = "0.12.7", default-features = false, features = ["rustls-tls-native-roots"] }
|
reqwest = { version = "0.12.7", default-features = false, features = ["rustls-tls-native-roots"] }
|
||||||
itertools = "0.13.0"
|
itertools = "0.13.0"
|
||||||
|
lru = "0.12.5"
|
||||||
|
|
||||||
egui-video = { git = "https://github.com/v0l/egui-video.git", rev = "396d0041b437d2354f7a5d4e61c8ce33a69eb3b2" }
|
egui-video = { git = "https://github.com/v0l/egui-video.git", rev = "eb2675dd5206d064afdd82ea72c0fac083596d86" }
|
||||||
#egui-video = { path = "../egui-video" }
|
#egui-video = { path = "../egui-video" }
|
||||||
|
|
||||||
[target.'cfg(target_os = "android")'.dependencies]
|
[target.'cfg(target_os = "android")'.dependencies]
|
||||||
android_logger = "0.14.1"
|
android_logger = "0.14.1"
|
||||||
android-activity = { version = "0.6.0", features = ["native-activity"] }
|
android-activity = { version = "0.6.0", features = ["native-activity"] }
|
||||||
winit = { version = "0.30.5", features = ["android-native-activity"] }
|
winit = { version = "0.30.5", features = ["android-native-activity"] }
|
||||||
|
@ -33,7 +33,6 @@ impl ZapStreamApp {
|
|||||||
.expect("Failed to add relay");
|
.expect("Failed to add relay");
|
||||||
client_clone.connect().await;
|
client_clone.connect().await;
|
||||||
});
|
});
|
||||||
egui_extras::install_image_loaders(&cc.egui_ctx);
|
|
||||||
|
|
||||||
let ndb_path = data_path.join("ndb");
|
let ndb_path = data_path.join("ndb");
|
||||||
std::fs::create_dir_all(&ndb_path).expect("Failed to create ndb directory");
|
std::fs::create_dir_all(&ndb_path).expect("Failed to create ndb directory");
|
||||||
|
13
src/lib.rs
13
src/lib.rs
@ -1,16 +1,15 @@
|
|||||||
|
|
||||||
pub mod app;
|
pub mod app;
|
||||||
mod link;
|
mod link;
|
||||||
|
mod note_store;
|
||||||
mod note_util;
|
mod note_util;
|
||||||
mod route;
|
mod route;
|
||||||
mod services;
|
mod services;
|
||||||
mod stream_info;
|
mod stream_info;
|
||||||
mod widgets;
|
|
||||||
mod theme;
|
mod theme;
|
||||||
mod note_store;
|
mod widgets;
|
||||||
|
|
||||||
use eframe::Renderer;
|
|
||||||
use crate::app::ZapStreamApp;
|
use crate::app::ZapStreamApp;
|
||||||
|
use eframe::Renderer;
|
||||||
|
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
use winit::platform::android::activity::AndroidApp;
|
use winit::platform::android::activity::AndroidApp;
|
||||||
@ -22,7 +21,9 @@ use winit::platform::android::EventLoopBuilderExtAndroid;
|
|||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
pub async fn android_main(app: AndroidApp) {
|
pub async fn android_main(app: AndroidApp) {
|
||||||
std::env::set_var("RUST_BACKTRACE", "full");
|
std::env::set_var("RUST_BACKTRACE", "full");
|
||||||
android_logger::init_once(android_logger::Config::default().with_max_level(log::LevelFilter::Info));
|
android_logger::init_once(
|
||||||
|
android_logger::Config::default().with_max_level(log::LevelFilter::Info),
|
||||||
|
);
|
||||||
|
|
||||||
let mut options = eframe::NativeOptions::default();
|
let mut options = eframe::NativeOptions::default();
|
||||||
options.renderer = Renderer::Glow;
|
options.renderer = Renderer::Glow;
|
||||||
@ -42,4 +43,4 @@ pub async fn android_main(app: AndroidApp) {
|
|||||||
options,
|
options,
|
||||||
Box::new(move |cc| Ok(Box::new(ZapStreamApp::new(cc, data_path)))),
|
Box::new(move |cc| Ok(Box::new(ZapStreamApp::new(cc, data_path)))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -59,7 +59,13 @@ impl NostrLink {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_note(note: &Note<'_>) -> Self {
|
pub fn from_note(note: &Note<'_>) -> Self {
|
||||||
if note.kind() >= 30_000 && note.kind() < 40_000 && note.get_tag_value("d").and_then(|v| v.variant().str()).is_some() {
|
if note.kind() >= 30_000
|
||||||
|
&& note.kind() < 40_000
|
||||||
|
&& note
|
||||||
|
.get_tag_value("d")
|
||||||
|
.and_then(|v| v.variant().str())
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
Self {
|
Self {
|
||||||
hrp: NostrLinkType::Coordinate,
|
hrp: NostrLinkType::Coordinate,
|
||||||
id: IdOrStr::Str(
|
id: IdOrStr::Str(
|
||||||
|
@ -9,7 +9,7 @@ pub struct NoteStore<'a> {
|
|||||||
impl<'a> NoteStore<'a> {
|
impl<'a> NoteStore<'a> {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
events: HashMap::new()
|
events: HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,12 +36,10 @@ impl<'a> NoteStore<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn key(note: &Note<'a>) -> String {
|
pub fn key(note: &Note<'a>) -> String {
|
||||||
NostrLink::from_note(note)
|
NostrLink::from_note(note).to_tag_value()
|
||||||
.to_tag_value()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn iter(&self) -> impl Iterator<Item=&Note<'a>> {
|
pub fn iter(&self) -> impl Iterator<Item = &Note<'a>> {
|
||||||
self.events.values()
|
self.events.values()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,8 +42,7 @@ impl NostrWidget for HomePage {
|
|||||||
|
|
||||||
let events = NoteStore::from_vec(events);
|
let events = NoteStore::from_vec(events);
|
||||||
ScrollArea::vertical()
|
ScrollArea::vertical()
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| widgets::StreamList::new(&events, services).ui(ui))
|
||||||
widgets::StreamList::new(&events, services).ui(ui)
|
.inner
|
||||||
}).inner
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,10 +25,7 @@ impl NostrWidget for LoginPage {
|
|||||||
ui.label(RichText::new("Login").size(32.));
|
ui.label(RichText::new("Login").size(32.));
|
||||||
ui.label("Pubkey");
|
ui.label("Pubkey");
|
||||||
ui.text_edit_singleline(&mut self.key);
|
ui.text_edit_singleline(&mut self.key);
|
||||||
if Button::new()
|
if Button::new().show(ui, |ui| ui.label("Login")).clicked() {
|
||||||
.show(ui, |ui| {
|
|
||||||
ui.label("Login")
|
|
||||||
}).clicked() {
|
|
||||||
if let Ok(pk) = hex::decode(&self.key) {
|
if let Ok(pk) = hex::decode(&self.key) {
|
||||||
if let Ok(pk) = pk.as_slice().try_into() {
|
if let Ok(pk) = pk.as_slice().try_into() {
|
||||||
services.action(RouteAction::LoginPubkey(pk));
|
services.action(RouteAction::LoginPubkey(pk));
|
||||||
@ -41,6 +38,7 @@ impl NostrWidget for LoginPage {
|
|||||||
if let Some(e) = &self.error {
|
if let Some(e) = &self.error {
|
||||||
ui.label(RichText::new(e).color(Color32::RED));
|
ui.label(RichText::new(e).color(Color32::RED));
|
||||||
}
|
}
|
||||||
}).response
|
})
|
||||||
|
.response
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,8 +14,8 @@ use nostrdb::{Ndb, Transaction};
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
mod home;
|
mod home;
|
||||||
mod stream;
|
|
||||||
mod login;
|
mod login;
|
||||||
|
mod stream;
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
pub enum Routes {
|
pub enum Routes {
|
||||||
@ -123,7 +123,8 @@ impl Router {
|
|||||||
} else {
|
} else {
|
||||||
ui.label("No widget")
|
ui.label("No widget")
|
||||||
}
|
}
|
||||||
}).response
|
})
|
||||||
|
.response
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,8 +25,7 @@ impl StreamPage {
|
|||||||
Self {
|
Self {
|
||||||
link,
|
link,
|
||||||
sub,
|
sub,
|
||||||
event: events
|
event: events.first().map(|n| OwnedNote(n.note_key.as_u64())),
|
||||||
.first().map(|n| OwnedNote(n.note_key.as_u64())),
|
|
||||||
chat: None,
|
chat: None,
|
||||||
player: None,
|
player: None,
|
||||||
new_msg: WriteChat::new(),
|
new_msg: WriteChat::new(),
|
||||||
@ -44,7 +43,8 @@ impl NostrWidget for StreamPage {
|
|||||||
let event = if let Some(k) = &self.event {
|
let event = if let Some(k) = &self.event {
|
||||||
services
|
services
|
||||||
.ndb
|
.ndb
|
||||||
.get_note_by_key(services.tx, NoteKey::new(k.0)).ok()
|
.get_note_by_key(services.tx, NoteKey::new(k.0))
|
||||||
|
.ok()
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
@ -79,9 +79,8 @@ impl NostrWidget for StreamPage {
|
|||||||
// consume rest of space
|
// consume rest of space
|
||||||
ui.add_space(ui.available_height());
|
ui.add_space(ui.available_height());
|
||||||
});
|
});
|
||||||
ui.allocate_ui(Vec2::new(w, chat_h), |ui| {
|
ui.allocate_ui(Vec2::new(w, chat_h), |ui| self.new_msg.render(ui, services))
|
||||||
self.new_msg.render(ui, services)
|
.response
|
||||||
}).response
|
|
||||||
} else {
|
} else {
|
||||||
ui.label("Loading..")
|
ui.label("Loading..")
|
||||||
}
|
}
|
||||||
|
54
src/services/ffmpeg_loader.rs
Normal file
54
src/services/ffmpeg_loader.rs
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
use anyhow::Error;
|
||||||
|
use egui::ColorImage;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
pub struct FfmpegLoader {}
|
||||||
|
|
||||||
|
impl FfmpegLoader {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_image(&self, path: PathBuf) -> Result<ColorImage, Error> {
|
||||||
|
unsafe {
|
||||||
|
let mut demux = egui_video::ffmpeg::demux::Demuxer::new(path.to_str().unwrap());
|
||||||
|
let info = demux.probe_input()?;
|
||||||
|
|
||||||
|
let bv = info.best_video();
|
||||||
|
if bv.is_none() {
|
||||||
|
anyhow::bail!("Not a video/image");
|
||||||
|
}
|
||||||
|
let bv = bv.unwrap();
|
||||||
|
let mut decode = egui_video::ffmpeg::decode::Decoder::new();
|
||||||
|
let rgb = egui_video::ffmpeg_sys_the_third::AVPixelFormat::AV_PIX_FMT_RGB24;
|
||||||
|
let mut scaler = egui_video::ffmpeg::scale::Scaler::new(rgb);
|
||||||
|
|
||||||
|
let mut n_pkt = 0;
|
||||||
|
loop {
|
||||||
|
let (mut pkt, stream) = demux.get_packet()?;
|
||||||
|
if (*stream).index as usize == bv.index {
|
||||||
|
let frames = decode.decode_pkt(pkt, stream)?;
|
||||||
|
if let Some((frame, _)) = frames.first() {
|
||||||
|
let mut frame = *frame;
|
||||||
|
let mut frame_rgb = scaler.process_frame(
|
||||||
|
frame,
|
||||||
|
(*frame).width as u16,
|
||||||
|
(*frame).height as u16,
|
||||||
|
)?;
|
||||||
|
egui_video::ffmpeg_sys_the_third::av_frame_free(&mut frame);
|
||||||
|
|
||||||
|
let image = egui_video::ffmpeg::video_frame_to_image(frame_rgb);
|
||||||
|
egui_video::ffmpeg_sys_the_third::av_frame_free(&mut frame_rgb);
|
||||||
|
return Ok(image);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
egui_video::ffmpeg_sys_the_third::av_packet_free(&mut pkt);
|
||||||
|
|
||||||
|
n_pkt += 1;
|
||||||
|
if n_pkt > 10 {
|
||||||
|
anyhow::bail!("No image found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,27 +1,47 @@
|
|||||||
use egui::Image;
|
use crate::services::ffmpeg_loader::FfmpegLoader;
|
||||||
|
use crate::theme::NEUTRAL_800;
|
||||||
|
use anyhow::Error;
|
||||||
|
use eframe::epaint::Color32;
|
||||||
|
use egui::load::SizedTexture;
|
||||||
|
use egui::{ColorImage, Context, Image, ImageData, TextureHandle, TextureOptions};
|
||||||
|
use itertools::Itertools;
|
||||||
use log::{error, info};
|
use log::{error, info};
|
||||||
|
use lru::LruCache;
|
||||||
use nostr_sdk::util::hex;
|
use nostr_sdk::util::hex;
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
use std::num::NonZeroUsize;
|
||||||
|
use std::ops::Deref;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::{Arc, Mutex};
|
||||||
use tokio::sync::Mutex;
|
|
||||||
|
type ImageCacheStore = Arc<Mutex<LruCache<String, TextureHandle>>>;
|
||||||
|
|
||||||
pub struct ImageCache {
|
pub struct ImageCache {
|
||||||
ctx: egui::Context,
|
ctx: Context,
|
||||||
dir: PathBuf,
|
dir: PathBuf,
|
||||||
fetch_lock: Arc<Mutex<HashSet<String>>>,
|
placeholder: TextureHandle,
|
||||||
|
cache: ImageCacheStore,
|
||||||
|
fetch_cache: Arc<Mutex<HashSet<String>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ImageCache {
|
impl ImageCache {
|
||||||
pub fn new(data_path: PathBuf, ctx: egui::Context) -> Self {
|
pub fn new(data_path: PathBuf, ctx: Context) -> Self {
|
||||||
let out = data_path.join("cache/images");
|
let out = data_path.join("cache/images");
|
||||||
fs::create_dir_all(&out).unwrap();
|
fs::create_dir_all(&out).unwrap();
|
||||||
|
|
||||||
|
let placeholder = ctx.load_texture(
|
||||||
|
"placeholder",
|
||||||
|
ImageData::from(ColorImage::new([1, 1], NEUTRAL_800)),
|
||||||
|
TextureOptions::default(),
|
||||||
|
);
|
||||||
Self {
|
Self {
|
||||||
ctx,
|
ctx,
|
||||||
dir: out,
|
dir: out,
|
||||||
fetch_lock: Arc::new(Mutex::new(HashSet::new())),
|
placeholder,
|
||||||
|
cache: Arc::new(Mutex::new(LruCache::new(NonZeroUsize::new(100).unwrap()))),
|
||||||
|
fetch_cache: Arc::new(Mutex::new(HashSet::new())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,29 +62,61 @@ impl ImageCache {
|
|||||||
U: Into<String>,
|
U: Into<String>,
|
||||||
{
|
{
|
||||||
let u = url.into();
|
let u = url.into();
|
||||||
|
if let Ok(mut c) = self.cache.lock() {
|
||||||
|
if let Some(i) = c.get(&u) {
|
||||||
|
return Image::from_texture(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
let path = self.find(&u);
|
let path = self.find(&u);
|
||||||
if !path.exists() && !u.is_empty() {
|
if !path.exists() && !u.is_empty() {
|
||||||
let path = path.clone();
|
let path = path.clone();
|
||||||
let fl = self.fetch_lock.clone();
|
let cache = self.cache.clone();
|
||||||
let ctx = self.ctx.clone();
|
let ctx = self.ctx.clone();
|
||||||
|
let fetch_cache = self.fetch_cache.clone();
|
||||||
|
let placeholder = self.placeholder.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
if fl.lock().await.insert(u.clone()) {
|
if fetch_cache.lock().unwrap().insert(u.clone()) {
|
||||||
info!("Fetching image: {}", &u);
|
info!("Fetching image: {}", &u);
|
||||||
if let Ok(data) = reqwest::get(&u)
|
if let Ok(data) = reqwest::get(&u).await {
|
||||||
.await {
|
tokio::fs::create_dir_all(path.parent().unwrap())
|
||||||
tokio::fs::create_dir_all(path.parent().unwrap()).await.unwrap();
|
.await
|
||||||
if let Err(e) = tokio::fs::write(path, data.bytes().await.unwrap()).await {
|
.unwrap();
|
||||||
|
let img_data = data.bytes().await.unwrap();
|
||||||
|
if let Err(e) = tokio::fs::write(path.clone(), img_data).await {
|
||||||
error!("Failed to write file: {}", e);
|
error!("Failed to write file: {}", e);
|
||||||
}
|
}
|
||||||
// forget cached url
|
let t = Self::load_image(&ctx, path, &u)
|
||||||
for t in ctx.loaders().texture.lock().iter() {
|
.await
|
||||||
t.forget(&u);
|
.unwrap_or(placeholder);
|
||||||
}
|
cache.lock().unwrap().put(u.clone(), t);
|
||||||
ctx.request_repaint();
|
ctx.request_repaint();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} else if path.exists() {
|
||||||
|
let path = path.clone();
|
||||||
|
let ctx = self.ctx.clone();
|
||||||
|
let cache = self.cache.clone();
|
||||||
|
let placeholder = self.placeholder.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let t = Self::load_image(&ctx, path, &u)
|
||||||
|
.await
|
||||||
|
.unwrap_or(placeholder);
|
||||||
|
cache.lock().unwrap().put(u.clone(), t);
|
||||||
|
ctx.request_repaint();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
Image::from_uri(format!("file://{}", path.to_str().unwrap()))
|
Image::from_texture(&self.placeholder)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
async fn load_image(ctx: &Context, path: PathBuf, key: &str) -> Option<TextureHandle> {
|
||||||
|
let mut loader = FfmpegLoader::new();
|
||||||
|
match loader.load_image(path) {
|
||||||
|
Ok(i) => Some(ctx.load_texture(key, ImageData::from(i), TextureOptions::default())),
|
||||||
|
Err(e) => {
|
||||||
|
println!("Failed to load image: {}", e);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
pub mod image_cache;
|
||||||
pub mod ndb_wrapper;
|
pub mod ndb_wrapper;
|
||||||
pub mod query;
|
pub mod query;
|
||||||
pub mod image_cache;
|
|
||||||
|
mod ffmpeg_loader;
|
||||||
|
@ -144,11 +144,12 @@ impl NDBWrapper {
|
|||||||
|
|
||||||
// TODO: fix this shit
|
// TODO: fix this shit
|
||||||
if p.is_none() && self.profiles.lock().unwrap().insert(*pubkey) {
|
if p.is_none() && self.profiles.lock().unwrap().insert(*pubkey) {
|
||||||
self.query_manager.queue_query("profile", &[
|
self.query_manager.queue_query(
|
||||||
nostr::Filter::new()
|
"profile",
|
||||||
|
&[nostr::Filter::new()
|
||||||
.kinds([Kind::Metadata])
|
.kinds([Kind::Metadata])
|
||||||
.authors([PublicKey::from_slice(pubkey).unwrap()])
|
.authors([PublicKey::from_slice(pubkey).unwrap()])],
|
||||||
])
|
)
|
||||||
}
|
}
|
||||||
let sub = None;
|
let sub = None;
|
||||||
(p, sub)
|
(p, sub)
|
||||||
|
@ -65,21 +65,24 @@ impl Query {
|
|||||||
let id = Uuid::new_v4();
|
let id = Uuid::new_v4();
|
||||||
|
|
||||||
// remove filters already sent
|
// remove filters already sent
|
||||||
next.retain(|f| self.traces.is_empty() || !self.traces.iter().all(|y| y.filters.iter().any(|z| z == f)));
|
next.retain(|f| {
|
||||||
|
self.traces.is_empty() || !self.traces.iter().all(|y| y.filters.iter().any(|z| z == f))
|
||||||
|
});
|
||||||
|
|
||||||
// force profile queries into single filter
|
// force profile queries into single filter
|
||||||
if next.iter().all(|f| if let Some(k) = &f.kinds {
|
if next.iter().all(|f| {
|
||||||
k.len() == 1 && k.first().unwrap().as_u16() == 0
|
if let Some(k) = &f.kinds {
|
||||||
} else {
|
k.len() == 1 && k.first().unwrap().as_u16() == 0
|
||||||
false
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
}) {
|
}) {
|
||||||
next = vec![Filter::new()
|
next = vec![Filter::new().kinds([Metadata]).authors(
|
||||||
.kinds([Metadata])
|
next.iter()
|
||||||
.authors(next.iter().flat_map(|f| f.authors.as_ref().unwrap().clone()))
|
.flat_map(|f| f.authors.as_ref().unwrap().clone()),
|
||||||
]
|
)]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if next.is_empty() {
|
if next.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@ -166,18 +169,24 @@ where
|
|||||||
where
|
where
|
||||||
F: Into<Vec<QueryFilter>>,
|
F: Into<Vec<QueryFilter>>,
|
||||||
{
|
{
|
||||||
self.queue_into_queries.send(QueueDefer {
|
self.queue_into_queries
|
||||||
id: id.to_string(),
|
.send(QueueDefer {
|
||||||
filters: filters.into(),
|
id: id.to_string(),
|
||||||
}).unwrap()
|
filters: filters.into(),
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl QueryClient for Client {
|
impl QueryClient for Client {
|
||||||
async fn subscribe(&self, id: &str, filters: &[QueryFilter]) -> Result<(), Error> {
|
async fn subscribe(&self, id: &str, filters: &[QueryFilter]) -> Result<(), Error> {
|
||||||
self.subscribe_with_id(SubscriptionId::new(id), filters.into(), Some(SubscribeAutoCloseOptions::default()))
|
self.subscribe_with_id(
|
||||||
.await?;
|
SubscriptionId::new(id),
|
||||||
|
filters.into(),
|
||||||
|
Some(SubscribeAutoCloseOptions::default()),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -74,11 +74,11 @@ impl<'a> StreamInfo for Note<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn starts(&self) -> u64 {
|
fn starts(&self) -> u64 {
|
||||||
if let Some(s) = self.get_tag_value("starts") {
|
if let Some(s) = self.get_tag_value("starts") {
|
||||||
s.variant().str()
|
s.variant().str().map_or(self.created_at(), |v| {
|
||||||
.map_or(self.created_at(), |v| v.parse::<u64>().unwrap_or(self.created_at()))
|
v.parse::<u64>().unwrap_or(self.created_at())
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
self.created_at()
|
self.created_at()
|
||||||
}
|
}
|
||||||
|
@ -4,4 +4,4 @@ pub const FONT_SIZE: f32 = 13.0;
|
|||||||
pub const PRIMARY: Color32 = Color32::from_rgb(248, 56, 217);
|
pub const PRIMARY: Color32 = Color32::from_rgb(248, 56, 217);
|
||||||
pub const NEUTRAL_500: Color32 = Color32::from_rgb(115, 115, 115);
|
pub const NEUTRAL_500: Color32 = Color32::from_rgb(115, 115, 115);
|
||||||
pub const NEUTRAL_800: Color32 = Color32::from_rgb(38, 38, 38);
|
pub const NEUTRAL_800: Color32 = Color32::from_rgb(38, 38, 38);
|
||||||
pub const NEUTRAL_900: Color32 = Color32::from_rgb(23, 23, 23);
|
pub const NEUTRAL_900: Color32 = Color32::from_rgb(23, 23, 23);
|
||||||
|
@ -28,8 +28,7 @@ impl<'a> Avatar<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_profile(p: &'a Option<NdbProfile<'a>>, svc: &'a ImageCache) -> Self {
|
pub fn from_profile(p: &'a Option<NdbProfile<'a>>, svc: &'a ImageCache) -> Self {
|
||||||
let img = p
|
let img = p.map_or(None, |f| f.picture().map(|f| svc.load(f)));
|
||||||
.map_or(None, |f| f.picture().map(|f| svc.load(f)));
|
|
||||||
Self {
|
Self {
|
||||||
image: img,
|
image: img,
|
||||||
sub: None,
|
sub: None,
|
||||||
@ -57,10 +56,17 @@ impl<'a> Widget for Avatar<'a> {
|
|||||||
let size_v = self.size.unwrap_or(40.);
|
let size_v = self.size.unwrap_or(40.);
|
||||||
let size = Vec2::new(size_v, size_v);
|
let size = Vec2::new(size_v, size_v);
|
||||||
match self.image {
|
match self.image {
|
||||||
Some(img) => img.fit_to_exact_size(size).rounding(Rounding::same(size_v)).ui(ui),
|
Some(img) => img
|
||||||
|
.fit_to_exact_size(size)
|
||||||
|
.rounding(Rounding::same(size_v))
|
||||||
|
.ui(ui),
|
||||||
None => {
|
None => {
|
||||||
let (response, painter) = ui.allocate_painter(size, Sense::click());
|
let (response, painter) = ui.allocate_painter(size, Sense::click());
|
||||||
painter.circle_filled(Pos2::new(size_v / 2., size_v / 2.), size_v / 2., Color32::from_rgb(200, 200, 200));
|
painter.circle_filled(
|
||||||
|
Pos2::new(size_v / 2., size_v / 2.),
|
||||||
|
size_v / 2.,
|
||||||
|
Color32::from_rgb(200, 200, 200),
|
||||||
|
);
|
||||||
response
|
response
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,9 +7,7 @@ pub struct Button {
|
|||||||
|
|
||||||
impl Button {
|
impl Button {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self { color: NEUTRAL_800 }
|
||||||
color: NEUTRAL_800
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn show<F>(self, ui: &mut Ui, add_contents: F) -> Response
|
pub fn show<F>(self, ui: &mut Ui, add_contents: F) -> Response
|
||||||
@ -24,9 +22,11 @@ impl Button {
|
|||||||
|
|
||||||
let id = r.response.id;
|
let id = r.response.id;
|
||||||
ui.interact(
|
ui.interact(
|
||||||
r.response.on_hover_and_drag_cursor(CursorIcon::PointingHand).rect,
|
r.response
|
||||||
|
.on_hover_and_drag_cursor(CursorIcon::PointingHand)
|
||||||
|
.rect,
|
||||||
id,
|
id,
|
||||||
Sense::click(),
|
Sense::click(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -50,11 +50,13 @@ impl NostrWidget for Chat {
|
|||||||
.map_while(|n| {
|
.map_while(|n| {
|
||||||
services
|
services
|
||||||
.ndb
|
.ndb
|
||||||
.get_note_by_key(services.tx, NoteKey::new(n.0)).ok()
|
.get_note_by_key(services.tx, NoteKey::new(n.0))
|
||||||
|
.ok()
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let stream = services.ndb
|
let stream = services
|
||||||
|
.ndb
|
||||||
.get_note_by_key(services.tx, NoteKey::new(self.stream.0))
|
.get_note_by_key(services.tx, NoteKey::new(self.stream.0))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@ -66,13 +68,17 @@ impl NostrWidget for Chat {
|
|||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
ui.vertical(|ui| {
|
ui.vertical(|ui| {
|
||||||
ui.spacing_mut().item_spacing.y = 8.0;
|
ui.spacing_mut().item_spacing.y = 8.0;
|
||||||
for ev in events.iter().sorted_by(|a, b| {
|
for ev in events
|
||||||
a.created_at().cmp(&b.created_at())
|
.iter()
|
||||||
}).tail(20) {
|
.sorted_by(|a, b| a.created_at().cmp(&b.created_at()))
|
||||||
|
.tail(20)
|
||||||
|
{
|
||||||
ChatMessage::new(&stream, ev, services).ui(ui);
|
ChatMessage::new(&stream, ev, services).ui(ui);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}).response
|
})
|
||||||
}).inner
|
.response
|
||||||
|
})
|
||||||
|
.inner
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,8 +16,17 @@ pub struct ChatMessage<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> ChatMessage<'a> {
|
impl<'a> ChatMessage<'a> {
|
||||||
pub fn new(stream: &'a Note<'a>, ev: &'a Note<'a>, services: &'a RouteServices<'a>) -> ChatMessage<'a> {
|
pub fn new(
|
||||||
ChatMessage { stream, ev, services, profile: services.ndb.fetch_profile(services.tx, ev.pubkey()) }
|
stream: &'a Note<'a>,
|
||||||
|
ev: &'a Note<'a>,
|
||||||
|
services: &'a RouteServices<'a>,
|
||||||
|
) -> ChatMessage<'a> {
|
||||||
|
ChatMessage {
|
||||||
|
stream,
|
||||||
|
ev,
|
||||||
|
services,
|
||||||
|
profile: services.ndb.fetch_profile(services.tx, ev.pubkey()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -29,17 +38,15 @@ impl<'a> Widget for ChatMessage<'a> {
|
|||||||
job.wrap.break_anywhere = true;
|
job.wrap.break_anywhere = true;
|
||||||
|
|
||||||
let is_host = self.stream.host().eq(self.ev.pubkey());
|
let is_host = self.stream.host().eq(self.ev.pubkey());
|
||||||
let profile = self.services.ndb.get_profile_by_pubkey(self.services.tx, self.ev.pubkey())
|
let profile = self
|
||||||
|
.services
|
||||||
|
.ndb
|
||||||
|
.get_profile_by_pubkey(self.services.tx, self.ev.pubkey())
|
||||||
.map_or(None, |p| p.record().profile());
|
.map_or(None, |p| p.record().profile());
|
||||||
|
|
||||||
let name = profile
|
let name = profile.map_or("Nostrich", |f| f.name().map_or("Nostrich", |f| f));
|
||||||
.map_or("Nostrich", |f| f.name().map_or("Nostrich", |f| f));
|
|
||||||
|
|
||||||
let name_color = if is_host {
|
let name_color = if is_host { PRIMARY } else { NEUTRAL_500 };
|
||||||
PRIMARY
|
|
||||||
} else {
|
|
||||||
NEUTRAL_500
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut format = TextFormat::default();
|
let mut format = TextFormat::default();
|
||||||
format.line_height = Some(24.0);
|
format.line_height = Some(24.0);
|
||||||
@ -50,10 +57,9 @@ impl<'a> Widget for ChatMessage<'a> {
|
|||||||
format.color = Color32::WHITE;
|
format.color = Color32::WHITE;
|
||||||
job.append(self.ev.content(), 5.0, format.clone());
|
job.append(self.ev.content(), 5.0, format.clone());
|
||||||
|
|
||||||
ui.add(Avatar::from_profile(&profile ,self.services.img_cache).size(24.));
|
ui.add(Avatar::from_profile(&profile, self.services.img_cache).size(24.));
|
||||||
ui.add(Label::new(job)
|
ui.add(Label::new(job).wrap_mode(TextWrapMode::Wrap));
|
||||||
.wrap_mode(TextWrapMode::Wrap)
|
})
|
||||||
);
|
.response
|
||||||
}).response
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,10 +37,7 @@ impl NostrWidget for Header {
|
|||||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||||
if let Some(pk) = services.login {
|
if let Some(pk) = services.login {
|
||||||
ui.add(Avatar::pubkey(pk, services));
|
ui.add(Avatar::pubkey(pk, services));
|
||||||
} else if Button::new()
|
} else if Button::new().show(ui, |ui| ui.label("Login")).clicked() {
|
||||||
.show(ui, |ui| {
|
|
||||||
ui.label("Login")
|
|
||||||
}).clicked() {
|
|
||||||
services.navigate(Routes::LoginPage);
|
services.navigate(Routes::LoginPage);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
mod avatar;
|
mod avatar;
|
||||||
|
mod button;
|
||||||
mod chat;
|
mod chat;
|
||||||
mod chat_message;
|
mod chat_message;
|
||||||
mod header;
|
mod header;
|
||||||
mod profile;
|
mod profile;
|
||||||
mod stream_tile;
|
|
||||||
mod stream_list;
|
mod stream_list;
|
||||||
mod stream_player;
|
mod stream_player;
|
||||||
mod video_placeholder;
|
mod stream_tile;
|
||||||
mod stream_title;
|
mod stream_title;
|
||||||
mod write_chat;
|
|
||||||
mod username;
|
mod username;
|
||||||
mod button;
|
mod video_placeholder;
|
||||||
|
mod write_chat;
|
||||||
|
|
||||||
use crate::route::RouteServices;
|
use crate::route::RouteServices;
|
||||||
use egui::{Response, Ui};
|
use egui::{Response, Ui};
|
||||||
@ -20,13 +20,13 @@ pub trait NostrWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub use self::avatar::Avatar;
|
pub use self::avatar::Avatar;
|
||||||
|
pub use self::button::Button;
|
||||||
pub use self::chat::Chat;
|
pub use self::chat::Chat;
|
||||||
pub use self::header::Header;
|
pub use self::header::Header;
|
||||||
pub use self::profile::Profile;
|
pub use self::profile::Profile;
|
||||||
pub use self::stream_list::StreamList;
|
pub use self::stream_list::StreamList;
|
||||||
pub use self::stream_player::StreamPlayer;
|
pub use self::stream_player::StreamPlayer;
|
||||||
pub use self::video_placeholder::VideoPlaceholder;
|
|
||||||
pub use self::stream_title::StreamTitle;
|
pub use self::stream_title::StreamTitle;
|
||||||
pub use self::write_chat::WriteChat;
|
|
||||||
pub use self::username::Username;
|
pub use self::username::Username;
|
||||||
pub use self::button::Button;
|
pub use self::video_placeholder::VideoPlaceholder;
|
||||||
|
pub use self::write_chat::WriteChat;
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
use crate::route::RouteServices;
|
use crate::route::RouteServices;
|
||||||
use crate::services::image_cache::ImageCache;
|
use crate::services::image_cache::ImageCache;
|
||||||
use crate::services::ndb_wrapper::SubWrapper;
|
use crate::services::ndb_wrapper::SubWrapper;
|
||||||
|
use crate::theme::FONT_SIZE;
|
||||||
use crate::widgets::{Avatar, Username};
|
use crate::widgets::{Avatar, Username};
|
||||||
use egui::{Response, Ui, Widget};
|
use egui::{Response, Ui, Widget};
|
||||||
use nostrdb::NdbProfile;
|
use nostrdb::NdbProfile;
|
||||||
use crate::theme::FONT_SIZE;
|
|
||||||
|
|
||||||
pub struct Profile<'a> {
|
pub struct Profile<'a> {
|
||||||
size: f32,
|
size: f32,
|
||||||
@ -39,6 +39,7 @@ impl<'a> Widget for Profile<'a> {
|
|||||||
|
|
||||||
ui.add(Avatar::from_profile(&self.profile, self.img_cache).size(self.size));
|
ui.add(Avatar::from_profile(&self.profile, self.img_cache).size(self.size));
|
||||||
ui.add(Username::new(&self.profile, FONT_SIZE))
|
ui.add(Username::new(&self.profile, FONT_SIZE))
|
||||||
}).response
|
})
|
||||||
|
.response
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,11 +23,11 @@ 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()
|
for event in self.streams.iter().sorted_by(|a, b| {
|
||||||
.sorted_by(|a, b| {
|
a.status()
|
||||||
a.status().cmp(&b.status())
|
.cmp(&b.status())
|
||||||
.then(a.starts().cmp(&b.starts()).reverse())
|
.then(a.starts().cmp(&b.starts()).reverse())
|
||||||
}) {
|
}) {
|
||||||
ui.add(StreamEvent::new(event, self.services));
|
ui.add(StreamEvent::new(event, self.services));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -11,9 +11,7 @@ impl StreamPlayer {
|
|||||||
let mut p = Player::new(ctx, url);
|
let mut p = Player::new(ctx, url);
|
||||||
p.set_debug(true);
|
p.set_debug(true);
|
||||||
p.start();
|
p.start();
|
||||||
Self {
|
Self { player: Some(p) }
|
||||||
player: Some(p)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
use crate::link::NostrLink;
|
use crate::link::NostrLink;
|
||||||
use crate::route::{RouteServices, Routes};
|
use crate::route::{RouteServices, Routes};
|
||||||
use crate::stream_info::{StreamInfo, StreamStatus};
|
use crate::stream_info::{StreamInfo, StreamStatus};
|
||||||
use crate::theme::{NEUTRAL_500, NEUTRAL_900, PRIMARY};
|
use crate::theme::{NEUTRAL_800, NEUTRAL_900, PRIMARY};
|
||||||
use crate::widgets::avatar::Avatar;
|
use crate::widgets::avatar::Avatar;
|
||||||
use eframe::epaint::{Rounding, Vec2};
|
use eframe::epaint::{Rounding, Vec2};
|
||||||
use egui::epaint::RectShape;
|
use egui::epaint::RectShape;
|
||||||
use egui::load::TexturePoll;
|
use egui::load::TexturePoll;
|
||||||
use egui::{vec2, Color32, CursorIcon, FontId, Label, Pos2, Rect, Response, RichText, Sense, TextWrapMode, Ui, Widget};
|
use egui::{
|
||||||
use image::Pixel;
|
vec2, Color32, CursorIcon, FontId, Label, Pos2, Rect, Response, RichText, Sense, TextWrapMode,
|
||||||
|
Ui, Widget,
|
||||||
|
};
|
||||||
use nostrdb::Note;
|
use nostrdb::Note;
|
||||||
|
|
||||||
pub struct StreamEvent<'a> {
|
pub struct StreamEvent<'a> {
|
||||||
@ -17,10 +19,7 @@ pub struct StreamEvent<'a> {
|
|||||||
|
|
||||||
impl<'a> StreamEvent<'a> {
|
impl<'a> StreamEvent<'a> {
|
||||||
pub fn new(event: &'a Note<'a>, services: &'a RouteServices) -> Self {
|
pub fn new(event: &'a Note<'a>, services: &'a RouteServices) -> Self {
|
||||||
Self {
|
Self { event, services }
|
||||||
event,
|
|
||||||
services,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl Widget for StreamEvent<'_> {
|
impl Widget for StreamEvent<'_> {
|
||||||
@ -33,14 +32,14 @@ impl Widget for StreamEvent<'_> {
|
|||||||
|
|
||||||
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 cover = self.event.image()
|
let cover = self.event.image().map(|p| self.services.img_cache.load(p));
|
||||||
.map(|p| self.services.img_cache.load(p));
|
|
||||||
|
|
||||||
let (response, painter) = ui.allocate_painter(Vec2::new(w, h), Sense::click());
|
let (response, painter) = ui.allocate_painter(Vec2::new(w, h), Sense::click());
|
||||||
|
|
||||||
if let Some(cover) = cover.map(|c|
|
if let Some(cover) = cover.map(|c| {
|
||||||
c.rounding(Rounding::same(12.))
|
c.rounding(Rounding::same(12.))
|
||||||
.load_for_size(painter.ctx(), Vec2::new(w, h))) {
|
.load_for_size(painter.ctx(), Vec2::new(w, h))
|
||||||
|
}) {
|
||||||
match cover {
|
match cover {
|
||||||
Ok(TexturePoll::Ready { texture }) => {
|
Ok(TexturePoll::Ready { texture }) => {
|
||||||
painter.add(RectShape {
|
painter.add(RectShape {
|
||||||
@ -54,11 +53,11 @@ impl Widget for StreamEvent<'_> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
painter.rect_filled(response.rect, 12., NEUTRAL_500);
|
painter.rect_filled(response.rect, 12., NEUTRAL_800);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
painter.rect_filled(response.rect, 12., NEUTRAL_500);
|
painter.rect_filled(response.rect, 12., NEUTRAL_800);
|
||||||
}
|
}
|
||||||
|
|
||||||
let overlay_label_pad = Vec2::new(5., 5.);
|
let overlay_label_pad = Vec2::new(5., 5.);
|
||||||
@ -68,20 +67,41 @@ impl Widget for StreamEvent<'_> {
|
|||||||
} else {
|
} else {
|
||||||
NEUTRAL_900
|
NEUTRAL_900
|
||||||
};
|
};
|
||||||
let live_label = painter.layout_no_wrap(live_label_text, FontId::default(), Color32::WHITE);
|
let live_label =
|
||||||
|
painter.layout_no_wrap(live_label_text, FontId::default(), Color32::WHITE);
|
||||||
|
|
||||||
let overlay_react = response.rect.shrink(8.0);
|
let overlay_react = response.rect.shrink(8.0);
|
||||||
let live_label_pos = overlay_react.min + vec2(overlay_react.width() - live_label.rect.width() - (overlay_label_pad.x * 2.), 0.0);
|
let live_label_pos = overlay_react.min
|
||||||
let live_label_background = Rect::from_two_pos(live_label_pos, live_label_pos + live_label.size() + (overlay_label_pad * 2.));
|
+ vec2(
|
||||||
|
overlay_react.width() - live_label.rect.width() - (overlay_label_pad.x * 2.),
|
||||||
|
0.0,
|
||||||
|
);
|
||||||
|
let live_label_background = Rect::from_two_pos(
|
||||||
|
live_label_pos,
|
||||||
|
live_label_pos + live_label.size() + (overlay_label_pad * 2.),
|
||||||
|
);
|
||||||
painter.rect_filled(live_label_background, 8., live_label_color);
|
painter.rect_filled(live_label_background, 8., live_label_color);
|
||||||
painter.galley(live_label_pos + overlay_label_pad, live_label, Color32::PLACEHOLDER);
|
painter.galley(
|
||||||
|
live_label_pos + overlay_label_pad,
|
||||||
|
live_label,
|
||||||
|
Color32::PLACEHOLDER,
|
||||||
|
);
|
||||||
|
|
||||||
if let Some(viewers) = self.event.viewers() {
|
if let Some(viewers) = self.event.viewers() {
|
||||||
let viewers_label = painter.layout_no_wrap(format!("{} viewers", viewers), FontId::default(), Color32::WHITE);
|
let viewers_label = painter.layout_no_wrap(
|
||||||
let rect_start = overlay_react.max - viewers_label.size() - (overlay_label_pad * 2.0);
|
format!("{} viewers", viewers),
|
||||||
|
FontId::default(),
|
||||||
|
Color32::WHITE,
|
||||||
|
);
|
||||||
|
let rect_start =
|
||||||
|
overlay_react.max - viewers_label.size() - (overlay_label_pad * 2.0);
|
||||||
let pos = Rect::from_two_pos(rect_start, overlay_react.max);
|
let pos = Rect::from_two_pos(rect_start, overlay_react.max);
|
||||||
painter.rect_filled(pos, 8., NEUTRAL_900);
|
painter.rect_filled(pos, 8., NEUTRAL_900);
|
||||||
painter.galley(rect_start + overlay_label_pad, viewers_label, Color32::PLACEHOLDER);
|
painter.galley(
|
||||||
|
rect_start + overlay_label_pad,
|
||||||
|
viewers_label,
|
||||||
|
Color32::PLACEHOLDER,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
let response = response.on_hover_and_drag_cursor(CursorIcon::PointingHand);
|
let response = response.on_hover_and_drag_cursor(CursorIcon::PointingHand);
|
||||||
if response.clicked() {
|
if response.clicked() {
|
||||||
@ -98,6 +118,6 @@ impl Widget for StreamEvent<'_> {
|
|||||||
ui.add(Label::new(title).wrap_mode(TextWrapMode::Truncate));
|
ui.add(Label::new(title).wrap_mode(TextWrapMode::Truncate));
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.response
|
.response
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,9 +11,7 @@ pub struct StreamTitle<'a> {
|
|||||||
|
|
||||||
impl<'a> StreamTitle<'a> {
|
impl<'a> StreamTitle<'a> {
|
||||||
pub fn new(event: &'a Note<'a>) -> StreamTitle {
|
pub fn new(event: &'a Note<'a>) -> StreamTitle {
|
||||||
StreamTitle {
|
StreamTitle { event }
|
||||||
event
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,15 +26,17 @@ impl<'a> NostrWidget for StreamTitle<'a> {
|
|||||||
.color(Color32::WHITE);
|
.color(Color32::WHITE);
|
||||||
ui.add(Label::new(title.strong()).wrap_mode(TextWrapMode::Truncate));
|
ui.add(Label::new(title.strong()).wrap_mode(TextWrapMode::Truncate));
|
||||||
|
|
||||||
Profile::new(self.event.host(), services)
|
Profile::new(self.event.host(), services).size(32.).ui(ui);
|
||||||
.size(32.)
|
|
||||||
.ui(ui);
|
|
||||||
|
|
||||||
if let Some(summary) = self.event.get_tag_value("summary").and_then(|r| r.variant().str()) {
|
if let Some(summary) = self
|
||||||
let summary = RichText::new(summary)
|
.event
|
||||||
.color(Color32::WHITE);
|
.get_tag_value("summary")
|
||||||
|
.and_then(|r| r.variant().str())
|
||||||
|
{
|
||||||
|
let summary = RichText::new(summary).color(Color32::WHITE);
|
||||||
ui.add(Label::new(summary).wrap_mode(TextWrapMode::Truncate));
|
ui.add(Label::new(summary).wrap_mode(TextWrapMode::Truncate));
|
||||||
}
|
}
|
||||||
}).response
|
})
|
||||||
|
.response
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,4 +20,4 @@ impl<'a> Widget for Username<'a> {
|
|||||||
let name = RichText::new(name).size(self.size).color(Color32::WHITE);
|
let name = RichText::new(name).size(self.size).color(Color32::WHITE);
|
||||||
ui.add(Label::new(name).wrap_mode(TextWrapMode::Truncate))
|
ui.add(Label::new(name).wrap_mode(TextWrapMode::Truncate))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,9 +10,7 @@ pub struct WriteChat {
|
|||||||
|
|
||||||
impl WriteChat {
|
impl WriteChat {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self { msg: String::new() }
|
||||||
msg: String::new(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,18 +28,19 @@ impl NostrWidget for WriteChat {
|
|||||||
.inner_margin(Margin::symmetric(12., 12.))
|
.inner_margin(Margin::symmetric(12., 12.))
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
let editor = TextEdit::singleline(&mut self.msg)
|
let editor = TextEdit::singleline(&mut self.msg).frame(false);
|
||||||
.frame(false);
|
|
||||||
ui.add(editor);
|
ui.add(editor);
|
||||||
if Image::from_bytes("send-03.svg", logo_bytes)
|
if Image::from_bytes("send-03.svg", logo_bytes)
|
||||||
.sense(Sense::click())
|
.sense(Sense::click())
|
||||||
.ui(ui)
|
.ui(ui)
|
||||||
.clicked() {
|
.clicked()
|
||||||
|
{
|
||||||
info!("Sending: {}", self.msg);
|
info!("Sending: {}", self.msg);
|
||||||
self.msg.clear();
|
self.msg.clear();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
}).response
|
})
|
||||||
|
.response
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user