feat: UI progress
This commit is contained in:
parent
5bed3fa86f
commit
b4a6991007
20
android.sh
20
android.sh
@ -2,15 +2,15 @@
|
||||
|
||||
git clone https://github.com/v0l/ffmpeg-kit.git
|
||||
export ANDROID_SDK_ROOT=$ANDROID_HOME
|
||||
cd ffmpeg-kit && ./android.sh \
|
||||
--disable-x86 \
|
||||
--disable-x86-64 \
|
||||
--disable-arm-v7a \
|
||||
--disable-arm-v7a-neon \
|
||||
--enable-openssl \
|
||||
--api-level=28 \
|
||||
--no-ffmpeg-kit-protocols \
|
||||
--no-archive
|
||||
#cd ffmpeg-kit && ./android.sh \
|
||||
# --disable-x86 \
|
||||
# --disable-x86-64 \
|
||||
# --disable-arm-v7a \
|
||||
# --disable-arm-v7a-neon \
|
||||
# --enable-openssl \
|
||||
# --api-level=28 \
|
||||
# --no-ffmpeg-kit-protocols \
|
||||
# --no-archive
|
||||
|
||||
NDK_VER="28.0.12433566"
|
||||
ARCH="arm64"
|
||||
@ -20,6 +20,8 @@ export FFMPEG_DIR="$(pwd)/ffmpeg-kit/prebuilt/$PLATFORM-$ARCH/ffmpeg"
|
||||
export PKG_CONFIG_SYSROOT_DIR="$(pwd)/ffmpeg-kit/prebuilt/$PLATFORM-$ARCH/pkgconfig"
|
||||
|
||||
# DIRTY HACK !!
|
||||
mkdir -p ./target/x/debug/android/$ARCH/cargo/$TRIPLET/release/deps
|
||||
mkdir -p ./target/x/release/android/$ARCH/cargo/$TRIPLET/release/deps
|
||||
cp "$ANDROID_HOME/ndk/$NDK_VER/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/$TRIPLET/35/libcamera2ndk.so" \
|
||||
./target/x/debug/android/$ARCH/cargo/$TRIPLET/debug/deps
|
||||
cp "$ANDROID_HOME/ndk/$NDK_VER/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/$TRIPLET/35/libcamera2ndk.so" \
|
||||
|
@ -60,6 +60,7 @@ impl App for ZapStreamApp {
|
||||
egui::CentralPanel::default()
|
||||
.frame(app_frame)
|
||||
.show(ctx, |ui| {
|
||||
ui.visuals_mut().override_text_color = Some(Color32::WHITE);
|
||||
self.router.show(ui);
|
||||
});
|
||||
}
|
||||
|
10
src/lib.rs
10
src/lib.rs
@ -8,8 +8,10 @@ mod note_util;
|
||||
mod route;
|
||||
mod services;
|
||||
mod stream_info;
|
||||
pub mod widgets;
|
||||
pub mod theme;
|
||||
mod widgets;
|
||||
mod theme;
|
||||
mod note_store;
|
||||
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
use winit::platform::android::activity::AndroidApp;
|
||||
@ -31,7 +33,7 @@ pub async fn android_main(app: AndroidApp) {
|
||||
builder.with_android_app(app_clone_for_event_loop);
|
||||
}));
|
||||
|
||||
let external_data_path = app
|
||||
let data_path = app
|
||||
.external_data_path()
|
||||
.expect("external data path")
|
||||
.to_path_buf();
|
||||
@ -39,6 +41,6 @@ pub async fn android_main(app: AndroidApp) {
|
||||
let _res = eframe::run_native(
|
||||
"zap.stream",
|
||||
options,
|
||||
Box::new(move |cc| Ok(Box::new(ZapStreamApp::new(cc, external_data_path)))),
|
||||
Box::new(move |cc| Ok(Box::new(ZapStreamApp::new(cc, data_path)))),
|
||||
);
|
||||
}
|
48
src/note_store.rs
Normal file
48
src/note_store.rs
Normal file
@ -0,0 +1,48 @@
|
||||
use crate::link::NostrLink;
|
||||
use nostrdb::Note;
|
||||
use std::borrow::Borrow;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub struct NoteStore<'a> {
|
||||
events: HashMap<String, Note<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> NoteStore<'a> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
events: HashMap::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_vec(events: Vec<Note<'a>>) -> Self {
|
||||
let mut store = Self::new();
|
||||
for note in events {
|
||||
store.add(note);
|
||||
}
|
||||
store
|
||||
}
|
||||
|
||||
pub fn add(&mut self, note: Note<'a>) -> Option<Note<'a>> {
|
||||
let k = Self::key(¬e);
|
||||
if let Some(v) = self.events.get(&k) {
|
||||
if v.created_at() < note.created_at() {
|
||||
return self.events.insert(k, note);
|
||||
}
|
||||
}
|
||||
self.events.insert(k, note)
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, note: &Note<'a>) -> Option<Note<'a>> {
|
||||
self.events.remove(&Self::key(note))
|
||||
}
|
||||
|
||||
pub fn key(note: &Note<'a>) -> String {
|
||||
NostrLink::from_note(note)
|
||||
.to_tag_value()
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item=&Note<'a>> {
|
||||
self.events.values()
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
use crate::note_store::NoteStore;
|
||||
use crate::note_util::OwnedNote;
|
||||
use crate::route::RouteServices;
|
||||
use crate::services::ndb_wrapper::{NDBWrapper, SubWrapper};
|
||||
@ -39,6 +40,7 @@ impl NostrWidget for HomePage {
|
||||
.map_while(|f| f.map_or(None, |f| Some(f)))
|
||||
.collect();
|
||||
|
||||
let events = NoteStore::from_vec(events);
|
||||
ScrollArea::vertical()
|
||||
.show(ui, |ui| {
|
||||
widgets::StreamList::new(&events, &services).ui(ui)
|
||||
|
46
src/route/login.rs
Normal file
46
src/route/login.rs
Normal file
@ -0,0 +1,46 @@
|
||||
use crate::route::{RouteAction, RouteServices, Routes};
|
||||
use crate::widgets::{Button, NostrWidget};
|
||||
use egui::{Color32, Response, RichText, Ui};
|
||||
use nostr_sdk::util::hex;
|
||||
|
||||
pub struct LoginPage {
|
||||
key: String,
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
impl LoginPage {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
key: String::new(),
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NostrWidget for LoginPage {
|
||||
fn render(&mut self, ui: &mut Ui, services: &RouteServices<'_>) -> Response {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.spacing_mut().item_spacing.y = 8.;
|
||||
|
||||
ui.label(RichText::new("Login").size(32.));
|
||||
ui.label("Pubkey");
|
||||
ui.text_edit_singleline(&mut self.key);
|
||||
if Button::new()
|
||||
.show(ui, |ui| {
|
||||
ui.label("Login")
|
||||
}).clicked() {
|
||||
if let Ok(pk) = hex::decode(&self.key) {
|
||||
if let Ok(pk) = pk.as_slice().try_into() {
|
||||
services.action(RouteAction::LoginPubkey(pk));
|
||||
services.navigate(Routes::HomePage);
|
||||
return;
|
||||
}
|
||||
}
|
||||
self.error = Some("Invalid pubkey".to_string());
|
||||
}
|
||||
if let Some(e) = &self.error {
|
||||
ui.label(RichText::new(e).color(Color32::RED));
|
||||
}
|
||||
}).response
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ use crate::link::NostrLink;
|
||||
use crate::note_util::OwnedNote;
|
||||
use crate::route;
|
||||
use crate::route::home::HomePage;
|
||||
use crate::route::login::LoginPage;
|
||||
use crate::route::stream::StreamPage;
|
||||
use crate::services::image_cache::ImageCache;
|
||||
use crate::services::ndb_wrapper::NDBWrapper;
|
||||
@ -18,11 +19,12 @@ use std::path::PathBuf;
|
||||
|
||||
mod home;
|
||||
mod stream;
|
||||
mod login;
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub enum Routes {
|
||||
HomePage,
|
||||
Event {
|
||||
EventPage {
|
||||
link: NostrLink,
|
||||
event: Option<OwnedNote>,
|
||||
},
|
||||
@ -30,6 +32,7 @@ pub enum Routes {
|
||||
link: NostrLink,
|
||||
profile: Option<OwnedNote>,
|
||||
},
|
||||
LoginPage,
|
||||
|
||||
// special kind for modifying route state
|
||||
Action(RouteAction),
|
||||
@ -37,7 +40,8 @@ pub enum Routes {
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub enum RouteAction {
|
||||
Login([u8; 32]),
|
||||
/// Login with public key
|
||||
LoginPubkey([u8; 32]),
|
||||
}
|
||||
|
||||
pub struct Router {
|
||||
@ -72,10 +76,14 @@ impl Router {
|
||||
let w = HomePage::new(&self.ndb, tx);
|
||||
self.current_widget = Some(Box::new(w));
|
||||
}
|
||||
Routes::Event { link, .. } => {
|
||||
Routes::EventPage { link, .. } => {
|
||||
let w = StreamPage::new_from_link(&self.ndb, tx, link.clone());
|
||||
self.current_widget = Some(Box::new(w));
|
||||
}
|
||||
Routes::LoginPage => {
|
||||
let w = LoginPage::new();
|
||||
self.current_widget = Some(Box::new(w));
|
||||
}
|
||||
_ => warn!("Not implemented"),
|
||||
}
|
||||
self.current = route;
|
||||
@ -85,10 +93,11 @@ impl Router {
|
||||
let tx = self.ndb.start_transaction();
|
||||
|
||||
// handle app state changes
|
||||
while let Some(r) = self.router.read(ui).next() {
|
||||
let mut q = self.router.read(ui);
|
||||
while let Some(r) = q.next() {
|
||||
if let Routes::Action(a) = &r {
|
||||
match a {
|
||||
RouteAction::Login(k) => self.login = Some(k.clone()),
|
||||
RouteAction::LoginPubkey(k) => self.login = Some(k.clone()),
|
||||
_ => info!("Not implemented"),
|
||||
}
|
||||
} else {
|
||||
@ -138,4 +147,10 @@ impl<'a> RouteServices<'a> {
|
||||
warn!("Failed to navigate");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn action(&self, route: RouteAction) {
|
||||
if let Err(e) = self.router.send(Routes::Action(route)) {
|
||||
warn!("Failed to navigate");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -74,10 +74,12 @@ impl NostrWidget for StreamPage {
|
||||
let h = ui.available_height();
|
||||
ui.allocate_ui(Vec2::new(w, h - chat_h), |ui| {
|
||||
if let Some(c) = self.chat.as_mut() {
|
||||
c.render(ui, services)
|
||||
c.render(ui, services);
|
||||
} else {
|
||||
ui.label("Loading..")
|
||||
ui.label("Loading..");
|
||||
}
|
||||
// consume rest of space
|
||||
ui.add_space(ui.available_height());
|
||||
});
|
||||
ui.allocate_ui(Vec2::new(w, chat_h), |ui| {
|
||||
self.new_msg.render(ui, services)
|
||||
|
@ -7,7 +7,8 @@ use nostrdb::{
|
||||
Error, Filter, Ndb, NdbProfile, Note, NoteKey, ProfileRecord, QueryResult, Subscription,
|
||||
Transaction,
|
||||
};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::collections::HashSet;
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
|
||||
pub struct NDBWrapper {
|
||||
@ -15,6 +16,7 @@ pub struct NDBWrapper {
|
||||
ndb: Ndb,
|
||||
client: Client,
|
||||
query_manager: QueryManager<Client>,
|
||||
profiles: Mutex<HashSet<[u8; 32]>>,
|
||||
}
|
||||
|
||||
/// Automatic cleanup for subscriptions
|
||||
@ -70,6 +72,7 @@ impl NDBWrapper {
|
||||
ndb,
|
||||
client,
|
||||
query_manager: qm,
|
||||
profiles: Mutex::new(HashSet::new()),
|
||||
}
|
||||
}
|
||||
|
||||
@ -144,11 +147,13 @@ impl NDBWrapper {
|
||||
|
||||
// TODO: fix this shit
|
||||
if p.is_none() {
|
||||
self.query_manager.queue_query("profile", &[
|
||||
nostr::Filter::new()
|
||||
.kinds([Kind::Metadata])
|
||||
.authors([PublicKey::from_slice(pubkey).unwrap()])
|
||||
])
|
||||
if self.profiles.lock().unwrap().insert(*pubkey) {
|
||||
self.query_manager.queue_query("profile", &[
|
||||
nostr::Filter::new()
|
||||
.kinds([Kind::Metadata])
|
||||
.authors([PublicKey::from_slice(pubkey).unwrap()])
|
||||
])
|
||||
}
|
||||
}
|
||||
let sub = None;
|
||||
(p, sub)
|
||||
|
@ -64,6 +64,12 @@ impl Query {
|
||||
let now = Utc::now();
|
||||
let id = Uuid::new_v4();
|
||||
|
||||
// remove filters already sent
|
||||
next = next
|
||||
.into_iter()
|
||||
.filter(|f| self.traces.len() == 0 || !self.traces.iter().all(|y| y.filters.iter().any(|z| z == f)))
|
||||
.collect();
|
||||
|
||||
// force profile queries into single filter
|
||||
if next.iter().all(|f| if let Some(k) = &f.kinds {
|
||||
k.len() == 1 && k.first().unwrap().as_u16() == 0
|
||||
@ -76,11 +82,6 @@ impl Query {
|
||||
]
|
||||
}
|
||||
|
||||
// remove filters already sent
|
||||
next = next
|
||||
.into_iter()
|
||||
.filter(|f| !self.traces.iter().any(|y| y.filters.iter().any(|z| z.eq(f))))
|
||||
.collect();
|
||||
|
||||
if next.len() == 0 {
|
||||
return None;
|
||||
|
@ -11,6 +11,12 @@ pub trait StreamInfo {
|
||||
fn stream(&self) -> Option<&str>;
|
||||
|
||||
fn starts(&self) -> u64;
|
||||
|
||||
fn image(&self) -> Option<&str>;
|
||||
|
||||
fn status(&self) -> Option<&str>;
|
||||
|
||||
fn viewers(&self) -> Option<u32>;
|
||||
}
|
||||
|
||||
impl<'a> StreamInfo for Note<'a> {
|
||||
@ -50,6 +56,7 @@ impl<'a> StreamInfo for Note<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn starts(&self) -> u64 {
|
||||
if let Some(s) = self.get_tag_value("starts") {
|
||||
s.variant().str()
|
||||
@ -58,4 +65,29 @@ impl<'a> StreamInfo for Note<'a> {
|
||||
self.created_at()
|
||||
}
|
||||
}
|
||||
|
||||
fn image(&self) -> Option<&str> {
|
||||
if let Some(s) = self.get_tag_value("image") {
|
||||
s.variant().str()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn status(&self) -> Option<&str> {
|
||||
if let Some(s) = self.get_tag_value("status") {
|
||||
s.variant().str()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn viewers(&self) -> Option<u32> {
|
||||
if let Some(s) = self.get_tag_value("current_participants") {
|
||||
s.variant().str()
|
||||
.map_or(None, |v| Some(v.parse::<u32>().unwrap_or(0)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
use egui::Color32;
|
||||
|
||||
pub const FONT_SIZE: f32 = 13.0;
|
||||
pub const PRIMARY: Color32 = Color32::from_rgb(248, 56, 217);
|
||||
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_900: Color32 = Color32::from_rgb(23, 23, 23);
|
@ -27,7 +27,7 @@ impl<'a> Avatar<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_profile(p: Option<NdbProfile<'a>>, svc: &'a ImageCache) -> Self {
|
||||
pub fn from_profile(p: &'a Option<NdbProfile<'a>>, svc: &'a ImageCache) -> Self {
|
||||
let img = p
|
||||
.map_or(None, |f| f.picture().map(|f| svc.load(f)));
|
||||
Self {
|
||||
|
32
src/widgets/button.rs
Normal file
32
src/widgets/button.rs
Normal file
@ -0,0 +1,32 @@
|
||||
use crate::theme::NEUTRAL_800;
|
||||
use egui::{Color32, CursorIcon, Frame, Margin, Response, Sense, Ui};
|
||||
|
||||
pub struct Button {
|
||||
color: Color32,
|
||||
}
|
||||
|
||||
impl Button {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
color: NEUTRAL_800
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show<F>(self, ui: &mut Ui, add_contents: F) -> Response
|
||||
where
|
||||
F: FnOnce(&mut Ui) -> Response,
|
||||
{
|
||||
let r = Frame::none()
|
||||
.inner_margin(Margin::symmetric(12., 8.))
|
||||
.fill(self.color)
|
||||
.rounding(12.)
|
||||
.show(ui, add_contents);
|
||||
|
||||
let id = r.response.id;
|
||||
ui.interact(
|
||||
r.response.on_hover_and_drag_cursor(CursorIcon::PointingHand).rect,
|
||||
id,
|
||||
Sense::click(),
|
||||
)
|
||||
}
|
||||
}
|
@ -25,6 +25,8 @@ impl<'a> Widget for ChatMessage<'a> {
|
||||
fn ui(self, ui: &mut Ui) -> Response {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
let mut job = LayoutJob::default();
|
||||
// TODO: avoid this somehow
|
||||
job.wrap.break_anywhere = true;
|
||||
|
||||
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())
|
||||
@ -48,7 +50,7 @@ impl<'a> Widget for ChatMessage<'a> {
|
||||
format.color = Color32::WHITE;
|
||||
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)
|
||||
.wrap_mode(TextWrapMode::Wrap)
|
||||
);
|
||||
|
@ -1,9 +1,9 @@
|
||||
use crate::route::{RouteServices, Routes};
|
||||
use crate::widgets::avatar::Avatar;
|
||||
use crate::widgets::NostrWidget;
|
||||
use crate::widgets::{Button, NostrWidget};
|
||||
use eframe::emath::Align;
|
||||
use eframe::epaint::Vec2;
|
||||
use egui::{Frame, Image, Layout, Margin, Response, Sense, Ui, Widget};
|
||||
use egui::{CursorIcon, Frame, Image, Layout, Margin, Response, Sense, Ui, Widget};
|
||||
|
||||
pub struct Header;
|
||||
|
||||
@ -28,13 +28,24 @@ impl NostrWidget for Header {
|
||||
.max_height(24.)
|
||||
.sense(Sense::click())
|
||||
.ui(ui)
|
||||
.on_hover_and_drag_cursor(CursorIcon::PointingHand)
|
||||
.clicked()
|
||||
{
|
||||
services.navigate(Routes::HomePage);
|
||||
}
|
||||
if let Some(pk) = services.login {
|
||||
//ui.add(Avatar::pubkey(pk, services));
|
||||
}
|
||||
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
if let Some(pk) = services.login {
|
||||
ui.add(Avatar::pubkey(pk, services));
|
||||
} else {
|
||||
if Button::new()
|
||||
.show(ui, |ui| {
|
||||
ui.label("Login")
|
||||
}).clicked() {
|
||||
services.navigate(Routes::LoginPage);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
)
|
||||
})
|
||||
|
@ -9,6 +9,8 @@ mod stream_player;
|
||||
mod video_placeholder;
|
||||
mod stream_title;
|
||||
mod write_chat;
|
||||
mod username;
|
||||
mod button;
|
||||
|
||||
use crate::route::RouteServices;
|
||||
use egui::{Response, Ui};
|
||||
@ -26,3 +28,5 @@ pub use self::stream_player::StreamPlayer;
|
||||
pub use self::video_placeholder::VideoPlaceholder;
|
||||
pub use self::stream_title::StreamTitle;
|
||||
pub use self::write_chat::WriteChat;
|
||||
pub use self::username::Username;
|
||||
pub use self::button::Button;
|
@ -1,9 +1,10 @@
|
||||
use crate::route::RouteServices;
|
||||
use crate::services::image_cache::ImageCache;
|
||||
use crate::services::ndb_wrapper::SubWrapper;
|
||||
use crate::widgets::Avatar;
|
||||
use egui::{Color32, Label, Response, RichText, TextWrapMode, Ui, Widget};
|
||||
use crate::widgets::{Avatar, Username};
|
||||
use egui::{Response, Ui, Widget};
|
||||
use nostrdb::NdbProfile;
|
||||
use crate::theme::FONT_SIZE;
|
||||
|
||||
pub struct Profile<'a> {
|
||||
size: f32,
|
||||
@ -36,13 +37,8 @@ impl<'a> Widget for Profile<'a> {
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 8.;
|
||||
|
||||
ui.add(Avatar::from_profile(self.profile, self.img_cache).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));
|
||||
ui.add(Avatar::from_profile(&self.profile, self.img_cache).size(self.size));
|
||||
ui.add(Username::new(&self.profile, FONT_SIZE))
|
||||
}).response
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,17 @@
|
||||
use crate::note_store::NoteStore;
|
||||
use crate::route::RouteServices;
|
||||
use crate::stream_info::StreamInfo;
|
||||
use crate::widgets::stream_tile::StreamEvent;
|
||||
use egui::{Frame, Margin, Response, Ui, Widget};
|
||||
use nostrdb::Note;
|
||||
use itertools::Itertools;
|
||||
|
||||
pub struct StreamList<'a> {
|
||||
streams: &'a Vec<Note<'a>>,
|
||||
streams: &'a NoteStore<'a>,
|
||||
services: &'a RouteServices<'a>,
|
||||
}
|
||||
|
||||
impl<'a> StreamList<'a> {
|
||||
pub fn new(streams: &'a Vec<Note<'a>>, services: &'a RouteServices) -> Self {
|
||||
pub fn new(streams: &'a NoteStore<'a>, services: &'a RouteServices) -> Self {
|
||||
Self { streams, services }
|
||||
}
|
||||
}
|
||||
@ -21,7 +23,10 @@ impl Widget for StreamList<'_> {
|
||||
.show(ui, |ui| {
|
||||
ui.vertical(|ui| {
|
||||
ui.style_mut().spacing.item_spacing = egui::vec2(0., 20.0);
|
||||
for event in self.streams {
|
||||
for event in self.streams.iter()
|
||||
.sorted_by(|a, b| {
|
||||
a.starts().cmp(&b.starts())
|
||||
}) {
|
||||
ui.add(StreamEvent::new(event, self.services));
|
||||
}
|
||||
})
|
||||
|
@ -1,32 +1,24 @@
|
||||
use crate::link::NostrLink;
|
||||
use crate::note_util::NoteUtil;
|
||||
use crate::route::{RouteServices, Routes};
|
||||
use crate::stream_info::StreamInfo;
|
||||
use crate::theme::{NEUTRAL_500, NEUTRAL_900, PRIMARY};
|
||||
use crate::widgets::avatar::Avatar;
|
||||
use crate::widgets::VideoPlaceholder;
|
||||
use eframe::epaint::Vec2;
|
||||
use egui::{Color32, Image, Label, Response, RichText, Rounding, Sense, TextWrapMode, Ui, Widget};
|
||||
use nostrdb::{NdbProfile, Note};
|
||||
use eframe::epaint::{Rounding, Vec2};
|
||||
use egui::epaint::RectShape;
|
||||
use egui::load::TexturePoll;
|
||||
use egui::{vec2, Color32, CursorIcon, FontId, Label, Pos2, Rect, Response, RichText, Sense, TextWrapMode, Ui, Widget};
|
||||
use image::Pixel;
|
||||
use nostrdb::Note;
|
||||
|
||||
pub struct StreamEvent<'a> {
|
||||
event: &'a Note<'a>,
|
||||
picture: Option<Image<'a>>,
|
||||
services: &'a RouteServices<'a>,
|
||||
}
|
||||
|
||||
impl<'a> StreamEvent<'a> {
|
||||
pub fn new(event: &'a Note<'a>, services: &'a RouteServices) -> Self {
|
||||
let image = event.get_tag_value("image");
|
||||
let cover = match image {
|
||||
Some(i) => match i.variant().str() {
|
||||
Some(i) => Some(services.img_cache.load(i)),
|
||||
None => None,
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
Self {
|
||||
event,
|
||||
picture: cover,
|
||||
services,
|
||||
}
|
||||
}
|
||||
@ -41,24 +33,65 @@ impl Widget for StreamEvent<'_> {
|
||||
|
||||
let w = ui.available_width();
|
||||
let h = (w / 16.0) * 9.0;
|
||||
let img_size = Vec2::new(w, h);
|
||||
let cover = self.event.image()
|
||||
.map(|p| self.services.img_cache.load(p));
|
||||
|
||||
let img = match self.picture {
|
||||
Some(picture) => picture
|
||||
.fit_to_exact_size(img_size)
|
||||
.rounding(Rounding::same(12.))
|
||||
.sense(Sense::click())
|
||||
.ui(ui),
|
||||
None => VideoPlaceholder.ui(ui),
|
||||
let (response, painter) = ui.allocate_painter(Vec2::new(w, h), Sense::click());
|
||||
|
||||
if let Some(cover) = cover.map(|c|
|
||||
c.rounding(Rounding::same(12.))
|
||||
.load_for_size(painter.ctx(), Vec2::new(w, h))) {
|
||||
match cover {
|
||||
Ok(TexturePoll::Ready { texture }) => {
|
||||
painter.add(RectShape {
|
||||
rect: response.rect,
|
||||
rounding: Rounding::same(12.),
|
||||
fill: Color32::WHITE,
|
||||
stroke: Default::default(),
|
||||
blur_width: 0.0,
|
||||
fill_texture_id: texture.id,
|
||||
uv: Rect::from_min_max(Pos2::new(0.0, 0.0), Pos2::new(1.0, 1.0)),
|
||||
});
|
||||
}
|
||||
_ => {
|
||||
painter.rect_filled(response.rect, 12., NEUTRAL_500);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
painter.rect_filled(response.rect, 12., NEUTRAL_500);
|
||||
}
|
||||
|
||||
let overlay_label_pad = Vec2::new(5., 5.);
|
||||
let live_label_text = self.event.status().unwrap_or("live").to_string().to_uppercase();
|
||||
let live_label_color = if live_label_text == "LIVE" {
|
||||
PRIMARY
|
||||
} else {
|
||||
NEUTRAL_900
|
||||
};
|
||||
if img.clicked() {
|
||||
self.services.navigate(Routes::Event {
|
||||
let live_label = painter.layout_no_wrap(live_label_text, FontId::default(), Color32::WHITE);
|
||||
|
||||
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_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.galley(live_label_pos + overlay_label_pad, live_label, Color32::PLACEHOLDER);
|
||||
|
||||
if let Some(viewers) = self.event.viewers() {
|
||||
let viewers_label = painter.layout_no_wrap(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);
|
||||
painter.rect_filled(pos, 8., NEUTRAL_900);
|
||||
painter.galley(rect_start + overlay_label_pad, viewers_label, Color32::PLACEHOLDER);
|
||||
}
|
||||
let response = response.on_hover_and_drag_cursor(CursorIcon::PointingHand);
|
||||
if response.clicked() {
|
||||
self.services.navigate(Routes::EventPage {
|
||||
link: NostrLink::from_note(&self.event),
|
||||
event: None,
|
||||
});
|
||||
}
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(Avatar::from_profile(host_profile, self.services.img_cache).size(40.));
|
||||
ui.add(Avatar::from_profile(&host_profile, self.services.img_cache).size(40.));
|
||||
let title = RichText::new(self.event.title().unwrap_or("Untitled"))
|
||||
.size(16.)
|
||||
.color(Color32::WHITE);
|
||||
|
23
src/widgets/username.rs
Normal file
23
src/widgets/username.rs
Normal file
@ -0,0 +1,23 @@
|
||||
use egui::{Color32, Label, Response, RichText, TextWrapMode, Ui, Widget};
|
||||
use nostrdb::NdbProfile;
|
||||
|
||||
pub struct Username<'a> {
|
||||
profile: &'a Option<NdbProfile<'a>>,
|
||||
size: f32,
|
||||
}
|
||||
|
||||
impl<'a> Username<'a> {
|
||||
pub fn new(profile: &'a Option<NdbProfile<'a>>, size: f32) -> Self {
|
||||
Self { profile, size }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for Username<'a> {
|
||||
fn ui(self, ui: &mut Ui) -> Response {
|
||||
let name = self
|
||||
.profile
|
||||
.map_or("Nostrich", |f| f.name().map_or("Nostrich", |f| f));
|
||||
let name = RichText::new(name).size(self.size).color(Color32::WHITE);
|
||||
ui.add(Label::new(name).wrap_mode(TextWrapMode::Truncate))
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
use egui::{Color32, Rect, Response, Rounding, Sense, Ui, Vec2, Widget};
|
||||
use egui::{Color32, Response, Rounding, Sense, Ui, Vec2, Widget};
|
||||
|
||||
pub struct VideoPlaceholder;
|
||||
|
||||
@ -10,7 +10,7 @@ impl Widget for VideoPlaceholder {
|
||||
|
||||
let (response, painter) = ui.allocate_painter(img_size, Sense::click());
|
||||
painter.rect_filled(
|
||||
Rect::EVERYTHING,
|
||||
response.rect,
|
||||
Rounding::same(12.),
|
||||
Color32::from_rgb(200, 200, 200),
|
||||
);
|
||||
|
Loading…
x
Reference in New Issue
Block a user