using a better set of data models in songs

This commit is contained in:
Chris Cochrun 2026-01-14 12:27:34 -06:00
parent a8027ec7b9
commit 6e4fe70838
3 changed files with 162 additions and 137 deletions

View file

@ -35,110 +35,80 @@ pub struct Song {
pub font: Option<String>,
pub font_size: Option<i32>,
pub stroke_size: Option<i32>,
pub verses: Option<Vec<Verse>>,
pub verses: Option<Vec<VerseName>>,
pub verse_map: Option<HashMap<VerseName, String>>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Verse {
Verse { number: usize, lyric: String },
PreChorus { number: usize, lyric: String },
Chorus { number: usize, lyric: String },
PostChorus { number: usize, lyric: String },
Bridge { number: usize, lyric: String },
Intro { number: usize, lyric: String },
Outro { number: usize, lyric: String },
Instrumental { number: usize, lyric: String },
Other { number: usize, lyric: String },
#[derive(
Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Hash,
)]
pub enum VerseName {
Verse { number: usize },
PreChorus { number: usize },
Chorus { number: usize },
PostChorus { number: usize },
Bridge { number: usize },
Intro { number: usize },
Outro { number: usize },
Instrumental { number: usize },
Other { number: usize },
}
impl Verse {
impl VerseName {
pub fn get_name(&self) -> String {
match self {
Verse::Verse { number, .. } => {
VerseName::Verse { number, .. } => {
let mut string = "Verse ".to_string();
string.push_str(&number.to_string());
string
}
Verse::PreChorus { number, .. } => {
VerseName::PreChorus { number, .. } => {
let mut string = "Pre-Chorus ".to_string();
string.push_str(&number.to_string());
string
}
Verse::Chorus { number, .. } => {
VerseName::Chorus { number, .. } => {
let mut string = "Chorus ".to_string();
string.push_str(&number.to_string());
string
}
Verse::PostChorus { number, .. } => {
VerseName::PostChorus { number, .. } => {
let mut string = "Post-Chorus ".to_string();
string.push_str(&number.to_string());
string
}
Verse::Bridge { number, .. } => {
VerseName::Bridge { number, .. } => {
let mut string = "Bridge ".to_string();
string.push_str(&number.to_string());
string
}
Verse::Intro { number, .. } => {
VerseName::Intro { number, .. } => {
let mut string = "Intro ".to_string();
string.push_str(&number.to_string());
string
}
Verse::Outro { number, .. } => {
VerseName::Outro { number, .. } => {
let mut string = "Outro ".to_string();
string.push_str(&number.to_string());
string
}
Verse::Instrumental { number, .. } => {
VerseName::Instrumental { number, .. } => {
let mut string = "Instrumental ".to_string();
string.push_str(&number.to_string());
string
}
Verse::Other { number, .. } => {
VerseName::Other { number, .. } => {
let mut string = "Other ".to_string();
string.push_str(&number.to_string());
string
}
}
}
pub fn get_lyric(&self) -> String {
match self {
Verse::Verse { lyric, .. } => lyric.clone(),
Verse::PreChorus { lyric, .. } => lyric.clone(),
Verse::Chorus { lyric, .. } => lyric.clone(),
Verse::PostChorus { lyric, .. } => lyric.clone(),
Verse::Bridge { lyric, .. } => lyric.clone(),
Verse::Intro { lyric, .. } => lyric.clone(),
Verse::Outro { lyric, .. } => lyric.clone(),
Verse::Instrumental { lyric, .. } => lyric.clone(),
Verse::Other { lyric, .. } => lyric.clone(),
}
}
pub fn set_lyrics(&mut self, lyrics: String) {
match self {
Verse::Verse { number: _, lyric } => *lyric = lyrics,
Verse::PreChorus { number: _, lyric } => *lyric = lyrics,
Verse::Chorus { number: _, lyric } => *lyric = lyrics,
Verse::PostChorus { number: _, lyric } => *lyric = lyrics,
Verse::Bridge { number: _, lyric } => *lyric = lyrics,
Verse::Intro { number: _, lyric } => *lyric = lyrics,
Verse::Outro { number: _, lyric } => *lyric = lyrics,
Verse::Instrumental { number: _, lyric } => {
*lyric = lyrics
}
Verse::Other { number: _, lyric } => *lyric = lyrics,
}
}
}
impl Default for Verse {
impl Default for VerseName {
fn default() -> Self {
Self::Verse {
number: 1,
lyric: "".into(),
}
Self::Verse { number: 1 }
}
}
@ -221,6 +191,17 @@ const VERSE_KEYWORDS: [&str; 24] = [
impl FromRow<'_, SqliteRow> for Song {
fn from_row(row: &SqliteRow) -> sqlx::Result<Self> {
let Some((verses, verse_map)) =
lyrics_to_verse(row.try_get(8)?).ok()
else {
return Err(sqlx::Error::ColumnDecode {
index: "8".into(),
source: miette!(
"Couldn't decode the song into verses"
)
.into(),
});
};
Ok(Self {
id: row.try_get(12)?,
title: row.try_get(5)?,
@ -267,7 +248,8 @@ impl FromRow<'_, SqliteRow> for Song {
font: row.try_get(6)?,
font_size: row.try_get(1)?,
stroke_size: None,
verses: lyrics_to_verse(row.try_get(8)?).ok(),
verses: Some(verses),
verse_map: Some(verse_map),
})
}
}
@ -281,8 +263,10 @@ impl From<Value> for Song {
}
}
fn lyrics_to_verse(lyrics: String) -> Result<Vec<Verse>> {
let mut lyric_list = Vec::new();
fn lyrics_to_verse(
lyrics: String,
) -> Result<(Vec<VerseName>, HashMap<VerseName, String>)> {
let mut verse_list = Vec::new();
if lyrics.is_empty() {
return Err(miette!("There is no lyrics here"));
}
@ -307,6 +291,7 @@ fn lyrics_to_verse(lyrics: String) -> Result<Vec<Verse>> {
}
}
lyric_map.insert(verse_title, lyric);
let mut verse_map = HashMap::new();
for (verse_name, lyric) in lyric_map {
let mut verse_elements = verse_name.trim().split_whitespace();
let verse_keyword = verse_elements.next();
@ -323,51 +308,24 @@ fn lyrics_to_verse(lyrics: String) -> Result<Vec<Verse>> {
};
let index = index.parse::<usize>().into_diagnostic()?;
let verse = match keyword {
"Verse" => Verse::Verse {
number: index,
lyric: lyric,
},
"Pre-Chorus" => Verse::PreChorus {
number: index,
lyric: lyric,
},
"Chorus" => Verse::Chorus {
number: index,
lyric: lyric,
},
"Post-Chorus" => Verse::PostChorus {
number: index,
lyric: lyric,
},
"Bridge" => Verse::Bridge {
number: index,
lyric: lyric,
},
"Intro" => Verse::Intro {
number: index,
lyric: lyric,
},
"Outro" => Verse::Outro {
number: index,
lyric: lyric,
},
"Instrumental" => Verse::Instrumental {
number: index,
lyric: lyric,
},
"Other" => Verse::Other {
number: index,
lyric: lyric,
},
_ => Verse::Other {
number: 99,
lyric: lyric,
},
"Verse" => VerseName::Verse { number: index },
"Pre-Chorus" => VerseName::PreChorus { number: index },
"Chorus" => VerseName::Chorus { number: index },
"Post-Chorus" => VerseName::PostChorus { number: index },
"Bridge" => VerseName::Bridge { number: index },
"Intro" => VerseName::Intro { number: index },
"Outro" => VerseName::Outro { number: index },
"Instrumental" => {
VerseName::Instrumental { number: index }
}
"Other" => VerseName::Other { number: index },
_ => VerseName::Other { number: 99 },
};
lyric_list.push(verse);
verse_list.push(verse.clone());
verse_map.insert(verse, lyric);
}
Ok(lyric_list)
Ok((verse_list, verse_map))
}
pub fn lisp_to_song(list: Vec<Value>) -> Song {
@ -725,6 +683,23 @@ pub async fn update_song_in_db(
}
impl Song {
pub fn get_lyric(&self, verse: &VerseName) -> Option<String> {
self.verse_map
.as_ref()
.map(|verse_map| verse_map.get(verse).cloned())
.flatten()
}
pub fn set_lyrics<T: Into<String>>(
&mut self,
verse: &VerseName,
lyrics: T,
) {
if let Some(verse_map) = self.verse_map.as_mut() {
verse_map.entry(verse.clone()).or_insert(lyrics.into());
}
}
pub fn get_lyrics(&self) -> Result<Vec<String>> {
let mut lyric_list = Vec::new();
if self.lyrics.is_none() {
@ -801,7 +776,7 @@ impl Song {
}
}
pub fn update_verse(&mut self, index: usize, verse: Verse) {
pub fn update_verse(&mut self, index: usize, verse: VerseName) {
if let Some(verses) = self.verses.as_mut() {
if let Some(old_verse) = verses.get_mut(index) {
*old_verse = verse;
@ -997,7 +972,8 @@ You saved my soul"
font: Some("Quicksand Bold".to_string()),
font_size: Some(60),
stroke_size: None,
verses: vec![]
verses: Some(vec![]),
verse_map: None,
}
}

View file

@ -23,13 +23,16 @@ use crate::{
core::{
service_items::ServiceTrait,
slide::Slide,
songs::{Song, Verse},
songs::{Song, VerseName},
},
ui::{
presenter::slide_view,
slide_editor::SlideEditor,
text_svg,
widgets::verse_editor::{self, VerseEditor},
widgets::{
draggable,
verse_editor::{self, VerseEditor},
},
},
};
@ -47,6 +50,7 @@ pub struct SongEditor {
verse_order: String,
pub lyrics: text_editor::Content,
editing: bool,
editing_verses_order: bool,
background: Option<Background>,
video: Option<Video>,
ccli: String,
@ -150,6 +154,7 @@ impl SongEditor {
stroke_size: 0,
stroke_open: false,
verses: None,
editing_verses_order: false,
}
}
pub fn update(&mut self, message: Message) -> Action {
@ -208,9 +213,11 @@ impl SongEditor {
},
|slides| Message::UpdateSlides(slides),
);
self.verses = song.verses.map(|vec| {
self.verses = song.verse_map.map(|vec| {
vec.into_iter()
.map(|verse| VerseEditor::new(verse))
.map(|(verse_name, lyric)| {
VerseEditor::new(verse_name, lyric)
})
.collect()
});
return Action::Task(task);
@ -469,18 +476,35 @@ order",
.spacing(5);
let verse_list = if let Some(verse_list) = &self.verses {
Element::from(
column(verse_list.into_iter().enumerate().map(
|(index, v)| {
v.view().map(move |message| {
Message::VerseEditorMessage((
index, message,
))
})
},
))
.spacing(space_l),
)
if self.editing_verses_order {
Element::from(
draggable::column(
verse_list.into_iter().enumerate().map(
|(index, v)| {
v.view(false).map(move |message| {
Message::VerseEditorMessage((
index, message,
))
})
},
),
)
.spacing(space_l),
)
} else {
Element::from(
column(verse_list.into_iter().enumerate().map(
|(index, v)| {
v.view(true).map(move |message| {
Message::VerseEditorMessage((
index, message,
))
})
},
))
.spacing(space_l),
)
}
} else {
Element::from(horizontal_space())
};

View file

@ -1,16 +1,17 @@
use cosmic::{
Apply, Element, Task,
iced::{Length, alignment::Vertical},
iced::{Border, Length, alignment::Vertical},
iced_widget::{column, row},
theme,
widget::{container, icon, text, text_editor},
};
use crate::core::songs::Verse;
use crate::core::songs::VerseName;
#[derive(Debug)]
pub struct VerseEditor {
verse: Verse,
verse_name: VerseName,
lyric: String,
content: text_editor::Content,
}
@ -22,16 +23,16 @@ pub enum Message {
pub enum Action {
Task(Task<Message>),
UpdateVerse(Verse),
UpdateVerse(VerseName),
None,
}
impl VerseEditor {
pub fn new(verse: Verse) -> Self {
let text = verse.get_lyric();
pub fn new(verse: VerseName, lyric: String) -> Self {
Self {
verse,
content: text_editor::Content::with_text(&text),
verse_name: verse,
lyric: lyric.clone(),
content: text_editor::Content::with_text(&lyric),
}
}
pub fn update(&mut self, message: Message) -> Action {
@ -39,15 +40,15 @@ impl VerseEditor {
Message::ChangeText(action) => {
self.content.perform(action);
let lyrics = self.content.text();
self.verse.set_lyrics(lyrics);
let verse = self.verse.clone();
self.lyric = lyrics;
let verse = self.verse_name.clone();
Action::UpdateVerse(verse)
}
Message::None => Action::None,
}
}
pub fn view(&self) -> Element<Message> {
pub fn view(&self, editable: bool) -> Element<Message> {
let cosmic::cosmic_theme::Spacing {
space_xxs,
space_xs,
@ -60,17 +61,41 @@ impl VerseEditor {
} = theme::spacing();
let verse_title =
text::heading(self.verse.get_name()).size(space_m);
let editor = text_editor(&self.content)
.on_action(Message::ChangeText)
.padding(space_s)
.height(Length::Fill);
text::heading(self.verse_name.get_name()).size(space_m);
let lyric: Element<Message> = if editable {
text_editor(&self.content)
.on_action(Message::ChangeText)
.padding(space_s)
.height(Length::Fill)
// .style(|theme, status| {
// let mut style =
// text_editor::default(theme, status);
// style.border = Border::default().rounded(space_s);
// style
// })
.into()
} else {
text(self.lyric.clone())
.apply(container)
.center_y(Length::Fill)
.width(Length::Fill)
.style(move |t| {
container::Style::default().border(
Border::default()
.rounded(space_s)
.width(space_xxs)
.color(t.cosmic().bg_component_divider()),
)
})
.padding(space_s)
.into()
};
let drag_handle = icon::from_name("object-rows")
.prefer_svg(true)
.size(space_xxl);
let row = row![drag_handle, editor]
let row = row![drag_handle, lyric]
.spacing(space_s)
.align_y(Vertical::Center);
container(column![verse_title, row].spacing(space_s))