651 lines
17 KiB
Rust
651 lines
17 KiB
Rust
// use cosmic::dialog::ashpd::url::Url;
|
|
use crisp::types::{Keyword, Symbol, Value};
|
|
use gstreamer::query::Uri;
|
|
use iced_video_player::Video;
|
|
use miette::{miette, Result};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::{
|
|
fmt::Display,
|
|
path::{Path, PathBuf},
|
|
};
|
|
use tracing::error;
|
|
|
|
use super::songs::Song;
|
|
|
|
#[derive(
|
|
Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize,
|
|
)]
|
|
pub enum TextAlignment {
|
|
TopLeft,
|
|
TopCenter,
|
|
TopRight,
|
|
MiddleLeft,
|
|
#[default]
|
|
MiddleCenter,
|
|
MiddleRight,
|
|
BottomLeft,
|
|
BottomCenter,
|
|
BottomRight,
|
|
}
|
|
|
|
impl From<Value> for TextAlignment {
|
|
fn from(value: Value) -> Self {
|
|
Self::from(&value)
|
|
}
|
|
}
|
|
|
|
impl From<&Value> for TextAlignment {
|
|
fn from(value: &Value) -> Self {
|
|
if value == &Value::Symbol("center".into()) {
|
|
Self::MiddleCenter
|
|
} else {
|
|
Self::TopCenter
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(
|
|
Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize,
|
|
)]
|
|
pub struct Background {
|
|
pub path: PathBuf,
|
|
pub kind: BackgroundKind,
|
|
}
|
|
|
|
impl TryFrom<Background> for Video {
|
|
type Error = ParseError;
|
|
|
|
fn try_from(
|
|
value: Background,
|
|
) -> std::result::Result<Self, Self::Error> {
|
|
Video::new(
|
|
&url::Url::from_file_path(value.path)
|
|
.map_err(|_| ParseError::BackgroundNotVideo)?,
|
|
)
|
|
.map_err(|_| ParseError::BackgroundNotVideo)
|
|
}
|
|
}
|
|
|
|
impl TryFrom<String> for Background {
|
|
type Error = ParseError;
|
|
fn try_from(value: String) -> Result<Self, Self::Error> {
|
|
Background::try_from(value.as_str())
|
|
}
|
|
}
|
|
|
|
impl TryFrom<PathBuf> for Background {
|
|
type Error = ParseError;
|
|
fn try_from(path: PathBuf) -> Result<Self, Self::Error> {
|
|
let path = if path.starts_with("~") {
|
|
let path = path.to_str().unwrap().to_string();
|
|
let path = path.trim_start_matches("file://");
|
|
let home = dirs::home_dir()
|
|
.unwrap()
|
|
.to_str()
|
|
.unwrap()
|
|
.to_string();
|
|
let path = path.replace("~", &home);
|
|
PathBuf::from(path)
|
|
} else {
|
|
path
|
|
};
|
|
|
|
match path.canonicalize() {
|
|
Ok(value) => {
|
|
let extension = value
|
|
.extension()
|
|
.unwrap_or_default()
|
|
.to_str()
|
|
.unwrap_or_default();
|
|
match extension {
|
|
"jpeg" | "jpg" | "png" | "webp" | "html" => {
|
|
Ok(Self {
|
|
path: value,
|
|
kind: BackgroundKind::Image,
|
|
})
|
|
}
|
|
"mp4" | "mkv" | "webm" => Ok(Self {
|
|
path: value,
|
|
kind: BackgroundKind::Video,
|
|
}),
|
|
_ => Err(ParseError::NonBackgroundFile),
|
|
}
|
|
}
|
|
Err(e) => {
|
|
error!("Couldn't canonicalize: {e} {:?}", path);
|
|
Err(ParseError::CannotCanonicalize)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl TryFrom<&str> for Background {
|
|
type Error = ParseError;
|
|
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
|
let value = value.trim_start_matches("file://");
|
|
if value.starts_with("~") {
|
|
if let Some(home) = dirs::home_dir() {
|
|
if let Some(home) = home.to_str() {
|
|
let value = value.replace("~", home);
|
|
Self::try_from(PathBuf::from(value))
|
|
} else {
|
|
Self::try_from(PathBuf::from(value))
|
|
}
|
|
} else {
|
|
Self::try_from(PathBuf::from(value))
|
|
}
|
|
} else if value.starts_with("./") {
|
|
Err(ParseError::CannotCanonicalize)
|
|
} else {
|
|
Self::try_from(PathBuf::from(value))
|
|
}
|
|
}
|
|
}
|
|
|
|
impl TryFrom<&Path> for Background {
|
|
type Error = ParseError;
|
|
fn try_from(value: &Path) -> Result<Self, Self::Error> {
|
|
Self::try_from(PathBuf::from(value))
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum ParseError {
|
|
NonBackgroundFile,
|
|
DoesNotExist,
|
|
CannotCanonicalize,
|
|
BackgroundNotVideo,
|
|
}
|
|
|
|
impl std::error::Error for ParseError {}
|
|
|
|
impl Display for ParseError {
|
|
fn fmt(
|
|
&self,
|
|
f: &mut std::fmt::Formatter<'_>,
|
|
) -> std::fmt::Result {
|
|
let message = match self {
|
|
Self::NonBackgroundFile => {
|
|
"The file is not a recognized image or video type"
|
|
}
|
|
Self::DoesNotExist => "This file doesn't exist",
|
|
Self::CannotCanonicalize => {
|
|
"Could not canonicalize this file"
|
|
}
|
|
Self::BackgroundNotVideo => {
|
|
"This background isn't a video"
|
|
}
|
|
};
|
|
write!(f, "Error: {message}")
|
|
}
|
|
}
|
|
|
|
#[derive(
|
|
Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize,
|
|
)]
|
|
pub enum BackgroundKind {
|
|
#[default]
|
|
Image,
|
|
Video,
|
|
}
|
|
|
|
impl From<String> for BackgroundKind {
|
|
fn from(value: String) -> Self {
|
|
if value == "image" {
|
|
BackgroundKind::Image
|
|
} else {
|
|
BackgroundKind::Video
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(
|
|
Clone, Debug, Default, PartialEq, Serialize, Deserialize,
|
|
)]
|
|
pub struct Slide {
|
|
id: i32,
|
|
background: Background,
|
|
text: String,
|
|
font: String,
|
|
font_size: i32,
|
|
text_alignment: TextAlignment,
|
|
audio: Option<PathBuf>,
|
|
video_loop: bool,
|
|
video_start_time: f32,
|
|
video_end_time: f32,
|
|
}
|
|
|
|
impl From<&Slide> for Value {
|
|
fn from(value: &Slide) -> Self {
|
|
Self::List(vec![Self::Symbol(Symbol("slide".into()))])
|
|
}
|
|
}
|
|
|
|
impl Slide {
|
|
pub fn background(&self) -> &Background {
|
|
&self.background
|
|
}
|
|
|
|
pub fn text(&self) -> String {
|
|
self.text.clone()
|
|
}
|
|
|
|
pub fn font_size(&self) -> i32 {
|
|
self.font_size
|
|
}
|
|
|
|
pub fn font(&self) -> String {
|
|
self.font.clone()
|
|
}
|
|
|
|
pub fn video_loop(&self) -> bool {
|
|
self.video_loop
|
|
}
|
|
|
|
pub fn audio(&self) -> Option<PathBuf> {
|
|
self.audio.clone()
|
|
}
|
|
|
|
pub fn song_slides(song: &Song) -> Result<Vec<Self>> {
|
|
let lyrics = song.get_lyrics()?;
|
|
let slides: Vec<Slide> = lyrics
|
|
.iter()
|
|
.map(|l| {
|
|
let song = song.clone();
|
|
SlideBuilder::new()
|
|
.background(song.background.unwrap_or_default())
|
|
.font(song.font.unwrap_or_default())
|
|
.font_size(song.font_size.unwrap_or_default())
|
|
.text_alignment(
|
|
song.text_alignment.unwrap_or_default(),
|
|
)
|
|
.audio(song.audio.unwrap_or_default())
|
|
.video_loop(true)
|
|
.video_start_time(0.0)
|
|
.video_end_time(0.0)
|
|
.text(l)
|
|
.build()
|
|
.unwrap_or_default()
|
|
})
|
|
.collect();
|
|
|
|
Ok(slides)
|
|
}
|
|
|
|
pub(crate) fn set_index(&mut self, index: i32) {
|
|
self.id = index;
|
|
}
|
|
|
|
// pub fn slides_from_item(item: &ServiceItem) -> Result<Vec<Self>> {
|
|
// todo!()
|
|
// }
|
|
}
|
|
|
|
impl From<Value> for Slide {
|
|
fn from(value: Value) -> Self {
|
|
Self::from(&value)
|
|
}
|
|
}
|
|
|
|
impl From<&Value> for Slide {
|
|
fn from(value: &Value) -> Self {
|
|
match value {
|
|
Value::List(list) => lisp_to_slide(list),
|
|
_ => Slide::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn lisp_to_slide(lisp: &Vec<Value>) -> Slide {
|
|
const DEFAULT_BACKGROUND_LOCATION: usize = 1;
|
|
const DEFAULT_TEXT_LOCATION: usize = 0;
|
|
|
|
let mut slide = SlideBuilder::new();
|
|
let background_position = if let Some(background) =
|
|
lisp.iter().position(|v| {
|
|
v == &Value::Keyword(Keyword::from("background"))
|
|
}) {
|
|
background + 1
|
|
} else {
|
|
DEFAULT_BACKGROUND_LOCATION
|
|
};
|
|
|
|
if let Some(background) = lisp.get(background_position) {
|
|
slide = slide.background(lisp_to_background(background));
|
|
} else {
|
|
slide = slide.background(Background::default());
|
|
};
|
|
|
|
let text_position = lisp.iter().position(|v| match v {
|
|
Value::List(vec) => {
|
|
vec[DEFAULT_TEXT_LOCATION]
|
|
== Value::Symbol(Symbol::from("text"))
|
|
}
|
|
_ => false,
|
|
});
|
|
|
|
if let Some(text_position) = text_position {
|
|
if let Some(text) = lisp.get(text_position) {
|
|
slide = slide.text(lisp_to_text(text));
|
|
} else {
|
|
slide = slide.text("");
|
|
}
|
|
} else {
|
|
slide = slide.text("");
|
|
}
|
|
|
|
if let Some(text_position) = text_position {
|
|
if let Some(text) = lisp.get(text_position) {
|
|
slide = slide.font_size(lisp_to_font_size(text));
|
|
} else {
|
|
slide = slide.font_size(0);
|
|
}
|
|
} else {
|
|
slide = slide.font_size(0);
|
|
}
|
|
|
|
slide = slide
|
|
.font("Quicksand")
|
|
.text_alignment(TextAlignment::MiddleCenter)
|
|
.video_loop(false)
|
|
.video_start_time(0.0)
|
|
.video_end_time(0.0);
|
|
|
|
match slide.build() {
|
|
Ok(slide) => slide,
|
|
Err(e) => {
|
|
error!("Shoot! Slide didn't build: {e}");
|
|
Slide::default()
|
|
}
|
|
}
|
|
}
|
|
|
|
fn lisp_to_font_size(lisp: &Value) -> i32 {
|
|
match lisp {
|
|
Value::List(list) => {
|
|
if let Some(font_size_position) =
|
|
list.iter().position(|v| {
|
|
v == &Value::Keyword(Keyword::from("font-size"))
|
|
})
|
|
{
|
|
if let Some(font_size_value) =
|
|
list.get(font_size_position + 1)
|
|
{
|
|
font_size_value.into()
|
|
} else {
|
|
50
|
|
}
|
|
} else {
|
|
50
|
|
}
|
|
}
|
|
_ => 50,
|
|
}
|
|
}
|
|
|
|
fn lisp_to_text(lisp: &Value) -> impl Into<String> {
|
|
match lisp {
|
|
Value::List(list) => list[1].clone(),
|
|
_ => "".into(),
|
|
}
|
|
}
|
|
|
|
// Need to return a Result here so that we can propogate
|
|
// errors and then handle them appropriately
|
|
pub fn lisp_to_background(lisp: &Value) -> Background {
|
|
match lisp {
|
|
Value::List(list) => {
|
|
let kind = list[0].clone();
|
|
if let Some(source) = list.iter().position(|v| {
|
|
v == &Value::Keyword(Keyword::from("source"))
|
|
}) {
|
|
let source = &list[source + 1];
|
|
match source {
|
|
Value::String(s) => {
|
|
if s.starts_with("./") {
|
|
let Some(home) = dirs::home_dir() else {
|
|
panic!("Should always be there");
|
|
};
|
|
let Some(home) = home.to_str() else {
|
|
panic!("Should always be there");
|
|
};
|
|
let mut home = home.to_string();
|
|
home.push('/');
|
|
|
|
let s = s.replace("./", &home);
|
|
match Background::try_from(s.as_str()) {
|
|
Ok(background) => background,
|
|
Err(e) => {
|
|
error!(
|
|
"Couldn't load background: {e}"
|
|
);
|
|
Background::default()
|
|
}
|
|
}
|
|
} else {
|
|
match Background::try_from(s.as_str()) {
|
|
Ok(background) => background,
|
|
Err(e) => {
|
|
error!(
|
|
"Couldn't load background: {e}"
|
|
);
|
|
Background::default()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
_ => Background::default(),
|
|
}
|
|
} else {
|
|
Background::default()
|
|
}
|
|
}
|
|
_ => Background::default(),
|
|
}
|
|
}
|
|
|
|
#[derive(
|
|
Clone, Debug, Default, PartialEq, Serialize, Deserialize,
|
|
)]
|
|
pub struct SlideBuilder {
|
|
background: Option<Background>,
|
|
text: Option<String>,
|
|
font: Option<String>,
|
|
font_size: Option<i32>,
|
|
audio: Option<PathBuf>,
|
|
text_alignment: Option<TextAlignment>,
|
|
video_loop: Option<bool>,
|
|
video_start_time: Option<f32>,
|
|
video_end_time: Option<f32>,
|
|
}
|
|
|
|
impl SlideBuilder {
|
|
pub(crate) fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
pub(crate) fn background_path(
|
|
mut self,
|
|
background: PathBuf,
|
|
) -> Result<Self, ParseError> {
|
|
let background = Background::try_from(background)?;
|
|
let _ = self.background.insert(background);
|
|
Ok(self)
|
|
}
|
|
|
|
pub(crate) fn background(
|
|
mut self,
|
|
background: Background,
|
|
) -> Self {
|
|
let _ = self.background.insert(background);
|
|
self
|
|
}
|
|
|
|
pub(crate) fn text(mut self, text: impl Into<String>) -> Self {
|
|
let _ = self.text.insert(text.into());
|
|
self
|
|
}
|
|
|
|
pub(crate) fn audio(mut self, audio: impl Into<PathBuf>) -> Self {
|
|
let _ = self.audio.insert(audio.into());
|
|
self
|
|
}
|
|
|
|
pub(crate) fn font(mut self, font: impl Into<String>) -> Self {
|
|
let _ = self.font.insert(font.into());
|
|
self
|
|
}
|
|
|
|
pub(crate) fn font_size(mut self, font_size: i32) -> Self {
|
|
let _ = self.font_size.insert(font_size);
|
|
self
|
|
}
|
|
|
|
pub(crate) fn text_alignment(
|
|
mut self,
|
|
text_alignment: TextAlignment,
|
|
) -> Self {
|
|
let _ = self.text_alignment.insert(text_alignment);
|
|
self
|
|
}
|
|
|
|
pub(crate) fn video_loop(mut self, video_loop: bool) -> Self {
|
|
let _ = self.video_loop.insert(video_loop);
|
|
self
|
|
}
|
|
|
|
pub(crate) fn video_start_time(
|
|
mut self,
|
|
video_start_time: f32,
|
|
) -> Self {
|
|
let _ = self.video_start_time.insert(video_start_time);
|
|
self
|
|
}
|
|
|
|
pub(crate) fn video_end_time(
|
|
mut self,
|
|
video_end_time: f32,
|
|
) -> Self {
|
|
let _ = self.video_end_time.insert(video_end_time);
|
|
self
|
|
}
|
|
|
|
pub(crate) fn build(self) -> Result<Slide> {
|
|
let Some(background) = self.background else {
|
|
return Err(miette!("No background"));
|
|
};
|
|
let Some(text) = self.text else {
|
|
return Err(miette!("No text"));
|
|
};
|
|
let Some(font) = self.font else {
|
|
return Err(miette!("No font"));
|
|
};
|
|
let Some(font_size) = self.font_size else {
|
|
return Err(miette!("No font_size"));
|
|
};
|
|
let Some(text_alignment) = self.text_alignment else {
|
|
return Err(miette!("No text_alignment"));
|
|
};
|
|
let Some(video_loop) = self.video_loop else {
|
|
return Err(miette!("No video_loop"));
|
|
};
|
|
let Some(video_start_time) = self.video_start_time else {
|
|
return Err(miette!("No video_start_time"));
|
|
};
|
|
let Some(video_end_time) = self.video_end_time else {
|
|
return Err(miette!("No video_end_time"));
|
|
};
|
|
Ok(Slide {
|
|
background,
|
|
text,
|
|
font,
|
|
font_size,
|
|
text_alignment,
|
|
audio: self.audio,
|
|
video_loop,
|
|
video_start_time,
|
|
video_end_time,
|
|
..Default::default()
|
|
})
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default)]
|
|
struct Image {
|
|
pub source: String,
|
|
pub fit: String,
|
|
pub children: Vec<String>,
|
|
}
|
|
|
|
impl Image {
|
|
fn new() -> Self {
|
|
Self {
|
|
..Default::default()
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use pretty_assertions::assert_eq;
|
|
use std::fs::read_to_string;
|
|
|
|
use super::*;
|
|
|
|
fn test_slide() -> Slide {
|
|
Slide {
|
|
text: "This is frodo".to_string(),
|
|
background: Background::try_from("~/pics/frodo.jpg")
|
|
.unwrap(),
|
|
font: "Quicksand".to_string(),
|
|
font_size: 70,
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
fn test_second_slide() -> Slide {
|
|
Slide {
|
|
text: "".to_string(),
|
|
background: Background::try_from(
|
|
"~/vids/test/camprules2024.mp4",
|
|
)
|
|
.unwrap(),
|
|
font: "Quicksand".to_string(),
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_lisp_serialize() {
|
|
let lisp =
|
|
read_to_string("./test_presentation.lisp").expect("oops");
|
|
let lisp_value = crisp::reader::read(&lisp);
|
|
match lisp_value {
|
|
Value::List(value) => {
|
|
let slide = Slide::from(value[0].clone());
|
|
let test_slide = test_slide();
|
|
assert_eq!(slide, test_slide);
|
|
|
|
let second_slide = Slide::from(value[1].clone());
|
|
let second_test_slide = test_second_slide();
|
|
assert_eq!(second_slide, second_test_slide)
|
|
}
|
|
_ => panic!("this should be a lisp"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_ron_deserialize() {
|
|
let slide = read_to_string("./test_presentation.ron")
|
|
.expect("Problem getting file read");
|
|
match ron::from_str::<Vec<Slide>>(&slide) {
|
|
Ok(s) => {
|
|
assert!(true)
|
|
}
|
|
Err(e) => {
|
|
assert!(false)
|
|
}
|
|
}
|
|
}
|
|
}
|