fixing some database and adding issues

This commit is contained in:
Chris Cochrun 2025-10-04 13:59:36 -05:00
parent 7f30b4395f
commit 375cea2408
11 changed files with 709 additions and 392 deletions

View file

@ -201,6 +201,27 @@ pub async fn remove_from_db(
.map(|_| ()) .map(|_| ())
} }
pub async fn add_image_to_db(
image: Image,
db: PoolConnection<Sqlite>,
) -> Result<()> {
let path = image
.path
.to_str()
.map(std::string::ToString::to_string)
.unwrap_or_default();
let mut db = db.detach();
query!(
r#"INSERT INTO images (title, file_path) VALUES ($1, $2)"#,
image.title,
path,
)
.execute(&mut db)
.await
.into_diagnostic()?;
Ok(())
}
pub async fn update_image_in_db( pub async fn update_image_in_db(
image: Image, image: Image,
db: PoolConnection<Sqlite>, db: PoolConnection<Sqlite>,
@ -211,44 +232,6 @@ pub async fn update_image_in_db(
.map(std::string::ToString::to_string) .map(std::string::ToString::to_string)
.unwrap_or_default(); .unwrap_or_default();
let mut db = db.detach(); let mut db = db.detach();
let id = image.id;
if let Err(e) = query!("SELECT id FROM images where id = $1", id)
.fetch_one(&mut db)
.await
{
if let Ok(ids) =
query!("SELECT id FROM images").fetch_all(&mut db).await
{
let Some(mut max) = ids.iter().map(|r| r.id).max() else {
return Err(miette::miette!("cannot find max id"));
};
debug!(?e, "Image not found");
max += 1;
let result = query!(
r#"INSERT into images VALUES($1, $2, $3)"#,
max,
image.title,
path,
)
.execute(&mut db)
.await
.into_diagnostic();
return match result {
Ok(_) => {
debug!("should have been updated");
Ok(())
}
Err(e) => {
error! {?e};
Err(e)
}
};
} else {
return Err(miette::miette!("cannot find ids"));
}
};
debug!(?image, "should be been updated"); debug!(?image, "should be been updated");
let result = query!( let result = query!(
r#"UPDATE images SET title = $2, file_path = $3 WHERE id = $1"#, r#"UPDATE images SET title = $2, file_path = $3 WHERE id = $1"#,

View file

@ -310,7 +310,6 @@ pub async fn remove_from_db(
pub async fn add_presentation_to_db( pub async fn add_presentation_to_db(
presentation: Presentation, presentation: Presentation,
db: PoolConnection<Sqlite>, db: PoolConnection<Sqlite>,
id: i32,
) -> Result<()> { ) -> Result<()> {
let path = presentation let path = presentation
.path .path
@ -319,9 +318,8 @@ pub async fn add_presentation_to_db(
.unwrap_or_default(); .unwrap_or_default();
let html = presentation.kind == PresKind::Html; let html = presentation.kind == PresKind::Html;
let mut db = db.detach(); let mut db = db.detach();
let result = query!( query!(
r#"INSERT into presentations VALUES($1, $2, $3, $4)"#, r#"INSERT INTO presentations (title, file_path, html) VALUES ($1, $2, $3)"#,
id,
presentation.title, presentation.title,
path, path,
html, html,
@ -329,13 +327,6 @@ pub async fn add_presentation_to_db(
.execute(&mut db) .execute(&mut db)
.await .await
.into_diagnostic()?; .into_diagnostic()?;
if result.last_insert_rowid() != id as i64 {
let rowid = result.last_insert_rowid();
error!(
rowid,
id, "It appears that rowid and id aren't the same"
);
}
Ok(()) Ok(())
} }

View file

@ -420,6 +420,51 @@ pub async fn remove_from_db(
.map(|_| ()) .map(|_| ())
} }
pub async fn add_song_to_db(
song: Song,
db: PoolConnection<Sqlite>,
) -> Result<()> {
let mut db = db.detach();
let verse_order = {
if let Some(vo) = song.verse_order {
vo.into_iter()
.map(|mut s| {
s.push(' ');
s
})
.collect::<String>()
} else {
String::new()
}
};
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());
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)
.await
.into_diagnostic()?;
Ok(())
}
pub async fn update_song_in_db( pub async fn update_song_in_db(
item: Song, item: Song,
db: PoolConnection<Sqlite>, db: PoolConnection<Sqlite>,

View file

@ -236,6 +236,30 @@ pub async fn remove_from_db(
.map(|_| ()) .map(|_| ())
} }
pub async fn add_video_to_db(
video: Video,
db: PoolConnection<Sqlite>,
) -> Result<()> {
let path = video
.path
.to_str()
.map(std::string::ToString::to_string)
.unwrap_or_default();
let mut db = db.detach();
query!(
r#"INSERT INTO videos (title, file_path, start_time, end_time, loop) VALUES ($1, $2, $3, $4, $5)"#,
video.title,
path,
video.start_time,
video.end_time,
video.looping
)
.execute(&mut db)
.await
.into_diagnostic()?;
Ok(())
}
pub async fn update_video_in_db( pub async fn update_video_in_db(
video: Video, video: Video,
db: PoolConnection<Sqlite>, db: PoolConnection<Sqlite>,
@ -246,47 +270,6 @@ pub async fn update_video_in_db(
.map(std::string::ToString::to_string) .map(std::string::ToString::to_string)
.unwrap_or_default(); .unwrap_or_default();
let mut db = db.detach(); let mut db = db.detach();
let id = video.id;
if let Err(e) = query!("SELECT id FROM videos where id = $1", id)
.fetch_one(&mut db)
.await
{
if let Ok(ids) =
query!("SELECT id FROM videos").fetch_all(&mut db).await
{
let Some(mut max) = ids.iter().map(|r| r.id).max() else {
return Err(miette::miette!("cannot find max id"));
};
debug!(?e, "Video not found");
max += 1;
let result = query!(
r#"INSERT into videos VALUES($1, $2, $3, $4, $5, $6)"#,
max,
video.title,
path,
video.start_time,
video.end_time,
video.looping,
)
.execute(&mut db)
.await
.into_diagnostic();
return match result {
Ok(_) => {
debug!("should have been updated");
Ok(())
}
Err(e) => {
error! {?e};
Err(e)
}
};
} else {
return Err(miette::miette!("cannot find ids"));
}
};
debug!(?video, "should be been updated"); debug!(?video, "should be been updated");
let result = query!( let result = query!(
r#"UPDATE videos SET title = $2, file_path = $3, start_time = $4, end_time = $5, loop = $6 WHERE id = $1"#, r#"UPDATE videos SET title = $2, file_path = $3, start_time = $4, end_time = $5, loop = $6 WHERE id = $1"#,

View file

@ -5,6 +5,7 @@ use core::slide::{
}; };
use cosmic::app::context_drawer::ContextDrawer; use cosmic::app::context_drawer::ContextDrawer;
use cosmic::app::{Core, Settings, Task}; use cosmic::app::{Core, Settings, Task};
use cosmic::dialog::file_chooser::{self, save};
use cosmic::iced::alignment::Vertical; use cosmic::iced::alignment::Vertical;
use cosmic::iced::keyboard::{Key, Modifiers}; use cosmic::iced::keyboard::{Key, Modifiers};
use cosmic::iced::window::{Mode, Position}; use cosmic::iced::window::{Mode, Position};
@ -14,15 +15,14 @@ use cosmic::iced::{
}; };
use cosmic::iced_core::text::Wrapping; use cosmic::iced_core::text::Wrapping;
use cosmic::iced_futures::Subscription; use cosmic::iced_futures::Subscription;
use cosmic::iced_widget::{column, row, stack}; use cosmic::iced_widget::{column, horizontal_rule, row, stack};
use cosmic::theme;
use cosmic::widget::button::Catalog; use cosmic::widget::button::Catalog;
use cosmic::widget::dnd_destination::dnd_destination; use cosmic::widget::dnd_destination::dnd_destination;
use cosmic::widget::menu::key_bind::Modifier; use cosmic::widget::menu::key_bind::Modifier;
use cosmic::widget::menu::{ItemWidth, KeyBind}; use cosmic::widget::menu::{ItemWidth, KeyBind};
use cosmic::widget::nav_bar::nav_bar_style; use cosmic::widget::nav_bar::nav_bar_style;
use cosmic::widget::tooltip::Position as TPosition; use cosmic::widget::tooltip::Position as TPosition;
use cosmic::widget::{Container, menu}; use cosmic::widget::{Container, divider, menu};
use cosmic::widget::{ use cosmic::widget::{
Space, button, context_menu, horizontal_space, mouse_area, Space, button, context_menu, horizontal_space, mouse_area,
nav_bar, nav_bar_toggle, responsive, scrollable, search_input, nav_bar, nav_bar_toggle, responsive, scrollable, search_input,
@ -30,10 +30,13 @@ use cosmic::widget::{
}; };
use cosmic::widget::{container, text}; use cosmic::widget::{container, text};
use cosmic::widget::{icon, slider}; use cosmic::widget::{icon, slider};
use cosmic::{Application, ApplicationExt, Element, executor}; use cosmic::{
Application, ApplicationExt, Element, dialog, executor,
};
use cosmic::{theme, widget};
use crisp::types::Value; use crisp::types::Value;
use lisp::parse_lisp; use lisp::parse_lisp;
use miette::{Result, miette}; use miette::{IntoDiagnostic, Result, miette};
use rayon::prelude::*; use rayon::prelude::*;
use resvg::usvg::fontdb; use resvg::usvg::fontdb;
use std::collections::HashMap; use std::collections::HashMap;
@ -49,6 +52,7 @@ use ui::presenter::{self, Presenter};
use ui::song_editor::{self, SongEditor}; use ui::song_editor::{self, SongEditor};
use crate::core::content::Content; use crate::core::content::Content;
use crate::core::file;
use crate::core::kinds::ServiceItemKind; use crate::core::kinds::ServiceItemKind;
use crate::core::model::KindWrapper; use crate::core::model::KindWrapper;
use crate::ui::image_editor::{self, ImageEditor}; use crate::ui::image_editor::{self, ImageEditor};
@ -123,6 +127,7 @@ struct App {
selected_items: Vec<usize>, selected_items: Vec<usize>,
current_item: (usize, usize), current_item: (usize, usize),
hovered_item: Option<usize>, hovered_item: Option<usize>,
hovered_dnd: Option<usize>,
presentation_open: bool, presentation_open: bool,
cli_mode: bool, cli_mode: bool,
library: Option<Library>, library: Option<Library>,
@ -166,6 +171,7 @@ enum Message {
SelectServiceItem(usize), SelectServiceItem(usize),
AddSelectServiceItem(usize), AddSelectServiceItem(usize),
HoveredServiceItem(Option<usize>), HoveredServiceItem(Option<usize>),
HoveredServiceDrop(Option<usize>),
AddServiceItem(usize, KindWrapper), AddServiceItem(usize, KindWrapper),
AddServiceItemsFiles(usize, Vec<ServiceItem>), AddServiceItemsFiles(usize, Vec<ServiceItem>),
RemoveServiceItem(usize), RemoveServiceItem(usize),
@ -183,8 +189,9 @@ enum Message {
New, New,
Open, Open,
OpenFile(PathBuf), OpenFile(PathBuf),
Save(Option<PathBuf>), Save,
SaveAs, SaveAsDialog,
SaveAs(PathBuf),
OpenSettings, OpenSettings,
ModifiersPressed(Modifiers), ModifiersPressed(Modifiers),
} }
@ -205,8 +212,8 @@ impl menu::Action for MenuAction {
fn message(&self) -> Self::Message { fn message(&self) -> Self::Message {
match self { match self {
MenuAction::New => Message::New, MenuAction::New => Message::New,
MenuAction::Save => Message::Save(None), MenuAction::Save => Message::Save,
MenuAction::SaveAs => Message::SaveAs, MenuAction::SaveAs => Message::SaveAsDialog,
MenuAction::Open => Message::Open, MenuAction::Open => Message::Open,
MenuAction::OpenSettings => Message::OpenSettings, MenuAction::OpenSettings => Message::OpenSettings,
MenuAction::DeleteItem(index) => { MenuAction::DeleteItem(index) => {
@ -352,6 +359,7 @@ impl cosmic::Application for App {
fontdb: Arc::clone(&fontdb), fontdb: Arc::clone(&fontdb),
menu_keys, menu_keys,
hovered_item: None, hovered_item: None,
hovered_dnd: None,
context_menu: None, context_menu: None,
modifiers_pressed: None, modifiers_pressed: None,
}; };
@ -620,8 +628,8 @@ impl cosmic::Application for App {
} }
iced::Event::Touch(_touch) => None, iced::Event::Touch(_touch) => None,
iced::Event::A11y(_id, _action_request) => None, iced::Event::A11y(_id, _action_request) => None,
iced::Event::Dnd(dnd_event) => { iced::Event::Dnd(_dnd_event) => {
debug!(?dnd_event); // debug!(?dnd_event);
None None
} }
iced::Event::PlatformSpecific(_platform_specific) => { iced::Event::PlatformSpecific(_platform_specific) => {
@ -1139,6 +1147,10 @@ impl cosmic::Application for App {
self.hovered_item = index; self.hovered_item = index;
Task::none() Task::none()
} }
Message::HoveredServiceDrop(index) => {
self.hovered_dnd = index;
Task::none()
}
Message::SelectServiceItem(index) => { Message::SelectServiceItem(index) => {
self.selected_items = vec![index]; self.selected_items = vec![index];
Task::none() Task::none()
@ -1234,6 +1246,7 @@ impl cosmic::Application for App {
Task::none() Task::none()
} }
Message::AddServiceItemsFiles(index, items) => { Message::AddServiceItemsFiles(index, items) => {
self.hovered_dnd = None;
for item in items { for item in items {
self.service.insert(index, item); self.service.insert(index, item);
} }
@ -1345,17 +1358,44 @@ impl cosmic::Application for App {
debug!(?file, "opening file"); debug!(?file, "opening file");
Task::none() Task::none()
} }
Message::Save(file) => { Message::Save => {
let Some(file) = file else { let service = self.service.clone();
debug!("saving current"); let file = self.file.clone();
return Task::none(); Task::perform(
}; file::save(service, file.clone()),
debug!(?file, "saving new file"); move |res| match res {
Task::none() Ok(_) => {
tracing::info!(
"saving file to: {:?}",
file
);
cosmic::Action::None
} }
Message::SaveAs => { Err(e) => {
debug!("saving as a file"); error!(?e, "There was a problem saving");
Task::none() cosmic::Action::None
}
},
)
}
Message::SaveAs(file) => {
debug!(?file, "saving as a file");
self.file = file;
return self.update(Message::Save);
}
Message::SaveAsDialog => {
Task::perform(save_as_dialog(), |file| match file {
Ok(file) => {
cosmic::Action::App(Message::SaveAs(file))
}
Err(e) => {
error!(
?e,
"There was an error during saving"
);
cosmic::Action::None
}
})
} }
Message::OpenSettings => { Message::OpenSettings => {
debug!("Opening settings"); debug!("Opening settings");
@ -1635,7 +1675,7 @@ where
} }
match (key, modifiers) { match (key, modifiers) {
(Key::Character(k), Modifiers::CTRL) if k == *"s" => { (Key::Character(k), Modifiers::CTRL) if k == *"s" => {
self.update(Message::Save(None)) self.update(Message::Save)
} }
(Key::Character(k), Modifiers::CTRL) if k == *"o" => { (Key::Character(k), Modifiers::CTRL) if k == *"o" => {
self.update(Message::Open) self.update(Message::Open)
@ -1743,7 +1783,20 @@ where
)) ))
}) })
.width(Length::Fill); .width(Length::Fill);
let mouse_area = mouse_area(container) let visual_item = if self.hovered_dnd.is_some_and(|h| h == index) {
let divider = divider::horizontal::default().class(theme::Rule::custom(|t| {
let color = t.cosmic().accent_color();
let style = cosmic::iced_widget::rule::Style {
color: color.into(),
width: 2,
radius: t.cosmic().corner_radii.radius_xs.into(),
fill_mode: cosmic::iced_widget::rule::FillMode::Full,
};
style
} ));
Container::new(column![divider, container].spacing(theme::spacing().space_s))
} else { container };
let mouse_area = mouse_area(visual_item)
.on_enter(Message::HoveredServiceItem(Some( .on_enter(Message::HoveredServiceItem(Some(
index, index,
))) )))
@ -1797,6 +1850,8 @@ where
tooltip, tooltip,
vec!["application/service-item".into(), "text/uri-list".into(), "x-special/gnome-copied-files".into()], vec!["application/service-item".into(), "text/uri-list".into(), "x-special/gnome-copied-files".into()],
) )
.on_enter(move |_, _, _| Message::HoveredServiceDrop(Some(index)))
.on_leave(move || Message::HoveredServiceDrop(None))
.on_finish(move |mime, data, _, _, _| { .on_finish(move |mime, data, _, _, _| {
match mime.as_str() { match mime.as_str() {
@ -1905,3 +1960,15 @@ where
container.center(Length::FillPortion(2)).into() container.center(Length::FillPortion(2)).into()
} }
} }
async fn save_as_dialog() -> Result<PathBuf> {
let dialog = save::Dialog::new();
save::file(dialog).await.into_diagnostic().map(|response| {
match response.url() {
Some(url) => Ok(url.to_file_path().unwrap()),
None => {
Err(miette!("Can't convert url of file to a path"))
}
}
})?
}

View file

@ -48,9 +48,7 @@ impl ImageEditor {
pub fn update(&mut self, message: Message) -> Action { pub fn update(&mut self, message: Message) -> Action {
match message { match message {
Message::ChangeImage(image) => { Message::ChangeImage(image) => {
self.image = Some(image.clone()); self.update_entire_image(&image);
self.title = image.title.clone();
return self.update(Message::Update(image));
} }
Message::ChangeTitle(title) => { Message::ChangeTitle(title) => {
self.title = title.clone(); self.title = title.clone();
@ -66,6 +64,7 @@ impl ImageEditor {
} }
Message::Update(image) => { Message::Update(image) => {
warn!(?image); warn!(?image);
self.update_entire_image(&image);
return Action::UpdateImage(image); return Action::UpdateImage(image);
} }
Message::PickImage => { Message::PickImage => {
@ -80,7 +79,7 @@ impl ImageEditor {
if let Ok(image) = image_result { if let Ok(image) = image_result {
let mut image = Image::from(image); let mut image = Image::from(image);
image.id = image_id; image.id = image_id;
Message::ChangeImage(image) Message::Update(image)
} else { } else {
Message::None Message::None
} }
@ -134,6 +133,11 @@ impl ImageEditor {
pub const fn editing(&self) -> bool { pub const fn editing(&self) -> bool {
self.editing self.editing
} }
fn update_entire_image(&mut self, image: &Image) {
self.image = Some(image.clone());
self.title = image.title.clone();
}
} }
impl Default for ImageEditor { impl Default for ImageEditor {

View file

@ -24,7 +24,7 @@ use tracing::{debug, error, warn};
use crate::core::{ use crate::core::{
content::Content, content::Content,
images::{self, Image, update_image_in_db}, images::{self, Image, add_image_to_db, update_image_in_db},
kinds::ServiceItemKind, kinds::ServiceItemKind,
model::{KindWrapper, LibraryKind, Model}, model::{KindWrapper, LibraryKind, Model},
presentations::{ presentations::{
@ -32,8 +32,8 @@ use crate::core::{
update_presentation_in_db, update_presentation_in_db,
}, },
service_items::ServiceItem, service_items::ServiceItem,
songs::{self, Song, update_song_in_db}, songs::{self, Song, add_song_to_db, update_song_in_db},
videos::{self, Video, update_video_in_db}, videos::{self, Video, add_video_to_db, update_video_in_db},
}; };
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -56,7 +56,7 @@ pub struct Library {
#[derive(Debug, Clone, Eq, PartialEq, Copy)] #[derive(Debug, Clone, Eq, PartialEq, Copy)]
enum MenuMessage { enum MenuMessage {
Delete, Delete,
Open((LibraryKind, i32)), Open,
} }
impl MenuAction for MenuMessage { impl MenuAction for MenuMessage {
@ -65,9 +65,7 @@ impl MenuAction for MenuMessage {
fn message(&self) -> Self::Message { fn message(&self) -> Self::Message {
match self { match self {
MenuMessage::Delete => Message::DeleteItem, MenuMessage::Delete => Message::DeleteItem,
MenuMessage::Open((kind, index)) => { MenuMessage::Open => Message::OpenContextItem,
Message::OpenItem(Some((*kind, *index)))
}
} }
} }
} }
@ -84,6 +82,7 @@ pub enum Message {
AddItem, AddItem,
DeleteItem, DeleteItem,
OpenItem(Option<(LibraryKind, i32)>), OpenItem(Option<(LibraryKind, i32)>),
OpenContextItem,
HoverLibrary(Option<LibraryKind>), HoverLibrary(Option<LibraryKind>),
OpenLibrary(Option<LibraryKind>), OpenLibrary(Option<LibraryKind>),
HoverItem(Option<(LibraryKind, i32)>), HoverItem(Option<(LibraryKind, i32)>),
@ -180,27 +179,61 @@ impl<'a> Library {
Message::AddVideos(videos) => { Message::AddVideos(videos) => {
debug!(?videos); debug!(?videos);
let mut index = self.video_library.items.len(); let mut index = self.video_library.items.len();
// Check if empty
let mut tasks = Vec::new();
if let Some(videos) = videos { if let Some(videos) = videos {
let len = videos.len();
for video in videos { for video in videos {
if let Err(e) = if let Err(e) =
self.video_library.add_item(video) self.video_library.add_item(video.clone())
{ {
error!(?e); error!(?e);
} }
let task = Task::future(self.db.acquire())
.and_then(move |db| {
Task::perform(
add_video_to_db(
video.to_owned(),
db,
),
move |res| {
debug!(
len,
index, "added to db"
);
if let Err(e) = res {
error!(?e);
}
if len == index {
debug!("open the pres");
Message::OpenItem(Some((
LibraryKind::Video,
index as i32,
)))
} else {
Message::None
}
},
)
});
tasks.push(task);
index += 1; index += 1;
} }
} }
return self.update(Message::OpenItem(Some(( let after_task =
Task::done(Message::OpenItem(Some((
LibraryKind::Video, LibraryKind::Video,
self.video_library.items.len() as i32 - 1, self.video_library.items.len() as i32 - 1,
)))); ))));
return Action::Task(
Task::batch(tasks).chain(after_task),
);
} }
Message::AddPresentations(presentations) => { Message::AddPresentations(presentations) => {
debug!(?presentations); debug!(?presentations);
let mut index = self.presentation_library.items.len(); let mut index = self.presentation_library.items.len();
// Check if empty // Check if empty
let mut tasks = Vec::new(); let mut tasks = Vec::new();
if index == 0 {
if let Some(presentations) = presentations { if let Some(presentations) = presentations {
let len = presentations.len(); let len = presentations.len();
for presentation in presentations { for presentation in presentations {
@ -218,7 +251,6 @@ impl<'a> Library {
add_presentation_to_db( add_presentation_to_db(
presentation.to_owned(), presentation.to_owned(),
db, db,
index as i32,
), ),
move |res| { move |res| {
debug!( debug!(
@ -244,53 +276,89 @@ impl<'a> Library {
index += 1; index += 1;
} }
} }
return Action::Task(Task::batch(tasks)); let after_task =
} else { Task::done(Message::OpenItem(Some((
if let Some(presentations) = presentations {
for presentation in presentations {
if let Err(e) = self
.presentation_library
.add_item(presentation)
{
error!(?e);
}
index += 1;
}
}
return self.update(Message::OpenItem(Some((
LibraryKind::Presentation, LibraryKind::Presentation,
self.presentation_library.items.len() as i32 self.presentation_library.items.len() as i32
- 1, - 1,
)))); ))));
} return Action::Task(
Task::batch(tasks).chain(after_task),
);
} }
Message::AddImages(images) => { Message::AddImages(images) => {
debug!(?images); debug!(?images);
let mut index = self.image_library.items.len(); let mut index = self.image_library.items.len();
// Check if empty
let mut tasks = Vec::new();
if let Some(images) = images { if let Some(images) = images {
let len = images.len();
for image in images { for image in images {
if let Err(e) = if let Err(e) =
self.image_library.add_item(image) self.image_library.add_item(image.clone())
{ {
error!(?e); error!(?e);
} }
let task = Task::future(self.db.acquire())
.and_then(move |db| {
Task::perform(
add_image_to_db(
image.to_owned(),
db,
),
move |res| {
debug!(
len,
index, "added to db"
);
if let Err(e) = res {
error!(?e);
}
if len == index {
debug!("open the pres");
Message::OpenItem(Some((
LibraryKind::Image,
index as i32,
)))
} else {
Message::None
}
},
)
});
tasks.push(task);
index += 1; index += 1;
} }
} }
return self.update(Message::OpenItem(Some(( let after_task =
Task::done(Message::OpenItem(Some((
LibraryKind::Image, LibraryKind::Image,
self.image_library.items.len() as i32 - 1, self.image_library.items.len() as i32 - 1,
)))); ))));
return Action::Task(
Task::batch(tasks).chain(after_task),
);
} }
Message::OpenItem(item) => { Message::OpenItem(item) => {
debug!(?item); debug!(?item);
self.editing_item = item; self.editing_item = item;
return Action::OpenItem(item); return Action::OpenItem(item);
} }
Message::OpenContextItem => {
let Some(kind) = self.library_open else {
return Action::None;
};
let Some(index) = self.context_menu else {
return Action::None;
};
return self
.update(Message::OpenItem(Some((kind, index))));
}
Message::HoverLibrary(library_kind) => { Message::HoverLibrary(library_kind) => {
self.library_hovered = library_kind; self.library_hovered = library_kind;
} }
Message::OpenLibrary(library_kind) => { Message::OpenLibrary(library_kind) => {
self.selected_items = None;
self.library_open = library_kind; self.library_open = library_kind;
} }
Message::HoverItem(item) => { Message::HoverItem(item) => {
@ -549,6 +617,7 @@ impl<'a> Library {
let Some(kind) = self.library_open else { let Some(kind) = self.library_open else {
return Action::None; return Action::None;
}; };
debug!(index, "should context");
let Some(items) = self.selected_items.as_mut() else { let Some(items) = self.selected_items.as_mut() else {
self.selected_items = vec![(kind, index)].into(); self.selected_items = vec![(kind, index)].into();
self.context_menu = Some(index); self.context_menu = Some(index);
@ -556,21 +625,59 @@ impl<'a> Library {
}; };
if items.contains(&(kind, index)) { if items.contains(&(kind, index)) {
} else { debug!(index, "should context contained");
items.push((kind, index));
}
self.selected_items = Some(items.to_vec()); self.selected_items = Some(items.to_vec());
} else {
debug!(index, "should context not contained");
self.selected_items = vec![(kind, index)].into();
}
self.context_menu = Some(index); self.context_menu = Some(index);
} }
Message::AddFiles(items) => { Message::AddFiles(items) => {
let mut tasks = Vec::new();
let last_item = &items.last();
let after_task = match last_item {
Some(ServiceItemKind::Image(image)) => {
Task::done(Message::OpenItem(Some((
LibraryKind::Image,
self.image_library.items.len() as i32 - 1,
))))
}
_ => Task::none(),
};
for item in items { for item in items {
match item { match item {
ServiceItemKind::Song(song) => { ServiceItemKind::Song(song) => {
let Some(e) = self let Some(e) = self
.song_library .song_library
.add_item(song) .add_item(song.clone())
.err() .err()
else { else {
let task = Task::future(
self.db.acquire(),
)
.and_then(move |db| {
Task::perform(
add_song_to_db(
song.clone(),
db,
),
{
let song = song.clone();
move |res| {
debug!(
?song,
"added to db"
);
if let Err(e) = res {
error!(?e);
}
Message::None
}
},
)
});
tasks.push(task);
continue; continue;
}; };
error!(?e); error!(?e);
@ -578,9 +685,34 @@ impl<'a> Library {
ServiceItemKind::Video(video) => { ServiceItemKind::Video(video) => {
let Some(e) = self let Some(e) = self
.video_library .video_library
.add_item(video) .add_item(video.clone())
.err() .err()
else { else {
let task = Task::future(
self.db.acquire(),
)
.and_then(move |db| {
Task::perform(
add_video_to_db(
video.clone(),
db,
),
{
let video = video.clone();
move |res| {
debug!(
?video,
"added to db"
);
if let Err(e) = res {
error!(?e);
}
Message::None
}
},
)
});
tasks.push(task);
continue; continue;
}; };
error!(?e); error!(?e);
@ -588,9 +720,34 @@ impl<'a> Library {
ServiceItemKind::Image(image) => { ServiceItemKind::Image(image) => {
let Some(e) = self let Some(e) = self
.image_library .image_library
.add_item(image) .add_item(image.clone())
.err() .err()
else { else {
let task = Task::future(
self.db.acquire(),
)
.and_then(move |db| {
Task::perform(
add_image_to_db(
image.clone(),
db,
),
{
let image = image.clone();
move |res| {
debug!(
?image,
"added to db"
);
if let Err(e) = res {
error!(?e);
}
Message::None
}
},
)
});
tasks.push(task);
continue; continue;
}; };
error!(?e); error!(?e);
@ -600,9 +757,35 @@ impl<'a> Library {
) => { ) => {
let Some(e) = self let Some(e) = self
.presentation_library .presentation_library
.add_item(presentation) .add_item(presentation.clone())
.err() .err()
else { else {
let task =
Task::future(self.db.acquire())
.and_then(move |db| {
Task::perform(
add_presentation_to_db(
presentation.clone(),
db,
),
{
let presentation =
presentation.clone();
move |res| {
debug!(
?presentation,
"added to db"
);
if let Err(e) = res {
error!(?e);
}
Message::None
}
},
)
});
tasks.push(task);
continue; continue;
}; };
error!(?e); error!(?e);
@ -610,6 +793,9 @@ impl<'a> Library {
ServiceItemKind::Content(slide) => todo!(), ServiceItemKind::Content(slide) => todo!(),
} }
} }
return Action::Task(
Task::batch(tasks).chain(after_task),
);
} }
} }
Action::None Action::None
@ -814,28 +1000,8 @@ impl<'a> Library {
)), )),
)); ));
if let Some(context_id) = self.context_menu { Element::from(mouse_area)
if index == context_id as usize {
let menu_items = vec![
menu::Item::Button("Open", None, MenuMessage::Open((model.kind, index as i32))),
menu::Item::Button("Delete", None, MenuMessage::Delete)
];
let context_menu = context_menu(
mouse_area,
self.context_menu.map_or_else(|| None, |_| {
Some(menu::items(&self.menu_keys,
menu_items))
}) })
);
Element::from(context_menu)
} else {
Element::from(mouse_area)
}
} else {
Element::from(mouse_area)
}
}
)
.action(DndAction::Copy) .action(DndAction::Copy)
.drag_icon({ .drag_icon({
let model = model.kind; let model = model.kind;
@ -874,8 +1040,9 @@ impl<'a> Library {
button::icon(icon::from_name("add")) button::icon(icon::from_name("add"))
.on_press(Message::AddItem) .on_press(Message::AddItem)
); );
let context_menu = self.context_menu(items.into());
let library_column = let library_column =
column![library_toolbar, items].spacing(3); column![library_toolbar, context_menu].spacing(3);
Container::new(library_column).padding(5) Container::new(library_column).padding(5)
} else { } else {
Container::new(Space::new(0, 0)) Container::new(Space::new(0, 0))
@ -993,6 +1160,34 @@ impl<'a> Library {
.into() .into()
} }
fn context_menu<'b>(
&self,
items: Element<'b, Message>,
) -> Element<'b, Message> {
if self.context_menu.is_some() {
let menu_items = vec![
menu::Item::Button("Open", None, MenuMessage::Open),
menu::Item::Button(
"Delete",
None,
MenuMessage::Delete,
),
];
let context_menu = context_menu(
items,
self.context_menu.map_or_else(
|| None,
|_| {
Some(menu::items(&self.menu_keys, menu_items))
},
),
);
Element::from(context_menu)
} else {
items
}
}
#[allow(clippy::unused_async)] #[allow(clippy::unused_async)]
pub async fn search_items( pub async fn search_items(
&self, &self,
@ -1079,8 +1274,7 @@ impl<'a> Library {
return Action::None; return Action::None;
}; };
items.sort_by(|(_, index), (_, other)| index.cmp(other)); items.sort_by(|(_, index), (_, other)| index.cmp(other));
let tasks: Vec<Task<Message>> = let tasks: Vec<Task<Message>> = items
items
.iter() .iter()
.rev() .rev()
.map(|(kind, index)| match kind { .map(|(kind, index)| match kind {
@ -1095,8 +1289,8 @@ impl<'a> Library {
error!(?e); error!(?e);
Task::none() Task::none()
} else { } else {
Task::future(self.db.acquire()) Task::future(self.db.acquire()).and_then(
.and_then(move |db| { move |db| {
Task::perform( Task::perform(
songs::remove_from_db( songs::remove_from_db(
db, song.id, db, song.id,
@ -1108,7 +1302,8 @@ impl<'a> Library {
Message::None Message::None
}, },
) )
}) },
)
} }
} else { } else {
Task::none() Task::none()
@ -1125,8 +1320,8 @@ impl<'a> Library {
error!(?e); error!(?e);
Task::none() Task::none()
} else { } else {
Task::future(self.db.acquire()) Task::future(self.db.acquire()).and_then(
.and_then(move |db| { move |db| {
Task::perform( Task::perform(
videos::remove_from_db( videos::remove_from_db(
db, video.id, db, video.id,
@ -1138,7 +1333,8 @@ impl<'a> Library {
Message::None Message::None
}, },
) )
}) },
)
} }
} else { } else {
Task::none() Task::none()
@ -1155,8 +1351,10 @@ impl<'a> Library {
error!(?e); error!(?e);
Task::none() Task::none()
} else { } else {
Task::future(self.db.acquire()) debug!("let's remove {0}", image.id);
.and_then(move |db| { debug!("let's remove {0}", image.title);
Task::future(self.db.acquire()).and_then(
move |db| {
Task::perform( Task::perform(
images::remove_from_db( images::remove_from_db(
db, image.id, db, image.id,
@ -1168,7 +1366,8 @@ impl<'a> Library {
Message::None Message::None
}, },
) )
}) },
)
} }
} else { } else {
Task::none() Task::none()
@ -1186,8 +1385,8 @@ impl<'a> Library {
error!(?e); error!(?e);
Task::none() Task::none()
} else { } else {
Task::future(self.db.acquire()) Task::future(self.db.acquire()).and_then(
.and_then(move |db| { move |db| {
Task::perform( Task::perform(
presentations::remove_from_db( presentations::remove_from_db(
db, db,
@ -1200,7 +1399,8 @@ impl<'a> Library {
Message::None Message::None
}, },
) )
}) },
)
} }
} else { } else {
Task::none() Task::none()

View file

@ -43,6 +43,7 @@ pub enum Message {
NextPage, NextPage,
PrevPage, PrevPage,
None, None,
ChangePresentationFile(Presentation),
} }
impl PresentationEditor { impl PresentationEditor {
@ -60,43 +61,7 @@ impl PresentationEditor {
pub fn update(&mut self, message: Message) -> Action { pub fn update(&mut self, message: Message) -> Action {
match message { match message {
Message::ChangePresentation(presentation) => { Message::ChangePresentation(presentation) => {
self.presentation = Some(presentation.clone()); self.update_entire_presentation(&presentation);
self.title = presentation.title.clone();
self.document =
Document::open(&presentation.path.as_path()).ok();
self.page_count = self
.document
.as_ref()
.and_then(|doc| doc.page_count().ok());
warn!("changing presentation");
self.current_slide =
self.document.as_ref().and_then(|doc| {
let page = doc.load_page(0).ok()?;
let matrix = Matrix::IDENTITY;
let colorspace = Colorspace::device_rgb();
let Ok(pixmap) = page
.to_pixmap(
&matrix,
&colorspace,
true,
true,
)
.into_diagnostic()
else {
error!(
"Can't turn this page into pixmap"
);
return None;
};
debug!(?pixmap);
Some(Handle::from_rgba(
pixmap.width(),
pixmap.height(),
pixmap.samples().to_vec(),
))
});
self.current_slide_index = Some(0);
return self.update(Message::Update(presentation));
} }
Message::ChangeTitle(title) => { Message::ChangeTitle(title) => {
self.title = title.clone(); self.title = title.clone();
@ -112,7 +77,7 @@ impl PresentationEditor {
self.editing = edit; self.editing = edit;
} }
Message::Update(presentation) => { Message::Update(presentation) => {
warn!(?presentation); warn!(?presentation, "about to update");
return Action::UpdatePresentation(presentation); return Action::UpdatePresentation(presentation);
} }
Message::PickPresentation => { Message::PickPresentation => {
@ -129,7 +94,9 @@ impl PresentationEditor {
let mut presentation = let mut presentation =
Presentation::from(presentation); Presentation::from(presentation);
presentation.id = presentation_id; presentation.id = presentation_id;
Message::ChangePresentation(presentation) Message::ChangePresentationFile(
presentation,
)
} else { } else {
Message::None Message::None
} }
@ -137,6 +104,10 @@ impl PresentationEditor {
); );
return Action::Task(task); return Action::Task(task);
} }
Message::ChangePresentationFile(presentation) => {
self.update_entire_presentation(&presentation);
return self.update(Message::Update(presentation));
}
Message::None => (), Message::None => (),
Message::NextPage => { Message::NextPage => {
let next_index = let next_index =
@ -261,6 +232,40 @@ impl PresentationEditor {
pub const fn editing(&self) -> bool { pub const fn editing(&self) -> bool {
self.editing self.editing
} }
fn update_entire_presentation(
&mut self,
presentation: &Presentation,
) {
self.presentation = Some(presentation.clone());
self.title = presentation.title.clone();
self.document =
Document::open(&presentation.path.as_path()).ok();
self.page_count = self
.document
.as_ref()
.and_then(|doc| doc.page_count().ok());
warn!("changing presentation");
self.current_slide = self.document.as_ref().and_then(|doc| {
let page = doc.load_page(0).ok()?;
let matrix = Matrix::IDENTITY;
let colorspace = Colorspace::device_rgb();
let Ok(pixmap) = page
.to_pixmap(&matrix, &colorspace, true, true)
.into_diagnostic()
else {
error!("Can't turn this page into pixmap");
return None;
};
debug!(?pixmap);
Some(Handle::from_rgba(
pixmap.width(),
pixmap.height(),
pixmap.samples().to_vec(),
))
});
self.current_slide_index = Some(0);
}
} }
impl Default for PresentationEditor { impl Default for PresentationEditor {

View file

@ -62,6 +62,7 @@ pub enum Message {
ChangeVerseOrder(String), ChangeVerseOrder(String),
ChangeLyrics(text_editor::Action), ChangeLyrics(text_editor::Action),
ChangeBackground(Result<PathBuf, SongError>), ChangeBackground(Result<PathBuf, SongError>),
UpdateSlides(Vec<Slide>),
PickBackground, PickBackground,
Edit(bool), Edit(bool),
None, None,
@ -134,17 +135,7 @@ impl SongEditor {
match message { match message {
Message::ChangeSong(song) => { Message::ChangeSong(song) => {
self.song = Some(song.clone()); self.song = Some(song.clone());
self.song_slides = song.to_slides().ok().map(|v| { let song_slides = song.clone().to_slides();
v.into_par_iter()
.map(|mut s| {
text_svg::text_svg_generator(
&mut s,
Arc::clone(&self.font_db),
);
s
})
.collect::<Vec<Slide>>()
});
self.title = song.title; self.title = song.title;
if let Some(font) = song.font { if let Some(font) = song.font {
self.font = font; self.font = font;
@ -175,7 +166,28 @@ impl SongEditor {
text_editor::Content::with_text(&lyrics); text_editor::Content::with_text(&lyrics);
} }
self.background_video(&song.background); self.background_video(&song.background);
self.background = song.background; self.background = song.background.clone();
let font_db = Arc::clone(&self.font_db);
let task = Task::perform(
async move {
song_slides
.ok()
.map(move |v| {
v.into_par_iter()
.map(move |mut s| {
text_svg::text_svg_generator(
&mut s,
Arc::clone(&font_db),
);
s
})
.collect::<Vec<Slide>>()
})
.unwrap_or_default()
},
|slides| Message::UpdateSlides(slides),
);
return Action::Task(task);
} }
Message::ChangeFont(font) => { Message::ChangeFont(font) => {
self.font = font.clone(); self.font = font.clone();
@ -258,6 +270,13 @@ impl SongEditor {
video.set_paused(!paused); video.set_paused(!paused);
}; };
} }
Message::UpdateSlides(slides) => {
self.song_slides = Some(slides);
}
Message::UpdateSong(song) => {
self.song = Some(song.clone());
return Action::UpdateSong(song);
}
_ => (), _ => (),
} }
Action::None Action::None
@ -431,18 +450,30 @@ order",
fn update_song(&mut self, song: Song) -> Action { fn update_song(&mut self, song: Song) -> Action {
self.song = Some(song.clone()); self.song = Some(song.clone());
self.song_slides = song.to_slides().ok().map(|v| {
let font_db = Arc::clone(&self.font_db);
let update_task =
Task::done(Message::UpdateSong(song.clone()));
let task = Task::perform(
async move {
song.to_slides()
.ok()
.map(move |v| {
v.into_par_iter() v.into_par_iter()
.map(|mut s| { .map(move |mut s| {
text_svg::text_svg_generator( text_svg::text_svg_generator(
&mut s, &mut s,
Arc::clone(&self.font_db), Arc::clone(&font_db),
); );
s s
}) })
.collect::<Vec<Slide>>() .collect::<Vec<Slide>>()
}); })
Action::UpdateSong(song) .unwrap_or_default()
},
|slides| Message::UpdateSlides(slides),
);
Action::Task(task.chain(update_task))
} }
fn background_video(&mut self, background: &Option<Background>) { fn background_video(&mut self, background: &Option<Background>) {

View file

@ -15,29 +15,32 @@ use iced_video_player::{Video, VideoPlayer};
use tracing::{debug, error, warn}; use tracing::{debug, error, warn};
use url::Url; use url::Url;
use crate::core::videos;
#[derive(Debug)] #[derive(Debug)]
pub struct VideoEditor { pub struct VideoEditor {
pub video: Option<Video>, pub video: Option<Video>,
core_video: Option<crate::core::videos::Video>, core_video: Option<videos::Video>,
title: String, title: String,
editing: bool, editing: bool,
} }
pub enum Action { pub enum Action {
Task(Task<Message>), Task(Task<Message>),
UpdateVideo(crate::core::videos::Video), UpdateVideo(videos::Video),
None, None,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Message { pub enum Message {
ChangeVideo(crate::core::videos::Video), ChangeVideo(videos::Video),
Update(crate::core::videos::Video), Update(videos::Video),
ChangeTitle(String), ChangeTitle(String),
PickVideo, PickVideo,
Edit(bool), Edit(bool),
None, None,
PauseVideo, PauseVideo,
UpdateVideoFile(videos::Video),
} }
impl VideoEditor { impl VideoEditor {
@ -52,20 +55,7 @@ impl VideoEditor {
pub fn update(&mut self, message: Message) -> Action { pub fn update(&mut self, message: Message) -> Action {
match message { match message {
Message::ChangeVideo(video) => { Message::ChangeVideo(video) => {
let Ok(mut player_video) = Url::from_file_path( self.update_entire_video(&video);
video.path.clone(),
)
.map(|url| Video::new(&url).expect("Should be here")) else {
self.video = None;
self.title = video.title.clone();
self.core_video = Some(video);
return Action::None;
};
player_video.set_paused(true);
self.video = Some(player_video);
self.title = video.title.clone();
self.core_video = Some(video.clone());
return self.update(Message::Update(video));
} }
Message::ChangeTitle(title) => { Message::ChangeTitle(title) => {
self.title = title.clone(); self.title = title.clone();
@ -100,11 +90,9 @@ impl VideoEditor {
move |video_result| { move |video_result| {
if let Ok(video) = video_result { if let Ok(video) = video_result {
let mut video = let mut video =
crate::core::videos::Video::from( videos::Video::from(video);
video,
);
video.id = video_id; video.id = video_id;
Message::ChangeVideo(video) Message::UpdateVideoFile(video)
} else { } else {
Message::None Message::None
} }
@ -112,6 +100,10 @@ impl VideoEditor {
); );
return Action::Task(task); return Action::Task(task);
} }
Message::UpdateVideoFile(video) => {
self.update_entire_video(&video);
return Action::UpdateVideo(video);
}
Message::None => (), Message::None => (),
} }
Action::None Action::None
@ -185,6 +177,22 @@ impl VideoEditor {
pub const fn editing(&self) -> bool { pub const fn editing(&self) -> bool {
self.editing self.editing
} }
fn update_entire_video(&mut self, video: &videos::Video) {
let Ok(mut player_video) =
Url::from_file_path(video.path.clone())
.map(|url| Video::new(&url).expect("Should be here"))
else {
self.video = None;
self.title = video.title.clone();
self.core_video = Some(video.clone());
return;
};
player_video.set_paused(true);
self.video = Some(player_video);
self.title = video.title.clone();
self.core_video = Some(video.clone());
}
} }
impl Default for VideoEditor { impl Default for VideoEditor {

View file

@ -3,7 +3,7 @@
* TODO Add OBS integration * TODO Add OBS integration
This will be based on each slide having the ability to activate an OBS scene when it is active. This will be based on each slide having the ability to activate an OBS scene when it is active.
* TODO Move text_generation function to be asynchronous so that UI doesn't lock up during song editing. * DONE Move text_generation function to be asynchronous so that UI doesn't lock up during song editing.
* DONE Build a presentation editor * DONE Build a presentation editor
* TODO [#A] Need to fix tests now that the basic app is working * TODO [#A] Need to fix tests now that the basic app is working