feat: native layer code
feat: login kinds (nsec) feat: write chat messages
This commit is contained in:
parent
a98eb6f4ce
commit
58132d2cf5
889
Cargo.lock
generated
889
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
14
Cargo.toml
14
Cargo.toml
@ -9,7 +9,6 @@ crate-type = ["lib", "cdylib"]
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
tokio = { version = "1.40.0", features = ["fs", "rt-multi-thread", "rt"] }
|
tokio = { version = "1.40.0", features = ["fs", "rt-multi-thread", "rt"] }
|
||||||
egui = { version = "0.29.1" }
|
egui = { version = "0.29.1" }
|
||||||
eframe = { version = "0.29.1", default-features = false, features = ["glow", "wgpu", "wayland", "x11", "android-native-activity"] }
|
|
||||||
nostrdb = { git = "https://github.com/damus-io/nostrdb-rs", version = "0.3.4" }
|
nostrdb = { git = "https://github.com/damus-io/nostrdb-rs", version = "0.3.4" }
|
||||||
nostr-sdk = { version = "0.35.0", features = ["all-nips"] }
|
nostr-sdk = { version = "0.35.0", features = ["all-nips"] }
|
||||||
log = "0.4.22"
|
log = "0.4.22"
|
||||||
@ -25,12 +24,19 @@ sha2 = "0.10.8"
|
|||||||
reqwest = { version = "0.12.7", default-features = false, features = ["rustls-tls-native-roots"] }
|
reqwest = { version = "0.12.7", default-features = false, features = ["rustls-tls-native-roots"] }
|
||||||
itertools = "0.13.0"
|
itertools = "0.13.0"
|
||||||
lru = "0.12.5"
|
lru = "0.12.5"
|
||||||
|
resvg = { version = "0.44.0", default-features = false }
|
||||||
|
|
||||||
egui-video = { git = "https://github.com/v0l/egui-video.git", rev = "ced65d0bb4d2d144b87c70518a04b767ba37c0c1" }
|
egui-video = { git = "https://github.com/v0l/egui-video.git", rev = "ced65d0bb4d2d144b87c70518a04b767ba37c0c1" }
|
||||||
resvg = { version = "0.44.0", default-features = false }
|
serde = { version = "1.0.214", features = ["derive"] }
|
||||||
|
serde_with = { version = "3.11.0", features = ["hex"] }
|
||||||
#egui-video = { path = "../egui-video" }
|
#egui-video = { path = "../egui-video" }
|
||||||
|
|
||||||
|
[target.'cfg(not(target_os = "android"))'.dependencies]
|
||||||
|
eframe = { version = "0.29.1" }
|
||||||
|
|
||||||
[target.'cfg(target_os = "android")'.dependencies]
|
[target.'cfg(target_os = "android")'.dependencies]
|
||||||
|
eframe = { version = "0.29.1", features = ["android-native-activity"] }
|
||||||
android_logger = "0.14.1"
|
android_logger = "0.14.1"
|
||||||
android-activity = { version = "0.6.0", features = ["native-activity"] }
|
android-activity = { version = "0.6.0" }
|
||||||
winit = { version = "0.30.5", features = ["android-native-activity"] }
|
winit = { version = "0.30.5" }
|
||||||
|
android-ndk-sys = "0.2.0"
|
||||||
|
90
src/android.rs
Normal file
90
src/android.rs
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
use crate::app::{NativeLayer, NativeSecureStorage, ZapStreamApp};
|
||||||
|
use crate::av_log_redirect;
|
||||||
|
use eframe::Renderer;
|
||||||
|
use egui::{Margin, ViewportBuilder};
|
||||||
|
use std::ops::Div;
|
||||||
|
use winit::platform::android::activity::AndroidApp;
|
||||||
|
use winit::platform::android::EventLoopBuilderExtAndroid;
|
||||||
|
|
||||||
|
pub fn start_android(app: AndroidApp) {
|
||||||
|
std::env::set_var("RUST_BACKTRACE", "full");
|
||||||
|
android_logger::init_once(
|
||||||
|
android_logger::Config::default().with_max_level(log::LevelFilter::Info),
|
||||||
|
);
|
||||||
|
unsafe {
|
||||||
|
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 = ViewportBuilder::default()
|
||||||
|
.with_active(true)
|
||||||
|
.with_always_on_top()
|
||||||
|
.with_fullscreen(true);
|
||||||
|
|
||||||
|
let app_clone_for_event_loop = app.clone();
|
||||||
|
options.event_loop_builder = Some(Box::new(move |builder| {
|
||||||
|
builder.with_android_app(app_clone_for_event_loop);
|
||||||
|
}));
|
||||||
|
|
||||||
|
let data_path = app
|
||||||
|
.external_data_path()
|
||||||
|
.expect("external data path")
|
||||||
|
.to_path_buf();
|
||||||
|
|
||||||
|
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))))),
|
||||||
|
) {
|
||||||
|
eprintln!("{}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NativeLayer for AndroidApp {
|
||||||
|
fn frame_margin(&self) -> Margin {
|
||||||
|
if let Some(wd) = self.native_window() {
|
||||||
|
let (w, h) = (wd.width(), wd.height());
|
||||||
|
let c_rect = self.content_rect();
|
||||||
|
let dpi = self.config().density().unwrap_or(160);
|
||||||
|
let dpi_scale = dpi as f32 / 160.0;
|
||||||
|
// TODO: this calc is weird but seems to work on my phone
|
||||||
|
Margin {
|
||||||
|
bottom: (h - c_rect.bottom) as f32,
|
||||||
|
left: c_rect.left as f32,
|
||||||
|
right: (w - c_rect.right) as f32,
|
||||||
|
top: (c_rect.top - (h - c_rect.bottom)) as f32,
|
||||||
|
}
|
||||||
|
.div(dpi_scale)
|
||||||
|
} else {
|
||||||
|
Margin::ZERO
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_keyboard(&self) {
|
||||||
|
self.show_soft_input(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hide_keyboard(&self) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set(&mut self, k: &str, v: &str) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove(&mut self, k: &str) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
39
src/app.rs
39
src/app.rs
@ -6,18 +6,30 @@ use nostr_sdk::Client;
|
|||||||
use nostrdb::{Config, Ndb};
|
use nostrdb::{Config, Ndb};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
pub struct ZapStreamApp<T> {
|
pub struct ZapStreamApp<T: NativeLayerOps> {
|
||||||
client: Client,
|
client: Client,
|
||||||
router: Router,
|
router: Router<T>,
|
||||||
config: T,
|
native_layer: T,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Trait to wrap native configuration layers
|
pub trait NativeLayerOps {
|
||||||
pub trait AppConfig {
|
/// Get any display layout margins
|
||||||
fn frame_margin(&self) -> Margin;
|
fn frame_margin(&self) -> Margin;
|
||||||
|
/// Show the keyboard on the screen
|
||||||
|
fn show_keyboard(&self);
|
||||||
|
/// Hide on screen keyboard
|
||||||
|
fn hide_keyboard(&self);
|
||||||
|
fn get(&self, k: &str) -> Option<String>;
|
||||||
|
fn set(&mut self, k: &str, v: &str) -> bool;
|
||||||
|
fn remove(&mut self, k: &str) -> bool;
|
||||||
|
fn get_obj<T: serde::de::DeserializeOwned>(&self, k: &str) -> Option<T>;
|
||||||
|
fn set_obj<T: serde::Serialize>(&mut self, k: &str, v: &T) -> bool;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> ZapStreamApp<T> {
|
impl<T> ZapStreamApp<T>
|
||||||
|
where
|
||||||
|
T: NativeLayerOps + Clone,
|
||||||
|
{
|
||||||
pub fn new(cc: &CreationContext, data_path: PathBuf, config: T) -> Self {
|
pub fn new(cc: &CreationContext, data_path: PathBuf, config: T) -> Self {
|
||||||
let client = Client::builder()
|
let client = Client::builder()
|
||||||
.database(MemoryDatabase::with_opts(Default::default()))
|
.database(MemoryDatabase::with_opts(Default::default()))
|
||||||
@ -48,21 +60,28 @@ impl<T> ZapStreamApp<T> {
|
|||||||
|
|
||||||
let ndb = Ndb::new(ndb_path.to_str().unwrap(), &ndb_config).unwrap();
|
let ndb = Ndb::new(ndb_path.to_str().unwrap(), &ndb_config).unwrap();
|
||||||
|
|
||||||
|
let cfg = config.clone();
|
||||||
Self {
|
Self {
|
||||||
client: client.clone(),
|
client: client.clone(),
|
||||||
router: Router::new(data_path, cc.egui_ctx.clone(), client.clone(), ndb.clone()),
|
router: Router::new(
|
||||||
config,
|
data_path,
|
||||||
|
cc.egui_ctx.clone(),
|
||||||
|
client.clone(),
|
||||||
|
ndb.clone(),
|
||||||
|
cfg,
|
||||||
|
),
|
||||||
|
native_layer: config,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> App for ZapStreamApp<T>
|
impl<T> App for ZapStreamApp<T>
|
||||||
where
|
where
|
||||||
T: AppConfig,
|
T: NativeLayerOps,
|
||||||
{
|
{
|
||||||
fn update(&mut self, ctx: &Context, frame: &mut Frame) {
|
fn update(&mut self, ctx: &Context, frame: &mut Frame) {
|
||||||
let mut app_frame = egui::containers::Frame::default();
|
let mut app_frame = egui::containers::Frame::default();
|
||||||
let margin = self.config.frame_margin();
|
let margin = self.native_layer.frame_margin();
|
||||||
|
|
||||||
app_frame.inner_margin = margin;
|
app_frame.inner_margin = margin;
|
||||||
app_frame.stroke.color = Color32::BLACK;
|
app_frame.stroke.color = Color32::BLACK;
|
||||||
|
@ -1,20 +1,29 @@
|
|||||||
use eframe::Renderer;
|
use eframe::Renderer;
|
||||||
use egui::{Margin, Vec2};
|
use egui::{Margin, Vec2};
|
||||||
|
use nostr_sdk::serde_json;
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::ops::Deref;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use zap_stream_app::app::{AppConfig, ZapStreamApp};
|
use std::sync::{Arc, RwLock};
|
||||||
|
use zap_stream_app::app::{NativeLayerOps, ZapStreamApp};
|
||||||
|
use zap_stream_app::av_log_redirect;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
pretty_env_logger::init();
|
pretty_env_logger::init();
|
||||||
|
|
||||||
// TODO: redirect FFMPEG logs to log file (noisy)
|
unsafe {
|
||||||
|
egui_video::ffmpeg_sys_the_third::av_log_set_callback(Some(av_log_redirect));
|
||||||
|
}
|
||||||
let mut options = eframe::NativeOptions::default();
|
let mut options = eframe::NativeOptions::default();
|
||||||
options.renderer = Renderer::Glow;
|
options.renderer = Renderer::Glow;
|
||||||
options.viewport = options.viewport.with_inner_size(Vec2::new(360., 720.));
|
options.viewport = options.viewport.with_inner_size(Vec2::new(360., 720.));
|
||||||
|
|
||||||
let config = DesktopApp;
|
|
||||||
let data_path = PathBuf::from("./.data");
|
let data_path = PathBuf::from("./.data");
|
||||||
|
let config = DesktopApp::new(data_path.clone());
|
||||||
let _res = eframe::run_native(
|
let _res = eframe::run_native(
|
||||||
"zap.stream",
|
"zap.stream",
|
||||||
options,
|
options,
|
||||||
@ -22,10 +31,85 @@ async fn main() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DesktopApp;
|
#[derive(Clone)]
|
||||||
|
pub struct DesktopApp {
|
||||||
|
data_path: PathBuf,
|
||||||
|
data: Arc<RwLock<HashMap<String, String>>>,
|
||||||
|
}
|
||||||
|
|
||||||
impl AppConfig for DesktopApp {
|
impl DesktopApp {
|
||||||
|
pub fn new(data_path: PathBuf) -> Self {
|
||||||
|
let mut r = Self {
|
||||||
|
data_path,
|
||||||
|
data: Arc::new(RwLock::new(HashMap::new())),
|
||||||
|
};
|
||||||
|
r.load();
|
||||||
|
r
|
||||||
|
}
|
||||||
|
|
||||||
|
fn storage_file_path(&self) -> PathBuf {
|
||||||
|
self.data_path.join("kv.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load(&mut self) {
|
||||||
|
let path = self.storage_file_path();
|
||||||
|
if path.exists() {
|
||||||
|
let mut file = std::fs::File::open(path).unwrap();
|
||||||
|
let mut data = Vec::new();
|
||||||
|
file.read_to_end(&mut data).unwrap();
|
||||||
|
if let Ok(d) = serde_json::from_slice(data.as_slice()) {
|
||||||
|
self.data = Arc::new(RwLock::new(d));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save(&self) {
|
||||||
|
let path = self.storage_file_path();
|
||||||
|
let mut file = std::fs::File::create(path).unwrap();
|
||||||
|
let json = serde_json::to_string_pretty(self.data.read().unwrap().deref()).unwrap();
|
||||||
|
file.write_all(json.as_bytes()).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NativeLayerOps for DesktopApp {
|
||||||
fn frame_margin(&self) -> Margin {
|
fn frame_margin(&self) -> Margin {
|
||||||
Margin::ZERO
|
Margin::ZERO
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn show_keyboard(&self) {
|
||||||
|
// nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hide_keyboard(&self) {
|
||||||
|
// nothing to do
|
||||||
|
}
|
||||||
|
fn get(&self, k: &str) -> Option<String> {
|
||||||
|
self.data.read().unwrap().get(k).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set(&mut self, k: &str, v: &str) -> bool {
|
||||||
|
self.data
|
||||||
|
.write()
|
||||||
|
.unwrap()
|
||||||
|
.insert(k.to_owned(), v.to_owned())
|
||||||
|
.is_none()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove(&mut self, k: &str) -> bool {
|
||||||
|
self.data.write().unwrap().remove(k).is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_obj<T: DeserializeOwned>(&self, k: &str) -> Option<T> {
|
||||||
|
serde_json::from_str(self.get(k)?.as_str()).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_obj<T: Serialize>(&mut self, k: &str, v: &T) -> bool {
|
||||||
|
self.set(k, serde_json::to_string(v).unwrap().as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for DesktopApp {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.save();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
97
src/lib.rs
97
src/lib.rs
@ -1,5 +1,8 @@
|
|||||||
|
#[cfg(target_os = "android")]
|
||||||
|
mod android;
|
||||||
pub mod app;
|
pub mod app;
|
||||||
mod link;
|
mod link;
|
||||||
|
mod login;
|
||||||
mod note_store;
|
mod note_store;
|
||||||
mod note_util;
|
mod note_util;
|
||||||
mod route;
|
mod route;
|
||||||
@ -8,65 +11,51 @@ mod stream_info;
|
|||||||
mod theme;
|
mod theme;
|
||||||
mod widgets;
|
mod widgets;
|
||||||
|
|
||||||
use crate::app::{AppConfig, ZapStreamApp};
|
|
||||||
use eframe::Renderer;
|
|
||||||
use egui::{Margin, ViewportBuilder};
|
|
||||||
use std::ops::{Div, Mul};
|
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
use winit::platform::android::activity::AndroidApp;
|
use android_activity::AndroidApp;
|
||||||
|
use log::log;
|
||||||
|
use std::ffi::CStr;
|
||||||
|
use std::ptr;
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
type VaList = *mut egui_video::ffmpeg_sys_the_third::__va_list_tag;
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
use winit::platform::android::EventLoopBuilderExtAndroid;
|
type VaList = [u64; 4];
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn av_log_redirect(
|
||||||
|
av_class: *mut libc::c_void,
|
||||||
|
level: libc::c_int,
|
||||||
|
fmt: *const libc::c_char,
|
||||||
|
args: VaList,
|
||||||
|
) {
|
||||||
|
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_INFO => log::Level::Info,
|
||||||
|
AV_LOG_ERROR => log::Level::Error,
|
||||||
|
AV_LOG_PANIC => log::Level::Error,
|
||||||
|
AV_LOG_FATAL => log::Level::Error,
|
||||||
|
_ => log::Level::Trace,
|
||||||
|
};
|
||||||
|
let mut buf: [u8; 1024] = [0; 1024];
|
||||||
|
let mut prefix: libc::c_int = 1;
|
||||||
|
av_log_format_line(
|
||||||
|
av_class,
|
||||||
|
level,
|
||||||
|
fmt,
|
||||||
|
args,
|
||||||
|
buf.as_mut_ptr() as *mut libc::c_char,
|
||||||
|
1024,
|
||||||
|
ptr::addr_of_mut!(prefix),
|
||||||
|
);
|
||||||
|
log!(target: "ffmpeg", log_level, "{}", CStr::from_ptr(buf.as_ptr() as *const libc::c_char).to_str().unwrap().trim());
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
pub async fn android_main(app: AndroidApp) {
|
pub async fn android_main(app: AndroidApp) {
|
||||||
std::env::set_var("RUST_BACKTRACE", "full");
|
android::start_android(app);
|
||||||
android_logger::init_once(
|
|
||||||
android_logger::Config::default().with_max_level(log::LevelFilter::Info),
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut options = eframe::NativeOptions::default();
|
|
||||||
options.renderer = Renderer::Glow;
|
|
||||||
|
|
||||||
options.viewport = ViewportBuilder::default().with_fullscreen(true);
|
|
||||||
|
|
||||||
let app_clone_for_event_loop = app.clone();
|
|
||||||
options.event_loop_builder = Some(Box::new(move |builder| {
|
|
||||||
builder.with_android_app(app_clone_for_event_loop);
|
|
||||||
}));
|
|
||||||
|
|
||||||
let data_path = app
|
|
||||||
.external_data_path()
|
|
||||||
.expect("external data path")
|
|
||||||
.to_path_buf();
|
|
||||||
|
|
||||||
let app = app.clone();
|
|
||||||
let _res = eframe::run_native(
|
|
||||||
"zap.stream",
|
|
||||||
options,
|
|
||||||
Box::new(move |cc| Ok(Box::new(ZapStreamApp::new(cc, data_path, app)))),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "android")]
|
|
||||||
impl AppConfig for AndroidApp {
|
|
||||||
fn frame_margin(&self) -> Margin {
|
|
||||||
if let Some(wd) = self.native_window() {
|
|
||||||
let (w, h) = (wd.width(), wd.height());
|
|
||||||
let c_rect = self.content_rect();
|
|
||||||
let dpi = self.config().density().unwrap_or(160);
|
|
||||||
let dpi_scale = dpi as f32 / 160.0;
|
|
||||||
// TODO: this calc is weird but seems to work on my phone
|
|
||||||
Margin {
|
|
||||||
bottom: (h - c_rect.bottom) as f32,
|
|
||||||
left: c_rect.left as f32,
|
|
||||||
right: (w - c_rect.right) as f32,
|
|
||||||
top: (c_rect.top - (h - c_rect.bottom)) as f32,
|
|
||||||
}
|
|
||||||
.div(dpi_scale)
|
|
||||||
} else {
|
|
||||||
Margin::ZERO
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
85
src/login.rs
Normal file
85
src/login.rs
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
use crate::app::NativeLayerOps;
|
||||||
|
use crate::link::NostrLink;
|
||||||
|
use anyhow::Error;
|
||||||
|
use nostr_sdk::secp256k1::{Keypair, XOnlyPublicKey};
|
||||||
|
use nostr_sdk::{Event, EventBuilder, Keys, Kind, SecretKey, Tag, UnsignedEvent};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_with::serde_as;
|
||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
|
#[serde_as]
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub enum LoginKind {
|
||||||
|
PublicKey {
|
||||||
|
#[serde_as(as = "serde_with::hex::Hex")]
|
||||||
|
key: [u8; 32],
|
||||||
|
},
|
||||||
|
PrivateKey {
|
||||||
|
#[serde_as(as = "serde_with::hex::Hex")]
|
||||||
|
key: [u8; 32],
|
||||||
|
},
|
||||||
|
LoggedOut,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Login {
|
||||||
|
kind: LoginKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Login {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
kind: LoginKind::LoggedOut,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load<T: NativeLayerOps>(&mut self, storage: &T) {
|
||||||
|
if let Some(k) = storage.get_obj("login") {
|
||||||
|
self.kind = k;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save<T: NativeLayerOps>(&mut self, storage: &mut T) {
|
||||||
|
storage.set_obj("login", &self.kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn login(&mut self, kind: LoginKind) {
|
||||||
|
self.kind = kind;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_logged_in(&self) -> bool {
|
||||||
|
!matches!(self.kind, LoginKind::LoggedOut)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn public_key(&self) -> Option<[u8; 32]> {
|
||||||
|
match self.kind {
|
||||||
|
LoginKind::PublicKey { key } => Some(key),
|
||||||
|
LoginKind::PrivateKey { key } => {
|
||||||
|
// TODO: wow this is annoying
|
||||||
|
let sk = Keypair::from_seckey_slice(nostr_sdk::SECP256K1.deref(), key.as_slice())
|
||||||
|
.unwrap();
|
||||||
|
Some(XOnlyPublicKey::from_keypair(&sk).0.serialize())
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn secret_key(&self) -> Result<Keys, Error> {
|
||||||
|
if let LoginKind::PrivateKey { key } = self.kind {
|
||||||
|
Ok(Keys::new(SecretKey::from_slice(key.as_slice())?))
|
||||||
|
} else {
|
||||||
|
anyhow::bail!("No private key");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sign_event(&self, ev: UnsignedEvent) -> Result<Event, Error> {
|
||||||
|
let secret = self.secret_key()?;
|
||||||
|
ev.sign(&secret).map_err(Error::new)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_live_chat_msg(&self, link: &NostrLink, msg: &str) -> Result<Event, Error> {
|
||||||
|
let secret = self.secret_key()?;
|
||||||
|
EventBuilder::new(Kind::LiveEventMessage, msg, [Tag::parse(&link.to_tag())?])
|
||||||
|
.to_event(&secret)
|
||||||
|
.map_err(Error::new)
|
||||||
|
}
|
||||||
|
}
|
@ -27,7 +27,7 @@ impl HomePage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl NostrWidget for HomePage {
|
impl NostrWidget for HomePage {
|
||||||
fn render(&mut self, ui: &mut Ui, services: &RouteServices<'_>) -> Response {
|
fn render(&mut self, ui: &mut Ui, services: &mut RouteServices<'_>) -> Response {
|
||||||
let new_notes = services.ndb.poll(&self.sub, 100);
|
let new_notes = services.ndb.poll(&self.sub, 100);
|
||||||
new_notes
|
new_notes
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
use crate::route::{RouteAction, RouteServices, Routes};
|
use crate::login::LoginKind;
|
||||||
use crate::widgets::{Button, NostrWidget};
|
use crate::route::{RouteServices, Routes};
|
||||||
use egui::{Color32, Response, RichText, Ui};
|
use crate::widgets::{Button, NativeTextInput, NostrWidget};
|
||||||
|
use egui::{Color32, Frame, Margin, Response, RichText, Ui};
|
||||||
use nostr_sdk::util::hex;
|
use nostr_sdk::util::hex;
|
||||||
|
|
||||||
pub struct LoginPage {
|
pub struct LoginPage {
|
||||||
@ -18,27 +19,49 @@ impl LoginPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl NostrWidget for LoginPage {
|
impl NostrWidget for LoginPage {
|
||||||
fn render(&mut self, ui: &mut Ui, services: &RouteServices<'_>) -> Response {
|
fn render(&mut self, ui: &mut Ui, services: &mut RouteServices<'_>) -> Response {
|
||||||
ui.vertical_centered(|ui| {
|
Frame::none()
|
||||||
ui.spacing_mut().item_spacing.y = 8.;
|
.inner_margin(Margin::same(12.))
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
ui.spacing_mut().item_spacing.y = 8.;
|
||||||
|
|
||||||
ui.label(RichText::new("Login").size(32.));
|
ui.label(RichText::new("Login").size(32.));
|
||||||
ui.label("Pubkey");
|
let mut input = NativeTextInput::new(&mut self.key).with_hint_text("npub/nsec");
|
||||||
ui.text_edit_singleline(&mut self.key);
|
input.render(ui, services);
|
||||||
if Button::new().show(ui, |ui| ui.label("Login")).clicked() {
|
|
||||||
if let Ok(pk) = hex::decode(&self.key) {
|
if Button::new().show(ui, |ui| ui.label("Login")).clicked() {
|
||||||
if let Ok(pk) = pk.as_slice().try_into() {
|
if let Ok((hrp, key)) = bech32::decode(&self.key) {
|
||||||
services.action(RouteAction::LoginPubkey(pk));
|
match hrp.to_lowercase().as_str() {
|
||||||
services.navigate(Routes::HomePage);
|
"nsec" => {
|
||||||
return;
|
services.login.login(LoginKind::PrivateKey {
|
||||||
|
key: key.as_slice().try_into().unwrap(),
|
||||||
|
});
|
||||||
|
services.navigate(Routes::HomePage);
|
||||||
|
}
|
||||||
|
"npub" | "nprofile" => {
|
||||||
|
services.login.login(LoginKind::PublicKey {
|
||||||
|
key: key.as_slice().try_into().unwrap(),
|
||||||
|
});
|
||||||
|
services.navigate(Routes::HomePage);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
} else if let Ok(pk) = hex::decode(&self.key) {
|
||||||
|
if let Ok(pk) = pk.as_slice().try_into() {
|
||||||
|
services.login.login(LoginKind::PublicKey { key: pk });
|
||||||
|
services.navigate(Routes::HomePage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.error = Some("Invalid pubkey".to_string());
|
||||||
}
|
}
|
||||||
}
|
if let Some(e) = &self.error {
|
||||||
self.error = Some("Invalid pubkey".to_string());
|
ui.label(RichText::new(e).color(Color32::RED));
|
||||||
}
|
}
|
||||||
if let Some(e) = &self.error {
|
})
|
||||||
ui.label(RichText::new(e).color(Color32::RED));
|
.response
|
||||||
}
|
})
|
||||||
})
|
.inner
|
||||||
.response
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
|
use crate::app::NativeLayerOps;
|
||||||
use crate::link::NostrLink;
|
use crate::link::NostrLink;
|
||||||
|
use crate::login::{Login, LoginKind};
|
||||||
use crate::note_util::OwnedNote;
|
use crate::note_util::OwnedNote;
|
||||||
use crate::route::home::HomePage;
|
use crate::route::home::HomePage;
|
||||||
use crate::route::login::LoginPage;
|
use crate::route::login::LoginPage;
|
||||||
@ -9,7 +11,7 @@ use crate::widgets::{Header, NostrWidget};
|
|||||||
use egui::{Context, Response, Ui};
|
use egui::{Context, Response, Ui};
|
||||||
use egui_inbox::{UiInbox, UiInboxSender};
|
use egui_inbox::{UiInbox, UiInboxSender};
|
||||||
use log::{info, warn};
|
use log::{info, warn};
|
||||||
use nostr_sdk::Client;
|
use nostr_sdk::{Client, Event, JsonUtil};
|
||||||
use nostrdb::{Ndb, Transaction};
|
use nostrdb::{Ndb, Transaction};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
@ -36,24 +38,40 @@ pub enum Routes {
|
|||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
pub enum RouteAction {
|
pub enum RouteAction {
|
||||||
/// Login with public key
|
ShowKeyboard,
|
||||||
LoginPubkey([u8; 32]),
|
HideKeyboard,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Router {
|
pub struct Router<T: NativeLayerOps> {
|
||||||
current: Routes,
|
current: Routes,
|
||||||
current_widget: Option<Box<dyn NostrWidget>>,
|
current_widget: Option<Box<dyn NostrWidget>>,
|
||||||
router: UiInbox<Routes>,
|
router: UiInbox<Routes>,
|
||||||
|
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
ndb: NDBWrapper,
|
ndb: NDBWrapper,
|
||||||
login: Option<[u8; 32]>,
|
login: Login,
|
||||||
client: Client,
|
client: Client,
|
||||||
image_cache: ImageCache,
|
image_cache: ImageCache,
|
||||||
|
native_layer: T,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Router {
|
impl<T: NativeLayerOps> Drop for Router<T> {
|
||||||
pub fn new(data_path: PathBuf, ctx: Context, client: Client, ndb: Ndb) -> Self {
|
fn drop(&mut self) {
|
||||||
|
self.login.save(&mut self.native_layer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: NativeLayerOps> Router<T> {
|
||||||
|
pub fn new(
|
||||||
|
data_path: PathBuf,
|
||||||
|
ctx: Context,
|
||||||
|
client: Client,
|
||||||
|
ndb: Ndb,
|
||||||
|
native_layer: T,
|
||||||
|
) -> Self {
|
||||||
|
let mut login = Login::new();
|
||||||
|
login.load(&native_layer);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
current: Routes::HomePage,
|
current: Routes::HomePage,
|
||||||
current_widget: None,
|
current_widget: None,
|
||||||
@ -61,8 +79,9 @@ impl Router {
|
|||||||
ctx: ctx.clone(),
|
ctx: ctx.clone(),
|
||||||
ndb: NDBWrapper::new(ctx.clone(), ndb.clone(), client.clone()),
|
ndb: NDBWrapper::new(ctx.clone(), ndb.clone(), client.clone()),
|
||||||
client,
|
client,
|
||||||
login: None,
|
login,
|
||||||
image_cache: ImageCache::new(data_path, ctx.clone()),
|
image_cache: ImageCache::new(data_path, ctx.clone()),
|
||||||
|
native_layer,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,9 +110,10 @@ impl Router {
|
|||||||
// handle app state changes
|
// handle app state changes
|
||||||
let q = self.router.read(ui);
|
let q = self.router.read(ui);
|
||||||
for r in q {
|
for r in q {
|
||||||
if let Routes::Action(a) = &r {
|
if let Routes::Action(a) = r {
|
||||||
match a {
|
match a {
|
||||||
RouteAction::LoginPubkey(k) => self.login = Some(*k),
|
RouteAction::ShowKeyboard => self.native_layer.show_keyboard(),
|
||||||
|
RouteAction::HideKeyboard => self.native_layer.hide_keyboard(),
|
||||||
_ => info!("Not implemented"),
|
_ => info!("Not implemented"),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -106,20 +126,21 @@ impl Router {
|
|||||||
self.load_widget(Routes::HomePage, &tx);
|
self.load_widget(Routes::HomePage, &tx);
|
||||||
}
|
}
|
||||||
|
|
||||||
let svc = RouteServices {
|
let mut svc = RouteServices {
|
||||||
context: self.ctx.clone(),
|
context: self.ctx.clone(),
|
||||||
router: self.router.sender(),
|
router: self.router.sender(),
|
||||||
|
client: self.client.clone(),
|
||||||
ndb: &self.ndb,
|
ndb: &self.ndb,
|
||||||
tx: &tx,
|
tx: &tx,
|
||||||
login: &self.login,
|
login: &mut self.login,
|
||||||
img_cache: &self.image_cache,
|
img_cache: &self.image_cache,
|
||||||
};
|
};
|
||||||
|
|
||||||
// display app
|
// display app
|
||||||
ui.vertical(|ui| {
|
ui.vertical(|ui| {
|
||||||
Header::new().render(ui, &svc);
|
Header::new().render(ui, &mut svc);
|
||||||
if let Some(w) = self.current_widget.as_mut() {
|
if let Some(w) = self.current_widget.as_mut() {
|
||||||
w.render(ui, &svc)
|
w.render(ui, &mut svc)
|
||||||
} else {
|
} else {
|
||||||
ui.label("No widget")
|
ui.label("No widget")
|
||||||
}
|
}
|
||||||
@ -131,11 +152,12 @@ impl Router {
|
|||||||
pub struct RouteServices<'a> {
|
pub struct RouteServices<'a> {
|
||||||
pub context: Context, //cloned
|
pub context: Context, //cloned
|
||||||
pub router: UiInboxSender<Routes>, //cloned
|
pub router: UiInboxSender<Routes>, //cloned
|
||||||
|
pub client: Client,
|
||||||
|
|
||||||
pub ndb: &'a NDBWrapper, //ref
|
pub ndb: &'a NDBWrapper, //ref
|
||||||
pub tx: &'a Transaction, //ref
|
pub tx: &'a Transaction, //ref
|
||||||
pub login: &'a Option<[u8; 32]>, //ref
|
pub login: &'a mut Login, //ref
|
||||||
pub img_cache: &'a ImageCache,
|
pub img_cache: &'a ImageCache, //ref
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> RouteServices<'a> {
|
impl<'a> RouteServices<'a> {
|
||||||
@ -150,4 +172,21 @@ impl<'a> RouteServices<'a> {
|
|||||||
warn!("Failed to navigate");
|
warn!("Failed to navigate");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn broadcast_event(&self, event: Event) {
|
||||||
|
let client = self.client.clone();
|
||||||
|
|
||||||
|
let ev_json = event.as_json();
|
||||||
|
if let Err(e) = self.ndb.submit_event(&ev_json) {
|
||||||
|
warn!("Failed to submit event {}", e);
|
||||||
|
}
|
||||||
|
tokio::spawn(async move {
|
||||||
|
match client.send_event(event).await {
|
||||||
|
Ok(e) => {
|
||||||
|
info!("Broadcast event: {:?}", e)
|
||||||
|
}
|
||||||
|
Err(e) => warn!("Failed to broadcast event: {:?}", e),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,18 +23,18 @@ impl StreamPage {
|
|||||||
let f = [f.limit_mut(1)];
|
let f = [f.limit_mut(1)];
|
||||||
let (sub, events) = ndb.subscribe_with_results("streams", &f, tx, 1);
|
let (sub, events) = ndb.subscribe_with_results("streams", &f, tx, 1);
|
||||||
Self {
|
Self {
|
||||||
link,
|
link: link.clone(),
|
||||||
sub,
|
sub,
|
||||||
event: events.first().map(|n| OwnedNote(n.note_key.as_u64())),
|
event: events.first().map(|n| OwnedNote(n.note_key.as_u64())),
|
||||||
chat: None,
|
chat: None,
|
||||||
player: None,
|
player: None,
|
||||||
new_msg: WriteChat::new(),
|
new_msg: WriteChat::new(link),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NostrWidget for StreamPage {
|
impl NostrWidget for StreamPage {
|
||||||
fn render(&mut self, ui: &mut Ui, services: &RouteServices<'_>) -> Response {
|
fn render(&mut self, ui: &mut Ui, services: &mut RouteServices<'_>) -> Response {
|
||||||
let poll = services.ndb.poll(&self.sub, 1);
|
let poll = services.ndb.poll(&self.sub, 1);
|
||||||
if let Some(k) = poll.first() {
|
if let Some(k) = poll.first() {
|
||||||
self.event = Some(OwnedNote(k.as_u64()))
|
self.event = Some(OwnedNote(k.as_u64()))
|
||||||
|
@ -154,4 +154,8 @@ impl NDBWrapper {
|
|||||||
let sub = None;
|
let sub = None;
|
||||||
(p, sub)
|
(p, sub)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn submit_event(&self, ev: &str) -> Result<(), Error> {
|
||||||
|
self.ndb.process_event(ev)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
use egui::Color32;
|
use egui::{Color32, Margin};
|
||||||
|
|
||||||
pub const FONT_SIZE: f32 = 13.0;
|
pub const FONT_SIZE: f32 = 13.0;
|
||||||
|
pub const ROUNDING_DEFAULT: f32 = 12.0;
|
||||||
|
pub const MARGIN_DEFAULT: Margin = Margin::symmetric(12., 6.);
|
||||||
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_800: Color32 = Color32::from_rgb(38, 38, 38);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use crate::theme::NEUTRAL_800;
|
use crate::theme::{NEUTRAL_800, ROUNDING_DEFAULT};
|
||||||
use egui::{Color32, CursorIcon, Frame, Margin, Response, Sense, Ui};
|
use egui::{Color32, CursorIcon, Frame, Margin, Response, Sense, Ui};
|
||||||
|
|
||||||
pub struct Button {
|
pub struct Button {
|
||||||
@ -17,7 +17,7 @@ impl Button {
|
|||||||
let r = Frame::none()
|
let r = Frame::none()
|
||||||
.inner_margin(Margin::symmetric(12., 8.))
|
.inner_margin(Margin::symmetric(12., 8.))
|
||||||
.fill(self.color)
|
.fill(self.color)
|
||||||
.rounding(12.)
|
.rounding(ROUNDING_DEFAULT)
|
||||||
.show(ui, add_contents);
|
.show(ui, add_contents);
|
||||||
|
|
||||||
let id = r.response.id;
|
let id = r.response.id;
|
||||||
|
@ -39,7 +39,7 @@ impl Chat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl NostrWidget for Chat {
|
impl NostrWidget for Chat {
|
||||||
fn render(&mut self, ui: &mut Ui, services: &RouteServices<'_>) -> Response {
|
fn render(&mut self, ui: &mut Ui, services: &mut RouteServices<'_>) -> Response {
|
||||||
let poll = services.ndb.poll(&self.sub, 500);
|
let poll = services.ndb.poll(&self.sub, 500);
|
||||||
poll.iter()
|
poll.iter()
|
||||||
.for_each(|n| self.events.push(OwnedNote(n.as_u64())));
|
.for_each(|n| self.events.push(OwnedNote(n.as_u64())));
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
use crate::login::LoginKind;
|
||||||
use crate::route::{RouteServices, Routes};
|
use crate::route::{RouteServices, Routes};
|
||||||
use crate::widgets::avatar::Avatar;
|
use crate::widgets::avatar::Avatar;
|
||||||
use crate::widgets::{Button, NostrWidget};
|
use crate::widgets::{Button, NostrWidget};
|
||||||
@ -14,7 +15,7 @@ impl Header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl NostrWidget for Header {
|
impl NostrWidget for Header {
|
||||||
fn render(&mut self, ui: &mut Ui, services: &RouteServices<'_>) -> Response {
|
fn render(&mut self, ui: &mut Ui, services: &mut RouteServices<'_>) -> Response {
|
||||||
let logo_bytes = include_bytes!("../resources/logo.svg");
|
let logo_bytes = include_bytes!("../resources/logo.svg");
|
||||||
Frame::none()
|
Frame::none()
|
||||||
.outer_margin(Margin::symmetric(16., 8.))
|
.outer_margin(Margin::symmetric(16., 8.))
|
||||||
@ -37,8 +38,8 @@ impl NostrWidget for Header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||||
if let Some(pk) = services.login {
|
if let Some(pk) = services.login.public_key() {
|
||||||
ui.add(Avatar::pubkey(pk, services));
|
ui.add(Avatar::pubkey(&pk, services));
|
||||||
} else if Button::new().show(ui, |ui| ui.label("Login")).clicked() {
|
} else if Button::new().show(ui, |ui| ui.label("Login")).clicked() {
|
||||||
services.navigate(Routes::LoginPage);
|
services.navigate(Routes::LoginPage);
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ mod stream_list;
|
|||||||
mod stream_player;
|
mod stream_player;
|
||||||
mod stream_tile;
|
mod stream_tile;
|
||||||
mod stream_title;
|
mod stream_title;
|
||||||
|
mod text_input;
|
||||||
mod username;
|
mod username;
|
||||||
mod video_placeholder;
|
mod video_placeholder;
|
||||||
mod write_chat;
|
mod write_chat;
|
||||||
@ -16,7 +17,7 @@ use crate::route::RouteServices;
|
|||||||
use egui::{Response, Ui};
|
use egui::{Response, Ui};
|
||||||
|
|
||||||
pub trait NostrWidget {
|
pub trait NostrWidget {
|
||||||
fn render(&mut self, ui: &mut Ui, services: &RouteServices<'_>) -> Response;
|
fn render(&mut self, ui: &mut Ui, services: &mut RouteServices<'_>) -> Response;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub use self::avatar::Avatar;
|
pub use self::avatar::Avatar;
|
||||||
@ -27,6 +28,7 @@ pub use self::profile::Profile;
|
|||||||
pub use self::stream_list::StreamList;
|
pub use self::stream_list::StreamList;
|
||||||
pub use self::stream_player::StreamPlayer;
|
pub use self::stream_player::StreamPlayer;
|
||||||
pub use self::stream_title::StreamTitle;
|
pub use self::stream_title::StreamTitle;
|
||||||
|
pub use self::text_input::NativeTextInput;
|
||||||
pub use self::username::Username;
|
pub use self::username::Username;
|
||||||
pub use self::video_placeholder::VideoPlaceholder;
|
pub use self::video_placeholder::VideoPlaceholder;
|
||||||
pub use self::write_chat::WriteChat;
|
pub use self::write_chat::WriteChat;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use crate::link::NostrLink;
|
use crate::link::NostrLink;
|
||||||
use crate::route::{RouteServices, Routes};
|
use crate::route::{RouteServices, Routes};
|
||||||
use crate::stream_info::{StreamInfo, StreamStatus};
|
use crate::stream_info::{StreamInfo, StreamStatus};
|
||||||
use crate::theme::{NEUTRAL_800, NEUTRAL_900, PRIMARY};
|
use crate::theme::{NEUTRAL_800, NEUTRAL_900, PRIMARY, ROUNDING_DEFAULT};
|
||||||
use crate::widgets::avatar::Avatar;
|
use crate::widgets::avatar::Avatar;
|
||||||
use eframe::epaint::{Rounding, Vec2};
|
use eframe::epaint::{Rounding, Vec2};
|
||||||
use egui::epaint::RectShape;
|
use egui::epaint::RectShape;
|
||||||
@ -44,7 +44,7 @@ impl Widget for StreamEvent<'_> {
|
|||||||
Ok(TexturePoll::Ready { texture }) => {
|
Ok(TexturePoll::Ready { texture }) => {
|
||||||
painter.add(RectShape {
|
painter.add(RectShape {
|
||||||
rect: response.rect,
|
rect: response.rect,
|
||||||
rounding: Rounding::same(12.),
|
rounding: Rounding::same(ROUNDING_DEFAULT),
|
||||||
fill: Color32::WHITE,
|
fill: Color32::WHITE,
|
||||||
stroke: Default::default(),
|
stroke: Default::default(),
|
||||||
blur_width: 0.0,
|
blur_width: 0.0,
|
||||||
|
@ -16,7 +16,7 @@ impl<'a> StreamTitle<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> NostrWidget for StreamTitle<'a> {
|
impl<'a> NostrWidget for StreamTitle<'a> {
|
||||||
fn render(&mut self, ui: &mut Ui, services: &RouteServices<'_>) -> Response {
|
fn render(&mut self, ui: &mut Ui, services: &mut RouteServices<'_>) -> Response {
|
||||||
Frame::none()
|
Frame::none()
|
||||||
.outer_margin(Margin::symmetric(12., 8.))
|
.outer_margin(Margin::symmetric(12., 8.))
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
|
46
src/widgets/text_input.rs
Normal file
46
src/widgets/text_input.rs
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
use crate::route::{RouteAction, RouteServices};
|
||||||
|
use crate::theme::{MARGIN_DEFAULT, NEUTRAL_500, NEUTRAL_800, ROUNDING_DEFAULT};
|
||||||
|
use crate::widgets::NostrWidget;
|
||||||
|
use egui::{Frame, Response, TextEdit, Ui};
|
||||||
|
|
||||||
|
/// Wrap the [TextEdit] widget to handle native keyboard
|
||||||
|
pub struct NativeTextInput<'a> {
|
||||||
|
pub text: &'a mut String,
|
||||||
|
hint_text: Option<&'a str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> NativeTextInput<'a> {
|
||||||
|
pub fn new(text: &'a mut String) -> Self {
|
||||||
|
Self {
|
||||||
|
text,
|
||||||
|
hint_text: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_hint_text(mut self, hint_text: &'a str) -> Self {
|
||||||
|
self.hint_text = Some(hint_text);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> NostrWidget for NativeTextInput<'a> {
|
||||||
|
fn render(&mut self, ui: &mut Ui, services: &mut RouteServices<'_>) -> Response {
|
||||||
|
let mut editor = TextEdit::singleline(self.text).frame(false);
|
||||||
|
if let Some(hint_text) = self.hint_text {
|
||||||
|
editor = editor.hint_text(egui::RichText::new(hint_text).color(NEUTRAL_500));
|
||||||
|
}
|
||||||
|
let response = Frame::none()
|
||||||
|
.inner_margin(MARGIN_DEFAULT)
|
||||||
|
.fill(NEUTRAL_800)
|
||||||
|
.rounding(ROUNDING_DEFAULT)
|
||||||
|
.show(ui, |ui| ui.add(editor))
|
||||||
|
.inner;
|
||||||
|
if response.lost_focus() {
|
||||||
|
services.action(RouteAction::HideKeyboard);
|
||||||
|
}
|
||||||
|
if response.gained_focus() {
|
||||||
|
services.action(RouteAction::ShowKeyboard);
|
||||||
|
}
|
||||||
|
response
|
||||||
|
}
|
||||||
|
}
|
@ -1,32 +1,36 @@
|
|||||||
use crate::route::RouteServices;
|
use crate::link::NostrLink;
|
||||||
use crate::theme::NEUTRAL_900;
|
use crate::route::{RouteAction, RouteServices};
|
||||||
use crate::widgets::NostrWidget;
|
use crate::theme::{MARGIN_DEFAULT, NEUTRAL_900, ROUNDING_DEFAULT};
|
||||||
|
use crate::widgets::{NativeTextInput, NostrWidget};
|
||||||
use eframe::emath::Align;
|
use eframe::emath::Align;
|
||||||
use egui::{Frame, Image, Layout, Margin, Response, Rounding, Sense, Stroke, TextEdit, Ui, Widget};
|
use egui::{Frame, Image, Layout, Margin, Response, Rounding, Sense, Stroke, TextEdit, Ui, Widget};
|
||||||
use log::info;
|
use log::info;
|
||||||
|
|
||||||
pub struct WriteChat {
|
pub struct WriteChat {
|
||||||
|
link: NostrLink,
|
||||||
msg: String,
|
msg: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WriteChat {
|
impl WriteChat {
|
||||||
pub fn new() -> Self {
|
pub fn new(link: NostrLink) -> Self {
|
||||||
Self { msg: String::new() }
|
Self {
|
||||||
|
link,
|
||||||
|
msg: String::new(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NostrWidget for WriteChat {
|
impl NostrWidget for WriteChat {
|
||||||
fn render(&mut self, ui: &mut Ui, services: &RouteServices<'_>) -> Response {
|
fn render(&mut self, ui: &mut Ui, services: &mut RouteServices<'_>) -> Response {
|
||||||
let size = ui.available_size();
|
|
||||||
let logo_bytes = include_bytes!("../resources/send-03.svg");
|
let logo_bytes = include_bytes!("../resources/send-03.svg");
|
||||||
Frame::none()
|
Frame::none()
|
||||||
.inner_margin(Margin::symmetric(12., 6.))
|
.inner_margin(MARGIN_DEFAULT)
|
||||||
.stroke(Stroke::new(1.0, NEUTRAL_900))
|
.stroke(Stroke::new(1.0, NEUTRAL_900))
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
Frame::none()
|
Frame::none()
|
||||||
.fill(NEUTRAL_900)
|
.fill(NEUTRAL_900)
|
||||||
.rounding(Rounding::same(12.0))
|
.rounding(ROUNDING_DEFAULT)
|
||||||
.inner_margin(Margin::symmetric(12., 10.))
|
.inner_margin(MARGIN_DEFAULT)
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
if services
|
if services
|
||||||
@ -36,12 +40,19 @@ impl NostrWidget for WriteChat {
|
|||||||
.ui(ui)
|
.ui(ui)
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
info!("Sending: {}", self.msg);
|
if let Ok(ev) =
|
||||||
|
services.login.write_live_chat_msg(&self.link, &self.msg)
|
||||||
|
{
|
||||||
|
info!("Sending: {:?}", ev);
|
||||||
|
services.broadcast_event(ev);
|
||||||
|
}
|
||||||
self.msg.clear();
|
self.msg.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
let editor = TextEdit::singleline(&mut self.msg).frame(false);
|
ui.allocate_ui(ui.available_size(), |ui| {
|
||||||
ui.add_sized(ui.available_size(), editor);
|
let mut editor = NativeTextInput::new(&mut self.msg);
|
||||||
|
editor.render(ui, services);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
Loading…
x
Reference in New Issue
Block a user