use std::path::{Path, PathBuf};
use iced::widget::{container, image, row, text};
use iced::{Alignment, Element, Length};
use super::theme;
const CHIP_THUMB: f32 = 28.0;
const CHIP_NAME_MAX_CHARS: usize = 24;
#[derive(Debug)]
pub(crate) enum Refusal {
Ignored,
TooLarge(String),
}
#[derive(Debug, Clone)]
pub struct Attachment {
pub name: String,
pub source: Source,
pub is_image: bool,
}
#[derive(Debug, Clone)]
pub enum Source {
File(PathBuf),
#[cfg(target_os = "macos")]
Bytes(image::Handle),
}
pub(crate) fn intake(paths: impl IntoIterator<Item = PathBuf>) -> (Vec<Attachment>, Vec<String>) {
let mut attachments = Vec::new();
let mut refused = Vec::new();
for path in paths {
match Attachment::from_path(&path) {
Ok(attachment) => attachments.push(attachment),
Err(Refusal::Ignored) => {}
Err(Refusal::TooLarge(warning)) => refused.push(warning),
}
}
(attachments, refused)
}
impl Attachment {
pub(crate) fn from_path(path: &Path) -> Result<Self, Refusal> {
let (Some(metadata), Some(name)) = (
path.metadata().ok().filter(std::fs::Metadata::is_file),
path.file_name().and_then(std::ffi::OsStr::to_str),
) else {
return Err(Refusal::Ignored);
};
if metadata.len() > crate::util::FILE_MAX_BYTES {
return Err(Refusal::TooLarge(too_large(name)));
}
let is_image =
crate::util::has_extension(path, crate::channels::telegram::IMAGE_EXTENSIONS);
Ok(Self {
name: name.to_string(),
source: Source::File(path.to_path_buf()),
is_image,
})
}
#[cfg(target_os = "macos")]
pub(crate) fn from_image_bytes(name: String, bytes: Vec<u8>) -> Result<Self, String> {
if u64::try_from(bytes.len()).unwrap_or(u64::MAX) > crate::util::FILE_MAX_BYTES {
return Err(too_large("The copied image"));
}
Ok(Self {
name,
source: Source::Bytes(image::Handle::from_bytes(bytes)),
is_image: true,
})
}
pub(crate) fn preview_handle(&self) -> Option<image::Handle> {
if !self.is_image {
return None;
}
Some(match &self.source {
Source::File(path) => image::Handle::from_path(path.clone()),
#[cfg(target_os = "macos")]
Source::Bytes(handle) => handle.clone(),
})
}
}
#[derive(Debug, Clone, Default)]
pub struct Staged {
pub dirs: Vec<String>,
pub markers: String,
}
pub(crate) async fn stage(attachments: &[Attachment]) -> Result<Staged, String> {
let root = crate::util::telegram_files_root();
if let Err(error) = tokio::fs::create_dir_all(&root).await {
return Err(format!("Could not create the attachment folder: {error}"));
}
let base = crate::util::telegram_staging_dir_name(
"gui",
i64::try_from(crate::util::unix_millis()).unwrap_or(i64::MAX),
);
let dir = crate::util::create_unique_dir(&root, &base)
.await
.map_err(|error| format!("Could not create the attachment folder: {error}"))?;
let dir_name = dir
.file_name()
.and_then(std::ffi::OsStr::to_str)
.unwrap_or(base.as_str())
.to_string();
let mut markers = Vec::with_capacity(attachments.len());
for attachment in attachments {
match write_attachment(&dir, attachment).await {
Ok(path) => markers.push(format!(
"[{}:{}]",
if attachment.is_image { "IMAGE" } else { "FILE" },
path.display()
)),
Err(message) => {
let _ = tokio::fs::remove_dir_all(&dir).await;
return Err(message);
}
}
}
Ok(Staged {
dirs: vec![dir_name],
markers: markers.join("\n"),
})
}
pub(crate) async fn discard(dirs: &[String]) {
let root = crate::util::telegram_files_root();
for dir in dirs {
let _ = tokio::fs::remove_dir_all(root.join(dir)).await;
}
}
#[cfg(target_os = "macos")]
async fn write_attachment(dir: &Path, attachment: &Attachment) -> Result<PathBuf, String> {
match &attachment.source {
Source::File(source) => copy_file(dir, attachment, source).await,
Source::Bytes(image::Handle::Bytes(_, bytes)) => write_bytes(dir, attachment, bytes).await,
Source::Bytes(_) => Err("a pasted image is held as encoded bytes".to_string()),
}
}
#[cfg(not(target_os = "macos"))]
async fn write_attachment(dir: &Path, attachment: &Attachment) -> Result<PathBuf, String> {
let Source::File(source) = &attachment.source;
copy_file(dir, attachment, source).await
}
async fn copy_file(dir: &Path, attachment: &Attachment, source: &Path) -> Result<PathBuf, String> {
let size = tokio::fs::metadata(source)
.await
.map_err(|error| format!("Could not read {}: {error}", attachment.name))?
.len();
if size > crate::util::FILE_MAX_BYTES {
return Err(too_large(&attachment.name));
}
let (file, path) = crate::util::create_unique_file(dir, &leaf_name(&attachment.name))
.await
.map_err(|error| format!("Could not attach {}: {error}", attachment.name))?;
drop(file);
tokio::fs::copy(source, &path)
.await
.map(|_| path)
.map_err(|error| format!("Could not attach {}: {error}", attachment.name))
}
#[cfg(target_os = "macos")]
async fn write_bytes(dir: &Path, attachment: &Attachment, bytes: &[u8]) -> Result<PathBuf, String> {
if u64::try_from(bytes.len()).unwrap_or(u64::MAX) > crate::util::FILE_MAX_BYTES {
return Err(too_large(&attachment.name));
}
crate::util::write_unique_file(dir, &leaf_name(&attachment.name), bytes)
.await
.map_err(|error| format!("Could not attach {}: {error}", attachment.name))
}
fn too_large(name: &str) -> String {
format!(
"{name} is too large to attach (maximum {} MB)",
crate::util::FILE_MAX_BYTES / (1024 * 1024)
)
}
fn leaf_name(raw: &str) -> String {
let sanitized = crate::util::neutralized_name(raw);
if sanitized.is_empty() || sanitized == "." || sanitized == ".." {
"attachment".to_string()
} else {
sanitized
}
}
pub(crate) fn chip<'a, M: Clone + 'a>(attachment: &'a Attachment, remove: M) -> Element<'a, M> {
let thumb: Element<'a, M> = match attachment.preview_handle() {
Some(handle) => image(handle)
.width(Length::Fixed(CHIP_THUMB))
.height(Length::Fixed(CHIP_THUMB))
.border_radius(4.0)
.into(),
None => container(
iced_fonts::lucide::file_text::<iced::Theme, iced::Renderer>()
.size(theme::TEXT_16)
.color(theme::TEXT_SECONDARY),
)
.width(Length::Fixed(CHIP_THUMB))
.height(Length::Fixed(CHIP_THUMB))
.center_x(Length::Fixed(CHIP_THUMB))
.center_y(Length::Fixed(CHIP_THUMB))
.into(),
};
container(
row![
thumb,
text(crate::util::truncate(&attachment.name, CHIP_NAME_MAX_CHARS))
.size(theme::TEXT_12)
.color(theme::TEXT_SECONDARY),
super::widgets::compact_x_button(
[theme::PAD_1, theme::PAD_4],
Some(remove),
Some("remove attachment"),
),
]
.spacing(theme::SPACE_6)
.align_y(Alignment::Center),
)
.padding([theme::PAD_2, theme::PAD_6])
.style(theme::elevated_card_style)
.into()
}