332 lines
11 KiB
Rust
332 lines
11 KiB
Rust
use std::{io, path::PathBuf};
|
|
|
|
use crate::core::presentations::Presentation;
|
|
use cosmic::{
|
|
Element, Task,
|
|
dialog::file_chooser::{FileFilter, open::Dialog},
|
|
iced::{ContentFit, Length, alignment::Vertical},
|
|
iced_widget::{column, row},
|
|
theme,
|
|
widget::{
|
|
self, Space, button, container, horizontal_space, icon,
|
|
image::Handle, text, text_input,
|
|
},
|
|
};
|
|
use miette::IntoDiagnostic;
|
|
use mupdf::{Colorspace, Document, Matrix};
|
|
use tracing::{debug, error, warn};
|
|
|
|
#[derive(Debug)]
|
|
pub struct PresentationEditor {
|
|
pub presentation: Option<Presentation>,
|
|
document: Option<Document>,
|
|
current_slide: Option<Handle>,
|
|
slides: Option<Vec<Handle>>,
|
|
page_count: Option<i32>,
|
|
current_slide_index: Option<i32>,
|
|
title: String,
|
|
editing: bool,
|
|
}
|
|
|
|
pub enum Action {
|
|
Task(Task<Message>),
|
|
UpdatePresentation(Presentation),
|
|
None,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum Message {
|
|
ChangePresentation(Presentation),
|
|
Update(Presentation),
|
|
ChangeTitle(String),
|
|
PickPresentation,
|
|
Edit(bool),
|
|
NextPage,
|
|
PrevPage,
|
|
None,
|
|
ChangePresentationFile(Presentation),
|
|
}
|
|
|
|
impl PresentationEditor {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
presentation: None,
|
|
document: None,
|
|
title: "".to_string(),
|
|
editing: false,
|
|
current_slide: None,
|
|
current_slide_index: None,
|
|
page_count: None,
|
|
slides: None,
|
|
}
|
|
}
|
|
pub fn update(&mut self, message: Message) -> Action {
|
|
match message {
|
|
Message::ChangePresentation(presentation) => {
|
|
self.update_entire_presentation(&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, "about to update");
|
|
return Action::UpdatePresentation(presentation);
|
|
}
|
|
Message::PickPresentation => {
|
|
let presentation_id = self
|
|
.presentation
|
|
.as_ref()
|
|
.map(|v| v.id)
|
|
.unwrap_or_default();
|
|
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::ChangePresentationFile(
|
|
presentation,
|
|
)
|
|
} else {
|
|
Message::None
|
|
}
|
|
},
|
|
);
|
|
return Action::Task(task);
|
|
}
|
|
Message::ChangePresentationFile(presentation) => {
|
|
self.update_entire_presentation(&presentation);
|
|
return self.update(Message::Update(presentation));
|
|
}
|
|
Message::None => (),
|
|
Message::NextPage => {
|
|
let next_index =
|
|
self.current_slide_index.unwrap_or_default() + 1;
|
|
if next_index > self.page_count.unwrap_or_default() {
|
|
return Action::None;
|
|
}
|
|
self.current_slide =
|
|
self.document.as_ref().and_then(|doc| {
|
|
let page = doc.load_page(next_index).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(next_index);
|
|
}
|
|
Message::PrevPage => {
|
|
let previous_index =
|
|
self.current_slide_index.unwrap_or_default() - 1;
|
|
if previous_index < 0 {
|
|
return Action::None;
|
|
}
|
|
self.current_slide =
|
|
self.document.as_ref().and_then(|doc| {
|
|
let page =
|
|
doc.load_page(previous_index).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(previous_index);
|
|
}
|
|
}
|
|
Action::None
|
|
}
|
|
|
|
pub fn view(&self) -> Element<Message> {
|
|
let presentation = if let Some(slide) = &self.current_slide {
|
|
container(
|
|
widget::image(slide)
|
|
.content_fit(ContentFit::ScaleDown),
|
|
)
|
|
} else {
|
|
container(Space::new(0, 0))
|
|
};
|
|
let pdf_pages: Vec<Element<Message>> =
|
|
if let Some(pages) = &self.slides {
|
|
pages
|
|
.iter()
|
|
.map(|page| {
|
|
let image = widget::image(page)
|
|
.content_fit(ContentFit::ScaleDown);
|
|
container(image).into()
|
|
})
|
|
.collect()
|
|
} else {
|
|
vec![horizontal_space().into()]
|
|
};
|
|
let pages_column = container(
|
|
column(pdf_pages)
|
|
.spacing(theme::active().cosmic().space_xs())
|
|
.padding(theme::spacing().space_l),
|
|
)
|
|
.class(theme::Container::Card);
|
|
let main_row = row![
|
|
pages_column.width(Length::FillPortion(1)),
|
|
presentation.center(Length::FillPortion(2))
|
|
]
|
|
.spacing(theme::spacing().space_xxl);
|
|
let control_buttons = row![
|
|
button::standard("Previous Page")
|
|
.on_press(Message::PrevPage),
|
|
horizontal_space(),
|
|
button::standard("Next Page").on_press(Message::NextPage),
|
|
];
|
|
let column =
|
|
column![self.toolbar(), main_row, control_buttons]
|
|
.spacing(theme::active().cosmic().space_l());
|
|
column.into()
|
|
}
|
|
|
|
fn toolbar(&self) -> Element<Message> {
|
|
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
|
|
}
|
|
|
|
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 {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
async fn pick_presentation() -> Result<PathBuf, PresentationError> {
|
|
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),
|
|
}
|