ServiceItems are loaded from lisp and converted to slides

This commit is contained in:
Chris Cochrun 2024-12-10 12:07:10 -06:00
parent cb7fa372a9
commit db39eb12b8
9 changed files with 283 additions and 125 deletions

View file

@ -39,7 +39,11 @@ impl From<&Value> for Image {
}; };
let title = path.clone().map(|p| { let title = path.clone().map(|p| {
p.to_str().unwrap_or_default().to_string() let path =
p.to_str().unwrap_or_default().to_string();
let title =
path.rsplit_once("/").unwrap_or_default().1;
title.to_string()
}); });
Self { Self {
title: title.unwrap_or_default(), title: title.unwrap_or_default(),

View file

@ -16,7 +16,7 @@ pub enum ServiceItemKind {
Song(Song), Song(Song),
Video(Video), Video(Video),
Image(Image), Image(Image),
Presentation((Presentation, PresKind)), Presentation(Presentation),
Content(Slide), Content(Slide),
} }
@ -26,7 +26,7 @@ impl std::fmt::Display for ServiceItemKind {
Self::Song(s) => "song".to_owned(), Self::Song(s) => "song".to_owned(),
Self::Image(i) => "image".to_owned(), Self::Image(i) => "image".to_owned(),
Self::Video(v) => "video".to_owned(), Self::Video(v) => "video".to_owned(),
Self::Presentation((p, k)) => "html".to_owned(), Self::Presentation(p) => "html".to_owned(),
Self::Content(s) => "content".to_owned(), Self::Content(s) => "content".to_owned(),
}; };
write!(f, "{s}") write!(f, "{s}")

View file

