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
|
git clone https://github.com/v0l/ffmpeg-kit.git
|
||||||
export ANDROID_SDK_ROOT=$ANDROID_HOME
|
export ANDROID_SDK_ROOT=$ANDROID_HOME
|
||||||
cd ffmpeg-kit && ./android.sh \
|
#cd ffmpeg-kit && ./android.sh \
|
||||||
--disable-x86 \
|
# --disable-x86 \
|
||||||
--disable-x86-64 \
|
# --disable-x86-64 \
|
||||||
--disable-arm-v7a \
|
# --disable-arm-v7a \
|
||||||
--disable-arm-v7a-neon \
|
# --disable-arm-v7a-neon \
|
||||||
--enable-openssl \
|
# --enable-openssl \
|
||||||
--api-level=28 \
|
# --api-level=28 \
|
||||||
--no-ffmpeg-kit-protocols \
|
# --no-ffmpeg-kit-protocols \
|
||||||
--no-archive
|
# --no-archive
|
||||||
|
|
||||||
NDK_VER="28.0.12433566"
|
NDK_VER="28.0.12433566"
|
||||||
ARCH="arm64"
|
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"
|
export PKG_CONFIG_SYSROOT_DIR="$(pwd)/ffmpeg-kit/prebuilt/$PLATFORM-$ARCH/pkgconfig"
|
||||||
|
|
||||||
# DIRTY HACK !!
|
# 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" \
|
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
|
./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" \
|
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()
|
egui::CentralPanel::default()
|
||||||
.frame(app_frame)
|
.frame(app_frame)
|
||||||
.show(ctx, |ui| {
|
.show(ctx, |ui| {
|
||||||
|
ui.visuals_mut().override_text_color = Some(Color32::WHITE);
|
||||||
self.router.show(ui);
|
self.router.show(ui);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
10
src/lib.rs
10
src/lib.rs
@ -8,8 +8,10 @@ mod note_util;
|
|||||||
mod route;
|
mod route;
|
||||||
mod services;
|
mod services;
|
||||||
mod stream_info;
|
mod stream_info;
|
||||||
pub mod widgets;
|
mod widgets;
|
||||||
pub mod theme;
|
mod theme;
|
||||||
|
mod note_store;
|
||||||
|
|
||||||
|
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
use winit::platform::android::activity::AndroidApp;
|
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);
|
builder.with_android_app(app_clone_for_event_loop);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let external_data_path = app
|
let data_path = app
|
||||||
.external_data_path()
|
.external_data_path()
|
||||||
.expect("external data path")
|
.expect("external data path")
|
||||||
.to_path_buf();
|
.to_path_buf();
|
||||||
@ -39,6 +41,6 @@ pub async fn android_main(app: AndroidApp) {
|
|||||||
let _res = eframe::run_native(
|
let _res = eframe::run_native(
|
||||||
"zap.stream",
|
"zap.stream",
|
||||||
options,
|
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::note_util::OwnedNote;
|
||||||
use crate::route::RouteServices;
|
use crate::route::RouteServices;
|
||||||
use crate::services::ndb_wrapper::{NDBWrapper, SubWrapper};
|
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)))
|
.map_while(|f| f.map_or(None, |f| Some(f)))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
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)
|
||||||
|
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::note_util::OwnedNote;
|
||||||
use crate::route;
|
use crate::route;
|
||||||
use crate::route::home::HomePage;
|
use crate::route::home::HomePage;
|
||||||
|
use crate::route::login::LoginPage;
|
||||||
use crate::route::stream::StreamPage;
|
use crate::route::stream::StreamPage;
|
||||||
use crate::services::image_cache::ImageCache;
|
use crate::services::image_cache::ImageCache;
|
||||||
use crate::services::ndb_wrapper::NDBWrapper;
|
use crate::services::ndb_wrapper::NDBWrapper;
|
||||||
@ -18,11 +19,12 @@ use std::path::PathBuf;
|
|||||||
|
|
||||||
mod home;
|
mod home;
|
||||||
mod stream;
|
mod stream;
|
||||||
|
mod login;
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
pub enum Routes {
|
pub enum Routes {
|
||||||
HomePage,
|
HomePage,
|
||||||
Event {
|
EventPage {
|
||||||
link: NostrLink,
|
link: NostrLink,
|
||||||
event: Option<OwnedNote>,
|
event: Option<OwnedNote>,
|
||||||
},
|
},
|
||||||
@ -30,6 +32,7 @@ pub enum Routes {
|
|||||||
link: NostrLink,
|
link: NostrLink,
|
||||||
profile: Option<OwnedNote>,
|
profile: Option<OwnedNote>,
|
||||||
},
|
},
|
||||||
|
LoginPage,
|
||||||
|
|
||||||
// special kind for modifying route state
|
// special kind for modifying route state
|
||||||
Action(RouteAction),
|
Action(RouteAction),
|
||||||
@ -37,7 +40,8 @@ pub enum Routes {
|
|||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
pub enum RouteAction {
|
pub enum RouteAction {
|
||||||
Login([u8; 32]),
|
/// Login with public key
|
||||||
|
LoginPubkey([u8; 32]),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Router {
|
pub struct Router {
|
||||||
@ -72,10 +76,14 @@ impl Router {
|
|||||||
let w = HomePage::new(&self.ndb, tx);
|
let w = HomePage::new(&self.ndb, tx);
|
||||||
self.current_widget = Some(Box::new(w));
|
self.current_widget = Some(Box::new(w));
|
||||||
}
|
}
|
||||||
Routes::Event { link, .. } => {
|
Routes::EventPage { link, .. } => {
|
||||||
let w = StreamPage::new_from_link(&self.ndb, tx, link.clone());
|
let w = StreamPage::new_from_link(&self.ndb, tx, link.clone());
|
||||||
self.current_widget = Some(Box::new(w));
|
self.current_widget = Some(Box::new(w));
|
||||||
}
|
}
|
||||||
|
Routes::LoginPage => {
|
||||||
|
let w = LoginPage::new();
|
||||||
|
self.current_widget = Some(Box::new(w));
|
||||||
|
}
|
||||||
_ => warn!("Not implemented"),
|
_ => warn!("Not implemented"),
|
||||||
}
|
}
|
||||||
self.current = route;
|
self.current = route;
|
||||||
@ -85,10 +93,11 @@ impl Router {
|
|||||||
let tx = self.ndb.start_transaction();
|
let tx = self.ndb.start_transaction();
|
||||||
|
|
||||||
// handle app state changes
|
// 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 {
|
if let Routes::Action(a) = &r {
|
||||||
match a {
|
match a {
|
||||||
RouteAction::Login(k) => self.login = Some(k.clone()),
|
RouteAction::LoginPubkey(k) => self.login = Some(k.clone()),
|
||||||
_ => info!("Not implemented"),
|
_ => info!("Not implemented"),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -138,4 +147,10 @@ impl<'a> RouteServices<'a> {
|
|||||||
warn!("Failed to navigate");
|
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();
|
let h = ui.available_height();
|
||||||
ui.allocate_ui(Vec2::new(w, h - chat_h), |ui| {
|
ui.allocate_ui(Vec2::new(w, h - chat_h), |ui| {
|
||||||
if let Some(c) = self.chat.as_mut() {
|
if let Some(c) = self.chat.as_mut() {
|
||||||
c.render(ui, services)
|
c.render(ui, services);
|
||||||
} else {
|
} 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| {
|
ui.allocate_ui(Vec2::new(w, chat_h), |ui| {
|
||||||
self.new_msg.render(ui, services)
|
self.new_msg.render(ui, services)
|
||||||
|
@ -7,7 +7,8 @@ use nostrdb::{
|
|||||||
Error, Filter, Ndb, NdbProfile, Note, NoteKey, ProfileRecord, QueryResult, Subscription,
|
Error, Filter, Ndb, NdbProfile, Note, NoteKey, ProfileRecord, QueryResult, Subscription,
|
||||||
Transaction,
|
Transaction,
|
||||||
};
|
};
|
||||||
use std::sync::{Arc, RwLock};
|
use std::collections::HashSet;
|
||||||
|
use std::sync::{Arc, Mutex, RwLock};
|
||||||
use tokio::sync::mpsc::UnboundedSender;
|
use tokio::sync::mpsc::UnboundedSender;
|
||||||
|
|
||||||
pub struct NDBWrapper {
|
pub struct NDBWrapper {
|
||||||
@ -15,6 +16,7 @@ pub struct NDBWrapper {
|
|||||||
ndb: Ndb,
|
ndb: Ndb,
|
||||||
client: Client,
|
client: Client,
|
||||||
query_manager: QueryManager<Client>,
|
query_manager: QueryManager<Client>,
|
||||||
|
profiles: Mutex<HashSet<[u8; 32]>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Automatic cleanup for subscriptions
|
/// Automatic cleanup for subscriptions
|
||||||
@ -70,6 +72,7 @@ impl NDBWrapper {
|
|||||||
ndb,
|
ndb,
|
||||||
client,
|
client,
|
||||||
query_manager: qm,
|
query_manager: qm,
|
||||||
|
profiles: Mutex::new(HashSet::new()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,11 +147,13 @@ impl NDBWrapper {
|
|||||||
|
|
||||||
// TODO: fix this shit
|
// TODO: fix this shit
|
||||||
if p.is_none() {
|
if p.is_none() {
|
||||||
self.query_manager.queue_query("profile", &[
|
if self.profiles.lock().unwrap().insert(*pubkey) {
|
||||||
nostr::Filter::new()
|
self.query_manager.queue_query("profile", &[
|
||||||
.kinds([Kind::Metadata])
|
nostr::Filter::new()
|
||||||
.authors([PublicKey::from_slice(pubkey).unwrap()])
|
.kinds([Kind::Metadata])
|
||||||
])
|
.authors([PublicKey::from_slice(pubkey).unwrap()])
|
||||||
|
])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let sub = None;
|
let sub = None;
|
||||||
(p, sub)
|
(p, sub)
|
||||||
|
@ -64,6 +64,12 @@ impl Query {
|
|||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
let id = Uuid::new_v4();
|
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
|
// force profile queries into single filter
|
||||||
if next.iter().all(|f| if let Some(k) = &f.kinds {
|
if next.iter().all(|f| if let Some(k) = &f.kinds {
|
||||||
k.len() == 1 && k.first().unwrap().as_u16() == 0
|
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 {
|
if next.len() == 0 {
|
||||||
return None;
|
return None;
|
||||||
|
@ -11,6 +11,12 @@ pub trait StreamInfo {
|
|||||||
fn stream(&self) -> Option<&str>;
|
fn stream(&self) -> Option<&str>;
|
||||||
|
|
||||||
fn starts(&self) -> u64;
|
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> {
|
impl<'a> StreamInfo for Note<'a> {
|
||||||
@ -50,6 +56,7 @@ 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()
|
||||||
@ -58,4 +65,29 @@ impl<'a> StreamInfo for Note<'a> {
|
|||||||
self.created_at()
|
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;
|
use egui::Color32;
|
||||||
|
|
||||||
|
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_900: Color32 = Color32::from_rgb(23, 23, 23);
|
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
|
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 {
|
||||||
|
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 {
|
fn ui(self, ui: &mut Ui) -> Response {
|
||||||
ui.horizontal_wrapped(|ui| {
|
ui.horizontal_wrapped(|ui| {
|
||||||
let mut job = LayoutJob::default();
|
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 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())
|
||||||
@ -48,7 +50,7 @@ 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)
|
||||||
);
|
);
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
use crate::route::{RouteServices, Routes};
|
use crate::route::{RouteServices, Routes};
|
||||||
use crate::widgets::avatar::Avatar;
|
use crate::widgets::avatar::Avatar;
|
||||||
use crate::widgets::NostrWidget;
|
use crate::widgets::{Button, NostrWidget};
|
||||||
use eframe::emath::Align;
|
use eframe::emath::Align;
|
||||||
use eframe::epaint::Vec2;
|
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;
|
pub struct Header;
|
||||||
|
|
||||||
@ -28,13 +28,24 @@ impl NostrWidget for Header {
|
|||||||
.max_height(24.)
|
.max_height(24.)
|
||||||
.sense(Sense::click())
|
.sense(Sense::click())
|
||||||
.ui(ui)
|
.ui(ui)
|
||||||
|
.on_hover_and_drag_cursor(CursorIcon::PointingHand)
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
services.navigate(Routes::HomePage);
|
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 video_placeholder;
|
||||||
mod stream_title;
|
mod stream_title;
|
||||||
mod write_chat;
|
mod write_chat;
|
||||||
|
mod username;
|
||||||
|
mod button;
|
||||||
|
|
||||||
use crate::route::RouteServices;
|
use crate::route::RouteServices;
|
||||||
use egui::{Response, Ui};
|
use egui::{Response, Ui};
|
||||||
@ -26,3 +28,5 @@ pub use self::stream_player::StreamPlayer;
|
|||||||
pub use self::video_placeholder::VideoPlaceholder;
|
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::write_chat::WriteChat;
|
||||||
|
pub use self::username::Username;
|
||||||
|
pub use self::button::Button;
|
@ -1,9 +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::widgets::Avatar;
|
use crate::widgets::{Avatar, Username};
|
||||||
use egui::{Color32, Label, Response, RichText, TextWrapMode, 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,
|
||||||
@ -36,13 +37,8 @@ impl<'a> Widget for Profile<'a> {
|
|||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.spacing_mut().item_spacing.x = 8.;
|
ui.spacing_mut().item_spacing.x = 8.;
|
||||||
|
|
||||||
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))
|
||||||
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
|
}).response
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
|
use crate::note_store::NoteStore;
|
||||||
use crate::route::RouteServices;
|
use crate::route::RouteServices;
|
||||||
|
use crate::stream_info::StreamInfo;
|
||||||
use crate::widgets::stream_tile::StreamEvent;
|
use crate::widgets::stream_tile::StreamEvent;
|
||||||
use egui::{Frame, Margin, Response, Ui, Widget};
|
use egui::{Frame, Margin, Response, Ui, Widget};
|
||||||
use nostrdb::Note;
|
use itertools::Itertools;
|
||||||
|
|
||||||
pub struct StreamList<'a> {
|
pub struct StreamList<'a> {
|
||||||
streams: &'a Vec<Note<'a>>,
|
streams: &'a NoteStore<'a>,
|
||||||
services: &'a RouteServices<'a>,
|
services: &'a RouteServices<'a>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> StreamList<'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 }
|
Self { streams, services }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -21,7 +23,10 @@ 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 {
|
for event in self.streams.iter()
|
||||||
|
.sorted_by(|a, b| {
|
||||||
|
a.starts().cmp(&b.starts())
|
||||||
|
}) {
|
||||||
ui.add(StreamEvent::new(event, self.services));
|
ui.add(StreamEvent::new(event, self.services));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -1,32 +1,24 @@
|
|||||||
use crate::link::NostrLink;
|
use crate::link::NostrLink;
|
||||||
use crate::note_util::NoteUtil;
|
|
||||||
use crate::route::{RouteServices, Routes};
|
use crate::route::{RouteServices, Routes};
|
||||||
use crate::stream_info::StreamInfo;
|
use crate::stream_info::StreamInfo;
|
||||||
|
use crate::theme::{NEUTRAL_500, NEUTRAL_900, PRIMARY};
|
||||||
use crate::widgets::avatar::Avatar;
|
use crate::widgets::avatar::Avatar;
|
||||||
use crate::widgets::VideoPlaceholder;
|
use eframe::epaint::{Rounding, Vec2};
|
||||||
use eframe::epaint::Vec2;
|
use egui::epaint::RectShape;
|
||||||
use egui::{Color32, Image, Label, Response, RichText, Rounding, Sense, TextWrapMode, Ui, Widget};
|
use egui::load::TexturePoll;
|
||||||
use nostrdb::{NdbProfile, Note};
|
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> {
|
pub struct StreamEvent<'a> {
|
||||||
event: &'a Note<'a>,
|
event: &'a Note<'a>,
|
||||||
picture: Option<Image<'a>>,
|
|
||||||
services: &'a RouteServices<'a>,
|
services: &'a RouteServices<'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 {
|
||||||
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 {
|
Self {
|
||||||
event,
|
event,
|
||||||
picture: cover,
|
|
||||||
services,
|
services,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -41,24 +33,65 @@ 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 img_size = Vec2::new(w, h);
|
let cover = self.event.image()
|
||||||
|
.map(|p| self.services.img_cache.load(p));
|
||||||
|
|
||||||
let img = match self.picture {
|
let (response, painter) = ui.allocate_painter(Vec2::new(w, h), Sense::click());
|
||||||
Some(picture) => picture
|
|
||||||
.fit_to_exact_size(img_size)
|
if let Some(cover) = cover.map(|c|
|
||||||
.rounding(Rounding::same(12.))
|
c.rounding(Rounding::same(12.))
|
||||||
.sense(Sense::click())
|
.load_for_size(painter.ctx(), Vec2::new(w, h))) {
|
||||||
.ui(ui),
|
match cover {
|
||||||
None => VideoPlaceholder.ui(ui),
|
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() {
|
let live_label = painter.layout_no_wrap(live_label_text, FontId::default(), Color32::WHITE);
|
||||||
self.services.navigate(Routes::Event {
|
|
||||||
|
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),
|
link: NostrLink::from_note(&self.event),
|
||||||
event: None,
|
event: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
ui.horizontal(|ui| {
|
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"))
|
let title = RichText::new(self.event.title().unwrap_or("Untitled"))
|
||||||
.size(16.)
|
.size(16.)
|
||||||
.color(Color32::WHITE);
|
.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;
|
pub struct VideoPlaceholder;
|
||||||
|
|
||||||
@ -10,7 +10,7 @@ impl Widget for VideoPlaceholder {
|
|||||||
|
|
||||||
let (response, painter) = ui.allocate_painter(img_size, Sense::click());
|
let (response, painter) = ui.allocate_painter(img_size, Sense::click());
|
||||||
painter.rect_filled(
|
painter.rect_filled(
|
||||||
Rect::EVERYTHING,
|
response.rect,
|
||||||
Rounding::same(12.),
|
Rounding::same(12.),
|
||||||
Color32::from_rgb(200, 200, 200),
|
Color32::from_rgb(200, 200, 200),
|
||||||
);
|
);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user