adding the basis for the full slide system
This commit is contained in:
parent
d4b40dbdc4
commit
66c37775d1
27
Cargo.lock
generated
27
Cargo.lock
generated
|
@ -3134,6 +3134,8 @@ dependencies = [
|
|||
"pretty_assertions",
|
||||
"serde",
|
||||
"serde-lexpr",
|
||||
"strum",
|
||||
"strum_macros",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
@ -4560,6 +4562,12 @@ dependencies = [
|
|||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248"
|
||||
|
||||
[[package]]
|
||||
name = "rustybuzz"
|
||||
version = "0.14.1"
|
||||
|
@ -4925,6 +4933,25 @@ version = "0.11.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.26.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.26.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn 2.0.81",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "supports-color"
|
||||
version = "3.0.1"
|
||||
|
|
|
@ -17,3 +17,5 @@ serde-lexpr = "0.1.3"
|
|||
tracing = "0.1.40"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["fmt", "std", "chrono", "time", "local-time", "env-filter"] }
|
||||
iced_video_player = { git = "https://github.com/jackpot51/iced_video_player", branch = "cosmic", features = ["wgpu"] }
|
||||
strum = "0.26.3"
|
||||
strum_macros = "0.26.4"
|
||||
|
|
2
justfile
2
justfile
|
@ -3,7 +3,7 @@ default:
|
|||
build:
|
||||
RUST_LOG=debug cargo build
|
||||
run:
|
||||
RUST_LOG=debug cargo run -- ~/dev/lumina-iced/test_presentation.lisp
|
||||
RUST_LOG=debug cargo run -- -i ~/dev/lumina-iced/test_presentation.lisp
|
||||
clean:
|
||||
RUST_LOG=debug cargo clean
|
||||
test:
|
||||
|
|
356
src/core/file.rs
Normal file
356
src/core/file.rs
Normal file
|
@ -0,0 +1,356 @@
|
|||
use tar::{Archive, Builder};
|
||||
use tracing::error;
|
||||
use zstd::Encoder;
|
||||
use std::{fs::{self, File}, iter, path::{Path, PathBuf}};
|
||||
use color_eyre::eyre::{eyre, Context, Result};
|
||||
use serde_json::Value;
|
||||
use sqlx::{query, query_as, FromRow, SqliteConnection};
|
||||
use crate::{images::{get_image_from_db, Image}, kinds::ServiceItemKind, model::get_db, presentations::{get_presentation_from_db, PresKind, Presentation}, service_items::ServiceItem, slides::Background, songs::{get_song_from_db, Song}, videos::{get_video_from_db, Video}};
|
||||
|
||||
pub async fn save(list: Vec<ServiceItem>, path: impl AsRef<Path>) -> Result<()> {
|
||||
let path = path.as_ref();
|
||||
let save_file = File::create(path)?;
|
||||
let mut db = get_db().await;
|
||||
let json = process_service_items(&list, &mut db).await?;
|
||||
let archive = store_service_items(&list, &mut db, &save_file, &json).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn store_service_items(items: &Vec<ServiceItem>, db: &mut SqliteConnection, save_file: &File, json: &Value) -> Result<()> {
|
||||
let encoder = Encoder::new(save_file, 3).unwrap();
|
||||
let mut tar = Builder::new(encoder);
|
||||
let mut temp_dir = dirs::data_dir().unwrap();
|
||||
temp_dir.push("lumina");
|
||||
let mut s: String =
|
||||
iter::repeat_with(fastrand::alphanumeric)
|
||||
.take(5)
|
||||
.collect();
|
||||
s.insert_str(0, "temp_");
|
||||
temp_dir.push(s);
|
||||
fs::create_dir_all(&temp_dir)?;
|
||||
let service_file = temp_dir.join("serviceitems.json");
|
||||
fs::File::create(&service_file)?;
|
||||
match fs::File::options().read(true).write(true).open(service_file) {
|
||||
Ok(f) => {
|
||||
serde_json::to_writer_pretty(f, json)?;
|
||||
},
|
||||
Err(e) => error!("There were problems making a file i guess: {e}"),
|
||||
};
|
||||
for item in items {
|
||||
let background;
|
||||
let audio: Option<PathBuf>;
|
||||
match item.kind {
|
||||
ServiceItemKind::Song => {
|
||||
let song = get_song_from_db(item.database_id, db).await?;
|
||||
background = song.background;
|
||||
audio = song.audio;
|
||||
},
|
||||
ServiceItemKind::Image => {
|
||||
let image = get_image_from_db(item.database_id, db).await?;
|
||||
background = Some(Background::try_from(image.path)?);
|
||||
audio = None;
|
||||
},
|
||||
ServiceItemKind::Video => {
|
||||
let video = get_video_from_db(item.database_id, db).await?;
|
||||
background = Some(Background::try_from(video.path)?);
|
||||
audio = None;
|
||||
},
|
||||
ServiceItemKind::Presentation(_) => {
|
||||
let presentation = get_presentation_from_db(item.database_id, db).await?;
|
||||
background = Some(Background::try_from(presentation.path)?);
|
||||
audio = None;
|
||||
},
|
||||
ServiceItemKind::Content => {
|
||||
todo!()
|
||||
},
|
||||
};
|
||||
if let Some(file) = audio {
|
||||
let audio_file = temp_dir.join(file.file_name().expect("Audio file couldn't be added to temp_dir"));
|
||||
if let Ok(file) = file.strip_prefix("file://") {
|
||||
fs::File::create(&audio_file).wrap_err("Couldn't create audio file")?;
|
||||
fs::copy(file, audio_file).wrap_err("Audio file could not be copied, the source file doesn't exist not be found");
|
||||
} else {
|
||||
fs::File::create(&audio_file).wrap_err("Couldn't create audio file")?;
|
||||
fs::copy(file, audio_file).wrap_err("Audio file could not be copied, the source file doesn't exist not be found");
|
||||
}
|
||||
};
|
||||
if let Some(file) = background {
|
||||
let background_file = temp_dir.join(file.path.file_name().expect("Background file couldn't be added to temp_dir"));
|
||||
if let Ok(file) = file.path.strip_prefix("file://") {
|
||||
fs::File::create(&background_file).wrap_err("Couldn't create background file")?;
|
||||
fs::copy(file, background_file).wrap_err("Background file could not be copied, the source file doesn't exist not be found");
|
||||
} else {
|
||||
fs::File::create(&background_file).wrap_err("Couldn't create background file")?;
|
||||
fs::copy(file.path, background_file).wrap_err("Background file could not be copied, the source file doesn't exist not be found");
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn clear_temp_dir(temp_dir: &Path) -> Result<()> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn process_service_items(items: &Vec<ServiceItem>, db: &mut SqliteConnection) -> Result<Value> {
|
||||
let mut values: Vec<Value> = vec![];
|
||||
for item in items {
|
||||
match item.kind {
|
||||
ServiceItemKind::Song => {
|
||||
let value = process_song(item.database_id, db).await?;
|
||||
values.push(value);
|
||||
},
|
||||
ServiceItemKind::Image => {
|
||||
let value = process_image(item.database_id, db).await?;
|
||||
values.push(value);
|
||||
},
|
||||
ServiceItemKind::Video => {
|
||||
let value = process_video(item.database_id, db).await?;
|
||||
values.push(value);
|
||||
},
|
||||
ServiceItemKind::Presentation(_) => {
|
||||
let value = process_presentation(item.database_id, db).await?;
|
||||
values.push(value);
|
||||
},
|
||||
ServiceItemKind::Content => {
|
||||
todo!()
|
||||
},
|
||||
}
|
||||
}
|
||||
let json = Value::from(values);
|
||||
Ok(json)
|
||||
}
|
||||
|
||||
async fn process_song(database_id: i32, db: &mut SqliteConnection) -> Result<Value> {
|
||||
let song = get_song_from_db(database_id, db).await?;
|
||||
let song_json = serde_json::to_value(&song)?;
|
||||
let kind_json = serde_json::to_value(ServiceItemKind::Song)?;
|
||||
let json = serde_json::json!({"item": song_json, "kind": kind_json});
|
||||
Ok(json)
|
||||
}
|
||||
|
||||
async fn process_image(database_id: i32, db: &mut SqliteConnection) -> Result<Value> {
|
||||
let image = get_image_from_db(database_id, db).await?;
|
||||
let image_json = serde_json::to_value(&image)?;
|
||||
let kind_json = serde_json::to_value(ServiceItemKind::Image)?;
|
||||
let json = serde_json::json!({"item": image_json, "kind": kind_json});
|
||||
Ok(json)
|
||||
}
|
||||
|
||||
async fn process_video(database_id: i32, db: &mut SqliteConnection) -> Result<Value> {
|
||||
let video = get_video_from_db(database_id, db).await?;
|
||||
let video_json = serde_json::to_value(&video)?;
|
||||
let kind_json = serde_json::to_value(ServiceItemKind::Video)?;
|
||||
let json = serde_json::json!({"item": video_json, "kind": kind_json});
|
||||
Ok(json)
|
||||
}
|
||||
|
||||
async fn process_presentation(database_id: i32, db: &mut SqliteConnection) -> Result<Value> {
|
||||
let presentation = get_presentation_from_db(database_id, db).await?;
|
||||
let presentation_json = serde_json::to_value(&presentation)?;
|
||||
let kind_json = match presentation.kind {
|
||||
PresKind::Html => serde_json::to_value(ServiceItemKind::Presentation(PresKind::Html))?,
|
||||
PresKind::Pdf => serde_json::to_value(ServiceItemKind::Presentation(PresKind::Pdf))?,
|
||||
PresKind::Generic => serde_json::to_value(ServiceItemKind::Presentation(PresKind::Generic))?,
|
||||
};
|
||||
let json = serde_json::json!({"item": presentation_json, "kind": kind_json});
|
||||
Ok(json)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use fs::canonicalize;
|
||||
use sqlx::Connection;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tracing::debug;
|
||||
use super::*;
|
||||
|
||||
async fn get_db() -> SqliteConnection {
|
||||
let mut data = dirs::data_local_dir().unwrap();
|
||||
data.push("lumina");
|
||||
data.push("library-db.sqlite3");
|
||||
let mut db_url = String::from("sqlite://");
|
||||
db_url.push_str(data.to_str().unwrap());
|
||||
SqliteConnection::connect(&db_url)
|
||||
.await
|
||||
.expect("problems")
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn test_process_song() {
|
||||
let mut db = get_db().await;
|
||||
let result = process_song(7, &mut db).await;
|
||||
let json_song_file = PathBuf::from("./test/test_song.json");
|
||||
if let Ok(path) = canonicalize(json_song_file) {
|
||||
debug!(file = ?&path);
|
||||
if let Ok(s) = fs::read_to_string(path) {
|
||||
debug!(s);
|
||||
match result {
|
||||
Ok(json) => assert_eq!(json.to_string(), s),
|
||||
Err(e) => panic!("There was an error in processing the song: {e}"),
|
||||
}
|
||||
} else {
|
||||
panic!("String wasn't read from file");
|
||||
}
|
||||
} else {
|
||||
panic!("Cannot find absolute path to test_song.json");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn test_process_image() {
|
||||
let mut db = get_db().await;
|
||||
let result = process_image(3, &mut db).await;
|
||||
let json_image_file = PathBuf::from("./test/test_image.json");
|
||||
if let Ok(path) = canonicalize(json_image_file) {
|
||||
debug!(file = ?&path);
|
||||
if let Ok(s) = fs::read_to_string(path) {
|
||||
debug!(s);
|
||||
match result {
|
||||
Ok(json) => assert_eq!(json.to_string(), s),
|
||||
Err(e) => panic!("There was an error in processing the image: {e}"),
|
||||
}
|
||||
} else {
|
||||
panic!("String wasn't read from file");
|
||||
}
|
||||
} else {
|
||||
panic!("Cannot find absolute path to test_image.json");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn test_process_video() {
|
||||
let mut db = get_db().await;
|
||||
let result = process_video(73, &mut db).await;
|
||||
let json_video_file = PathBuf::from("./test/test_video.json");
|
||||
if let Ok(path) = canonicalize(json_video_file) {
|
||||
debug!(file = ?&path);
|
||||
if let Ok(s) = fs::read_to_string(path) {
|
||||
debug!(s);
|
||||
match result {
|
||||
Ok(json) => assert_eq!(json.to_string(), s),
|
||||
Err(e) => panic!("There was an error in processing the video: {e}"),
|
||||
}
|
||||
} else {
|
||||
panic!("String wasn't read from file");
|
||||
}
|
||||
} else {
|
||||
panic!("Cannot find absolute path to test_video.json");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn test_process_presentation() {
|
||||
let mut db = get_db().await;
|
||||
let result = process_presentation(54, &mut db).await;
|
||||
let json_presentation_file = PathBuf::from("./test/test_presentation.json");
|
||||
if let Ok(path) = canonicalize(json_presentation_file) {
|
||||
debug!(file = ?&path);
|
||||
if let Ok(s) = fs::read_to_string(path) {
|
||||
debug!(s);
|
||||
match result {
|
||||
Ok(json) => assert_eq!(json.to_string(), s),
|
||||
Err(e) => panic!("There was an error in processing the presentation: {e}"),
|
||||
}
|
||||
} else {
|
||||
panic!("String wasn't read from file");
|
||||
}
|
||||
} else {
|
||||
panic!("Cannot find absolute path to test_presentation.json");
|
||||
}
|
||||
}
|
||||
|
||||
fn get_items() -> Vec<ServiceItem> {
|
||||
let items = vec![
|
||||
ServiceItem {
|
||||
database_id: 7,
|
||||
kind: ServiceItemKind::Song,
|
||||
id: 0,
|
||||
},
|
||||
ServiceItem {
|
||||
database_id: 54,
|
||||
kind: ServiceItemKind::Presentation(PresKind::Html),
|
||||
id: 0,
|
||||
},
|
||||
ServiceItem {
|
||||
database_id: 73,
|
||||
kind: ServiceItemKind::Video,
|
||||
id: 0,
|
||||
},
|
||||
];
|
||||
items
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_service_items() {
|
||||
let mut db = get_db().await;
|
||||
let items = get_items();
|
||||
let json_item_file = PathBuf::from("./test/test_service_items.json");
|
||||
let result = process_service_items(&items, &mut db).await;
|
||||
if let Ok(path) = canonicalize(json_item_file) {
|
||||
if let Ok(s) = fs::read_to_string(path) {
|
||||
match result {
|
||||
Ok(strings) => assert_eq!(strings.to_string(), s),
|
||||
Err(e) => panic!("There was an error: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// #[tokio::test]
|
||||
// async fn test_save() {
|
||||
// let path = PathBuf::from("~/dev/lumina/src/rust/core/test.pres");
|
||||
// let list = get_items();
|
||||
// match save(list, path).await {
|
||||
// Ok(_) => assert!(true),
|
||||
// Err(e) => panic!("There was an error: {e}"),
|
||||
// }
|
||||
// }
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_store() {
|
||||
let path = PathBuf::from("/home/chris/dev/lumina/src/rust/core/test.pres");
|
||||
let save_file = match File::create(path) {
|
||||
Ok(f) => f,
|
||||
Err(e) => panic!("Couldn't create save_file: {e}"),
|
||||
};
|
||||
let mut db = get_db().await;
|
||||
let list = get_items();
|
||||
if let Ok(json) = process_service_items(&list, &mut db).await {
|
||||
println!("{:?}", json);
|
||||
match store_service_items(&list, &mut db, &save_file, &json).await {
|
||||
Ok(_) => assert!(true),
|
||||
Err(e) => panic!("There was an error: {e}"),
|
||||
}
|
||||
} else {
|
||||
panic!("There was an error getting the json value");
|
||||
}
|
||||
}
|
||||
|
||||
// #[tokio::test]
|
||||
// async fn test_things() {
|
||||
// let mut temp_dir = dirs::data_dir().unwrap();
|
||||
// temp_dir.push("lumina");
|
||||
// let mut s: String =
|
||||
// iter::repeat_with(fastrand::alphanumeric)
|
||||
// .take(5)
|
||||
// .collect();
|
||||
// s.insert_str(0, "temp_");
|
||||
// temp_dir.push(s);
|
||||
// let _ = fs::create_dir_all(&temp_dir);
|
||||
// let mut db = get_db().await;
|
||||
// let service_file = temp_dir.join("serviceitems.json");
|
||||
// let list = get_items();
|
||||
// if let Ok(json) = process_service_items(&list, &mut db).await {
|
||||
// let _ = fs::File::create(&service_file);
|
||||
// match fs::write(service_file, json.to_string()) {
|
||||
// Ok(_) => assert!(true),
|
||||
// Err(e) => panic!("There was an error: {e}"),
|
||||
// }
|
||||
// } else {
|
||||
// panic!("There was an error getting the json value");
|
||||
// }
|
||||
// }
|
||||
}
|
83
src/core/images.rs
Normal file
83
src/core/images.rs
Normal file
|
@ -0,0 +1,83 @@
|
|||
use crate::model::Model;
|
||||
use color_eyre::eyre::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{query_as, SqliteConnection};
|
||||
use std::path::PathBuf;
|
||||
use tracing::error;
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Image {
|
||||
pub id: i32,
|
||||
pub title: String,
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
impl Model<Image> {
|
||||
pub fn load_from_db(&mut self) {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
rt.block_on(async {
|
||||
let result = query_as!(Image, r#"SELECT title as "title!", filePath as "path!", id as "id: i32" from images"#).fetch_all(&mut self.db).await;
|
||||
match result {
|
||||
Ok(v) => {
|
||||
for image in v.into_iter() {
|
||||
let _ = self.add_item(image);
|
||||
}
|
||||
}
|
||||
Err(e) => error!("There was an error in converting images: {e}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub async fn get_image_from_db(database_id: i32, db: &mut SqliteConnection) -> Result<Image> {
|
||||
Ok(query_as!(Image, r#"SELECT title as "title!", filePath as "path!", id as "id: i32" from images where id = ?"#, database_id).fetch_one(db).await?)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::{assert_eq, assert_ne};
|
||||
|
||||
fn test_image(title: String) -> Image {
|
||||
Image {
|
||||
title,
|
||||
path: PathBuf::from("~/pics/camprules2024.mp4"),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_db_and_model() {
|
||||
let mut image_model: Model<Image> = Model::default();
|
||||
image_model.load_from_db();
|
||||
if let Some(image) = image_model.find(|i| i.id == 3) {
|
||||
let test_image = test_image("nccq5".into());
|
||||
assert_eq!(test_image.title, image.title);
|
||||
} else {
|
||||
assert!(false);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_add_image() {
|
||||
let image = test_image("A new image".into());
|
||||
let mut image_model: Model<Image> = Model::default();
|
||||
let result = image_model.add_item(image.clone());
|
||||
let new_image = test_image("A newer image".into());
|
||||
match result {
|
||||
Ok(_) => {
|
||||
assert_eq!(&image, image_model.find(|i| i.id == 0).unwrap());
|
||||
assert_ne!(
|
||||
&new_image,
|
||||
image_model.find(|i| i.id == 0).unwrap()
|
||||
);
|
||||
}
|
||||
Err(e) => assert!(
|
||||
false,
|
||||
"There was an error adding the image: {:?}",
|
||||
e
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
91
src/core/kinds.rs
Normal file
91
src/core/kinds.rs
Normal file
|
@ -0,0 +1,91 @@
|
|||
use std::{error::Error, fmt::Display};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::presentations::PresKind;
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ServiceItemKind {
|
||||
#[default]
|
||||
Song,
|
||||
Video,
|
||||
Image,
|
||||
Presentation(PresKind),
|
||||
Content,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ServiceItemKind {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
let s = match self {
|
||||
Self::Song => "song".to_owned(),
|
||||
Self::Image => "image".to_owned(),
|
||||
Self::Video => "video".to_owned(),
|
||||
Self::Presentation(PresKind::Html) => "html".to_owned(),
|
||||
Self::Presentation(PresKind::Pdf) => "pdf".to_owned(),
|
||||
Self::Presentation(PresKind::Generic) => {
|
||||
"presentation".to_owned()
|
||||
}
|
||||
Self::Content => "content".to_owned(),
|
||||
};
|
||||
write!(f, "{s}")
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for ServiceItemKind {
|
||||
type Error = ParseError;
|
||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||
match value.as_str() {
|
||||
"song" => Ok(Self::Song),
|
||||
"image" => Ok(Self::Image),
|
||||
"video" => Ok(Self::Video),
|
||||
"presentation" => {
|
||||
Ok(Self::Presentation(PresKind::Generic))
|
||||
}
|
||||
"html" => Ok(Self::Presentation(PresKind::Html)),
|
||||
"pdf" => Ok(Self::Presentation(PresKind::Pdf)),
|
||||
"content" => Ok(Self::Content),
|
||||
_ => Err(ParseError::UnknownType),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ServiceItemKind> for String {
|
||||
fn from(val: ServiceItemKind) -> String {
|
||||
match val {
|
||||
ServiceItemKind::Song => "song".to_owned(),
|
||||
ServiceItemKind::Video => "video".to_owned(),
|
||||
ServiceItemKind::Image => "image".to_owned(),
|
||||
ServiceItemKind::Presentation(_) => {
|
||||
"presentation".to_owned()
|
||||
}
|
||||
ServiceItemKind::Content => "content".to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ParseError {
|
||||
UnknownType,
|
||||
}
|
||||
|
||||
impl Error for ParseError {}
|
||||
|
||||
impl Display for ParseError {
|
||||
fn fmt(
|
||||
&self,
|
||||
f: &mut std::fmt::Formatter<'_>,
|
||||
) -> std::fmt::Result {
|
||||
let message = match self {
|
||||
Self::UnknownType => "The type does not exist. It needs to be one of 'song', 'video', 'image', 'presentation', or 'content'",
|
||||
};
|
||||
write!(f, "Error: {message}")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
#[test]
|
||||
pub fn test_kinds() {
|
||||
assert_eq!(true, true)
|
||||
}
|
||||
}
|
160
src/core/lisp.rs
Normal file
160
src/core/lisp.rs
Normal file
|
@ -0,0 +1,160 @@
|
|||
use lexpr::Value;
|
||||
use strum_macros::EnumString;
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, EnumString)]
|
||||
pub(crate) enum Symbol {
|
||||
#[strum(ascii_case_insensitive)]
|
||||
Slide,
|
||||
#[strum(ascii_case_insensitive)]
|
||||
Image,
|
||||
#[strum(ascii_case_insensitive)]
|
||||
Text,
|
||||
#[strum(ascii_case_insensitive)]
|
||||
Video,
|
||||
#[strum(ascii_case_insensitive)]
|
||||
Song,
|
||||
#[strum(disabled)]
|
||||
ImageFit(ImageFit),
|
||||
#[strum(disabled)]
|
||||
VerseOrder(VerseOrder),
|
||||
#[strum(disabled)]
|
||||
#[default]
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, EnumString)]
|
||||
pub(crate) enum Keyword {
|
||||
ImageFit(ImageFit),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, EnumString)]
|
||||
pub(crate) enum ImageFit {
|
||||
#[strum(ascii_case_insensitive)]
|
||||
Cover,
|
||||
#[strum(ascii_case_insensitive)]
|
||||
Fill,
|
||||
#[strum(ascii_case_insensitive)]
|
||||
Crop,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, EnumString)]
|
||||
pub(crate) enum VerseOrder {
|
||||
#[strum(ascii_case_insensitive)]
|
||||
#[default]
|
||||
V1,
|
||||
#[strum(ascii_case_insensitive)]
|
||||
V2,
|
||||
#[strum(ascii_case_insensitive)]
|
||||
V3,
|
||||
#[strum(ascii_case_insensitive)]
|
||||
V4,
|
||||
#[strum(ascii_case_insensitive)]
|
||||
V5,
|
||||
#[strum(ascii_case_insensitive)]
|
||||
V6,
|
||||
#[strum(ascii_case_insensitive)]
|
||||
C1,
|
||||
#[strum(ascii_case_insensitive)]
|
||||
C2,
|
||||
#[strum(ascii_case_insensitive)]
|
||||
C3,
|
||||
#[strum(ascii_case_insensitive)]
|
||||
C4,
|
||||
#[strum(ascii_case_insensitive)]
|
||||
B1,
|
||||
#[strum(ascii_case_insensitive)]
|
||||
B2,
|
||||
#[strum(ascii_case_insensitive)]
|
||||
B3,
|
||||
#[strum(ascii_case_insensitive)]
|
||||
B4,
|
||||
#[strum(ascii_case_insensitive)]
|
||||
O1,
|
||||
#[strum(ascii_case_insensitive)]
|
||||
O2,
|
||||
#[strum(ascii_case_insensitive)]
|
||||
O3,
|
||||
#[strum(ascii_case_insensitive)]
|
||||
O4,
|
||||
#[strum(ascii_case_insensitive)]
|
||||
E1,
|
||||
#[strum(ascii_case_insensitive)]
|
||||
E2,
|
||||
#[strum(ascii_case_insensitive)]
|
||||
I1,
|
||||
#[strum(ascii_case_insensitive)]
|
||||
I2,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, EnumString)]
|
||||
pub(crate) enum SongKeyword {
|
||||
#[strum(ascii_case_insensitive)]
|
||||
Title,
|
||||
#[strum(ascii_case_insensitive)]
|
||||
Author,
|
||||
#[strum(ascii_case_insensitive)]
|
||||
Ccli,
|
||||
#[strum(ascii_case_insensitive)]
|
||||
Audio,
|
||||
#[strum(ascii_case_insensitive)]
|
||||
Font,
|
||||
#[strum(ascii_case_insensitive)]
|
||||
FontSize,
|
||||
#[strum(ascii_case_insensitive)]
|
||||
Background,
|
||||
#[strum(ascii_case_insensitive)]
|
||||
VerseOrder(VerseOrder),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, EnumString)]
|
||||
pub(crate) enum ImageKeyword {
|
||||
#[strum(ascii_case_insensitive)]
|
||||
Source,
|
||||
#[strum(ascii_case_insensitive)]
|
||||
Fit,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, EnumString)]
|
||||
pub(crate) enum VideoKeyword {
|
||||
#[strum(ascii_case_insensitive)]
|
||||
Source,
|
||||
#[strum(ascii_case_insensitive)]
|
||||
Fit,
|
||||
}
|
||||
|
||||
pub(crate) fn get_lists(exp: &Value) -> Vec<Value> {
|
||||
if exp.is_cons() {
|
||||
exp.as_cons().unwrap().to_vec().0
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::fs::read_to_string;
|
||||
|
||||
use lexpr::{parse::Options, Parser};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_list() {
|
||||
let lisp = read_to_string("./test_presentation.lisp").expect("oops");
|
||||
println!("{lisp}");
|
||||
let mut parser = Parser::from_str_custom(&lisp, Options::elisp());
|
||||
for atom in parser.value_iter() {
|
||||
match atom {
|
||||
Ok(atom) => {
|
||||
println!("{atom}");
|
||||
let lists = get_lists(&atom);
|
||||
assert_eq!(lists, vec![Value::Null])
|
||||
}
|
||||
Err(e) => {
|
||||
panic!("{e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1 +1,2 @@
|
|||
pub mod lisp;
|
||||
pub mod slide;
|
||||
|
|
101
src/core/model.rs
Normal file
101
src/core/model.rs
Normal file
|
@ -0,0 +1,101 @@
|
|||
use std::mem::replace;
|
||||
|
||||
use color_eyre::eyre::{eyre, Result};
|
||||
use sqlx::{Connection, SqliteConnection};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Model<T> {
|
||||
pub items: Vec<T>,
|
||||
pub db: SqliteConnection,
|
||||
}
|
||||
impl<T> Model<T> {
|
||||
pub fn add_item(&mut self, item: T) -> Result<()> {
|
||||
self.items.push(item);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn add_to_db(&mut self, item: T) -> Result<()> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub fn update_item(&mut self, item: T, index: i32) -> Result<()> {
|
||||
if let Some(current_item) = self.items.get_mut(index as usize)
|
||||
{
|
||||
let _old_item = replace(current_item, item);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(eyre!(
|
||||
"Item doesn't exist in model. Id was {}",
|
||||
index
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_item(&mut self, index: i32) -> Result<()> {
|
||||
self.items.remove(index as usize);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_item(&self, index: i32) -> Option<&T>
|
||||
{
|
||||
self.items.get(index as usize)
|
||||
}
|
||||
|
||||
pub fn find<P>(&self, f: P) -> Option<&T>
|
||||
where
|
||||
P: FnMut(&&T) -> bool,
|
||||
{
|
||||
self.items.iter().find(f)
|
||||
}
|
||||
|
||||
pub fn insert_item(&mut self, item: T, index: i32) -> Result<()> {
|
||||
self.items.insert(index as usize, item);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Default for Model<T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
items: vec![],
|
||||
db: {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
rt.block_on(async {
|
||||
get_db().await
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_db() -> SqliteConnection {
|
||||
let mut data = dirs::data_local_dir().unwrap();
|
||||
data.push("lumina");
|
||||
data.push("library-db.sqlite3");
|
||||
let mut db_url = String::from("sqlite://");
|
||||
db_url.push_str(data.to_str().unwrap());
|
||||
SqliteConnection::connect(&db_url)
|
||||
.await
|
||||
.expect("problems")
|
||||
}
|
||||
|
||||
pub trait Modeling {
|
||||
type Item;
|
||||
|
||||
fn setup_db() -> SqliteConnection {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
let mut data = dirs::data_local_dir().unwrap();
|
||||
data.push("lumina");
|
||||
data.push("library-db.sqlite3");
|
||||
let mut db_url = String::from("sqlite://");
|
||||
db_url.push_str(data.to_str().unwrap());
|
||||
rt.block_on(async {
|
||||
SqliteConnection::connect(&db_url)
|
||||
.await
|
||||
.expect("problems")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {}
|
121
src/core/presentations.rs
Normal file
121
src/core/presentations.rs
Normal file
|
@ -0,0 +1,121 @@
|
|||
use std::path::PathBuf;
|
||||
use color_eyre::eyre::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{prelude::FromRow, query, sqlite::SqliteRow, Row, SqliteConnection};
|
||||
use tracing::error;
|
||||
|
||||
use crate::model::Model;
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum PresKind {
|
||||
Html,
|
||||
#[default]
|
||||
Pdf,
|
||||
Generic,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Presentation {
|
||||
pub id: i32,
|
||||
pub title: String,
|
||||
pub path: PathBuf,
|
||||
pub kind: PresKind,
|
||||
}
|
||||
|
||||
impl Presentation {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
title: "".to_string(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_kind(&self) -> &PresKind {
|
||||
&self.kind
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRow<'_, SqliteRow> for Presentation {
|
||||
fn from_row(row: &SqliteRow) -> sqlx::Result<Self> {
|
||||
Ok(Self {
|
||||
id: row.try_get(0)?,
|
||||
title: row.try_get(1)?,
|
||||
path: PathBuf::from({
|
||||
let string: String = row.try_get(2)?;
|
||||
string
|
||||
}),
|
||||
kind: if row.try_get(3)? {
|
||||
PresKind::Html
|
||||
} else {
|
||||
PresKind::Pdf
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Model<Presentation> {
|
||||
pub fn load_from_db(&mut self) {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
rt.block_on(async {
|
||||
let result = query!(r#"SELECT id as "id: i32", title, filePath as "path", html from presentations"#).fetch_all(&mut self.db).await;
|
||||
match result {
|
||||
Ok(v) => {
|
||||
for presentation in v.into_iter() {
|
||||
let _ = self.add_item(Presentation {
|
||||
id: presentation.id,
|
||||
title: presentation.title,
|
||||
path: presentation.path.into(),
|
||||
kind: if presentation.html {
|
||||
PresKind::Html
|
||||
} else {
|
||||
PresKind::Pdf
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Err(e) => error!("There was an error in converting presentations: {e}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_presentation_from_db(database_id: i32, db: &mut SqliteConnection) -> Result<Presentation> {
|
||||
let row = query(r#"SELECT id as "id: i32", title, filePath as "path", html from presentations where id = $1"#).bind(database_id).fetch_one(db).await?;
|
||||
Ok(Presentation::from_row(&row)?)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
fn test_presentation() -> Presentation {
|
||||
Presentation {
|
||||
id: 54,
|
||||
title: "20240327T133649--12-isaiah-and-jesus__lesson_project_tfc".into(),
|
||||
path: PathBuf::from(
|
||||
"file:///home/chris/docs/notes/lessons/20240327T133649--12-isaiah-and-jesus__lesson_project_tfc.html",
|
||||
),
|
||||
kind: PresKind::Html,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_pres() {
|
||||
let pres = Presentation::new();
|
||||
assert_eq!(pres.get_kind(), &PresKind::Pdf)
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_db_and_model() {
|
||||
let mut presentation_model: Model<Presentation> =
|
||||
Model::default();
|
||||
presentation_model.load_from_db();
|
||||
if let Some(presentation) = presentation_model.find(|p| p.id == 54) {
|
||||
let test_presentation = test_presentation();
|
||||
assert_eq!(&test_presentation, presentation);
|
||||
} else {
|
||||
assert!(false);
|
||||
}
|
||||
}
|
||||
}
|
114
src/core/service_items.rs
Normal file
114
src/core/service_items.rs
Normal file
|
@ -0,0 +1,114 @@
|
|||
use color_eyre::eyre::Result;
|
||||
|
||||
use crate::images::Image;
|
||||
use crate::presentations::Presentation;
|
||||
use crate::songs::Song;
|
||||
use crate::videos::Video;
|
||||
|
||||
use super::kinds::ServiceItemKind;
|
||||
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
pub struct ServiceItem {
|
||||
pub id: i32,
|
||||
pub database_id: i32,
|
||||
pub kind: ServiceItemKind,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
pub struct ServiceItemModel {
|
||||
items: Vec<ServiceItem>,
|
||||
}
|
||||
|
||||
impl From<&Song> for ServiceItem {
|
||||
fn from(song: &Song) -> Self {
|
||||
Self {
|
||||
kind: ServiceItemKind::Song,
|
||||
database_id: song.id,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Video> for ServiceItem {
|
||||
fn from(video: &Video) -> Self {
|
||||
Self {
|
||||
kind: ServiceItemKind::Video,
|
||||
database_id: video.id,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Image> for ServiceItem {
|
||||
fn from(image: &Image) -> Self {
|
||||
Self {
|
||||
kind: ServiceItemKind::Image,
|
||||
database_id: image.id,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Presentation> for ServiceItem {
|
||||
fn from(presentation: &Presentation) -> Self {
|
||||
Self {
|
||||
kind: ServiceItemKind::Presentation(presentation.kind.clone()),
|
||||
database_id: presentation.id,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ServiceItemModel {
|
||||
fn add_item(&mut self, item: impl Into<ServiceItem>) -> Result<()> {
|
||||
let service_item: ServiceItem = item.into();
|
||||
self.items.push(service_item);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::presentations::PresKind;
|
||||
|
||||
use super::*;
|
||||
use pretty_assertions::{assert_eq, assert_ne};
|
||||
|
||||
fn test_song() -> Song {
|
||||
Song {
|
||||
title: "Death Was Arrested".to_string(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn test_presentation() -> Presentation {
|
||||
Presentation {
|
||||
id: 0,
|
||||
title: "20240327T133649--12-isaiah-and-jesus__lesson_project_tfc".into(),
|
||||
path: PathBuf::from(
|
||||
"~/docs/notes/lessons/20240327T133649--12-isaiah-and-jesus__lesson_project_tfc.html",
|
||||
),
|
||||
kind: PresKind::Html,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
pub fn test_service_item() {
|
||||
let song = test_song();
|
||||
let service_item = ServiceItem::from(&song);
|
||||
let pres = test_presentation();
|
||||
let pres_item = ServiceItem::from(&pres);
|
||||
let mut service_model = ServiceItemModel::default();
|
||||
match service_model.add_item(&song) {
|
||||
Ok(_) => {
|
||||
assert_eq!(ServiceItemKind::Song, service_model.items[0].kind);
|
||||
assert_eq!(ServiceItemKind::Presentation(PresKind::Html), pres_item.kind);
|
||||
assert_eq!(service_item, service_model.items[0]);
|
||||
},
|
||||
Err(e) => panic!("Problem adding item: {:?}", e),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,12 +1,20 @@
|
|||
use lexpr::{parse::from_str_elisp, Value};
|
||||
use lexpr::{
|
||||
parse::{from_str_elisp, Options},
|
||||
Parser, Value,
|
||||
};
|
||||
use miette::{miette, IntoDiagnostic, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt::Display,
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use crate::core::lisp::Symbol;
|
||||
|
||||
use super::lisp::get_lists;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum TextAlignment {
|
||||
TopLeft,
|
||||
|
@ -259,9 +267,18 @@ impl SlideBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
struct Image {
|
||||
source: String,
|
||||
fit: String,
|
||||
pub source: String,
|
||||
pub fit: String,
|
||||
pub children: Vec<String>,
|
||||
}
|
||||
impl Image {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_image_bg(atom: &Value, image_map: &mut HashMap<String, String>, map_index: usize) {
|
||||
|
@ -299,22 +316,37 @@ fn build_image_bg(atom: &Value, image_map: &mut HashMap<String, String>, map_ind
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use lexpr::{parse::Options, Parser};
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_lexpr::from_str;
|
||||
use std::fs::read_to_string;
|
||||
use tracing::debug;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn test_slide() -> Slide {
|
||||
Slide::default()
|
||||
fn build_slide(exp: Value) -> Result<Slide> {
|
||||
let mut slide_builder = SlideBuilder::new();
|
||||
let mut keyword = "idk";
|
||||
for value in exp.as_cons().unwrap().to_vec().0 {
|
||||
let mut vecs = vec![vec![]];
|
||||
match value {
|
||||
Value::Symbol(symbol) => {}
|
||||
Value::Keyword(keyword) => {}
|
||||
Value::String(string) => {}
|
||||
Value::Number(num) => {}
|
||||
Value::Cons(cons) => {
|
||||
vecs.push(get_lists(&value));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn print_list(atom: &Value) {
|
||||
match atom {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn build_slides(
|
||||
cons: &lexpr::Cons,
|
||||
mut current_symbol: Symbol,
|
||||
mut slide_builder: SlideBuilder,
|
||||
) -> SlideBuilder {
|
||||
for value in cons.list_iter() {
|
||||
dbg!(¤t_symbol);
|
||||
match value {
|
||||
Value::Cons(v) => {
|
||||
slide_builder = build_slides(&v, current_symbol.clone(), slide_builder);
|
||||
}
|
||||
Value::Nil => {
|
||||
dbg!(Value::Nil);
|
||||
}
|
||||
|
@ -329,26 +361,11 @@ mod test {
|
|||
}
|
||||
Value::Symbol(symbol) => {
|
||||
dbg!(symbol);
|
||||
match symbol.as_ref() {
|
||||
"image" => {
|
||||
dbg!("This is an image");
|
||||
}
|
||||
"slide" => {
|
||||
dbg!("This is a slide");
|
||||
}
|
||||
"text" => {
|
||||
dbg!("This is a text");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
current_symbol = Symbol::from_str(symbol).unwrap_or_default();
|
||||
}
|
||||
Value::Keyword(keyword) => {
|
||||
dbg!(keyword);
|
||||
}
|
||||
Value::Cons(v) => {
|
||||
print_list(v.car());
|
||||
print_list(v.cdr());
|
||||
}
|
||||
Value::Null => {
|
||||
dbg!("null");
|
||||
}
|
||||
|
@ -362,14 +379,22 @@ mod test {
|
|||
dbg!(v);
|
||||
}
|
||||
}
|
||||
// if atom.is_list() {
|
||||
// for atom in atom.list_iter().unwrap() {
|
||||
// dbg!(atom);
|
||||
// print_list(atom);
|
||||
// }
|
||||
// } else {
|
||||
// dbg!(atom);
|
||||
// }
|
||||
}
|
||||
slide_builder
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use lexpr::{parse::Options, Datum, Parser};
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_lexpr::from_str;
|
||||
use std::fs::read_to_string;
|
||||
use tracing::debug;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn test_slide() -> Slide {
|
||||
Slide::default()
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -380,7 +405,10 @@ mod test {
|
|||
for atom in parser.value_iter() {
|
||||
match atom {
|
||||
Ok(atom) => {
|
||||
print_list(&atom);
|
||||
let symbol = Symbol::None;
|
||||
let slide_builder = SlideBuilder::new();
|
||||
atom.as_cons()
|
||||
.map(|c| build_slides(c, symbol, slide_builder));
|
||||
}
|
||||
Err(e) => {
|
||||
dbg!(e);
|
||||
|
|
342
src/core/songs.rs
Normal file
342
src/core/songs.rs
Normal file
|
@ -0,0 +1,342 @@
|
|||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
use color_eyre::eyre::{eyre, Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{query, query_as, sqlite::SqliteRow, FromRow, Row, SqliteConnection};
|
||||
use tracing::{debug, error};
|
||||
|
||||
use crate::{
|
||||
model::{get_db, Model},
|
||||
slides::{Background, TextAlignment},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Song {
|
||||
pub id: i32,
|
||||
pub title: String,
|
||||
pub lyrics: Option<String>,
|
||||
pub author: Option<String>,
|
||||
pub ccli: Option<String>,
|
||||
pub audio: Option<PathBuf>,
|
||||
pub verse_order: Option<Vec<String>>,
|
||||
pub background: Option<Background>,
|
||||
pub text_alignment: Option<TextAlignment>,
|
||||
pub font: Option<String>,
|
||||
pub font_size: Option<i32>,
|
||||
}
|
||||
|
||||
const VERSE_KEYWORDS: [&str; 24] = [
|
||||
"Verse 1", "Verse 2", "Verse 3", "Verse 4", "Verse 5", "Verse 6",
|
||||
"Verse 7", "Verse 8", "Chorus 1", "Chorus 2", "Chorus 3",
|
||||
"Chorus 4", "Bridge 1", "Bridge 2", "Bridge 3", "Bridge 4",
|
||||
"Intro 1", "Intro 2", "Ending 1", "Ending 2", "Other 1",
|
||||
"Other 2", "Other 3", "Other 4",
|
||||
];
|
||||
|
||||
impl FromRow<'_, SqliteRow> for Song {
|
||||
fn from_row(row: &SqliteRow) -> sqlx::Result<Self> {
|
||||
Ok(Self {
|
||||
id: row.try_get(12)?,
|
||||
title: row.try_get(5)?,
|
||||
lyrics: row.try_get(8)?,
|
||||
author: row.try_get(10)?,
|
||||
ccli: row.try_get(9)?,
|
||||
audio: Some(PathBuf::from({
|
||||
let string: String = row.try_get(11)?;
|
||||
string
|
||||
})),
|
||||
verse_order: Some({
|
||||
let str: &str = row.try_get(0)?;
|
||||
str.split(' ').map(|s| s.to_string()).collect()
|
||||
}),
|
||||
background: Some({
|
||||
let string: String = row.try_get(7)?;
|
||||
Background::try_from(string)?
|
||||
}),
|
||||
text_alignment: Some({
|
||||
let horizontal_alignment: String = row.try_get(3)?;
|
||||
let vertical_alignment: String = row.try_get(4)?;
|
||||
match (horizontal_alignment.to_lowercase().as_str(), vertical_alignment.to_lowercase().as_str()) {
|
||||
("left", "top") => TextAlignment::TopLeft,
|
||||
("left", "center") => TextAlignment::MiddleLeft,
|
||||
("left", "bottom") => TextAlignment::BottomLeft,
|
||||
("center", "top") => TextAlignment::TopCenter,
|
||||
("center", "center") => TextAlignment::MiddleCenter,
|
||||
("center", "bottom") => TextAlignment::BottomCenter,
|
||||
("right", "top") => TextAlignment::TopRight,
|
||||
("right", "center") => TextAlignment::MiddleRight,
|
||||
("right", "bottom") => TextAlignment::BottomRight,
|
||||
_ => TextAlignment::MiddleCenter
|
||||
}
|
||||
}),
|
||||
font: row.try_get(6)?,
|
||||
font_size: row.try_get(1)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub async fn get_song_from_db(index: i32, db: &mut SqliteConnection) -> Result<Song> {
|
||||
let row = query(r#"SELECT vorder as "verse_order!", fontSize as "font_size!: i32", backgroundType as "background_type!", horizontalTextAlignment as "horizontal_text_alignment!", verticalTextAlignment as "vertical_text_alignment!", title as "title!", font as "font!", background as "background!", lyrics as "lyrics!", ccli as "ccli!", author as "author!", audio as "audio!", id as "id: i32" from songs where id = $1"#).bind(index).fetch_one(db).await?;
|
||||
Ok(Song::from_row(&row)?)
|
||||
}
|
||||
|
||||
|
||||
impl Model<Song> {
|
||||
pub fn load_from_db(&mut self) {
|
||||
// static DATABASE_URL: &str = "sqlite:///home/chris/.local/share/lumina/library-db.sqlite3";
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
rt.block_on(async {
|
||||
let result = query(r#"SELECT vorder as "verse_order!", fontSize as "font_size!: i32", backgroundType as "background_type!", horizontalTextAlignment as "horizontal_text_alignment!", verticalTextAlignment as "vertical_text_alignment!", title as "title!", font as "font!", background as "background!", lyrics as "lyrics!", ccli as "ccli!", author as "author!", audio as "audio!", id as "id: i32" from songs"#).fetch_all(&mut self.db).await;
|
||||
match result {
|
||||
Ok(s) => {
|
||||
for song in s.into_iter() {
|
||||
match Song::from_row(&song) {
|
||||
Ok(song) => {
|
||||
let _ = self.add_item(song);
|
||||
},
|
||||
Err(e) => error!("Could not convert song: {e}"),
|
||||
};
|
||||
};
|
||||
},
|
||||
Err(e) => {
|
||||
error!("There was an error in converting songs: {e}");
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Song {
|
||||
pub fn get_lyrics(&self) -> Result<Vec<String>> {
|
||||
let mut lyric_list = Vec::new();
|
||||
if self.lyrics.is_none() {
|
||||
return Err(eyre!("There is no lyrics here"));
|
||||
} else if self.verse_order.is_none() {
|
||||
return Err(eyre!("There is no verse_order here"));
|
||||
} else if self
|
||||
.verse_order
|
||||
.clone()
|
||||
.is_some_and(|v| v.is_empty())
|
||||
{
|
||||
return Err(eyre!("There is no verse_order here"));
|
||||
}
|
||||
if let Some(raw_lyrics) = self.lyrics.clone() {
|
||||
let raw_lyrics = raw_lyrics.as_str();
|
||||
let verse_order = self.verse_order.clone();
|
||||
|
||||
let mut lyric_map = HashMap::new();
|
||||
let mut verse_title = String::from("");
|
||||
let mut lyric = String::from("");
|
||||
for (i, line) in raw_lyrics.split('\n').enumerate() {
|
||||
if VERSE_KEYWORDS.contains(&line) {
|
||||
if i != 0 {
|
||||
lyric_map.insert(verse_title, lyric);
|
||||
lyric = String::from("");
|
||||
verse_title = line.to_string();
|
||||
} else {
|
||||
verse_title = line.to_string();
|
||||
}
|
||||
} else {
|
||||
lyric.push_str(line);
|
||||
lyric.push('\n');
|
||||
}
|
||||
}
|
||||
lyric_map.insert(verse_title, lyric);
|
||||
|
||||
for verse in verse_order.unwrap_or_default() {
|
||||
let mut verse_name = "";
|
||||
debug!(verse = verse);
|
||||
for word in VERSE_KEYWORDS {
|
||||
let end_verse =
|
||||
verse.get(1..2).unwrap_or_default();
|
||||
let beg_verse =
|
||||
verse.get(0..1).unwrap_or_default();
|
||||
if word.starts_with(beg_verse)
|
||||
&& word.ends_with(end_verse)
|
||||
{
|
||||
verse_name = word;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if let Some(lyric) = lyric_map.get(verse_name) {
|
||||
if lyric.contains("\n\n") {
|
||||
let split_lyrics: Vec<&str> =
|
||||
lyric.split("\n\n").collect();
|
||||
for lyric in split_lyrics {
|
||||
if lyric.is_empty() {
|
||||
continue;
|
||||
}
|
||||
lyric_list.push(lyric.to_string());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
lyric_list.push(lyric.to_string());
|
||||
} else {
|
||||
error!("NOT WORKING!");
|
||||
};
|
||||
}
|
||||
for lyric in lyric_list.iter() {
|
||||
debug!(lyric = ?lyric)
|
||||
}
|
||||
Ok(lyric_list)
|
||||
} else {
|
||||
Err(eyre!("There are no lyrics"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::{assert_eq, assert_ne};
|
||||
|
||||
#[test]
|
||||
pub fn test_song_lyrics() {
|
||||
let mut song = Song::default();
|
||||
song.lyrics = Some(
|
||||
"Verse 1
|
||||
When You found me,
|
||||
I was so blind
|
||||
My sin was before me,
|
||||
I was swallowed by pride
|
||||
|
||||
Chorus 1
|
||||
But out of the darkness,
|
||||
You brought me to Your light
|
||||
You showed me new mercy
|
||||
And opened up my eyes
|
||||
|
||||
Chorus 2
|
||||
From the day
|
||||
You saved my soul
|
||||
'Til the very moment
|
||||
When I come home
|
||||
|
||||
I'll sing, I'll dance,
|
||||
My heart will overflow
|
||||
From the day
|
||||
You saved my soul
|
||||
|
||||
Verse 2
|
||||
Where brilliant light
|
||||
Is all around
|
||||
And endless joy
|
||||
Is the only sound
|
||||
|
||||
Chorus 3
|
||||
Oh, rest my heart
|
||||
Forever now
|
||||
Oh, in Your arms
|
||||
I'll always be found
|
||||
|
||||
Bridge 1
|
||||
My love is Yours
|
||||
My heart is Yours
|
||||
My life is Yours
|
||||
Forever
|
||||
|
||||
My love is Yours
|
||||
My heart is Yours
|
||||
My life is Yours
|
||||
Forever
|
||||
|
||||
Other 1
|
||||
From the Day
|
||||
I Am They
|
||||
|
||||
Other 2
|
||||
|
||||
|
||||
Ending 1
|
||||
Oh Oh Oh
|
||||
From the day
|
||||
You saved my soul"
|
||||
.to_string(),
|
||||
);
|
||||
song.verse_order =
|
||||
"O1 V1 C1 C2 O2 V2 C3 C2 O2 B1 C2 C2 E1 O2"
|
||||
.to_string()
|
||||
.split(' ')
|
||||
.map(|s| Some(s.to_string()))
|
||||
.collect();
|
||||
let lyrics = song.get_lyrics();
|
||||
match lyrics {
|
||||
Ok(lyrics) => {
|
||||
assert_eq!(vec!["From the Day\nI Am They", "When You found me,\nI was so blind\nMy sin was before me,\nI was swallowed by pride", "But out of the darkness,\nYou brought me to Your light\nYou showed me new mercy\nAnd opened up my eyes", "From the day\nYou saved my soul\n'Til the very moment\nWhen I come home", "I'll sing, I'll dance,\nMy heart will overflow\nFrom the day\nYou saved my soul", "Where brilliant light\nIs all around\nAnd endless joy\nIs the only sound", "Oh, rest my heart\nForever now\nOh, in Your arms\nI'll always be found", "From the day\nYou saved my soul\n'Til the very moment\nWhen I come home", "I'll sing, I'll dance,\nMy heart will overflow\nFrom the day\nYou saved my soul", "My love is Yours\nMy heart is Yours\nMy life is Yours\nForever", "My love is Yours\nMy heart is Yours\nMy life is Yours\nForever", "From the day\nYou saved my soul\n'Til the very moment\nWhen I come home", "I'll sing, I'll dance,\nMy heart will overflow\nFrom the day\nYou saved my soul", "From the day\nYou saved my soul\n'Til the very moment\nWhen I come home", "I'll sing, I'll dance,\nMy heart will overflow\nFrom the day\nYou saved my soul", "Oh Oh Oh\nFrom the day\nYou saved my soul\n"], lyrics);
|
||||
}
|
||||
Err(e) => {
|
||||
assert!(false, "{:?}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_db_and_model() {
|
||||
let mut song_model: Model<Song> = Model::default();
|
||||
song_model.load_from_db();
|
||||
if let Some(song) = song_model.find(|s| s.id == 7) {
|
||||
let test_song = test_song();
|
||||
assert_eq!(&test_song, song);
|
||||
} else {
|
||||
assert!(false);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
pub async fn test_song_from_db() {
|
||||
let song = test_song();
|
||||
let result = get_song_from_db(7, &mut get_db().await).await;
|
||||
match result {
|
||||
Ok(db_song) => assert_eq!(song, db_song),
|
||||
Err(e) => assert!(false, "{e}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_update() {
|
||||
let song = test_song();
|
||||
let cloned_song = song.clone();
|
||||
let mut song_model: Model<Song> = Model::default();
|
||||
song_model.load_from_db();
|
||||
|
||||
match song_model.update_item(song, 2) {
|
||||
Ok(()) => assert_eq!(
|
||||
&cloned_song,
|
||||
song_model.find(|s| s.id == 7).unwrap()
|
||||
),
|
||||
Err(e) => assert!(false, "{e}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn test_song() -> Song {
|
||||
Song {
|
||||
id: 7,
|
||||
title: "Death Was Arrested".to_string(),
|
||||
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()),
|
||||
author: Some(
|
||||
"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)
|
||||
}
|
||||
}
|
||||
}
|
87
src/core/videos.rs
Normal file
87
src/core/videos.rs
Normal file
|
@ -0,0 +1,87 @@
|
|||
use crate::model::Model;
|
||||
use color_eyre::eyre::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{query_as, SqliteConnection};
|
||||
use std::path::PathBuf;
|
||||
use tracing::error;
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Video {
|
||||
pub id: i32,
|
||||
pub title: String,
|
||||
pub path: PathBuf,
|
||||
pub start_time: Option<f32>,
|
||||
pub end_time: Option<f32>,
|
||||
pub looping: bool,
|
||||
}
|
||||
|
||||
impl Model<Video> {
|
||||
pub fn load_from_db(&mut self) {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
rt.block_on(async {
|
||||
let result = query_as!(Video, r#"SELECT title as "title!", filePath as "path!", startTime as "start_time!: f32", endTime as "end_time!: f32", loop as "looping!", id as "id: i32" from videos"#).fetch_all(&mut self.db).await;
|
||||
match result {
|
||||
Ok(v) => {
|
||||
for video in v.into_iter() {
|
||||
let _ = self.add_item(video);
|
||||
}
|
||||
}
|
||||
Err(e) => error!("There was an error in converting videos: {e}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub async fn get_video_from_db(database_id: i32, db: &mut SqliteConnection) -> Result<Video> {
|
||||
Ok(query_as!(Video, r#"SELECT title as "title!", filePath as "path!", startTime as "start_time!: f32", endTime as "end_time!: f32", loop as "looping!", id as "id: i32" from videos where id = ?"#, database_id).fetch_one(db).await?)
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::{assert_eq, assert_ne};
|
||||
|
||||
fn test_video(title: String) -> Video {
|
||||
Video {
|
||||
title,
|
||||
path: PathBuf::from("~/vids/camprules2024.mp4"),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_db_and_model() {
|
||||
let mut video_model: Model<Video> = Model::default();
|
||||
video_model.load_from_db();
|
||||
if let Some(video) = video_model.find(|v| v.id == 73) {
|
||||
let test_video = test_video("Getting started with Tokio. The ultimate starter guide to writing async Rust.".into());
|
||||
assert_eq!(test_video.title, video.title);
|
||||
} else {
|
||||
assert!(false);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_add_video() {
|
||||
let video = test_video("A new video".into());
|
||||
let mut video_model: Model<Video> = Model::default();
|
||||
let result = video_model.add_item(video.clone());
|
||||
let new_video = test_video("A newer video".into());
|
||||
match result {
|
||||
Ok(_) => {
|
||||
assert_eq!(&video, video_model.find(|v| v.id == 0).unwrap());
|
||||
assert_ne!(
|
||||
&new_video,
|
||||
video_model.find(|v| v.id == 0).unwrap()
|
||||
);
|
||||
}
|
||||
Err(e) => assert!(
|
||||
false,
|
||||
"There was an error adding the video: {:?}",
|
||||
e
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,13 +1,19 @@
|
|||
(slide (image :source "~/pics/frodo.jpg" :fill crop
|
||||
(text "This is frodo" :font-size 50)))
|
||||
|
||||
;; (slide ((background . ((path . "cool.jpg")
|
||||
;; (kind . Image)))
|
||||
;; (text . "hi")
|
||||
;; (font . "quicksand")
|
||||
;; (id . 0)
|
||||
;; (font_size . 50)
|
||||
;; (text_alignment . MiddleCenter)
|
||||
;; (video_loop . #f)
|
||||
;; (video_start_time . 0.0)
|
||||
;; (video_end_time . 0.0)))
|
||||
(slide :background (image :source "~/pics/frodo.jpg" :fit crop)
|
||||
(text "This is frodo" :font-size 50))
|
||||
(slide (video :source "~/vids/test/chosensmol.mp4" :fit fill))
|
||||
(song :author "Jordan Feliz" :ccli 97987
|
||||
:font "Quicksand" :font-size 80
|
||||
:title "The River"
|
||||
:background (image :source "./coolbg.jpg")
|
||||
(text "I'm going down to the river")
|
||||
(text "Down to the river")
|
||||
(text "Down to the river to pray ay ay!"))
|
||||
(song :author "Jordan Feliz" :ccli 97987
|
||||
:font "Quicksand" :font-size 80
|
||||
:title "The River"
|
||||
:background (video :source "./coolerbg.mkv" :fit cover)
|
||||
:verse-order (v1 c1 v2 c1)
|
||||
(v1 "I'm going down to the river")
|
||||
(c1 "Down to the river")
|
||||
(v2 "Down to the river to pray ay ay!"))
|
||||
(load "./10000-reasons.lisp")
|
||||
|
|
Loading…
Reference in a new issue