trying to remodel the models...

This may not be possible since the models are owned by the
library. This means that in order to run the sql queries, you will
need to pass owned versions of the models to the task and therefore
clone the entire model perhaps. Not ideal....
This commit is contained in:
Chris Cochrun 2026-04-06 13:59:55 -05:00
parent 69410e3b6e
commit 588ef0df56
7 changed files with 433 additions and 203 deletions

View file

@ -12,6 +12,8 @@ This is working but the right click context menu is all the way on the edge of t
Let's build some tests that ensure that these functions are working for the models. Make sure the models are built in such a way as to make sure that they are testable and work fast for the user.
Need to make some tests in the library module that can run things without needing messages as much. I'm redoing how the models work too such that the adding, updating, and removing of items all happens in just the model without needing to have a separate function to do the same action in the database.
* TODO [#B] Font in the song editor doesn't always use the original version
There seems to be some issue with fontdb not able to decipher all the versions of some fonts that are OTF and then end up loading the wrong ones in some issues.

View file

@ -156,6 +156,70 @@ impl ServiceTrait for Image {
}
impl Model<Image> {
pub async fn append_image(
&mut self,
image: Image,
db: PoolConnection<Sqlite>,
) -> Result<()> {
todo!()
}
pub async fn new_image(
&mut self,
db: PoolConnection<Sqlite>,
) -> Result<Image> {
todo!()
}
pub async fn update_image(
&mut self,
image: Image,
db: PoolConnection<Sqlite>,
) -> Result<()> {
let id = image.id;
self.update_item(image.clone(), |current_image| {
current_image.id == id
})?;
let path = image
.path
.to_str()
.map(std::string::ToString::to_string)
.unwrap_or_default();
let mut db = db.detach();
debug!(?image, "should be been updated");
let result = query!(
r#"UPDATE images SET title = $2, file_path = $3 WHERE id = $1"#,
image.id,
image.title,
path,
)
.execute(&mut db)
.await.into_diagnostic();
match result {
Ok(_) => {
debug!("should have been updated");
Ok(())
}
Err(e) => {
error! {?e};
Err(e)
}
}
}
pub async fn remove_image(
&mut self,
id: i32,
db: PoolConnection<Sqlite>,
) -> Result<()> {
self.remove_item(|image| image.id == id)?;
query!("DELETE FROM images WHERE id = $1", id)
.execute(&mut db.detach())
.await
.into_diagnostic()
.map(|_| ())
}
pub async fn new_image_model(db: &mut SqlitePool) -> Self {
let mut model = Self {
items: vec![],

View file

@ -83,30 +83,35 @@ impl<T> Model<T> {
todo!()
}
pub fn update_item(&mut self, item: T, index: i32) -> Result<()> {
pub fn update_item<P>(
&mut self,
item: T,
predicate: P,
) -> Result<()>
where
P: Fn(&T) -> bool,
{
self.items
.get_mut(
usize::try_from(index)
.expect("Shouldn't be negative"),
)
.map_or_else(
|| {
Err(miette!(
"Item doesn't exist in model. Id was {index}"
))
},
|current_item| {
let _old_item = replace(current_item, item);
Ok(())
},
)
.iter()
.position(predicate)
.ok_or(miette!("Item cannot be found"))
.map(|index| self.items.get_mut(index).expect("Since we found position this should always exist"))
.map(|current_item| {
let _old_item = replace(current_item, item);
})
}
pub fn remove_item(&mut self, index: i32) -> Result<()> {
self.items.remove(
usize::try_from(index).expect("Shouldn't be negative"),
);
Ok(())
pub fn remove_item<P>(&mut self, predicate: P) -> Result<()>
where
P: Fn(&T) -> bool,
{
self.items
.iter()
.position(predicate)
.ok_or(miette!("Item cannot be found"))
.map(|index| {
self.items.remove(index);
})
}
#[must_use]
@ -123,11 +128,12 @@ impl<T> Model<T> {
self.items.iter().find(f)
}
pub fn insert_item(&mut self, item: T, index: i32) -> Result<()> {
self.items.insert(
usize::try_from(index).expect("Shouldn't be negative"),
item,
);
pub fn insert_item(
&mut self,
item: T,
index: usize,
) -> Result<()> {
self.items.insert(index, item);
Ok(())
}
}

View file

@ -298,6 +298,35 @@ impl FromRow<'_, SqliteRow> for Presentation {
}
impl Model<Presentation> {
pub async fn append_presentation(
&mut self,
presentation: Presentation,
db: PoolConnection<Sqlite>,
) -> Result<()> {
todo!()
}
pub async fn new_presentation(
&mut self,
db: PoolConnection<Sqlite>,
) -> Result<()> {
todo!()
}
pub async fn update_presentation(
&mut self,
presentation: Presentation,
db: PoolConnection<Sqlite>,
) -> Result<()> {
todo!()
}
pub async fn remove_presentation(
&mut self,
id: i32,
db: PoolConnection<Sqlite>,
) -> Result<()> {
todo!()
}
pub async fn new_presentation_model(db: &mut SqlitePool) -> Self {
let mut model = Self {
items: vec![],

View file

@ -14,9 +14,8 @@ use itertools::Itertools;
use miette::{IntoDiagnostic, Result, miette};
use serde::{Deserialize, Serialize};
use sqlx::{
FromRow, Row, Sqlite, SqliteConnection, SqliteExecutor,
SqlitePool, Transaction, pool::PoolConnection, query,
sqlite::SqliteRow,
FromRow, Row, Sqlite, SqliteConnection, SqlitePool,
pool::PoolConnection, query, sqlite::SqliteRow,
};
use tracing::{debug, error};
@ -740,6 +739,187 @@ pub async fn get_song_from_db(
}
impl Model<Song> {
// Not sure we will use this function. As it is, it makes more sense for
// a new song to be made within the model and then passed back out.
// But maybe for encapsulation reasons, it makes sense to have this?
pub async fn append_song(
&mut self,
song: Song,
db: PoolConnection<Sqlite>,
) -> Result<()> {
self.add_item(song)?;
todo!()
}
pub async fn new_song(
&mut self,
db: PoolConnection<Sqlite>,
) -> Result<Song> {
let mut song = Song::default();
let verse_order = {
song.verse_order.clone().map_or_else(String::new, |vo| {
vo.into_iter()
.map(|mut s| {
s.push(' ');
s
})
.collect::<String>()
})
};
let audio = song
.audio
.clone()
.map(|a| a.to_str().unwrap_or_default().to_string());
let background = song
.background
.clone()
.map(|b| b.path.to_str().unwrap_or_default().to_string());
let res = query!(
r#"INSERT INTO songs (title, lyrics, author, ccli, verse_order, audio, font, font_size, background) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)"#,
song.title,
song.lyrics,
song.author,
song.ccli,
verse_order,
audio,
song.font,
song.font_size,
background
)
.execute(&mut db.detach())
.await
.into_diagnostic()?;
song.id = i32::try_from(res.last_insert_rowid()).expect(
"Fairly confident that this number won't get that high",
);
self.add_item(song.clone())?;
Ok(song)
}
pub async fn update_song(
&mut self,
song: Song,
db: PoolConnection<Sqlite>,
) -> Result<()> {
let id = song.id;
self.update_item(song.clone(), |song| song.id == id)?;
// debug!(?item);
let verse_order =
ron::ser::to_string(&song.verses).into_diagnostic()?;
let audio = song
.audio
.map(|a| a.to_str().unwrap_or_default().to_string());
let background = song
.background
.map(|b| b.path.to_str().unwrap_or_default().to_string());
let lyrics = song.verse_map.map(|map| {
map.iter()
.map(|(name, lyric)| {
let lyric =
lyric.trim_end_matches('\n').to_string();
(name.to_owned(), lyric)
})
.collect::<HashMap<VerseName, String>>()
});
let lyrics =
ron::ser::to_string(&lyrics).into_diagnostic()?;
let (vertical_alignment, horizontal_alignment) =
song.text_alignment.map_or_else(
|| ("center", "center"),
|ta| match ta {
TextAlignment::TopLeft => ("top", "left"),
TextAlignment::TopCenter => ("top", "center"),
TextAlignment::TopRight => ("top", "right"),
TextAlignment::MiddleLeft => ("center", "left"),
TextAlignment::MiddleCenter => {
("center", "center")
}
TextAlignment::MiddleRight => ("center", "right"),
TextAlignment::BottomLeft => ("bottom", "left"),
TextAlignment::BottomCenter => {
("bottom", "center")
}
TextAlignment::BottomRight => ("bottom", "right"),
},
);
let stroke_size = song.stroke_size.unwrap_or_default();
let shadow_size = song.shadow_size.unwrap_or_default();
let (shadow_offset_x, shadow_offset_y) =
song.shadow_offset.unwrap_or_default();
let stroke_color = ron::ser::to_string(&song.stroke_color)
.into_diagnostic()?;
let shadow_color = ron::ser::to_string(&song.shadow_color)
.into_diagnostic()?;
let style = ron::ser::to_string(&song.font_style)
.into_diagnostic()?;
let weight = ron::ser::to_string(&song.font_weight)
.into_diagnostic()?;
// debug!(
// ?stroke_size,
// ?stroke_color,
// ?shadow_size,
// ?shadow_color,
// ?shadow_offset_x,
// ?shadow_offset_y
// );
let result = query!(
r#"UPDATE songs SET title = $2, lyrics = $3, author = $4, ccli = $5, verse_order = $6, audio = $7, font = $8, font_size = $9, background = $10, horizontal_text_alignment = $11, vertical_text_alignment = $12, stroke_color = $13, shadow_color = $14, stroke_size = $15, shadow_size = $16, shadow_offset_x = $17, shadow_offset_y = $18, style = $19, weight = $20 WHERE id = $1"#,
song.id,
song.title,
lyrics,
song.author,
song.ccli,
verse_order,
audio,
song.font,
song.font_size,
background,
horizontal_alignment,
vertical_alignment,
stroke_color,
shadow_color,
stroke_size,
shadow_size,
shadow_offset_x,
shadow_offset_y,
style,
weight
)
.execute(&mut db.detach())
.await
.into_diagnostic()?;
debug!(rows_affected = ?result.rows_affected());
Ok(())
}
pub async fn remove_song(
&mut self,
id: i32,
db: PoolConnection<Sqlite>,
) -> Result<()> {
self.remove_item(|current_song| id == current_song.id)?;
query!("DELETE FROM songs WHERE id = $1", id)
.execute(&mut db.detach())
.await
.into_diagnostic()
.map(|_| ())
}
pub async fn new_song_model(db: &mut SqlitePool) -> Self {
let mut model = Self {
items: vec![],
@ -1429,7 +1609,9 @@ You saved my soul"
let test_song = song_model.get_item(2);
assert_ne!(test_song, Some(&cloned_song));
match song_model.update_item(song, 2) {
match song_model
.update_item(song, |song| song.id == cloned_song.id)
{
Ok(()) => {
let updated_model_song =
song_model.find(|s| s.id == 7).unwrap();

View file

@ -198,6 +198,36 @@ impl ServiceTrait for Video {
}
impl Model<Video> {
pub async fn append_video(
&mut self,
video: Video,
db: PoolConnection<Sqlite>,
) -> Result<()> {
todo!()
}
pub async fn new_video(
&mut self,
db: PoolConnection<Sqlite>,
) -> Result<()> {
todo!()
}
pub async fn update_video(
&mut self,
video: Video,
db: PoolConnection<Sqlite>,
) -> Result<()> {
todo!()
}
pub async fn remove_video(
&mut self,
id: i32,
db: PoolConnection<Sqlite>,
) -> Result<()> {
todo!()
}
pub async fn new_video_model(db: &mut SqlitePool) -> Self {
let mut model = Self {
items: vec![],

View file

@ -492,23 +492,15 @@ impl<'a> Library {
return Action::None;
}
if self
.song_library
.update_item(song.clone(), index)
.is_err()
{
error!("Couldn't update song in model");
return Action::None;
}
return Action::Task(
Task::future(self.db.acquire())
.map_err(|e| {
miette::miette!("Database error: {e}")
})
.and_then(move |conn| {
.and_then(move |db| {
Task::perform(
update_song_in_db(song.clone(), conn),
self.song_library
.update_song(song, db),
|r| r.map(|_| Message::SongChanged),
)
})
@ -530,24 +522,6 @@ impl<'a> Library {
return Action::None;
}
if self
.image_library
.update_item(image.clone(), index)
.is_err()
{
error!("Couldn't update image in model");
return Action::None;
}
if self
.image_library
.update_item(image.clone(), index)
.is_err()
{
error!("Couldn't update image in model");
return Action::None;
}
return Action::Task(
Task::future(self.db.acquire())
.map_err(|e| {
@ -555,10 +529,8 @@ impl<'a> Library {
})
.and_then(move |conn| {
Task::perform(
update_image_in_db(
image.clone(),
conn,
),
self.image_library
.update_image(image, conn),
|r| r.map(|_| Message::ImageChanged),
)
})
@ -577,15 +549,6 @@ impl<'a> Library {
return Action::None;
}
if self
.video_library
.update_item(video.clone(), index)
.is_err()
{
error!("Couldn't update video in model");
return Action::None;
}
return Action::Task(
Task::future(self.db.acquire())
.map_err(|e| {
@ -593,10 +556,8 @@ impl<'a> Library {
})
.and_then(move |conn| {
Task::perform(
update_video_in_db(
video.clone(),
conn,
),
self.video_library
.update_video(video, conn),
|r| r.map(|_| Message::VideoChanged),
)
})
@ -614,36 +575,25 @@ impl<'a> Library {
error!("Not editing a presentation item");
return Action::None;
}
let index = self
.presentation_library
.items
.iter()
.position(|pres| pres.id == presentation.id)
.unwrap_or_default()
.try_into()
.unwrap_or_default();
let mut update_fn = move |conn| {
self.presentation_library
.update_presentation(presentation, conn)
};
match self
.presentation_library
.update_item(presentation.clone(), index)
{
Ok(()) => return Action::Task(
Task::future(self.db.acquire()).map_err(|e| {
miette::miette!("Database error: {e}")
}).and_then(
move |conn| {
Task::perform(
update_presentation_in_db(
presentation.clone(),
conn,
),
|r| r.map(|_| Message::PresentationChanged)
)
},
).map(|r| r.unwrap_or(Message::None)),
),
Err(_) => todo!(),
}
return Action::Task(
Task::future(self.db.acquire())
.map_err(|e| {
miette::miette!("Database error: {e}")
})
.and_then(move |conn| {
Task::perform(update_fn(conn), |r| {
r.map(|_| {
Message::PresentationChanged
})
})
})
.map(|r| r.unwrap_or(Message::None)),
);
}
Message::PresentationChanged => (),
Message::Error(_) => (),
@ -1327,30 +1277,21 @@ impl<'a> Library {
if let Some(song) =
self.song_library.get_item(*index)
{
let song = song.clone();
if let Err(e) =
self.song_library.remove_item(*index)
{
error!(?e);
Task::none()
} else {
Task::future(self.db.acquire()).and_then(
move |db| {
Task::perform(
songs::remove_from_db(
db, song.id,
),
|r| {
if let Err(e) = r {
error!(?e);
}
Message::None
},
)
.map(|m| Ok(m))
},
)
}
Task::future(self.db.acquire()).and_then(
move |db| {
Task::perform(
self.song_library
.remove_song(song.id, db),
|r| {
if let Err(e) = r {
error!(?e);
}
Message::None
},
)
.map(|m| Ok(m))
},
)
} else {
Task::none()
}
@ -1359,30 +1300,21 @@ impl<'a> Library {
if let Some(video) =
self.video_library.get_item(*index)
{
let video = video.clone();
if let Err(e) =
self.video_library.remove_item(*index)
{
error!(?e);
Task::none()
} else {
Task::future(self.db.acquire()).and_then(
move |db| {
Task::perform(
videos::remove_from_db(
db, video.id,
),
|r| {
if let Err(e) = r {
error!(?e);
}
Message::None
},
)
.map(|m| Ok(m))
},
)
}
Task::future(self.db.acquire()).and_then(
move |db| {
Task::perform(
self.video_library
.remove_video(video.id, db),
|r| {
if let Err(e) = r {
error!(?e);
}
Message::None
},
)
.map(|m| Ok(m))
},
)
} else {
Task::none()
}
@ -1391,32 +1323,25 @@ impl<'a> Library {
if let Some(image) =
self.image_library.get_item(*index)
{
let image = image.clone();
if let Err(e) =
self.image_library.remove_item(*index)
{
error!(?e);
Task::none()
} else {
debug!("let's remove {0}", image.id);
debug!("let's remove {0}", image.title);
Task::future(self.db.acquire()).and_then(
move |db| {
Task::perform(
images::remove_from_db(
db, image.id,
),
|r| {
if let Err(e) = r {
error!(?e);
}
Message::None
},
)
.map(|m| Ok(m))
},
)
}
debug!(
image.id,
image.title, "let's remove this image",
);
Task::future(self.db.acquire()).and_then(
move |db| {
Task::perform(
self.image_library
.remove_image(image.id, db),
|r| {
if let Err(e) = r {
error!(?e);
}
Message::None
},
)
.map(|m| Ok(m))
},
)
} else {
Task::none()
}
@ -1425,32 +1350,24 @@ impl<'a> Library {
if let Some(presentation) =
self.presentation_library.get_item(*index)
{
let presentation = presentation.clone();
if let Err(e) = self
.presentation_library
.remove_item(*index)
{
error!(?e);
Task::none()
} else {
Task::future(self.db.acquire()).and_then(
move |db| {
Task::perform(
presentations::remove_from_db(
db,
Task::future(self.db.acquire()).and_then(
move |db| {
Task::perform(
self.presentation_library
.remove_presentation(
presentation.id,
db,
),
|r| {
if let Err(e) = r {
error!(?e);
}
Message::None
},
)
.map(|m| Ok(m))
},
)
}
|r| {
if let Err(e) = r {
error!(?e);
}
Message::None
},
)
.map(|m| Ok(m))
},
)
} else {
Task::none()
}