Introducing Damus Notedeck: a nostr browser
This splits notedeck into: - notedeck - notedeck_chrome - notedeck_columns The `notedeck` crate is the library that `notedeck_chrome` and `notedeck_columns`, use. It contains common functionality related to notedeck apps such as the NoteCache, ImageCache, etc. The `notedeck_chrome` crate is the binary and ui chrome. It is responsible for managing themes, user accounts, signing, data paths, nostrdb, image caches etc. It will eventually have its own ui which has yet to be determined. For now it just manages the browser data, which is passed to apps via a new struct called `AppContext`. `notedeck_columns` is our columns app, with less responsibility now that more things are handled by `notedeck_chrome` There is still much work left to do before this is a proper browser: - process isolation - sandboxing - etc This is the beginning of a new era! We're just getting started. Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
85
crates/notedeck_chrome/src/app_size.rs
Normal file
85
crates/notedeck_chrome/src/app_size.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use egui::Context;
|
||||
use tracing::info;
|
||||
|
||||
use notedeck::{storage, DataPath, DataPathType, Directory};
|
||||
|
||||
pub struct AppSizeHandler {
|
||||
directory: Directory,
|
||||
saved_size: Option<egui::Vec2>,
|
||||
last_saved: Instant,
|
||||
}
|
||||
|
||||
static FILE_NAME: &str = "app_size.json";
|
||||
static DELAY: Duration = Duration::from_millis(500);
|
||||
|
||||
impl AppSizeHandler {
|
||||
pub fn new(path: &DataPath) -> Self {
|
||||
let directory = Directory::new(path.path(DataPathType::Setting));
|
||||
|
||||
Self {
|
||||
directory,
|
||||
saved_size: None,
|
||||
last_saved: Instant::now() - DELAY,
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
if self.last_saved.elapsed() >= DELAY {
|
||||
internal_try_save_app_size(&self.directory, &mut self.saved_size, ctx);
|
||||
self.last_saved = Instant::now();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_app_size(&self) -> Option<egui::Vec2> {
|
||||
if self.saved_size.is_some() {
|
||||
return self.saved_size;
|
||||
}
|
||||
|
||||
if let Ok(file_contents) = self.directory.get_file(FILE_NAME.to_owned()) {
|
||||
if let Ok(rect) = serde_json::from_str::<egui::Vec2>(&file_contents) {
|
||||
return Some(rect);
|
||||
}
|
||||
} else {
|
||||
info!("Could not find {}", FILE_NAME);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn internal_try_save_app_size(
|
||||
interactor: &Directory,
|
||||
maybe_saved_size: &mut Option<egui::Vec2>,
|
||||
ctx: &Context,
|
||||
) {
|
||||
let cur_size = ctx.input(|i| i.screen_rect.size());
|
||||
if let Some(saved_size) = maybe_saved_size {
|
||||
if cur_size != *saved_size {
|
||||
try_save_size(interactor, cur_size, maybe_saved_size);
|
||||
}
|
||||
} else {
|
||||
try_save_size(interactor, cur_size, maybe_saved_size);
|
||||
}
|
||||
}
|
||||
|
||||
fn try_save_size(
|
||||
interactor: &Directory,
|
||||
cur_size: egui::Vec2,
|
||||
maybe_saved_size: &mut Option<egui::Vec2>,
|
||||
) {
|
||||
if let Ok(serialized_rect) = serde_json::to_string(&cur_size) {
|
||||
if storage::write_file(
|
||||
&interactor.file_path,
|
||||
FILE_NAME.to_owned(),
|
||||
&serialized_rect,
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
info!("wrote size {}", cur_size,);
|
||||
*maybe_saved_size = Some(cur_size);
|
||||
}
|
||||
}
|
||||
}
|
||||
146
crates/notedeck_chrome/src/fonts.rs
Normal file
146
crates/notedeck_chrome/src/fonts.rs
Normal file
@@ -0,0 +1,146 @@
|
||||
use egui::{FontData, FontDefinitions, FontTweak};
|
||||
use std::collections::BTreeMap;
|
||||
use tracing::debug;
|
||||
|
||||
use notedeck::fonts::NamedFontFamily;
|
||||
|
||||
// Use gossip's approach to font loading. This includes japanese fonts
|
||||
// for rending stuff from japanese users.
|
||||
pub fn setup_fonts(ctx: &egui::Context) {
|
||||
let mut font_data: BTreeMap<String, FontData> = BTreeMap::new();
|
||||
let mut families = BTreeMap::new();
|
||||
|
||||
font_data.insert(
|
||||
"Onest".to_owned(),
|
||||
FontData::from_static(include_bytes!(
|
||||
"../../../assets/fonts/onest/OnestRegular1602-hint.ttf"
|
||||
)),
|
||||
);
|
||||
|
||||
font_data.insert(
|
||||
"OnestMedium".to_owned(),
|
||||
FontData::from_static(include_bytes!(
|
||||
"../../../assets/fonts/onest/OnestMedium1602-hint.ttf"
|
||||
)),
|
||||
);
|
||||
|
||||
font_data.insert(
|
||||
"DejaVuSans".to_owned(),
|
||||
FontData::from_static(include_bytes!(
|
||||
"../../../assets/fonts/DejaVuSansSansEmoji.ttf"
|
||||
)),
|
||||
);
|
||||
|
||||
font_data.insert(
|
||||
"OnestBold".to_owned(),
|
||||
FontData::from_static(include_bytes!(
|
||||
"../../../assets/fonts/onest/OnestBold1602-hint.ttf"
|
||||
)),
|
||||
);
|
||||
|
||||
/*
|
||||
font_data.insert(
|
||||
"DejaVuSansBold".to_owned(),
|
||||
FontData::from_static(include_bytes!(
|
||||
"../assets/fonts/DejaVuSans-Bold-SansEmoji.ttf"
|
||||
)),
|
||||
);
|
||||
|
||||
font_data.insert(
|
||||
"DejaVuSans".to_owned(),
|
||||
FontData::from_static(include_bytes!("../assets/fonts/DejaVuSansSansEmoji.ttf")),
|
||||
);
|
||||
font_data.insert(
|
||||
"DejaVuSansBold".to_owned(),
|
||||
FontData::from_static(include_bytes!(
|
||||
"../assets/fonts/DejaVuSans-Bold-SansEmoji.ttf"
|
||||
)),
|
||||
);
|
||||
*/
|
||||
|
||||
font_data.insert(
|
||||
"Inconsolata".to_owned(),
|
||||
FontData::from_static(include_bytes!(
|
||||
"../../../assets/fonts/Inconsolata-Regular.ttf"
|
||||
))
|
||||
.tweak(FontTweak {
|
||||
scale: 1.22, // This font is smaller than DejaVuSans
|
||||
y_offset_factor: -0.18, // and too low
|
||||
y_offset: 0.0,
|
||||
baseline_offset_factor: 0.0,
|
||||
}),
|
||||
);
|
||||
|
||||
font_data.insert(
|
||||
"NotoSansCJK".to_owned(),
|
||||
FontData::from_static(include_bytes!(
|
||||
"../../../assets/fonts/NotoSansCJK-Regular.ttc"
|
||||
)),
|
||||
);
|
||||
|
||||
font_data.insert(
|
||||
"NotoSansThai".to_owned(),
|
||||
FontData::from_static(include_bytes!(
|
||||
"../../../assets/fonts/NotoSansThai-Regular.ttf"
|
||||
)),
|
||||
);
|
||||
|
||||
// Some good looking emojis. Use as first priority:
|
||||
font_data.insert(
|
||||
"NotoEmoji".to_owned(),
|
||||
FontData::from_static(include_bytes!(
|
||||
"../../../assets/fonts/NotoEmoji-Regular.ttf"
|
||||
))
|
||||
.tweak(FontTweak {
|
||||
scale: 1.1, // make them a touch larger
|
||||
y_offset_factor: 0.0,
|
||||
y_offset: 0.0,
|
||||
baseline_offset_factor: 0.0,
|
||||
}),
|
||||
);
|
||||
|
||||
let base_fonts = vec![
|
||||
"DejaVuSans".to_owned(),
|
||||
"NotoEmoji".to_owned(),
|
||||
"NotoSansCJK".to_owned(),
|
||||
"NotoSansThai".to_owned(),
|
||||
];
|
||||
|
||||
let mut proportional = vec!["Onest".to_owned()];
|
||||
proportional.extend(base_fonts.clone());
|
||||
|
||||
let mut medium = vec!["OnestMedium".to_owned()];
|
||||
medium.extend(base_fonts.clone());
|
||||
|
||||
let mut mono = vec!["Inconsolata".to_owned()];
|
||||
mono.extend(base_fonts.clone());
|
||||
|
||||
let mut bold = vec!["OnestBold".to_owned()];
|
||||
bold.extend(base_fonts.clone());
|
||||
|
||||
let emoji = vec!["NotoEmoji".to_owned()];
|
||||
|
||||
families.insert(egui::FontFamily::Proportional, proportional);
|
||||
families.insert(egui::FontFamily::Monospace, mono);
|
||||
families.insert(
|
||||
egui::FontFamily::Name(NamedFontFamily::Medium.as_str().into()),
|
||||
medium,
|
||||
);
|
||||
families.insert(
|
||||
egui::FontFamily::Name(NamedFontFamily::Bold.as_str().into()),
|
||||
bold,
|
||||
);
|
||||
families.insert(
|
||||
egui::FontFamily::Name(NamedFontFamily::Emoji.as_str().into()),
|
||||
emoji,
|
||||
);
|
||||
|
||||
debug!("fonts: {:?}", families);
|
||||
|
||||
let defs = FontDefinitions {
|
||||
font_data,
|
||||
families,
|
||||
};
|
||||
|
||||
ctx.set_fonts(defs);
|
||||
}
|
||||
4
crates/notedeck_chrome/src/lib.rs
Normal file
4
crates/notedeck_chrome/src/lib.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod app_size;
|
||||
pub mod fonts;
|
||||
pub mod setup;
|
||||
pub mod theme;
|
||||
392
crates/notedeck_chrome/src/notedeck.rs
Normal file
392
crates/notedeck_chrome/src/notedeck.rs
Normal file
@@ -0,0 +1,392 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
|
||||
use notedeck_chrome::{
|
||||
app_size::AppSizeHandler,
|
||||
setup::{generate_native_options, setup_cc},
|
||||
theme,
|
||||
};
|
||||
|
||||
use notedeck_columns::Damus;
|
||||
|
||||
use notedeck::{
|
||||
Accounts, AppContext, Args, DataPath, DataPathType, Directory, FileKeyStorage, ImageCache,
|
||||
KeyStorageType, NoteCache, ThemeHandler, UnknownIds,
|
||||
};
|
||||
|
||||
use enostr::RelayPool;
|
||||
use nostrdb::{Config, Ndb, Transaction};
|
||||
use std::cell::RefCell;
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
use std::{path::PathBuf, str::FromStr};
|
||||
use tracing::info;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
/// Our browser app state
|
||||
struct Notedeck {
|
||||
ndb: Ndb,
|
||||
img_cache: ImageCache,
|
||||
unknown_ids: UnknownIds,
|
||||
pool: RelayPool,
|
||||
note_cache: NoteCache,
|
||||
accounts: Accounts,
|
||||
path: DataPath,
|
||||
args: Args,
|
||||
theme: ThemeHandler,
|
||||
tabs: Tabs,
|
||||
app_rect_handler: AppSizeHandler,
|
||||
egui: egui::Context,
|
||||
}
|
||||
|
||||
struct Tabs {
|
||||
app: Option<Rc<RefCell<dyn notedeck::App>>>,
|
||||
}
|
||||
|
||||
impl Tabs {
|
||||
pub fn new(app: Option<Rc<RefCell<dyn notedeck::App>>>) -> Self {
|
||||
Self { app }
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for Notedeck {
|
||||
/// Called by the frame work to save state before shutdown.
|
||||
fn save(&mut self, _storage: &mut dyn eframe::Storage) {
|
||||
//eframe::set_value(storage, eframe::APP_KEY, self);
|
||||
}
|
||||
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
// TODO: render chrome
|
||||
|
||||
// render app
|
||||
if let Some(app) = &self.tabs.app {
|
||||
let app = app.clone();
|
||||
app.borrow_mut().update(&mut self.app_context());
|
||||
}
|
||||
|
||||
self.app_rect_handler.try_save_app_size(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
impl Notedeck {
|
||||
pub fn new<P: AsRef<Path>>(ctx: &egui::Context, data_path: P, args: &[String]) -> Self {
|
||||
let parsed_args = Args::parse(args);
|
||||
let is_mobile = parsed_args
|
||||
.is_mobile
|
||||
.unwrap_or(notedeck::ui::is_compiled_as_mobile());
|
||||
|
||||
// Some people have been running notedeck in debug, let's catch that!
|
||||
if !cfg!(test) && cfg!(debug_assertions) && !parsed_args.debug {
|
||||
println!("--- WELCOME TO DAMUS NOTEDECK! ---");
|
||||
println!("It looks like are running notedeck in debug mode, unless you are a developer, this is not likely what you want.");
|
||||
println!("If you are a developer, run `cargo run -- --debug` to skip this message.");
|
||||
println!("For everyone else, try again with `cargo run --release`. Enjoy!");
|
||||
println!("---------------------------------");
|
||||
panic!();
|
||||
}
|
||||
|
||||
setup_cc(ctx, is_mobile, parsed_args.light);
|
||||
|
||||
let data_path = parsed_args
|
||||
.datapath
|
||||
.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
|
||||
.unwrap_or_else(|| path.path(DataPathType::Db).to_str().unwrap().to_string());
|
||||
|
||||
let _ = std::fs::create_dir_all(&dbpath_str);
|
||||
|
||||
let imgcache_dir = path.path(DataPathType::Cache).join(ImageCache::rel_dir());
|
||||
let _ = std::fs::create_dir_all(imgcache_dir.clone());
|
||||
|
||||
let mapsize = 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);
|
||||
ctx.options_mut(|o| {
|
||||
let cur_theme = theme.load();
|
||||
info!("Loaded theme {:?} from disk", cur_theme);
|
||||
o.theme_preference = cur_theme;
|
||||
});
|
||||
ctx.set_visuals_of(
|
||||
egui::Theme::Dark,
|
||||
theme::dark_mode(notedeck::ui::is_compiled_as_mobile()),
|
||||
);
|
||||
ctx.set_visuals_of(egui::Theme::Light, theme::light_mode());
|
||||
|
||||
let config = Config::new().set_ingester_threads(4).set_mapsize(mapsize);
|
||||
|
||||
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);
|
||||
|
||||
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)
|
||||
.process_action(&mut unknown_ids, &ndb, &txn);
|
||||
}
|
||||
}
|
||||
|
||||
if num_keys != 0 {
|
||||
accounts.select_account(0);
|
||||
}
|
||||
|
||||
// AccountManager will setup the pool on first update
|
||||
let pool = RelayPool::new();
|
||||
|
||||
let img_cache = ImageCache::new(imgcache_dir);
|
||||
let note_cache = NoteCache::default();
|
||||
let unknown_ids = UnknownIds::default();
|
||||
let egui = ctx.clone();
|
||||
let tabs = Tabs::new(None);
|
||||
let parsed_args = Args::parse(args);
|
||||
let app_rect_handler = AppSizeHandler::new(&path);
|
||||
|
||||
Self {
|
||||
ndb,
|
||||
img_cache,
|
||||
app_rect_handler,
|
||||
unknown_ids,
|
||||
pool,
|
||||
note_cache,
|
||||
accounts,
|
||||
path: path.clone(),
|
||||
args: parsed_args,
|
||||
theme,
|
||||
egui,
|
||||
tabs,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn app_context(&mut self) -> AppContext<'_> {
|
||||
AppContext {
|
||||
ndb: &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,
|
||||
egui: &self.egui,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_app<T: notedeck::App + 'static>(&mut self, app: T) {
|
||||
self.tabs.app = Some(Rc::new(RefCell::new(app)));
|
||||
}
|
||||
}
|
||||
|
||||
// Entry point for wasm
|
||||
//#[cfg(target_arch = "wasm32")]
|
||||
//use wasm_bindgen::prelude::*;
|
||||
|
||||
fn setup_logging(path: &DataPath) {
|
||||
#[allow(unused_variables)] // need guard to live for lifetime of program
|
||||
let (maybe_non_blocking, maybe_guard) = {
|
||||
let log_path = path.path(DataPathType::Log);
|
||||
// Setup logging to file
|
||||
|
||||
use tracing_appender::{
|
||||
non_blocking,
|
||||
rolling::{RollingFileAppender, Rotation},
|
||||
};
|
||||
|
||||
let file_appender = RollingFileAppender::new(
|
||||
Rotation::DAILY,
|
||||
log_path,
|
||||
format!("notedeck-{}.log", env!("CARGO_PKG_VERSION")),
|
||||
);
|
||||
|
||||
let (non_blocking, _guard) = non_blocking(file_appender);
|
||||
|
||||
(Some(non_blocking), Some(_guard))
|
||||
};
|
||||
|
||||
// Log to stdout (if you run with `RUST_LOG=debug`).
|
||||
if let Some(non_blocking_writer) = maybe_non_blocking {
|
||||
use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt};
|
||||
|
||||
let console_layer = fmt::layer().with_target(true).with_writer(std::io::stdout);
|
||||
|
||||
// Create the file layer (writes to the file)
|
||||
let file_layer = fmt::layer()
|
||||
.with_ansi(false)
|
||||
.with_writer(non_blocking_writer);
|
||||
|
||||
let env_filter =
|
||||
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("notedeck=info"));
|
||||
|
||||
// Set up the subscriber to combine both layers
|
||||
tracing_subscriber::registry()
|
||||
.with(console_layer)
|
||||
.with(file_layer)
|
||||
.with(env_filter)
|
||||
.init();
|
||||
} else {
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(EnvFilter::from_default_env())
|
||||
.init();
|
||||
}
|
||||
}
|
||||
|
||||
// Desktop
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let base_path = DataPath::default_base().unwrap_or(PathBuf::from_str(".").unwrap());
|
||||
let path = DataPath::new(&base_path);
|
||||
|
||||
setup_logging(&path);
|
||||
|
||||
let _res = eframe::run_native(
|
||||
"Damus Notedeck",
|
||||
generate_native_options(path),
|
||||
Box::new(|cc| {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let mut notedeck = Notedeck::new(&cc.egui_ctx, base_path, &args);
|
||||
|
||||
let damus = Damus::new(&mut notedeck.app_context(), &args);
|
||||
notedeck.add_app(damus);
|
||||
|
||||
Ok(Box::new(notedeck))
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* TODO: nostrdb not supported on web
|
||||
*
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn main() {
|
||||
// Make sure panics are logged using `console.error`.
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
// Redirect tracing to console.log and friends:
|
||||
tracing_wasm::set_as_global_default();
|
||||
|
||||
wasm_bindgen_futures::spawn_local(async {
|
||||
let web_options = eframe::WebOptions::default();
|
||||
eframe::start_web(
|
||||
"the_canvas_id", // hardcode it
|
||||
web_options,
|
||||
Box::new(|cc| Box::new(Damus::new(cc, "."))),
|
||||
)
|
||||
.await
|
||||
.expect("failed to start eframe");
|
||||
});
|
||||
}
|
||||
*/
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{Damus, Notedeck};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
fn create_tmp_dir() -> PathBuf {
|
||||
tempfile::TempDir::new()
|
||||
.expect("tmp path")
|
||||
.path()
|
||||
.to_path_buf()
|
||||
}
|
||||
|
||||
fn rmrf(path: impl AsRef<Path>) {
|
||||
let _ = std::fs::remove_dir_all(path);
|
||||
}
|
||||
|
||||
/// Ensure dbpath actually sets the dbpath correctly.
|
||||
#[tokio::test]
|
||||
async fn test_dbpath() {
|
||||
let datapath = create_tmp_dir();
|
||||
let dbpath = create_tmp_dir();
|
||||
let args: Vec<String> = vec![
|
||||
"--datapath",
|
||||
&datapath.to_str().unwrap(),
|
||||
"--dbpath",
|
||||
&dbpath.to_str().unwrap(),
|
||||
]
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect();
|
||||
|
||||
let ctx = egui::Context::default();
|
||||
let _app = Notedeck::new(&ctx, &datapath, &args);
|
||||
|
||||
assert!(Path::new(&dbpath.join("data.mdb")).exists());
|
||||
assert!(Path::new(&dbpath.join("lock.mdb")).exists());
|
||||
assert!(!Path::new(&datapath.join("db")).exists());
|
||||
|
||||
rmrf(datapath);
|
||||
rmrf(dbpath);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_column_args() {
|
||||
let tmpdir = create_tmp_dir();
|
||||
let npub = "npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s";
|
||||
let args: Vec<String> = vec![
|
||||
"--no-keystore",
|
||||
"--pub",
|
||||
npub,
|
||||
"-c",
|
||||
"notifications",
|
||||
"-c",
|
||||
"contacts",
|
||||
]
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect();
|
||||
|
||||
let ctx = egui::Context::default();
|
||||
let mut notedeck = Notedeck::new(&ctx, &tmpdir, &args);
|
||||
let mut app_ctx = notedeck.app_context();
|
||||
let app = Damus::new(&mut app_ctx, &args);
|
||||
|
||||
assert_eq!(app.columns(app_ctx.accounts).columns().len(), 2);
|
||||
|
||||
let tl1 = app
|
||||
.columns(app_ctx.accounts)
|
||||
.column(0)
|
||||
.router()
|
||||
.top()
|
||||
.timeline_id();
|
||||
|
||||
let tl2 = app
|
||||
.columns(app_ctx.accounts)
|
||||
.column(1)
|
||||
.router()
|
||||
.top()
|
||||
.timeline_id();
|
||||
|
||||
assert_eq!(tl1.is_some(), true);
|
||||
assert_eq!(tl2.is_some(), true);
|
||||
|
||||
let timelines = app.columns(app_ctx.accounts).timelines();
|
||||
assert!(timelines[0].kind.is_notifications());
|
||||
assert!(timelines[1].kind.is_contacts());
|
||||
|
||||
rmrf(tmpdir);
|
||||
}
|
||||
}
|
||||
110
crates/notedeck_chrome/src/preview.rs
Normal file
110
crates/notedeck_chrome/src/preview.rs
Normal file
@@ -0,0 +1,110 @@
|
||||
use notedeck::DataPath;
|
||||
use notedeck_chrome::setup::{
|
||||
generate_mobile_emulator_native_options, generate_native_options, setup_cc,
|
||||
};
|
||||
use notedeck_columns::ui::configure_deck::ConfigureDeckView;
|
||||
use notedeck_columns::ui::edit_deck::EditDeckView;
|
||||
use notedeck_columns::ui::{
|
||||
account_login_view::AccountLoginView, PostView, Preview, PreviewApp, PreviewConfig, ProfilePic,
|
||||
ProfilePreview, RelayView,
|
||||
};
|
||||
use std::env;
|
||||
|
||||
struct PreviewRunner {
|
||||
force_mobile: bool,
|
||||
light_mode: bool,
|
||||
}
|
||||
|
||||
impl PreviewRunner {
|
||||
fn new(force_mobile: bool, light_mode: bool) -> Self {
|
||||
PreviewRunner {
|
||||
force_mobile,
|
||||
light_mode,
|
||||
}
|
||||
}
|
||||
|
||||
async fn run<P>(self, preview: P)
|
||||
where
|
||||
P: Into<PreviewApp> + 'static,
|
||||
{
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let native_options = if self.force_mobile {
|
||||
generate_mobile_emulator_native_options()
|
||||
} else {
|
||||
// TODO: tmp preview pathbuf?
|
||||
generate_native_options(DataPath::new("previews"))
|
||||
};
|
||||
|
||||
let is_mobile = self.force_mobile;
|
||||
let light_mode = self.light_mode;
|
||||
|
||||
let _ = eframe::run_native(
|
||||
"UI Preview Runner",
|
||||
native_options,
|
||||
Box::new(move |cc| {
|
||||
let app = Into::<PreviewApp>::into(preview);
|
||||
setup_cc(&cc.egui_ctx, is_mobile, light_mode);
|
||||
Ok(Box::new(app))
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! previews {
|
||||
// Accept a runner and name variable, followed by one or more identifiers for the views
|
||||
($runner:expr, $name:expr, $is_mobile:expr, $($view:ident),* $(,)?) => {
|
||||
match $name.as_ref() {
|
||||
$(
|
||||
stringify!($view) => {
|
||||
$runner.run($view::preview(PreviewConfig { is_mobile: $is_mobile })).await;
|
||||
}
|
||||
)*
|
||||
_ => println!("Component not found."),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let mut name: Option<String> = None;
|
||||
let mut is_mobile: Option<bool> = None;
|
||||
let mut light_mode: bool = false;
|
||||
|
||||
for arg in env::args() {
|
||||
if arg == "--mobile" {
|
||||
is_mobile = Some(true);
|
||||
} else if arg == "--light" {
|
||||
light_mode = true;
|
||||
} else {
|
||||
name = Some(arg);
|
||||
}
|
||||
}
|
||||
|
||||
let name = if let Some(name) = name {
|
||||
name
|
||||
} else {
|
||||
println!("Please specify a component to test");
|
||||
return;
|
||||
};
|
||||
|
||||
println!(
|
||||
"light mode previews: {}",
|
||||
if light_mode { "enabled" } else { "disabled" }
|
||||
);
|
||||
let is_mobile = is_mobile.unwrap_or(notedeck::ui::is_compiled_as_mobile());
|
||||
let runner = PreviewRunner::new(is_mobile, light_mode);
|
||||
|
||||
previews!(
|
||||
runner,
|
||||
name,
|
||||
is_mobile,
|
||||
RelayView,
|
||||
AccountLoginView,
|
||||
ProfilePreview,
|
||||
ProfilePic,
|
||||
PostView,
|
||||
ConfigureDeckView,
|
||||
EditDeckView,
|
||||
);
|
||||
}
|
||||
79
crates/notedeck_chrome/src/setup.rs
Normal file
79
crates/notedeck_chrome/src/setup.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
use crate::{app_size::AppSizeHandler, fonts, theme};
|
||||
|
||||
use eframe::NativeOptions;
|
||||
use notedeck::DataPath;
|
||||
|
||||
pub fn setup_cc(ctx: &egui::Context, is_mobile: bool, light: bool) {
|
||||
fonts::setup_fonts(ctx);
|
||||
|
||||
//ctx.set_pixels_per_point(ctx.pixels_per_point() + UI_SCALE_FACTOR);
|
||||
//ctx.set_pixels_per_point(1.0);
|
||||
//
|
||||
//
|
||||
//ctx.tessellation_options_mut(|to| to.feathering = false);
|
||||
|
||||
egui_extras::install_image_loaders(ctx);
|
||||
|
||||
if light {
|
||||
ctx.set_visuals(theme::light_mode())
|
||||
} else {
|
||||
ctx.set_visuals(theme::dark_mode(is_mobile));
|
||||
}
|
||||
|
||||
ctx.all_styles_mut(|style| theme::add_custom_style(is_mobile, style));
|
||||
}
|
||||
|
||||
//pub const UI_SCALE_FACTOR: f32 = 0.2;
|
||||
|
||||
pub fn generate_native_options(paths: DataPath) -> NativeOptions {
|
||||
let window_builder = Box::new(move |builder: egui::ViewportBuilder| {
|
||||
let builder = builder
|
||||
.with_fullsize_content_view(true)
|
||||
.with_titlebar_shown(false)
|
||||
.with_title_shown(false)
|
||||
.with_icon(std::sync::Arc::new(
|
||||
eframe::icon_data::from_png_bytes(app_icon()).expect("icon"),
|
||||
));
|
||||
|
||||
if let Some(window_size) = AppSizeHandler::new(&paths).get_app_size() {
|
||||
builder.with_inner_size(window_size)
|
||||
} else {
|
||||
builder
|
||||
}
|
||||
});
|
||||
|
||||
eframe::NativeOptions {
|
||||
window_builder: Some(window_builder),
|
||||
viewport: egui::ViewportBuilder::default().with_icon(std::sync::Arc::new(
|
||||
eframe::icon_data::from_png_bytes(app_icon()).expect("icon"),
|
||||
)),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_native_options_with_builder_modifiers(
|
||||
apply_builder_modifiers: fn(egui::ViewportBuilder) -> egui::ViewportBuilder,
|
||||
) -> NativeOptions {
|
||||
let window_builder =
|
||||
Box::new(move |builder: egui::ViewportBuilder| apply_builder_modifiers(builder));
|
||||
|
||||
eframe::NativeOptions {
|
||||
window_builder: Some(window_builder),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn app_icon() -> &'static [u8; 271986] {
|
||||
std::include_bytes!("../../../assets/damus-app-icon.png")
|
||||
}
|
||||
|
||||
pub fn generate_mobile_emulator_native_options() -> eframe::NativeOptions {
|
||||
generate_native_options_with_builder_modifiers(|builder| {
|
||||
builder
|
||||
.with_fullsize_content_view(true)
|
||||
.with_titlebar_shown(false)
|
||||
.with_title_shown(false)
|
||||
.with_inner_size([405.0, 915.0])
|
||||
.with_icon(eframe::icon_data::from_png_bytes(app_icon()).expect("icon"))
|
||||
})
|
||||
}
|
||||
132
crates/notedeck_chrome/src/theme.rs
Normal file
132
crates/notedeck_chrome/src/theme.rs
Normal file
@@ -0,0 +1,132 @@
|
||||
use egui::{style::Interaction, Color32, FontId, Style, Visuals};
|
||||
use notedeck::{ColorTheme, NotedeckTextStyle};
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
pub const PURPLE: Color32 = Color32::from_rgb(0xCC, 0x43, 0xC5);
|
||||
const PURPLE_ALT: Color32 = Color32::from_rgb(0x82, 0x56, 0xDD);
|
||||
//pub const DARK_BG: Color32 = egui::Color32::from_rgb(40, 44, 52);
|
||||
pub const GRAY_SECONDARY: Color32 = Color32::from_rgb(0x8A, 0x8A, 0x8A);
|
||||
const BLACK: Color32 = Color32::from_rgb(0x00, 0x00, 0x00);
|
||||
const RED_700: Color32 = Color32::from_rgb(0xC7, 0x37, 0x5A);
|
||||
const ORANGE_700: Color32 = Color32::from_rgb(0xF6, 0xB1, 0x4A);
|
||||
|
||||
// BACKGROUNDS
|
||||
const SEMI_DARKER_BG: Color32 = Color32::from_rgb(0x39, 0x39, 0x39);
|
||||
const DARKER_BG: Color32 = Color32::from_rgb(0x1F, 0x1F, 0x1F);
|
||||
const DARK_BG: Color32 = Color32::from_rgb(0x2C, 0x2C, 0x2C);
|
||||
const DARK_ISH_BG: Color32 = Color32::from_rgb(0x25, 0x25, 0x25);
|
||||
const SEMI_DARK_BG: Color32 = Color32::from_rgb(0x44, 0x44, 0x44);
|
||||
|
||||
const LIGHTER_GRAY: Color32 = Color32::from_rgb(0xf8, 0xf8, 0xf8);
|
||||
const LIGHT_GRAY: Color32 = Color32::from_rgb(0xc8, 0xc8, 0xc8); // 78%
|
||||
const DARKER_GRAY: Color32 = Color32::from_rgb(0xa5, 0xa5, 0xa5); // 65%
|
||||
const EVEN_DARKER_GRAY: Color32 = Color32::from_rgb(0x89, 0x89, 0x89); // 54%
|
||||
|
||||
pub fn desktop_dark_color_theme() -> ColorTheme {
|
||||
ColorTheme {
|
||||
// VISUALS
|
||||
panel_fill: DARKER_BG,
|
||||
extreme_bg_color: DARK_ISH_BG,
|
||||
text_color: Color32::WHITE,
|
||||
err_fg_color: RED_700,
|
||||
warn_fg_color: ORANGE_700,
|
||||
hyperlink_color: PURPLE,
|
||||
selection_color: PURPLE_ALT,
|
||||
|
||||
// WINDOW
|
||||
window_fill: DARK_ISH_BG,
|
||||
window_stroke_color: DARK_BG,
|
||||
|
||||
// NONINTERACTIVE WIDGET
|
||||
noninteractive_bg_fill: DARK_ISH_BG,
|
||||
noninteractive_weak_bg_fill: DARK_BG,
|
||||
noninteractive_bg_stroke_color: SEMI_DARKER_BG,
|
||||
noninteractive_fg_stroke_color: GRAY_SECONDARY,
|
||||
|
||||
// INACTIVE WIDGET
|
||||
inactive_bg_stroke_color: SEMI_DARKER_BG,
|
||||
inactive_bg_fill: Color32::from_rgb(0x25, 0x25, 0x25),
|
||||
inactive_weak_bg_fill: SEMI_DARK_BG,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mobile_dark_color_theme() -> ColorTheme {
|
||||
ColorTheme {
|
||||
panel_fill: Color32::BLACK,
|
||||
noninteractive_weak_bg_fill: Color32::from_rgb(0x1F, 0x1F, 0x1F),
|
||||
..desktop_dark_color_theme()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn light_color_theme() -> ColorTheme {
|
||||
ColorTheme {
|
||||
// VISUALS
|
||||
panel_fill: Color32::WHITE,
|
||||
extreme_bg_color: LIGHTER_GRAY,
|
||||
text_color: BLACK,
|
||||
err_fg_color: RED_700,
|
||||
warn_fg_color: ORANGE_700,
|
||||
hyperlink_color: PURPLE,
|
||||
selection_color: PURPLE_ALT,
|
||||
|
||||
// WINDOW
|
||||
window_fill: Color32::WHITE,
|
||||
window_stroke_color: DARKER_GRAY,
|
||||
|
||||
// NONINTERACTIVE WIDGET
|
||||
noninteractive_bg_fill: Color32::WHITE,
|
||||
noninteractive_weak_bg_fill: LIGHTER_GRAY,
|
||||
noninteractive_bg_stroke_color: LIGHT_GRAY,
|
||||
noninteractive_fg_stroke_color: GRAY_SECONDARY,
|
||||
|
||||
// INACTIVE WIDGET
|
||||
inactive_bg_stroke_color: EVEN_DARKER_GRAY,
|
||||
inactive_bg_fill: LIGHT_GRAY,
|
||||
inactive_weak_bg_fill: EVEN_DARKER_GRAY,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn light_mode() -> Visuals {
|
||||
notedeck::theme::create_themed_visuals(light_color_theme(), Visuals::light())
|
||||
}
|
||||
|
||||
pub fn dark_mode(mobile: bool) -> Visuals {
|
||||
notedeck::theme::create_themed_visuals(
|
||||
if mobile {
|
||||
mobile_dark_color_theme()
|
||||
} else {
|
||||
desktop_dark_color_theme()
|
||||
},
|
||||
Visuals::dark(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Create custom text sizes for any FontSizes
|
||||
pub fn add_custom_style(is_mobile: bool, style: &mut Style) {
|
||||
let font_size = if is_mobile {
|
||||
notedeck::fonts::mobile_font_size
|
||||
} else {
|
||||
notedeck::fonts::desktop_font_size
|
||||
};
|
||||
|
||||
style.text_styles = NotedeckTextStyle::iter()
|
||||
.map(|text_style| {
|
||||
(
|
||||
text_style.text_style(),
|
||||
FontId::new(font_size(&text_style), text_style.font_family()),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
style.interaction = Interaction {
|
||||
tooltip_delay: 0.1,
|
||||
show_tooltips_only_when_still: false,
|
||||
..Interaction::default()
|
||||
};
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
style.debug.show_interactive_widgets = true;
|
||||
style.debug.debug_on_hover_with_all_modifiers = true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user