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:
32
Cargo.lock
generated
32
Cargo.lock
generated
@@ -1269,7 +1269,7 @@ checksum = "bf3c1f5cd8dfe2ade470a218696c66cf556fcfd701e7830fa2e9f4428292a2a1"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
"egui",
|
"egui",
|
||||||
"ehttp 0.5.0",
|
"ehttp",
|
||||||
"enum-map",
|
"enum-map",
|
||||||
"image",
|
"image",
|
||||||
"log",
|
"log",
|
||||||
@@ -1326,19 +1326,6 @@ dependencies = [
|
|||||||
"web-time 1.1.0",
|
"web-time 1.1.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ehttp"
|
|
||||||
version = "0.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "80b69a6f9168b96c0ae04763bec27a8b06b34343c334dd2703a4ec21f0f5e110"
|
|
||||||
dependencies = [
|
|
||||||
"js-sys",
|
|
||||||
"ureq",
|
|
||||||
"wasm-bindgen",
|
|
||||||
"wasm-bindgen-futures",
|
|
||||||
"web-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ehttp"
|
name = "ehttp"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@@ -2384,7 +2371,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
|
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"windows-targets 0.48.5",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2520,6 +2507,16 @@ version = "0.3.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mime_guess"
|
||||||
|
version = "2.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
|
||||||
|
dependencies = [
|
||||||
|
"mime",
|
||||||
|
"unicase",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mime_guess2"
|
name = "mime_guess2"
|
||||||
version = "2.0.5"
|
version = "2.0.5"
|
||||||
@@ -2747,12 +2744,15 @@ name = "notedeck"
|
|||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base32",
|
"base32",
|
||||||
|
"bincode",
|
||||||
"dirs",
|
"dirs",
|
||||||
"eframe",
|
"eframe",
|
||||||
"egui",
|
"egui",
|
||||||
|
"ehttp",
|
||||||
"enostr",
|
"enostr",
|
||||||
"hex",
|
"hex",
|
||||||
"image",
|
"image",
|
||||||
|
"mime_guess",
|
||||||
"nostrdb",
|
"nostrdb",
|
||||||
"poll-promise",
|
"poll-promise",
|
||||||
"puffin 0.19.1 (git+https://github.com/jb55/puffin?rev=70ff86d5503815219b01a009afd3669b7903a057)",
|
"puffin 0.19.1 (git+https://github.com/jb55/puffin?rev=70ff86d5503815219b01a009afd3669b7903a057)",
|
||||||
@@ -2808,7 +2808,7 @@ dependencies = [
|
|||||||
"egui_nav",
|
"egui_nav",
|
||||||
"egui_tabs",
|
"egui_tabs",
|
||||||
"egui_virtual_list",
|
"egui_virtual_list",
|
||||||
"ehttp 0.2.0",
|
"ehttp",
|
||||||
"enostr",
|
"enostr",
|
||||||
"hex",
|
"hex",
|
||||||
"image",
|
"image",
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ egui_extras = { version = "0.29.1", features = ["all_loaders"] }
|
|||||||
egui_nav = { git = "https://github.com/damus-io/egui-nav", rev = "ac7d663307b76634757024b438dd4b899790da99" }
|
egui_nav = { git = "https://github.com/damus-io/egui-nav", rev = "ac7d663307b76634757024b438dd4b899790da99" }
|
||||||
egui_tabs = "0.2.0"
|
egui_tabs = "0.2.0"
|
||||||
egui_virtual_list = "0.5.0"
|
egui_virtual_list = "0.5.0"
|
||||||
ehttp = "0.2.0"
|
ehttp = "0.5.0"
|
||||||
enostr = { path = "crates/enostr" }
|
enostr = { path = "crates/enostr" }
|
||||||
ewebsock = { version = "0.2.0", features = ["tls"] }
|
ewebsock = { version = "0.2.0", features = ["tls"] }
|
||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
@@ -56,6 +56,8 @@ urlencoding = "2.1.3"
|
|||||||
uuid = { version = "1.10.0", features = ["v4"] }
|
uuid = { version = "1.10.0", features = ["v4"] }
|
||||||
security-framework = "2.11.0"
|
security-framework = "2.11.0"
|
||||||
sha2 = "0.10.8"
|
sha2 = "0.10.8"
|
||||||
|
bincode = "1.3.3"
|
||||||
|
mime_guess = "2.0.5"
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
egui = { git = "https://github.com/damus-io/egui", branch = "update_layouter_0.29.1" }
|
egui = { git = "https://github.com/damus-io/egui", branch = "update_layouter_0.29.1" }
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ thiserror = { workspace = true }
|
|||||||
puffin = { workspace = true, optional = true }
|
puffin = { workspace = true, optional = true }
|
||||||
puffin_egui = { workspace = true, optional = true }
|
puffin_egui = { workspace = true, optional = true }
|
||||||
sha2 = { workspace = true }
|
sha2 = { workspace = true }
|
||||||
|
bincode = { workspace = true }
|
||||||
|
ehttp = {workspace = true }
|
||||||
|
mime_guess = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = { workspace = true }
|
tempfile = { workspace = true }
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::persist::{AppSizeHandler, ZoomHandler};
|
use crate::persist::{AppSizeHandler, ZoomHandler};
|
||||||
use crate::{
|
use crate::{
|
||||||
Accounts, AppContext, Args, DataPath, DataPathType, Directory, FileKeyStorage, ImageCache,
|
Accounts, AppContext, Args, DataPath, DataPathType, Directory, FileKeyStorage, Images,
|
||||||
KeyStorageType, NoteCache, RelayDebugView, ThemeHandler, UnknownIds,
|
KeyStorageType, NoteCache, RelayDebugView, ThemeHandler, UnknownIds,
|
||||||
};
|
};
|
||||||
use egui::ThemePreference;
|
use egui::ThemePreference;
|
||||||
@@ -19,7 +19,7 @@ pub trait App {
|
|||||||
/// Main notedeck app framework
|
/// Main notedeck app framework
|
||||||
pub struct Notedeck {
|
pub struct Notedeck {
|
||||||
ndb: Ndb,
|
ndb: Ndb,
|
||||||
img_cache: ImageCache,
|
img_cache: Images,
|
||||||
unknown_ids: UnknownIds,
|
unknown_ids: UnknownIds,
|
||||||
pool: RelayPool,
|
pool: RelayPool,
|
||||||
note_cache: NoteCache,
|
note_cache: NoteCache,
|
||||||
@@ -129,7 +129,7 @@ impl Notedeck {
|
|||||||
|
|
||||||
let _ = std::fs::create_dir_all(&dbpath_str);
|
let _ = std::fs::create_dir_all(&dbpath_str);
|
||||||
|
|
||||||
let img_cache_dir = path.path(DataPathType::Cache).join(ImageCache::rel_dir());
|
let img_cache_dir = path.path(DataPathType::Cache);
|
||||||
let _ = std::fs::create_dir_all(img_cache_dir.clone());
|
let _ = std::fs::create_dir_all(img_cache_dir.clone());
|
||||||
|
|
||||||
let map_size = if cfg!(target_os = "windows") {
|
let map_size = if cfg!(target_os = "windows") {
|
||||||
@@ -184,7 +184,7 @@ impl Notedeck {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let img_cache = ImageCache::new(img_cache_dir);
|
let img_cache = Images::new(img_cache_dir);
|
||||||
let note_cache = NoteCache::default();
|
let note_cache = NoteCache::default();
|
||||||
let unknown_ids = UnknownIds::default();
|
let unknown_ids = UnknownIds::default();
|
||||||
let zoom = ZoomHandler::new(&path);
|
let zoom = ZoomHandler::new(&path);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::{Accounts, Args, DataPath, ImageCache, NoteCache, ThemeHandler, UnknownIds};
|
use crate::{Accounts, Args, DataPath, Images, NoteCache, ThemeHandler, UnknownIds};
|
||||||
|
|
||||||
use enostr::RelayPool;
|
use enostr::RelayPool;
|
||||||
use nostrdb::Ndb;
|
use nostrdb::Ndb;
|
||||||
@@ -7,7 +7,7 @@ use nostrdb::Ndb;
|
|||||||
|
|
||||||
pub struct AppContext<'a> {
|
pub struct AppContext<'a> {
|
||||||
pub ndb: &'a mut Ndb,
|
pub ndb: &'a mut Ndb,
|
||||||
pub img_cache: &'a mut ImageCache,
|
pub img_cache: &'a mut Images,
|
||||||
pub unknown_ids: &'a mut UnknownIds,
|
pub unknown_ids: &'a mut UnknownIds,
|
||||||
pub pool: &'a mut RelayPool,
|
pub pool: &'a mut RelayPool,
|
||||||
pub note_cache: &'a mut NoteCache,
|
pub note_cache: &'a mut NoteCache,
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fs::{create_dir_all, File};
|
use std::fs::{create_dir_all, File};
|
||||||
|
use std::sync::mpsc::Receiver;
|
||||||
|
use std::time::{Duration, Instant, SystemTime};
|
||||||
|
|
||||||
use hex::ToHex;
|
use hex::ToHex;
|
||||||
use sha2::Digest;
|
use sha2::Digest;
|
||||||
@@ -13,15 +17,56 @@ use std::path;
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
|
||||||
pub type ImageCacheValue = Promise<Result<TextureHandle>>;
|
pub type MediaCacheValue = Promise<Result<TexturedImage>>;
|
||||||
pub type ImageCacheMap = HashMap<String, ImageCacheValue>;
|
pub type MediaCacheMap = HashMap<String, MediaCacheValue>;
|
||||||
|
|
||||||
pub struct ImageCache {
|
pub enum TexturedImage {
|
||||||
pub cache_dir: path::PathBuf,
|
Static(TextureHandle),
|
||||||
url_imgs: ImageCacheMap,
|
Animated(Animation),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ImageCache {
|
pub struct Animation {
|
||||||
|
pub first_frame: TextureFrame,
|
||||||
|
pub other_frames: Vec<TextureFrame>,
|
||||||
|
pub receiver: Option<Receiver<TextureFrame>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Animation {
|
||||||
|
pub fn get_frame(&self, index: usize) -> Option<&TextureFrame> {
|
||||||
|
if index == 0 {
|
||||||
|
Some(&self.first_frame)
|
||||||
|
} else {
|
||||||
|
self.other_frames.get(index - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn num_frames(&self) -> usize {
|
||||||
|
self.other_frames.len() + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TextureFrame {
|
||||||
|
pub delay: Duration,
|
||||||
|
pub texture: TextureHandle,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ImageFrame {
|
||||||
|
pub delay: Duration,
|
||||||
|
pub image: ColorImage,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MediaCache {
|
||||||
|
pub cache_dir: path::PathBuf,
|
||||||
|
url_imgs: MediaCacheMap,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum MediaCacheType {
|
||||||
|
Image,
|
||||||
|
Gif,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MediaCache {
|
||||||
pub fn new(cache_dir: path::PathBuf) -> Self {
|
pub fn new(cache_dir: path::PathBuf) -> Self {
|
||||||
Self {
|
Self {
|
||||||
cache_dir,
|
cache_dir,
|
||||||
@@ -29,35 +74,15 @@ impl ImageCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn rel_dir() -> &'static str {
|
pub fn rel_dir(cache_type: MediaCacheType) -> &'static str {
|
||||||
"img"
|
match cache_type {
|
||||||
}
|
MediaCacheType::Image => "img",
|
||||||
|
MediaCacheType::Gif => "gif",
|
||||||
/*
|
|
||||||
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(
|
||||||
@@ -70,6 +95,33 @@ impl ImageCache {
|
|||||||
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])
|
||||||
@@ -118,11 +170,58 @@ impl ImageCache {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn map(&self) -> &ImageCacheMap {
|
pub fn map(&self) -> &MediaCacheMap {
|
||||||
&self.url_imgs
|
&self.url_imgs
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn map_mut(&mut self) -> &mut ImageCacheMap {
|
pub fn map_mut(&mut self) -> &mut MediaCacheMap {
|
||||||
&mut self.url_imgs
|
&mut self.url_imgs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn color_image_to_rgba(color_image: ColorImage) -> image::RgbaImage {
|
||||||
|
let width = color_image.width() as u32;
|
||||||
|
let height = color_image.height() as u32;
|
||||||
|
|
||||||
|
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 static_imgs: MediaCache,
|
||||||
|
pub gifs: MediaCache,
|
||||||
|
pub urls: UrlMimes,
|
||||||
|
pub gif_states: GifStateMap,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Images {
|
||||||
|
/// path to directory to place [`MediaCache`]s
|
||||||
|
pub fn new(path: path::PathBuf) -> Self {
|
||||||
|
Self {
|
||||||
|
static_imgs: MediaCache::new(path.join(MediaCache::rel_dir(MediaCacheType::Image))),
|
||||||
|
gifs: MediaCache::new(path.join(MediaCache::rel_dir(MediaCacheType::Gif))),
|
||||||
|
urls: UrlMimes::new(UrlCache::new(path.join(UrlCache::rel_dir()))),
|
||||||
|
gif_states: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn migrate_v0(&self) -> Result<()> {
|
||||||
|
self.static_imgs.migrate_v0()?;
|
||||||
|
self.gifs.migrate_v0()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type GifStateMap = HashMap<String, GifState>;
|
||||||
|
|
||||||
|
pub struct GifState {
|
||||||
|
pub last_frame_rendered: Instant,
|
||||||
|
pub last_frame_duration: Duration,
|
||||||
|
pub next_frame_time: Option<SystemTime>,
|
||||||
|
pub last_frame_index: usize,
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ mod timecache;
|
|||||||
mod timed_serializer;
|
mod timed_serializer;
|
||||||
pub mod ui;
|
pub mod ui;
|
||||||
mod unknowns;
|
mod unknowns;
|
||||||
|
mod urls;
|
||||||
mod user_account;
|
mod user_account;
|
||||||
|
|
||||||
pub use accounts::{AccountData, Accounts, AccountsAction, AddAccountAction, SwitchAccountAction};
|
pub use accounts::{AccountData, Accounts, AccountsAction, AddAccountAction, SwitchAccountAction};
|
||||||
@@ -30,7 +31,10 @@ pub use context::AppContext;
|
|||||||
pub use error::{Error, FilterError};
|
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::ImageCache;
|
pub use imgcache::{
|
||||||
|
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};
|
||||||
pub use notecache::{CachedNote, NoteCache};
|
pub use notecache::{CachedNote, NoteCache};
|
||||||
@@ -46,6 +50,7 @@ pub use theme::ColorTheme;
|
|||||||
pub use time::time_ago_since;
|
pub use time::time_ago_since;
|
||||||
pub use timecache::TimeCached;
|
pub use timecache::TimeCached;
|
||||||
pub use unknowns::{get_unknown_note_ids, NoteRefsUnkIdAction, SingleUnkIdAction, UnknownIds};
|
pub use unknowns::{get_unknown_note_ids, NoteRefsUnkIdAction, SingleUnkIdAction, UnknownIds};
|
||||||
|
pub use urls::{supported_mime_hosted_at_url, SupportedMimeType, UrlMimes};
|
||||||
pub use user_account::UserAccount;
|
pub use user_account::UserAccount;
|
||||||
|
|
||||||
// export libs
|
// export libs
|
||||||
|
|||||||
298
crates/notedeck/src/urls.rs
Normal file
298
crates/notedeck/src/urls.rs
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
fs::File,
|
||||||
|
io::{Read, Write},
|
||||||
|
path::PathBuf,
|
||||||
|
sync::{Arc, RwLock},
|
||||||
|
time::{Duration, SystemTime},
|
||||||
|
};
|
||||||
|
|
||||||
|
use egui::TextBuffer;
|
||||||
|
use poll_promise::Promise;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::{Error, MediaCacheType};
|
||||||
|
|
||||||
|
const FILE_NAME: &str = "urls.bin";
|
||||||
|
const SAVE_INTERVAL: Duration = Duration::from_secs(60);
|
||||||
|
|
||||||
|
type UrlsToMime = HashMap<String, String>;
|
||||||
|
|
||||||
|
/// caches mime type for a URL. saves to disk on interval [`SAVE_INTERVAL`]
|
||||||
|
pub struct UrlCache {
|
||||||
|
last_saved: SystemTime,
|
||||||
|
path: PathBuf,
|
||||||
|
cache: Arc<RwLock<UrlsToMime>>,
|
||||||
|
from_disk_promise: Option<Promise<Option<UrlsToMime>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UrlCache {
|
||||||
|
pub fn rel_dir() -> &'static str {
|
||||||
|
FILE_NAME
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(path: PathBuf) -> Self {
|
||||||
|
Self {
|
||||||
|
last_saved: SystemTime::now(),
|
||||||
|
path: path.clone(),
|
||||||
|
cache: Default::default(),
|
||||||
|
from_disk_promise: Some(read_from_disk(path)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_type(&self, url: &str) -> Option<String> {
|
||||||
|
self.cache.read().ok()?.get(url).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_type(&mut self, url: String, mime_type: String) {
|
||||||
|
if let Ok(mut locked_cache) = self.cache.write() {
|
||||||
|
locked_cache.insert(url, mime_type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_io(&mut self) {
|
||||||
|
if let Some(promise) = &mut self.from_disk_promise {
|
||||||
|
if let Some(maybe_cache) = promise.ready_mut() {
|
||||||
|
if let Some(cache) = maybe_cache.take() {
|
||||||
|
merge_cache(self.cache.clone(), cache)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.from_disk_promise = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(cur_duration) = SystemTime::now().duration_since(self.last_saved) {
|
||||||
|
if cur_duration >= SAVE_INTERVAL {
|
||||||
|
save_to_disk(self.path.clone(), self.cache.clone());
|
||||||
|
self.last_saved = SystemTime::now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn merge_cache(cur_cache: Arc<RwLock<UrlsToMime>>, from_disk: UrlsToMime) {
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
if let Ok(mut locked_cache) = cur_cache.write() {
|
||||||
|
locked_cache.extend(from_disk);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_from_disk(path: PathBuf) -> Promise<Option<UrlsToMime>> {
|
||||||
|
let (sender, promise) = Promise::new();
|
||||||
|
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let result: Result<UrlsToMime, Error> = (|| {
|
||||||
|
let mut file = File::open(path)?;
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
file.read_to_end(&mut buffer)?;
|
||||||
|
let data: UrlsToMime =
|
||||||
|
bincode::deserialize(&buffer).map_err(|e| Error::Generic(e.to_string()))?;
|
||||||
|
Ok(data)
|
||||||
|
})();
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(data) => sender.send(Some(data)),
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("problem deserializing UrlMimes: {e}");
|
||||||
|
sender.send(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
promise
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_to_disk(path: PathBuf, cache: Arc<RwLock<UrlsToMime>>) {
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let result: Result<(), Error> = (|| {
|
||||||
|
if let Ok(cache) = cache.read() {
|
||||||
|
let cache = &*cache;
|
||||||
|
let encoded =
|
||||||
|
bincode::serialize(cache).map_err(|e| Error::Generic(e.to_string()))?;
|
||||||
|
let mut file = File::create(&path)?;
|
||||||
|
file.write_all(&encoded)?;
|
||||||
|
file.sync_all()?;
|
||||||
|
tracing::info!("Saved UrlCache to disk.");
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(Error::Generic(
|
||||||
|
"Could not read UrlMimes behind RwLock".to_owned(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
if let Err(e) = result {
|
||||||
|
tracing::error!("Failed to save UrlMimes: {}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ehttp_get_mime_type(url: &str, sender: poll_promise::Sender<MimeResult>) {
|
||||||
|
let request = ehttp::Request::head(url);
|
||||||
|
|
||||||
|
let url = url.to_owned();
|
||||||
|
ehttp::fetch(
|
||||||
|
request,
|
||||||
|
move |response: Result<ehttp::Response, String>| match response {
|
||||||
|
Ok(resp) => {
|
||||||
|
if let Some(content_type) = resp.headers.get("content-type") {
|
||||||
|
sender.send(MimeResult::Ok(extract_mime_type(content_type).to_owned()));
|
||||||
|
} else {
|
||||||
|
sender.send(MimeResult::Err(HttpError::MissingHeader));
|
||||||
|
tracing::error!("Content-Type header not found for {url}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
sender.send(MimeResult::Err(HttpError::HttpFailure));
|
||||||
|
tracing::error!("failed ehttp for UrlMimes: {err}");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum HttpError {
|
||||||
|
HttpFailure,
|
||||||
|
MissingHeader,
|
||||||
|
}
|
||||||
|
|
||||||
|
type MimeResult = Result<String, HttpError>;
|
||||||
|
|
||||||
|
fn extract_mime_type(content_type: &str) -> &str {
|
||||||
|
content_type
|
||||||
|
.split(';')
|
||||||
|
.next()
|
||||||
|
.unwrap_or(content_type)
|
||||||
|
.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct UrlMimes {
|
||||||
|
pub cache: UrlCache,
|
||||||
|
in_flight: HashMap<String, Promise<MimeResult>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UrlMimes {
|
||||||
|
pub fn new(url_cache: UrlCache) -> Self {
|
||||||
|
Self {
|
||||||
|
cache: url_cache,
|
||||||
|
in_flight: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&mut self, url: &str) -> Option<String> {
|
||||||
|
if let Some(mime_type) = self.cache.get_type(url) {
|
||||||
|
Some(mime_type)
|
||||||
|
} else if let Some(promise) = self.in_flight.get_mut(url) {
|
||||||
|
if let Some(mime_result) = promise.ready_mut() {
|
||||||
|
match mime_result {
|
||||||
|
Ok(mime_type) => {
|
||||||
|
let mime_type = mime_type.take();
|
||||||
|
self.cache.set_type(url.to_owned(), mime_type.clone());
|
||||||
|
self.in_flight.remove(url);
|
||||||
|
Some(mime_type)
|
||||||
|
}
|
||||||
|
Err(HttpError::HttpFailure) => {
|
||||||
|
// allow retrying
|
||||||
|
self.in_flight.remove(url);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
Err(HttpError::MissingHeader) => {
|
||||||
|
// response was malformed, don't retry
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let (sender, promise) = Promise::new();
|
||||||
|
ehttp_get_mime_type(url, sender);
|
||||||
|
self.in_flight.insert(url.to_owned(), promise);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SupportedMimeType {
|
||||||
|
mime: mime_guess::Mime,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SupportedMimeType {
|
||||||
|
pub fn from_extension(extension: &str) -> Result<Self, Error> {
|
||||||
|
if let Some(mime) = mime_guess::from_ext(extension)
|
||||||
|
.first()
|
||||||
|
.filter(is_mime_supported)
|
||||||
|
{
|
||||||
|
Ok(Self { mime })
|
||||||
|
} else {
|
||||||
|
Err(Error::Generic("Unsupported mime type".to_owned()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_mime(&self) -> &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 {
|
||||||
|
mime.type_() == mime_guess::mime::IMAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
fn url_has_supported_mime(url: &str) -> MimeHostedAtUrl {
|
||||||
|
if let Ok(url) = Url::parse(url) {
|
||||||
|
if let Some(path) = url.path_segments() {
|
||||||
|
if let Some(file_name) = path.last() {
|
||||||
|
if let Some(ext) = std::path::Path::new(file_name)
|
||||||
|
.extension()
|
||||||
|
.and_then(|ext| ext.to_str())
|
||||||
|
{
|
||||||
|
if let Ok(supported) = SupportedMimeType::from_extension(ext) {
|
||||||
|
return MimeHostedAtUrl::Yes(supported.to_cache_type());
|
||||||
|
} else {
|
||||||
|
return MimeHostedAtUrl::No;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MimeHostedAtUrl::Maybe
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn supported_mime_hosted_at_url(urls: &mut UrlMimes, url: &str) -> Option<MediaCacheType> {
|
||||||
|
match url_has_supported_mime(url) {
|
||||||
|
MimeHostedAtUrl::Yes(cache_type) => Some(cache_type),
|
||||||
|
MimeHostedAtUrl::Maybe => urls
|
||||||
|
.get(url)
|
||||||
|
.and_then(|s| s.parse::<mime_guess::mime::Mime>().ok())
|
||||||
|
.and_then(|mime: mime_guess::mime::Mime| {
|
||||||
|
SupportedMimeType::from_mime(mime)
|
||||||
|
.ok()
|
||||||
|
.map(|s| s.to_cache_type())
|
||||||
|
}),
|
||||||
|
MimeHostedAtUrl::No => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum MimeHostedAtUrl {
|
||||||
|
Yes(MediaCacheType),
|
||||||
|
Maybe,
|
||||||
|
No,
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ use enostr::FullKeypair;
|
|||||||
use nostrdb::Ndb;
|
use nostrdb::Ndb;
|
||||||
|
|
||||||
use notedeck::{
|
use notedeck::{
|
||||||
Accounts, AccountsAction, AddAccountAction, ImageCache, SingleUnkIdAction, SwitchAccountAction,
|
Accounts, AccountsAction, AddAccountAction, Images, SingleUnkIdAction, SwitchAccountAction,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::app::get_active_columns_mut;
|
use crate::app::get_active_columns_mut;
|
||||||
@@ -27,7 +27,7 @@ pub fn render_accounts_route(
|
|||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
ndb: &Ndb,
|
ndb: &Ndb,
|
||||||
col: usize,
|
col: usize,
|
||||||
img_cache: &mut ImageCache,
|
img_cache: &mut Images,
|
||||||
accounts: &mut Accounts,
|
accounts: &mut Accounts,
|
||||||
decks: &mut DecksCache,
|
decks: &mut DecksCache,
|
||||||
login_state: &mut AcquireKeyState,
|
login_state: &mut AcquireKeyState,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use crate::{
|
|||||||
Result,
|
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 enostr::{ClientMessage, Keypair, PoolRelay, Pubkey, RelayEvent, RelayMessage, RelayPool};
|
||||||
use uuid::Uuid;
|
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) {
|
fn update_damus(damus: &mut Damus, app_ctx: &mut AppContext<'_>, ctx: &egui::Context) {
|
||||||
|
app_ctx.img_cache.urls.cache.handle_io();
|
||||||
|
|
||||||
match damus.state {
|
match damus.state {
|
||||||
DamusState::Initializing => {
|
DamusState::Initializing => {
|
||||||
damus.state = DamusState::Initialized;
|
damus.state = DamusState::Initialized;
|
||||||
@@ -464,7 +466,7 @@ impl Damus {
|
|||||||
let decks_cache = DecksCache::default();
|
let decks_cache = DecksCache::default();
|
||||||
|
|
||||||
let path = DataPath::new(&data_path);
|
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 _ = std::fs::create_dir_all(imgcache_dir.clone());
|
||||||
let debug = true;
|
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 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::Result;
|
||||||
|
use notedeck::TextureFrame;
|
||||||
|
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 ImageCache = 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,
|
||||||
@@ -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")]
|
#[cfg(feature = "profiling")]
|
||||||
puffin::profile_function!();
|
puffin::profile_function!();
|
||||||
|
|
||||||
@@ -125,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)
|
||||||
@@ -166,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())
|
||||||
}
|
}
|
||||||
@@ -177,28 +189,162 @@ fn fetch_img_from_disk(
|
|||||||
ctx: &egui::Context,
|
ctx: &egui::Context,
|
||||||
url: &str,
|
url: &str,
|
||||||
path: &path::Path,
|
path: &path::Path,
|
||||||
) -> Promise<Result<TextureHandle>> {
|
cache_type: MediaCacheType,
|
||||||
|
) -> 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(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>> {
|
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()))
|
||||||
}
|
}
|
||||||
@@ -213,18 +359,19 @@ pub enum ImageType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn fetch_img(
|
pub fn fetch_img(
|
||||||
img_cache: &ImageCache,
|
img_cache: &MediaCache,
|
||||||
ctx: &egui::Context,
|
ctx: &egui::Context,
|
||||||
url: &str,
|
url: &str,
|
||||||
imgtyp: ImageType,
|
imgtyp: ImageType,
|
||||||
) -> Promise<Result<TextureHandle>> {
|
cache_type: MediaCacheType,
|
||||||
let key = ImageCache::key(url);
|
) -> Promise<Result<TexturedImage>> {
|
||||||
|
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
|
||||||
@@ -235,24 +382,43 @@ fn fetch_img_from_net(
|
|||||||
ctx: &egui::Context,
|
ctx: &egui::Context,
|
||||||
url: &str,
|
url: &str,
|
||||||
imgtyp: ImageType,
|
imgtyp: ImageType,
|
||||||
) -> Promise<Result<TextureHandle>> {
|
cache_type: MediaCacheType,
|
||||||
|
) -> 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);
|
||||||
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();
|
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 || ImageCache::write(&cache_path, &cloned_url, img));
|
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.
|
sender.send(handle); // send the results back to the UI thread.
|
||||||
ctx.request_repaint();
|
ctx.request_repaint();
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ mod deck_state;
|
|||||||
mod decks;
|
mod decks;
|
||||||
mod draft;
|
mod draft;
|
||||||
mod frame_history;
|
mod frame_history;
|
||||||
|
mod gif;
|
||||||
mod images;
|
mod images;
|
||||||
mod key_parsing;
|
mod key_parsing;
|
||||||
pub mod login_manager;
|
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 base64::{prelude::BASE64_URL_SAFE, Engine};
|
||||||
use ehttp::Request;
|
use ehttp::Request;
|
||||||
use nostrdb::{Note, NoteBuilder};
|
use nostrdb::{Note, NoteBuilder};
|
||||||
|
use notedeck::SupportedMimeType;
|
||||||
use poll_promise::Promise;
|
use poll_promise::Promise;
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
@@ -104,15 +105,13 @@ fn create_nip96_request(
|
|||||||
body.extend(file_contents);
|
body.extend(file_contents);
|
||||||
body.extend(format!("\r\n--{}--\r\n", boundary).as_bytes());
|
body.extend(format!("\r\n--{}--\r\n", boundary).as_bytes());
|
||||||
|
|
||||||
let headers = {
|
let headers = ehttp::Headers::new(&[
|
||||||
let mut map = BTreeMap::new();
|
(
|
||||||
map.insert(
|
"Content-Type",
|
||||||
"Content-Type".to_owned(),
|
format!("multipart/form-data; boundary={boundary}").as_str(),
|
||||||
format!("multipart/form-data; boundary={boundary}"),
|
),
|
||||||
);
|
("Authorization", format!("Nostr {nip98_base64}").as_str()),
|
||||||
map.insert("Authorization".to_owned(), format!("Nostr {nip98_base64}"));
|
]);
|
||||||
map
|
|
||||||
};
|
|
||||||
|
|
||||||
Request {
|
Request {
|
||||||
method: "POST".to_string(),
|
method: "POST".to_string(),
|
||||||
@@ -234,13 +233,13 @@ fn find_nip94_ev_in_json(json: String) -> Result<Nip94Event, Error> {
|
|||||||
pub struct MediaPath {
|
pub struct MediaPath {
|
||||||
full_path: PathBuf,
|
full_path: PathBuf,
|
||||||
file_name: String,
|
file_name: String,
|
||||||
media_type: SupportedMediaType,
|
media_type: SupportedMimeType,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MediaPath {
|
impl MediaPath {
|
||||||
pub fn new(path: PathBuf) -> Result<Self, Error> {
|
pub fn new(path: PathBuf) -> Result<Self, Error> {
|
||||||
if let Some(ex) = path.extension().and_then(|f| f.to_str()) {
|
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
|
let file_name = path
|
||||||
.file_name()
|
.file_name()
|
||||||
.and_then(|name| name.to_str())
|
.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)]
|
#[derive(Clone, Debug, serde::Deserialize)]
|
||||||
pub struct Nip94Event {
|
pub struct Nip94Event {
|
||||||
pub url: String,
|
pub url: String,
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ use crate::{
|
|||||||
|
|
||||||
use enostr::Pubkey;
|
use enostr::Pubkey;
|
||||||
use nostrdb::Ndb;
|
use nostrdb::Ndb;
|
||||||
use notedeck::{Accounts, ImageCache, MuteFun, NoteCache, UnknownIds};
|
use notedeck::{Accounts, Images, MuteFun, NoteCache, UnknownIds};
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn render_timeline_route(
|
pub fn render_timeline_route(
|
||||||
ndb: &Ndb,
|
ndb: &Ndb,
|
||||||
img_cache: &mut ImageCache,
|
img_cache: &mut Images,
|
||||||
unknown_ids: &mut UnknownIds,
|
unknown_ids: &mut UnknownIds,
|
||||||
note_cache: &mut NoteCache,
|
note_cache: &mut NoteCache,
|
||||||
timeline_cache: &mut TimelineCache,
|
timeline_cache: &mut TimelineCache,
|
||||||
@@ -102,7 +102,7 @@ pub fn render_profile_route(
|
|||||||
accounts: &Accounts,
|
accounts: &Accounts,
|
||||||
ndb: &Ndb,
|
ndb: &Ndb,
|
||||||
timeline_cache: &mut TimelineCache,
|
timeline_cache: &mut TimelineCache,
|
||||||
img_cache: &mut ImageCache,
|
img_cache: &mut Images,
|
||||||
note_cache: &mut NoteCache,
|
note_cache: &mut NoteCache,
|
||||||
unknown_ids: &mut UnknownIds,
|
unknown_ids: &mut UnknownIds,
|
||||||
col: usize,
|
col: usize,
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ use egui::{
|
|||||||
Align, Button, Frame, Image, InnerResponse, Layout, RichText, ScrollArea, Ui, UiBuilder, Vec2,
|
Align, Button, Frame, Image, InnerResponse, Layout, RichText, ScrollArea, Ui, UiBuilder, Vec2,
|
||||||
};
|
};
|
||||||
use nostrdb::{Ndb, Transaction};
|
use nostrdb::{Ndb, Transaction};
|
||||||
use notedeck::{Accounts, ImageCache};
|
use notedeck::{Accounts, Images};
|
||||||
|
|
||||||
use super::profile::preview::SimpleProfilePreview;
|
use super::profile::preview::SimpleProfilePreview;
|
||||||
|
|
||||||
pub struct AccountsView<'a> {
|
pub struct AccountsView<'a> {
|
||||||
ndb: &'a Ndb,
|
ndb: &'a Ndb,
|
||||||
accounts: &'a Accounts,
|
accounts: &'a Accounts,
|
||||||
img_cache: &'a mut ImageCache,
|
img_cache: &'a mut Images,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -27,7 +27,7 @@ enum ProfilePreviewAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> AccountsView<'a> {
|
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 {
|
AccountsView {
|
||||||
ndb,
|
ndb,
|
||||||
accounts,
|
accounts,
|
||||||
@@ -54,7 +54,7 @@ impl<'a> AccountsView<'a> {
|
|||||||
ui: &mut Ui,
|
ui: &mut Ui,
|
||||||
accounts: &Accounts,
|
accounts: &Accounts,
|
||||||
ndb: &Ndb,
|
ndb: &Ndb,
|
||||||
img_cache: &mut ImageCache,
|
img_cache: &mut Images,
|
||||||
) -> Option<AccountsViewResponse> {
|
) -> Option<AccountsViewResponse> {
|
||||||
let mut return_op: Option<AccountsViewResponse> = None;
|
let mut return_op: Option<AccountsViewResponse> = None;
|
||||||
ui.allocate_ui_with_layout(
|
ui.allocate_ui_with_layout(
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ use crate::{
|
|||||||
Damus,
|
Damus,
|
||||||
};
|
};
|
||||||
|
|
||||||
use notedeck::{AppContext, ImageCache, NotedeckTextStyle, UserAccount};
|
use notedeck::{AppContext, Images, NotedeckTextStyle, UserAccount};
|
||||||
use tokenator::{ParseError, TokenParser, TokenSerializable, TokenWriter};
|
use tokenator::{ParseError, TokenParser, TokenSerializable, TokenWriter};
|
||||||
|
|
||||||
use super::{anim::AnimationHelper, padding, ProfilePreview};
|
use super::{anim::AnimationHelper, padding, ProfilePreview};
|
||||||
@@ -163,7 +163,7 @@ impl AddColumnOption {
|
|||||||
pub struct AddColumnView<'a> {
|
pub struct AddColumnView<'a> {
|
||||||
key_state_map: &'a mut HashMap<Id, AcquireKeyState>,
|
key_state_map: &'a mut HashMap<Id, AcquireKeyState>,
|
||||||
ndb: &'a Ndb,
|
ndb: &'a Ndb,
|
||||||
img_cache: &'a mut ImageCache,
|
img_cache: &'a mut Images,
|
||||||
cur_account: Option<&'a UserAccount>,
|
cur_account: Option<&'a UserAccount>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,7 +171,7 @@ impl<'a> AddColumnView<'a> {
|
|||||||
pub fn new(
|
pub fn new(
|
||||||
key_state_map: &'a mut HashMap<Id, AcquireKeyState>,
|
key_state_map: &'a mut HashMap<Id, AcquireKeyState>,
|
||||||
ndb: &'a Ndb,
|
ndb: &'a Ndb,
|
||||||
img_cache: &'a mut ImageCache,
|
img_cache: &'a mut Images,
|
||||||
cur_account: Option<&'a UserAccount>,
|
cur_account: Option<&'a UserAccount>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|||||||
@@ -16,11 +16,11 @@ use egui::Margin;
|
|||||||
use egui::{RichText, Stroke, UiBuilder};
|
use egui::{RichText, Stroke, UiBuilder};
|
||||||
use enostr::Pubkey;
|
use enostr::Pubkey;
|
||||||
use nostrdb::{Ndb, Transaction};
|
use nostrdb::{Ndb, Transaction};
|
||||||
use notedeck::{ImageCache, NotedeckTextStyle};
|
use notedeck::{Images, NotedeckTextStyle};
|
||||||
|
|
||||||
pub struct NavTitle<'a> {
|
pub struct NavTitle<'a> {
|
||||||
ndb: &'a Ndb,
|
ndb: &'a Ndb,
|
||||||
img_cache: &'a mut ImageCache,
|
img_cache: &'a mut Images,
|
||||||
columns: &'a Columns,
|
columns: &'a Columns,
|
||||||
routes: &'a [Route],
|
routes: &'a [Route],
|
||||||
col_id: usize,
|
col_id: usize,
|
||||||
@@ -29,7 +29,7 @@ pub struct NavTitle<'a> {
|
|||||||
impl<'a> NavTitle<'a> {
|
impl<'a> NavTitle<'a> {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
ndb: &'a Ndb,
|
ndb: &'a Ndb,
|
||||||
img_cache: &'a mut ImageCache,
|
img_cache: &'a mut Images,
|
||||||
columns: &'a Columns,
|
columns: &'a Columns,
|
||||||
routes: &'a [Route],
|
routes: &'a [Route],
|
||||||
col_id: usize,
|
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 egui::Sense;
|
||||||
use enostr::Pubkey;
|
use enostr::Pubkey;
|
||||||
use nostrdb::{Ndb, Transaction};
|
use nostrdb::{Ndb, Transaction};
|
||||||
use notedeck::ImageCache;
|
use notedeck::Images;
|
||||||
|
|
||||||
pub struct Mention<'a> {
|
pub struct Mention<'a> {
|
||||||
ndb: &'a Ndb,
|
ndb: &'a Ndb,
|
||||||
img_cache: &'a mut ImageCache,
|
img_cache: &'a mut Images,
|
||||||
txn: &'a Transaction,
|
txn: &'a Transaction,
|
||||||
pk: &'a [u8; 32],
|
pk: &'a [u8; 32],
|
||||||
selectable: bool,
|
selectable: bool,
|
||||||
@@ -17,7 +17,7 @@ pub struct Mention<'a> {
|
|||||||
impl<'a> Mention<'a> {
|
impl<'a> Mention<'a> {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
ndb: &'a Ndb,
|
ndb: &'a Ndb,
|
||||||
img_cache: &'a mut ImageCache,
|
img_cache: &'a mut Images,
|
||||||
txn: &'a Transaction,
|
txn: &'a Transaction,
|
||||||
pk: &'a [u8; 32],
|
pk: &'a [u8; 32],
|
||||||
) -> Self {
|
) -> Self {
|
||||||
@@ -62,9 +62,10 @@ impl egui::Widget for Mention<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn mention_ui(
|
fn mention_ui(
|
||||||
ndb: &Ndb,
|
ndb: &Ndb,
|
||||||
img_cache: &mut ImageCache,
|
img_cache: &mut Images,
|
||||||
txn: &Transaction,
|
txn: &Transaction,
|
||||||
pk: &[u8; 32],
|
pk: &[u8; 32],
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ pub mod anim;
|
|||||||
pub mod column;
|
pub mod column;
|
||||||
pub mod configure_deck;
|
pub mod configure_deck;
|
||||||
pub mod edit_deck;
|
pub mod edit_deck;
|
||||||
|
pub mod images;
|
||||||
pub mod mention;
|
pub mod mention;
|
||||||
pub mod note;
|
pub mod note;
|
||||||
pub mod preview;
|
pub mod preview;
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
|
use crate::gif::{handle_repaint, retrieve_latest_texture};
|
||||||
|
use crate::ui::images::render_images;
|
||||||
use crate::ui::{
|
use crate::ui::{
|
||||||
self,
|
self,
|
||||||
note::{NoteOptions, NoteResponse},
|
note::{NoteOptions, NoteResponse},
|
||||||
ProfilePic,
|
|
||||||
};
|
};
|
||||||
use crate::{actionbar::NoteAction, images::ImageType, timeline::TimelineKind};
|
use crate::{actionbar::NoteAction, images::ImageType, timeline::TimelineKind};
|
||||||
use egui::{Color32, Hyperlink, Image, RichText};
|
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::{ImageCache, 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,
|
||||||
img_cache: &'a mut ImageCache,
|
img_cache: &'a mut Images,
|
||||||
note_cache: &'a mut NoteCache,
|
note_cache: &'a mut NoteCache,
|
||||||
txn: &'a Transaction,
|
txn: &'a Transaction,
|
||||||
note: &'a Note<'a>,
|
note: &'a Note<'a>,
|
||||||
@@ -22,9 +23,10 @@ pub struct NoteContents<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> NoteContents<'a> {
|
impl<'a> NoteContents<'a> {
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn new(
|
pub fn new(
|
||||||
ndb: &'a Ndb,
|
ndb: &'a Ndb,
|
||||||
img_cache: &'a mut ImageCache,
|
img_cache: &'a mut Images,
|
||||||
note_cache: &'a mut NoteCache,
|
note_cache: &'a mut NoteCache,
|
||||||
txn: &'a Transaction,
|
txn: &'a Transaction,
|
||||||
note: &'a Note,
|
note: &'a Note,
|
||||||
@@ -72,7 +74,7 @@ pub fn render_note_preview(
|
|||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
ndb: &Ndb,
|
ndb: &Ndb,
|
||||||
note_cache: &mut NoteCache,
|
note_cache: &mut NoteCache,
|
||||||
img_cache: &mut ImageCache,
|
img_cache: &mut Images,
|
||||||
txn: &Transaction,
|
txn: &Transaction,
|
||||||
id: &[u8; 32],
|
id: &[u8; 32],
|
||||||
parent: NoteKey,
|
parent: NoteKey,
|
||||||
@@ -126,15 +128,11 @@ pub fn render_note_preview(
|
|||||||
.inner
|
.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)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn render_note_contents(
|
fn render_note_contents(
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
ndb: &Ndb,
|
ndb: &Ndb,
|
||||||
img_cache: &mut ImageCache,
|
img_cache: &mut Images,
|
||||||
note_cache: &mut NoteCache,
|
note_cache: &mut NoteCache,
|
||||||
txn: &Transaction,
|
txn: &Transaction,
|
||||||
note: &Note,
|
note: &Note,
|
||||||
@@ -145,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,9 +209,14 @@ fn render_note_contents(
|
|||||||
}
|
}
|
||||||
|
|
||||||
BlockType::Url => {
|
BlockType::Url => {
|
||||||
let lower_url = block.as_str().to_lowercase();
|
if !hide_media {
|
||||||
if !hide_media && is_image_link(&lower_url) {
|
let url = block.as_str().to_string();
|
||||||
images.push(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 {
|
} else {
|
||||||
#[cfg(feature = "profiling")]
|
#[cfg(feature = "profiling")]
|
||||||
puffin::profile_scope!("url contents");
|
puffin::profile_scope!("url contents");
|
||||||
@@ -279,8 +282,8 @@ fn rot13(input: &str) -> String {
|
|||||||
|
|
||||||
fn image_carousel(
|
fn image_carousel(
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
img_cache: &mut ImageCache,
|
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,56 +297,39 @@ 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 {
|
||||||
// If the cache is empty, initiate the fetch
|
render_images(
|
||||||
let m_cached_promise = img_cache.map().get(&image);
|
ui,
|
||||||
if m_cached_promise.is_none() {
|
img_cache,
|
||||||
let res = crate::images::fetch_img(
|
&image,
|
||||||
img_cache,
|
ImageType::Content(width.round() as u32, height.round() as u32),
|
||||||
ui.ctx(),
|
cache_type,
|
||||||
&image,
|
|ui| {
|
||||||
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 => {
|
|
||||||
ui.allocate_space(egui::vec2(spinsz, spinsz));
|
ui.allocate_space(egui::vec2(spinsz, spinsz));
|
||||||
//ui.add(egui::Spinner::new().size(spinsz));
|
},
|
||||||
}
|
|ui, _| {
|
||||||
// Failed to fetch image!
|
ui.allocate_space(egui::vec2(spinsz, spinsz));
|
||||||
Some(Err(_err)) => {
|
},
|
||||||
// FIXME - use content-specific error instead
|
|ui, url, renderable_media, gifs| {
|
||||||
let no_pfp = crate::images::fetch_img(
|
let texture = handle_repaint(
|
||||||
img_cache,
|
ui,
|
||||||
ui.ctx(),
|
retrieve_latest_texture(&image, gifs, renderable_media),
|
||||||
ProfilePic::no_pfp_url(),
|
|
||||||
ImageType::Profile(128),
|
|
||||||
);
|
);
|
||||||
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(
|
let img_resp = ui.add(
|
||||||
Image::new(img)
|
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(image);
|
ui.ctx().copy_text(url.to_owned());
|
||||||
ui.close_menu();
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.response
|
.response
|
||||||
|
|||||||
@@ -25,14 +25,14 @@ use egui::emath::{pos2, Vec2};
|
|||||||
use egui::{Id, Label, Pos2, Rect, Response, RichText, Sense};
|
use egui::{Id, Label, Pos2, Rect, Response, RichText, Sense};
|
||||||
use enostr::{NoteId, Pubkey};
|
use enostr::{NoteId, Pubkey};
|
||||||
use nostrdb::{Ndb, Note, NoteKey, Transaction};
|
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;
|
use super::profile::preview::one_line_display_name_widget;
|
||||||
|
|
||||||
pub struct NoteView<'a> {
|
pub struct NoteView<'a> {
|
||||||
ndb: &'a Ndb,
|
ndb: &'a Ndb,
|
||||||
note_cache: &'a mut NoteCache,
|
note_cache: &'a mut NoteCache,
|
||||||
img_cache: &'a mut ImageCache,
|
img_cache: &'a mut Images,
|
||||||
parent: Option<NoteKey>,
|
parent: Option<NoteKey>,
|
||||||
note: &'a nostrdb::Note<'a>,
|
note: &'a nostrdb::Note<'a>,
|
||||||
flags: NoteOptions,
|
flags: NoteOptions,
|
||||||
@@ -74,7 +74,7 @@ impl<'a> NoteView<'a> {
|
|||||||
pub fn new(
|
pub fn new(
|
||||||
ndb: &'a Ndb,
|
ndb: &'a Ndb,
|
||||||
note_cache: &'a mut NoteCache,
|
note_cache: &'a mut NoteCache,
|
||||||
img_cache: &'a mut ImageCache,
|
img_cache: &'a mut Images,
|
||||||
note: &'a nostrdb::Note<'a>,
|
note: &'a nostrdb::Note<'a>,
|
||||||
mut flags: NoteOptions,
|
mut flags: NoteOptions,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
use crate::draft::{Draft, Drafts, MentionHint};
|
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::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;
|
||||||
|
use crate::ui::images::render_images;
|
||||||
use crate::ui::search_results::SearchResultsView;
|
use crate::ui::search_results::SearchResultsView;
|
||||||
use crate::ui::{self, note::NoteOptions, Preview, PreviewConfig};
|
use crate::ui::{self, note::NoteOptions, Preview, PreviewConfig};
|
||||||
use crate::Result;
|
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 enostr::{FilledKeypair, FullKeypair, NoteId, Pubkey, RelayPool};
|
||||||
use nostrdb::{Ndb, Transaction};
|
use nostrdb::{Ndb, Transaction};
|
||||||
|
|
||||||
use notedeck::{ImageCache, NoteCache};
|
use notedeck::{supported_mime_hosted_at_url, Images, NoteCache};
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
use super::contents::render_note_preview;
|
use super::contents::render_note_preview;
|
||||||
@@ -22,7 +23,7 @@ pub struct PostView<'a> {
|
|||||||
ndb: &'a Ndb,
|
ndb: &'a Ndb,
|
||||||
draft: &'a mut Draft,
|
draft: &'a mut Draft,
|
||||||
post_type: PostType,
|
post_type: PostType,
|
||||||
img_cache: &'a mut ImageCache,
|
img_cache: &'a mut Images,
|
||||||
note_cache: &'a mut NoteCache,
|
note_cache: &'a mut NoteCache,
|
||||||
poster: FilledKeypair<'a>,
|
poster: FilledKeypair<'a>,
|
||||||
id_source: Option<egui::Id>,
|
id_source: Option<egui::Id>,
|
||||||
@@ -88,7 +89,7 @@ impl<'a> PostView<'a> {
|
|||||||
ndb: &'a Ndb,
|
ndb: &'a Ndb,
|
||||||
draft: &'a mut Draft,
|
draft: &'a mut Draft,
|
||||||
post_type: PostType,
|
post_type: PostType,
|
||||||
img_cache: &'a mut ImageCache,
|
img_cache: &'a mut Images,
|
||||||
note_cache: &'a mut NoteCache,
|
note_cache: &'a mut NoteCache,
|
||||||
poster: FilledKeypair<'a>,
|
poster: FilledKeypair<'a>,
|
||||||
inner_rect: egui::Rect,
|
inner_rect: egui::Rect,
|
||||||
@@ -384,49 +385,59 @@ impl<'a> PostView<'a> {
|
|||||||
} else {
|
} else {
|
||||||
(300, 300)
|
(300, 300)
|
||||||
};
|
};
|
||||||
let m_cached_promise = self.img_cache.map().get(&media.url);
|
|
||||||
if m_cached_promise.is_none() {
|
if let Some(cache_type) =
|
||||||
let promise = fetch_img(
|
supported_mime_hosted_at_url(&mut self.img_cache.urls, &media.url)
|
||||||
|
{
|
||||||
|
render_images(
|
||||||
|
ui,
|
||||||
self.img_cache,
|
self.img_cache,
|
||||||
ui.ctx(),
|
|
||||||
&media.url,
|
&media.url,
|
||||||
crate::images::ImageType::Content(width, height),
|
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
|
} else {
|
||||||
.map_mut()
|
self.draft
|
||||||
.insert(media.url.to_owned(), promise);
|
.upload_errors
|
||||||
}
|
.push("Uploaded media is not supported.".to_owned());
|
||||||
|
error!("Unsupported mime type at url: {}", &media.url);
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
to_remove.reverse();
|
to_remove.reverse();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use enostr::{FilledKeypair, NoteId};
|
use enostr::{FilledKeypair, NoteId};
|
||||||
use nostrdb::Ndb;
|
use nostrdb::Ndb;
|
||||||
use notedeck::{ImageCache, NoteCache};
|
use notedeck::{Images, NoteCache};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
draft::Draft,
|
draft::Draft,
|
||||||
@@ -13,7 +13,7 @@ pub struct QuoteRepostView<'a> {
|
|||||||
ndb: &'a Ndb,
|
ndb: &'a Ndb,
|
||||||
poster: FilledKeypair<'a>,
|
poster: FilledKeypair<'a>,
|
||||||
note_cache: &'a mut NoteCache,
|
note_cache: &'a mut NoteCache,
|
||||||
img_cache: &'a mut ImageCache,
|
img_cache: &'a mut Images,
|
||||||
draft: &'a mut Draft,
|
draft: &'a mut Draft,
|
||||||
quoting_note: &'a nostrdb::Note<'a>,
|
quoting_note: &'a nostrdb::Note<'a>,
|
||||||
id_source: Option<egui::Id>,
|
id_source: Option<egui::Id>,
|
||||||
@@ -27,7 +27,7 @@ impl<'a> QuoteRepostView<'a> {
|
|||||||
ndb: &'a Ndb,
|
ndb: &'a Ndb,
|
||||||
poster: FilledKeypair<'a>,
|
poster: FilledKeypair<'a>,
|
||||||
note_cache: &'a mut NoteCache,
|
note_cache: &'a mut NoteCache,
|
||||||
img_cache: &'a mut ImageCache,
|
img_cache: &'a mut Images,
|
||||||
draft: &'a mut Draft,
|
draft: &'a mut Draft,
|
||||||
quoting_note: &'a nostrdb::Note<'a>,
|
quoting_note: &'a nostrdb::Note<'a>,
|
||||||
inner_rect: egui::Rect,
|
inner_rect: egui::Rect,
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ use crate::ui::note::{NoteOptions, PostResponse, PostType};
|
|||||||
use enostr::{FilledKeypair, NoteId};
|
use enostr::{FilledKeypair, NoteId};
|
||||||
use nostrdb::Ndb;
|
use nostrdb::Ndb;
|
||||||
|
|
||||||
use notedeck::{ImageCache, NoteCache};
|
use notedeck::{Images, NoteCache};
|
||||||
|
|
||||||
pub struct PostReplyView<'a> {
|
pub struct PostReplyView<'a> {
|
||||||
ndb: &'a Ndb,
|
ndb: &'a Ndb,
|
||||||
poster: FilledKeypair<'a>,
|
poster: FilledKeypair<'a>,
|
||||||
note_cache: &'a mut NoteCache,
|
note_cache: &'a mut NoteCache,
|
||||||
img_cache: &'a mut ImageCache,
|
img_cache: &'a mut Images,
|
||||||
draft: &'a mut Draft,
|
draft: &'a mut Draft,
|
||||||
note: &'a nostrdb::Note<'a>,
|
note: &'a nostrdb::Note<'a>,
|
||||||
id_source: Option<egui::Id>,
|
id_source: Option<egui::Id>,
|
||||||
@@ -25,7 +25,7 @@ impl<'a> PostReplyView<'a> {
|
|||||||
poster: FilledKeypair<'a>,
|
poster: FilledKeypair<'a>,
|
||||||
draft: &'a mut Draft,
|
draft: &'a mut Draft,
|
||||||
note_cache: &'a mut NoteCache,
|
note_cache: &'a mut NoteCache,
|
||||||
img_cache: &'a mut ImageCache,
|
img_cache: &'a mut Images,
|
||||||
note: &'a nostrdb::Note<'a>,
|
note: &'a nostrdb::Note<'a>,
|
||||||
inner_rect: egui::Rect,
|
inner_rect: egui::Rect,
|
||||||
note_options: NoteOptions,
|
note_options: NoteOptions,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use crate::{
|
|||||||
};
|
};
|
||||||
use egui::{Label, RichText, Sense};
|
use egui::{Label, RichText, Sense};
|
||||||
use nostrdb::{Ndb, Note, NoteReply, Transaction};
|
use nostrdb::{Ndb, Note, NoteReply, Transaction};
|
||||||
use notedeck::{ImageCache, NoteCache};
|
use notedeck::{Images, NoteCache};
|
||||||
|
|
||||||
#[must_use = "Please handle the resulting note action"]
|
#[must_use = "Please handle the resulting note action"]
|
||||||
pub fn reply_desc(
|
pub fn reply_desc(
|
||||||
@@ -12,7 +12,7 @@ pub fn reply_desc(
|
|||||||
txn: &Transaction,
|
txn: &Transaction,
|
||||||
note_reply: &NoteReply,
|
note_reply: &NoteReply,
|
||||||
ndb: &Ndb,
|
ndb: &Ndb,
|
||||||
img_cache: &mut ImageCache,
|
img_cache: &mut Images,
|
||||||
note_cache: &mut NoteCache,
|
note_cache: &mut NoteCache,
|
||||||
note_options: NoteOptions,
|
note_options: NoteOptions,
|
||||||
) -> Option<NoteAction> {
|
) -> Option<NoteAction> {
|
||||||
@@ -29,7 +29,7 @@ pub fn reply_desc(
|
|||||||
// note link renderer helper
|
// note link renderer helper
|
||||||
let note_link = |ui: &mut egui::Ui,
|
let note_link = |ui: &mut egui::Ui,
|
||||||
note_cache: &mut NoteCache,
|
note_cache: &mut NoteCache,
|
||||||
img_cache: &mut ImageCache,
|
img_cache: &mut Images,
|
||||||
text: &str,
|
text: &str,
|
||||||
note: &Note<'_>| {
|
note: &Note<'_>| {
|
||||||
let r = ui.add(
|
let r = ui.add(
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use core::f32;
|
use core::f32;
|
||||||
|
|
||||||
use egui::{vec2, Button, Layout, Margin, RichText, Rounding, ScrollArea, TextEdit};
|
use egui::{vec2, Button, Layout, Margin, RichText, Rounding, ScrollArea, TextEdit};
|
||||||
use notedeck::{ImageCache, NotedeckTextStyle};
|
use notedeck::{Images, NotedeckTextStyle};
|
||||||
|
|
||||||
use crate::{colors, profile_state::ProfileState};
|
use crate::{colors, profile_state::ProfileState};
|
||||||
|
|
||||||
@@ -9,11 +9,11 @@ use super::{banner, unwrap_profile_url, ProfilePic};
|
|||||||
|
|
||||||
pub struct EditProfileView<'a> {
|
pub struct EditProfileView<'a> {
|
||||||
state: &'a mut ProfileState,
|
state: &'a mut ProfileState,
|
||||||
img_cache: &'a mut ImageCache,
|
img_cache: &'a mut Images,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> EditProfileView<'a> {
|
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 }
|
Self { state, img_cache }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ use crate::{
|
|||||||
NostrName,
|
NostrName,
|
||||||
};
|
};
|
||||||
|
|
||||||
use notedeck::{Accounts, ImageCache, MuteFun, NoteCache, NotedeckTextStyle, UnknownIds};
|
use notedeck::{Accounts, Images, MuteFun, NoteCache, NotedeckTextStyle, UnknownIds};
|
||||||
|
|
||||||
pub struct ProfileView<'a> {
|
pub struct ProfileView<'a> {
|
||||||
pubkey: &'a Pubkey,
|
pubkey: &'a Pubkey,
|
||||||
@@ -33,7 +33,7 @@ pub struct ProfileView<'a> {
|
|||||||
note_options: NoteOptions,
|
note_options: NoteOptions,
|
||||||
ndb: &'a Ndb,
|
ndb: &'a Ndb,
|
||||||
note_cache: &'a mut NoteCache,
|
note_cache: &'a mut NoteCache,
|
||||||
img_cache: &'a mut ImageCache,
|
img_cache: &'a mut Images,
|
||||||
unknown_ids: &'a mut UnknownIds,
|
unknown_ids: &'a mut UnknownIds,
|
||||||
is_muted: &'a MuteFun,
|
is_muted: &'a MuteFun,
|
||||||
}
|
}
|
||||||
@@ -52,7 +52,7 @@ impl<'a> ProfileView<'a> {
|
|||||||
timeline_cache: &'a mut TimelineCache,
|
timeline_cache: &'a mut TimelineCache,
|
||||||
ndb: &'a Ndb,
|
ndb: &'a Ndb,
|
||||||
note_cache: &'a mut NoteCache,
|
note_cache: &'a mut NoteCache,
|
||||||
img_cache: &'a mut ImageCache,
|
img_cache: &'a mut Images,
|
||||||
unknown_ids: &'a mut UnknownIds,
|
unknown_ids: &'a mut UnknownIds,
|
||||||
is_muted: &'a MuteFun,
|
is_muted: &'a MuteFun,
|
||||||
note_options: NoteOptions,
|
note_options: NoteOptions,
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
|
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::{Preview, PreviewConfig};
|
use crate::ui::{Preview, PreviewConfig};
|
||||||
use egui::{vec2, Sense, Stroke, TextureHandle};
|
use egui::{vec2, Sense, Stroke, TextureHandle};
|
||||||
use nostrdb::{Ndb, Transaction};
|
use nostrdb::{Ndb, Transaction};
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
use notedeck::{AppContext, ImageCache};
|
use notedeck::{supported_mime_hosted_at_url, AppContext, Images};
|
||||||
|
|
||||||
pub struct ProfilePic<'cache, 'url> {
|
pub struct ProfilePic<'cache, 'url> {
|
||||||
cache: &'cache mut ImageCache,
|
cache: &'cache mut Images,
|
||||||
url: &'url str,
|
url: &'url str,
|
||||||
size: f32,
|
size: f32,
|
||||||
border: Option<Stroke>,
|
border: Option<Stroke>,
|
||||||
@@ -20,7 +22,7 @@ impl egui::Widget for ProfilePic<'_, '_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'cache, 'url> ProfilePic<'cache, 'url> {
|
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();
|
let size = Self::default_size();
|
||||||
ProfilePic {
|
ProfilePic {
|
||||||
cache,
|
cache,
|
||||||
@@ -35,7 +37,7 @@ impl<'cache, 'url> ProfilePic<'cache, 'url> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_profile(
|
pub fn from_profile(
|
||||||
cache: &'cache mut ImageCache,
|
cache: &'cache mut Images,
|
||||||
profile: &nostrdb::ProfileRecord<'url>,
|
profile: &nostrdb::ProfileRecord<'url>,
|
||||||
) -> Option<Self> {
|
) -> Option<Self> {
|
||||||
profile
|
profile
|
||||||
@@ -80,7 +82,7 @@ impl<'cache, 'url> ProfilePic<'cache, 'url> {
|
|||||||
|
|
||||||
fn render_pfp(
|
fn render_pfp(
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
img_cache: &mut ImageCache,
|
img_cache: &mut Images,
|
||||||
url: &str,
|
url: &str,
|
||||||
ui_size: f32,
|
ui_size: f32,
|
||||||
border: Option<Stroke>,
|
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
|
// We will want to downsample these so it's not blurry on hi res displays
|
||||||
let img_size = 128u32;
|
let img_size = 128u32;
|
||||||
|
|
||||||
let m_cached_promise = img_cache.map().get(url);
|
let cache_type = supported_mime_hosted_at_url(&mut img_cache.urls, url)
|
||||||
if m_cached_promise.is_none() {
|
.unwrap_or(notedeck::MediaCacheType::Image);
|
||||||
let res = crate::images::fetch_img(img_cache, ui.ctx(), url, ImageType::Profile(img_size));
|
|
||||||
img_cache.map_mut().insert(url.to_owned(), res);
|
|
||||||
}
|
|
||||||
|
|
||||||
match img_cache.map()[url].ready() {
|
render_images(
|
||||||
None => paint_circle(ui, ui_size, border),
|
ui,
|
||||||
|
img_cache,
|
||||||
// Failed to fetch profile!
|
url,
|
||||||
Some(Err(_err)) => {
|
ImageType::Profile(img_size),
|
||||||
let m_failed_promise = img_cache.map().get(url);
|
cache_type,
|
||||||
if m_failed_promise.is_none() {
|
|ui| {
|
||||||
let no_pfp = crate::images::fetch_img(
|
paint_circle(ui, ui_size, border);
|
||||||
img_cache,
|
},
|
||||||
ui.ctx(),
|
|ui, _| {
|
||||||
ProfilePic::no_pfp_url(),
|
paint_circle(ui, ui_size, border);
|
||||||
ImageType::Profile(img_size),
|
},
|
||||||
);
|
|ui, url, renderable_media, gifs| {
|
||||||
img_cache.map_mut().insert(url.to_owned(), no_pfp);
|
let texture_handle =
|
||||||
}
|
handle_repaint(ui, retrieve_latest_texture(url, gifs, renderable_media));
|
||||||
|
pfp_image(ui, texture_handle, ui_size, border);
|
||||||
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),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pfp_image(
|
fn pfp_image(
|
||||||
|
|||||||
@@ -4,18 +4,18 @@ use egui::{Frame, Label, RichText, Widget};
|
|||||||
use egui_extras::Size;
|
use egui_extras::Size;
|
||||||
use nostrdb::ProfileRecord;
|
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};
|
use super::{about_section_widget, banner, display_name_widget, get_display_name, get_profile_url};
|
||||||
|
|
||||||
pub struct ProfilePreview<'a, 'cache> {
|
pub struct ProfilePreview<'a, 'cache> {
|
||||||
profile: &'a ProfileRecord<'a>,
|
profile: &'a ProfileRecord<'a>,
|
||||||
cache: &'cache mut ImageCache,
|
cache: &'cache mut Images,
|
||||||
banner_height: Size,
|
banner_height: Size,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 'cache> ProfilePreview<'a, 'cache> {
|
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);
|
let banner_height = Size::exact(80.0);
|
||||||
ProfilePreview {
|
ProfilePreview {
|
||||||
profile,
|
profile,
|
||||||
@@ -69,14 +69,14 @@ impl egui::Widget for ProfilePreview<'_, '_> {
|
|||||||
|
|
||||||
pub struct SimpleProfilePreview<'a, 'cache> {
|
pub struct SimpleProfilePreview<'a, 'cache> {
|
||||||
profile: Option<&'a ProfileRecord<'a>>,
|
profile: Option<&'a ProfileRecord<'a>>,
|
||||||
cache: &'cache mut ImageCache,
|
cache: &'cache mut Images,
|
||||||
is_nsec: bool,
|
is_nsec: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 'cache> SimpleProfilePreview<'a, 'cache> {
|
impl<'a, 'cache> SimpleProfilePreview<'a, 'cache> {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
profile: Option<&'a ProfileRecord<'a>>,
|
profile: Option<&'a ProfileRecord<'a>>,
|
||||||
cache: &'cache mut ImageCache,
|
cache: &'cache mut Images,
|
||||||
is_nsec: bool,
|
is_nsec: bool,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
SimpleProfilePreview {
|
SimpleProfilePreview {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use egui::{vec2, FontId, Pos2, Rect, ScrollArea, Vec2b};
|
use egui::{vec2, FontId, Pos2, Rect, ScrollArea, Vec2b};
|
||||||
use nostrdb::{Ndb, ProfileRecord, Transaction};
|
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 tracing::error;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -13,13 +13,13 @@ use super::{profile::get_profile_url, ProfilePic};
|
|||||||
pub struct SearchResultsView<'a> {
|
pub struct SearchResultsView<'a> {
|
||||||
ndb: &'a Ndb,
|
ndb: &'a Ndb,
|
||||||
txn: &'a Transaction,
|
txn: &'a Transaction,
|
||||||
img_cache: &'a mut ImageCache,
|
img_cache: &'a mut Images,
|
||||||
results: &'a Vec<&'a [u8; 32]>,
|
results: &'a Vec<&'a [u8; 32]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> SearchResultsView<'a> {
|
impl<'a> SearchResultsView<'a> {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
img_cache: &'a mut ImageCache,
|
img_cache: &'a mut Images,
|
||||||
ndb: &'a Ndb,
|
ndb: &'a Ndb,
|
||||||
txn: &'a Transaction,
|
txn: &'a Transaction,
|
||||||
results: &'a Vec<&'a [u8; 32]>,
|
results: &'a Vec<&'a [u8; 32]>,
|
||||||
@@ -84,7 +84,7 @@ impl<'a> SearchResultsView<'a> {
|
|||||||
|
|
||||||
fn user_result<'a>(
|
fn user_result<'a>(
|
||||||
profile: &'a ProfileRecord<'_>,
|
profile: &'a ProfileRecord<'_>,
|
||||||
cache: &'a mut ImageCache,
|
cache: &'a mut Images,
|
||||||
index: usize,
|
index: usize,
|
||||||
width: f32,
|
width: f32,
|
||||||
) -> impl egui::Widget + 'a {
|
) -> impl egui::Widget + 'a {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ use crate::{
|
|||||||
support::Support,
|
support::Support,
|
||||||
};
|
};
|
||||||
|
|
||||||
use notedeck::{Accounts, ImageCache, NotedeckTextStyle, ThemeHandler, UserAccount};
|
use notedeck::{Accounts, Images, NotedeckTextStyle, ThemeHandler, UserAccount};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE},
|
anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE},
|
||||||
@@ -29,7 +29,7 @@ static ICON_WIDTH: f32 = 40.0;
|
|||||||
|
|
||||||
pub struct DesktopSidePanel<'a> {
|
pub struct DesktopSidePanel<'a> {
|
||||||
ndb: &'a nostrdb::Ndb,
|
ndb: &'a nostrdb::Ndb,
|
||||||
img_cache: &'a mut ImageCache,
|
img_cache: &'a mut Images,
|
||||||
selected_account: Option<&'a UserAccount>,
|
selected_account: Option<&'a UserAccount>,
|
||||||
decks_cache: &'a DecksCache,
|
decks_cache: &'a DecksCache,
|
||||||
}
|
}
|
||||||
@@ -70,7 +70,7 @@ impl SidePanelResponse {
|
|||||||
impl<'a> DesktopSidePanel<'a> {
|
impl<'a> DesktopSidePanel<'a> {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
ndb: &'a nostrdb::Ndb,
|
ndb: &'a nostrdb::Ndb,
|
||||||
img_cache: &'a mut ImageCache,
|
img_cache: &'a mut Images,
|
||||||
selected_account: Option<&'a UserAccount>,
|
selected_account: Option<&'a UserAccount>,
|
||||||
decks_cache: &'a DecksCache,
|
decks_cache: &'a DecksCache,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use nostrdb::{Ndb, Transaction};
|
use nostrdb::{Ndb, Transaction};
|
||||||
use notedeck::{ImageCache, MuteFun, NoteCache, RootNoteId, UnknownIds};
|
use notedeck::{Images, MuteFun, NoteCache, RootNoteId, UnknownIds};
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
use super::timeline::TimelineTabView;
|
use super::timeline::TimelineTabView;
|
||||||
@@ -15,7 +15,7 @@ pub struct ThreadView<'a> {
|
|||||||
ndb: &'a Ndb,
|
ndb: &'a Ndb,
|
||||||
note_cache: &'a mut NoteCache,
|
note_cache: &'a mut NoteCache,
|
||||||
unknown_ids: &'a mut UnknownIds,
|
unknown_ids: &'a mut UnknownIds,
|
||||||
img_cache: &'a mut ImageCache,
|
img_cache: &'a mut Images,
|
||||||
selected_note_id: &'a [u8; 32],
|
selected_note_id: &'a [u8; 32],
|
||||||
note_options: NoteOptions,
|
note_options: NoteOptions,
|
||||||
id_source: egui::Id,
|
id_source: egui::Id,
|
||||||
@@ -29,7 +29,7 @@ impl<'a> ThreadView<'a> {
|
|||||||
ndb: &'a Ndb,
|
ndb: &'a Ndb,
|
||||||
note_cache: &'a mut NoteCache,
|
note_cache: &'a mut NoteCache,
|
||||||
unknown_ids: &'a mut UnknownIds,
|
unknown_ids: &'a mut UnknownIds,
|
||||||
img_cache: &'a mut ImageCache,
|
img_cache: &'a mut Images,
|
||||||
selected_note_id: &'a [u8; 32],
|
selected_note_id: &'a [u8; 32],
|
||||||
note_options: NoteOptions,
|
note_options: NoteOptions,
|
||||||
is_muted: &'a MuteFun,
|
is_muted: &'a MuteFun,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use egui::{vec2, Direction, Layout, Pos2, Stroke};
|
|||||||
use egui_tabs::TabColor;
|
use egui_tabs::TabColor;
|
||||||
use nostrdb::{Ndb, Transaction};
|
use nostrdb::{Ndb, Transaction};
|
||||||
use notedeck::note::root_note_id_from_selected_id;
|
use notedeck::note::root_note_id_from_selected_id;
|
||||||
use notedeck::{ImageCache, MuteFun, NoteCache};
|
use notedeck::{Images, MuteFun, NoteCache};
|
||||||
use tracing::{error, warn};
|
use tracing::{error, warn};
|
||||||
|
|
||||||
use super::anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE};
|
use super::anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE};
|
||||||
@@ -22,19 +22,20 @@ pub struct TimelineView<'a> {
|
|||||||
timeline_cache: &'a mut TimelineCache,
|
timeline_cache: &'a mut TimelineCache,
|
||||||
ndb: &'a Ndb,
|
ndb: &'a Ndb,
|
||||||
note_cache: &'a mut NoteCache,
|
note_cache: &'a mut NoteCache,
|
||||||
img_cache: &'a mut ImageCache,
|
img_cache: &'a mut Images,
|
||||||
note_options: NoteOptions,
|
note_options: NoteOptions,
|
||||||
reverse: bool,
|
reverse: bool,
|
||||||
is_muted: &'a MuteFun,
|
is_muted: &'a MuteFun,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> TimelineView<'a> {
|
impl<'a> TimelineView<'a> {
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn new(
|
pub fn new(
|
||||||
timeline_id: &'a TimelineKind,
|
timeline_id: &'a TimelineKind,
|
||||||
timeline_cache: &'a mut TimelineCache,
|
timeline_cache: &'a mut TimelineCache,
|
||||||
ndb: &'a Ndb,
|
ndb: &'a Ndb,
|
||||||
note_cache: &'a mut NoteCache,
|
note_cache: &'a mut NoteCache,
|
||||||
img_cache: &'a mut ImageCache,
|
img_cache: &'a mut Images,
|
||||||
note_options: NoteOptions,
|
note_options: NoteOptions,
|
||||||
is_muted: &'a MuteFun,
|
is_muted: &'a MuteFun,
|
||||||
) -> TimelineView<'a> {
|
) -> TimelineView<'a> {
|
||||||
@@ -78,7 +79,7 @@ fn timeline_ui(
|
|||||||
timeline_id: &TimelineKind,
|
timeline_id: &TimelineKind,
|
||||||
timeline_cache: &mut TimelineCache,
|
timeline_cache: &mut TimelineCache,
|
||||||
note_cache: &mut NoteCache,
|
note_cache: &mut NoteCache,
|
||||||
img_cache: &mut ImageCache,
|
img_cache: &mut Images,
|
||||||
reversed: bool,
|
reversed: bool,
|
||||||
note_options: NoteOptions,
|
note_options: NoteOptions,
|
||||||
is_muted: &MuteFun,
|
is_muted: &MuteFun,
|
||||||
@@ -321,7 +322,7 @@ pub struct TimelineTabView<'a> {
|
|||||||
txn: &'a Transaction,
|
txn: &'a Transaction,
|
||||||
ndb: &'a Ndb,
|
ndb: &'a Ndb,
|
||||||
note_cache: &'a mut NoteCache,
|
note_cache: &'a mut NoteCache,
|
||||||
img_cache: &'a mut ImageCache,
|
img_cache: &'a mut Images,
|
||||||
is_muted: &'a MuteFun,
|
is_muted: &'a MuteFun,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,7 +335,7 @@ impl<'a> TimelineTabView<'a> {
|
|||||||
txn: &'a Transaction,
|
txn: &'a Transaction,
|
||||||
ndb: &'a Ndb,
|
ndb: &'a Ndb,
|
||||||
note_cache: &'a mut NoteCache,
|
note_cache: &'a mut NoteCache,
|
||||||
img_cache: &'a mut ImageCache,
|
img_cache: &'a mut Images,
|
||||||
is_muted: &'a MuteFun,
|
is_muted: &'a MuteFun,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|||||||
Reference in New Issue
Block a user