diff --git a/migrations/20250925190420_Remove_the_page_count_column_in_presentations_table.sql b/migrations/20250925190420_Remove_the_page_count_column_in_presentations_table.sql new file mode 100644 index 0000000..b1a6d30 --- /dev/null +++ b/migrations/20250925190420_Remove_the_page_count_column_in_presentations_table.sql @@ -0,0 +1,3 @@ +-- Add migration script here +ALTER TABLE presentations +DROP COLUMN pageCount; diff --git a/src/core/presentations.rs b/src/core/presentations.rs index 768e693..246164b 100644 --- a/src/core/presentations.rs +++ b/src/core/presentations.rs @@ -7,7 +7,7 @@ use sqlx::{ pool::PoolConnection, prelude::FromRow, query, sqlite::SqliteRow, Row, Sqlite, SqliteConnection, SqlitePool, }; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use tracing::{debug, error}; use crate::{Background, Slide, SlideBuilder, TextAlignment}; @@ -48,6 +48,39 @@ impl PartialEq for Presentation { } } +impl From for Presentation { + fn from(value: PathBuf) -> Self { + let kind = match value + .extension() + .unwrap_or_default() + .to_str() + .unwrap_or_default() + { + "pdf" => PresKind::Pdf, + "html" => PresKind::Html, + _ => PresKind::Generic, + }; + let title = value + .file_name() + .unwrap_or_default() + .to_str() + .unwrap_or_default() + .to_string(); + Self { + id: 0, + title, + path: value.canonicalize().unwrap_or(value), + kind, + } + } +} + +impl From<&Path> for Presentation { + fn from(value: &Path) -> Self { + Self::from(value.to_owned()) + } +} + impl From<&Presentation> for Value { fn from(value: &Presentation) -> Self { Self::List(vec![Self::Symbol(Symbol("presentation".into()))]) @@ -284,18 +317,69 @@ pub async fn update_presentation_in_db( .map(std::string::ToString::to_string) .unwrap_or_default(); let html = presentation.kind == PresKind::Html; - query!( + let mut db = db.detach(); + let id = presentation.id.clone(); + if let Err(e) = + query!("SELECT id FROM presentations where id = $1", id) + .fetch_one(&mut db) + .await + { + if let Ok(ids) = query!("SELECT id FROM presentations") + .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, "Presentation not found"); + max += 1; + let result = query!( + r#"INSERT into presentations VALUES($1, $2, $3, $4)"#, + max, + presentation.title, + path, + html, + ) + .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!(?presentation, "should be been updated"); + let result = query!( r#"UPDATE presentations SET title = $2, file_path = $3, html = $4 WHERE id = $1"#, presentation.id, presentation.title, path, html ) - .execute(&mut db.detach()) - .await - .into_diagnostic()?; + .execute(&mut db) + .await.into_diagnostic(); - Ok(()) + match result { + Ok(_) => { + debug!("should have been updated"); + Ok(()) + } + Err(e) => { + error! {?e}; + Err(e) + } + } } pub async fn get_presentation_from_db( diff --git a/src/ui/mod.rs b/src/ui/mod.rs index e69efc6..82b3935 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -3,6 +3,7 @@ use crate::core::model::LibraryKind; pub mod double_ended_slider; pub mod image_editor; pub mod library; +pub mod presentation_editor; pub mod presenter; pub mod service; pub mod slide_editor; diff --git a/src/ui/presentation_editor.rs b/src/ui/presentation_editor.rs new file mode 100644 index 0000000..d5b32bc --- /dev/null +++ b/src/ui/presentation_editor.rs @@ -0,0 +1,190 @@ +use std::{io, path::PathBuf}; + +use crate::core::{ + presentations::Presentation, service_items::ServiceTrait, + slide::Slide, +}; +use cosmic::{ + dialog::file_chooser::{open::Dialog, FileFilter}, + iced::{alignment::Vertical, Length}, + iced_widget::{column, row}, + theme, + widget::{ + self, button, container, horizontal_space, icon, text, + text_input, Space, + }, + Element, Task, +}; +use tracing::{debug, error, warn}; + +#[derive(Debug)] +pub struct PresentationEditor { + pub presentation: Option, + slides: Option>, + title: String, + editing: bool, +} + +pub enum Action { + Task(Task), + UpdatePresentation(Presentation), + None, +} + +#[derive(Debug, Clone)] +pub enum Message { + ChangePresentation(Presentation), + Update(Presentation), + ChangeTitle(String), + PickPresentation, + Edit(bool), + None, +} + +impl PresentationEditor { + pub fn new() -> Self { + Self { + presentation: None, + slides: None, + title: "Death was Arrested".to_string(), + editing: false, + } + } + pub fn update(&mut self, message: Message) -> Action { + match message { + Message::ChangePresentation(presentation) => { + self.presentation = Some(presentation.clone()); + self.title = presentation.title.clone(); + let Ok(slides) = presentation.to_slides() else { + return Action::None; + }; + self.slides = Some(slides); + return self.update(Message::Update(presentation)); + } + Message::ChangeTitle(title) => { + self.title = title.clone(); + if let Some(presentation) = &self.presentation { + let mut presentation = presentation.clone(); + presentation.title = title; + return self + .update(Message::Update(presentation)); + } + } + Message::Edit(edit) => { + debug!(edit); + self.editing = edit; + } + Message::Update(presentation) => { + warn!(?presentation); + return Action::UpdatePresentation(presentation); + } + Message::PickPresentation => { + let presentation_id = self + .presentation + .as_ref() + .map(|v| v.id) + .unwrap_or_default() + .clone(); + let task = Task::perform( + pick_presentation(), + move |presentation_result| { + if let Ok(presentation) = presentation_result + { + let mut presentation = + Presentation::from(presentation); + presentation.id = presentation_id; + Message::ChangePresentation(presentation) + } else { + Message::None + } + }, + ); + return Action::Task(task); + } + Message::None => (), + } + Action::None + } + + pub fn view(&self) -> Element { + let container = if let Some(slides) = &self.slides { + todo!(); + // container(presentation) + } else { + container(Space::new(0, 0)) + }; + let column = column![ + self.toolbar(), + container.center_x(Length::FillPortion(2)) + ] + .spacing(theme::active().cosmic().space_l()); + column.into() + } + + fn toolbar(&self) -> Element { + let title_box = text_input("Title...", &self.title) + .on_input(Message::ChangeTitle); + + let presentation_selector = button::icon( + icon::from_name("folder-presentations-symbolic").scale(2), + ) + .label("Presentation") + .tooltip("Select a presentation") + .on_press(Message::PickPresentation) + .padding(10); + + row![ + text::body("Title:"), + title_box, + horizontal_space(), + presentation_selector + ] + .align_y(Vertical::Center) + .spacing(10) + .into() + } + + pub const fn editing(&self) -> bool { + self.editing + } +} + +impl Default for PresentationEditor { + fn default() -> Self { + Self::new() + } +} + +async fn pick_presentation() -> Result { + let dialog = Dialog::new().title("Choose a presentation..."); + let bg_filter = FileFilter::new("Presentations") + .extension("pdf") + .extension("html"); + dialog + .filter(bg_filter) + .directory(dirs::home_dir().expect("oops")) + .open_file() + .await + .map_err(|e| { + error!(?e); + PresentationError::DialogClosed + }) + .map(|file| file.url().to_file_path().unwrap()) + // rfd::AsyncFileDialog::new() + // .set_title("Choose a background...") + // .add_filter( + // "Presentations and Presentations", + // &["png", "jpeg", "mp4", "webm", "mkv", "jpg", "mpeg"], + // ) + // .set_directory(dirs::home_dir().unwrap()) + // .pick_file() + // .await + // .ok_or(PresentationError::BackgroundDialogClosed) + // .map(|file| file.path().to_owned()) +} + +#[derive(Debug, Clone)] +pub enum PresentationError { + DialogClosed, + IOError(io::ErrorKind), +}