1146 lines
40 KiB
Rust
1146 lines
40 KiB
Rust
use clap::{command, Parser};
|
|
use core::service_items::ServiceItem;
|
|
use core::slide::*;
|
|
use core::songs::Song;
|
|
use crisp::types::Value;
|
|
use iced::keyboard::{Key, Modifiers};
|
|
use iced::theme::{self, Palette};
|
|
use iced::widget::tooltip::Position as TPosition;
|
|
use iced::widget::{
|
|
button, horizontal_space, mouse_area, slider, text, text_input,
|
|
tooltip, vertical_space, Space,
|
|
};
|
|
use iced::widget::{column, row};
|
|
use iced::window::{Mode, Position};
|
|
use iced::{self, event, window, Length, Padding, Point};
|
|
use iced::{color, Subscription};
|
|
use iced::{executor, Application, Element};
|
|
use iced::{widget::Container, Theme};
|
|
use iced::{Settings, Task};
|
|
use lisp::parse_lisp;
|
|
use miette::{miette, Result};
|
|
use rayon::prelude::*;
|
|
use std::collections::BTreeMap;
|
|
use std::fs::read_to_string;
|
|
use std::path::PathBuf;
|
|
use tracing::{debug, level_filters::LevelFilter};
|
|
use tracing::{error, warn};
|
|
use tracing_subscriber::EnvFilter;
|
|
use ui::library::{self, Library};
|
|
use ui::presenter::{self, Presenter};
|
|
use ui::song_editor::{self, SongEditor};
|
|
use ui::EditorMode;
|
|
|
|
use crate::ui::widgets::icon;
|
|
|
|
pub mod core;
|
|
pub mod lisp;
|
|
pub mod ui;
|
|
|
|
#[derive(Debug, Parser)]
|
|
#[command(name = "lumina", version, about)]
|
|
struct Cli {
|
|
#[arg(short, long)]
|
|
watch: bool,
|
|
#[arg(short = 'i', long)]
|
|
ui: bool,
|
|
file: PathBuf,
|
|
}
|
|
|
|
fn main() -> Result<()> {
|
|
let timer = tracing_subscriber::fmt::time::ChronoLocal::new(
|
|
"%Y-%m-%d_%I:%M:%S%.6f %P".to_owned(),
|
|
);
|
|
let filter = EnvFilter::builder()
|
|
.with_default_directive(LevelFilter::WARN.into())
|
|
.parse_lossy("lumina=debug");
|
|
tracing_subscriber::FmtSubscriber::builder()
|
|
.pretty()
|
|
.with_line_number(true)
|
|
.with_level(true)
|
|
.with_target(true)
|
|
.with_env_filter(filter)
|
|
.with_target(true)
|
|
.with_timer(timer)
|
|
.init();
|
|
|
|
// let settings;
|
|
// if args.ui {
|
|
// debug!("main view");
|
|
// settings = iced::daemon::Settings::default()
|
|
// .debug(false)
|
|
// .is_daemon(true)
|
|
// .transparent(true);
|
|
// } else {
|
|
// debug!("window view");
|
|
// settings = Settings::default()
|
|
// .debug(false)
|
|
// .no_main_window(true)
|
|
// .is_daemon(true);
|
|
// }
|
|
|
|
iced::daemon(App::init, App::update, App::view)
|
|
.settings(Settings::default())
|
|
.subscription(App::subscription)
|
|
.theme(App::theme)
|
|
.title(App::title)
|
|
.run()
|
|
.map_err(|e| miette!("Invalid things... {}", e))
|
|
}
|
|
|
|
struct App {
|
|
file: PathBuf,
|
|
presenter: Presenter,
|
|
windows: BTreeMap<window::Id, Window>,
|
|
service: Vec<ServiceItem>,
|
|
current_item: (usize, usize),
|
|
presentation_open: bool,
|
|
cli_mode: bool,
|
|
library: Option<Library>,
|
|
library_open: bool,
|
|
library_width: f32,
|
|
editor_mode: Option<EditorMode>,
|
|
song_editor: SongEditor,
|
|
searching: bool,
|
|
library_dragged_item: Option<ServiceItem>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
enum Message {
|
|
Present(presenter::Message),
|
|
Library(library::Message),
|
|
SongEditor(song_editor::Message),
|
|
File(PathBuf),
|
|
DndEnter(Vec<String>),
|
|
DndDrop,
|
|
OpenWindow,
|
|
CloseWindow(Option<window::Id>),
|
|
WindowOpened(window::Id, Option<Point>),
|
|
WindowClosed(window::Id),
|
|
AddLibrary(Library),
|
|
LibraryToggle,
|
|
Quit,
|
|
Key(Key, Modifiers),
|
|
None,
|
|
DndLeave(),
|
|
EditorToggle(bool),
|
|
SearchFocus,
|
|
ChangeServiceItem(usize),
|
|
AddServiceItem(usize, ServiceItem),
|
|
AddServiceItemDrop(usize),
|
|
AppendServiceItem(Option<ServiceItem>),
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct Window {
|
|
title: String,
|
|
scale_input: String,
|
|
current_scale: f64,
|
|
theme: Theme,
|
|
}
|
|
|
|
impl Default for Window {
|
|
fn default() -> Self {
|
|
Self {
|
|
title: Default::default(),
|
|
scale_input: Default::default(),
|
|
current_scale: Default::default(),
|
|
theme: Theme::custom(
|
|
"Snazzy",
|
|
Palette {
|
|
background: color!(0x282a36),
|
|
text: color!(0xe2e4e5),
|
|
primary: color!(0x57c7ff),
|
|
success: color!(0x5af78e),
|
|
warning: color!(0xff9f43),
|
|
danger: color!(0xff5c57),
|
|
},
|
|
),
|
|
}
|
|
}
|
|
}
|
|
|
|
const HEADER_SPACE: f32 = 6.0;
|
|
|
|
impl App {
|
|
const APP_ID: &'static str = "lumina";
|
|
fn title(&self, id: window::Id) -> String {
|
|
self.windows
|
|
.get(&id)
|
|
.map(|window| window.title.clone())
|
|
.unwrap_or(String::from("Lumina"))
|
|
}
|
|
|
|
fn init() -> (Self, Task<Message>) {
|
|
debug!("init");
|
|
let args = Cli::parse();
|
|
|
|
let mut batch = vec![];
|
|
let mut windows = BTreeMap::new();
|
|
if args.ui {
|
|
let settings = window::Settings {
|
|
..Default::default()
|
|
};
|
|
let (id, open) = window::open(settings);
|
|
batch
|
|
.push(open.map(|id| Message::WindowOpened(id, None)));
|
|
let window = Window {
|
|
title: "Lumina".into(),
|
|
scale_input: "".into(),
|
|
current_scale: 1.0,
|
|
theme: Theme::custom(
|
|
"Snazzy",
|
|
Palette {
|
|
background: color!(0x282a36),
|
|
text: color!(0xe2e4e5),
|
|
primary: color!(0x57c7ff),
|
|
success: color!(0x5af78e),
|
|
warning: color!(0xff9f43),
|
|
danger: color!(0xff5c57),
|
|
},
|
|
),
|
|
};
|
|
|
|
windows.insert(id, window);
|
|
}
|
|
|
|
let items = match read_to_string(args.file) {
|
|
Ok(lisp) => {
|
|
let mut slide_vector = vec![];
|
|
let lisp = crisp::reader::read(&lisp);
|
|
match lisp {
|
|
Value::List(vec) => {
|
|
// let items = vec
|
|
// .into_par_iter()
|
|
// .map(|value| parse_lisp(value))
|
|
// .collect();
|
|
// slide_vector.append(items);
|
|
for value in vec {
|
|
let mut inner_vector = parse_lisp(value);
|
|
slide_vector.append(&mut inner_vector);
|
|
}
|
|
}
|
|
_ => todo!(),
|
|
}
|
|
slide_vector
|
|
}
|
|
Err(e) => {
|
|
warn!("Missing file or could not read: {e}");
|
|
vec![]
|
|
}
|
|
};
|
|
|
|
let presenter = Presenter::with_items(items.clone());
|
|
let song_editor = SongEditor::new();
|
|
|
|
// for item in items.iter() {
|
|
// nav_model.insert().text(item.title()).data(item.clone());
|
|
// }
|
|
|
|
// nav_model.activate_position(0);
|
|
|
|
let mut app = App {
|
|
presenter,
|
|
service: items,
|
|
file: PathBuf::default(),
|
|
windows,
|
|
presentation_open: false,
|
|
cli_mode: !args.ui,
|
|
library: None,
|
|
library_open: true,
|
|
library_width: 60.0,
|
|
editor_mode: None,
|
|
song_editor,
|
|
searching: false,
|
|
current_item: (0, 0),
|
|
library_dragged_item: None,
|
|
};
|
|
|
|
if args.ui {
|
|
debug!("main view");
|
|
// batch.push(app.update_title())
|
|
} else {
|
|
debug!("window view");
|
|
batch.push(app.show_window())
|
|
};
|
|
|
|
batch.push(app.add_library());
|
|
// batch.push(app.add_service(items));
|
|
let batch = Task::batch(batch);
|
|
(app, batch)
|
|
}
|
|
|
|
fn nav_bar(&self) -> Option<Element<Message>> {
|
|
// if !self.core().nav_bar_active() {
|
|
// return None;
|
|
// }
|
|
|
|
// let nav_model = self.nav_model()?;
|
|
|
|
// let mut nav = iced::widget::nav_bar(nav_model, |id| {
|
|
// iced::Action::Iced(iced::app::Action::NavBar(id))
|
|
// })
|
|
// .on_dnd_drop::<ServiceItem>(|entity, data, action| {
|
|
// debug!(?entity);
|
|
// debug!(?data);
|
|
// debug!(?action);
|
|
// iced::Action::App(Message::DndDrop)
|
|
// })
|
|
// .on_dnd_enter(|entity, data| {
|
|
// debug!("entered");
|
|
// iced::Action::App(Message::DndEnter(entity, data))
|
|
// })
|
|
// .on_dnd_leave(|entity| {
|
|
// debug!("left");
|
|
// iced::Action::App(Message::DndLeave(entity))
|
|
// })
|
|
// .drag_id(DragId::new())
|
|
// .on_context(|id| {
|
|
// iced::Action::Iced(
|
|
// iced::app::Action::NavBarContext(id),
|
|
// )
|
|
// })
|
|
// .context_menu(None)
|
|
// .into_container()
|
|
// // XXX both must be shrink to avoid flex layout from ignoring it
|
|
// .width(Length::Shrink)
|
|
// .height(Length::Shrink);
|
|
|
|
let list =
|
|
self.service.iter().enumerate().map(|(index, item)| {
|
|
let icon = match item.kind {
|
|
core::kinds::ServiceItemKind::Song(_) => {
|
|
icon::from_name("folder-music-symbolic")
|
|
}
|
|
core::kinds::ServiceItemKind::Video(_) => {
|
|
icon::from_name("folder-videos-symbolic")
|
|
}
|
|
core::kinds::ServiceItemKind::Image(_) => {
|
|
icon::from_name("folder-pictures-symbolic")
|
|
}
|
|
core::kinds::ServiceItemKind::Presentation(_) => {
|
|
icon::from_name(
|
|
"x-office-presentation-symbolic",
|
|
)
|
|
}
|
|
core::kinds::ServiceItemKind::Content(_) => {
|
|
icon::from_name(
|
|
"x-office-presentation-symbolic",
|
|
)
|
|
}
|
|
};
|
|
let button =
|
|
button(row![text(item.title.clone()), icon])
|
|
.padding(5)
|
|
.width(Length::Fill)
|
|
.on_press(Message::ChangeServiceItem(index));
|
|
let tooltip = tooltip(
|
|
button,
|
|
text(item.kind.to_string()),
|
|
TPosition::Right,
|
|
);
|
|
mouse_area(tooltip)
|
|
.on_release(Message::AddServiceItemDrop(index))
|
|
.into()
|
|
});
|
|
|
|
let end_index = self.service.len();
|
|
let column = column![
|
|
text("Service List").center().width(280),
|
|
column(list).spacing(10),
|
|
mouse_area(vertical_space(),).on_release(
|
|
Message::AppendServiceItem(
|
|
self.library_dragged_item.clone()
|
|
)
|
|
)
|
|
]
|
|
.padding(10)
|
|
.spacing(10);
|
|
let padding = Padding::new(0.0).top(20);
|
|
let mut container = Container::new(column)
|
|
// .height(Length::Fill)
|
|
// .style(nav_bar_style)
|
|
.padding(padding);
|
|
|
|
// if !self.core().is_condensed() {
|
|
// container = container.max_width(280);
|
|
// }
|
|
Some(container.into())
|
|
}
|
|
|
|
fn header_start(&self) -> Vec<Element<Message>> {
|
|
vec![]
|
|
}
|
|
|
|
fn header_center(&self) -> Vec<Element<Message>> {
|
|
vec![text_input("Search...", "")
|
|
.on_input(|_| Message::None)
|
|
.on_submit(Message::None)
|
|
.width(1200)
|
|
.into()]
|
|
}
|
|
|
|
fn header_end(&self) -> Vec<Element<Message>> {
|
|
// let editor_toggle = toggler(self.editor_mode.is_some())
|
|
// .label("Editor")
|
|
// .spacing(10)
|
|
// .width(Length::Shrink)
|
|
// .on_toggle(Message::EditorToggle);
|
|
|
|
let presenter_window = self.windows.len() > 1;
|
|
let presentation_button_text = if self.presentation_open {
|
|
text("End Presentation")
|
|
} else {
|
|
text("Present")
|
|
};
|
|
|
|
let row = row![
|
|
tooltip(
|
|
button(
|
|
row!(
|
|
Container::new(
|
|
icon::from_name("document-edit-symbolic")
|
|
.scale(3)
|
|
)
|
|
.center_y(Length::Fill),
|
|
text(if self.editor_mode.is_some() {
|
|
"Present Mode"
|
|
} else {
|
|
"Edit Mode"
|
|
})
|
|
)
|
|
.spacing(5),
|
|
)
|
|
.on_press(Message::EditorToggle(
|
|
self.editor_mode.is_none(),
|
|
)),
|
|
"Enter Edit Mode",
|
|
TPosition::Bottom,
|
|
),
|
|
tooltip(
|
|
button(
|
|
row!(
|
|
Container::new(
|
|
icon::from_name(
|
|
if self.presentation_open {
|
|
"window-close-symbolic"
|
|
} else {
|
|
"view-presentation-symbolic"
|
|
}
|
|
)
|
|
.scale(3)
|
|
)
|
|
.center_y(Length::Fill),
|
|
presentation_button_text
|
|
)
|
|
.spacing(5),
|
|
)
|
|
.on_press({
|
|
if self.presentation_open {
|
|
// Message::CloseWindow(
|
|
// presenter_window.copied(),
|
|
// )
|
|
Message::None
|
|
} else {
|
|
Message::OpenWindow
|
|
}
|
|
}),
|
|
"Start Presentation",
|
|
TPosition::Bottom,
|
|
),
|
|
tooltip(
|
|
button(
|
|
row!(
|
|
Container::new(
|
|
icon::from_name("view-list-symbolic")
|
|
.scale(3)
|
|
)
|
|
.center_y(Length::Fill),
|
|
text(if self.library_open {
|
|
"Close Library"
|
|
} else {
|
|
"Open Library"
|
|
})
|
|
)
|
|
.spacing(5),
|
|
)
|
|
.on_press(Message::LibraryToggle),
|
|
"Open Library",
|
|
TPosition::Bottom,
|
|
)
|
|
]
|
|
.spacing(HEADER_SPACE)
|
|
.into();
|
|
vec![row]
|
|
}
|
|
|
|
fn footer(&self) -> Option<Element<Message>> {
|
|
let total_items_text =
|
|
format!("Total Service Items: {}", self.service.len());
|
|
let total_slides_text =
|
|
format!("Total Slides: {}", self.presenter.total_slides);
|
|
let row =
|
|
row![text(total_items_text), text(total_slides_text)]
|
|
.spacing(10);
|
|
Some(
|
|
Container::new(row)
|
|
.align_right(Length::Fill)
|
|
.padding([5, 0])
|
|
.into(),
|
|
)
|
|
}
|
|
|
|
fn subscription(&self) -> Subscription<Message> {
|
|
event::listen_with(|event, _, id| {
|
|
// debug!(?event);
|
|
match event {
|
|
iced::Event::Keyboard(event) => match event {
|
|
iced::keyboard::Event::KeyReleased {
|
|
key,
|
|
modifiers,
|
|
..
|
|
} => Some(Message::Key(key, modifiers)),
|
|
_ => None,
|
|
},
|
|
iced::Event::Mouse(_event) => None,
|
|
iced::Event::Window(window_event) => {
|
|
match window_event {
|
|
window::Event::CloseRequested => {
|
|
debug!("Closing window");
|
|
Some(Message::CloseWindow(Some(id)))
|
|
}
|
|
window::Event::Opened {
|
|
position, ..
|
|
} => {
|
|
debug!(?window_event, ?id);
|
|
Some(Message::WindowOpened(id, position))
|
|
}
|
|
window::Event::Closed => {
|
|
debug!("Closed window");
|
|
Some(Message::WindowClosed(id))
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
iced::Event::Touch(_touch) => None,
|
|
iced::Event::InputMethod(event) => todo!(),
|
|
}
|
|
})
|
|
}
|
|
|
|
fn dialog(&self) -> Option<Element<'_, Message>> {
|
|
if self.searching {
|
|
Some(text("hello").into())
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn update(&mut self, message: Message) -> Task<Message> {
|
|
match message {
|
|
Message::Key(key, modifiers) => {
|
|
self.process_key_press(key, modifiers)
|
|
}
|
|
Message::SongEditor(message) => {
|
|
// debug!(?message);
|
|
match self.song_editor.update(message) {
|
|
song_editor::Action::Task(task) => {
|
|
task.map(|m| Message::SongEditor(m))
|
|
}
|
|
song_editor::Action::UpdateSong(song) => {
|
|
if let Some(library) = &mut self.library {
|
|
self.update(Message::Library(
|
|
library::Message::UpdateSong(song),
|
|
))
|
|
} else {
|
|
Task::none()
|
|
}
|
|
}
|
|
song_editor::Action::None => Task::none(),
|
|
}
|
|
}
|
|
Message::Present(message) => {
|
|
// debug!(?message);
|
|
if self.presentation_open
|
|
&& let Some(video) = &mut self.presenter.video
|
|
{
|
|
video.set_muted(false);
|
|
}
|
|
match self.presenter.update(message) {
|
|
presenter::Action::Task(task) => {
|
|
task.map(|m| Message::Present(m))
|
|
}
|
|
presenter::Action::None => Task::none(),
|
|
presenter::Action::NextSlide => {
|
|
let slide_index = self.current_item.1;
|
|
let item_index = self.current_item.0;
|
|
let mut tasks = vec![];
|
|
if let Some(item) =
|
|
self.service.get(item_index)
|
|
{
|
|
if item.slides.len() > slide_index + 1 {
|
|
// let slide_length = item.slides.len();
|
|
// debug!(
|
|
// slide_index,
|
|
// slide_length,
|
|
// ?item,
|
|
// "Slides are longer"
|
|
// );
|
|
let slide = item.slides
|
|
[slide_index + 1]
|
|
.clone();
|
|
let action = self.presenter.update(
|
|
presenter::Message::SlideChange(
|
|
slide,
|
|
),
|
|
);
|
|
match action {
|
|
presenter::Action::Task(task) => {
|
|
tasks.push(task.map(|m| {
|
|
Message::Present(m)
|
|
}))
|
|
}
|
|
_ => todo!(),
|
|
}
|
|
self.current_item =
|
|
(item_index, slide_index + 1);
|
|
Task::batch(tasks)
|
|
} else {
|
|
// debug!("Slides are not longer");
|
|
self.current_item =
|
|
(item_index + 1, 0);
|
|
if let Some(item) =
|
|
self.service.get(item_index + 1)
|
|
{
|
|
let action = self.presenter.update(presenter::Message::SlideChange(item.slides[0].clone()));
|
|
match action {
|
|
presenter::Action::Task(
|
|
task,
|
|
) => tasks.push(task.map(
|
|
|m| Message::Present(m),
|
|
)),
|
|
_ => todo!(),
|
|
}
|
|
}
|
|
Task::batch(tasks)
|
|
}
|
|
} else {
|
|
Task::none()
|
|
}
|
|
}
|
|
presenter::Action::PrevSlide => {
|
|
let slide_index = self.current_item.1;
|
|
let item_index = self.current_item.0;
|
|
let mut tasks = vec![];
|
|
if let Some(item) =
|
|
self.service.get(item_index)
|
|
{
|
|
if slide_index != 0 {
|
|
let slide = item.slides
|
|
[slide_index - 1]
|
|
.clone();
|
|
let action = self.presenter.update(
|
|
presenter::Message::SlideChange(
|
|
slide,
|
|
),
|
|
);
|
|
match action {
|
|
presenter::Action::Task(task) => {
|
|
tasks.push(task.map(|m| {
|
|
Message::Present(m)
|
|
}))
|
|
}
|
|
_ => todo!(),
|
|
}
|
|
self.current_item =
|
|
(item_index, slide_index - 1);
|
|
Task::batch(tasks)
|
|
} else if slide_index == 0
|
|
&& item_index == 0
|
|
{
|
|
Task::none()
|
|
} else {
|
|
// debug!("Change slide to previous items slides");
|
|
let previous_item_slides_length =
|
|
if let Some(item) = self
|
|
.service
|
|
.get(item_index - 1)
|
|
{
|
|
item.slides.len()
|
|
} else {
|
|
0
|
|
};
|
|
self.current_item = (
|
|
item_index - 1,
|
|
previous_item_slides_length - 1,
|
|
);
|
|
if let Some(item) =
|
|
self.service.get(item_index - 1)
|
|
{
|
|
let action = self.presenter.update(presenter::Message::SlideChange(item.slides[previous_item_slides_length - 1].clone()));
|
|
match action {
|
|
presenter::Action::Task(
|
|
task,
|
|
) => tasks.push(task.map(
|
|
|m| Message::Present(m),
|
|
)),
|
|
_ => todo!(),
|
|
}
|
|
}
|
|
Task::batch(tasks)
|
|
}
|
|
} else {
|
|
Task::none()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Message::Library(message) => {
|
|
let mut song = Song::default();
|
|
if let Some(library) = &mut self.library {
|
|
match library.update(message) {
|
|
library::Action::OpenItem(None) => {
|
|
return Task::none();
|
|
}
|
|
library::Action::Task(task) => {
|
|
return task.map(|message| {
|
|
Message::Library(message)
|
|
});
|
|
}
|
|
library::Action::None => return Task::none(),
|
|
library::Action::OpenItem(Some((
|
|
kind,
|
|
index,
|
|
))) => {
|
|
debug!(
|
|
"Should get song at index: {:?}",
|
|
index
|
|
);
|
|
let Some(lib_song) =
|
|
library.get_song(index)
|
|
else {
|
|
return Task::none();
|
|
};
|
|
self.editor_mode = Some(kind.into());
|
|
song = lib_song.to_owned();
|
|
debug!(
|
|
"Should change songs to: {:?}",
|
|
song
|
|
);
|
|
}
|
|
library::Action::DraggedItem(
|
|
service_item,
|
|
) => {
|
|
debug!("hi");
|
|
self.library_dragged_item =
|
|
Some(service_item);
|
|
// self.nav_model
|
|
// .insert()
|
|
// .text(service_item.title.clone())
|
|
// .data(service_item);
|
|
}
|
|
}
|
|
}
|
|
self.update(Message::SongEditor(
|
|
song_editor::Message::ChangeSong(song),
|
|
))
|
|
}
|
|
Message::File(file) => {
|
|
self.file = file;
|
|
Task::none()
|
|
}
|
|
Message::OpenWindow => self.show_window(),
|
|
Message::CloseWindow(id) => {
|
|
if let Some(id) = id {
|
|
window::close(id)
|
|
} else {
|
|
Task::none()
|
|
}
|
|
}
|
|
Message::WindowOpened(id, _) => {
|
|
debug!(?id, "Window opened");
|
|
let main_id =
|
|
self.windows.first_key_value().unwrap().0;
|
|
if self.cli_mode || &id > main_id {
|
|
self.presentation_open = true;
|
|
if let Some(video) = &mut self.presenter.video {
|
|
video.set_muted(false);
|
|
}
|
|
window::set_mode(id, Mode::Fullscreen)
|
|
} else {
|
|
Task::none()
|
|
}
|
|
}
|
|
Message::WindowClosed(id) => {
|
|
warn!("Closing window: {id}");
|
|
self.windows.remove(&id);
|
|
// This closes the app if using the cli example
|
|
if self.windows.is_empty() {
|
|
self.update(Message::Quit)
|
|
} else {
|
|
self.presentation_open = false;
|
|
if let Some(video) = &mut self.presenter.video {
|
|
video.set_muted(true);
|
|
}
|
|
Task::none()
|
|
}
|
|
}
|
|
Message::LibraryToggle => {
|
|
self.library_open = !self.library_open;
|
|
Task::none()
|
|
}
|
|
Message::Quit => iced::exit(),
|
|
Message::DndEnter(data) => {
|
|
debug!(?data);
|
|
Task::none()
|
|
}
|
|
Message::DndDrop => {
|
|
// debug!(?entity);
|
|
// debug!(?action);
|
|
// debug!(?service_item);
|
|
|
|
if let Some(library) = &self.library
|
|
&& let Some((lib, item)) = library.dragged_item
|
|
{
|
|
// match lib {
|
|
// core::model::LibraryKind::Song => ,
|
|
// core::model::LibraryKind::Video => todo!(),
|
|
// core::model::LibraryKind::Image => todo!(),
|
|
// core::model::LibraryKind::Presentation => todo!(),
|
|
// }
|
|
let item = library.get_song(item).unwrap();
|
|
let item = ServiceItem::from(item);
|
|
}
|
|
Task::none()
|
|
}
|
|
Message::AddLibrary(library) => {
|
|
self.library = Some(library);
|
|
Task::none()
|
|
}
|
|
Message::None => Task::none(),
|
|
Message::DndLeave() => {
|
|
// debug!(?entity);
|
|
Task::none()
|
|
}
|
|
Message::EditorToggle(edit) => {
|
|
if edit {
|
|
self.editor_mode = Some(EditorMode::Song);
|
|
} else {
|
|
self.editor_mode = None;
|
|
}
|
|
Task::none()
|
|
}
|
|
Message::SearchFocus => {
|
|
self.searching = true;
|
|
Task::none()
|
|
}
|
|
Message::ChangeServiceItem(index) => {
|
|
if let Some((index, item)) = self
|
|
.service
|
|
.iter()
|
|
.enumerate()
|
|
.find(|(id, _)| index == *id)
|
|
&& let Some(slide) = item.slides.first()
|
|
{
|
|
self.current_item = (index, 0);
|
|
self.presenter.update(
|
|
presenter::Message::SlideChange(
|
|
slide.clone(),
|
|
),
|
|
);
|
|
}
|
|
Task::none()
|
|
}
|
|
Message::AddServiceItem(index, item) => {
|
|
self.service.insert(index, item);
|
|
Task::none()
|
|
}
|
|
Message::AddServiceItemDrop(index) => {
|
|
if let Some(item) = &self.library_dragged_item {
|
|
self.service.insert(index, item.clone());
|
|
}
|
|
Task::none()
|
|
}
|
|
Message::AppendServiceItem(item) => {
|
|
if let Some(item) = item {
|
|
self.service.push(item);
|
|
}
|
|
Task::none()
|
|
}
|
|
}
|
|
}
|
|
|
|
// Main window view
|
|
fn view(&self, window: window::Id) -> Element<Message> {
|
|
if window == *self.windows.first_key_value().unwrap().0 {
|
|
self.main_view()
|
|
} else {
|
|
self.view_presenter()
|
|
}
|
|
}
|
|
|
|
fn main_view(&self) -> Element<Message> {
|
|
let icon_left = icon::from_name("arrow-left");
|
|
let icon_right = icon::from_name("arrow-right");
|
|
|
|
let video_range = self.presenter.video.as_ref().map_or_else(
|
|
|| 0.0,
|
|
|video| video.duration().as_secs_f32(),
|
|
);
|
|
|
|
let video_button_icon = if let Some(video) =
|
|
&self.presenter.video
|
|
{
|
|
let (icon_name, tt) = if video.paused() {
|
|
("media-play", "Play")
|
|
} else {
|
|
("media-pause", "Pause")
|
|
};
|
|
tooltip(
|
|
button(icon::from_name(icon_name)).on_press(
|
|
Message::Present(presenter::Message::StartVideo),
|
|
),
|
|
tt,
|
|
TPosition::FollowCursor,
|
|
)
|
|
} else {
|
|
tooltip(
|
|
button(icon::from_name("media-play")).on_press(
|
|
Message::Present(presenter::Message::StartVideo),
|
|
),
|
|
"Play",
|
|
TPosition::FollowCursor,
|
|
)
|
|
};
|
|
|
|
let slide_preview = column![
|
|
Space::with_height(Length::Fill),
|
|
Container::new(
|
|
self.presenter.view_preview().map(Message::Present),
|
|
)
|
|
.height(250)
|
|
.align_bottom(Length::Fill),
|
|
Container::new(if self.presenter.video.is_some() {
|
|
row![
|
|
video_button_icon,
|
|
Container::new(
|
|
slider(
|
|
0.0..=video_range,
|
|
self.presenter.video_position,
|
|
|pos| {
|
|
Message::Present(
|
|
presenter::Message::VideoPos(pos),
|
|
)
|
|
}
|
|
)
|
|
.step(0.1)
|
|
)
|
|
.center_x(Length::Fill)
|
|
.padding([7, 0])
|
|
]
|
|
.padding(5)
|
|
} else {
|
|
row![]
|
|
})
|
|
.center_x(Length::Fill),
|
|
Space::with_height(Length::Fill),
|
|
]
|
|
.spacing(3);
|
|
|
|
let library = if self.library_open {
|
|
Container::new(if let Some(library) = &self.library {
|
|
library.view().map(Message::Library)
|
|
} else {
|
|
Space::new(0, 0).into()
|
|
})
|
|
// .style(nav_bar_style)
|
|
.center(Length::FillPortion(2))
|
|
} else {
|
|
Container::new(horizontal_space().width(0))
|
|
};
|
|
|
|
let song_editor =
|
|
self.song_editor.view().map(Message::SongEditor);
|
|
|
|
let row = row![
|
|
Container::new(tooltip(
|
|
button(icon_left.size(128)).width(128).on_press(
|
|
Message::Present(presenter::Message::PrevSlide)
|
|
),
|
|
"Previous Slide",
|
|
TPosition::FollowCursor
|
|
))
|
|
.center_y(Length::Fill)
|
|
.align_right(Length::FillPortion(1)),
|
|
Container::new(slide_preview)
|
|
.center_y(Length::Fill)
|
|
.width(Length::FillPortion(3)),
|
|
Container::new(tooltip(
|
|
button(icon_right.size(128)).width(128).on_press(
|
|
Message::Present(presenter::Message::NextSlide)
|
|
),
|
|
"Next Slide",
|
|
TPosition::FollowCursor
|
|
))
|
|
.center_y(Length::Fill)
|
|
.align_left(Length::FillPortion(1)),
|
|
library
|
|
]
|
|
.width(Length::Fill)
|
|
.height(Length::Fill)
|
|
.spacing(20);
|
|
|
|
let column = column![
|
|
Container::new(row).center_y(Length::Fill),
|
|
Container::new(
|
|
self.presenter.preview_bar().map(Message::Present)
|
|
)
|
|
.clip(true)
|
|
.width(Length::Fill)
|
|
.center_y(180)
|
|
];
|
|
|
|
if let Some(_editor) = &self.editor_mode {
|
|
Element::from(song_editor)
|
|
} else {
|
|
Element::from(column)
|
|
}
|
|
}
|
|
|
|
// View for presentation
|
|
fn view_presenter(&self) -> Element<Message> {
|
|
self.presenter.view().map(Message::Present)
|
|
}
|
|
|
|
// fn update_title(&mut self) -> Task<Message> {
|
|
// let window_title = "Lumina";
|
|
// self.windows.first_key_value().unwrap().1.title =
|
|
// window_title.to_string();
|
|
// Task::none()
|
|
// }
|
|
|
|
fn show_window(&mut self) -> Task<Message> {
|
|
let (id, spawn_window) = window::open(window::Settings {
|
|
position: Position::Centered,
|
|
exit_on_close_request: true,
|
|
decorations: false,
|
|
fullscreen: true,
|
|
..Default::default()
|
|
});
|
|
let window = Window {
|
|
title: "Presentation".into(),
|
|
scale_input: "".into(),
|
|
current_scale: 1.0,
|
|
theme: self.theme(id),
|
|
};
|
|
self.windows.insert(id, window);
|
|
spawn_window.map(|id| Message::WindowOpened(id, None))
|
|
}
|
|
|
|
fn add_library(&mut self) -> Task<Message> {
|
|
Task::perform(async move { Library::new().await }, |x| {
|
|
Message::AddLibrary(x)
|
|
})
|
|
}
|
|
|
|
// fn add_service(
|
|
// &mut self,
|
|
// items: Vec<ServiceItem>,
|
|
// ) -> Task<Message> {
|
|
// Task::perform(
|
|
// async move {
|
|
// for item in items {
|
|
// debug!(?item, "Item to be appended");
|
|
// let slides = item.to_slides().unwrap_or(vec![]);
|
|
// map.insert(item, slides);
|
|
// }
|
|
// let len = map.len();
|
|
// debug!(len, "to be append: ");
|
|
// map
|
|
// },
|
|
// |x| {
|
|
// let len = x.len();
|
|
// debug!(len, "to append: ");
|
|
// iced::Action::App(Message::AppendService(x))
|
|
// },
|
|
// )
|
|
// }
|
|
|
|
fn process_key_press(
|
|
&mut self,
|
|
key: Key,
|
|
modifiers: Modifiers,
|
|
) -> Task<Message> {
|
|
debug!(?key, ?modifiers);
|
|
if self.editor_mode.is_some() {
|
|
return Task::none();
|
|
}
|
|
if self.song_editor.editing() {
|
|
return Task::none();
|
|
}
|
|
match (key, modifiers) {
|
|
(
|
|
Key::Named(iced::keyboard::key::Named::ArrowRight),
|
|
_,
|
|
) => self.update(Message::Present(
|
|
presenter::Message::NextSlide,
|
|
)),
|
|
(
|
|
Key::Named(iced::keyboard::key::Named::ArrowLeft),
|
|
_,
|
|
) => self.update(Message::Present(
|
|
presenter::Message::PrevSlide,
|
|
)),
|
|
(Key::Named(iced::keyboard::key::Named::Space), _) => {
|
|
self.update(Message::Present(
|
|
presenter::Message::NextSlide,
|
|
))
|
|
}
|
|
(Key::Character(k), _) if k == *"j" || k == *"l" => self
|
|
.update(Message::Present(
|
|
presenter::Message::NextSlide,
|
|
)),
|
|
(Key::Character(k), _) if k == *"k" || k == *"h" => self
|
|
.update(Message::Present(
|
|
presenter::Message::PrevSlide,
|
|
)),
|
|
(Key::Character(k), _) if k == *"q" => {
|
|
self.update(Message::Quit)
|
|
}
|
|
_ => Task::none(),
|
|
}
|
|
}
|
|
|
|
fn theme(&self, _window: window::Id) -> Theme {
|
|
Theme::custom(
|
|
"Snazzy",
|
|
Palette {
|
|
background: color!(0x282a36),
|
|
text: color!(0xe2e4e5),
|
|
primary: color!(0x57c7ff),
|
|
success: color!(0x5af78e),
|
|
warning: color!(0xff9f43),
|
|
danger: color!(0xff5c57),
|
|
},
|
|
)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
|
|
fn test_slide() -> String {
|
|
let slide = r#"(slide (image :source "./somehting.jpg" :fill cover
|
|
(text "Something cooler" :font-size 50)))"#;
|
|
String::from(slide)
|
|
}
|
|
// #[test]
|
|
// fn test_lisp() {
|
|
// let slide = test_slide();
|
|
// if let Ok(data) = lexpr::parse::from_str_elisp(slide.as_str()) {
|
|
// assert_eq!(slide, data)
|
|
// } else {
|
|
// assert!(false)
|
|
// }
|
|
// }
|
|
}
|