lumina/src/core/slide.rs
Chris Cochrun a36a1d59c6 Drag and Drop works ok now.
The main functionality works but only in cosmic desktop. So there are
some issues that need to be worked out yet in regards to libcosmic.
2025-02-20 06:53:54 -06:00

629 lines
17 KiB
Rust

use crisp::types::{Keyword, Symbol, Value};
use miette::{miette, Result};
use serde::{Deserialize, Serialize};
use std::{
fmt::Display,
path::{Path, PathBuf},
};
use tracing::{debug, 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<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,
}
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"
}
};
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)
}
}
}
}