feat: responsive design
This commit is contained in:
parent
58132d2cf5
commit
7e67c3d8e7
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -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",
|
||||
|
@ -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]
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
|
@ -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,
|
||||
|
@ -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(¬e);
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
if ui.available_width() < 720.0 {
|
||||
self.render_mobile(&event, ui, services)
|
||||
} else {
|
||||
ui.label("Loading..");
|
||||
self.render_desktop(&event, ui, services)
|
||||
}
|
||||
// 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
|
||||
} else {
|
||||
ui.label("Loading..")
|
||||
}
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -2,33 +2,70 @@ 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);
|
||||
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(StreamEvent::new(event, self.services));
|
||||
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();
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
Loading…
x
Reference in New Issue
Block a user