Merge GIF support by kernel
kernelkind (20):
use bincode
update ehttp to 0.5.0
introduce UrlMimes
use mime_guess
add SupportedMimeType
rename ImageCache -> MediaCache
Use TexturedImage in MediaCache
render MediaCache method
move MediaCache rendering to render_media_cache call
support multiple media cache files
introduce Images
render Images method
migrate to using Images instead of MediaCache directly
URL mime hosted completeness
handle UrlCache io
introduce gif animation
handle gif state
integrate gifs
use SupportedMimeType for media_upload
render gif in PostView
This commit is contained in:
@@ -2,7 +2,7 @@ use enostr::FullKeypair;
|
||||
use nostrdb::Ndb;
|
||||
|
||||
use notedeck::{
|
||||
Accounts, AccountsAction, AddAccountAction, ImageCache, SingleUnkIdAction, SwitchAccountAction,
|
||||
Accounts, AccountsAction, AddAccountAction, Images, SingleUnkIdAction, SwitchAccountAction,
|
||||
};
|
||||
|
||||
use crate::app::get_active_columns_mut;
|
||||
@@ -27,7 +27,7 @@ pub fn render_accounts_route(
|
||||
ui: &mut egui::Ui,
|
||||
ndb: &Ndb,
|
||||
col: usize,
|
||||
img_cache: &mut ImageCache,
|
||||
img_cache: &mut Images,
|
||||
accounts: &mut Accounts,
|
||||
decks: &mut DecksCache,
|
||||
login_state: &mut AcquireKeyState,
|
||||
|
||||
@@ -13,7 +13,7 @@ use crate::{
|
||||
Result,
|
||||
};
|
||||
|
||||
use notedeck::{Accounts, AppContext, DataPath, DataPathType, FilterState, ImageCache, UnknownIds};
|
||||
use notedeck::{Accounts, AppContext, DataPath, DataPathType, FilterState, UnknownIds};
|
||||
|
||||
use enostr::{ClientMessage, Keypair, PoolRelay, Pubkey, RelayEvent, RelayMessage, RelayPool};
|
||||
use uuid::Uuid;
|
||||
@@ -172,6 +172,8 @@ fn unknown_id_send(unknown_ids: &mut UnknownIds, pool: &mut RelayPool) {
|
||||
}
|
||||
|
||||
fn update_damus(damus: &mut Damus, app_ctx: &mut AppContext<'_>, ctx: &egui::Context) {
|
||||
app_ctx.img_cache.urls.cache.handle_io();
|
||||
|
||||
match damus.state {
|
||||
DamusState::Initializing => {
|
||||
damus.state = DamusState::Initialized;
|
||||
@@ -464,7 +466,7 @@ impl Damus {
|
||||
let decks_cache = DecksCache::default();
|
||||
|
||||
let path = DataPath::new(&data_path);
|
||||
let imgcache_dir = path.path(DataPathType::Cache).join(ImageCache::rel_dir());
|
||||
let imgcache_dir = path.path(DataPathType::Cache);
|
||||
let _ = std::fs::create_dir_all(imgcache_dir.clone());
|
||||
let debug = true;
|
||||
|
||||
|
||||
122
crates/notedeck_columns/src/gif.rs
Normal file
122
crates/notedeck_columns/src/gif.rs
Normal file
@@ -0,0 +1,122 @@
|
||||
use std::{
|
||||
sync::mpsc::TryRecvError,
|
||||
time::{Instant, SystemTime},
|
||||
};
|
||||
|
||||
use egui::TextureHandle;
|
||||
use notedeck::{GifState, GifStateMap, TexturedImage};
|
||||
|
||||
pub struct LatextTexture<'a> {
|
||||
pub texture: &'a TextureHandle,
|
||||
pub request_next_repaint: Option<SystemTime>,
|
||||
}
|
||||
|
||||
/// This is necessary because other repaint calls can effectively steal our repaint request.
|
||||
/// So we must keep on requesting to repaint at our desired time to ensure our repaint goes through.
|
||||
/// See [`egui::Context::request_repaint_after`]
|
||||
pub fn handle_repaint<'a>(ui: &egui::Ui, latest: LatextTexture<'a>) -> &'a TextureHandle {
|
||||
if let Some(repaint) = latest.request_next_repaint {
|
||||
if let Ok(dur) = repaint.duration_since(SystemTime::now()) {
|
||||
ui.ctx().request_repaint_after(dur);
|
||||
}
|
||||
}
|
||||
latest.texture
|
||||
}
|
||||
|
||||
#[must_use = "caller should pass the return value to `gif::handle_repaint`"]
|
||||
pub fn retrieve_latest_texture<'a>(
|
||||
url: &str,
|
||||
gifs: &'a mut GifStateMap,
|
||||
cached_image: &'a mut TexturedImage,
|
||||
) -> LatextTexture<'a> {
|
||||
match cached_image {
|
||||
TexturedImage::Static(texture) => LatextTexture {
|
||||
texture,
|
||||
request_next_repaint: None,
|
||||
},
|
||||
TexturedImage::Animated(animation) => {
|
||||
if let Some(receiver) = &animation.receiver {
|
||||
loop {
|
||||
match receiver.try_recv() {
|
||||
Ok(frame) => animation.other_frames.push(frame),
|
||||
Err(TryRecvError::Empty) => {
|
||||
break;
|
||||
}
|
||||
Err(TryRecvError::Disconnected) => {
|
||||
animation.receiver = None;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let now = Instant::now();
|
||||
let (texture, maybe_new_state, request_next_repaint) = match gifs.get(url) {
|
||||
Some(prev_state) => {
|
||||
let should_advance =
|
||||
now - prev_state.last_frame_rendered >= prev_state.last_frame_duration;
|
||||
|
||||
if should_advance {
|
||||
let maybe_new_index = if animation.receiver.is_some()
|
||||
|| prev_state.last_frame_index < animation.num_frames() - 1
|
||||
{
|
||||
prev_state.last_frame_index + 1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
match animation.get_frame(maybe_new_index) {
|
||||
Some(frame) => {
|
||||
let next_frame_time = SystemTime::now().checked_add(frame.delay);
|
||||
(
|
||||
&frame.texture,
|
||||
Some(GifState {
|
||||
last_frame_rendered: now,
|
||||
last_frame_duration: frame.delay,
|
||||
next_frame_time,
|
||||
last_frame_index: maybe_new_index,
|
||||
}),
|
||||
next_frame_time,
|
||||
)
|
||||
}
|
||||
None => {
|
||||
let (tex, state) =
|
||||
match animation.get_frame(prev_state.last_frame_index) {
|
||||
Some(frame) => (&frame.texture, None),
|
||||
None => (&animation.first_frame.texture, None),
|
||||
};
|
||||
|
||||
(tex, state, prev_state.next_frame_time)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let (tex, state) = match animation.get_frame(prev_state.last_frame_index) {
|
||||
Some(frame) => (&frame.texture, None),
|
||||
None => (&animation.first_frame.texture, None),
|
||||
};
|
||||
(tex, state, prev_state.next_frame_time)
|
||||
}
|
||||
}
|
||||
None => (
|
||||
&animation.first_frame.texture,
|
||||
Some(GifState {
|
||||
last_frame_rendered: now,
|
||||
last_frame_duration: animation.first_frame.delay,
|
||||
next_frame_time: None,
|
||||
last_frame_index: 0,
|
||||
}),
|
||||
None,
|
||||
),
|
||||
};
|
||||
|
||||
if let Some(new_state) = maybe_new_state {
|
||||
gifs.insert(url.to_owned(), new_state);
|
||||
}
|
||||
|
||||
LatextTexture {
|
||||
texture,
|
||||
request_next_repaint,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,28 @@
|
||||
use egui::{pos2, Color32, ColorImage, Rect, Sense, SizeHint, TextureHandle};
|
||||
use egui::{pos2, Color32, ColorImage, Rect, Sense, SizeHint};
|
||||
use image::codecs::gif::GifDecoder;
|
||||
use image::imageops::FilterType;
|
||||
use notedeck::ImageCache;
|
||||
use image::AnimationDecoder;
|
||||
use image::DynamicImage;
|
||||
use image::FlatSamples;
|
||||
use image::Frame;
|
||||
use notedeck::Animation;
|
||||
use notedeck::ImageFrame;
|
||||
use notedeck::MediaCache;
|
||||
use notedeck::MediaCacheType;
|
||||
use notedeck::Result;
|
||||
use notedeck::TextureFrame;
|
||||
use notedeck::TexturedImage;
|
||||
use poll_promise::Promise;
|
||||
use std::collections::VecDeque;
|
||||
use std::io::Cursor;
|
||||
use std::path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::mpsc;
|
||||
use std::sync::mpsc::SyncSender;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use tokio::fs;
|
||||
|
||||
//pub type ImageCacheKey = String;
|
||||
//pub type ImageCacheValue = Promise<Result<TextureHandle>>;
|
||||
//pub type ImageCache = HashMap<String, ImageCacheValue>;
|
||||
|
||||
// NOTE(jb55): chatgpt wrote this because I was too dumb to
|
||||
pub fn aspect_fill(
|
||||
ui: &mut egui::Ui,
|
||||
@@ -102,7 +114,7 @@ pub fn round_image(image: &mut ColorImage) {
|
||||
}
|
||||
}
|
||||
|
||||
fn process_pfp_bitmap(imgtyp: ImageType, image: &mut image::DynamicImage) -> ColorImage {
|
||||
fn process_pfp_bitmap(imgtyp: ImageType, mut image: image::DynamicImage) -> ColorImage {
|
||||
#[cfg(feature = "profiling")]
|
||||
puffin::profile_function!();
|
||||
|
||||
@@ -125,10 +137,10 @@ fn process_pfp_bitmap(imgtyp: ImageType, image: &mut image::DynamicImage) -> Col
|
||||
|
||||
if image.width() > smaller {
|
||||
let excess = image.width() - smaller;
|
||||
*image = image.crop_imm(excess / 2, 0, image.width() - excess, image.height());
|
||||
image = image.crop_imm(excess / 2, 0, image.width() - excess, image.height());
|
||||
} else if image.height() > smaller {
|
||||
let excess = image.height() - smaller;
|
||||
*image = image.crop_imm(0, excess / 2, image.width(), image.height() - excess);
|
||||
image = image.crop_imm(0, excess / 2, image.width(), image.height() - excess);
|
||||
}
|
||||
let image = image.resize(size, size, FilterType::CatmullRom); // DynamicImage
|
||||
let image_buffer = image.into_rgba8(); // RgbaImage (ImageBuffer)
|
||||
@@ -166,8 +178,8 @@ fn parse_img_response(response: ehttp::Response, imgtyp: ImageType) -> Result<Co
|
||||
} else if content_type.starts_with("image/") {
|
||||
#[cfg(feature = "profiling")]
|
||||
puffin::profile_scope!("load_from_memory");
|
||||
let mut dyn_image = image::load_from_memory(&response.bytes)?;
|
||||
Ok(process_pfp_bitmap(imgtyp, &mut dyn_image))
|
||||
let dyn_image = image::load_from_memory(&response.bytes)?;
|
||||
Ok(process_pfp_bitmap(imgtyp, dyn_image))
|
||||
} else {
|
||||
Err(format!("Expected image, found content-type {:?}", content_type).into())
|
||||
}
|
||||
@@ -177,28 +189,162 @@ fn fetch_img_from_disk(
|
||||
ctx: &egui::Context,
|
||||
url: &str,
|
||||
path: &path::Path,
|
||||
) -> Promise<Result<TextureHandle>> {
|
||||
cache_type: MediaCacheType,
|
||||
) -> Promise<Result<TexturedImage>> {
|
||||
let ctx = ctx.clone();
|
||||
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).map_err(notedeck::Error::Image)?;
|
||||
match cache_type {
|
||||
MediaCacheType::Image => {
|
||||
let data = fs::read(path).await?;
|
||||
let image_buffer =
|
||||
image::load_from_memory(&data).map_err(notedeck::Error::Image)?;
|
||||
|
||||
// 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()))
|
||||
let img = buffer_to_color_image(
|
||||
image_buffer.as_flat_samples_u8(),
|
||||
image_buffer.width(),
|
||||
image_buffer.height(),
|
||||
);
|
||||
Ok(TexturedImage::Static(ctx.load_texture(
|
||||
&url,
|
||||
img,
|
||||
Default::default(),
|
||||
)))
|
||||
}
|
||||
MediaCacheType::Gif => {
|
||||
let gif_bytes = fs::read(path.clone()).await?; // Read entire file into a Vec<u8>
|
||||
generate_gif(ctx, url, &path, gif_bytes, false, |i| {
|
||||
buffer_to_color_image(i.as_flat_samples_u8(), i.width(), i.height())
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_gif(
|
||||
ctx: egui::Context,
|
||||
url: String,
|
||||
path: &path::Path,
|
||||
data: Vec<u8>,
|
||||
write_to_disk: bool,
|
||||
process_to_egui: impl Fn(DynamicImage) -> ColorImage + Send + Copy + 'static,
|
||||
) -> Result<TexturedImage> {
|
||||
let decoder = {
|
||||
let reader = Cursor::new(data.as_slice());
|
||||
GifDecoder::new(reader)?
|
||||
};
|
||||
let (tex_input, tex_output) = mpsc::sync_channel(4);
|
||||
let (maybe_encoder_input, maybe_encoder_output) = if write_to_disk {
|
||||
let (inp, out) = mpsc::sync_channel(4);
|
||||
(Some(inp), Some(out))
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
let mut frames: VecDeque<Frame> = decoder
|
||||
.into_frames()
|
||||
.collect::<std::result::Result<VecDeque<_>, image::ImageError>>()
|
||||
.map_err(|e| notedeck::Error::Generic(e.to_string()))?;
|
||||
|
||||
let first_frame = frames.pop_front().map(|frame| {
|
||||
generate_animation_frame(
|
||||
&ctx,
|
||||
&url,
|
||||
0,
|
||||
frame,
|
||||
maybe_encoder_input.as_ref(),
|
||||
process_to_egui,
|
||||
)
|
||||
});
|
||||
|
||||
let cur_url = url.clone();
|
||||
thread::spawn(move || {
|
||||
for (index, frame) in frames.into_iter().enumerate() {
|
||||
let texture_frame = generate_animation_frame(
|
||||
&ctx,
|
||||
&cur_url,
|
||||
index,
|
||||
frame,
|
||||
maybe_encoder_input.as_ref(),
|
||||
process_to_egui,
|
||||
);
|
||||
|
||||
if tex_input.send(texture_frame).is_err() {
|
||||
tracing::error!("AnimationTextureFrame mpsc stopped abruptly");
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(encoder_output) = maybe_encoder_output {
|
||||
let path = path.to_owned();
|
||||
|
||||
thread::spawn(move || {
|
||||
let mut imgs = Vec::new();
|
||||
while let Ok(img) = encoder_output.recv() {
|
||||
imgs.push(img);
|
||||
}
|
||||
|
||||
if let Err(e) = MediaCache::write_gif(&path, &url, imgs) {
|
||||
tracing::error!("Could not write gif to disk: {e}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
first_frame.map_or_else(
|
||||
|| {
|
||||
Err(notedeck::Error::Generic(
|
||||
"first frame not found for gif".to_owned(),
|
||||
))
|
||||
},
|
||||
|first_frame| {
|
||||
Ok(TexturedImage::Animated(Animation {
|
||||
other_frames: Default::default(),
|
||||
receiver: Some(tex_output),
|
||||
first_frame,
|
||||
}))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn generate_animation_frame(
|
||||
ctx: &egui::Context,
|
||||
url: &str,
|
||||
index: usize,
|
||||
frame: image::Frame,
|
||||
maybe_encoder_input: Option<&SyncSender<ImageFrame>>,
|
||||
process_to_egui: impl Fn(DynamicImage) -> ColorImage + Send + 'static,
|
||||
) -> TextureFrame {
|
||||
let delay = Duration::from(frame.delay());
|
||||
let img = DynamicImage::ImageRgba8(frame.into_buffer());
|
||||
let color_img = process_to_egui(img);
|
||||
|
||||
if let Some(sender) = maybe_encoder_input {
|
||||
if let Err(e) = sender.send(ImageFrame {
|
||||
delay,
|
||||
image: color_img.clone(),
|
||||
}) {
|
||||
tracing::error!("ImageFrame mpsc unexpectedly closed: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
TextureFrame {
|
||||
delay,
|
||||
texture: ctx.load_texture(format!("{}{}", url, index), color_img, Default::default()),
|
||||
}
|
||||
}
|
||||
|
||||
fn buffer_to_color_image(
|
||||
samples: Option<FlatSamples<&[u8]>>,
|
||||
width: u32,
|
||||
height: u32,
|
||||
) -> ColorImage {
|
||||
// TODO(jb55): remove unwrap here
|
||||
let flat_samples = samples.unwrap();
|
||||
ColorImage::from_rgba_unmultiplied([width as usize, height as usize], flat_samples.as_slice())
|
||||
}
|
||||
|
||||
pub fn fetch_binary_from_disk(path: PathBuf) -> Result<Vec<u8>> {
|
||||
std::fs::read(path).map_err(|e| notedeck::Error::Generic(e.to_string()))
|
||||
}
|
||||
@@ -213,18 +359,19 @@ pub enum ImageType {
|
||||
}
|
||||
|
||||
pub fn fetch_img(
|
||||
img_cache: &ImageCache,
|
||||
img_cache: &MediaCache,
|
||||
ctx: &egui::Context,
|
||||
url: &str,
|
||||
imgtyp: ImageType,
|
||||
) -> Promise<Result<TextureHandle>> {
|
||||
let key = ImageCache::key(url);
|
||||
cache_type: MediaCacheType,
|
||||
) -> Promise<Result<TexturedImage>> {
|
||||
let key = MediaCache::key(url);
|
||||
let path = img_cache.cache_dir.join(key);
|
||||
|
||||
if path.exists() {
|
||||
fetch_img_from_disk(ctx, url, &path)
|
||||
fetch_img_from_disk(ctx, url, &path, cache_type)
|
||||
} else {
|
||||
fetch_img_from_net(&img_cache.cache_dir, ctx, url, imgtyp)
|
||||
fetch_img_from_net(&img_cache.cache_dir, ctx, url, imgtyp, cache_type)
|
||||
}
|
||||
|
||||
// TODO: fetch image from local cache
|
||||
@@ -235,24 +382,43 @@ fn fetch_img_from_net(
|
||||
ctx: &egui::Context,
|
||||
url: &str,
|
||||
imgtyp: ImageType,
|
||||
) -> Promise<Result<TextureHandle>> {
|
||||
cache_type: MediaCacheType,
|
||||
) -> Promise<Result<TexturedImage>> {
|
||||
let (sender, promise) = Promise::new();
|
||||
let request = ehttp::Request::get(url);
|
||||
let ctx = ctx.clone();
|
||||
let cloned_url = url.to_owned();
|
||||
let cache_path = cache_path.to_owned();
|
||||
ehttp::fetch(request, move |response| {
|
||||
let handle = response
|
||||
.map_err(notedeck::Error::Generic)
|
||||
.and_then(|resp| parse_img_response(resp, imgtyp))
|
||||
.map(|img| {
|
||||
let texture_handle = ctx.load_texture(&cloned_url, img.clone(), Default::default());
|
||||
let handle = response.map_err(notedeck::Error::Generic).and_then(|resp| {
|
||||
match cache_type {
|
||||
MediaCacheType::Image => {
|
||||
let img = parse_img_response(resp, imgtyp);
|
||||
img.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));
|
||||
// write to disk
|
||||
std::thread::spawn(move || {
|
||||
MediaCache::write(&cache_path, &cloned_url, img)
|
||||
});
|
||||
|
||||
texture_handle
|
||||
});
|
||||
TexturedImage::Static(texture_handle)
|
||||
})
|
||||
}
|
||||
MediaCacheType::Gif => {
|
||||
let gif_bytes = resp.bytes;
|
||||
generate_gif(
|
||||
ctx.clone(),
|
||||
cloned_url,
|
||||
&cache_path,
|
||||
gif_bytes,
|
||||
true,
|
||||
move |img| process_pfp_bitmap(imgtyp, img),
|
||||
)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
sender.send(handle); // send the results back to the UI thread.
|
||||
ctx.request_repaint();
|
||||
|
||||
@@ -15,6 +15,7 @@ mod deck_state;
|
||||
mod decks;
|
||||
mod draft;
|
||||
mod frame_history;
|
||||
mod gif;
|
||||
mod images;
|
||||
mod key_parsing;
|
||||
pub mod login_manager;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use std::{collections::BTreeMap, path::PathBuf};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use base64::{prelude::BASE64_URL_SAFE, Engine};
|
||||
use ehttp::Request;
|
||||
use nostrdb::{Note, NoteBuilder};
|
||||
use notedeck::SupportedMimeType;
|
||||
use poll_promise::Promise;
|
||||
use sha2::{Digest, Sha256};
|
||||
use url::Url;
|
||||
@@ -104,15 +105,13 @@ fn create_nip96_request(
|
||||
body.extend(file_contents);
|
||||
body.extend(format!("\r\n--{}--\r\n", boundary).as_bytes());
|
||||
|
||||
let headers = {
|
||||
let mut map = BTreeMap::new();
|
||||
map.insert(
|
||||
"Content-Type".to_owned(),
|
||||
format!("multipart/form-data; boundary={boundary}"),
|
||||
);
|
||||
map.insert("Authorization".to_owned(), format!("Nostr {nip98_base64}"));
|
||||
map
|
||||
};
|
||||
let headers = ehttp::Headers::new(&[
|
||||
(
|
||||
"Content-Type",
|
||||
format!("multipart/form-data; boundary={boundary}").as_str(),
|
||||
),
|
||||
("Authorization", format!("Nostr {nip98_base64}").as_str()),
|
||||
]);
|
||||
|
||||
Request {
|
||||
method: "POST".to_string(),
|
||||
@@ -234,13 +233,13 @@ fn find_nip94_ev_in_json(json: String) -> Result<Nip94Event, Error> {
|
||||
pub struct MediaPath {
|
||||
full_path: PathBuf,
|
||||
file_name: String,
|
||||
media_type: SupportedMediaType,
|
||||
media_type: SupportedMimeType,
|
||||
}
|
||||
|
||||
impl MediaPath {
|
||||
pub fn new(path: PathBuf) -> Result<Self, Error> {
|
||||
if let Some(ex) = path.extension().and_then(|f| f.to_str()) {
|
||||
let media_type = SupportedMediaType::from_extension(ex)?;
|
||||
let media_type = SupportedMimeType::from_extension(ex)?;
|
||||
let file_name = path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
@@ -261,47 +260,6 @@ impl MediaPath {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SupportedMediaType {
|
||||
Png,
|
||||
Jpeg,
|
||||
Webp,
|
||||
}
|
||||
|
||||
impl SupportedMediaType {
|
||||
pub fn mime_extension(&self) -> &str {
|
||||
match &self {
|
||||
SupportedMediaType::Png => "png",
|
||||
SupportedMediaType::Jpeg => "jpeg",
|
||||
SupportedMediaType::Webp => "webp",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_mime(&self) -> String {
|
||||
format!("{}/{}", self.mime_type(), self.mime_extension())
|
||||
}
|
||||
|
||||
fn mime_type(&self) -> String {
|
||||
match &self {
|
||||
SupportedMediaType::Png | SupportedMediaType::Jpeg | SupportedMediaType::Webp => {
|
||||
"image"
|
||||
}
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn from_extension(ext: &str) -> Result<Self, Error> {
|
||||
match ext.to_lowercase().as_str() {
|
||||
"jpeg" | "jpg" => Ok(SupportedMediaType::Jpeg),
|
||||
"png" => Ok(SupportedMediaType::Png),
|
||||
"webp" => Ok(SupportedMediaType::Webp),
|
||||
unsupported_type => Err(Error::Generic(format!(
|
||||
"{unsupported_type} is not a valid file type to upload."
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Deserialize)]
|
||||
pub struct Nip94Event {
|
||||
pub url: String,
|
||||
|
||||
@@ -7,12 +7,12 @@ use crate::{
|
||||
|
||||
use enostr::Pubkey;
|
||||
use nostrdb::Ndb;
|
||||
use notedeck::{Accounts, ImageCache, MuteFun, NoteCache, UnknownIds};
|
||||
use notedeck::{Accounts, Images, MuteFun, NoteCache, UnknownIds};
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn render_timeline_route(
|
||||
ndb: &Ndb,
|
||||
img_cache: &mut ImageCache,
|
||||
img_cache: &mut Images,
|
||||
unknown_ids: &mut UnknownIds,
|
||||
note_cache: &mut NoteCache,
|
||||
timeline_cache: &mut TimelineCache,
|
||||
@@ -102,7 +102,7 @@ pub fn render_profile_route(
|
||||
accounts: &Accounts,
|
||||
ndb: &Ndb,
|
||||
timeline_cache: &mut TimelineCache,
|
||||
img_cache: &mut ImageCache,
|
||||
img_cache: &mut Images,
|
||||
note_cache: &mut NoteCache,
|
||||
unknown_ids: &mut UnknownIds,
|
||||
col: usize,
|
||||
|
||||
@@ -3,14 +3,14 @@ use egui::{
|
||||
Align, Button, Frame, Image, InnerResponse, Layout, RichText, ScrollArea, Ui, UiBuilder, Vec2,
|
||||
};
|
||||
use nostrdb::{Ndb, Transaction};
|
||||
use notedeck::{Accounts, ImageCache};
|
||||
use notedeck::{Accounts, Images};
|
||||
|
||||
use super::profile::preview::SimpleProfilePreview;
|
||||
|
||||
pub struct AccountsView<'a> {
|
||||
ndb: &'a Ndb,
|
||||
accounts: &'a Accounts,
|
||||
img_cache: &'a mut ImageCache,
|
||||
img_cache: &'a mut Images,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -27,7 +27,7 @@ enum ProfilePreviewAction {
|
||||
}
|
||||
|
||||
impl<'a> AccountsView<'a> {
|
||||
pub fn new(ndb: &'a Ndb, accounts: &'a Accounts, img_cache: &'a mut ImageCache) -> Self {
|
||||
pub fn new(ndb: &'a Ndb, accounts: &'a Accounts, img_cache: &'a mut Images) -> Self {
|
||||
AccountsView {
|
||||
ndb,
|
||||
accounts,
|
||||
@@ -54,7 +54,7 @@ impl<'a> AccountsView<'a> {
|
||||
ui: &mut Ui,
|
||||
accounts: &Accounts,
|
||||
ndb: &Ndb,
|
||||
img_cache: &mut ImageCache,
|
||||
img_cache: &mut Images,
|
||||
) -> Option<AccountsViewResponse> {
|
||||
let mut return_op: Option<AccountsViewResponse> = None;
|
||||
ui.allocate_ui_with_layout(
|
||||
|
||||
@@ -17,7 +17,7 @@ use crate::{
|
||||
Damus,
|
||||
};
|
||||
|
||||
use notedeck::{AppContext, ImageCache, NotedeckTextStyle, UserAccount};
|
||||
use notedeck::{AppContext, Images, NotedeckTextStyle, UserAccount};
|
||||
use tokenator::{ParseError, TokenParser, TokenSerializable, TokenWriter};
|
||||
|
||||
use super::{anim::AnimationHelper, padding, ProfilePreview};
|
||||
@@ -163,7 +163,7 @@ impl AddColumnOption {
|
||||
pub struct AddColumnView<'a> {
|
||||
key_state_map: &'a mut HashMap<Id, AcquireKeyState>,
|
||||
ndb: &'a Ndb,
|
||||
img_cache: &'a mut ImageCache,
|
||||
img_cache: &'a mut Images,
|
||||
cur_account: Option<&'a UserAccount>,
|
||||
}
|
||||
|
||||
@@ -171,7 +171,7 @@ impl<'a> AddColumnView<'a> {
|
||||
pub fn new(
|
||||
key_state_map: &'a mut HashMap<Id, AcquireKeyState>,
|
||||
ndb: &'a Ndb,
|
||||
img_cache: &'a mut ImageCache,
|
||||
img_cache: &'a mut Images,
|
||||
cur_account: Option<&'a UserAccount>,
|
||||
) -> Self {
|
||||
Self {
|
||||
|
||||
@@ -16,11 +16,11 @@ use egui::Margin;
|
||||
use egui::{RichText, Stroke, UiBuilder};
|
||||
use enostr::Pubkey;
|
||||
use nostrdb::{Ndb, Transaction};
|
||||
use notedeck::{ImageCache, NotedeckTextStyle};
|
||||
use notedeck::{Images, NotedeckTextStyle};
|
||||
|
||||
pub struct NavTitle<'a> {
|
||||
ndb: &'a Ndb,
|
||||
img_cache: &'a mut ImageCache,
|
||||
img_cache: &'a mut Images,
|
||||
columns: &'a Columns,
|
||||
routes: &'a [Route],
|
||||
col_id: usize,
|
||||
@@ -29,7 +29,7 @@ pub struct NavTitle<'a> {
|
||||
impl<'a> NavTitle<'a> {
|
||||
pub fn new(
|
||||
ndb: &'a Ndb,
|
||||
img_cache: &'a mut ImageCache,
|
||||
img_cache: &'a mut Images,
|
||||
columns: &'a Columns,
|
||||
routes: &'a [Route],
|
||||
col_id: usize,
|
||||
|
||||
75
crates/notedeck_columns/src/ui/images.rs
Normal file
75
crates/notedeck_columns/src/ui/images.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
use notedeck::{GifStateMap, Images, MediaCache, MediaCacheType, TexturedImage};
|
||||
|
||||
use crate::images::ImageType;
|
||||
|
||||
use super::ProfilePic;
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn render_images(
|
||||
ui: &mut egui::Ui,
|
||||
images: &mut Images,
|
||||
url: &str,
|
||||
img_type: ImageType,
|
||||
cache_type: MediaCacheType,
|
||||
show_waiting: impl FnOnce(&mut egui::Ui),
|
||||
show_error: impl FnOnce(&mut egui::Ui, String),
|
||||
show_success: impl FnOnce(&mut egui::Ui, &str, &mut TexturedImage, &mut GifStateMap),
|
||||
) -> egui::Response {
|
||||
let cache = match cache_type {
|
||||
MediaCacheType::Image => &mut images.static_imgs,
|
||||
MediaCacheType::Gif => &mut images.gifs,
|
||||
};
|
||||
|
||||
render_media_cache(
|
||||
ui,
|
||||
cache,
|
||||
&mut images.gif_states,
|
||||
url,
|
||||
img_type,
|
||||
cache_type,
|
||||
show_waiting,
|
||||
show_error,
|
||||
show_success,
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_media_cache(
|
||||
ui: &mut egui::Ui,
|
||||
cache: &mut MediaCache,
|
||||
gif_states: &mut GifStateMap,
|
||||
url: &str,
|
||||
img_type: ImageType,
|
||||
cache_type: MediaCacheType,
|
||||
show_waiting: impl FnOnce(&mut egui::Ui),
|
||||
show_error: impl FnOnce(&mut egui::Ui, String),
|
||||
show_success: impl FnOnce(&mut egui::Ui, &str, &mut TexturedImage, &mut GifStateMap),
|
||||
) -> egui::Response {
|
||||
let m_cached_promise = cache.map().get(url);
|
||||
|
||||
if m_cached_promise.is_none() {
|
||||
let res = crate::images::fetch_img(cache, ui.ctx(), url, img_type, cache_type.clone());
|
||||
cache.map_mut().insert(url.to_owned(), res);
|
||||
}
|
||||
|
||||
egui::Frame::none()
|
||||
.show(ui, |ui| {
|
||||
match cache.map_mut().get_mut(url).and_then(|p| p.ready_mut()) {
|
||||
None => show_waiting(ui),
|
||||
Some(Err(err)) => {
|
||||
let err = err.to_string();
|
||||
let no_pfp = crate::images::fetch_img(
|
||||
cache,
|
||||
ui.ctx(),
|
||||
ProfilePic::no_pfp_url(),
|
||||
ImageType::Profile(128),
|
||||
cache_type,
|
||||
);
|
||||
cache.map_mut().insert(url.to_owned(), no_pfp);
|
||||
show_error(ui, err)
|
||||
}
|
||||
Some(Ok(renderable_media)) => show_success(ui, url, renderable_media, gif_states),
|
||||
}
|
||||
})
|
||||
.response
|
||||
}
|
||||
@@ -3,11 +3,11 @@ use crate::{actionbar::NoteAction, profile::get_display_name, timeline::Timeline
|
||||
use egui::Sense;
|
||||
use enostr::Pubkey;
|
||||
use nostrdb::{Ndb, Transaction};
|
||||
use notedeck::ImageCache;
|
||||
use notedeck::Images;
|
||||
|
||||
pub struct Mention<'a> {
|
||||
ndb: &'a Ndb,
|
||||
img_cache: &'a mut ImageCache,
|
||||
img_cache: &'a mut Images,
|
||||
txn: &'a Transaction,
|
||||
pk: &'a [u8; 32],
|
||||
selectable: bool,
|
||||
@@ -17,7 +17,7 @@ pub struct Mention<'a> {
|
||||
impl<'a> Mention<'a> {
|
||||
pub fn new(
|
||||
ndb: &'a Ndb,
|
||||
img_cache: &'a mut ImageCache,
|
||||
img_cache: &'a mut Images,
|
||||
txn: &'a Transaction,
|
||||
pk: &'a [u8; 32],
|
||||
) -> Self {
|
||||
@@ -62,9 +62,10 @@ impl egui::Widget for Mention<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn mention_ui(
|
||||
ndb: &Ndb,
|
||||
img_cache: &mut ImageCache,
|
||||
img_cache: &mut Images,
|
||||
txn: &Transaction,
|
||||
pk: &[u8; 32],
|
||||
ui: &mut egui::Ui,
|
||||
|
||||
@@ -5,6 +5,7 @@ pub mod anim;
|
||||
pub mod column;
|
||||
pub mod configure_deck;
|
||||
pub mod edit_deck;
|
||||
pub mod images;
|
||||
pub mod mention;
|
||||
pub mod note;
|
||||
pub mod preview;
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
use crate::gif::{handle_repaint, retrieve_latest_texture};
|
||||
use crate::ui::images::render_images;
|
||||
use crate::ui::{
|
||||
self,
|
||||
note::{NoteOptions, NoteResponse},
|
||||
ProfilePic,
|
||||
};
|
||||
use crate::{actionbar::NoteAction, images::ImageType, timeline::TimelineKind};
|
||||
use egui::{Color32, Hyperlink, Image, RichText};
|
||||
use nostrdb::{BlockType, Mention, Ndb, Note, NoteKey, Transaction};
|
||||
use tracing::warn;
|
||||
|
||||
use notedeck::{ImageCache, NoteCache};
|
||||
use notedeck::{supported_mime_hosted_at_url, Images, MediaCacheType, NoteCache};
|
||||
|
||||
pub struct NoteContents<'a> {
|
||||
ndb: &'a Ndb,
|
||||
img_cache: &'a mut ImageCache,
|
||||
img_cache: &'a mut Images,
|
||||
note_cache: &'a mut NoteCache,
|
||||
txn: &'a Transaction,
|
||||
note: &'a Note<'a>,
|
||||
@@ -22,9 +23,10 @@ pub struct NoteContents<'a> {
|
||||
}
|
||||
|
||||
impl<'a> NoteContents<'a> {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
ndb: &'a Ndb,
|
||||
img_cache: &'a mut ImageCache,
|
||||
img_cache: &'a mut Images,
|
||||
note_cache: &'a mut NoteCache,
|
||||
txn: &'a Transaction,
|
||||
note: &'a Note,
|
||||
@@ -72,7 +74,7 @@ pub fn render_note_preview(
|
||||
ui: &mut egui::Ui,
|
||||
ndb: &Ndb,
|
||||
note_cache: &mut NoteCache,
|
||||
img_cache: &mut ImageCache,
|
||||
img_cache: &mut Images,
|
||||
txn: &Transaction,
|
||||
id: &[u8; 32],
|
||||
parent: NoteKey,
|
||||
@@ -126,15 +128,11 @@ pub fn render_note_preview(
|
||||
.inner
|
||||
}
|
||||
|
||||
fn is_image_link(url: &str) -> bool {
|
||||
url.ends_with("png") || url.ends_with("jpg") || url.ends_with("jpeg")
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_note_contents(
|
||||
ui: &mut egui::Ui,
|
||||
ndb: &Ndb,
|
||||
img_cache: &mut ImageCache,
|
||||
img_cache: &mut Images,
|
||||
note_cache: &mut NoteCache,
|
||||
txn: &Transaction,
|
||||
note: &Note,
|
||||
@@ -145,7 +143,7 @@ fn render_note_contents(
|
||||
puffin::profile_function!();
|
||||
|
||||
let selectable = options.has_selectable_text();
|
||||
let mut images: Vec<String> = vec![];
|
||||
let mut images: Vec<(String, MediaCacheType)> = vec![];
|
||||
let mut note_action: Option<NoteAction> = None;
|
||||
let mut inline_note: Option<(&[u8; 32], &str)> = None;
|
||||
let hide_media = options.has_hide_media();
|
||||
@@ -211,9 +209,14 @@ fn render_note_contents(
|
||||
}
|
||||
|
||||
BlockType::Url => {
|
||||
let lower_url = block.as_str().to_lowercase();
|
||||
if !hide_media && is_image_link(&lower_url) {
|
||||
images.push(block.as_str().to_string());
|
||||
if !hide_media {
|
||||
let url = block.as_str().to_string();
|
||||
|
||||
if let Some(cache_type) =
|
||||
supported_mime_hosted_at_url(&mut img_cache.urls, &url)
|
||||
{
|
||||
images.push((url, cache_type));
|
||||
}
|
||||
} else {
|
||||
#[cfg(feature = "profiling")]
|
||||
puffin::profile_scope!("url contents");
|
||||
@@ -279,8 +282,8 @@ fn rot13(input: &str) -> String {
|
||||
|
||||
fn image_carousel(
|
||||
ui: &mut egui::Ui,
|
||||
img_cache: &mut ImageCache,
|
||||
images: Vec<String>,
|
||||
img_cache: &mut Images,
|
||||
images: Vec<(String, MediaCacheType)>,
|
||||
carousel_id: egui::Id,
|
||||
) {
|
||||
// let's make sure everything is within our area
|
||||
@@ -294,56 +297,39 @@ fn image_carousel(
|
||||
.id_salt(carousel_id)
|
||||
.show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
for image in images {
|
||||
// If the cache is empty, initiate the fetch
|
||||
let m_cached_promise = img_cache.map().get(&image);
|
||||
if m_cached_promise.is_none() {
|
||||
let res = crate::images::fetch_img(
|
||||
img_cache,
|
||||
ui.ctx(),
|
||||
&image,
|
||||
ImageType::Content(width.round() as u32, height.round() as u32),
|
||||
);
|
||||
img_cache.map_mut().insert(image.to_owned(), res);
|
||||
}
|
||||
|
||||
// What is the state of the fetch?
|
||||
match img_cache.map()[&image].ready() {
|
||||
// Still waiting
|
||||
None => {
|
||||
for (image, cache_type) in images {
|
||||
render_images(
|
||||
ui,
|
||||
img_cache,
|
||||
&image,
|
||||
ImageType::Content(width.round() as u32, height.round() as u32),
|
||||
cache_type,
|
||||
|ui| {
|
||||
ui.allocate_space(egui::vec2(spinsz, spinsz));
|
||||
//ui.add(egui::Spinner::new().size(spinsz));
|
||||
}
|
||||
// Failed to fetch image!
|
||||
Some(Err(_err)) => {
|
||||
// FIXME - use content-specific error instead
|
||||
let no_pfp = crate::images::fetch_img(
|
||||
img_cache,
|
||||
ui.ctx(),
|
||||
ProfilePic::no_pfp_url(),
|
||||
ImageType::Profile(128),
|
||||
},
|
||||
|ui, _| {
|
||||
ui.allocate_space(egui::vec2(spinsz, spinsz));
|
||||
},
|
||||
|ui, url, renderable_media, gifs| {
|
||||
let texture = handle_repaint(
|
||||
ui,
|
||||
retrieve_latest_texture(&image, gifs, renderable_media),
|
||||
);
|
||||
img_cache.map_mut().insert(image.to_owned(), no_pfp);
|
||||
// spin until next pass
|
||||
ui.allocate_space(egui::vec2(spinsz, spinsz));
|
||||
//ui.add(egui::Spinner::new().size(spinsz));
|
||||
}
|
||||
// Use the previously resolved image
|
||||
Some(Ok(img)) => {
|
||||
let img_resp = ui.add(
|
||||
Image::new(img)
|
||||
Image::new(texture)
|
||||
.max_height(height)
|
||||
.rounding(5.0)
|
||||
.fit_to_original_size(1.0),
|
||||
);
|
||||
|
||||
img_resp.context_menu(|ui| {
|
||||
if ui.button("Copy Link").clicked() {
|
||||
ui.ctx().copy_text(image);
|
||||
ui.ctx().copy_text(url.to_owned());
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
})
|
||||
.response
|
||||
|
||||
@@ -25,14 +25,14 @@ use egui::emath::{pos2, Vec2};
|
||||
use egui::{Id, Label, Pos2, Rect, Response, RichText, Sense};
|
||||
use enostr::{NoteId, Pubkey};
|
||||
use nostrdb::{Ndb, Note, NoteKey, Transaction};
|
||||
use notedeck::{CachedNote, ImageCache, NoteCache, NotedeckTextStyle};
|
||||
use notedeck::{CachedNote, Images, NoteCache, NotedeckTextStyle};
|
||||
|
||||
use super::profile::preview::one_line_display_name_widget;
|
||||
|
||||
pub struct NoteView<'a> {
|
||||
ndb: &'a Ndb,
|
||||
note_cache: &'a mut NoteCache,
|
||||
img_cache: &'a mut ImageCache,
|
||||
img_cache: &'a mut Images,
|
||||
parent: Option<NoteKey>,
|
||||
note: &'a nostrdb::Note<'a>,
|
||||
flags: NoteOptions,
|
||||
@@ -74,7 +74,7 @@ impl<'a> NoteView<'a> {
|
||||
pub fn new(
|
||||
ndb: &'a Ndb,
|
||||
note_cache: &'a mut NoteCache,
|
||||
img_cache: &'a mut ImageCache,
|
||||
img_cache: &'a mut Images,
|
||||
note: &'a nostrdb::Note<'a>,
|
||||
mut flags: NoteOptions,
|
||||
) -> Self {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use crate::draft::{Draft, Drafts, MentionHint};
|
||||
use crate::images::fetch_img;
|
||||
use crate::gif::{handle_repaint, retrieve_latest_texture};
|
||||
use crate::media_upload::{nostrbuild_nip96_upload, MediaPath};
|
||||
use crate::post::{downcast_post_buffer, MentionType, NewPost};
|
||||
use crate::profile::get_display_name;
|
||||
use crate::ui::images::render_images;
|
||||
use crate::ui::search_results::SearchResultsView;
|
||||
use crate::ui::{self, note::NoteOptions, Preview, PreviewConfig};
|
||||
use crate::Result;
|
||||
@@ -13,7 +14,7 @@ use egui::{vec2, Frame, Layout, Margin, Pos2, ScrollArea, Sense, TextBuffer};
|
||||
use enostr::{FilledKeypair, FullKeypair, NoteId, Pubkey, RelayPool};
|
||||
use nostrdb::{Ndb, Transaction};
|
||||
|
||||
use notedeck::{ImageCache, NoteCache};
|
||||
use notedeck::{supported_mime_hosted_at_url, Images, NoteCache};
|
||||
use tracing::error;
|
||||
|
||||
use super::contents::render_note_preview;
|
||||
@@ -22,7 +23,7 @@ pub struct PostView<'a> {
|
||||
ndb: &'a Ndb,
|
||||
draft: &'a mut Draft,
|
||||
post_type: PostType,
|
||||
img_cache: &'a mut ImageCache,
|
||||
img_cache: &'a mut Images,
|
||||
note_cache: &'a mut NoteCache,
|
||||
poster: FilledKeypair<'a>,
|
||||
id_source: Option<egui::Id>,
|
||||
@@ -88,7 +89,7 @@ impl<'a> PostView<'a> {
|
||||
ndb: &'a Ndb,
|
||||
draft: &'a mut Draft,
|
||||
post_type: PostType,
|
||||
img_cache: &'a mut ImageCache,
|
||||
img_cache: &'a mut Images,
|
||||
note_cache: &'a mut NoteCache,
|
||||
poster: FilledKeypair<'a>,
|
||||
inner_rect: egui::Rect,
|
||||
@@ -384,49 +385,59 @@ impl<'a> PostView<'a> {
|
||||
} else {
|
||||
(300, 300)
|
||||
};
|
||||
let m_cached_promise = self.img_cache.map().get(&media.url);
|
||||
if m_cached_promise.is_none() {
|
||||
let promise = fetch_img(
|
||||
|
||||
if let Some(cache_type) =
|
||||
supported_mime_hosted_at_url(&mut self.img_cache.urls, &media.url)
|
||||
{
|
||||
render_images(
|
||||
ui,
|
||||
self.img_cache,
|
||||
ui.ctx(),
|
||||
&media.url,
|
||||
crate::images::ImageType::Content(width, height),
|
||||
cache_type,
|
||||
|ui| {
|
||||
ui.spinner();
|
||||
},
|
||||
|_, e| {
|
||||
self.draft.upload_errors.push(e.to_string());
|
||||
error!("{e}");
|
||||
},
|
||||
|ui, url, renderable_media, gifs| {
|
||||
let media_size = vec2(width as f32, height as f32);
|
||||
let max_size = vec2(300.0, 300.0);
|
||||
let size = if media_size.x > max_size.x || media_size.y > max_size.y {
|
||||
max_size
|
||||
} else {
|
||||
media_size
|
||||
};
|
||||
|
||||
let texture_handle = handle_repaint(
|
||||
ui,
|
||||
retrieve_latest_texture(url, gifs, renderable_media),
|
||||
);
|
||||
let img_resp = ui.add(
|
||||
egui::Image::new(texture_handle)
|
||||
.max_size(size)
|
||||
.rounding(12.0),
|
||||
);
|
||||
|
||||
let remove_button_rect = {
|
||||
let top_left = img_resp.rect.left_top();
|
||||
let spacing = 13.0;
|
||||
let center = Pos2::new(top_left.x + spacing, top_left.y + spacing);
|
||||
egui::Rect::from_center_size(center, egui::vec2(26.0, 26.0))
|
||||
};
|
||||
if show_remove_upload_button(ui, remove_button_rect).clicked() {
|
||||
to_remove.push(i);
|
||||
}
|
||||
ui.advance_cursor_after_rect(img_resp.rect);
|
||||
},
|
||||
);
|
||||
self.img_cache
|
||||
.map_mut()
|
||||
.insert(media.url.to_owned(), promise);
|
||||
}
|
||||
|
||||
match self.img_cache.map()[&media.url].ready() {
|
||||
Some(Ok(texture)) => {
|
||||
let media_size = vec2(width as f32, height as f32);
|
||||
let max_size = vec2(300.0, 300.0);
|
||||
let size = if media_size.x > max_size.x || media_size.y > max_size.y {
|
||||
max_size
|
||||
} else {
|
||||
media_size
|
||||
};
|
||||
|
||||
let img_resp = ui.add(egui::Image::new(texture).max_size(size).rounding(12.0));
|
||||
|
||||
let remove_button_rect = {
|
||||
let top_left = img_resp.rect.left_top();
|
||||
let spacing = 13.0;
|
||||
let center = Pos2::new(top_left.x + spacing, top_left.y + spacing);
|
||||
egui::Rect::from_center_size(center, egui::vec2(26.0, 26.0))
|
||||
};
|
||||
if show_remove_upload_button(ui, remove_button_rect).clicked() {
|
||||
to_remove.push(i);
|
||||
}
|
||||
ui.advance_cursor_after_rect(img_resp.rect);
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
self.draft.upload_errors.push(e.to_string());
|
||||
error!("{e}");
|
||||
}
|
||||
None => {
|
||||
ui.spinner();
|
||||
}
|
||||
} else {
|
||||
self.draft
|
||||
.upload_errors
|
||||
.push("Uploaded media is not supported.".to_owned());
|
||||
error!("Unsupported mime type at url: {}", &media.url);
|
||||
}
|
||||
}
|
||||
to_remove.reverse();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use enostr::{FilledKeypair, NoteId};
|
||||
use nostrdb::Ndb;
|
||||
use notedeck::{ImageCache, NoteCache};
|
||||
use notedeck::{Images, NoteCache};
|
||||
|
||||
use crate::{
|
||||
draft::Draft,
|
||||
@@ -13,7 +13,7 @@ pub struct QuoteRepostView<'a> {
|
||||
ndb: &'a Ndb,
|
||||
poster: FilledKeypair<'a>,
|
||||
note_cache: &'a mut NoteCache,
|
||||
img_cache: &'a mut ImageCache,
|
||||
img_cache: &'a mut Images,
|
||||
draft: &'a mut Draft,
|
||||
quoting_note: &'a nostrdb::Note<'a>,
|
||||
id_source: Option<egui::Id>,
|
||||
@@ -27,7 +27,7 @@ impl<'a> QuoteRepostView<'a> {
|
||||
ndb: &'a Ndb,
|
||||
poster: FilledKeypair<'a>,
|
||||
note_cache: &'a mut NoteCache,
|
||||
img_cache: &'a mut ImageCache,
|
||||
img_cache: &'a mut Images,
|
||||
draft: &'a mut Draft,
|
||||
quoting_note: &'a nostrdb::Note<'a>,
|
||||
inner_rect: egui::Rect,
|
||||
|
||||
@@ -4,13 +4,13 @@ use crate::ui::note::{NoteOptions, PostResponse, PostType};
|
||||
use enostr::{FilledKeypair, NoteId};
|
||||
use nostrdb::Ndb;
|
||||
|
||||
use notedeck::{ImageCache, NoteCache};
|
||||
use notedeck::{Images, NoteCache};
|
||||
|
||||
pub struct PostReplyView<'a> {
|
||||
ndb: &'a Ndb,
|
||||
poster: FilledKeypair<'a>,
|
||||
note_cache: &'a mut NoteCache,
|
||||
img_cache: &'a mut ImageCache,
|
||||
img_cache: &'a mut Images,
|
||||
draft: &'a mut Draft,
|
||||
note: &'a nostrdb::Note<'a>,
|
||||
id_source: Option<egui::Id>,
|
||||
@@ -25,7 +25,7 @@ impl<'a> PostReplyView<'a> {
|
||||
poster: FilledKeypair<'a>,
|
||||
draft: &'a mut Draft,
|
||||
note_cache: &'a mut NoteCache,
|
||||
img_cache: &'a mut ImageCache,
|
||||
img_cache: &'a mut Images,
|
||||
note: &'a nostrdb::Note<'a>,
|
||||
inner_rect: egui::Rect,
|
||||
note_options: NoteOptions,
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::{
|
||||
};
|
||||
use egui::{Label, RichText, Sense};
|
||||
use nostrdb::{Ndb, Note, NoteReply, Transaction};
|
||||
use notedeck::{ImageCache, NoteCache};
|
||||
use notedeck::{Images, NoteCache};
|
||||
|
||||
#[must_use = "Please handle the resulting note action"]
|
||||
pub fn reply_desc(
|
||||
@@ -12,7 +12,7 @@ pub fn reply_desc(
|
||||
txn: &Transaction,
|
||||
note_reply: &NoteReply,
|
||||
ndb: &Ndb,
|
||||
img_cache: &mut ImageCache,
|
||||
img_cache: &mut Images,
|
||||
note_cache: &mut NoteCache,
|
||||
note_options: NoteOptions,
|
||||
) -> Option<NoteAction> {
|
||||
@@ -29,7 +29,7 @@ pub fn reply_desc(
|
||||
// note link renderer helper
|
||||
let note_link = |ui: &mut egui::Ui,
|
||||
note_cache: &mut NoteCache,
|
||||
img_cache: &mut ImageCache,
|
||||
img_cache: &mut Images,
|
||||
text: &str,
|
||||
note: &Note<'_>| {
|
||||
let r = ui.add(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use core::f32;
|
||||
|
||||
use egui::{vec2, Button, Layout, Margin, RichText, Rounding, ScrollArea, TextEdit};
|
||||
use notedeck::{ImageCache, NotedeckTextStyle};
|
||||
use notedeck::{Images, NotedeckTextStyle};
|
||||
|
||||
use crate::{colors, profile_state::ProfileState};
|
||||
|
||||
@@ -9,11 +9,11 @@ use super::{banner, unwrap_profile_url, ProfilePic};
|
||||
|
||||
pub struct EditProfileView<'a> {
|
||||
state: &'a mut ProfileState,
|
||||
img_cache: &'a mut ImageCache,
|
||||
img_cache: &'a mut Images,
|
||||
}
|
||||
|
||||
impl<'a> EditProfileView<'a> {
|
||||
pub fn new(state: &'a mut ProfileState, img_cache: &'a mut ImageCache) -> Self {
|
||||
pub fn new(state: &'a mut ProfileState, img_cache: &'a mut Images) -> Self {
|
||||
Self { state, img_cache }
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ use crate::{
|
||||
NostrName,
|
||||
};
|
||||
|
||||
use notedeck::{Accounts, ImageCache, MuteFun, NoteCache, NotedeckTextStyle, UnknownIds};
|
||||
use notedeck::{Accounts, Images, MuteFun, NoteCache, NotedeckTextStyle, UnknownIds};
|
||||
|
||||
pub struct ProfileView<'a> {
|
||||
pubkey: &'a Pubkey,
|
||||
@@ -33,7 +33,7 @@ pub struct ProfileView<'a> {
|
||||
note_options: NoteOptions,
|
||||
ndb: &'a Ndb,
|
||||
note_cache: &'a mut NoteCache,
|
||||
img_cache: &'a mut ImageCache,
|
||||
img_cache: &'a mut Images,
|
||||
unknown_ids: &'a mut UnknownIds,
|
||||
is_muted: &'a MuteFun,
|
||||
}
|
||||
@@ -52,7 +52,7 @@ impl<'a> ProfileView<'a> {
|
||||
timeline_cache: &'a mut TimelineCache,
|
||||
ndb: &'a Ndb,
|
||||
note_cache: &'a mut NoteCache,
|
||||
img_cache: &'a mut ImageCache,
|
||||
img_cache: &'a mut Images,
|
||||
unknown_ids: &'a mut UnknownIds,
|
||||
is_muted: &'a MuteFun,
|
||||
note_options: NoteOptions,
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
use crate::gif::{handle_repaint, retrieve_latest_texture};
|
||||
use crate::images::ImageType;
|
||||
use crate::ui::images::render_images;
|
||||
use crate::ui::{Preview, PreviewConfig};
|
||||
use egui::{vec2, Sense, Stroke, TextureHandle};
|
||||
use nostrdb::{Ndb, Transaction};
|
||||
use tracing::info;
|
||||
|
||||
use notedeck::{AppContext, ImageCache};
|
||||
use notedeck::{supported_mime_hosted_at_url, AppContext, Images};
|
||||
|
||||
pub struct ProfilePic<'cache, 'url> {
|
||||
cache: &'cache mut ImageCache,
|
||||
cache: &'cache mut Images,
|
||||
url: &'url str,
|
||||
size: f32,
|
||||
border: Option<Stroke>,
|
||||
@@ -20,7 +22,7 @@ impl egui::Widget for ProfilePic<'_, '_> {
|
||||
}
|
||||
|
||||
impl<'cache, 'url> ProfilePic<'cache, 'url> {
|
||||
pub fn new(cache: &'cache mut ImageCache, url: &'url str) -> Self {
|
||||
pub fn new(cache: &'cache mut Images, url: &'url str) -> Self {
|
||||
let size = Self::default_size();
|
||||
ProfilePic {
|
||||
cache,
|
||||
@@ -35,7 +37,7 @@ impl<'cache, 'url> ProfilePic<'cache, 'url> {
|
||||
}
|
||||
|
||||
pub fn from_profile(
|
||||
cache: &'cache mut ImageCache,
|
||||
cache: &'cache mut Images,
|
||||
profile: &nostrdb::ProfileRecord<'url>,
|
||||
) -> Option<Self> {
|
||||
profile
|
||||
@@ -80,7 +82,7 @@ impl<'cache, 'url> ProfilePic<'cache, 'url> {
|
||||
|
||||
fn render_pfp(
|
||||
ui: &mut egui::Ui,
|
||||
img_cache: &mut ImageCache,
|
||||
img_cache: &mut Images,
|
||||
url: &str,
|
||||
ui_size: f32,
|
||||
border: Option<Stroke>,
|
||||
@@ -91,39 +93,27 @@ fn render_pfp(
|
||||
// We will want to downsample these so it's not blurry on hi res displays
|
||||
let img_size = 128u32;
|
||||
|
||||
let m_cached_promise = img_cache.map().get(url);
|
||||
if m_cached_promise.is_none() {
|
||||
let res = crate::images::fetch_img(img_cache, ui.ctx(), url, ImageType::Profile(img_size));
|
||||
img_cache.map_mut().insert(url.to_owned(), res);
|
||||
}
|
||||
let cache_type = supported_mime_hosted_at_url(&mut img_cache.urls, url)
|
||||
.unwrap_or(notedeck::MediaCacheType::Image);
|
||||
|
||||
match img_cache.map()[url].ready() {
|
||||
None => paint_circle(ui, ui_size, border),
|
||||
|
||||
// Failed to fetch profile!
|
||||
Some(Err(_err)) => {
|
||||
let m_failed_promise = img_cache.map().get(url);
|
||||
if m_failed_promise.is_none() {
|
||||
let no_pfp = crate::images::fetch_img(
|
||||
img_cache,
|
||||
ui.ctx(),
|
||||
ProfilePic::no_pfp_url(),
|
||||
ImageType::Profile(img_size),
|
||||
);
|
||||
img_cache.map_mut().insert(url.to_owned(), no_pfp);
|
||||
}
|
||||
|
||||
match img_cache.map().get(url).unwrap().ready() {
|
||||
None => paint_circle(ui, ui_size, border),
|
||||
Some(Err(_e)) => {
|
||||
//error!("Image load error: {:?}", e);
|
||||
paint_circle(ui, ui_size, border)
|
||||
}
|
||||
Some(Ok(img)) => pfp_image(ui, img, ui_size, border),
|
||||
}
|
||||
}
|
||||
Some(Ok(img)) => pfp_image(ui, img, ui_size, border),
|
||||
}
|
||||
render_images(
|
||||
ui,
|
||||
img_cache,
|
||||
url,
|
||||
ImageType::Profile(img_size),
|
||||
cache_type,
|
||||
|ui| {
|
||||
paint_circle(ui, ui_size, border);
|
||||
},
|
||||
|ui, _| {
|
||||
paint_circle(ui, ui_size, border);
|
||||
},
|
||||
|ui, url, renderable_media, gifs| {
|
||||
let texture_handle =
|
||||
handle_repaint(ui, retrieve_latest_texture(url, gifs, renderable_media));
|
||||
pfp_image(ui, texture_handle, ui_size, border);
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn pfp_image(
|
||||
|
||||
@@ -4,18 +4,18 @@ use egui::{Frame, Label, RichText, Widget};
|
||||
use egui_extras::Size;
|
||||
use nostrdb::ProfileRecord;
|
||||
|
||||
use notedeck::{ImageCache, NotedeckTextStyle, UserAccount};
|
||||
use notedeck::{Images, NotedeckTextStyle, UserAccount};
|
||||
|
||||
use super::{about_section_widget, banner, display_name_widget, get_display_name, get_profile_url};
|
||||
|
||||
pub struct ProfilePreview<'a, 'cache> {
|
||||
profile: &'a ProfileRecord<'a>,
|
||||
cache: &'cache mut ImageCache,
|
||||
cache: &'cache mut Images,
|
||||
banner_height: Size,
|
||||
}
|
||||
|
||||
impl<'a, 'cache> ProfilePreview<'a, 'cache> {
|
||||
pub fn new(profile: &'a ProfileRecord<'a>, cache: &'cache mut ImageCache) -> Self {
|
||||
pub fn new(profile: &'a ProfileRecord<'a>, cache: &'cache mut Images) -> Self {
|
||||
let banner_height = Size::exact(80.0);
|
||||
ProfilePreview {
|
||||
profile,
|
||||
@@ -69,14 +69,14 @@ impl egui::Widget for ProfilePreview<'_, '_> {
|
||||
|
||||
pub struct SimpleProfilePreview<'a, 'cache> {
|
||||
profile: Option<&'a ProfileRecord<'a>>,
|
||||
cache: &'cache mut ImageCache,
|
||||
cache: &'cache mut Images,
|
||||
is_nsec: bool,
|
||||
}
|
||||
|
||||
impl<'a, 'cache> SimpleProfilePreview<'a, 'cache> {
|
||||
pub fn new(
|
||||
profile: Option<&'a ProfileRecord<'a>>,
|
||||
cache: &'cache mut ImageCache,
|
||||
cache: &'cache mut Images,
|
||||
is_nsec: bool,
|
||||
) -> Self {
|
||||
SimpleProfilePreview {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use egui::{vec2, FontId, Pos2, Rect, ScrollArea, Vec2b};
|
||||
use nostrdb::{Ndb, ProfileRecord, Transaction};
|
||||
use notedeck::{fonts::get_font_size, ImageCache, NotedeckTextStyle};
|
||||
use notedeck::{fonts::get_font_size, Images, NotedeckTextStyle};
|
||||
use tracing::error;
|
||||
|
||||
use crate::{
|
||||
@@ -13,13 +13,13 @@ use super::{profile::get_profile_url, ProfilePic};
|
||||
pub struct SearchResultsView<'a> {
|
||||
ndb: &'a Ndb,
|
||||
txn: &'a Transaction,
|
||||
img_cache: &'a mut ImageCache,
|
||||
img_cache: &'a mut Images,
|
||||
results: &'a Vec<&'a [u8; 32]>,
|
||||
}
|
||||
|
||||
impl<'a> SearchResultsView<'a> {
|
||||
pub fn new(
|
||||
img_cache: &'a mut ImageCache,
|
||||
img_cache: &'a mut Images,
|
||||
ndb: &'a Ndb,
|
||||
txn: &'a Transaction,
|
||||
results: &'a Vec<&'a [u8; 32]>,
|
||||
@@ -84,7 +84,7 @@ impl<'a> SearchResultsView<'a> {
|
||||
|
||||
fn user_result<'a>(
|
||||
profile: &'a ProfileRecord<'_>,
|
||||
cache: &'a mut ImageCache,
|
||||
cache: &'a mut Images,
|
||||
index: usize,
|
||||
width: f32,
|
||||
) -> impl egui::Widget + 'a {
|
||||
|
||||
@@ -15,7 +15,7 @@ use crate::{
|
||||
support::Support,
|
||||
};
|
||||
|
||||
use notedeck::{Accounts, ImageCache, NotedeckTextStyle, ThemeHandler, UserAccount};
|
||||
use notedeck::{Accounts, Images, NotedeckTextStyle, ThemeHandler, UserAccount};
|
||||
|
||||
use super::{
|
||||
anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE},
|
||||
@@ -29,7 +29,7 @@ static ICON_WIDTH: f32 = 40.0;
|
||||
|
||||
pub struct DesktopSidePanel<'a> {
|
||||
ndb: &'a nostrdb::Ndb,
|
||||
img_cache: &'a mut ImageCache,
|
||||
img_cache: &'a mut Images,
|
||||
selected_account: Option<&'a UserAccount>,
|
||||
decks_cache: &'a DecksCache,
|
||||
}
|
||||
@@ -70,7 +70,7 @@ impl SidePanelResponse {
|
||||
impl<'a> DesktopSidePanel<'a> {
|
||||
pub fn new(
|
||||
ndb: &'a nostrdb::Ndb,
|
||||
img_cache: &'a mut ImageCache,
|
||||
img_cache: &'a mut Images,
|
||||
selected_account: Option<&'a UserAccount>,
|
||||
decks_cache: &'a DecksCache,
|
||||
) -> Self {
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::{
|
||||
};
|
||||
|
||||
use nostrdb::{Ndb, Transaction};
|
||||
use notedeck::{ImageCache, MuteFun, NoteCache, RootNoteId, UnknownIds};
|
||||
use notedeck::{Images, MuteFun, NoteCache, RootNoteId, UnknownIds};
|
||||
use tracing::error;
|
||||
|
||||
use super::timeline::TimelineTabView;
|
||||
@@ -15,7 +15,7 @@ pub struct ThreadView<'a> {
|
||||
ndb: &'a Ndb,
|
||||
note_cache: &'a mut NoteCache,
|
||||
unknown_ids: &'a mut UnknownIds,
|
||||
img_cache: &'a mut ImageCache,
|
||||
img_cache: &'a mut Images,
|
||||
selected_note_id: &'a [u8; 32],
|
||||
note_options: NoteOptions,
|
||||
id_source: egui::Id,
|
||||
@@ -29,7 +29,7 @@ impl<'a> ThreadView<'a> {
|
||||
ndb: &'a Ndb,
|
||||
note_cache: &'a mut NoteCache,
|
||||
unknown_ids: &'a mut UnknownIds,
|
||||
img_cache: &'a mut ImageCache,
|
||||
img_cache: &'a mut Images,
|
||||
selected_note_id: &'a [u8; 32],
|
||||
note_options: NoteOptions,
|
||||
is_muted: &'a MuteFun,
|
||||
|
||||
@@ -12,7 +12,7 @@ use egui::{vec2, Direction, Layout, Pos2, Stroke};
|
||||
use egui_tabs::TabColor;
|
||||
use nostrdb::{Ndb, Transaction};
|
||||
use notedeck::note::root_note_id_from_selected_id;
|
||||
use notedeck::{ImageCache, MuteFun, NoteCache};
|
||||
use notedeck::{Images, MuteFun, NoteCache};
|
||||
use tracing::{error, warn};
|
||||
|
||||
use super::anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE};
|
||||
@@ -22,19 +22,20 @@ pub struct TimelineView<'a> {
|
||||
timeline_cache: &'a mut TimelineCache,
|
||||
ndb: &'a Ndb,
|
||||
note_cache: &'a mut NoteCache,
|
||||
img_cache: &'a mut ImageCache,
|
||||
img_cache: &'a mut Images,
|
||||
note_options: NoteOptions,
|
||||
reverse: bool,
|
||||
is_muted: &'a MuteFun,
|
||||
}
|
||||
|
||||
impl<'a> TimelineView<'a> {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
timeline_id: &'a TimelineKind,
|
||||
timeline_cache: &'a mut TimelineCache,
|
||||
ndb: &'a Ndb,
|
||||
note_cache: &'a mut NoteCache,
|
||||
img_cache: &'a mut ImageCache,
|
||||
img_cache: &'a mut Images,
|
||||
note_options: NoteOptions,
|
||||
is_muted: &'a MuteFun,
|
||||
) -> TimelineView<'a> {
|
||||
@@ -78,7 +79,7 @@ fn timeline_ui(
|
||||
timeline_id: &TimelineKind,
|
||||
timeline_cache: &mut TimelineCache,
|
||||
note_cache: &mut NoteCache,
|
||||
img_cache: &mut ImageCache,
|
||||
img_cache: &mut Images,
|
||||
reversed: bool,
|
||||
note_options: NoteOptions,
|
||||
is_muted: &MuteFun,
|
||||
@@ -321,7 +322,7 @@ pub struct TimelineTabView<'a> {
|
||||
txn: &'a Transaction,
|
||||
ndb: &'a Ndb,
|
||||
note_cache: &'a mut NoteCache,
|
||||
img_cache: &'a mut ImageCache,
|
||||
img_cache: &'a mut Images,
|
||||
is_muted: &'a MuteFun,
|
||||
}
|
||||
|
||||
@@ -334,7 +335,7 @@ impl<'a> TimelineTabView<'a> {
|
||||
txn: &'a Transaction,
|
||||
ndb: &'a Ndb,
|
||||
note_cache: &'a mut NoteCache,
|
||||
img_cache: &'a mut ImageCache,
|
||||
img_cache: &'a mut Images,
|
||||
is_muted: &'a MuteFun,
|
||||
) -> Self {
|
||||
Self {
|
||||
|
||||
Reference in New Issue
Block a user