Init
This commit is contained in:
commit
6dd62737d2
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target
|
17
Cargo.toml
Normal file
17
Cargo.toml
Normal file
@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "zap_stream_app"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
tokio = "1.40.0"
|
||||
|
||||
egui = { git = "https://github.com/emilk/egui", rev = "fcb7764e48ce00f8f8e58da10f937410d65b0bfb" }
|
||||
eframe = { git = "https://github.com/emilk/egui", rev = "fcb7764e48ce00f8f8e58da10f937410d65b0bfb", package = "eframe", default-features = false, features = ["wgpu", "wayland", "x11", "android-native-activity"] }
|
||||
nostrdb = { git = "https://github.com/damus-io/nostrdb-rs", rev = "6d22af6d5159be4c9e4579f8c9d3af836e0d470a" }
|
||||
nostr-sdk = { version = "0.34.0", features = ["all-nips"] }
|
||||
egui_nav = { git = "https://github.com/damus-io/egui-nav", branch = "egui-0.28" }
|
||||
egui_extras = { git = "https://github.com/emilk/egui", rev = "fcb7764e48ce00f8f8e58da10f937410d65b0bfb", package = "egui_extras", features = ["all_loaders"] }
|
||||
image = { version = "0.25", features = ["jpeg", "png", "webp"] }
|
||||
log = "0.4.22"
|
||||
pretty_env_logger = "0.5.0"
|
85
src/app.rs
Normal file
85
src/app.rs
Normal file
@ -0,0 +1,85 @@
|
||||
use crate::services::Services;
|
||||
use crate::widget::NostrWidget;
|
||||
use crate::widgets::header::Header;
|
||||
use crate::widgets::stream_list::StreamList;
|
||||
use eframe::{App, CreationContext, Frame};
|
||||
use egui::{Color32, Context, ScrollArea};
|
||||
use nostr_sdk::database::{MemoryDatabase, MemoryDatabaseOptions};
|
||||
use nostr_sdk::{Client, Event, Filter, Kind, RelayPoolNotification};
|
||||
use nostrdb::{Config, Ndb, Transaction};
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
pub struct ZapStreamApp {
|
||||
ndb: Ndb,
|
||||
client: Client,
|
||||
notifications: broadcast::Receiver<RelayPoolNotification>,
|
||||
events: Vec<Event>,
|
||||
services: Services,
|
||||
}
|
||||
|
||||
impl ZapStreamApp {
|
||||
pub fn new(cc: &CreationContext) -> Self {
|
||||
let client = Client::builder().database(MemoryDatabase::with_opts(MemoryDatabaseOptions {
|
||||
events: true,
|
||||
..Default::default()
|
||||
})).build();
|
||||
let notifications = client.notifications();
|
||||
|
||||
let ctx_clone = cc.egui_ctx.clone();
|
||||
let client_clone = client.clone();
|
||||
tokio::spawn(async move {
|
||||
client_clone.add_relay("wss://nos.lol").await.expect("Failed to add relay");
|
||||
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();
|
||||
while let Ok(_) = notifications.recv().await {
|
||||
ctx_clone.request_repaint();
|
||||
}
|
||||
});
|
||||
egui_extras::install_image_loaders(&cc.egui_ctx);
|
||||
|
||||
Self {
|
||||
ndb: Ndb::new(".", &Config::default()).unwrap(),
|
||||
client: client.clone(),
|
||||
notifications,
|
||||
services: Services::new(client, cc.egui_ctx.clone()),
|
||||
events: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn process_nostr(&mut self) {
|
||||
while let Ok(msg) = self.notifications.try_recv() {
|
||||
match msg {
|
||||
RelayPoolNotification::Event { event, .. } => {
|
||||
self.events.push(*event);
|
||||
}
|
||||
_ => {
|
||||
// dont care
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl App for ZapStreamApp {
|
||||
fn update(&mut self, ctx: &Context, frame: &mut Frame) {
|
||||
let txn = Transaction::new(&self.ndb).expect("txn");
|
||||
self.process_nostr();
|
||||
|
||||
let mut app_frame = egui::containers::Frame::default();
|
||||
app_frame.stroke.color = Color32::BLACK;
|
||||
|
||||
ctx.set_debug_on_hover(true);
|
||||
egui::CentralPanel::default()
|
||||
.frame(app_frame)
|
||||
.show(ctx, |ui| {
|
||||
ScrollArea::vertical().show(ui, |ui| {
|
||||
ui.add(Header::new(&self.services));
|
||||
ui.add(StreamList::new(&self.events, &self.services));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
13
src/logo.svg
Normal file
13
src/logo.svg
Normal file
@ -0,0 +1,13 @@
|
||||
<svg width="33" height="24" viewBox="0 0 33 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_4303_1419)">
|
||||
<path d="M4.45977 2.75527L4.44727 2.74902C4.44727 2.74902 4.45352 2.74902 4.45977 2.75527Z" fill="white"/>
|
||||
<path d="M8.97119 1.70361L9.00244 1.71611C8.98994 1.70986 8.98369 1.70361 8.97119 1.70361Z" fill="white"/>
|
||||
<path d="M9.01006 1.71762C9.00381 1.71137 9.00381 1.71762 9.01006 1.71762V1.71762Z" fill="white"/>
|
||||
<path d="M32.7877 2.41234C32.3558 1.36818 31.3439 0.691406 30.216 0.691406H10.6802C10.6738 0.691406 10.6673 0.691406 10.6609 0.691406C10.6545 0.691406 10.648 0.691406 10.6416 0.691406C6.54235 0.691406 3.21012 4.02369 3.21012 8.11654C3.21012 10.2693 4.13825 12.2738 5.70446 13.6661L0.812466 18.5581C0.0132476 19.3574 -0.225229 20.5498 0.206607 21.5875C0.638443 22.6316 1.65036 23.3084 2.77829 23.3084H22.314C22.3205 23.3084 22.3269 23.3084 22.3398 23.3084C22.3463 23.3084 22.3527 23.3084 22.3656 23.3084C26.4584 23.3084 29.7906 19.9761 29.7906 15.8833C29.7906 13.7305 28.8625 11.726 27.2963 10.3338L32.1882 5.44169C32.981 4.64245 33.2195 3.45005 32.7877 2.41234ZM2.71383 20.5498C2.6945 20.5047 2.70739 20.4918 2.73317 20.466L8.10856 15.0905L22.3914 20.1437C22.4043 20.1502 22.4236 20.1566 22.4365 20.1566C22.4558 20.163 22.4752 20.1695 22.4945 20.1824C22.5267 20.2017 22.5525 20.2339 22.5718 20.2726C22.5783 20.2791 22.5783 20.2855 22.5847 20.292C22.5912 20.3048 22.5912 20.3177 22.5912 20.3306C22.5912 20.3435 22.5976 20.3564 22.5976 20.3693C22.5976 20.4853 22.4687 20.5884 22.3269 20.5884H2.78473C2.75251 20.6013 2.73317 20.6013 2.71383 20.5498ZM25.208 19.6474C25.208 19.641 25.2015 19.6281 25.2015 19.6216C25.1757 19.5185 25.1435 19.4218 25.1048 19.3251C25.0984 19.3122 25.0984 19.2994 25.092 19.2865C25.0533 19.1833 25.0017 19.0867 24.9502 18.99C24.9437 18.9771 24.9308 18.9577 24.9244 18.9449C24.8148 18.7515 24.6859 18.571 24.5377 18.4034C24.5248 18.3905 24.5119 18.3777 24.499 18.3648C24.4216 18.2874 24.3443 18.2101 24.2605 18.1392C24.2476 18.1327 24.2347 18.1198 24.2283 18.1134C24.1509 18.0489 24.0672 17.9909 23.9769 17.9329C23.964 17.9265 23.9511 17.9136 23.9382 17.9071C23.848 17.8491 23.7513 17.7976 23.6547 17.746C23.6353 17.7331 23.6095 17.7267 23.5902 17.7138C23.4935 17.6687 23.3904 17.6235 23.2808 17.5913L16.6744 15.2516L9.03668 12.551L9.0109 12.5445C9.00446 12.5445 9.00446 12.5445 8.99801 12.5381C8.8111 12.4672 8.62418 12.3834 8.43727 12.2867C6.88395 11.4682 5.9236 9.86969 5.9236 8.11654C5.9236 6.58253 6.65836 5.2161 7.79918 4.35241C7.79918 4.35885 7.80563 4.37175 7.80563 4.37819C7.83141 4.47487 7.86364 4.57155 7.90231 4.66823C7.9152 4.70046 7.92809 4.73269 7.94098 4.76492C7.97321 4.83582 8.00543 4.90027 8.03766 4.97117C8.05055 5.0034 8.06989 5.03562 8.08278 5.06141C8.1279 5.1452 8.17946 5.22254 8.23747 5.29989C8.26325 5.33211 8.28258 5.36434 8.30836 5.39657C8.35348 5.45458 8.3986 5.51259 8.45016 5.56415C8.4695 5.58993 8.48883 5.61571 8.51462 5.63505C8.57907 5.70595 8.65641 5.7704 8.72731 5.83486C8.75309 5.86064 8.77887 5.87998 8.8111 5.89931C8.882 5.95732 8.9529 6.00888 9.03024 6.06045C9.04313 6.07334 9.05602 6.07978 9.07536 6.09267C9.16559 6.15068 9.26227 6.20225 9.35895 6.24736C9.38473 6.26026 9.41051 6.27315 9.43629 6.28604C9.53942 6.33116 9.64254 6.37627 9.74567 6.4085L24.0091 11.4553C24.0156 11.4553 24.022 11.4617 24.022 11.4617C24.209 11.5326 24.3894 11.61 24.5634 11.7066C26.1168 12.5252 27.0771 14.1237 27.0771 15.8768C27.0836 17.4173 26.3423 18.7837 25.208 19.6474ZM30.2675 3.52739L24.8922 8.90288C24.8793 8.89643 24.8599 8.88999 24.847 8.88354L10.6222 3.85611C10.6029 3.84967 10.5836 3.84322 10.5707 3.83677H10.5642C10.5449 3.83033 10.532 3.82388 10.5127 3.81099C10.5062 3.80455 10.4998 3.7981 10.4933 3.79166C10.4804 3.78521 10.474 3.77232 10.4675 3.76588C10.4611 3.75943 10.4547 3.74654 10.4547 3.74009C10.4482 3.7272 10.4418 3.71431 10.4353 3.70142C10.4353 3.69498 10.4289 3.68208 10.4289 3.66919C10.4289 3.6563 10.4224 3.63697 10.4224 3.61763C10.4224 3.60474 10.4289 3.59829 10.4289 3.5854C10.4482 3.48872 10.5642 3.40493 10.6867 3.40493H30.2289C30.2611 3.40493 30.2804 3.40493 30.2998 3.45005C30.3062 3.48872 30.2933 3.50806 30.2675 3.52739Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_4303_1419">
|
||||
<rect width="33" height="22.617" fill="white" transform="translate(0 0.691406)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 4.3 KiB |
22
src/main.rs
Normal file
22
src/main.rs
Normal file
@ -0,0 +1,22 @@
|
||||
use crate::app::ZapStreamApp;
|
||||
use egui::Vec2;
|
||||
|
||||
mod app;
|
||||
mod widget;
|
||||
mod widgets;
|
||||
mod services;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
pretty_env_logger::init();
|
||||
|
||||
let mut options = eframe::NativeOptions::default();
|
||||
options.viewport = options.viewport
|
||||
.with_inner_size(Vec2::new(360., 720.));
|
||||
|
||||
let _res = eframe::run_native(
|
||||
"zap.stream",
|
||||
options,
|
||||
Box::new(move |cc| Ok(Box::new(ZapStreamApp::new(cc)))),
|
||||
);
|
||||
}
|
19
src/services/mod.rs
Normal file
19
src/services/mod.rs
Normal file
@ -0,0 +1,19 @@
|
||||
use crate::services::profile::ProfileService;
|
||||
use egui::Context;
|
||||
use nostr_sdk::Client;
|
||||
|
||||
pub mod profile;
|
||||
|
||||
pub struct Services {
|
||||
pub context: Context,
|
||||
pub profile: ProfileService,
|
||||
}
|
||||
|
||||
impl Services {
|
||||
pub fn new(client: Client, context: Context) -> Self {
|
||||
Self {
|
||||
context: context.clone(),
|
||||
profile: ProfileService::new(client.clone(), context.clone()),
|
||||
}
|
||||
}
|
||||
}
|
82
src/services/profile.rs
Normal file
82
src/services/profile.rs
Normal file
@ -0,0 +1,82 @@
|
||||
use egui::load::BytesLoader;
|
||||
use log::{info, warn};
|
||||
use nostr_sdk::{Client, Metadata, PublicKey};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
pub struct ProfileService {
|
||||
client: Client,
|
||||
pub profiles: Arc<Mutex<HashMap<PublicKey, Option<Metadata>>>>,
|
||||
ctx: egui::Context,
|
||||
request_profile: UnboundedSender<PublicKey>,
|
||||
}
|
||||
|
||||
struct Loader {
|
||||
client: Client,
|
||||
ctx: egui::Context,
|
||||
profiles: Arc<Mutex<HashMap<PublicKey, Option<Metadata>>>>,
|
||||
queue: UnboundedReceiver<PublicKey>,
|
||||
}
|
||||
|
||||
impl Loader {
|
||||
pub async fn run(&mut self) {
|
||||
while let Some(key) = self.queue.recv().await {
|
||||
let mut profiles = self.profiles.lock().await;
|
||||
if !profiles.contains_key(&key) {
|
||||
info!("Requesting profile {}", key);
|
||||
match self.client.metadata(key).await {
|
||||
Ok(meta) => {
|
||||
profiles.insert(key, Some(meta));
|
||||
self.ctx.request_repaint();
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Error getting metadata: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProfileService {
|
||||
pub fn new(client: Client, ctx: egui::Context) -> ProfileService
|
||||
{
|
||||
let profiles = Arc::new(Mutex::new(HashMap::new()));
|
||||
let (tx, rx) = unbounded_channel();
|
||||
let mut loader = Loader {
|
||||
client: client.clone(),
|
||||
ctx: ctx.clone(),
|
||||
profiles: profiles.clone(),
|
||||
queue: rx,
|
||||
};
|
||||
|
||||
tokio::spawn(async move {
|
||||
loader.run().await;
|
||||
});
|
||||
|
||||
Self {
|
||||
client,
|
||||
ctx,
|
||||
profiles,
|
||||
request_profile: tx,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_profile(&self, public_key: &PublicKey) -> Option<Metadata> {
|
||||
if let Ok(profiles) = self.profiles.try_lock() {
|
||||
return if let Some(p) = profiles.get(public_key) {
|
||||
if let Some(p) = p {
|
||||
Some(p.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
self.request_profile.send(*public_key).expect("Failed request");
|
||||
None
|
||||
};
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
5
src/widget.rs
Normal file
5
src/widget.rs
Normal file
@ -0,0 +1,5 @@
|
||||
use nostr_sdk::Filter;
|
||||
|
||||
pub trait NostrWidget {
|
||||
fn subscribe(&self) -> Vec<Filter>;
|
||||
}
|
55
src/widgets/avatar.rs
Normal file
55
src/widgets/avatar.rs
Normal file
@ -0,0 +1,55 @@
|
||||
use crate::services::Services;
|
||||
use egui::{Color32, Image, Rect, Response, Rounding, Sense, Ui, Vec2, Widget};
|
||||
use nostr_sdk::PublicKey;
|
||||
|
||||
pub struct Avatar<'a> {
|
||||
image: Option<Image<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Avatar<'a> {
|
||||
pub fn new(img: Image<'a>) -> Self {
|
||||
Self { image: Some(img) }
|
||||
}
|
||||
|
||||
pub fn public_key(svc: &'a Services, pk: &PublicKey) -> Self {
|
||||
if let Some(meta) = svc.profile.get_profile(pk) {
|
||||
if let Some(img) = &meta.picture {
|
||||
return Self { image: Some(Image::from_uri(img.clone())) };
|
||||
}
|
||||
}
|
||||
Self { image: None }
|
||||
}
|
||||
|
||||
pub fn max_size(mut self, size: f32) -> Self {
|
||||
self.image = if let Some(i) = self.image {
|
||||
Some(i.max_height(size))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self
|
||||
}
|
||||
|
||||
pub fn size(mut self, size: f32) -> Self {
|
||||
self.image = if let Some(i) = self.image {
|
||||
Some(i.fit_to_exact_size(Vec2::new(size, size)))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for Avatar<'a> {
|
||||
fn ui(self, ui: &mut Ui) -> Response {
|
||||
match self.image {
|
||||
Some(img) => {
|
||||
img.rounding(Rounding::same(ui.available_height())).ui(ui)
|
||||
}
|
||||
None => {
|
||||
let (response, painter) = ui.allocate_painter(Vec2::new(32., 32.), Sense::hover());
|
||||
painter.rect_filled(Rect::EVERYTHING, Rounding::same(32.), Color32::from_rgb(200, 200, 200));
|
||||
response
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
34
src/widgets/header.rs
Normal file
34
src/widgets/header.rs
Normal file
@ -0,0 +1,34 @@
|
||||
use crate::services::Services;
|
||||
use crate::widgets::avatar::Avatar;
|
||||
use eframe::emath::Align;
|
||||
use eframe::epaint::Vec2;
|
||||
use egui::{Frame, Image, Layout, Margin, Response, Ui, Widget};
|
||||
use nostr_sdk::PublicKey;
|
||||
|
||||
pub struct Header<'a> {
|
||||
services: &'a Services,
|
||||
}
|
||||
|
||||
impl<'a> Header<'a> {
|
||||
pub fn new(services: &'a Services) -> Self {
|
||||
Self { services }
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Header<'_> {
|
||||
fn ui(self, ui: &mut Ui) -> Response {
|
||||
let login = PublicKey::from_hex("63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed").unwrap();
|
||||
let logo_bytes = include_bytes!("../logo.svg");
|
||||
Frame::none()
|
||||
.outer_margin(Margin::symmetric(16., 8.))
|
||||
.show(ui, |ui| {
|
||||
ui.allocate_ui_with_layout(Vec2::new(ui.available_width(), 32.), Layout::left_to_right(Align::Center), |ui| {
|
||||
ui.style_mut()
|
||||
.spacing.item_spacing.x = 16.;
|
||||
Image::from_bytes("logo.svg", logo_bytes)
|
||||
.max_height(22.62).ui(ui);
|
||||
ui.add(Avatar::public_key(&self.services, &login));
|
||||
})
|
||||
}).response
|
||||
}
|
||||
}
|
4
src/widgets/mod.rs
Normal file
4
src/widgets/mod.rs
Normal file
@ -0,0 +1,4 @@
|
||||
pub mod header;
|
||||
pub mod stream;
|
||||
pub mod stream_list;
|
||||
mod avatar;
|
52
src/widgets/stream.rs
Normal file
52
src/widgets/stream.rs
Normal file
@ -0,0 +1,52 @@
|
||||
use crate::services::Services;
|
||||
use crate::widgets::avatar::Avatar;
|
||||
use eframe::epaint::Vec2;
|
||||
use egui::{Color32, Image, Rect, Response, RichText, Rounding, Sense, Ui, Widget};
|
||||
use nostr_sdk::{Alphabet, Event, PublicKey, SingleLetterTag, TagKind};
|
||||
|
||||
pub struct StreamEvent<'a> {
|
||||
event: &'a Event,
|
||||
picture: Option<Image<'a>>,
|
||||
services: &'a Services,
|
||||
}
|
||||
|
||||
impl<'a> StreamEvent<'a> {
|
||||
pub fn new(event: &'a Event, services: &'a Services) -> Self {
|
||||
let image = event.get_tag_content(TagKind::Image);
|
||||
let cover = match image {
|
||||
Some(i) => Some(Image::from_uri(i)),
|
||||
None => None,
|
||||
};
|
||||
Self { event, picture: cover, services }
|
||||
}
|
||||
}
|
||||
impl Widget for StreamEvent<'_> {
|
||||
fn ui(self, ui: &mut Ui) -> Response {
|
||||
ui.vertical(|ui| {
|
||||
ui.style_mut()
|
||||
.spacing.item_spacing = Vec2::new(12., 16.);
|
||||
|
||||
let host = match self.event.tags.iter().find(|t| t.kind() == TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::P)) && t.as_vec()[3] == "host") {
|
||||
Some(t) => PublicKey::from_hex(t.as_vec().get(1).unwrap()).unwrap(),
|
||||
None => self.event.author()
|
||||
};
|
||||
match self.picture {
|
||||
Some(picture) => picture.rounding(Rounding::same(12.)).ui(ui),
|
||||
None => {
|
||||
let w = ui.available_width();
|
||||
let h = (w / 16.0) * 9.0;
|
||||
let (response, painter) = ui.allocate_painter(Vec2::new(w, h), Sense::hover());
|
||||
painter.rect_filled(Rect::EVERYTHING, Rounding::same(12.), Color32::from_rgb(200, 200, 200));
|
||||
response
|
||||
}
|
||||
};
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(Avatar::public_key(self.services, &host).size(40.));
|
||||
ui.label(RichText::new(self.event.get_tag_content(TagKind::Title).unwrap_or("Unknown"))
|
||||
.size(16.)
|
||||
.color(Color32::WHITE)
|
||||
);
|
||||
})
|
||||
}).response
|
||||
}
|
||||
}
|
30
src/widgets/stream_list.rs
Normal file
30
src/widgets/stream_list.rs
Normal file
@ -0,0 +1,30 @@
|
||||
use crate::services::Services;
|
||||
use crate::widgets::stream::StreamEvent;
|
||||
use egui::{Frame, Margin, Response, Ui, Widget};
|
||||
use nostr_sdk::Event;
|
||||
|
||||
pub struct StreamList<'a> {
|
||||
streams: &'a Vec<Event>,
|
||||
services: &'a Services,
|
||||
}
|
||||
|
||||
impl<'a> StreamList<'a> {
|
||||
pub fn new(streams: &'a Vec<Event>, services: &'a Services) -> Self {
|
||||
Self { streams, services }
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for StreamList<'_> {
|
||||
fn ui(self, ui: &mut Ui) -> Response {
|
||||
Frame::none()
|
||||
.inner_margin(Margin::symmetric(16., 8.))
|
||||
.show(ui, |ui| {
|
||||
ui.vertical(|ui| {
|
||||
ui.style_mut().spacing.item_spacing = egui::vec2(0., 20.0);
|
||||
for event in self.streams.iter().take(5) {
|
||||
ui.add(StreamEvent::new(event, self.services));
|
||||
}
|
||||
})
|
||||
}).response
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user