From cea2c87aa74b76938f87c57f4b9826f3e8ea9169 Mon Sep 17 00:00:00 2001 From: Chris Cochrun Date: Thu, 30 Apr 2026 11:28:51 -0500 Subject: [PATCH] [feat]: thumbnailing videos and loading images into slides --- src/core/slide.rs | 2 + src/main.rs | 101 ++++++++++++++++++++++++++++++++++---------- src/ui/gst_video.rs | 43 ++++++++++++++----- src/ui/presenter.rs | 20 +++++++++ 4 files changed, 134 insertions(+), 32 deletions(-) diff --git a/src/core/slide.rs b/src/core/slide.rs index a426ecf..49329e8 100755 --- a/src/core/slide.rs +++ b/src/core/slide.rs @@ -21,6 +21,8 @@ use super::songs::Song; pub struct Slide { id: i32, pub(crate) background: Background, + #[serde(skip)] + pub(crate) thumbnail: Option, text: String, font: Option, font_size: i32, diff --git a/src/main.rs b/src/main.rs index 22c4837..0a34ae3 100755 --- a/src/main.rs +++ b/src/main.rs @@ -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) { diff --git a/src/ui/gst_video.rs b/src/ui/gst_video.rs index e99f41e..41c65bd 100644 --- a/src/ui/gst_video.rs +++ b/src/ui/gst_video.rs @@ -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 = std::result::Result; pub fn create_video(url: &Url, settings: &VideoSettings) -> Result