@ -1,4 +1,4 @@
use crisp::types::Value; use crisp::types::{Keyword, Value};
use miette::{miette, IntoDiagnostic, Result}; use miette::{miette, IntoDiagnostic, Result};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{ use sqlx::{
@ -39,7 +39,30 @@ impl From<Value> for Presentation {
impl From<&Value> for Presentation { impl From<&Value> for Presentation {
fn from(value: &Value) -> Self { fn from(value: &Value) -> Self {
todo!() match value {
Value::List(list) => {
let path = if let Some(path_pos) =
list.iter().position(|v| {
v == &Value::Keyword(Keyword::from("source"))
}) {
let pos = path_pos + 1;
list.get(pos)
.map(|p| PathBuf::from(String::from(p)))
} else {
None
};
let title = path.clone().map(|p| {
p.to_str().unwrap_or_default().to_string()
});
Self {
title: title.unwrap_or_default(),
path: path.unwrap_or_default(),
..Default::default()
}
}
_ => todo!(),
}
} }
} }

View file

@ -1,6 +1,8 @@
use std::ops::Deref;
use crisp::types::{Keyword, Symbol, Value}; use crisp::types::{Keyword, Symbol, Value};
use miette::Result; use miette::Result;
use tracing::error; use tracing::{debug, error};
use crate::Slide; use crate::Slide;
@ -21,12 +23,16 @@ pub struct ServiceItem {
} }
impl ServiceItem { impl ServiceItem {
pub fn to_slide(&self) -> Result<Vec<Slide>> { pub fn title(&self) -> String {
self.title.clone()
}
pub fn to_slides(&self) -> Result<Vec<Slide>> {
match &self.kind { match &self.kind {
ServiceItemKind::Song(song) => song.to_slides(), ServiceItemKind::Song(song) => song.to_slides(),
ServiceItemKind::Video(video) => video.to_slides(), ServiceItemKind::Video(video) => video.to_slides(),
ServiceItemKind::Image(image) => image.to_slides(), ServiceItemKind::Image(image) => image.to_slides(),
ServiceItemKind::Presentation((presentation, _)) => { ServiceItemKind::Presentation(presentation) => {
presentation.to_slides() presentation.to_slides()
} }
ServiceItemKind::Content(slide) => { ServiceItemKind::Content(slide) => {
@ -124,11 +130,27 @@ impl From<&Value> for ServiceItem {
} }
} }
#[derive(Debug, Default)] #[derive(Debug, Default, Clone)]
pub struct ServiceItemModel { pub struct ServiceItemModel {
items: Vec<ServiceItem>, items: Vec<ServiceItem>,
} }
impl Deref for ServiceItemModel {
type Target = Vec<ServiceItem>;
fn deref(&self) -> &Self::Target {
&self.items
}
}
// impl Iterator for ServiceItemModel {
// type Item = ServiceItem;
// fn next(&mut self) -> Option<Self::Item> {
// *self.items.iter().next()
// }
// }
impl From<Vec<ServiceItem>> for ServiceItemModel { impl From<Vec<ServiceItem>> for ServiceItemModel {
fn from(items: Vec<ServiceItem>) -> Self { fn from(items: Vec<ServiceItem>) -> Self {
Self { items } Self { items }
@ -171,10 +193,7 @@ impl From<&Image> for ServiceItem {
impl From<&Presentation> for ServiceItem { impl From<&Presentation> for ServiceItem {
fn from(presentation: &Presentation) -> Self { fn from(presentation: &Presentation) -> Self {
Self { Self {
kind: ServiceItemKind::Presentation(( kind: ServiceItemKind::Presentation(presentation.clone()),
presentation.clone(),
presentation.kind.clone(),
)),
database_id: presentation.id, database_id: presentation.id,
title: presentation.title.clone(), title: presentation.title.clone(),
..Default::default() ..Default::default()
@ -196,7 +215,11 @@ impl ServiceItemModel {
Ok(self Ok(self
.items .items
.iter() .iter()
.filter_map(|item| item.to_slide().ok()) .filter_map(|item| {
let slides = item.to_slides().ok();
debug!(?slides);
slides
})
.flatten() .flatten()
.collect::<Vec<Slide>>()) .collect::<Vec<Slide>>())
} }
@ -251,26 +274,26 @@ mod test {
} }
} }
// #[test] #[test]
// pub fn test_service_item() { pub fn test_service_item() {
// let song = test_song(); let song = test_song();
// let service_item = ServiceItem::from(&song); let service_item = ServiceItem::from(&song);
// let pres = test_presentation(); let pres = test_presentation();
// let pres_item = ServiceItem::from(&pres); let pres_item = ServiceItem::from(&pres);
// let mut service_model = ServiceItemModel::default(); let mut service_model = ServiceItemModel::default();
// match service_model.add_item(&song) { match service_model.add_item(&song) {
// Ok(_) => { Ok(_) => {
// assert_eq!( assert_eq!(
// ServiceItemKind::Song, ServiceItemKind::Song(song),
// service_model.items[0].kind service_model.items[0].kind
// ); );
// assert_eq!( assert_eq!(
// ServiceItemKind::Presentation(PresKind::Html), ServiceItemKind::Presentation(pres),
// pres_item.kind pres_item.kind
// ); );
// assert_eq!(service_item, service_model.items[0]); assert_eq!(service_item, service_model.items[0]);
// } }
// Err(e) => panic!("Problem adding item: {:?}", e), Err(e) => panic!("Problem adding item: {:?}", e),
// } }
// } }
} }

View file

@ -60,8 +60,22 @@ impl TryFrom<String> for Background {
impl TryFrom<PathBuf> for Background { impl TryFrom<PathBuf> for Background {
type Error = ParseError; type Error = ParseError;
fn try_from(value: PathBuf) -> Result<Self, Self::Error> { fn try_from(path: PathBuf) -> Result<Self, Self::Error> {
match value.canonicalize() { 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) => { Ok(value) => {
let extension = value let extension = value
.extension() .extension()
@ -83,7 +97,7 @@ impl TryFrom<PathBuf> for Background {
} }
} }
Err(e) => { Err(e) => {
error!("Couldn't canonicalize: {e}"); error!("Couldn't canonicalize: {e} {:?}", path);
Err(ParseError::CannotCanonicalize) Err(ParseError::CannotCanonicalize)
} }
} }

View file

@ -4,7 +4,7 @@ use super::{
model::Model, service_items::ServiceTrait, slide::Slide, model::Model, service_items::ServiceTrait, slide::Slide,
}; };
use cosmic::{executor, iced::Executor}; use cosmic::{executor, iced::Executor};
use crisp::types::Value; use crisp::types::{Keyword, Value};
use miette::{miette, IntoDiagnostic, Result}; use miette::{miette, IntoDiagnostic, Result};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{query_as, SqliteConnection}; use sqlx::{query_as, SqliteConnection};
@ -31,7 +31,76 @@ impl From<Value> for Video {
impl From<&Value> for Video { impl From<&Value> for Video {
fn from(value: &Value) -> Self { fn from(value: &Value) -> Self {
todo!() match value {
Value::List(list) => {
let path = if let Some(path_pos) =
list.iter().position(|v| {
v == &Value::Keyword(Keyword::from("source"))
}) {
let pos = path_pos + 1;
list.get(pos)
.map(|p| PathBuf::from(String::from(p)))
} else {
None
};
let title = path.clone().map(|p| {
let path =
p.to_str().unwrap_or_default().to_string();
let title =
path.rsplit_once("/").unwrap_or_default().1;
title.to_string()
});
let start_time = if let Some(start_pos) =
list.iter().position(|v| {
v == &Value::Keyword(Keyword::from(
"start-time",
))
}) {
let pos = start_pos + 1;
list.get(pos).map(|p| i32::from(p) as f32)
} else {
None
};
let end_time = if let Some(end_pos) =
list.iter().position(|v| {
v == &Value::Keyword(Keyword::from(
"end-time",
))
}) {
let pos = end_pos + 1;
list.get(pos).map(|p| i32::from(p) as f32)
} else {
None
};
let looping = if let Some(loop_pos) =
list.iter().position(|v| {
v == &Value::Keyword(Keyword::from("loop"))
}) {
let pos = loop_pos + 1;
list.get(pos)
.map(|l| {
String::from(l) == "true".to_string()
})
.unwrap_or_default()
} else {
false
};
Self {
title: title.unwrap_or_default(),
path: path.unwrap_or_default(),
start_time,
end_time,
looping,
..Default::default()
}
}
_ => todo!(),
}
} }
} }

View file

@ -3,30 +3,25 @@ use std::{fs::read_to_string, path::PathBuf};
use crisp::types::{Symbol, Value}; use crisp::types::{Symbol, Value};
use tracing::error; use tracing::error;
use crate::{core::songs::lisp_to_song, Slide}; use crate::{
core::{service_items::ServiceItem, songs::lisp_to_song},
Slide,
};
pub fn parse_lisp(value: Value) -> Vec<Slide> { pub fn parse_lisp(value: Value) -> Vec<ServiceItem> {
match &value { match &value {
Value::List(vec) => match &vec[0] { Value::List(vec) => match &vec[0] {
Value::Symbol(Symbol(s)) if s == "slide" => { Value::Symbol(Symbol(s))
let slide = Slide::from(value.clone()); if s == "slide" || s == "song" =>
vec![slide] {
} let item = ServiceItem::from(value.clone());
Value::Symbol(Symbol(s)) if s == "song" => { vec![item]
let song = lisp_to_song(vec.clone());
match Slide::song_slides(&song) {
Ok(s) => s,
Err(e) => {
error!("Couldn't load song: {e}");
vec![Slide::default()]
}
}
} }
Value::Symbol(Symbol(s)) if s == "load" => { Value::Symbol(Symbol(s)) if s == "load" => {
let Ok(path) = PathBuf::from(String::from(&vec[1])) let Ok(path) = PathBuf::from(String::from(&vec[1]))
.canonicalize() .canonicalize()
else { else {
return vec![Slide::default()]; return vec![ServiceItem::default()];
}; };
let lisp = read_to_string(path).expect("oops"); let lisp = read_to_string(path).expect("oops");
let lisp_value = crisp::reader::read(&lisp); let lisp_value = crisp::reader::read(&lisp);
@ -48,7 +43,13 @@ pub fn parse_lisp(value: Value) -> Vec<Slide> {
mod test { mod test {
use std::{fs::read_to_string, path::PathBuf}; use std::{fs::read_to_string, path::PathBuf};
use crate::{Background, SlideBuilder, TextAlignment}; use crate::{
core::{
images::Image, kinds::ServiceItemKind, songs::Song,
videos::Video,
},
Background, SlideBuilder, TextAlignment,
};
use super::*; use super::*;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
@ -58,15 +59,15 @@ mod test {
let lisp = let lisp =
read_to_string("./test_slides.lisp").expect("oops"); read_to_string("./test_slides.lisp").expect("oops");
let lisp_value = crisp::reader::read(&lisp); let lisp_value = crisp::reader::read(&lisp);
let test_vec = vec![test_slide(), test_second_slide()]; let test_vec = vec![service_item_1(), service_item_2()];
match lisp_value { match lisp_value {
Value::List(value) => { Value::List(value) => {
let mut slide_vec = vec![]; let mut item_vec = vec![];
for value in value { for value in value {
let mut vec = parse_lisp(value); let mut vec = parse_lisp(value);
slide_vec.append(&mut vec); item_vec.append(&mut vec);
} }
assert_eq!(slide_vec, test_vec) assert_eq!(item_vec, test_vec)
} }
_ => panic!("this should be a lisp"), _ => panic!("this should be a lisp"),
} }
@ -77,76 +78,95 @@ mod test {
let lisp = let lisp =
read_to_string("./test_presentation.lisp").expect("oops"); read_to_string("./test_presentation.lisp").expect("oops");
let lisp_value = crisp::reader::read(&lisp); let lisp_value = crisp::reader::read(&lisp);
let test_vec = let test_vec = vec![
vec![test_slide(), test_second_slide(), song_slide()]; service_item_1(),
service_item_2(),
service_item_3(),
];
match lisp_value { match lisp_value {
Value::List(value) => { Value::List(value) => {
let mut slide_vec = vec![]; let mut item_vec = vec![];
for value in value { for value in value {
let mut vec = parse_lisp(value); let mut vec = parse_lisp(value);
slide_vec.append(&mut vec); item_vec.append(&mut vec);
} }
let first_lisp_slide = &slide_vec[0]; let item_1 = &item_vec[0];
let second_lisp_slide = &slide_vec[1]; let item_2 = &item_vec[1];
let third_lisp_slide = &slide_vec[2]; let item_3 = &item_vec[2];
assert_eq!(first_lisp_slide, &test_vec[0]); assert_eq!(item_1, &test_vec[0]);
assert_eq!(second_lisp_slide, &test_vec[1]); assert_eq!(item_2, &test_vec[1]);
assert_eq!(third_lisp_slide, &test_vec[2]); assert_eq!(item_3, &test_vec[2]);
assert_eq!(slide_vec, test_vec); assert_eq!(item_vec, test_vec);
} }
_ => panic!("this should be a lisp"), _ => panic!("this should be a lisp"),
} }
} }
fn test_slide() -> Slide { fn service_item_1() -> ServiceItem {
SlideBuilder::new() ServiceItem {
.text("This is frodo") title: "frodo.jpg".to_string(),
.background( kind: ServiceItemKind::Image(Image {
Background::try_from("~/pics/frodo.jpg").unwrap(), title: "frodo.jpg".to_string(),
) path: PathBuf::from("~/pics/frodo.jpg"),
.font("Quicksand") ..Default::default()
.font_size(70) }),
.text_alignment(TextAlignment::MiddleCenter) ..Default::default()
.video_loop(false) }
.video_start_time(0.0)
.video_end_time(0.0)
.build()
.unwrap()
} }
fn test_second_slide() -> Slide { fn service_item_2() -> ServiceItem {
SlideBuilder::new() ServiceItem {
.text("") title: "camprules2024.mp4".to_string(),
.background( kind: ServiceItemKind::Video(Video {
Background::try_from("~/vids/test/camprules2024.mp4") title: "camprules2024.mp4".to_string(),
.unwrap(), path: PathBuf::from("~/vids/test/camprules2024.mp4"),
) start_time: None,
.font("Quicksand") end_time: None,
.font_size(0) looping: false,
.text_alignment(TextAlignment::MiddleCenter) ..Default::default()
.video_loop(false) }),
.video_start_time(0.0) ..Default::default()
.video_end_time(0.0) }
.build()
.unwrap()
} }
fn song_slide() -> Slide { fn service_item_3() -> ServiceItem {
SlideBuilder::new() ServiceItem {
.text("Death Was Arrested\nNorth Point Worship") title: "Death Was Arrested".to_string(),
.background( kind: ServiceItemKind::Song(test_song()),
Background::try_from("~/nc/tfc/openlp/CMG - Bright Mountains 01.jpg") database_id: 7,
.unwrap(), ..Default::default()
) }
.font("Quicksand Bold") }
.font_size(60)
.text_alignment(TextAlignment::MiddleCenter) fn test_song() -> Song {
.audio(PathBuf::from("file:///home/chris/music/North Point InsideOut/Nothing Ordinary, Pt. 1 (Live)/05 Death Was Arrested (feat. Seth Condrey).mp3")) Song {
.video_loop(true) id: 7,
.video_start_time(0.0) title: "Death Was Arrested".to_string(),
.video_end_time(0.0) lyrics: Some("Intro 1\nDeath Was Arrested\nNorth Point Worship\n\nVerse 1\nAlone in my sorrow\nAnd dead in my sin\n\nLost without hope\nWith no place to begin\n\nYour love made a way\nTo let mercy come in\n\nWhen death was arrested\nAnd my life began\n\nVerse 2\nAsh was redeemed\nOnly beauty remains\n\nMy orphan heart\nWas given a name\n\nMy mourning grew quiet,\nMy feet rose to dance\n\nWhen death was arrested\nAnd my life began\n\nChorus 1\nOh, Your grace so free,\nWashes over me\n\nYou have made me new,\nNow life begins with You\n\nIt's Your endless love,\nPouring down on us\n\nYou have made us new,\nNow life begins with You\n\nVerse 3\nReleased from my chains,\nI'm a prisoner no more\n\nMy shame was a ransom\nHe faithfully bore\n\nHe cancelled my debt and\nHe called me His friend\n\nWhen death was arrested\nAnd my life began\n\nVerse 4\nOur Savior displayed\nOn a criminal's cross\n\nDarkness rejoiced as though\nHeaven had lost\n\nBut then Jesus arose\nWith our freedom in hand\n\nThat's when death was arrested\nAnd my life began\n\nThat's when death was arrested\nAnd my life began\n\nBridge 1\nOh, we're free, free,\nForever we're free\n\nCome join the song\nOf all the redeemed\n\nYes, we're free, free,\nForever amen\n\nWhen death was arrested\nAnd my life began\n\nOh, we're free, free,\nForever we're free\n\nCome join the song\nOf all the redeemed\n\nYes, we're free, free,\nForever amen\n\nWhen death was arrested\nAnd my life began\n\nEnding 1\nWhen death was arrested\nAnd my life began\n\nThat's when death was arrested\nAnd my life began".to_string()),
.build() author: Some(
.unwrap() "North Point Worship".to_string(),
),
ccli: None,
audio: Some("file:///home/chris/music/North Point InsideOut/Nothing Ordinary, Pt. 1 (Live)/05 Death Was Arrested (feat. Seth Condrey).mp3".into()),
verse_order: Some(vec![
"I1".to_string(),
"V1".to_string(),
"V2".to_string(),
"C1".to_string(),
"V3".to_string(),
"C1".to_string(),
"V4".to_string(),
"C1".to_string(),
"B1".to_string(),
"B1".to_string(),
"E1".to_string(),
"E2".to_string(),
]),
background: Some(Background::try_from("file:///home/chris/nc/tfc/openlp/CMG - Bright Mountains 01.jpg").unwrap()),
text_alignment: Some(TextAlignment::MiddleCenter),
font: Some("Quicksand Bold".to_string()),
font_size: Some(60)
}
} }
} }

View file

@ -1,5 +1,5 @@
use clap::{command, Parser}; use clap::{command, Parser};
use core::service_items::ServiceItem; use core::service_items::{ServiceItem, ServiceItemModel};
use cosmic::app::{Core, Settings, Task}; use cosmic::app::{Core, Settings, Task};
use cosmic::iced::keyboard::Key; use cosmic::iced::keyboard::Key;
use cosmic::iced::window::{Mode, Position}; use cosmic::iced::window::{Mode, Position};
@ -127,7 +127,7 @@ impl cosmic::Application for App {
windows.push(core.main_window_id().unwrap()); windows.push(core.main_window_id().unwrap());
} }
let slides = match read_to_string(input.file) { let items = match read_to_string(input.file) {
Ok(lisp) => { Ok(lisp) => {
let mut slide_vector = vec![]; let mut slide_vector = vec![];
let lisp = crisp::reader::read(&lisp); let lisp = crisp::reader::read(&lisp);
@ -148,11 +148,17 @@ impl cosmic::Application for App {
} }
}; };
let items = ServiceItemModel::from(items);
let presenter = Presenter::with_items(items.clone());
let slides = if let Ok(slides) = items.to_slides() {
slides
} else {
vec![]
};
let current_slide = slides[0].clone(); let current_slide = slides[0].clone();
let presenter = Presenter::with_slides(slides.clone());
for slide in slides.clone() { for item in items.iter() {
nav_model.insert().text(slide.text()).data(slide); nav_model.insert().text(item.title()).data(item.clone());
} }
nav_model.activate_position(0); nav_model.activate_position(0);

View file

@ -89,8 +89,7 @@ impl Presenter {
} }
} }
pub fn with_items(items: Vec<ServiceItem>) -> Self { pub fn with_items(items: ServiceItemModel) -> Self {
let items = ServiceItemModel::from(items);
let slides = if let Ok(slides) = items.to_slides() { let slides = if let Ok(slides) = items.to_slides() {
slides slides
} else { } else {