[feat]: thumbnailing videos and loading images into slides
This commit is contained in:
parent
cafd113a3b
commit
cea2c87aa7
4 changed files with 134 additions and 32 deletions
|
|
@ -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,
|
||||
|
|
|
|||
101
src/main.rs
101
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) {
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue