diff --git a/src/core/presentations.rs b/src/core/presentations.rs index bf47b5b..19f9166 100644 --- a/src/core/presentations.rs +++ b/src/core/presentations.rs @@ -1,13 +1,12 @@ -use cosmic::widget::image::Handle; use crisp::types::{Keyword, Symbol, Value}; use miette::{IntoDiagnostic, Result}; -use mupdf::{Colorspace, Document, Matrix}; +use mupdf::{Document, Page}; use serde::{Deserialize, Serialize}; use sqlx::{ pool::PoolConnection, prelude::FromRow, query, sqlite::SqliteRow, Row, Sqlite, SqliteConnection, SqlitePool, }; -use std::path::PathBuf; +use std::{path::PathBuf, sync::Arc}; use tracing::{debug, error}; use crate::{Background, Slide, SlideBuilder, TextAlignment}; @@ -132,36 +131,13 @@ impl ServiceTrait for Presentation { let background = Background::try_from(self.path.clone()) .into_diagnostic()?; debug!(?background); - let document = Document::open(background.path.as_path()) + let doc = Document::open(background.path.as_path()) .into_diagnostic()?; - debug!(?document); - let pages = document.pages().into_diagnostic()?; + debug!(?doc); + let pages = doc.page_count().into_diagnostic()?; debug!(?pages); - let pages: Vec = pages - .filter_map(|page| { - let Some(page) = page.ok() else { - return None; - }; - 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(), - )) - }) - .collect(); - let mut slides: Vec = vec![]; - for (index, page) in pages.into_iter().enumerate() { + for page in 0..pages { let slide = SlideBuilder::new() .background( Background::try_from(self.path.clone()) @@ -175,8 +151,7 @@ impl ServiceTrait for Presentation { .video_loop(false) .video_start_time(0.0) .video_end_time(0.0) - .pdf_index(index as u32) - .pdf_page(page) + .pdf_index(page as u32) .build()?; slides.push(slide); } diff --git a/src/core/service_items.rs b/src/core/service_items.rs index 91a1e82..da6267d 100644 --- a/src/core/service_items.rs +++ b/src/core/service_items.rs @@ -1,10 +1,12 @@ use std::borrow::Cow; use std::cmp::Ordering; use std::ops::Deref; +use std::sync::{Arc, Mutex}; use cosmic::iced::clipboard::mime::{AllowedMimeTypes, AsMimeTypes}; use crisp::types::{Keyword, Symbol, Value}; use miette::{IntoDiagnostic, Result}; +use resvg::usvg::fontdb; use serde::{Deserialize, Serialize}; use tracing::{debug, error}; @@ -258,7 +260,7 @@ impl From<&Song> for ServiceItem { kind: ServiceItemKind::Song(song.clone()), database_id: song.id, title: song.title.clone(), - slides: slides, + slides: slides.into(), ..Default::default() } } else { @@ -279,7 +281,7 @@ impl From<&Video> for ServiceItem { kind: ServiceItemKind::Video(video.clone()), database_id: video.id, title: video.title.clone(), - slides: slides, + slides: slides.into(), ..Default::default() } } else { @@ -300,7 +302,7 @@ impl From<&Image> for ServiceItem { kind: ServiceItemKind::Image(image.clone()), database_id: image.id, title: image.title.clone(), - slides: slides, + slides: slides.into(), ..Default::default() } } else { @@ -323,7 +325,7 @@ impl From<&Presentation> for ServiceItem { ), database_id: presentation.id, title: presentation.title.clone(), - slides: slides, + slides: slides.into(), ..Default::default() }, Err(e) => { diff --git a/src/core/slide.rs b/src/core/slide.rs index b857afa..97ad589 100644 --- a/src/core/slide.rs +++ b/src/core/slide.rs @@ -1,16 +1,20 @@ -use cosmic::widget::image::Handle; // use cosmic::dialog::ashpd::url::Url; use crisp::types::{Keyword, Symbol, Value}; use iced_video_player::Video; use miette::{miette, Result}; +use resvg::usvg::fontdb; use serde::{Deserialize, Serialize}; use std::{ fmt::Display, path::{Path, PathBuf}, + sync::Arc, }; use tracing::error; -use crate::ui::text_svg::TextSvg; +use crate::ui::{ + pdf::PdfViewer, + text_svg::{self, TextSvg}, +}; use super::songs::Song; @@ -30,8 +34,6 @@ pub struct Slide { video_end_time: f32, pdf_index: u32, #[serde(skip)] - pdf_page: Option, - #[serde(skip)] pub text_svg: Option, } @@ -305,7 +307,7 @@ impl Slide { } pub fn text_alignment(&self) -> TextAlignment { - self.text_alignment + self.text_alignment.clone() } pub fn font_size(&self) -> i32 { @@ -324,10 +326,6 @@ impl Slide { self.audio.clone() } - pub fn pdf_page(&self) -> Option { - self.pdf_page.clone() - } - pub fn pdf_index(&self) -> u32 { self.pdf_index } @@ -549,8 +547,6 @@ pub struct SlideBuilder { video_end_time: Option, pdf_index: Option, #[serde(skip)] - pdf_page: Option, - #[serde(skip)] text_svg: Option, } @@ -633,11 +629,6 @@ impl SlideBuilder { self } - pub(crate) fn pdf_page(mut self, pdf_page: Handle) -> Self { - let _ = self.pdf_page.insert(pdf_page); - self - } - pub(crate) fn pdf_index( mut self, pdf_index: impl Into, @@ -683,7 +674,6 @@ impl SlideBuilder { video_end_time, text_svg: self.text_svg, pdf_index: self.pdf_index.unwrap_or_default(), - pdf_page: self.pdf_page, ..Default::default() }) } diff --git a/src/main.rs b/src/main.rs index 9027860..930a524 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,7 @@ use cosmic::app::{Core, Settings, Task}; use cosmic::iced::alignment::Vertical; use cosmic::iced::keyboard::{Key, Modifiers}; use cosmic::iced::window::{Mode, Position}; -use cosmic::iced::{self, event, window, Length, Point}; +use cosmic::iced::{self, event, window, Color, Length, Point}; use cosmic::iced_futures::Subscription; use cosmic::iced_widget::{column, row, stack}; use cosmic::theme; @@ -40,7 +40,7 @@ use ui::song_editor::{self, SongEditor}; use ui::EditorMode; use crate::core::kinds::ServiceItemKind; -use crate::ui::text_svg::{self}; +use crate::ui::text_svg::{self, shadow, stroke, TextSvg}; pub mod core; pub mod lisp; @@ -985,7 +985,7 @@ impl cosmic::Application for App { } Message::Search(query) => { self.search_query = query.clone(); - self.search(query) + return self.search(query); } Message::UpdateSearchResults(items) => { self.search_results = items; @@ -1206,14 +1206,14 @@ where fn search(&self, query: String) -> Task { if let Some(library) = self.library.clone() { - Task::perform( + return Task::perform( async move { library.search_items(query).await }, |items| { cosmic::Action::App(Message::UpdateSearchResults( items, )) }, - ) + ); } else { Task::none() } @@ -1378,7 +1378,7 @@ where .data_received_for::(|item| { item.map_or_else( || Message::None, - Message::AppendServiceItem, + |item| Message::AppendServiceItem(item), ) }) .on_finish( @@ -1396,7 +1396,7 @@ where ] .padding(10) .spacing(10); - let container = Container::new(column) + let mut container = Container::new(column) // .height(Length::Fill) .style(nav_bar_style); diff --git a/src/ui/library.rs b/src/ui/library.rs index d8b18be..b8a9e8d 100644 --- a/src/ui/library.rs +++ b/src/ui/library.rs @@ -264,8 +264,7 @@ impl<'a> Library { let video_library = self.library_item(&self.video_library); let presentation_library = self.library_item(&self.presentation_library); - - column![ + let column = column![ text::heading("Library").center().width(Length::Fill), cosmic::iced::widget::horizontal_rule(1), song_library, @@ -276,7 +275,8 @@ impl<'a> Library { .height(Length::Fill) .padding(10) .spacing(10) - .into() + .into(); + column } pub fn library_item( @@ -481,23 +481,25 @@ impl<'a> Library { .accent_text_color() .into() } - } else if let Some((library, selected)) = self.selected_item - { - if model.kind == library - && selected == index as i32 + } else { + if let Some((library, selected)) = self.selected_item { - theme::active().cosmic().control_0().into() + if model.kind == library + && selected == index as i32 + { + theme::active().cosmic().control_0().into() + } else { + theme::active() + .cosmic() + .destructive_text_color() + .into() + } } else { theme::active() .cosmic() .destructive_text_color() .into() } - } else { - theme::active() - .cosmic() - .destructive_text_color() - .into() }; text::body(elide_text(item.subtext(), size.width)) .center() @@ -527,18 +529,20 @@ impl<'a> Library { && selected == index as i32 { t.cosmic().accent.selected.into() - } else if let Some((library, hovered)) = - self.hovered_item - { - if model.kind == library - && hovered == index as i32 + } else { + if let Some((library, hovered)) = + self.hovered_item { - t.cosmic().button.hover.into() + if model.kind == library + && hovered == index as i32 + { + t.cosmic().button.hover.into() + } else { + t.cosmic().button.base.into() + } } else { t.cosmic().button.base.into() } - } else { - t.cosmic().button.base.into() } } else if let Some((library, hovered)) = self.hovered_item diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 8372371..4719bc0 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -2,6 +2,7 @@ use crate::core::model::LibraryKind; pub mod double_ended_slider; pub mod library; +pub mod pdf; pub mod presenter; pub mod slide_editor; pub mod song_editor; diff --git a/src/ui/pdf.rs b/src/ui/pdf.rs new file mode 100644 index 0000000..60e9fe5 --- /dev/null +++ b/src/ui/pdf.rs @@ -0,0 +1,161 @@ +use std::path::Path; + +use cosmic::iced::ContentFit; +use cosmic::iced::Length; +use cosmic::widget::image; +use cosmic::widget::image::Handle; +use cosmic::Element; +use miette::IntoDiagnostic; +use miette::Result; +use miette::Severity; +use mupdf::Colorspace; +use mupdf::Document; +use mupdf::Matrix; +use tracing::debug; +use tracing::error; + +#[derive(Debug, Clone, Default)] +pub struct PdfViewer { + document: Option, + pages: Option>, + current_page: Option, + current_index: usize, +} + +pub enum Message {} + +impl PdfViewer { + pub fn with_pdf(pdf: impl AsRef) -> Result { + let pdf_path = pdf.as_ref(); + let document = Document::open(pdf_path).into_diagnostic()?; + let pages = document.pages().into_diagnostic()?; + let pages: Vec = pages + .filter_map(|page| { + let Some(page) = page.ok() else { + return None; + }; + 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; + }; + let handle = pixmap.samples().to_vec(); + Some(Handle::from_bytes(handle)) + }) + .collect(); + let Some(page) = document.pages().into_diagnostic()?.next() + else { + return Err(miette::miette!( + severity = Severity::Warning, + "There isn't a first page here" + )); + }; + let page = page.into_diagnostic()?; + let matrix = Matrix::IDENTITY; + let colorspace = Colorspace::device_rgb(); + let pixmap = page + .to_pixmap(&matrix, &colorspace, true, true) + .into_diagnostic()?; + let handle = pixmap.samples().to_vec(); + Ok(Self { + document: Some(document), + pages: Some(pages), + current_index: 0, + current_page: Some(Handle::from_bytes(handle)), + }) + } + + pub fn insert_pdf( + &mut self, + pdf: impl AsRef, + ) -> Result<()> { + let pdf_path = pdf.as_ref(); + let document = Document::open(pdf_path).into_diagnostic()?; + let pages = document.pages().into_diagnostic()?; + + let pages: Vec = pages + .filter_map(|page| { + let Some(page) = page.ok() else { + return None; + }; + 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(), + )) + }) + .collect(); + let Some(page) = document.pages().into_diagnostic()?.next() + else { + return Err(miette::miette!( + severity = Severity::Warning, + "There isn't a first page here" + )); + }; + self.current_page = pages.get(0).map(|h| h.to_owned()); + self.document = Some(document); + self.pages = Some(pages); + self.current_index = 0; + debug!(?self); + Ok(()) + } + + pub fn next_page(&mut self) -> Result<()> { + let Some(ref pages) = self.pages else { + return Err(miette::miette!("No pages in doc")); + }; + let Some(page) = pages.get(self.current_index + 1) else { + return Err(miette::miette!("There isn't a next page")); + }; + self.current_page = Some(page.to_owned()); + self.current_index += 1; + Ok(()) + } + + pub fn previous_page(&mut self) -> Result<()> { + if self.current_index == 0 { + return Err(miette::miette!("You are at the first page")); + } + let Some(ref pages) = self.pages else { + return Err(miette::miette!("No pages in doc")); + }; + let Some(page) = pages.get(self.current_index - 1) else { + return Err(miette::miette!( + "There isn't a previous page" + )); + }; + self.current_page = Some(page.to_owned()); + self.current_index -= 1; + Ok(()) + } + + pub fn view(&self, index: u32) -> Option> { + let Some(pages) = &self.pages else { + return None; + }; + let Some(handle) = pages.get(index as usize) else { + return None; + }; + Some( + image(handle) + .width(Length::Fill) + .height(Length::Fill) + .content_fit(ContentFit::Contain) + .into(), + ) + } +} diff --git a/src/ui/presenter.rs b/src/ui/presenter.rs index 74b77d0..ea268cc 100644 --- a/src/ui/presenter.rs +++ b/src/ui/presenter.rs @@ -3,18 +3,23 @@ use std::{fs::File, io::BufReader, path::PathBuf, sync::Arc}; use cosmic::{ iced::{ + alignment::Horizontal, + border, font::{Family, Stretch, Style, Weight}, Background, Border, Color, ContentFit, Font, Length, Shadow, Vector, }, iced_widget::{ + rich_text, scrollable::{ scroll_to, AbsoluteOffset, Direction, Scrollbar, - }, stack, vertical_rule, + }, + span, stack, vertical_rule, }, prelude::*, widget::{ - container, image, mouse_area, responsive, scrollable, text, Container, Id, Row, Space, + container, image, mouse_area, responsive, scrollable, text, + Column, Container, Id, Image, Row, Space, }, Task, }; @@ -28,6 +33,8 @@ use crate::{ BackgroundKind, }; +use crate::ui::pdf::PdfViewer; + const REFERENCE_WIDTH: f32 = 1920.0; const REFERENCE_HEIGHT: f32 = 1080.0; @@ -46,6 +53,7 @@ pub(crate) struct Presenter { hovered_slide: Option<(usize, usize)>, scroll_id: Id, current_font: Font, + pdf_viewer: PdfViewer, } pub(crate) enum Action { @@ -166,6 +174,7 @@ impl Presenter { }, scroll_id: Id::unique(), current_font: cosmic::font::default(), + pdf_viewer: PdfViewer::default(), } } @@ -216,6 +225,11 @@ impl Presenter { self.reset_video(); } + if slide.background().kind == BackgroundKind::Pdf { + self.pdf_viewer + .insert_pdf(&slide.background().path); + } + let offset = AbsoluteOffset { x: { if self.current_slide_index > 2 { @@ -397,6 +411,7 @@ impl Presenter { slide_view( &self.current_slide, &self.video, + &self.pdf_viewer, self.current_font, false, true, @@ -407,6 +422,7 @@ impl Presenter { slide_view( &self.current_slide, &self.video, + &self.pdf_viewer, self.current_font, false, false, @@ -441,8 +457,9 @@ impl Presenter { ); let container = slide_view( - slide, + &slide, &self.video, + &self.pdf_viewer, font, true, false, @@ -705,6 +722,7 @@ fn scale_font(font_size: f32, width: f32) -> f32 { pub(crate) fn slide_view<'a>( slide: &'a Slide, video: &'a Option