[feat]: thumbnailing videos and loading images into slides
Some checks failed
/ clippy (push) Failing after 6m7s
/ test (push) Has been cancelled

This commit is contained in:
Chris Cochrun 2026-04-30 11:28:51 -05:00
parent cafd113a3b
commit cea2c87aa7
4 changed files with 134 additions and 32 deletions

View file

@ -21,6 +21,8 @@ use super::songs::Song;
pub struct Slide {
id: i32,
pub(crate) background: Background,
#[serde(skip)]
pub(crate) thumbnail: Option<Allocation>,
text: String,
font: Option<Font>,
font_size: i32,

View file

@ -16,6 +16,7 @@ use cosmic::iced::{
window,
};
use cosmic::widget::dnd_destination::dnd_destination;
use cosmic::widget::image::Handle;
use cosmic::widget::menu::key_bind::Modifier;
use cosmic::widget::menu::{ItemWidth, KeyBind};
use cosmic::widget::nav_bar::nav_bar_style;
@ -50,6 +51,7 @@ use crate::core::content::Content;
use crate::core::file;
use crate::core::kinds::ServiceItemKind;
use crate::core::model::KindWrapper;
use crate::ui::gst_video;
use crate::ui::image_editor::{self, ImageEditor};
use crate::ui::presentation_editor::{self, PresentationEditor};
use crate::ui::text_svg::{self};
@ -229,6 +231,7 @@ enum Message {
ShowGeniusToken,
SetGeniusToken(String),
InsertBackgroundImage((iced::core::image::Allocation, usize)),
InsertThumbnail((iced::core::image::Allocation, usize)),
}
#[allow(dead_code)]
@ -1323,43 +1326,97 @@ impl cosmic::Application for App {
})
.collect();
}
let mut tasks = Vec::new();
let task = if matches!(
if matches!(
item.kind,
ServiceItemKind::Song(_) | ServiceItemKind::Image(_)
) {
let path = item
if let Some(path) = item
.slides
.first()
.filter(|slide| slide.background.kind == BackgroundKind::Image)
.map(|slide| slide.background.path.clone())
.unwrap_or_default();
let item_index = self.service.len();
cosmic::iced::runtime::image::allocate(path).map(move |allocation| {
match allocation {
{
let item_index = self.service.len();
tasks.push(cosmic::iced::runtime::image::allocate(path).map(
move |allocation| match allocation {
Ok(allocation) => {
cosmic::Action::App(Message::InsertBackgroundImage((
allocation, item_index,
)))
}
Err(e) => {
error!("{e}");
cosmic::Action::App(Message::None)
}
},
))
}
};
if matches!(
item.kind,
ServiceItemKind::Song(_) | ServiceItemKind::Video(_)
) {
if let Some(path) = item
.slides
.first()
.filter(|slide| slide.background.kind == BackgroundKind::Video)
.map(|slide| slide.background.path.clone())
{
let item_index = self.service.len();
let task = cosmic::Task::future(async move {
let url =
url::Url::from_file_path(&path).expect("Should be here");
let file_name =
path.file_name().expect("hope").to_string_lossy();
let mut output =
dirs::cache_dir().expect("Should have on every platform");
output.push("lumina");
output.push("video_thumbnails");
if !output.exists() {
if let Err(e) = std::fs::create_dir_all(&output) {
error!("{e}");
}
}
output.push(file_name.to_string());
debug!(?output);
match gst_video::thumbnail(&url, &mut output) {
Ok(handle) => handle,
Err(e) => {
error!("{e}");
Handle::from_path(path)
}
}
})
.then(cosmic::iced::runtime::image::allocate)
.map(move |allocation| match allocation {
Ok(allocation) => cosmic::Action::App(
Message::InsertBackgroundImage((allocation, item_index)),
Message::InsertThumbnail((allocation, item_index)),
),
Err(e) => {
error!("{e}");
cosmic::Action::App(Message::None)
}
}
})
// Task::perform(load_images(path), move |res| match res {
// Ok(handle) => cosmic::Action::App(
// Message::InsertBackgroundImage((handle, item_index)),
// ),
// Err(e) => {
// error!("Couldn't load image: {e:?}");
// cosmic::Action::None
// }
// })
} else {
Task::none()
};
});
tasks.push(task)
}
}
Arc::make_mut(&mut self.service).push(item);
self.presenter.update_items(Arc::clone(&self.service));
task
Task::batch(tasks)
}
Message::InsertThumbnail((allocation, item_index)) => {
if let Some(item) = Arc::make_mut(&mut self.service).get_mut(item_index) {
debug!(?allocation, item_index, "Inserting handle into item: ");
for slide in &mut item.slides {
slide.thumbnail = Some(allocation.clone());
}
}
self.presenter.update_items(Arc::clone(&self.service));
Task::none()
}
Message::InsertBackgroundImage((allocation, item_index)) => {
if let Some(item) = Arc::make_mut(&mut self.service).get_mut(item_index) {

View file

@ -1,6 +1,6 @@
use std::fmt::Display;
use std::num::NonZero;
use std::path::Path;
use std::path::{Path, PathBuf};
use std::time::Duration;
use cosmic::widget::image::Handle;
@ -8,15 +8,26 @@ use iced_video_player::gst_app::prelude::*;
use iced_video_player::gst_app::{self};
use iced_video_player::{Position, Video, gst};
use image::{DynamicImage, ImageFormat, RgbaImage};
use tracing::debug;
use url::Url;
#[derive(Debug, Default)]
#[derive(Debug)]
pub struct VideoSettings {
pub mute: bool,
pub framerate: u16,
pub appsink_name: String,
}
impl Default for VideoSettings {
fn default() -> Self {
Self {
mute: true,
framerate: 60,
appsink_name: String::from("lumina_video"),
}
}
}
type Result<T> = std::result::Result<T, VideoError>;
pub fn create_video(url: &Url, settings: &VideoSettings) -> Result<Video> {
@ -67,15 +78,18 @@ pub fn create_video(url: &Url, settings: &VideoSettings) -> Result<Video> {
.map_err(VideoError::IcedVideoError)
}
pub fn thumbnail(input: &Url, output: &Path) -> Result<Handle> {
pub fn thumbnail(input: &Url, output: &mut PathBuf) -> Result<Handle> {
output.set_extension("png");
if output.exists() {
let image = image::open(&output).map_err(VideoError::ThumbnailImageError)?;
let (width, height, pixels) =
(image.width(), image.height(), image.to_rgba8().to_vec());
return Ok(Handle::from_rgba(width, height, pixels));
}
debug!(?output);
let thumbnails = {
let mut video = create_video(
input,
&VideoSettings {
mute: true,
..Default::default()
},
)?;
let mut video = create_video(input, &VideoSettings::default())?;
let duration = video.duration();
//TODO: how best to decide time?
@ -104,6 +118,11 @@ pub fn thumbnail(input: &Url, output: &Path) -> Result<Handle> {
VideoError::ThumbnailError(String::from("Cannot convert handle to image"))
})?;
if !output.exists() {
output.set_extension("png");
debug!(?output);
}
image
.save_with_format(output, ImageFormat::Png)
.map_err(VideoError::ThumbnailImageError)?;
@ -125,6 +144,7 @@ pub enum VideoError {
IcedVideoError(iced_video_player::Error),
GlibError(gst::glib::Error),
ThumbnailImageError(image::ImageError),
IOError(std::io::Error),
}
impl std::error::Error for VideoError {}
@ -144,6 +164,9 @@ impl Display for VideoError {
Self::ThumbnailImageError(error) => {
write!(f, "ImageError: {error}")
}
Self::IOError(error) => {
write!(f, "IOError: {error}")
}
}
}
}

View file

@ -1096,6 +1096,26 @@ pub(crate) fn slide_view<'a>(
.center(Length::Fill)
.clip(true),
);
} else if delegate {
stack = stack.push(slide.thumbnail.as_ref().map_or_else(
|| {
Container::new(space::horizontal())
.center(Length::Fill)
.clip(true)
},
|allocation| {
loaded_image(
allocation.handle(),
cosmic_image(allocation.handle())
.content_fit(ContentFit::Contain)
.width(width)
.height(Length::Fill),
)
.apply(container)
.center(Length::Fill)
.clip(true)
},
));
}
}
BackgroundKind::Pdf => {