mirror of
https://github.com/damus-io/notedeck.git
synced 2024-09-16 20:03:28 +00:00
profile picture image cache
coding from a plane so this is helping alot with PFPs
This commit is contained in:
parent
1c16ddf9af
commit
87f385b683
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,6 +2,7 @@
|
|||||||
.buildcmd
|
.buildcmd
|
||||||
target
|
target
|
||||||
.git
|
.git
|
||||||
|
cache
|
||||||
/dist
|
/dist
|
||||||
.direnv/
|
.direnv/
|
||||||
src/camera.rs
|
src/camera.rs
|
||||||
|
3
Makefile
3
Makefile
@ -1,4 +1,7 @@
|
|||||||
|
|
||||||
|
all:
|
||||||
|
cargo check
|
||||||
|
|
||||||
tags: fake
|
tags: fake
|
||||||
find . -type d -name target -prune -o -type f -name '*.rs' -print | xargs ctags
|
find . -type d -name target -prune -o -type f -name '*.rs' -print | xargs ctags
|
||||||
|
|
||||||
|
78
src/app.rs
78
src/app.rs
@ -3,16 +3,18 @@ use crate::error::Error;
|
|||||||
use crate::fonts::{setup_fonts, NamedFontFamily};
|
use crate::fonts::{setup_fonts, NamedFontFamily};
|
||||||
use crate::frame_history::FrameHistory;
|
use crate::frame_history::FrameHistory;
|
||||||
use crate::images::fetch_img;
|
use crate::images::fetch_img;
|
||||||
|
use crate::imgcache::ImageCache;
|
||||||
use crate::notecache::NoteCache;
|
use crate::notecache::NoteCache;
|
||||||
use crate::timeline;
|
use crate::timeline;
|
||||||
use crate::ui::padding;
|
use crate::ui::padding;
|
||||||
use crate::Result;
|
use crate::Result;
|
||||||
use egui::containers::scroll_area::ScrollBarVisibility;
|
use egui::containers::scroll_area::ScrollBarVisibility;
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use egui::widgets::Spinner;
|
use egui::widgets::Spinner;
|
||||||
use egui::{
|
use egui::{
|
||||||
Color32, Context, Frame, Hyperlink, Image, Label, Margin, RichText, Style, TextureHandle,
|
Color32, Context, Frame, Hyperlink, Image, Label, Margin, RichText, Sense, Style,
|
||||||
Visuals,
|
TextureHandle, Vec2, Visuals,
|
||||||
};
|
};
|
||||||
|
|
||||||
use enostr::{ClientMessage, Filter, Pubkey, RelayEvent, RelayMessage};
|
use enostr::{ClientMessage, Filter, Pubkey, RelayEvent, RelayMessage};
|
||||||
@ -33,22 +35,6 @@ use enostr::RelayPool;
|
|||||||
const PURPLE: Color32 = Color32::from_rgb(0xCC, 0x43, 0xC5);
|
const PURPLE: Color32 = Color32::from_rgb(0xCC, 0x43, 0xC5);
|
||||||
const DARK_BG: Color32 = egui::Color32::from_rgb(40, 44, 52);
|
const DARK_BG: Color32 = egui::Color32::from_rgb(40, 44, 52);
|
||||||
|
|
||||||
#[derive(Hash, Eq, PartialEq, Clone, Debug)]
|
|
||||||
enum UrlKey<'a> {
|
|
||||||
Orig(&'a str),
|
|
||||||
Failed(&'a str),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UrlKey<'_> {
|
|
||||||
fn to_u64(&self) -> u64 {
|
|
||||||
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
|
||||||
self.hash(&mut hasher);
|
|
||||||
hasher.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type ImageCache = HashMap<u64, Promise<Result<TextureHandle>>>;
|
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq, Clone)]
|
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||||
pub enum DamusState {
|
pub enum DamusState {
|
||||||
Initializing,
|
Initializing,
|
||||||
@ -454,12 +440,15 @@ impl Damus {
|
|||||||
vec![get_home_filter()]
|
vec![get_home_filter()]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let imgcache_dir = data_path.as_ref().join("cache/img");
|
||||||
|
std::fs::create_dir_all(imgcache_dir.clone());
|
||||||
|
|
||||||
let mut config = Config::new();
|
let mut config = Config::new();
|
||||||
config.set_ingester_threads(2);
|
config.set_ingester_threads(2);
|
||||||
Self {
|
Self {
|
||||||
state: DamusState::Initializing,
|
state: DamusState::Initializing,
|
||||||
pool: RelayPool::new(),
|
pool: RelayPool::new(),
|
||||||
img_cache: HashMap::new(),
|
img_cache: ImageCache::new(imgcache_dir),
|
||||||
note_cache: HashMap::new(),
|
note_cache: HashMap::new(),
|
||||||
initial_filter,
|
initial_filter,
|
||||||
n_panels: 1,
|
n_panels: 1,
|
||||||
@ -477,48 +466,55 @@ impl Damus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_pfp(ui: &mut egui::Ui, img_cache: &mut ImageCache, url: &str) {
|
fn paint_circle(ui: &mut egui::Ui, size: f32) {
|
||||||
|
let (rect, _response) = ui.allocate_at_least(Vec2::new(size, size), Sense::hover());
|
||||||
|
ui.painter()
|
||||||
|
.circle_filled(rect.center(), size / 2.0, ui.visuals().weak_text_color());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_pfp(ui: &mut egui::Ui, damus: &mut Damus, url: &str) {
|
||||||
#[cfg(feature = "profiling")]
|
#[cfg(feature = "profiling")]
|
||||||
puffin::profile_function!();
|
puffin::profile_function!();
|
||||||
|
|
||||||
let urlkey = UrlKey::Orig(url).to_u64();
|
let ui_size = 30.0;
|
||||||
let m_cached_promise = img_cache.get(&urlkey);
|
|
||||||
|
// We will want to downsample these so it's not blurry on hi res displays
|
||||||
|
let img_size = (ui_size * 2.0) as u32;
|
||||||
|
|
||||||
|
let m_cached_promise = damus.img_cache.map().get(url);
|
||||||
if m_cached_promise.is_none() {
|
if m_cached_promise.is_none() {
|
||||||
debug!("urlkey: {:?}", &urlkey);
|
let res = fetch_img(&damus.img_cache, ui.ctx(), url, img_size);
|
||||||
img_cache.insert(urlkey, fetch_img(ui.ctx(), url));
|
damus.img_cache.map_mut().insert(url.to_owned(), res);
|
||||||
}
|
}
|
||||||
|
|
||||||
let pfp_size = 40.0;
|
match damus.img_cache.map()[url].ready() {
|
||||||
|
|
||||||
match img_cache[&urlkey].ready() {
|
|
||||||
None => {
|
None => {
|
||||||
ui.add(Spinner::new().size(pfp_size));
|
ui.add(Spinner::new().size(ui_size));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Failed to fetch profile!
|
||||||
Some(Err(_err)) => {
|
Some(Err(_err)) => {
|
||||||
let failed_key = UrlKey::Failed(url).to_u64();
|
let m_failed_promise = damus.img_cache.map().get(url);
|
||||||
//debug!("has failed promise? {}", img_cache.contains_key(&failed_key));
|
|
||||||
let m_failed_promise = img_cache.get_mut(&failed_key);
|
|
||||||
if m_failed_promise.is_none() {
|
if m_failed_promise.is_none() {
|
||||||
warn!("failed key: {:?}", &failed_key);
|
let no_pfp = fetch_img(&damus.img_cache, ui.ctx(), no_pfp_url(), img_size);
|
||||||
let no_pfp = fetch_img(ui.ctx(), no_pfp_url());
|
damus.img_cache.map_mut().insert(url.to_owned(), no_pfp);
|
||||||
img_cache.insert(failed_key, no_pfp);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
match img_cache[&failed_key].ready() {
|
match damus.img_cache.map().get(url).unwrap().ready() {
|
||||||
None => {
|
None => {
|
||||||
ui.add(Spinner::new().size(pfp_size));
|
paint_circle(ui, ui_size);
|
||||||
}
|
}
|
||||||
Some(Err(_e)) => {
|
Some(Err(_e)) => {
|
||||||
//error!("Image load error: {:?}", e);
|
//error!("Image load error: {:?}", e);
|
||||||
ui.label("❌");
|
paint_circle(ui, ui_size);
|
||||||
}
|
}
|
||||||
Some(Ok(img)) => {
|
Some(Ok(img)) => {
|
||||||
pfp_image(ui, img, pfp_size);
|
pfp_image(ui, img, ui_size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(Ok(img)) => {
|
Some(Ok(img)) => {
|
||||||
pfp_image(ui, img, pfp_size);
|
pfp_image(ui, img, ui_size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -744,8 +740,8 @@ fn render_note(ui: &mut egui::Ui, damus: &mut Damus, note_key: NoteKey) -> Resul
|
|||||||
{
|
{
|
||||||
// these have different lifetimes and types,
|
// these have different lifetimes and types,
|
||||||
// so the calls must be separate
|
// so the calls must be separate
|
||||||
Some(pic) => render_pfp(ui, &mut damus.img_cache, pic),
|
Some(pic) => render_pfp(ui, damus, pic),
|
||||||
None => render_pfp(ui, &mut damus.img_cache, no_pfp_url()),
|
None => render_pfp(ui, damus, no_pfp_url()),
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| {
|
ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| {
|
||||||
|
14
src/error.rs
14
src/error.rs
@ -1,8 +1,10 @@
|
|||||||
use std::fmt;
|
use std::{fmt, io};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
NoActiveSubscription,
|
NoActiveSubscription,
|
||||||
|
LoadFailed,
|
||||||
|
Io(io::Error),
|
||||||
Nostr(enostr::Error),
|
Nostr(enostr::Error),
|
||||||
Ndb(nostrdb::Error),
|
Ndb(nostrdb::Error),
|
||||||
Image(image::error::ImageError),
|
Image(image::error::ImageError),
|
||||||
@ -15,10 +17,14 @@ impl fmt::Display for Error {
|
|||||||
Self::NoActiveSubscription => {
|
Self::NoActiveSubscription => {
|
||||||
write!(f, "subscription not active in timeline")
|
write!(f, "subscription not active in timeline")
|
||||||
}
|
}
|
||||||
|
Self::LoadFailed => {
|
||||||
|
write!(f, "load failed")
|
||||||
|
}
|
||||||
Self::Nostr(e) => write!(f, "{e}"),
|
Self::Nostr(e) => write!(f, "{e}"),
|
||||||
Self::Ndb(e) => write!(f, "{e}"),
|
Self::Ndb(e) => write!(f, "{e}"),
|
||||||
Self::Image(e) => write!(f, "{e}"),
|
Self::Image(e) => write!(f, "{e}"),
|
||||||
Self::Generic(e) => write!(f, "{e}"),
|
Self::Generic(e) => write!(f, "{e}"),
|
||||||
|
Self::Io(e) => write!(f, "{e}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -46,3 +52,9 @@ impl From<enostr::Error> for Error {
|
|||||||
Error::Nostr(err)
|
Error::Nostr(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<io::Error> for Error {
|
||||||
|
fn from(err: io::Error) -> Self {
|
||||||
|
Error::Io(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,8 +1,16 @@
|
|||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use crate::result::Result;
|
use crate::result::Result;
|
||||||
|
use crate::imgcache::ImageCache;
|
||||||
use egui::{Color32, ColorImage, SizeHint, TextureHandle};
|
use egui::{Color32, ColorImage, SizeHint, TextureHandle};
|
||||||
use image::imageops::FilterType;
|
use image::imageops::FilterType;
|
||||||
use poll_promise::Promise;
|
use poll_promise::Promise;
|
||||||
|
use tokio::fs;
|
||||||
|
use std::path;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
//pub type ImageCacheKey = String;
|
||||||
|
//pub type ImageCacheValue = Promise<Result<TextureHandle>>;
|
||||||
|
//pub type ImageCache = HashMap<String, ImageCacheValue>;
|
||||||
|
|
||||||
pub fn round_image(image: &mut ColorImage) {
|
pub fn round_image(image: &mut ColorImage) {
|
||||||
#[cfg(feature = "profiling")]
|
#[cfg(feature = "profiling")]
|
||||||
@ -78,12 +86,11 @@ fn process_pfp_bitmap(size: u32, image: &mut image::DynamicImage) -> ColorImage
|
|||||||
color_image
|
color_image
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_img_response(response: ehttp::Response) -> Result<ColorImage> {
|
fn parse_img_response(response: ehttp::Response, size: u32) -> Result<ColorImage> {
|
||||||
#[cfg(feature = "profiling")]
|
#[cfg(feature = "profiling")]
|
||||||
puffin::profile_function!();
|
puffin::profile_function!();
|
||||||
|
|
||||||
let content_type = response.content_type().unwrap_or_default();
|
let content_type = response.content_type().unwrap_or_default();
|
||||||
let size: u32 = 100;
|
|
||||||
|
|
||||||
if content_type.starts_with("image/svg") {
|
if content_type.starts_with("image/svg") {
|
||||||
#[cfg(feature = "profiling")]
|
#[cfg(feature = "profiling")]
|
||||||
@ -105,24 +112,70 @@ fn parse_img_response(response: ehttp::Response) -> Result<ColorImage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fetch_img(ctx: &egui::Context, url: &str) -> Promise<Result<TextureHandle>> {
|
fn fetch_img_from_disk(ctx: &egui::Context, url: &str, path: &path::Path) -> Promise<Result<TextureHandle>> {
|
||||||
// TODO: fetch image from local cache
|
let ctx = ctx.clone();
|
||||||
fetch_img_from_net(ctx, url)
|
let url = url.to_owned();
|
||||||
|
let path = path.to_owned();
|
||||||
|
Promise::spawn_async(async move {
|
||||||
|
let data = fs::read(path).await?;
|
||||||
|
let image_buffer = image::load_from_memory(&data)?;
|
||||||
|
|
||||||
|
// TODO: remove unwrap here
|
||||||
|
let flat_samples = image_buffer.as_flat_samples_u8().unwrap();
|
||||||
|
let img = ColorImage::from_rgba_unmultiplied(
|
||||||
|
[
|
||||||
|
image_buffer.width() as usize,
|
||||||
|
image_buffer.height() as usize,
|
||||||
|
],
|
||||||
|
flat_samples.as_slice(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(ctx.load_texture(&url, img, Default::default()))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fetch_img_from_net(ctx: &egui::Context, url: &str) -> Promise<Result<TextureHandle>> {
|
pub fn fetch_img(
|
||||||
|
img_cache: &ImageCache,
|
||||||
|
ctx: &egui::Context,
|
||||||
|
url: &str,
|
||||||
|
size: u32,
|
||||||
|
) -> Promise<Result<TextureHandle>> {
|
||||||
|
let key = ImageCache::key(url);
|
||||||
|
let path = img_cache.cache_dir.join(&key);
|
||||||
|
|
||||||
|
if path.exists() {
|
||||||
|
fetch_img_from_disk(ctx, url, &path)
|
||||||
|
} else {
|
||||||
|
fetch_img_from_net(&img_cache.cache_dir, ctx, url, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: fetch image from local cache
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fetch_img_from_net(cache_path: &path::Path, ctx: &egui::Context, url: &str, size: u32) -> Promise<Result<TextureHandle>> {
|
||||||
let (sender, promise) = Promise::new();
|
let (sender, promise) = Promise::new();
|
||||||
let request = ehttp::Request::get(url);
|
let request = ehttp::Request::get(url);
|
||||||
let ctx = ctx.clone();
|
let ctx = ctx.clone();
|
||||||
let cloned_url = url.to_owned();
|
let cloned_url = url.to_owned();
|
||||||
|
let cache_path = cache_path.to_owned();
|
||||||
ehttp::fetch(request, move |response| {
|
ehttp::fetch(request, move |response| {
|
||||||
let handle = response
|
let handle = response
|
||||||
.map_err(Error::Generic)
|
.map_err(Error::Generic)
|
||||||
.and_then(parse_img_response)
|
.and_then(|resp| parse_img_response(resp, size))
|
||||||
.map(|img| ctx.load_texture(&cloned_url, img, Default::default()));
|
.map(|img| {
|
||||||
|
let texture_handle = ctx.load_texture(&cloned_url, img.clone(), Default::default());
|
||||||
|
|
||||||
|
// write to disk
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
ImageCache::write(&cache_path, &cloned_url, img)
|
||||||
|
});
|
||||||
|
|
||||||
|
texture_handle
|
||||||
|
});
|
||||||
|
|
||||||
sender.send(handle); // send the results back to the UI thread.
|
sender.send(handle); // send the results back to the UI thread.
|
||||||
ctx.request_repaint();
|
ctx.request_repaint();
|
||||||
});
|
});
|
||||||
|
|
||||||
promise
|
promise
|
||||||
}
|
}
|
||||||
|
57
src/imgcache.rs
Normal file
57
src/imgcache.rs
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
use crate::{Error, Result};
|
||||||
|
use egui::TextureHandle;
|
||||||
|
use poll_promise::Promise;
|
||||||
|
|
||||||
|
use egui::ColorImage;
|
||||||
|
use hex;
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::collections::hash_map::Entry;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
use std::io;
|
||||||
|
use std::path;
|
||||||
|
use tokio::fs;
|
||||||
|
|
||||||
|
pub type ImageCacheValue = Promise<Result<TextureHandle>>;
|
||||||
|
pub type ImageCacheMap = HashMap<String, ImageCacheValue>;
|
||||||
|
|
||||||
|
pub struct ImageCache {
|
||||||
|
pub cache_dir: path::PathBuf,
|
||||||
|
url_imgs: ImageCacheMap,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImageCache {
|
||||||
|
pub fn new(cache_dir: path::PathBuf) -> Self {
|
||||||
|
Self {
|
||||||
|
cache_dir,
|
||||||
|
url_imgs: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write(cache_dir: &path::Path, url: &str, data: ColorImage) -> Result<()> {
|
||||||
|
let file_path = cache_dir.join(&Self::key(url));
|
||||||
|
let file = File::options().write(true).create(true).open(file_path)?;
|
||||||
|
let encoder = image::codecs::webp::WebPEncoder::new_lossless(file);
|
||||||
|
encoder.encode(
|
||||||
|
data.as_raw(),
|
||||||
|
data.size[0] as u32,
|
||||||
|
data.size[1] as u32,
|
||||||
|
image::ColorType::Rgba8,
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn key(url: &str) -> String {
|
||||||
|
base32::encode(base32::Alphabet::Crockford, url.as_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn map(&self) -> &ImageCacheMap {
|
||||||
|
&self.url_imgs
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn map_mut(&mut self) -> &mut ImageCacheMap {
|
||||||
|
&mut self.url_imgs
|
||||||
|
}
|
||||||
|
}
|
@ -7,6 +7,7 @@ mod abbrev;
|
|||||||
mod fonts;
|
mod fonts;
|
||||||
mod images;
|
mod images;
|
||||||
mod result;
|
mod result;
|
||||||
|
mod imgcache;
|
||||||
mod filter;
|
mod filter;
|
||||||
mod ui;
|
mod ui;
|
||||||
mod timecache;
|
mod timecache;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
/// Show a relative time string based on some timestamp
|
/// Show a relative time string based on some timestamp
|
||||||
pub fn time_ago_since(timestamp: u64) -> String {
|
pub fn time_ago_since(timestamp: u64) -> String {
|
||||||
|
Loading…
Reference in New Issue
Block a user