moving the lisp presentation parsing to service_items

Since we need the nav_bar to have the ServiceItemModel, the
presentation needs to be a list of service items, not slides, each
service item will have a list of slides attached though.
This commit is contained in:
Chris Cochrun 2024-12-10 09:27:50 -06:00
parent 9eb5bec320
commit cb7fa372a9
9 changed files with 493 additions and 79 deletions

View file

@ -1,4 +1,7 @@
use super::model::Model; use crate::{Background, Slide, SlideBuilder, TextAlignment};
use super::{model::Model, service_items::ServiceTrait};
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};
@ -14,6 +17,74 @@ pub struct Image {
pub path: PathBuf, pub path: PathBuf,
} }
impl From<Value> for Image {
fn from(value: Value) -> Self {
Self::from(&value)
}
}
impl From<&Value> for Image {
fn from(value: &Value) -> Self {
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!(),
}
}
}
impl ServiceTrait for Image {
fn title(&self) -> String {
self.title.clone()
}
fn id(&self) -> i32 {
self.id
}
fn to_slides(&self) -> Result<Vec<Slide>> {
let slide = SlideBuilder::new()
.background(
Background::try_from(self.path.clone())
.into_diagnostic()?,
)
.text("")
.audio("")
.font("")
.font_size(50)
.text_alignment(TextAlignment::MiddleCenter)
.video_loop(false)
.video_start_time(0.0)
.video_end_time(0.0)
.build()?;
Ok(vec![slide])
}
fn box_clone(&self) -> Box<dyn ServiceTrait> {
Box::new((*self).clone())
}
}
impl Model<Image> { impl Model<Image> {
pub async fn load_from_db(&mut self) { pub async fn load_from_db(&mut self) {
let result = query_as!( let result = query_as!(

View file

@ -2,65 +2,65 @@ use std::{error::Error, fmt::Display};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::presentations::PresKind; use crate::Slide;
#[derive( use super::{
Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, images::Image,
)] presentations::{PresKind, Presentation},
songs::Song,
videos::Video,
};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ServiceItemKind { pub enum ServiceItemKind {
#[default] Song(Song),
Song, Video(Video),
Video, Image(Image),
Image, Presentation((Presentation, PresKind)),
Presentation(PresKind), Content(Slide),
Content,
} }
impl std::fmt::Display for ServiceItemKind { impl std::fmt::Display for ServiceItemKind {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let s = match self { let s = match self {
Self::Song => "song".to_owned(), Self::Song(s) => "song".to_owned(),
Self::Image => "image".to_owned(), Self::Image(i) => "image".to_owned(),
Self::Video => "video".to_owned(), Self::Video(v) => "video".to_owned(),
Self::Presentation(PresKind::Html) => "html".to_owned(), Self::Presentation((p, k)) => "html".to_owned(),
Self::Presentation(PresKind::Pdf) => "pdf".to_owned(), Self::Content(s) => "content".to_owned(),
Self::Presentation(PresKind::Generic) => {
"presentation".to_owned()
}
Self::Content => "content".to_owned(),
}; };
write!(f, "{s}") write!(f, "{s}")
} }
} }
impl TryFrom<String> for ServiceItemKind { // impl TryFrom<String> for ServiceItemKind {
type Error = ParseError; // type Error = ParseError;
fn try_from(value: String) -> Result<Self, Self::Error> { // fn try_from(value: String) -> Result<Self, Self::Error> {
match value.as_str() { // match value.as_str() {
"song" => Ok(Self::Song), // "song" => Ok(Self::Song),
"image" => Ok(Self::Image), // "image" => Ok(Self::Image),
"video" => Ok(Self::Video), // "video" => Ok(Self::Video),
"presentation" => { // "presentation" => {
Ok(Self::Presentation(PresKind::Generic)) // Ok(Self::Presentation(PresKind::Generic))
} // }
"html" => Ok(Self::Presentation(PresKind::Html)), // "html" => Ok(Self::Presentation(PresKind::Html)),
"pdf" => Ok(Self::Presentation(PresKind::Pdf)), // "pdf" => Ok(Self::Presentation(PresKind::Pdf)),
"content" => Ok(Self::Content), // "content" => Ok(Self::Content),
_ => Err(ParseError::UnknownType), // _ => Err(ParseError::UnknownType),
} // }
} // }
} // }
impl From<ServiceItemKind> for String { impl From<ServiceItemKind> for String {
fn from(val: ServiceItemKind) -> String { fn from(val: ServiceItemKind) -> String {
match val { match val {
ServiceItemKind::Song => "song".to_owned(), ServiceItemKind::Song(_) => "song".to_owned(),
ServiceItemKind::Video => "video".to_owned(), ServiceItemKind::Video(_) => "video".to_owned(),
ServiceItemKind::Image => "image".to_owned(), ServiceItemKind::Image(_) => "image".to_owned(),
ServiceItemKind::Presentation(_) => { ServiceItemKind::Presentation(_) => {
"presentation".to_owned() "presentation".to_owned()
} }
ServiceItemKind::Content => "content".to_owned(), ServiceItemKind::Content(_) => "content".to_owned(),
} }
} }
} }

View file

@ -1,3 +1,4 @@
use crisp::types::Value;
use miette::{miette, IntoDiagnostic, Result}; use miette::{miette, IntoDiagnostic, Result};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{ use sqlx::{
@ -6,7 +7,9 @@ use sqlx::{
use std::path::PathBuf; use std::path::PathBuf;
use tracing::error; use tracing::error;
use super::model::Model; use crate::{Background, Slide, SlideBuilder, TextAlignment};
use super::{model::Model, service_items::ServiceTrait};
#[derive( #[derive(
Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize,
@ -28,6 +31,51 @@ pub struct Presentation {
pub kind: PresKind, pub kind: PresKind,
} }
impl From<Value> for Presentation {
fn from(value: Value) -> Self {
Self::from(&value)
}
}
impl From<&Value> for Presentation {
fn from(value: &Value) -> Self {
todo!()
}
}
impl ServiceTrait for Presentation {
fn title(&self) -> String {
self.title.clone()
}
fn id(&self) -> i32 {
self.id
}
fn to_slides(&self) -> Result<Vec<Slide>> {
let slide = SlideBuilder::new()
.background(
Background::try_from(self.path.clone())
.into_diagnostic()?,
)
.text("")
.audio("")
.font("")
.font_size(50)
.text_alignment(TextAlignment::MiddleCenter)
.video_loop(false)
.video_start_time(0.0)
.video_end_time(0.0)
.build()?;
Ok(vec![slide])
}
fn box_clone(&self) -> Box<dyn ServiceTrait> {
Box::new((*self).clone())
}
}
impl Presentation { impl Presentation {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {

View file

@ -1,29 +1,146 @@
use crisp::types::{Keyword, Symbol, Value};
use miette::Result; use miette::Result;
use tracing::error;
use crate::Slide;
use super::images::Image; use super::images::Image;
use super::presentations::Presentation; use super::presentations::Presentation;
use super::songs::Song; use super::songs::{lisp_to_song, Song};
use super::videos::Video; use super::videos::Video;
use super::kinds::ServiceItemKind; use super::kinds::ServiceItemKind;
#[derive(Debug, Default, PartialEq)] #[derive(Debug, PartialEq, Clone)]
pub struct ServiceItem { pub struct ServiceItem {
pub id: i32, pub id: i32,
pub title: String,
pub database_id: i32, pub database_id: i32,
pub kind: ServiceItemKind, pub kind: ServiceItemKind,
// pub item: Box<dyn ServiceTrait>,
} }
#[derive(Debug, Default, PartialEq)] impl ServiceItem {
pub fn to_slide(&self) -> Result<Vec<Slide>> {
match &self.kind {
ServiceItemKind::Song(song) => song.to_slides(),
ServiceItemKind::Video(video) => video.to_slides(),
ServiceItemKind::Image(image) => image.to_slides(),
ServiceItemKind::Presentation((presentation, _)) => {
presentation.to_slides()
}
ServiceItemKind::Content(slide) => {
Ok(vec![slide.clone()])
}
}
}
}
impl Default for ServiceItem {
fn default() -> Self {
Self {
id: 0,
title: String::default(),
database_id: 0,
kind: ServiceItemKind::Content(Slide::default()),
// item: Box::new(Image::default()),
}
}
}
impl From<Value> for ServiceItem {
fn from(value: Value) -> Self {
Self::from(&value)
}
}
impl From<&Value> for ServiceItem {
fn from(value: &Value) -> Self {
match value {
Value::List(list) => match &list[0] {
Value::Symbol(Symbol(s)) if s == "slide" => {
let background_pos = list
.iter()
.position(|v| match v {
Value::Keyword(Keyword(background))
if background == "background" =>
{
true
}
_ => false,
})
.map_or_else(|| 1, |pos| pos + 1);
if let Some(background) = list.get(background_pos)
{
match background {
Value::List(item) => match &item[0] {
Value::Symbol(Symbol(s))
if s == "image" =>
{
Self::from(&Image::from(
background,
))
}
Value::Symbol(Symbol(s))
if s == "video" =>
{
Self::from(&Video::from(
background,
))
}
Value::Symbol(Symbol(s))
if s == "presentation" =>
{
Self::from(&Presentation::from(
background,
))
}
_ => todo!(),
},
_ => {
error!(
"There is no background here: {:?}",
background
);
ServiceItem::default()
}
}
} else {
error!(
"There is no background here: {:?}",
background_pos
);
ServiceItem::default()
}
}
Value::Symbol(Symbol(s)) if s == "song" => {
let song = lisp_to_song(list.clone());
Self::from(&song)
}
_ => todo!(),
},
_ => todo!(),
}
}
}
#[derive(Debug, Default)]
pub struct ServiceItemModel { pub struct ServiceItemModel {
items: Vec<ServiceItem>, items: Vec<ServiceItem>,
} }
impl From<Vec<ServiceItem>> for ServiceItemModel {
fn from(items: Vec<ServiceItem>) -> Self {
Self { items }
}
}
impl From<&Song> for ServiceItem { impl From<&Song> for ServiceItem {
fn from(song: &Song) -> Self { fn from(song: &Song) -> Self {
Self { Self {
kind: ServiceItemKind::Song, kind: ServiceItemKind::Song(song.clone()),
database_id: song.id, database_id: song.id,
title: song.title.clone(),
..Default::default() ..Default::default()
} }
} }
@ -32,8 +149,9 @@ impl From<&Song> for ServiceItem {
impl From<&Video> for ServiceItem { impl From<&Video> for ServiceItem {
fn from(video: &Video) -> Self { fn from(video: &Video) -> Self {
Self { Self {
kind: ServiceItemKind::Video, kind: ServiceItemKind::Video(video.clone()),
database_id: video.id, database_id: video.id,
title: video.title.clone(),
..Default::default() ..Default::default()
} }
} }
@ -42,8 +160,9 @@ impl From<&Video> for ServiceItem {
impl From<&Image> for ServiceItem { impl From<&Image> for ServiceItem {
fn from(image: &Image) -> Self { fn from(image: &Image) -> Self {
Self { Self {
kind: ServiceItemKind::Image, kind: ServiceItemKind::Image(image.clone()),
database_id: image.id, database_id: image.id,
title: image.title.clone(),
..Default::default() ..Default::default()
} }
} }
@ -52,10 +171,12 @@ 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.kind.clone(), presentation.kind.clone(),
), )),
database_id: presentation.id, database_id: presentation.id,
title: presentation.title.clone(),
..Default::default() ..Default::default()
} }
} }
@ -70,6 +191,37 @@ impl ServiceItemModel {
self.items.push(service_item); self.items.push(service_item);
Ok(()) Ok(())
} }
pub fn to_slides(&self) -> Result<Vec<Slide>> {
Ok(self
.items
.iter()
.filter_map(|item| item.to_slide().ok())
.flatten()
.collect::<Vec<Slide>>())
}
}
pub trait ServiceTrait {
fn title(&self) -> String;
fn id(&self) -> i32;
fn to_slides(&self) -> Result<Vec<Slide>>;
fn box_clone(&self) -> Box<dyn ServiceTrait>;
}
impl Clone for Box<dyn ServiceTrait> {
fn clone(&self) -> Self {
self.box_clone()
}
}
impl std::fmt::Debug for Box<dyn ServiceTrait> {
fn fmt(
&self,
f: &mut std::fmt::Formatter<'_>,
) -> Result<(), std::fmt::Error> {
write!(f, "{}: {}", self.id(), self.title())
}
} }
#[cfg(test)] #[cfg(test)]
@ -99,26 +251,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,
service_model.items[0].kind // service_model.items[0].kind
); // );
assert_eq!( // assert_eq!(
ServiceItemKind::Presentation(PresKind::Html), // ServiceItemKind::Presentation(PresKind::Html),
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

@ -228,6 +228,10 @@ impl Slide {
Ok(slides) Ok(slides)
} }
// pub fn slides_from_item(item: &ServiceItem) -> Result<Vec<Self>> {
// todo!()
// }
} }
impl From<Value> for Slide { impl From<Value> for Slide {

View file

@ -10,10 +10,11 @@ use sqlx::{
}; };
use tracing::{debug, error}; use tracing::{debug, error};
use crate::core::slide; use crate::{core::slide, Slide, SlideBuilder};
use super::{ use super::{
model::Model, model::Model,
service_items::ServiceTrait,
slide::{Background, TextAlignment}, slide::{Background, TextAlignment},
}; };
@ -34,6 +35,47 @@ pub struct Song {
pub font_size: Option<i32>, pub font_size: Option<i32>,
} }
impl ServiceTrait for Song {
fn title(&self) -> String {
self.title.clone()
}
fn id(&self) -> i32 {
self.id
}
fn to_slides(&self) -> Result<Vec<Slide>> {
let lyrics = self.get_lyrics()?;
let slides: Vec<Slide> = lyrics
.iter()
.map(|l| {
SlideBuilder::new()
.background(
self.background.clone().unwrap_or_default(),
)
.font(self.font.clone().unwrap_or_default())
.font_size(self.font_size.unwrap_or_default())
.text_alignment(
self.text_alignment.unwrap_or_default(),
)
.audio(self.audio.clone().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)
}
fn box_clone(&self) -> Box<dyn ServiceTrait> {
Box::new((*self).clone())
}
}
const VERSE_KEYWORDS: [&str; 24] = [ const VERSE_KEYWORDS: [&str; 24] = [
"Verse 1", "Verse 2", "Verse 3", "Verse 4", "Verse 5", "Verse 6", "Verse 1", "Verse 2", "Verse 3", "Verse 4", "Verse 5", "Verse 6",
"Verse 7", "Verse 8", "Chorus 1", "Chorus 2", "Chorus 3", "Verse 7", "Verse 8", "Chorus 1", "Chorus 2", "Chorus 3",

View file

@ -1,5 +1,10 @@
use super::model::Model; use crate::{Background, SlideBuilder, TextAlignment};
use super::{
model::Model, service_items::ServiceTrait, slide::Slide,
};
use cosmic::{executor, iced::Executor}; use cosmic::{executor, iced::Executor};
use crisp::types::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};
@ -18,6 +23,51 @@ pub struct Video {
pub looping: bool, pub looping: bool,
} }
impl From<Value> for Video {
fn from(value: Value) -> Self {
Self::from(&value)
}
}
impl From<&Value> for Video {
fn from(value: &Value) -> Self {
todo!()
}
}
impl ServiceTrait for Video {
fn title(&self) -> String {
self.title.clone()
}
fn id(&self) -> i32 {
self.id
}
fn to_slides(&self) -> Result<Vec<Slide>> {
let slide = SlideBuilder::new()
.background(
Background::try_from(self.path.clone())
.into_diagnostic()?,
)
.text("")
.audio("")
.font("")
.font_size(50)
.text_alignment(TextAlignment::MiddleCenter)
.video_loop(self.looping)
.video_start_time(self.start_time.unwrap_or(0.0))
.video_end_time(self.end_time.unwrap_or(0.0))
.build()?;
Ok(vec![slide])
}
fn box_clone(&self) -> Box<dyn ServiceTrait> {
Box::new((*self).clone())
}
}
impl Model<Video> { impl Model<Video> {
pub async fn load_from_db(&mut self) { pub async fn load_from_db(&mut self) {
let result = query_as!(Video, r#"SELECT title as "title!", file_path as "path!", start_time as "start_time!: f32", end_time as "end_time!: f32", loop as "looping!", id as "id: i32" from videos"#).fetch_all(&mut self.db).await; let result = query_as!(Video, r#"SELECT title as "title!", file_path as "path!", start_time as "start_time!: f32", end_time as "end_time!: f32", loop as "looping!", id as "id: i32" from videos"#).fetch_all(&mut self.db).await;

View file

@ -1,4 +1,5 @@
use clap::{command, Parser}; use clap::{command, Parser};
use core::service_items::ServiceItem;
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};
@ -93,6 +94,8 @@ struct App {
enum Message { enum Message {
Present(presenter::Message), Present(presenter::Message),
File(PathBuf), File(PathBuf),
DndEnter(ServiceItem),
DndDrop(ServiceItem),
OpenWindow, OpenWindow,
CloseWindow(Option<window::Id>), CloseWindow(Option<window::Id>),
WindowOpened(window::Id, Option<Point>), WindowOpened(window::Id, Option<Point>),
@ -429,6 +432,8 @@ impl cosmic::Application for App {
} }
} }
Message::Quit => cosmic::iced::exit(), Message::Quit => cosmic::iced::exit(),
Message::DndEnter(service_item) => todo!(),
Message::DndDrop(service_item) => todo!(),
} }
} }

View file

@ -1,4 +1,4 @@
use std::time::Duration; use std::{rc::Rc, time::Duration};
use cosmic::{ use cosmic::{
dialog::ashpd::url::Url, dialog::ashpd::url::Url,
@ -13,7 +13,6 @@ use cosmic::{
stack, stack,
}, },
prelude::*, prelude::*,
theme::CosmicTheme,
widget::{ widget::{
container, image, mouse_area, responsive, scrollable, container, image, mouse_area, responsive, scrollable,
Container, Responsive, Row, Space, Container, Responsive, Row, Space,
@ -25,14 +24,17 @@ use miette::{Context, IntoDiagnostic, Result};
use tracing::{debug, error, info}; use tracing::{debug, error, info};
use crate::{ use crate::{
core::{service_items::ServiceItem, slide::Slide}, core::{
service_items::{ServiceItem, ServiceItemModel},
slide::Slide,
},
BackgroundKind, BackgroundKind,
}; };
// #[derive(Default, Clone, Debug)] // #[derive(Default, Clone, Debug)]
pub(crate) struct Presenter { pub(crate) struct Presenter {
pub slides: Vec<Slide>, pub slides: Vec<Slide>,
pub items: Vec<ServiceItem>, pub items: ServiceItemModel,
pub current_slide: Slide, pub current_slide: Slide,
pub current_slide_index: u16, pub current_slide_index: u16,
pub video: Option<Video>, pub video: Option<Video>,
@ -56,7 +58,47 @@ impl Presenter {
pub fn with_slides(slides: Vec<Slide>) -> Self { pub fn with_slides(slides: Vec<Slide>) -> Self {
Self { Self {
slides: slides.clone(), slides: slides.clone(),
items: vec![], items: ServiceItemModel::default(),
current_slide: slides[0].clone(),
current_slide_index: 0,
video: {
if let Some(slide) = slides.get(0) {
let path = slide.background().path.clone();
if path.exists() {
let url = Url::from_file_path(path).unwrap();
let result = Video::new(&url);
match result {
Ok(mut v) => {
v.set_paused(true);
Some(v)
}
Err(e) => {
error!("Had an error creating the video object: {e}");
None
}
}
} else {
None
}
} else {
None
}
},
video_position: 0.0,
hovered_slide: -1,
}
}
pub fn with_items(items: Vec<ServiceItem>) -> Self {
let items = ServiceItemModel::from(items);
let slides = if let Ok(slides) = items.to_slides() {
slides
} else {
vec![]
};
Self {
slides: slides.clone(),
items: items.into(),
current_slide: slides[0].clone(), current_slide: slides[0].clone(),
current_slide_index: 0, current_slide_index: 0,
video: { video: {