@@ -1,6 +1,7 @@
|
|||||||
use crate::urls::{UrlCache, UrlMimes};
|
use crate::urls::{UrlCache, UrlMimes};
|
||||||
use crate::Result;
|
use crate::Result;
|
||||||
use egui::TextureHandle;
|
use egui::TextureHandle;
|
||||||
|
use image::{Delay, Frame};
|
||||||
use poll_promise::Promise;
|
use poll_promise::Promise;
|
||||||
|
|
||||||
use egui::ColorImage;
|
use egui::ColorImage;
|
||||||
@@ -80,31 +81,8 @@ impl MediaCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
pub fn fetch(image: &str) -> Result<Image> {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
pub fn write(cache_dir: &path::Path, url: &str, data: ColorImage) -> Result<()> {
|
pub fn write(cache_dir: &path::Path, url: &str, data: ColorImage) -> Result<()> {
|
||||||
let file_path = cache_dir.join(Self::key(url));
|
let file = Self::create_file(cache_dir, url)?;
|
||||||
if let Some(p) = file_path.parent() {
|
|
||||||
create_dir_all(p)?;
|
|
||||||
}
|
|
||||||
let file = File::options()
|
|
||||||
.write(true)
|
|
||||||
.create(true)
|
|
||||||
.truncate(true)
|
|
||||||
.open(file_path)?;
|
|
||||||
let encoder = image::codecs::webp::WebPEncoder::new_lossless(file);
|
let encoder = image::codecs::webp::WebPEncoder::new_lossless(file);
|
||||||
|
|
||||||
encoder.encode(
|
encoder.encode(
|
||||||
@@ -117,6 +95,33 @@ impl MediaCache {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_file(cache_dir: &path::Path, url: &str) -> Result<File> {
|
||||||
|
let file_path = cache_dir.join(Self::key(url));
|
||||||
|
if let Some(p) = file_path.parent() {
|
||||||
|
create_dir_all(p)?;
|
||||||
|
}
|
||||||
|
Ok(File::options()
|
||||||
|
.write(true)
|
||||||
|
.create(true)
|
||||||
|
.truncate(true)
|
||||||
|
.open(file_path)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_gif(cache_dir: &path::Path, url: &str, data: Vec<ImageFrame>) -> Result<()> {
|
||||||
|
let file = Self::create_file(cache_dir, url)?;
|
||||||
|
|
||||||
|
let mut encoder = image::codecs::gif::GifEncoder::new(file);
|
||||||
|
for img in data {
|
||||||
|
let buf = color_image_to_rgba(img.image);
|
||||||
|
let frame = Frame::from_parts(buf, 0, 0, Delay::from_saturating_duration(img.delay));
|
||||||
|
if let Err(e) = encoder.encode_frame(frame) {
|
||||||
|
tracing::error!("problem encoding frame: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn key(url: &str) -> String {
|
pub fn key(url: &str) -> String {
|
||||||
let k: String = sha2::Sha256::digest(url.as_bytes()).encode_hex();
|
let k: String = sha2::Sha256::digest(url.as_bytes()).encode_hex();
|
||||||
PathBuf::from(&k[0..2])
|
PathBuf::from(&k[0..2])
|
||||||
@@ -174,12 +179,18 @@ impl MediaCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: temporary...
|
fn color_image_to_rgba(color_image: ColorImage) -> image::RgbaImage {
|
||||||
pub fn get_texture(textured_image: &TexturedImage) -> &TextureHandle {
|
let width = color_image.width() as u32;
|
||||||
match textured_image {
|
let height = color_image.height() as u32;
|
||||||
TexturedImage::Static(texture_handle) => texture_handle,
|
|
||||||
TexturedImage::Animated(_animation) => todo!(), // Temporary...
|
let rgba_pixels: Vec<u8> = color_image
|
||||||
}
|
.pixels
|
||||||
|
.iter()
|
||||||
|
.flat_map(|color| color.to_array()) // Convert Color32 to `[u8; 4]`
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
image::RgbaImage::from_raw(width, height, rgba_pixels)
|
||||||
|
.expect("Failed to create RgbaImage from ColorImage")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Images {
|
pub struct Images {
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ pub use error::{Error, FilterError};
|
|||||||
pub use filter::{FilterState, FilterStates, UnifiedSubscription};
|
pub use filter::{FilterState, FilterStates, UnifiedSubscription};
|
||||||
pub use fonts::NamedFontFamily;
|
pub use fonts::NamedFontFamily;
|
||||||
pub use imgcache::{
|
pub use imgcache::{
|
||||||
get_texture, GifState, GifStateMap, Images, MediaCache, MediaCacheType, TexturedImage,
|
Animation, GifState, GifStateMap, ImageFrame, Images, MediaCache, MediaCacheType,
|
||||||
|
MediaCacheValue, TextureFrame, TexturedImage,
|
||||||
};
|
};
|
||||||
pub use muted::{MuteFun, Muted};
|
pub use muted::{MuteFun, Muted};
|
||||||
pub use note::{NoteRef, RootIdError, RootNoteId, RootNoteIdBuf};
|
pub use note::{NoteRef, RootIdError, RootNoteId, RootNoteIdBuf};
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use egui::TextBuffer;
|
|||||||
use poll_promise::Promise;
|
use poll_promise::Promise;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::Error;
|
use crate::{Error, MediaCacheType};
|
||||||
|
|
||||||
const FILE_NAME: &str = "urls.bin";
|
const FILE_NAME: &str = "urls.bin";
|
||||||
const SAVE_INTERVAL: Duration = Duration::from_secs(60);
|
const SAVE_INTERVAL: Duration = Duration::from_secs(60);
|
||||||
@@ -230,10 +230,26 @@ impl SupportedMimeType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn from_mime(mime: mime_guess::mime::Mime) -> Result<Self, Error> {
|
||||||
|
if is_mime_supported(&mime) {
|
||||||
|
Ok(Self { mime })
|
||||||
|
} else {
|
||||||
|
Err(Error::Generic("Unsupported mime type".to_owned()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
pub fn to_mime(&self) -> &str {
|
pub fn to_mime(&self) -> &str {
|
||||||
self.mime.essence_str()
|
self.mime.essence_str()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn to_cache_type(&self) -> MediaCacheType {
|
||||||
|
if self.mime == mime_guess::mime::IMAGE_GIF {
|
||||||
|
MediaCacheType::Gif
|
||||||
|
} else {
|
||||||
|
MediaCacheType::Image
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_mime_supported(mime: &mime_guess::Mime) -> bool {
|
fn is_mime_supported(mime: &mime_guess::Mime) -> bool {
|
||||||
@@ -248,8 +264,8 @@ fn url_has_supported_mime(url: &str) -> MimeHostedAtUrl {
|
|||||||
.extension()
|
.extension()
|
||||||
.and_then(|ext| ext.to_str())
|
.and_then(|ext| ext.to_str())
|
||||||
{
|
{
|
||||||
if SupportedMimeType::from_extension(ext).is_ok() {
|
if let Ok(supported) = SupportedMimeType::from_extension(ext) {
|
||||||
return MimeHostedAtUrl::Yes;
|
return MimeHostedAtUrl::Yes(supported.to_cache_type());
|
||||||
} else {
|
} else {
|
||||||
return MimeHostedAtUrl::No;
|
return MimeHostedAtUrl::No;
|
||||||
}
|
}
|
||||||
@@ -260,21 +276,23 @@ fn url_has_supported_mime(url: &str) -> MimeHostedAtUrl {
|
|||||||
MimeHostedAtUrl::Maybe
|
MimeHostedAtUrl::Maybe
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn supported_mime_hosted_at_url(urls: &mut UrlMimes, url: &str) -> bool {
|
pub fn supported_mime_hosted_at_url(urls: &mut UrlMimes, url: &str) -> Option<MediaCacheType> {
|
||||||
match url_has_supported_mime(url) {
|
match url_has_supported_mime(url) {
|
||||||
MimeHostedAtUrl::Yes => true,
|
MimeHostedAtUrl::Yes(cache_type) => Some(cache_type),
|
||||||
MimeHostedAtUrl::Maybe => urls
|
MimeHostedAtUrl::Maybe => urls
|
||||||
.get(url)
|
.get(url)
|
||||||
.and_then(|s| s.parse::<mime_guess::mime::Mime>().ok())
|
.and_then(|s| s.parse::<mime_guess::mime::Mime>().ok())
|
||||||
.map_or(false, |mime: mime_guess::mime::Mime| {
|
.and_then(|mime: mime_guess::mime::Mime| {
|
||||||
is_mime_supported(&mime)
|
SupportedMimeType::from_mime(mime)
|
||||||
|
.ok()
|
||||||
|
.map(|s| s.to_cache_type())
|
||||||
}),
|
}),
|
||||||
MimeHostedAtUrl::No => false,
|
MimeHostedAtUrl::No => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum MimeHostedAtUrl {
|
enum MimeHostedAtUrl {
|
||||||
Yes,
|
Yes(MediaCacheType),
|
||||||
Maybe,
|
Maybe,
|
||||||
No,
|
No,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,28 @@
|
|||||||
use egui::{pos2, Color32, ColorImage, Rect, Sense, SizeHint};
|
use egui::{pos2, Color32, ColorImage, Rect, Sense, SizeHint};
|
||||||
|
use image::codecs::gif::GifDecoder;
|
||||||
use image::imageops::FilterType;
|
use image::imageops::FilterType;
|
||||||
|
use image::AnimationDecoder;
|
||||||
|
use image::DynamicImage;
|
||||||
|
use image::FlatSamples;
|
||||||
|
use image::Frame;
|
||||||
|
use notedeck::Animation;
|
||||||
|
use notedeck::ImageFrame;
|
||||||
use notedeck::MediaCache;
|
use notedeck::MediaCache;
|
||||||
|
use notedeck::MediaCacheType;
|
||||||
use notedeck::Result;
|
use notedeck::Result;
|
||||||
|
use notedeck::TextureFrame;
|
||||||
use notedeck::TexturedImage;
|
use notedeck::TexturedImage;
|
||||||
use poll_promise::Promise;
|
use poll_promise::Promise;
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
use std::io::Cursor;
|
||||||
use std::path;
|
use std::path;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use std::sync::mpsc;
|
||||||
|
use std::sync::mpsc::SyncSender;
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
|
|
||||||
//pub type ImageCacheKey = String;
|
|
||||||
//pub type ImageCacheValue = Promise<Result<TextureHandle>>;
|
|
||||||
//pub type MediaCache = HashMap<String, ImageCacheValue>;
|
|
||||||
|
|
||||||
// NOTE(jb55): chatgpt wrote this because I was too dumb to
|
// NOTE(jb55): chatgpt wrote this because I was too dumb to
|
||||||
pub fn aspect_fill(
|
pub fn aspect_fill(
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
@@ -103,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")]
|
#[cfg(feature = "profiling")]
|
||||||
puffin::profile_function!();
|
puffin::profile_function!();
|
||||||
|
|
||||||
@@ -126,10 +137,10 @@ fn process_pfp_bitmap(imgtyp: ImageType, image: &mut image::DynamicImage) -> Col
|
|||||||
|
|
||||||
if image.width() > smaller {
|
if image.width() > smaller {
|
||||||
let excess = 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 {
|
} else if image.height() > smaller {
|
||||||
let excess = 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 = image.resize(size, size, FilterType::CatmullRom); // DynamicImage
|
||||||
let image_buffer = image.into_rgba8(); // RgbaImage (ImageBuffer)
|
let image_buffer = image.into_rgba8(); // RgbaImage (ImageBuffer)
|
||||||
@@ -167,8 +178,8 @@ fn parse_img_response(response: ehttp::Response, imgtyp: ImageType) -> Result<Co
|
|||||||
} else if content_type.starts_with("image/") {
|
} else if content_type.starts_with("image/") {
|
||||||
#[cfg(feature = "profiling")]
|
#[cfg(feature = "profiling")]
|
||||||
puffin::profile_scope!("load_from_memory");
|
puffin::profile_scope!("load_from_memory");
|
||||||
let mut dyn_image = image::load_from_memory(&response.bytes)?;
|
let dyn_image = image::load_from_memory(&response.bytes)?;
|
||||||
Ok(process_pfp_bitmap(imgtyp, &mut dyn_image))
|
Ok(process_pfp_bitmap(imgtyp, dyn_image))
|
||||||
} else {
|
} else {
|
||||||
Err(format!("Expected image, found content-type {:?}", content_type).into())
|
Err(format!("Expected image, found content-type {:?}", content_type).into())
|
||||||
}
|
}
|
||||||
@@ -178,32 +189,162 @@ fn fetch_img_from_disk(
|
|||||||
ctx: &egui::Context,
|
ctx: &egui::Context,
|
||||||
url: &str,
|
url: &str,
|
||||||
path: &path::Path,
|
path: &path::Path,
|
||||||
|
cache_type: MediaCacheType,
|
||||||
) -> Promise<Result<TexturedImage>> {
|
) -> Promise<Result<TexturedImage>> {
|
||||||
let ctx = ctx.clone();
|
let ctx = ctx.clone();
|
||||||
let url = url.to_owned();
|
let url = url.to_owned();
|
||||||
let path = path.to_owned();
|
let path = path.to_owned();
|
||||||
Promise::spawn_async(async move {
|
Promise::spawn_async(async move {
|
||||||
let data = fs::read(path).await?;
|
match cache_type {
|
||||||
let image_buffer = image::load_from_memory(&data).map_err(notedeck::Error::Image)?;
|
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 img = buffer_to_color_image(
|
||||||
let flat_samples = image_buffer.as_flat_samples_u8().unwrap();
|
image_buffer.as_flat_samples_u8(),
|
||||||
let img = ColorImage::from_rgba_unmultiplied(
|
image_buffer.width(),
|
||||||
[
|
image_buffer.height(),
|
||||||
image_buffer.width() as usize,
|
);
|
||||||
image_buffer.height() as usize,
|
Ok(TexturedImage::Static(ctx.load_texture(
|
||||||
],
|
&url,
|
||||||
flat_samples.as_slice(),
|
img,
|
||||||
);
|
Default::default(),
|
||||||
|
)))
|
||||||
Ok(TexturedImage::Static(ctx.load_texture(
|
}
|
||||||
&url,
|
MediaCacheType::Gif => {
|
||||||
img,
|
let gif_bytes = fs::read(path.clone()).await?; // Read entire file into a Vec<u8>
|
||||||
Default::default(),
|
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>> {
|
pub fn fetch_binary_from_disk(path: PathBuf) -> Result<Vec<u8>> {
|
||||||
std::fs::read(path).map_err(|e| notedeck::Error::Generic(e.to_string()))
|
std::fs::read(path).map_err(|e| notedeck::Error::Generic(e.to_string()))
|
||||||
}
|
}
|
||||||
@@ -222,14 +363,15 @@ pub fn fetch_img(
|
|||||||
ctx: &egui::Context,
|
ctx: &egui::Context,
|
||||||
url: &str,
|
url: &str,
|
||||||
imgtyp: ImageType,
|
imgtyp: ImageType,
|
||||||
|
cache_type: MediaCacheType,
|
||||||
) -> Promise<Result<TexturedImage>> {
|
) -> Promise<Result<TexturedImage>> {
|
||||||
let key = MediaCache::key(url);
|
let key = MediaCache::key(url);
|
||||||
let path = img_cache.cache_dir.join(key);
|
let path = img_cache.cache_dir.join(key);
|
||||||
|
|
||||||
if path.exists() {
|
if path.exists() {
|
||||||
fetch_img_from_disk(ctx, url, &path)
|
fetch_img_from_disk(ctx, url, &path, cache_type)
|
||||||
} else {
|
} 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
|
// TODO: fetch image from local cache
|
||||||
@@ -240,6 +382,7 @@ fn fetch_img_from_net(
|
|||||||
ctx: &egui::Context,
|
ctx: &egui::Context,
|
||||||
url: &str,
|
url: &str,
|
||||||
imgtyp: ImageType,
|
imgtyp: ImageType,
|
||||||
|
cache_type: MediaCacheType,
|
||||||
) -> Promise<Result<TexturedImage>> {
|
) -> Promise<Result<TexturedImage>> {
|
||||||
let (sender, promise) = Promise::new();
|
let (sender, promise) = Promise::new();
|
||||||
let request = ehttp::Request::get(url);
|
let request = ehttp::Request::get(url);
|
||||||
@@ -247,17 +390,35 @@ fn fetch_img_from_net(
|
|||||||
let cloned_url = url.to_owned();
|
let cloned_url = url.to_owned();
|
||||||
let cache_path = cache_path.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(notedeck::Error::Generic).and_then(|resp| {
|
||||||
.map_err(notedeck::Error::Generic)
|
match cache_type {
|
||||||
.and_then(|resp| parse_img_response(resp, imgtyp))
|
MediaCacheType::Image => {
|
||||||
.map(|img| {
|
let img = parse_img_response(resp, imgtyp);
|
||||||
let texture_handle = ctx.load_texture(&cloned_url, img.clone(), Default::default());
|
img.map(|img| {
|
||||||
|
let texture_handle =
|
||||||
|
ctx.load_texture(&cloned_url, img.clone(), Default::default());
|
||||||
|
|
||||||
// write to disk
|
// write to disk
|
||||||
std::thread::spawn(move || MediaCache::write(&cache_path, &cloned_url, img));
|
std::thread::spawn(move || {
|
||||||
|
MediaCache::write(&cache_path, &cloned_url, img)
|
||||||
|
});
|
||||||
|
|
||||||
TexturedImage::Static(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.
|
sender.send(handle); // send the results back to the UI thread.
|
||||||
ctx.request_repaint();
|
ctx.request_repaint();
|
||||||
|
|||||||
@@ -1,44 +1,54 @@
|
|||||||
use notedeck::{Images, MediaCache, TexturedImage};
|
use notedeck::{GifStateMap, Images, MediaCache, MediaCacheType, TexturedImage};
|
||||||
|
|
||||||
use crate::images::ImageType;
|
use crate::images::ImageType;
|
||||||
|
|
||||||
use super::ProfilePic;
|
use super::ProfilePic;
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn render_images(
|
pub fn render_images(
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
images: &mut Images,
|
images: &mut Images,
|
||||||
url: &str,
|
url: &str,
|
||||||
img_type: ImageType,
|
img_type: ImageType,
|
||||||
|
cache_type: MediaCacheType,
|
||||||
show_waiting: impl FnOnce(&mut egui::Ui),
|
show_waiting: impl FnOnce(&mut egui::Ui),
|
||||||
show_error: impl FnOnce(&mut egui::Ui, String),
|
show_error: impl FnOnce(&mut egui::Ui, String),
|
||||||
show_success: impl FnOnce(&mut egui::Ui, &str, &mut TexturedImage),
|
show_success: impl FnOnce(&mut egui::Ui, &str, &mut TexturedImage, &mut GifStateMap),
|
||||||
) -> egui::Response {
|
) -> egui::Response {
|
||||||
let cache = &mut images.static_imgs;
|
let cache = match cache_type {
|
||||||
|
MediaCacheType::Image => &mut images.static_imgs,
|
||||||
|
MediaCacheType::Gif => &mut images.gifs,
|
||||||
|
};
|
||||||
|
|
||||||
render_media_cache(
|
render_media_cache(
|
||||||
ui,
|
ui,
|
||||||
cache,
|
cache,
|
||||||
|
&mut images.gif_states,
|
||||||
url,
|
url,
|
||||||
img_type,
|
img_type,
|
||||||
|
cache_type,
|
||||||
show_waiting,
|
show_waiting,
|
||||||
show_error,
|
show_error,
|
||||||
show_success,
|
show_success,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_media_cache(
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn render_media_cache(
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
cache: &mut MediaCache,
|
cache: &mut MediaCache,
|
||||||
|
gif_states: &mut GifStateMap,
|
||||||
url: &str,
|
url: &str,
|
||||||
img_type: ImageType,
|
img_type: ImageType,
|
||||||
|
cache_type: MediaCacheType,
|
||||||
show_waiting: impl FnOnce(&mut egui::Ui),
|
show_waiting: impl FnOnce(&mut egui::Ui),
|
||||||
show_error: impl FnOnce(&mut egui::Ui, String),
|
show_error: impl FnOnce(&mut egui::Ui, String),
|
||||||
show_success: impl FnOnce(&mut egui::Ui, &str, &mut TexturedImage),
|
show_success: impl FnOnce(&mut egui::Ui, &str, &mut TexturedImage, &mut GifStateMap),
|
||||||
) -> egui::Response {
|
) -> egui::Response {
|
||||||
let m_cached_promise = cache.map().get(url);
|
let m_cached_promise = cache.map().get(url);
|
||||||
|
|
||||||
if m_cached_promise.is_none() {
|
if m_cached_promise.is_none() {
|
||||||
let res = crate::images::fetch_img(cache, ui.ctx(), url, img_type);
|
let res = crate::images::fetch_img(cache, ui.ctx(), url, img_type, cache_type.clone());
|
||||||
cache.map_mut().insert(url.to_owned(), res);
|
cache.map_mut().insert(url.to_owned(), res);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,11 +63,12 @@ pub fn render_media_cache(
|
|||||||
ui.ctx(),
|
ui.ctx(),
|
||||||
ProfilePic::no_pfp_url(),
|
ProfilePic::no_pfp_url(),
|
||||||
ImageType::Profile(128),
|
ImageType::Profile(128),
|
||||||
|
cache_type,
|
||||||
);
|
);
|
||||||
cache.map_mut().insert(url.to_owned(), no_pfp);
|
cache.map_mut().insert(url.to_owned(), no_pfp);
|
||||||
show_error(ui, err)
|
show_error(ui, err)
|
||||||
}
|
}
|
||||||
Some(Ok(renderable_media)) => show_success(ui, url, renderable_media),
|
Some(Ok(renderable_media)) => show_success(ui, url, renderable_media, gif_states),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.response
|
.response
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::gif::{handle_repaint, retrieve_latest_texture};
|
||||||
use crate::ui::images::render_images;
|
use crate::ui::images::render_images;
|
||||||
use crate::ui::{
|
use crate::ui::{
|
||||||
self,
|
self,
|
||||||
@@ -8,7 +9,7 @@ use egui::{Color32, Hyperlink, Image, RichText};
|
|||||||
use nostrdb::{BlockType, Mention, Ndb, Note, NoteKey, Transaction};
|
use nostrdb::{BlockType, Mention, Ndb, Note, NoteKey, Transaction};
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
|
||||||
use notedeck::{supported_mime_hosted_at_url, Images, NoteCache};
|
use notedeck::{supported_mime_hosted_at_url, Images, MediaCacheType, NoteCache};
|
||||||
|
|
||||||
pub struct NoteContents<'a> {
|
pub struct NoteContents<'a> {
|
||||||
ndb: &'a Ndb,
|
ndb: &'a Ndb,
|
||||||
@@ -142,7 +143,7 @@ fn render_note_contents(
|
|||||||
puffin::profile_function!();
|
puffin::profile_function!();
|
||||||
|
|
||||||
let selectable = options.has_selectable_text();
|
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 note_action: Option<NoteAction> = None;
|
||||||
let mut inline_note: Option<(&[u8; 32], &str)> = None;
|
let mut inline_note: Option<(&[u8; 32], &str)> = None;
|
||||||
let hide_media = options.has_hide_media();
|
let hide_media = options.has_hide_media();
|
||||||
@@ -211,8 +212,10 @@ fn render_note_contents(
|
|||||||
if !hide_media {
|
if !hide_media {
|
||||||
let url = block.as_str().to_string();
|
let url = block.as_str().to_string();
|
||||||
|
|
||||||
if supported_mime_hosted_at_url(&mut img_cache.urls, &url) {
|
if let Some(cache_type) =
|
||||||
images.push(url);
|
supported_mime_hosted_at_url(&mut img_cache.urls, &url)
|
||||||
|
{
|
||||||
|
images.push((url, cache_type));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
#[cfg(feature = "profiling")]
|
#[cfg(feature = "profiling")]
|
||||||
@@ -280,7 +283,7 @@ fn rot13(input: &str) -> String {
|
|||||||
fn image_carousel(
|
fn image_carousel(
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
img_cache: &mut Images,
|
img_cache: &mut Images,
|
||||||
images: Vec<String>,
|
images: Vec<(String, MediaCacheType)>,
|
||||||
carousel_id: egui::Id,
|
carousel_id: egui::Id,
|
||||||
) {
|
) {
|
||||||
// let's make sure everything is within our area
|
// let's make sure everything is within our area
|
||||||
@@ -294,25 +297,31 @@ fn image_carousel(
|
|||||||
.id_salt(carousel_id)
|
.id_salt(carousel_id)
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
for image in images {
|
for (image, cache_type) in images {
|
||||||
render_images(
|
render_images(
|
||||||
ui,
|
ui,
|
||||||
img_cache,
|
img_cache,
|
||||||
&image,
|
&image,
|
||||||
ImageType::Content(width.round() as u32, height.round() as u32),
|
ImageType::Content(width.round() as u32, height.round() as u32),
|
||||||
|
cache_type,
|
||||||
|ui| {
|
|ui| {
|
||||||
ui.allocate_space(egui::vec2(spinsz, spinsz));
|
ui.allocate_space(egui::vec2(spinsz, spinsz));
|
||||||
},
|
},
|
||||||
|ui, _| {
|
|ui, _| {
|
||||||
ui.allocate_space(egui::vec2(spinsz, spinsz));
|
ui.allocate_space(egui::vec2(spinsz, spinsz));
|
||||||
},
|
},
|
||||||
|ui, url, renderable_media| {
|
|ui, url, renderable_media, gifs| {
|
||||||
|
let texture = handle_repaint(
|
||||||
|
ui,
|
||||||
|
retrieve_latest_texture(&image, gifs, renderable_media),
|
||||||
|
);
|
||||||
let img_resp = ui.add(
|
let img_resp = ui.add(
|
||||||
Image::new(notedeck::get_texture(renderable_media))
|
Image::new(texture)
|
||||||
.max_height(height)
|
.max_height(height)
|
||||||
.rounding(5.0)
|
.rounding(5.0)
|
||||||
.fit_to_original_size(1.0),
|
.fit_to_original_size(1.0),
|
||||||
);
|
);
|
||||||
|
|
||||||
img_resp.context_menu(|ui| {
|
img_resp.context_menu(|ui| {
|
||||||
if ui.button("Copy Link").clicked() {
|
if ui.button("Copy Link").clicked() {
|
||||||
ui.ctx().copy_text(url.to_owned());
|
ui.ctx().copy_text(url.to_owned());
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use crate::draft::{Draft, Drafts, MentionHint};
|
use crate::draft::{Draft, Drafts, MentionHint};
|
||||||
|
use crate::gif::{handle_repaint, retrieve_latest_texture};
|
||||||
use crate::media_upload::{nostrbuild_nip96_upload, MediaPath};
|
use crate::media_upload::{nostrbuild_nip96_upload, MediaPath};
|
||||||
use crate::post::{downcast_post_buffer, MentionType, NewPost};
|
use crate::post::{downcast_post_buffer, MentionType, NewPost};
|
||||||
use crate::profile::get_display_name;
|
use crate::profile::get_display_name;
|
||||||
@@ -13,7 +14,7 @@ use egui::{vec2, Frame, Layout, Margin, Pos2, ScrollArea, Sense, TextBuffer};
|
|||||||
use enostr::{FilledKeypair, FullKeypair, NoteId, Pubkey, RelayPool};
|
use enostr::{FilledKeypair, FullKeypair, NoteId, Pubkey, RelayPool};
|
||||||
use nostrdb::{Ndb, Transaction};
|
use nostrdb::{Ndb, Transaction};
|
||||||
|
|
||||||
use notedeck::{get_texture, Images, NoteCache};
|
use notedeck::{Images, NoteCache};
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
use super::contents::render_note_preview;
|
use super::contents::render_note_preview;
|
||||||
@@ -390,6 +391,7 @@ impl<'a> PostView<'a> {
|
|||||||
self.img_cache,
|
self.img_cache,
|
||||||
&media.url,
|
&media.url,
|
||||||
crate::images::ImageType::Content(width, height),
|
crate::images::ImageType::Content(width, height),
|
||||||
|
notedeck::MediaCacheType::Image, // TODO(kernelkind): support gifs in PostView
|
||||||
|ui| {
|
|ui| {
|
||||||
ui.spinner();
|
ui.spinner();
|
||||||
},
|
},
|
||||||
@@ -397,7 +399,7 @@ impl<'a> PostView<'a> {
|
|||||||
self.draft.upload_errors.push(e.to_string());
|
self.draft.upload_errors.push(e.to_string());
|
||||||
error!("{e}");
|
error!("{e}");
|
||||||
},
|
},
|
||||||
|ui, _, renderable_media| {
|
|ui, url, renderable_media, gifs| {
|
||||||
let media_size = vec2(width as f32, height as f32);
|
let media_size = vec2(width as f32, height as f32);
|
||||||
let max_size = vec2(300.0, 300.0);
|
let max_size = vec2(300.0, 300.0);
|
||||||
let size = if media_size.x > max_size.x || media_size.y > max_size.y {
|
let size = if media_size.x > max_size.x || media_size.y > max_size.y {
|
||||||
@@ -406,7 +408,8 @@ impl<'a> PostView<'a> {
|
|||||||
media_size
|
media_size
|
||||||
};
|
};
|
||||||
|
|
||||||
let texture_handle = get_texture(renderable_media);
|
let texture_handle =
|
||||||
|
handle_repaint(ui, retrieve_latest_texture(url, gifs, renderable_media));
|
||||||
let img_resp = ui.add(
|
let img_resp = ui.add(
|
||||||
egui::Image::new(texture_handle)
|
egui::Image::new(texture_handle)
|
||||||
.max_size(size)
|
.max_size(size)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::gif::{handle_repaint, retrieve_latest_texture};
|
||||||
use crate::images::ImageType;
|
use crate::images::ImageType;
|
||||||
use crate::ui::images::render_images;
|
use crate::ui::images::render_images;
|
||||||
use crate::ui::{Preview, PreviewConfig};
|
use crate::ui::{Preview, PreviewConfig};
|
||||||
@@ -5,7 +6,7 @@ use egui::{vec2, Sense, Stroke, TextureHandle};
|
|||||||
use nostrdb::{Ndb, Transaction};
|
use nostrdb::{Ndb, Transaction};
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
use notedeck::{AppContext, Images};
|
use notedeck::{supported_mime_hosted_at_url, AppContext, Images};
|
||||||
|
|
||||||
pub struct ProfilePic<'cache, 'url> {
|
pub struct ProfilePic<'cache, 'url> {
|
||||||
cache: &'cache mut Images,
|
cache: &'cache mut Images,
|
||||||
@@ -92,19 +93,24 @@ fn render_pfp(
|
|||||||
// We will want to downsample these so it's not blurry on hi res displays
|
// We will want to downsample these so it's not blurry on hi res displays
|
||||||
let img_size = 128u32;
|
let img_size = 128u32;
|
||||||
|
|
||||||
|
let cache_type = supported_mime_hosted_at_url(&mut img_cache.urls, url)
|
||||||
|
.unwrap_or(notedeck::MediaCacheType::Image);
|
||||||
|
|
||||||
render_images(
|
render_images(
|
||||||
ui,
|
ui,
|
||||||
img_cache,
|
img_cache,
|
||||||
url,
|
url,
|
||||||
ImageType::Profile(img_size),
|
ImageType::Profile(img_size),
|
||||||
|
cache_type,
|
||||||
|ui| {
|
|ui| {
|
||||||
paint_circle(ui, ui_size, border);
|
paint_circle(ui, ui_size, border);
|
||||||
},
|
},
|
||||||
|ui, _| {
|
|ui, _| {
|
||||||
paint_circle(ui, ui_size, border);
|
paint_circle(ui, ui_size, border);
|
||||||
},
|
},
|
||||||
|ui, _, renderable_media| {
|
|ui, url, renderable_media, gifs| {
|
||||||
let texture_handle = notedeck::get_texture(renderable_media);
|
let texture_handle =
|
||||||
|
handle_repaint(ui, retrieve_latest_texture(url, gifs, renderable_media));
|
||||||
pfp_image(ui, texture_handle, ui_size, border);
|
pfp_image(ui, texture_handle, ui_size, border);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user