move Notedeck to notedeck crate
This commit is contained in:
@@ -12,6 +12,7 @@ strum_macros = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
enostr = { workspace = true }
|
||||
egui = { workspace = true }
|
||||
eframe = { workspace = true }
|
||||
image = { workspace = true }
|
||||
base32 = { workspace = true }
|
||||
poll-promise = { workspace = true }
|
||||
@@ -22,6 +23,7 @@ serde = { workspace = true }
|
||||
hex = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
puffin = { workspace = true, optional = true }
|
||||
puffin_egui = { workspace = true, optional = true }
|
||||
sha2 = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
@@ -31,4 +33,4 @@ tempfile = { workspace = true }
|
||||
security-framework = { workspace = true }
|
||||
|
||||
[features]
|
||||
profiling = ["puffin"]
|
||||
profiling = ["puffin", "puffin_egui"]
|
||||
|
||||
@@ -1,5 +1,236 @@
|
||||
use crate::AppContext;
|
||||
use crate::persist::{AppSizeHandler, ZoomHandler};
|
||||
use crate::{
|
||||
Accounts, AppContext, Args, DataPath, DataPathType, Directory, FileKeyStorage, ImageCache,
|
||||
KeyStorageType, NoteCache, ThemeHandler, UnknownIds,
|
||||
};
|
||||
use egui::ThemePreference;
|
||||
use enostr::RelayPool;
|
||||
use nostrdb::{Config, Ndb, Transaction};
|
||||
use std::cell::RefCell;
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
use tracing::{error, info};
|
||||
|
||||
pub trait App {
|
||||
fn update(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui);
|
||||
}
|
||||
|
||||
/// Main notedeck app framework
|
||||
pub struct Notedeck {
|
||||
ndb: Ndb,
|
||||
img_cache: ImageCache,
|
||||
unknown_ids: UnknownIds,
|
||||
pool: RelayPool,
|
||||
note_cache: NoteCache,
|
||||
accounts: Accounts,
|
||||
path: DataPath,
|
||||
args: Args,
|
||||
theme: ThemeHandler,
|
||||
app: Option<Rc<RefCell<dyn App>>>,
|
||||
zoom: ZoomHandler,
|
||||
app_size: AppSizeHandler,
|
||||
}
|
||||
|
||||
fn margin_top(narrow: bool) -> f32 {
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
// FIXME - query the system bar height and adjust more precisely
|
||||
let _ = narrow; // suppress compiler warning on android
|
||||
40.0
|
||||
}
|
||||
#[cfg(not(target_os = "android"))]
|
||||
{
|
||||
if narrow {
|
||||
50.0
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Our chrome, which is basically nothing
|
||||
fn main_panel(style: &egui::Style, narrow: bool) -> egui::CentralPanel {
|
||||
let inner_margin = egui::Margin {
|
||||
top: margin_top(narrow),
|
||||
left: 0.0,
|
||||
right: 0.0,
|
||||
bottom: 0.0,
|
||||
};
|
||||
egui::CentralPanel::default().frame(egui::Frame {
|
||||
inner_margin,
|
||||
fill: style.visuals.panel_fill,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
impl eframe::App for Notedeck {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
#[cfg(feature = "profiling")]
|
||||
puffin::GlobalProfiler::lock().new_frame();
|
||||
|
||||
main_panel(&ctx.style(), crate::ui::is_narrow(ctx)).show(ctx, |ui| {
|
||||
// render app
|
||||
if let Some(app) = &self.app {
|
||||
let app = app.clone();
|
||||
app.borrow_mut().update(&mut self.app_context(), ui);
|
||||
}
|
||||
});
|
||||
|
||||
self.zoom.try_save_zoom_factor(ctx);
|
||||
self.app_size.try_save_app_size(ctx);
|
||||
|
||||
if self.args.relay_debug && self.pool.debug.is_none() {
|
||||
self.pool.use_debug();
|
||||
}
|
||||
|
||||
#[cfg(feature = "profiling")]
|
||||
puffin_egui::profiler_window(ctx);
|
||||
}
|
||||
|
||||
/// Called by the framework to save state before shutdown.
|
||||
fn save(&mut self, _storage: &mut dyn eframe::Storage) {
|
||||
//eframe::set_value(storage, eframe::APP_KEY, self);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "profiling")]
|
||||
fn setup_profiling() {
|
||||
puffin::set_scopes_on(true); // tell puffin to collect data
|
||||
}
|
||||
|
||||
impl Notedeck {
|
||||
pub fn new<P: AsRef<Path>>(ctx: &egui::Context, data_path: P, args: &[String]) -> Self {
|
||||
#[cfg(feature = "profiling")]
|
||||
setup_profiling();
|
||||
|
||||
let parsed_args = Args::parse(args);
|
||||
|
||||
let data_path = parsed_args
|
||||
.datapath
|
||||
.clone()
|
||||
.unwrap_or(data_path.as_ref().to_str().expect("db path ok").to_string());
|
||||
let path = DataPath::new(&data_path);
|
||||
let dbpath_str = parsed_args
|
||||
.dbpath
|
||||
.clone()
|
||||
.unwrap_or_else(|| path.path(DataPathType::Db).to_str().unwrap().to_string());
|
||||
|
||||
let _ = std::fs::create_dir_all(&dbpath_str);
|
||||
|
||||
let img_cache_dir = path.path(DataPathType::Cache).join(ImageCache::rel_dir());
|
||||
let _ = std::fs::create_dir_all(img_cache_dir.clone());
|
||||
|
||||
let map_size = if cfg!(target_os = "windows") {
|
||||
// 16 Gib on windows because it actually creates the file
|
||||
1024usize * 1024usize * 1024usize * 16usize
|
||||
} else {
|
||||
// 1 TiB for everything else since its just virtually mapped
|
||||
1024usize * 1024usize * 1024usize * 1024usize
|
||||
};
|
||||
|
||||
let theme = ThemeHandler::new(&path);
|
||||
let config = Config::new().set_ingester_threads(4).set_mapsize(map_size);
|
||||
|
||||
let keystore = if parsed_args.use_keystore {
|
||||
let keys_path = path.path(DataPathType::Keys);
|
||||
let selected_key_path = path.path(DataPathType::SelectedKey);
|
||||
KeyStorageType::FileSystem(FileKeyStorage::new(
|
||||
Directory::new(keys_path),
|
||||
Directory::new(selected_key_path),
|
||||
))
|
||||
} else {
|
||||
KeyStorageType::None
|
||||
};
|
||||
|
||||
let mut accounts = Accounts::new(keystore, parsed_args.relays.clone());
|
||||
|
||||
let num_keys = parsed_args.keys.len();
|
||||
|
||||
let mut unknown_ids = UnknownIds::default();
|
||||
let ndb = Ndb::new(&dbpath_str, &config).expect("ndb");
|
||||
|
||||
{
|
||||
let txn = Transaction::new(&ndb).expect("txn");
|
||||
for key in &parsed_args.keys {
|
||||
info!("adding account: {}", &key.pubkey);
|
||||
accounts
|
||||
.add_account(key.clone())
|
||||
.process_action(&mut unknown_ids, &ndb, &txn);
|
||||
}
|
||||
}
|
||||
|
||||
if num_keys != 0 {
|
||||
accounts.select_account(0);
|
||||
}
|
||||
|
||||
// AccountManager will setup the pool on first update
|
||||
let mut pool = RelayPool::new();
|
||||
{
|
||||
let ctx = ctx.clone();
|
||||
if let Err(err) = pool.add_multicast_relay(move || ctx.request_repaint()) {
|
||||
error!("error setting up multicast relay: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
let img_cache = ImageCache::new(img_cache_dir);
|
||||
let note_cache = NoteCache::default();
|
||||
let unknown_ids = UnknownIds::default();
|
||||
let zoom = ZoomHandler::new(&path);
|
||||
let app_size = AppSizeHandler::new(&path);
|
||||
|
||||
if let Some(z) = zoom.get_zoom_factor() {
|
||||
ctx.set_zoom_factor(z);
|
||||
}
|
||||
|
||||
// migrate
|
||||
if let Err(e) = img_cache.migrate_v0() {
|
||||
error!("error migrating image cache: {e}");
|
||||
}
|
||||
|
||||
Self {
|
||||
ndb,
|
||||
img_cache,
|
||||
unknown_ids,
|
||||
pool,
|
||||
note_cache,
|
||||
accounts,
|
||||
path: path.clone(),
|
||||
args: parsed_args,
|
||||
theme,
|
||||
app: None,
|
||||
zoom,
|
||||
app_size,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn app<A: App + 'static>(mut self, app: A) -> Self {
|
||||
self.set_app(app);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn app_context(&mut self) -> AppContext<'_> {
|
||||
AppContext {
|
||||
ndb: &mut self.ndb,
|
||||
img_cache: &mut self.img_cache,
|
||||
unknown_ids: &mut self.unknown_ids,
|
||||
pool: &mut self.pool,
|
||||
note_cache: &mut self.note_cache,
|
||||
accounts: &mut self.accounts,
|
||||
path: &self.path,
|
||||
args: &self.args,
|
||||
theme: &mut self.theme,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_app<T: App + 'static>(&mut self, app: T) {
|
||||
self.app = Some(Rc::new(RefCell::new(app)));
|
||||
}
|
||||
|
||||
pub fn args(&self) -> &Args {
|
||||
&self.args
|
||||
}
|
||||
|
||||
pub fn theme(&self) -> ThemePreference {
|
||||
self.theme.load()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,19 +9,20 @@ mod imgcache;
|
||||
mod muted;
|
||||
pub mod note;
|
||||
mod notecache;
|
||||
mod persist;
|
||||
mod result;
|
||||
pub mod storage;
|
||||
mod style;
|
||||
pub mod theme;
|
||||
mod theme_handler;
|
||||
mod time;
|
||||
mod timecache;
|
||||
mod timed_serializer;
|
||||
pub mod ui;
|
||||
mod unknowns;
|
||||
mod user_account;
|
||||
|
||||
pub use accounts::{AccountData, Accounts, AccountsAction, AddAccountAction, SwitchAccountAction};
|
||||
pub use app::App;
|
||||
pub use app::{App, Notedeck};
|
||||
pub use args::Args;
|
||||
pub use context::AppContext;
|
||||
pub use error::{Error, FilterError};
|
||||
@@ -31,13 +32,13 @@ pub use imgcache::ImageCache;
|
||||
pub use muted::{MuteFun, Muted};
|
||||
pub use note::{NoteRef, RootIdError, RootNoteId, RootNoteIdBuf};
|
||||
pub use notecache::{CachedNote, NoteCache};
|
||||
pub use persist::*;
|
||||
pub use result::Result;
|
||||
pub use storage::{
|
||||
DataPath, DataPathType, Directory, FileKeyStorage, KeyStorageResponse, KeyStorageType,
|
||||
};
|
||||
pub use style::NotedeckTextStyle;
|
||||
pub use theme::ColorTheme;
|
||||
pub use theme_handler::ThemeHandler;
|
||||
pub use time::time_ago_since;
|
||||
pub use timecache::TimeCached;
|
||||
pub use unknowns::{get_unknown_note_ids, NoteRefsUnkIdAction, SingleUnkIdAction, UnknownIds};
|
||||
|
||||
30
crates/notedeck/src/persist/app_size.rs
Normal file
30
crates/notedeck/src/persist/app_size.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use egui::Context;
|
||||
|
||||
use crate::timed_serializer::TimedSerializer;
|
||||
use crate::{DataPath, DataPathType};
|
||||
|
||||
pub struct AppSizeHandler {
|
||||
serializer: TimedSerializer<egui::Vec2>,
|
||||
}
|
||||
|
||||
impl AppSizeHandler {
|
||||
pub fn new(path: &DataPath) -> Self {
|
||||
let serializer =
|
||||
TimedSerializer::new(path, DataPathType::Setting, "app_size.json".to_owned())
|
||||
.with_delay(Duration::from_millis(500));
|
||||
|
||||
Self { serializer }
|
||||
}
|
||||
|
||||
pub fn try_save_app_size(&mut self, ctx: &Context) {
|
||||
// There doesn't seem to be a way to check if user is resizing window, so if the rect is different than last saved, we'll wait DELAY before saving again to avoid spamming io
|
||||
let cur_size = ctx.input(|i| i.screen_rect.size());
|
||||
self.serializer.try_save(cur_size);
|
||||
}
|
||||
|
||||
pub fn get_app_size(&self) -> Option<egui::Vec2> {
|
||||
self.serializer.get_item()
|
||||
}
|
||||
}
|
||||
7
crates/notedeck/src/persist/mod.rs
Normal file
7
crates/notedeck/src/persist/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
mod app_size;
|
||||
mod theme_handler;
|
||||
mod zoom;
|
||||
|
||||
pub use app_size::AppSizeHandler;
|
||||
pub use theme_handler::ThemeHandler;
|
||||
pub use zoom::ZoomHandler;
|
||||
26
crates/notedeck/src/persist/zoom.rs
Normal file
26
crates/notedeck/src/persist/zoom.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
use crate::{DataPath, DataPathType};
|
||||
use egui::Context;
|
||||
|
||||
use crate::timed_serializer::TimedSerializer;
|
||||
|
||||
pub struct ZoomHandler {
|
||||
serializer: TimedSerializer<f32>,
|
||||
}
|
||||
|
||||
impl ZoomHandler {
|
||||
pub fn new(path: &DataPath) -> Self {
|
||||
let serializer =
|
||||
TimedSerializer::new(path, DataPathType::Setting, "zoom_level.json".to_owned());
|
||||
|
||||
Self { serializer }
|
||||
}
|
||||
|
||||
pub fn try_save_zoom_factor(&mut self, ctx: &Context) {
|
||||
let cur_zoom_level = ctx.zoom_factor();
|
||||
self.serializer.try_save(cur_zoom_level);
|
||||
}
|
||||
|
||||
pub fn get_zoom_factor(&self) -> Option<f32> {
|
||||
self.serializer.get_item()
|
||||
}
|
||||
}
|
||||
86
crates/notedeck/src/timed_serializer.rs
Normal file
86
crates/notedeck/src/timed_serializer.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::{storage, DataPath, DataPathType, Directory};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::info;
|
||||
|
||||
pub struct TimedSerializer<T: PartialEq + Copy + Serialize + for<'de> Deserialize<'de>> {
|
||||
directory: Directory,
|
||||
file_name: String,
|
||||
delay: Duration,
|
||||
last_saved: Instant,
|
||||
saved_item: Option<T>,
|
||||
}
|
||||
|
||||
impl<T: PartialEq + Copy + Serialize + for<'de> Deserialize<'de>> TimedSerializer<T> {
|
||||
pub fn new(path: &DataPath, path_type: DataPathType, file_name: String) -> Self {
|
||||
let directory = Directory::new(path.path(path_type));
|
||||
let delay = Duration::from_millis(1000);
|
||||
|
||||
Self {
|
||||
directory,
|
||||
file_name,
|
||||
delay,
|
||||
last_saved: Instant::now() - delay,
|
||||
saved_item: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_delay(mut self, delay: Duration) -> Self {
|
||||
self.delay = delay;
|
||||
self
|
||||
}
|
||||
|
||||
fn should_save(&self) -> bool {
|
||||
self.last_saved.elapsed() >= self.delay
|
||||
}
|
||||
|
||||
// returns whether successful
|
||||
pub fn try_save(&mut self, cur_item: T) -> bool {
|
||||
if self.should_save() {
|
||||
if let Some(saved_item) = self.saved_item {
|
||||
if saved_item != cur_item {
|
||||
return self.save(cur_item);
|
||||
}
|
||||
} else {
|
||||
return self.save(cur_item);
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn get_item(&self) -> Option<T> {
|
||||
if self.saved_item.is_some() {
|
||||
return self.saved_item;
|
||||
}
|
||||
|
||||
if let Ok(file_contents) = self.directory.get_file(self.file_name.clone()) {
|
||||
if let Ok(item) = serde_json::from_str::<T>(&file_contents) {
|
||||
return Some(item);
|
||||
}
|
||||
} else {
|
||||
info!("Could not find file {}", self.file_name);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn save(&mut self, cur_item: T) -> bool {
|
||||
if let Ok(serialized_item) = serde_json::to_string(&cur_item) {
|
||||
if storage::write_file(
|
||||
&self.directory.file_path,
|
||||
self.file_name.clone(),
|
||||
&serialized_item,
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
info!("wrote item {}", serialized_item);
|
||||
self.last_saved = Instant::now();
|
||||
self.saved_item = Some(cur_item);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user