use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use anyhow::Result;
use image::imageops::FilterType;
use ratatui::layout::Size;
use ratatui_image::picker::Picker;
use ratatui_image::sliced::SlicedProtocol;
use ratatui_image::{FontSize, Resize};
use tokio::sync::mpsc::UnboundedSender;
use crate::messages::Preview;
use crate::tg::Tg;
const RESIZE: Resize = Resize::Scale(Some(FilterType::Triangle));
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct Key {
pub file_id: i32,
pub cols: u16,
pub rows: u16,
pub thumbnail: bool,
}
pub struct ImageEvent {
key: Key,
result: Result<SlicedProtocol>,
}
enum FileState {
Downloading,
Ready(String),
Failed,
}
pub struct Images {
picker: Picker,
tx: UnboundedSender<ImageEvent>,
ready: HashMap<Key, SlicedProtocol>,
building: HashSet<Key>,
failed: HashSet<Key>,
files: HashMap<i32, FileState>,
wanted: Vec<(Preview, u16, u16)>,
}
impl Images {
pub fn new(picker: Picker, tx: UnboundedSender<ImageEvent>) -> Self {
Self {
picker,
tx,
ready: HashMap::new(),
building: HashSet::new(),
failed: HashSet::new(),
files: HashMap::new(),
wanted: Vec::new(),
}
}
pub fn font_size(&self) -> FontSize {
self.picker.font_size()
}
pub fn get(&self, photo: &Preview, cols: u16, rows: u16) -> Option<&SlicedProtocol> {
let key = |thumbnail| Key {
file_id: photo.file_id,
cols,
rows,
thumbnail,
};
self.ready
.get(&key(false))
.or_else(|| self.ready.get(&key(true)))
}
pub fn is_broken(&self, photo: &Preview) -> bool {
matches!(self.files.get(&photo.file_id), Some(FileState::Failed))
}
pub fn want(&mut self, photo: &Preview, cols: u16, rows: u16) {
self.wanted.push((photo.clone(), cols, rows));
}
pub fn fetch(&mut self, tg: &Tg) {
for (photo, cols, rows) in std::mem::take(&mut self.wanted) {
let full = Key {
file_id: photo.file_id,
cols,
rows,
thumbnail: false,
};
if self.ready.contains_key(&full) {
continue;
}
match self.files.get(&photo.file_id) {
Some(FileState::Ready(path)) => {
let path = path.clone();
self.build(full, move || Ok(image::open(path)?));
}
Some(FileState::Downloading | FileState::Failed) => {}
None => {
self.files.insert(photo.file_id, FileState::Downloading);
tg.download(photo.file_id);
}
}
if let Some(data) = photo.thumbnail {
let key = Key {
thumbnail: true,
..full
};
self.build(key, move || Ok(image::load_from_memory(&data)?));
}
}
}
pub fn on_downloaded(&mut self, file_id: i32, path: Option<String>) {
let state = path.map_or(FileState::Failed, FileState::Ready);
self.files.insert(file_id, state);
}
pub fn on_built(&mut self, event: ImageEvent) {
self.building.remove(&event.key);
match event.result {
Ok(image) => {
self.ready.insert(event.key, image);
}
Err(_) => {
self.failed.insert(event.key);
if !event.key.thumbnail {
self.files.insert(event.key.file_id, FileState::Failed);
}
}
}
}
pub fn clear(&mut self) {
self.ready.clear();
}
fn build(
&mut self,
key: Key,
decode: impl FnOnce() -> Result<image::DynamicImage> + Send + 'static,
) {
if self.ready.contains_key(&key)
|| self.building.contains(&key)
|| self.failed.contains(&key)
{
return;
}
self.building.insert(key);
let picker = self.picker.clone();
let tx = self.tx.clone();
tokio::task::spawn_blocking(move || {
let result = decode().and_then(|image| {
let size = Size::new(key.cols, key.rows);
Ok(SlicedProtocol::new_with_resize(
&picker, image, size, RESIZE,
)?)
});
let _ = tx.send(ImageEvent { key, result });
});
}
#[cfg(test)]
pub fn insert_ready(&mut self, key: Key, image: image::DynamicImage) {
let size = Size::new(key.cols, key.rows);
let image = SlicedProtocol::new_with_resize(&self.picker, image, size, RESIZE).unwrap();
self.ready.insert(key, image);
}
}
pub type Thumbnail = Arc<[u8]>;