feat: responsive design

This commit is contained in:
kieran 2024-11-02 11:22:42 +00:00
parent 58132d2cf5
commit 7e67c3d8e7
No known key found for this signature in database
GPG Key ID: DE71CEB3925BE941
11 changed files with 229 additions and 80 deletions

2
Cargo.lock generated
View File

@ -2831,7 +2831,7 @@ dependencies = [
[[package]]
name = "nostrdb"
version = "0.3.4"
source = "git+https://github.com/damus-io/nostrdb-rs#9bbafd8a2e904b77a51e7cfca71eb5bb5650e829"
source = "git+https://github.com/damus-io/nostrdb-rs?branch=master#9bbafd8a2e904b77a51e7cfca71eb5bb5650e829"
dependencies = [
"bindgen 0.69.5",
"cc",

View File

@ -9,7 +9,7 @@ crate-type = ["lib", "cdylib"]
[dependencies]
tokio = { version = "1.40.0", features = ["fs", "rt-multi-thread", "rt"] }
egui = { version = "0.29.1" }
nostrdb = { git = "https://github.com/damus-io/nostrdb-rs", version = "0.3.4" }
nostrdb = { git = "https://github.com/damus-io/nostrdb-rs", branch = "master" }
nostr-sdk = { version = "0.35.0", features = ["all-nips"] }
log = "0.4.22"
pretty_env_logger = "0.5.0"
@ -25,10 +25,10 @@ reqwest = { version = "0.12.7", default-features = false, features = ["rustls-tl
itertools = "0.13.0"
lru = "0.12.5"
resvg = { version = "0.44.0", default-features = false }
egui-video = { git = "https://github.com/v0l/egui-video.git", rev = "ced65d0bb4d2d144b87c70518a04b767ba37c0c1" }
serde = { version = "1.0.214", features = ["derive"] }
serde_with = { version = "3.11.0", features = ["hex"] }
egui-video = { git = "https://github.com/v0l/egui-video.git", rev = "ced65d0bb4d2d144b87c70518a04b767ba37c0c1" }
#egui-video = { path = "../egui-video" }
[target.'cfg(not(target_os = "android"))'.dependencies]

View File

@ -1,7 +1,9 @@
use crate::app::{NativeLayer, NativeSecureStorage, ZapStreamApp};
use crate::app::{NativeLayerOps, ZapStreamApp};
use crate::av_log_redirect;
use eframe::Renderer;
use egui::{Margin, ViewportBuilder};
use serde::de::DeserializeOwned;
use serde::Serialize;
use std::ops::Div;
use winit::platform::android::activity::AndroidApp;
use winit::platform::android::EventLoopBuilderExtAndroid;
@ -36,13 +38,13 @@ pub fn start_android(app: AndroidApp) {
if let Err(e) = eframe::run_native(
"zap.stream",
options,
Box::new(move |cc| Ok(Box::new(ZapStreamApp::new(cc, data_path, Box::new(app))))),
Box::new(move |cc| Ok(Box::new(ZapStreamApp::new(cc, data_path, app)))),
) {
eprintln!("{}", e);
}
}
impl NativeLayer for AndroidApp {
impl NativeLayerOps for AndroidApp {
fn frame_margin(&self) -> Margin {
if let Some(wd) = self.native_window() {
let (w, h) = (wd.width(), wd.height());
@ -70,12 +72,6 @@ impl NativeLayer for AndroidApp {
self.hide_soft_input(true);
}
fn secure_storage(&self) -> Box<dyn NativeSecureStorage> {
Box::new(self.clone())
}
}
impl NativeSecureStorage for AndroidApp {
fn get(&self, k: &str) -> Option<String> {
None
}
@ -87,4 +83,12 @@ impl NativeSecureStorage for AndroidApp {
fn remove(&mut self, k: &str) -> bool {
false
}
fn get_obj<T: DeserializeOwned>(&self, k: &str) -> Option<T> {
None
}
fn set_obj<T: Serialize>(&mut self, k: &str, v: &T) -> bool {
false
}
}

View File

@ -1,5 +1,4 @@
use eframe::Renderer;
use egui::{Margin, Vec2};
use egui::{Margin, Vec2, ViewportBuilder};
use nostr_sdk::serde_json;
use serde::de::DeserializeOwned;
use serde::Serialize;
@ -19,8 +18,7 @@ async fn main() {
egui_video::ffmpeg_sys_the_third::av_log_set_callback(Some(av_log_redirect));
}
let mut options = eframe::NativeOptions::default();
options.renderer = Renderer::Glow;
options.viewport = options.viewport.with_inner_size(Vec2::new(360., 720.));
options.viewport = ViewportBuilder::default().with_inner_size(Vec2::new(1280., 720.));
let data_path = PathBuf::from("./.data");
let config = DesktopApp::new(data_path.clone());

View File

@ -32,7 +32,7 @@ pub unsafe extern "C" fn av_log_redirect(
use egui_video::ffmpeg_sys_the_third::*;
let log_level = match level {
AV_LOG_DEBUG => log::Level::Debug,
AV_LOG_WARNING => log::Level::Warn,
AV_LOG_WARNING => log::Level::Debug, // downgrade to debug (spammy)
AV_LOG_INFO => log::Level::Info,
AV_LOG_ERROR => log::Level::Error,
AV_LOG_PANIC => log::Level::Error,

View File

@ -3,7 +3,7 @@ use nostrdb::Note;
use std::collections::HashMap;
pub struct NoteStore<'a> {
events: HashMap<String, Note<'a>>,
events: HashMap<String, &'a Note<'a>>,
}
impl<'a> NoteStore<'a> {
@ -13,7 +13,11 @@ impl<'a> NoteStore<'a> {
}
}
pub fn from_vec(events: Vec<Note<'a>>) -> Self {
pub fn len(&self) -> usize {
self.events.len()
}
pub fn from_vec(events: Vec<&'a Note<'a>>) -> Self {
let mut store = Self::new();
for note in events {
store.add(note);
@ -21,7 +25,7 @@ impl<'a> NoteStore<'a> {
store
}
pub fn add(&mut self, note: Note<'a>) -> Option<Note<'a>> {
pub fn add(&mut self, note: &'a Note<'a>) -> Option<&'a Note<'a>> {
let k = Self::key(&note);
if let Some(v) = self.events.get(&k) {
if v.created_at() < note.created_at() {
@ -31,7 +35,7 @@ impl<'a> NoteStore<'a> {
self.events.insert(k, note)
}
pub fn remove(&mut self, note: &Note<'a>) -> Option<Note<'a>> {
pub fn remove(&mut self, note: &Note<'a>) -> Option<&'a Note<'a>> {
self.events.remove(&Self::key(note))
}
@ -39,7 +43,7 @@ impl<'a> NoteStore<'a> {
NostrLink::from_note(note).to_tag_value()
}
pub fn iter(&self) -> impl Iterator<Item = &Note<'a>> {
pub fn iter(&self) -> impl Iterator<Item = &&Note<'a>> {
self.events.values()
}
}

View File

@ -2,9 +2,10 @@ use crate::note_store::NoteStore;
use crate::note_util::OwnedNote;
use crate::route::RouteServices;
use crate::services::ndb_wrapper::{NDBWrapper, SubWrapper};
use crate::stream_info::{StreamInfo, StreamStatus};
use crate::widgets;
use crate::widgets::NostrWidget;
use egui::{Response, ScrollArea, Ui, Widget};
use egui::{Id, Response, RichText, ScrollArea, Ui, Widget};
use nostrdb::{Filter, Note, NoteKey, Transaction};
pub struct HomePage {
@ -14,8 +15,8 @@ pub struct HomePage {
impl HomePage {
pub fn new(ndb: &NDBWrapper, tx: &Transaction) -> Self {
let filter = [Filter::new().kinds([30_311]).limit(10).build()];
let (sub, events) = ndb.subscribe_with_results("home-page", &filter, tx, 100);
let filter = [Filter::new().kinds([30_311]).limit(100).build()];
let (sub, events) = ndb.subscribe_with_results("home-page", &filter, tx, 1000);
Self {
sub,
events: events
@ -40,9 +41,55 @@ impl NostrWidget for HomePage {
.map_while(|f| f.ok())
.collect();
let events = NoteStore::from_vec(events);
ScrollArea::vertical()
.show(ui, |ui| widgets::StreamList::new(&events, services).ui(ui))
.show(ui, |ui| {
let events_live = NoteStore::from_vec(
events
.iter()
.filter(|r| matches!(r.status(), StreamStatus::Live))
.collect(),
);
if events_live.len() > 0 {
widgets::StreamList::new(
Id::new("live-streams"),
&events_live,
services,
Some(RichText::new("Live").size(32.0)),
)
.ui(ui);
}
let events_planned = NoteStore::from_vec(
events
.iter()
.filter(|r| matches!(r.status(), StreamStatus::Planned))
.collect(),
);
if events_planned.len() > 0 {
widgets::StreamList::new(
Id::new("planned-streams"),
&events_planned,
services,
Some(RichText::new("Planned").size(32.0)),
)
.ui(ui);
}
let events_ended = NoteStore::from_vec(
events
.iter()
.filter(|r| matches!(r.status(), StreamStatus::Ended))
.collect(),
);
if events_ended.len() > 0 {
widgets::StreamList::new(
Id::new("ended-streams"),
&events_ended,
services,
Some(RichText::new("Ended").size(32.0)),
)
.ui(ui);
}
ui.response()
})
.inner
}
}

View File

@ -4,8 +4,8 @@ use crate::route::RouteServices;
use crate::services::ndb_wrapper::{NDBWrapper, SubWrapper};
use crate::stream_info::StreamInfo;
use crate::widgets::{Chat, NostrWidget, StreamPlayer, StreamTitle, WriteChat};
use egui::{Response, Ui, Vec2, Widget};
use nostrdb::{Filter, NoteKey, Transaction};
use egui::{vec2, Response, Ui, Vec2, Widget};
use nostrdb::{Filter, Note, NoteKey, Transaction};
use std::borrow::Borrow;
pub struct StreamPage {
@ -31,6 +31,81 @@ impl StreamPage {
new_msg: WriteChat::new(link),
}
}
fn render_mobile(
&mut self,
event: &Note<'_>,
ui: &mut Ui,
services: &mut RouteServices<'_>,
) -> Response {
if let Some(player) = &mut self.player {
player.ui(ui);
}
StreamTitle::new(&event).render(ui, services);
let chat_h = 60.0;
let w = ui.available_width();
let h = ui
.available_height()
.max(ui.available_rect_before_wrap().height())
.max(chat_h);
ui.allocate_ui(Vec2::new(w, h - chat_h), |ui| {
if let Some(c) = self.chat.as_mut() {
c.render(ui, services);
} else {
ui.label("Loading..");
}
// consume rest of space
if ui.available_height().is_finite() {
ui.add_space(ui.available_height());
}
});
ui.allocate_ui(vec2(w, chat_h), |ui| {
self.new_msg.render(ui, services);
});
ui.response()
}
fn render_desktop(
&mut self,
event: &Note<'_>,
ui: &mut Ui,
services: &mut RouteServices<'_>,
) -> Response {
let max_h = ui.available_height();
let chat_w = 450.0;
let video_width = ui.available_width() - chat_w;
let video_height = max_h.min((video_width / 16.0) * 9.0);
ui.horizontal_top(|ui| {
ui.vertical(|ui| {
if let Some(player) = &mut self.player {
ui.allocate_ui(vec2(video_width, video_height), |ui| player.ui(ui));
}
ui.add_space(10.);
StreamTitle::new(&event).render(ui, services);
});
ui.allocate_ui(vec2(chat_w, max_h), |ui| {
ui.vertical(|ui| {
let chat_h = 60.0;
if let Some(c) = self.chat.as_mut() {
ui.allocate_ui(vec2(chat_w, max_h - chat_h), |ui| {
c.render(ui, services);
if ui.available_height().is_finite() {
ui.add_space(ui.available_height() - chat_h);
}
});
} else {
ui.label("Loading..");
}
ui.allocate_ui(vec2(chat_w, chat_h), |ui| {
self.new_msg.render(ui, services);
});
})
});
});
ui.response()
}
}
impl NostrWidget for StreamPage {
@ -56,36 +131,17 @@ impl NostrWidget for StreamPage {
}
}
if let Some(player) = &mut self.player {
player.ui(ui);
}
StreamTitle::new(&event).render(ui, services);
if self.chat.is_none() {
let ok = OwnedNote(event.key().unwrap().as_u64());
let chat = Chat::new(self.link.clone(), ok, services.ndb, services.tx);
self.chat = Some(chat);
}
let chat_h = 60.0;
let w = ui.available_width();
let h = ui
.available_height()
.max(ui.available_rect_before_wrap().height())
.max(chat_h);
ui.allocate_ui(Vec2::new(w, h - chat_h), |ui| {
if let Some(c) = self.chat.as_mut() {
c.render(ui, services);
} else {
ui.label("Loading..");
}
// consume rest of space
if ui.available_height().is_finite() {
ui.add_space(ui.available_height());
}
});
ui.allocate_ui(Vec2::new(w, chat_h), |ui| self.new_msg.render(ui, services))
.response
if ui.available_width() < 720.0 {
self.render_mobile(&event, ui, services)
} else {
self.render_desktop(&event, ui, services)
}
} else {
ui.label("Loading..")
}

View File

@ -1,7 +1,7 @@
use crate::route::RouteServices;
use crate::services::image_cache::ImageCache;
use crate::services::ndb_wrapper::SubWrapper;
use egui::{Color32, Image, Pos2, Response, Rounding, Sense, Ui, Vec2, Widget};
use egui::{vec2, Color32, Image, Pos2, Response, Rounding, Sense, Ui, Vec2, Widget};
use nostrdb::NdbProfile;
pub struct Avatar<'a> {
@ -49,26 +49,31 @@ impl<'a> Avatar<'a> {
self.size = Some(size);
self
}
fn placeholder(ui: &mut Ui, size: f32) -> Response {
let (response, painter) = ui.allocate_painter(vec2(size, size), Sense::click());
painter.circle_filled(
Pos2::new(size / 2., size / 2.),
size / 2.,
Color32::from_rgb(200, 200, 200),
);
response
}
}
impl<'a> Widget for Avatar<'a> {
fn ui(self, ui: &mut Ui) -> Response {
let size_v = self.size.unwrap_or(40.);
let size = Vec2::new(size_v, size_v);
if !ui.is_rect_visible(ui.cursor()) {
return Self::placeholder(ui, size_v);
}
match self.image {
Some(img) => img
.fit_to_exact_size(size)
.rounding(Rounding::same(size_v))
.ui(ui),
None => {
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),
);
response
}
None => Self::placeholder(ui, size_v),
}
}
}

View File

@ -2,7 +2,6 @@ use crate::link::NostrLink;
use crate::note_util::OwnedNote;
use crate::route::RouteServices;
use crate::services::ndb_wrapper::{NDBWrapper, SubWrapper};
use crate::stream_info::StreamInfo;
use crate::widgets::chat_message::ChatMessage;
use crate::widgets::NostrWidget;
use egui::{Frame, Margin, Response, ScrollArea, Ui, Widget};
@ -71,7 +70,6 @@ impl NostrWidget for Chat {
for ev in events
.iter()
.sorted_by(|a, b| a.created_at().cmp(&b.created_at()))
.tail(20)
{
ChatMessage::new(&stream, ev, services).ui(ui);
}

View File

@ -2,35 +2,72 @@ 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 egui::{vec2, Frame, Grid, Margin, Response, Ui, Widget, WidgetText};
use itertools::Itertools;
pub struct StreamList<'a> {
id: egui::Id,
streams: &'a NoteStore<'a>,
services: &'a RouteServices<'a>,
heading: Option<WidgetText>,
}
impl<'a> StreamList<'a> {
pub fn new(streams: &'a NoteStore<'a>, services: &'a RouteServices) -> Self {
Self { streams, services }
pub fn new(
id: egui::Id,
streams: &'a NoteStore<'a>,
services: &'a RouteServices,
heading: Option<impl Into<WidgetText>>,
) -> Self {
Self {
id,
streams,
services,
heading: heading.map(Into::into),
}
}
}
impl Widget for StreamList<'_> {
fn ui(self, ui: &mut Ui) -> Response {
let cols = match ui.available_width() as u16 {
720..1080 => 2,
1080..1300 => 3,
1300..1500 => 4,
1500..2000 => 5,
2000.. => 6,
_ => 1,
};
let grid_padding = 20.;
let frame_margin = 16.0;
Frame::none()
.inner_margin(Margin::symmetric(16., 8.))
.inner_margin(Margin::symmetric(frame_margin, 0.))
.show(ui, |ui| {
ui.vertical(|ui| {
ui.style_mut().spacing.item_spacing = egui::vec2(0., 20.0);
for event in self.streams.iter().sorted_by(|a, b| {
a.status()
.cmp(&b.status())
.then(a.starts().cmp(&b.starts()).reverse())
}) {
ui.add(StreamEvent::new(event, self.services));
}
})
let grid_spacing_consumed = (cols - 1) as f32 * grid_padding;
let g_w = (ui.available_width() - grid_spacing_consumed) / cols as f32;
if let Some(heading) = self.heading {
ui.label(heading);
}
Grid::new(self.id)
.spacing(vec2(grid_padding, grid_padding))
.show(ui, |ui| {
let mut ctr = 0;
for event in self.streams.iter().sorted_by(|a, b| {
a.status()
.cmp(&b.status())
.then(a.starts().cmp(&b.starts()).reverse())
}) {
ui.add_sized(
vec2(g_w, (g_w / 16.0) * 9.0),
StreamEvent::new(event, self.services),
);
ctr += 1;
if ctr % cols == 0 {
ui.end_row();
}
}
})
})
.response
}