presentations now show up and reworked some of dnd for better
Some checks are pending
/ test (push) Waiting to run

This commit is contained in:
Chris Cochrun 2025-09-29 15:21:24 -05:00
parent a3516ef70c
commit ee45a11a0f
5 changed files with 323 additions and 57 deletions

View file

@ -1,7 +1,10 @@
use std::mem::replace; use std::{borrow::Cow, mem::replace};
use miette::{miette, Result}; use cosmic::iced::clipboard::mime::{AllowedMimeTypes, AsMimeTypes};
use miette::{miette, IntoDiagnostic, Result};
use serde::{Deserialize, Serialize};
use sqlx::{Connection, SqliteConnection}; use sqlx::{Connection, SqliteConnection};
use tracing::debug;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Model<T> { pub struct Model<T> {
@ -9,7 +12,9 @@ pub struct Model<T> {
pub kind: LibraryKind, pub kind: LibraryKind,
} }
#[derive(Debug, Clone, PartialEq, Eq, Copy)] #[derive(
Debug, Clone, PartialEq, Eq, Copy, Hash, Serialize, Deserialize,
)]
pub enum LibraryKind { pub enum LibraryKind {
Song, Song,
Video, Video,
@ -17,6 +22,46 @@ pub enum LibraryKind {
Presentation, Presentation,
} }
#[derive(
Debug, Clone, PartialEq, Eq, Copy, Hash, Serialize, Deserialize,
)]
pub struct KindWrapper(pub (LibraryKind, i32));
impl TryFrom<(Vec<u8>, String)> for KindWrapper {
type Error = miette::Error;
fn try_from(
value: (Vec<u8>, String),
) -> std::result::Result<Self, Self::Error> {
debug!(?value);
ron::de::from_bytes(&value.0).into_diagnostic()
}
}
impl AllowedMimeTypes for KindWrapper {
fn allowed() -> Cow<'static, [String]> {
Cow::from(vec!["application/service-item".to_string()])
}
}
impl AsMimeTypes for KindWrapper {
fn available(&self) -> Cow<'static, [String]> {
debug!(?self);
Cow::from(vec!["application/service-item".to_string()])
}
fn as_bytes(
&self,
mime_type: &str,
) -> Option<std::borrow::Cow<'static, [u8]>> {
debug!(?self);
debug!(mime_type);
let ron = ron::ser::to_string(self).ok()?;
debug!(ron);
Some(Cow::from(ron.into_bytes()))
}
}
impl<T> Model<T> { impl<T> Model<T> {
pub fn add_item(&mut self, item: T) -> Result<()> { pub fn add_item(&mut self, item: T) -> Result<()> {
self.items.push(item); self.items.push(item);

View file

@ -82,7 +82,7 @@ impl From<&Path> for Presentation {
} }
impl From<&Presentation> for Value { impl From<&Presentation> for Value {
fn from(value: &Presentation) -> Self { fn from(_value: &Presentation) -> Self {
Self::List(vec![Self::Symbol(Symbol("presentation".into()))]) Self::List(vec![Self::Symbol(Symbol("presentation".into()))])
} }
} }
@ -307,6 +307,38 @@ pub async fn remove_from_db(
.map(|_| ()) .map(|_| ())
} }
pub async fn add_presentation_to_db(
presentation: Presentation,
db: PoolConnection<Sqlite>,
id: i32,
) -> Result<()> {
let path = presentation
.path
.to_str()
.map(std::string::ToString::to_string)
.unwrap_or_default();
let html = presentation.kind == PresKind::Html;
let mut db = db.detach();
let result = query!(
r#"INSERT into presentations VALUES($1, $2, $3, $4)"#,
id,
presentation.title,
path,
html,
)
.execute(&mut db)
.await
.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(())
}
pub async fn update_presentation_in_db( pub async fn update_presentation_in_db(
presentation: Presentation, presentation: Presentation,
db: PoolConnection<Sqlite>, db: PoolConnection<Sqlite>,

View file

@ -47,8 +47,11 @@ use ui::presenter::{self, Presenter};
use ui::song_editor::{self, SongEditor}; use ui::song_editor::{self, SongEditor};
use ui::EditorMode; use ui::EditorMode;
use crate::core::content::Content;
use crate::core::kinds::ServiceItemKind; use crate::core::kinds::ServiceItemKind;
use crate::core::model::KindWrapper;
use crate::ui::image_editor::{self, ImageEditor}; use crate::ui::image_editor::{self, ImageEditor};
use crate::ui::presentation_editor::{self, PresentationEditor};
use crate::ui::text_svg::{self}; use crate::ui::text_svg::{self};
use crate::ui::video_editor::{self, VideoEditor}; use crate::ui::video_editor::{self, VideoEditor};
use crate::ui::widgets::draggable; use crate::ui::widgets::draggable;
@ -127,6 +130,7 @@ struct App {
song_editor: SongEditor, song_editor: SongEditor,
video_editor: VideoEditor, video_editor: VideoEditor,
image_editor: ImageEditor, image_editor: ImageEditor,
presentation_editor: PresentationEditor,
searching: bool, searching: bool,
search_query: String, search_query: String,
search_results: Vec<ServiceItem>, search_results: Vec<ServiceItem>,
@ -145,6 +149,7 @@ enum Message {
SongEditor(song_editor::Message), SongEditor(song_editor::Message),
VideoEditor(video_editor::Message), VideoEditor(video_editor::Message),
ImageEditor(image_editor::Message), ImageEditor(image_editor::Message),
PresentationEditor(presentation_editor::Message),
File(PathBuf), File(PathBuf),
OpenWindow, OpenWindow,
CloseWindow(Option<window::Id>), CloseWindow(Option<window::Id>),
@ -160,7 +165,7 @@ enum Message {
SelectServiceItem(usize), SelectServiceItem(usize),
AddSelectServiceItem(usize), AddSelectServiceItem(usize),
HoveredServiceItem(Option<usize>), HoveredServiceItem(Option<usize>),
AddServiceItem(usize, ServiceItem), AddServiceItem(usize, KindWrapper),
RemoveServiceItem(usize), RemoveServiceItem(usize),
AddServiceItemDrop(usize), AddServiceItemDrop(usize),
AppendServiceItem(ServiceItem), AppendServiceItem(ServiceItem),
@ -333,6 +338,7 @@ impl cosmic::Application for App {
song_editor, song_editor,
video_editor: VideoEditor::new(), video_editor: VideoEditor::new(),
image_editor: ImageEditor::new(), image_editor: ImageEditor::new(),
presentation_editor: PresentationEditor::new(),
searching: false, searching: false,
search_results: vec![], search_results: vec![],
search_query: String::new(), search_query: String::new(),
@ -818,6 +824,27 @@ impl cosmic::Application for App {
video_editor::Action::None => Task::none(), video_editor::Action::None => Task::none(),
} }
} }
Message::PresentationEditor(message) => {
match self.presentation_editor.update(message) {
presentation_editor::Action::Task(task) => {
task.map(|m| {
cosmic::Action::App(Message::PresentationEditor(
m,
))
})
}
presentation_editor::Action::UpdatePresentation(presentation) => {
if self.library.is_some() {
self.update(Message::Library(
library::Message::UpdatePresentation(presentation),
))
} else {
Task::none()
}
}
presentation_editor::Action::None => Task::none(),
}
}
Message::Present(message) => { Message::Present(message) => {
// debug!(?message); // debug!(?message);
if self.presentation_open if self.presentation_open
@ -854,7 +881,7 @@ impl cosmic::Application for App {
) )
})); }));
} }
_ => todo!(), _ => (),
} }
self.current_item = self.current_item =
(item_index, slide_index + 1); (item_index, slide_index + 1);
@ -882,7 +909,7 @@ impl cosmic::Application for App {
) )
})); }));
} }
_ => todo!(), _ => (),
} }
} }
Task::batch(tasks) Task::batch(tasks)
@ -914,7 +941,7 @@ impl cosmic::Application for App {
) )
})); }));
} }
_ => todo!(), _ => (),
} }
self.current_item = self.current_item =
(item_index, slide_index - 1); (item_index, slide_index - 1);
@ -957,7 +984,7 @@ impl cosmic::Application for App {
) )
})); }));
} }
_ => todo!(), _ => (),
} }
} }
Task::batch(tasks) Task::batch(tasks)
@ -1013,7 +1040,14 @@ impl cosmic::Application for App {
let image = lib_image.to_owned(); let image = lib_image.to_owned();
return self.update(Message::ImageEditor(image_editor::Message::ChangeImage(image))); return self.update(Message::ImageEditor(image_editor::Message::ChangeImage(image)));
}, },
core::model::LibraryKind::Presentation => todo!(), core::model::LibraryKind::Presentation => {
let Some(lib_presentation) = library.get_presentation(index) else {
return Task::none();
};
self.editor_mode = Some(kind.into());
let presentation = lib_presentation.to_owned();
return self.update(Message::PresentationEditor(presentation_editor::Message::ChangePresentation(presentation)));
},
} }
} }
library::Action::DraggedItem( library::Action::DraggedItem(
@ -1145,20 +1179,40 @@ impl cosmic::Application for App {
} }
Task::none() Task::none()
} }
Message::AddServiceItem(index, mut item) => { Message::AddServiceItem(index, item) => {
item.slides = item let item_index = item.0 .1;
.slides let kind = item.0 .0;
.into_par_iter() match kind {
.map(|mut slide| { core::model::LibraryKind::Song => todo!(),
let fontdb = Arc::clone(&self.fontdb); core::model::LibraryKind::Video => todo!(),
text_svg::text_svg_generator( core::model::LibraryKind::Image => todo!(),
&mut slide, fontdb, core::model::LibraryKind::Presentation => {
); let Some(library) = self.library.as_mut()
slide else {
}) return Task::none();
.collect(); };
self.service.insert(index, item); let Some(presentation) =
self.presenter.update_items(self.service.clone()); library.get_presentation(item_index)
else {
return Task::none();
};
let mut item = presentation.to_service_item();
item.slides = item
.slides
.into_par_iter()
.map(|mut slide| {
let fontdb = Arc::clone(&self.fontdb);
text_svg::text_svg_generator(
&mut slide, fontdb,
);
slide
})
.collect();
self.service.insert(index, item);
self.presenter
.update_items(self.service.clone());
}
}
Task::none() Task::none()
} }
Message::RemoveServiceItem(index) => { Message::RemoveServiceItem(index) => {
@ -1221,10 +1275,24 @@ impl cosmic::Application for App {
song_editor::Message::ChangeSong(song), song_editor::Message::ChangeSong(song),
)) ))
} }
ServiceItemKind::Video(_video) => todo!(), ServiceItemKind::Video(video) => {
ServiceItemKind::Image(_image) => todo!(), self.editor_mode = Some(EditorMode::Video);
ServiceItemKind::Presentation(_presentation) => { self.update(Message::VideoEditor(
todo!() video_editor::Message::ChangeVideo(video),
))
}
ServiceItemKind::Image(image) => {
self.editor_mode = Some(EditorMode::Image);
self.update(Message::ImageEditor(
image_editor::Message::ChangeImage(image),
))
}
ServiceItemKind::Presentation(presentation) => {
self.editor_mode =
Some(EditorMode::Presentation);
self.update(Message::PresentationEditor(
presentation_editor::Message::ChangePresentation(presentation),
))
} }
ServiceItemKind::Content(_slide) => todo!(), ServiceItemKind::Content(_slide) => todo!(),
} }
@ -1371,7 +1439,10 @@ impl cosmic::Application for App {
EditorMode::Video => { EditorMode::Video => {
self.video_editor.view().map(Message::VideoEditor) self.video_editor.view().map(Message::VideoEditor)
} }
EditorMode::Presentation => todo!(), EditorMode::Presentation => self
.presentation_editor
.view()
.map(Message::PresentationEditor),
EditorMode::Slide => todo!(), EditorMode::Slide => todo!(),
}, },
); );
@ -1692,7 +1763,7 @@ where
) )
.on_finish(move |mime, data, _, _, _| { .on_finish(move |mime, data, _, _, _| {
let Ok(item) = let Ok(item) =
ServiceItem::try_from((data, mime)) KindWrapper::try_from((data, mime))
else { else {
error!("couldn't drag in Service item"); error!("couldn't drag in Service item");
return Message::None; return Message::None;

View file

@ -26,8 +26,11 @@ use tracing::{debug, error, warn};
use crate::core::{ use crate::core::{
content::Content, content::Content,
images::{self, update_image_in_db, Image}, images::{self, update_image_in_db, Image},
model::{LibraryKind, Model}, model::{KindWrapper, LibraryKind, Model},
presentations::{self, update_presentation_in_db, Presentation}, presentations::{
self, add_presentation_to_db, update_presentation_in_db,
Presentation,
},
service_items::ServiceItem, service_items::ServiceItem,
songs::{self, update_song_in_db, Song}, songs::{self, update_song_in_db, Song},
videos::{self, update_video_in_db, Video}, videos::{self, update_video_in_db, Video},
@ -99,6 +102,7 @@ pub enum Message {
None, None,
AddImages(Option<Vec<Image>>), AddImages(Option<Vec<Image>>),
AddVideos(Option<Vec<Video>>), AddVideos(Option<Vec<Video>>),
AddPresentations(Option<Vec<Presentation>>),
} }
impl<'a> Library { impl<'a> Library {
@ -143,11 +147,10 @@ impl<'a> Library {
let item = match kind { let item = match kind {
LibraryKind::Song => { LibraryKind::Song => {
let song = Song::default(); let song = Song::default();
let index = self.song_library.items.len();
self.song_library self.song_library
.add_item(song) .add_item(song)
.map(|_| { .map(|_| {
let index =
self.song_library.items.len();
(LibraryKind::Song, index as i32) (LibraryKind::Song, index as i32)
}) })
.ok() .ok()
@ -165,26 +168,17 @@ impl<'a> Library {
)); ));
} }
LibraryKind::Presentation => { LibraryKind::Presentation => {
let presentation = Presentation::default(); return Action::Task(Task::perform(
self.presentation_library add_presentations(),
.add_item(presentation) Message::AddPresentations,
.map(|_| { ));
let index = self
.presentation_library
.items
.len();
(
LibraryKind::Presentation,
index as i32,
)
})
.ok()
} }
}; };
return self.update(Message::OpenItem(item)); return self.update(Message::OpenItem(item));
} }
Message::AddVideos(videos) => { Message::AddVideos(videos) => {
debug!(?videos); debug!(?videos);
let mut index = self.video_library.items.len();
if let Some(videos) = videos { if let Some(videos) = videos {
for video in videos { for video in videos {
if let Err(e) = if let Err(e) =
@ -192,6 +186,7 @@ impl<'a> Library {
{ {
error!(?e); error!(?e);
} }
index += 1;
} }
} }
return self.update(Message::OpenItem(Some(( return self.update(Message::OpenItem(Some((
@ -199,8 +194,78 @@ impl<'a> Library {
self.video_library.items.len() as i32 - 1, self.video_library.items.len() as i32 - 1,
)))); ))));
} }
Message::AddPresentations(presentations) => {
debug!(?presentations);
let mut index = self.presentation_library.items.len();
// Check if empty
let mut tasks = Vec::new();
if index == 0 {
if let Some(presentations) = presentations {
let len = presentations.len();
for presentation in presentations {
if let Err(e) = self
.presentation_library
.add_item(presentation.clone())
{
error!(?e);
}
let task = Task::future(
self.db.acquire(),
)
.and_then(move |db| {
Task::perform(
add_presentation_to_db(
presentation.to_owned(),
db,
index as i32,
),
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::Presentation,
index as i32,
)))
} else {
Message::None
}
},
)
});
tasks.push(task);
index += 1;
}
}
return Action::Task(Task::batch(tasks));
} else {
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,
self.presentation_library.items.len() as i32
- 1,
))));
}
}
Message::AddImages(images) => { Message::AddImages(images) => {
debug!(?images); debug!(?images);
let mut index = self.image_library.items.len();
if let Some(images) = images { if let Some(images) = images {
for image in images { for image in images {
if let Err(e) = if let Err(e) =
@ -208,6 +273,7 @@ impl<'a> Library {
{ {
error!(?e); error!(?e);
} }
index += 1;
} }
} }
return self.update(Message::OpenItem(Some(( return self.update(Message::OpenItem(Some((
@ -479,6 +545,21 @@ impl<'a> Library {
Message::PresentationChanged => (), Message::PresentationChanged => (),
Message::Error(_) => (), Message::Error(_) => (),
Message::OpenContext(index) => { Message::OpenContext(index) => {
let Some(kind) = self.library_open else {
return Action::None;
};
let Some(items) = self.selected_items.as_mut() else {
self.selected_items = vec![(kind, index)].into();
self.context_menu = Some(index);
return Action::None;
};
if items.contains(&(kind, index)) {
()
} else {
items.push((kind, index));
}
self.selected_items = Some(items.to_vec());
self.context_menu = Some(index); self.context_menu = Some(index);
} }
} }
@ -597,13 +678,12 @@ impl<'a> Library {
column({ column({
model.items.iter().enumerate().map( model.items.iter().enumerate().map(
|(index, item)| { |(index, item)| {
let kind = model.kind;
let service_item = item.to_service_item();
let visual_item = self let visual_item = self
.single_item(index, item, model) .single_item(index, item, model)
.map(|()| Message::None); .map(|()| Message::None);
DndSource::<Message, ServiceItem>::new({ DndSource::<Message, KindWrapper>::new({
let mouse_area = mouse_area(visual_item); let mouse_area = mouse_area(visual_item);
let mouse_area = mouse_area.on_enter(Message::HoverItem( let mouse_area = mouse_area.on_enter(Message::HoverItem(
Some(( Some((
@ -669,7 +749,7 @@ impl<'a> Library {
) )
}}) }})
.drag_content(move || { .drag_content(move || {
service_item.clone() KindWrapper((kind, index as i32))
}) })
.into() .into()
}, },
@ -908,6 +988,13 @@ impl<'a> Library {
self.image_library.get_item(index) self.image_library.get_item(index)
} }
pub fn get_presentation(
&self,
index: i32,
) -> Option<&Presentation> {
self.presentation_library.get_item(index)
}
pub fn set_modifiers(&mut self, modifiers: Option<Modifiers>) { pub fn set_modifiers(&mut self, modifiers: Option<Modifiers>) {
self.modifiers_pressed = modifiers; self.modifiers_pressed = modifiers;
} }
@ -1083,6 +1170,23 @@ async fn add_videos() -> Option<Vec<Video>> {
) )
} }
async fn add_presentations() -> Option<Vec<Presentation>> {
let paths = Dialog::new()
.title("pick presentation")
.open_files()
.await
.ok()?;
Some(
paths
.urls()
.iter()
.map(|path| {
Presentation::from(path.to_file_path().expect("oops"))
})
.collect(),
)
}
async fn add_db() -> Result<SqlitePool> { async fn add_db() -> Result<SqlitePool> {
let mut data = dirs::data_local_dir().unwrap(); let mut data = dirs::data_local_dir().unwrap();
data.push("lumina"); data.push("lumina");

View file

@ -6,12 +6,12 @@ use crate::core::{
}; };
use cosmic::{ use cosmic::{
dialog::file_chooser::{open::Dialog, FileFilter}, dialog::file_chooser::{open::Dialog, FileFilter},
iced::{alignment::Vertical, Length}, iced::{alignment::Vertical, ContentFit, Length},
iced_widget::{column, row}, iced_widget::{column, row},
theme, theme,
widget::{ widget::{
button, container, horizontal_space, icon, text, text_input, button, container, horizontal_space, icon, image, text,
Space, text_input, Space,
}, },
Element, Task, Element, Task,
}; };
@ -23,6 +23,7 @@ pub struct PresentationEditor {
slides: Option<Vec<Slide>>, slides: Option<Vec<Slide>>,
title: String, title: String,
editing: bool, editing: bool,
current_slide: i16,
} }
pub enum Action { pub enum Action {
@ -48,6 +49,7 @@ impl PresentationEditor {
slides: None, slides: None,
title: "Death was Arrested".to_string(), title: "Death was Arrested".to_string(),
editing: false, editing: false,
current_slide: 0,
} }
} }
pub fn update(&mut self, message: Message) -> Action { pub fn update(&mut self, message: Message) -> Action {
@ -55,10 +57,12 @@ impl PresentationEditor {
Message::ChangePresentation(presentation) => { Message::ChangePresentation(presentation) => {
self.presentation = Some(presentation.clone()); self.presentation = Some(presentation.clone());
self.title = presentation.title.clone(); self.title = presentation.title.clone();
warn!("changing presentation");
let Ok(slides) = presentation.to_slides() else { let Ok(slides) = presentation.to_slides() else {
return Action::None; return Action::None;
}; };
self.slides = Some(slides); self.slides = Some(slides);
self.current_slide = 0;
return self.update(Message::Update(presentation)); return self.update(Message::Update(presentation));
} }
Message::ChangeTitle(title) => { Message::ChangeTitle(title) => {
@ -107,8 +111,18 @@ impl PresentationEditor {
pub fn view(&self) -> Element<Message> { pub fn view(&self) -> Element<Message> {
let container = if let Some(slides) = &self.slides { let container = if let Some(slides) = &self.slides {
todo!(); if let Some(slide) =
// container(presentation) slides.get(self.current_slide as usize)
{
container(
image(slide.pdf_page().unwrap_or(
image::Handle::from_path("res/chad.png"),
))
.content_fit(ContentFit::Cover),
)
} else {
container(Space::new(0, 0))
}
} else { } else {
container(Space::new(0, 0)) container(Space::new(0, 0))
}; };