lumina/src/ui/library.rs

1436 lines
No EOL
54 KiB
Rust
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use std::{collections::HashMap, sync::Arc};
use cosmic::{
Element, Task,
dialog::file_chooser::open::Dialog,
iced::{
Background, Border, Color, Length, alignment::Vertical,
clipboard::dnd::DndAction, keyboard::Modifiers,
},
iced_core::widget::tree::State,
iced_widget::{column, row as rowm, text as textm},
theme,
widget::{
Container, DndSource, Space, button, container, context_menu,
divider, dnd_destination, icon,
menu::{self, Action as MenuAction},
mouse_area, responsive, row, scrollable,
space::{self, horizontal},
text, text_input,
},
};
use miette::{IntoDiagnostic, Result};
use rapidfuzz::distance::levenshtein;
use sqlx::{Sqlite, SqlitePool, migrate, pool::PoolConnection};
use tracing::{debug, error, warn};
use crate::core::{
content::Content,
images::{self, Image, add_image_to_db, update_image_in_db},
kinds::ServiceItemKind,
model::{KindWrapper, LibraryKind, Model},
presentations::{
self, Presentation, add_presentation_to_db,
update_presentation_in_db,
},
service_items::ServiceItem,
songs::{self, Song, add_to_db, update_song_in_db},
videos::{self, Video, add_video_to_db, update_video_in_db},
};
#[allow(clippy::struct_field_names)]
#[derive(Debug, Clone)]
pub struct Library {
song_library: Model<Song>,
image_library: Model<Image>,
video_library: Model<Video>,
presentation_library: Model<Presentation>,
library_open: Option<LibraryKind>,
library_hovered: Option<LibraryKind>,
selected_items: Option<Vec<(LibraryKind, i32)>>,
hovered_item: Option<(LibraryKind, i32)>,
editing_item: Option<(LibraryKind, i32)>,
db: Arc<SqlitePool>,
menu_keys: std::collections::HashMap<menu::KeyBind, MenuMessage>,
context_menu: Option<i32>,
modifiers_pressed: Option<Modifiers>,
}
#[derive(Debug, Clone, Eq, PartialEq, Copy)]
enum MenuMessage {
Delete,
Open,
}
impl MenuAction for MenuMessage {
type Message = Message;
fn message(&self) -> Self::Message {
match self {
Self::Delete => Message::DeleteItem,
Self::Open => Message::OpenContextItem,
}
}
}
#[allow(clippy::large_enum_variant)]
pub enum Action {
OpenItem(Option<(LibraryKind, i32)>),
DraggedItem(ServiceItem),
Task(Task<Message>),
None,
}
#[derive(Clone, Debug)]
pub enum Message {
AddItem,
DeleteItem,
OpenItem(Option<(LibraryKind, i32)>),
OpenContextItem,
HoverLibrary(Option<LibraryKind>),
OpenLibrary(Option<LibraryKind>),
HoverItem(Option<(LibraryKind, i32)>),
SelectItem(Option<(LibraryKind, i32)>),
DragItem(ServiceItem),
UpdateSong(Song),
SongChanged,
UpdateImage(Image),
ImageChanged,
UpdateVideo(Video),
VideoChanged,
UpdatePresentation(Presentation),
PresentationChanged,
Error(String),
OpenContext(i32),
None,
AddFiles(Vec<ServiceItemKind>),
ReaddSongs(Vec<Song>),
AddSong,
AddImages(Option<Vec<Image>>),
AddVideos(Option<Vec<Video>>),
AddPresentations(Option<Vec<Presentation>>),
AddPresentationSplit(Option<Presentation>),
}
impl<'a> Library {
pub async fn new() -> Self {
let mut db = add_db().await.expect("probs");
if let Err(e) = migrate!().run(&db).await {
error!(?e);
}
Self {
song_library: Model::new_song_model(&mut db).await,
image_library: Model::new_image_model(&mut db).await,
video_library: Model::new_video_model(&mut db).await,
presentation_library: Model::new_presentation_model(
&mut db,
)
.await,
library_open: None,
library_hovered: None,
selected_items: None,
hovered_item: None,
editing_item: None,
db: db.into(),
menu_keys: HashMap::new(),
context_menu: None,
modifiers_pressed: None,
}
}
#[must_use]
pub fn get_song(&self, index: i32) -> Option<&Song> {
self.song_library.get_item(index)
}
async fn test(&mut self) -> Result<Song> {
self.song_library.new_song(Arc::clone(&self.db)).await
}
#[allow(clippy::cast_possible_wrap)]
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::too_many_lines)]
#[allow(clippy::match_same_arms)]
pub fn update(&mut self, message: Message) -> Action {
match message {
Message::None => (),
Message::DeleteItem => {
return self.delete_items();
}
Message::ReaddSongs(songs) => {
self.song_library.items = songs;
}
Message::AddSong => {
let songs =
self.song_library.items.drain(..).collect();
return Action::Task(Task::perform(
add_to_db(songs, Arc::clone(&self.db)),
move |res| match res {
Ok(songs) => Message::ReaddSongs(songs),
Err(e) => {
error!("adding error: {e}");
Message::None
}
},
));
}
Message::AddItem => {
let kind =
self.library_open.unwrap_or(LibraryKind::Song);
match kind {
LibraryKind::Song => {
return self.update(Message::AddSong);
}
LibraryKind::Video => {
return Action::Task(Task::perform(
add_videos(),
Message::AddVideos,
));
}
LibraryKind::Image => {
return Action::Task(Task::perform(
add_images(),
Message::AddImages,
));
}
LibraryKind::Presentation => {
return Action::Task(Task::perform(
add_presentations(),
Message::AddPresentations,
));
}
};
}
Message::AddVideos(videos) => {
debug!(?videos);
let mut index = self.video_library.items.len();
// Check if empty
let mut tasks = Vec::new();
if let Some(videos) = videos {
// let len = videos.len();
for video in videos {
if let Err(e) =
self.video_library.add_item(video.clone())
{
error!(?e);
}
let task = Task::future(self.db.acquire())
.map_err(|e| {
miette::miette!("Database error: {e}")
})
.and_then(move |db| {
Task::perform(
add_video_to_db(
video.clone(),
db,
),
move |res| {
res.map(|_| {
Message::OpenItem(Some((
LibraryKind::Video,
index as i32,
)))
})
},
)
})
.map(|r| r.unwrap_or(Message::None));
tasks.push(task);
index += 1;
}
}
let after_task =
Task::done(Message::OpenItem(Some((
LibraryKind::Video,
self.video_library.items.len() as i32 - 1,
))));
return Action::Task(
Task::batch(tasks).chain(after_task),
);
}
Message::AddPresentationSplit(presentation) => {
debug!(?presentation, "adding to db");
if let Some(presentation) = presentation {
if let Err(e) = self
.presentation_library
.add_item(presentation.clone())
{
error!(?e);
}
return Action::Task(
Task::future(self.db.acquire())
.map_err(|e| {
miette::miette!("Database error: {e}")
})
.and_then(move |db| {
Task::perform(
add_presentation_to_db(
presentation.clone(),
db,
),
move |res| {
res.map(|_| Message::None)
},
)
})
.map(|r| r.unwrap_or(Message::None)),
);
}
}
Message::AddPresentations(presentations) => {
debug!(?presentations, "adding to db");
let mut index = self.presentation_library.items.len();
// Check if empty
let mut tasks = Vec::new();
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(),
)
.map_err(|e| miette::miette!("Database error: {e}"))
.and_then(move |db| {
Task::perform(
add_presentation_to_db(
presentation.clone(),
db,
),
move |res| res.map(|_| Message::OpenItem(Some((LibraryKind::Presentation, index as i32))))
)
}).map(|r| r.unwrap_or(Message::None));
tasks.push(task);
index += 1;
}
}
let after_task =
Task::done(Message::OpenItem(Some((
LibraryKind::Presentation,
self.presentation_library.items.len() as i32
- 1,
))));
return Action::Task(
Task::batch(tasks).chain(after_task),
);
}
Message::AddImages(images) => {
debug!(?images);
let mut index = self.image_library.items.len();
// Check if empty
let mut tasks = Vec::new();
if let Some(images) = images {
// let len = images.len();
for image in images {
if let Err(e) =
self.image_library.add_item(image.clone())
{
error!(?e);
}
let task = Task::future(self.db.acquire())
.map_err(|e| {
miette::miette!("Database error: {e}")
})
.and_then(move |db| {
Task::perform(
add_image_to_db(
image.clone(),
db,
),
move |res| {
res.map(|_| {
Message::OpenItem(Some((
LibraryKind::Image,
index as i32,
)))
})
},
)
})
.map(|r| r.unwrap_or(Message::None));
tasks.push(task);
index += 1;
}
}
let after_task =
Task::done(Message::OpenItem(Some((
LibraryKind::Image,
self.image_library.items.len() as i32 - 1,
))));
return Action::Task(
Task::batch(tasks).chain(after_task),
);
}
Message::OpenItem(item) => {
debug!(?item);
self.editing_item = item;
return Action::OpenItem(item);
}
Message::OpenContextItem => {
let Some(kind) = self.library_open else {
return Action::None;
};
let Some(index) = self.context_menu else {
return Action::None;
};
return self
.update(Message::OpenItem(Some((kind, index))));
}
Message::HoverLibrary(library_kind) => {
self.library_hovered = library_kind;
}
Message::OpenLibrary(library_kind) => {
self.selected_items = None;
self.library_open = library_kind;
}
Message::HoverItem(item) => {
self.hovered_item = item;
}
Message::SelectItem(item) => {
let Some(modifiers) = self.modifiers_pressed else {
let Some(item) = item else {
return Action::None;
};
self.selected_items = Some(vec![item]);
return Action::None;
};
if modifiers.is_empty() {
let Some(item) = item else {
return Action::None;
};
self.selected_items = Some(vec![item]);
return Action::None;
}
if modifiers.shift() {
let Some(first_item) = self
.selected_items
.as_ref()
.and_then(|items| {
items
.iter()
.next()
.map(|(_, index)| index)
})
else {
let Some(item) = item else {
return Action::None;
};
self.selected_items = Some(vec![item]);
return Action::None;
};
let Some((kind, index)) = item else {
return Action::None;
};
if first_item < &index {
for id in *first_item..=index {
self.selected_items = self
.selected_items
.clone()
.map(|mut items| {
items.push((kind, id));
items
});
}
} else if first_item > &index {
for id in index..*first_item {
self.selected_items = self
.selected_items
.clone()
.map(|mut items| {
items.push((kind, id));
items
});
}
}
}
if modifiers.control() {
let Some(item) = item else {
return Action::None;
};
let Some(items) = self.selected_items.as_mut()
else {
self.selected_items = Some(vec![item]);
return Action::None;
};
items.push(item);
self.selected_items = Some(items.clone());
}
}
Message::DragItem(item) => {
debug!(?item);
// self.dragged_item = item;
return Action::DraggedItem(item);
}
Message::UpdateSong(song) => {
let Some((kind, index)) = self.editing_item else {
error!("Not editing an item");
return Action::None;
};
if kind != LibraryKind::Song {
error!("Not editing a song item");
return Action::None;
}
return Action::Task(Task::perform(
self.song_library.update_song(song, &self.db),
|r| {
r.map(|_| Message::SongChanged)
.unwrap_or(Message::None)
},
));
}
Message::SongChanged => {
// self.song_library.update_item(song, index);
debug!("song changed");
}
Message::UpdateImage(image) => {
let Some((kind, index)) = self.editing_item else {
error!("Not editing an item");
return Action::None;
};
if kind != LibraryKind::Image {
error!("Not editing a image item");
return Action::None;
}
return Action::Task(Task::perform(
self.image_library.update_image(image, &self.db),
|r| {
r.map(|_| Message::ImageChanged)
.unwrap_or(Message::None)
},
));
}
Message::ImageChanged => (),
Message::UpdateVideo(video) => {
let Some((kind, index)) = self.editing_item else {
error!("Not editing an item");
return Action::None;
};
if kind != LibraryKind::Video {
error!("Not editing a video item");
return Action::None;
}
return Action::Task(Task::perform(
self.video_library.update_video(video, &self.db),
|r| {
r.map(|_| Message::VideoChanged)
.unwrap_or(Message::None)
},
));
}
Message::VideoChanged => debug!("vid shoulda changed"),
Message::UpdatePresentation(presentation) => {
let Some((kind, _index)) = self.editing_item else {
error!("Not editing an item");
return Action::None;
};
if kind != LibraryKind::Presentation {
error!("Not editing a presentation item");
return Action::None;
}
return Action::Task(Task::perform(
self.presentation_library
.update_presentation(presentation, &self.db),
|r| {
r.map(|_| Message::PresentationChanged)
.unwrap_or(Message::None)
},
));
}
Message::PresentationChanged => (),
Message::Error(_) => (),
Message::OpenContext(index) => {
let Some(kind) = self.library_open else {
return Action::None;
};
debug!(index, "should context");
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)) {
debug!(index, "should context contained");
self.selected_items = Some(items.clone());
} else {
debug!(index, "should context not contained");
self.selected_items = vec![(kind, index)].into();
}
self.context_menu = Some(index);
}
Message::AddFiles(items) => {
let mut tasks = Vec::new();
let last_item = &items.last();
let after_task = match last_item {
Some(ServiceItemKind::Image(_image)) => {
Task::done(Message::OpenItem(Some((
LibraryKind::Image,
self.image_library.items.len() as i32 - 1,
))))
}
_ => Task::none(),
};
for item in items {
match item {
ServiceItemKind::Song(song) => {
let Some(e) = self
.song_library
.add_item(song.clone())
.err()
else {
let songs = self
.song_library
.items
.drain(..)
.collect();
let task = Task::perform(
songs::add_to_db(
songs,
Arc::clone(&self.db),
),
{
move |res| match res {
Ok(songs) => {
Message::ReaddSongs(
songs,
)
}
Err(e) => {
error!(?e);
Message::None
}
}
},
);
tasks.push(task);
continue;
};
error!(?e);
}
ServiceItemKind::Video(video) => {
let Some(e) = self
.video_library
.add_item(video.clone())
.err()
else {
let task =
Task::future(self.db.acquire())
.map_err(|e| {
miette::miette!(
"Database error: {e}"
)
})
.and_then(move |db| {
Task::perform(
add_video_to_db(
video.clone(),
db,
),
move |res| {
res.map(|_| {
Message::None
})
},
)
})
.map(|r| {
r.unwrap_or(Message::None)
});
tasks.push(task);
continue;
};
error!(?e);
}
ServiceItemKind::Image(image) => {
let Some(e) = self
.image_library
.add_item(image.clone())
.err()
else {
let task =
Task::future(self.db.acquire())
.map_err(|e| {
miette::miette!(
"Database error: {e}"
)
})
.and_then(move |db| {
Task::perform(
add_image_to_db(
image.clone(),
db,
),
move |res| {
res.map(|_| {
Message::None
})
},
)
})
.map(|r| {
r.unwrap_or(Message::None)
});
tasks.push(task);
continue;
};
error!(?e);
}
ServiceItemKind::Presentation(
presentation,
) => {
let Some(e) = self
.presentation_library
.add_item(presentation.clone())
.err()
else {
let task =
Task::future(self.db.acquire())
.map_err(|e| {
miette::miette!(
"Database error: {e}"
)
})
.and_then(move |db| {
Task::perform(
add_presentation_to_db(
presentation.clone(),
db,
),
{
move |res| {
res.map(|_| {
Message::None
})
}
},
)
})
.map(|r| {
r.unwrap_or(Message::None)
});
tasks.push(task);
continue;
};
error!(?e);
}
ServiceItemKind::Content(_slide) => todo!(),
}
}
return Action::Task(
Task::batch(tasks).chain(after_task),
);
}
}
Action::None
}
#[must_use]
pub fn view(&self) -> Element<Message> {
let cosmic::cosmic_theme::Spacing { space_s, .. } =
cosmic::theme::spacing();
let song_library = self.library_item(&self.song_library);
let image_library = self.library_item(&self.image_library);
let video_library = self.library_item(&self.video_library);
let presentation_library =
self.library_item(&self.presentation_library);
let library_column = column![
text::heading("Library").center().width(Length::Fill),
divider::horizontal::light(),
song_library,
image_library,
video_library,
presentation_library,
]
.height(Length::Fill)
.padding(10)
.spacing(space_s);
let library_dnd = dnd_destination(
library_column,
vec![
"image/png".into(),
"image/jpg".into(),
"image/heif".into(),
"image/gif".into(),
"video/mp4".into(),
"video/AV1".into(),
"video/H264".into(),
"video/H265".into(),
"video/mpeg".into(),
"video/mkv".into(),
"video/webm".into(),
"video/ogg".into(),
"video/vnd.youtube.yt".into(),
"video/x-matroska".into(),
"application/pdf".into(),
"text/html".into(),
"text/md".into(),
"text/org".into(),
"text/uri-list".into(),
],
)
.on_enter(|_, _, mimes| {
warn!(?mimes);
Message::None
})
.on_finish(|mime, data, _action, _, _| {
// warn!(?mime, ?data, ?action);
match mime.as_str() {
"text/uri-list" => {
let Ok(text) = str::from_utf8(&data) else {
return Message::None;
};
let mut items = Vec::new();
for line in text.lines() {
let Ok(url) = url::Url::parse(line) else {
error!(
?line,
"problem parsing this file url"
);
continue;
};
let Ok(path) = url.to_file_path() else {
error!(?url, "invalid file URL");
continue;
};
let item = ServiceItemKind::try_from(path);
match item {
Ok(item) => items.push(item),
Err(e) => error!(?e),
}
}
Message::AddFiles(items)
}
_ => Message::None,
}
});
container(library_dnd).padding(2).into()
}
#[allow(clippy::too_many_lines)]
pub fn library_item<T>(
&'a self,
model: &'a Model<T>,
) -> Element<'a, Message>
where
T: Content,
{
let mut row = row::with_capacity(5).spacing(5);
match &model.kind {
LibraryKind::Song => {
row = row
.push(icon::from_name("folder-music-symbolic"));
row = row
.push(textm!("Songs").align_y(Vertical::Center));
}
LibraryKind::Video => {
row = row
.push(icon::from_name("folder-videos-symbolic"));
row = row
.push(textm!("Videos").align_y(Vertical::Center));
}
LibraryKind::Image => {
row = row.push(icon::from_name(
"folder-pictures-symbolic",
));
row = row
.push(textm!("Images").align_y(Vertical::Center));
}
LibraryKind::Presentation => {
row = row.push(icon::from_name(
"x-office-presentation-symbolic",
));
row = row.push(
textm!("Presentations").align_y(Vertical::Center),
);
}
}
let item_count = model.items.len();
row = row.push(space::horizontal());
row = row
.push(textm!("{}", item_count).align_y(Vertical::Center));
row = row.push(
icon::from_name({
if self.library_open == Some(model.kind) {
"arrow-up"
} else {
"arrow-down"
}
})
.size(20),
);
let row_container =
Container::new(row.align_y(Vertical::Center))
.padding(5)
.style(|t| {
container::Style::default()
.background({
self.library_hovered.map_or_else(
|| {
Background::Color(
t.cosmic().button.base.into(),
)
},
|library| {
Background::Color(
if library == model.kind {
t.cosmic()
.button
.hover
.into()
} else {
t.cosmic()
.button
.base
.into()
},
)
},
)
})
.border(Border::default().rounded(
t.cosmic().corner_radii.radius_s,
))
})
.center_x(Length::Fill)
.center_y(Length::Shrink);
let library_button = mouse_area(row_container)
.on_press({
if self.library_open == Some(model.kind) {
Message::OpenLibrary(None)
} else {
Message::OpenLibrary(Some(model.kind))
}
})
.on_enter(Message::HoverLibrary(Some(model.kind)))
.on_exit(Message::HoverLibrary(None));
let lib_container = if self.library_open == Some(model.kind) {
let items = scrollable(
column({
model.items.iter().enumerate().map(
|(index, item)| {
let i32_index = i32::try_from(index).expect("shouldn't be negative");
let kind = model.kind;
let visual_item = self
.single_item(index, item, model)
.map(|()| Message::None);
DndSource::<Message, KindWrapper>::new({
let mouse_area = mouse_area(visual_item);
let mouse_area = mouse_area.on_enter(Message::HoverItem(
Some((
model.kind,
i32_index ,
)),
))
.on_double_click(
Message::OpenItem(Some((
model.kind,
i32_index,
))),
)
.on_right_press(Message::OpenContext(i32_index ))
.on_exit(Message::HoverItem(None))
.on_press(Message::SelectItem(
Some((
model.kind,
i32_index,
)),
));
Element::from(mouse_area)
})
.action(DndAction::Copy)
.drag_icon({
let model = model.kind;
move |i| {
let state = State::None;
let icon = match model {
LibraryKind::Song => icon::from_name(
"folder-music-symbolic",
).symbolic(true)
,
LibraryKind::Video => icon::from_name("folder-videos-symbolic"),
LibraryKind::Image => icon::from_name("folder-pictures-symbolic"),
LibraryKind::Presentation => icon::from_name("x-office-presentation-symbolic"),
};
(
icon.into(),
state,
i,
)
}})
.drag_content(move || {
KindWrapper((kind, i32_index))
})
.into()
},
)
})
.spacing(2)
.width(Length::Fill),
)
.spacing(5)
.height(Length::Fill);
let library_toolbar = rowm!(
text_input("Search...", ""),
button::icon(icon::from_name("add"))
.on_press(Message::AddItem)
);
let context_menu = self.context_menu(items.into());
let library_column =
column![library_toolbar, context_menu].spacing(3);
Container::new(library_column).padding(5)
} else {
Container::new(Space::new())
};
column![library_button, lib_container].into()
}
#[allow(clippy::too_many_lines)]
fn single_item<T>(
&'a self,
index: usize,
item: &'a T,
model: &'a Model<T>,
) -> Element<'a, ()>
where
T: Content,
{
let cosmic::cosmic_theme::Spacing {
space_xxs, space_s, ..
} = theme::spacing();
let text = Container::new(responsive(|size| {
text::heading(elide_text(item.title(), size.width))
.center()
.wrapping(textm::Wrapping::None)
.into()
}))
.center_y(20)
.center_x(Length::Fill);
let subtext = container(responsive(move |size| {
let color: Color = if item.background().is_some() {
if let Some(items) = &self.selected_items
&& items.contains(&(
model.kind,
i32::try_from(index)
.expect("Should never be negative"),
))
{
theme::active().cosmic().control_0().into()
} else {
theme::active()
.cosmic()
.accent_text_color()
.into()
}
} else if let Some(items) = &self.selected_items
&& items.contains(&(
model.kind,
i32::try_from(index)
.expect("Should never be negative"),
))
{
theme::active().cosmic().control_0().into()
} else {
theme::active()
.cosmic()
.destructive_text_color()
.into()
};
text::body(elide_text(item.subtext(), size.width))
.center()
.wrapping(textm::Wrapping::None)
.class(color)
.into()
}))
.center_y(20)
.center_x(Length::Fill);
let texts = column([text.into(), subtext.into()]);
Container::new(
rowm![horizontal().width(0), texts]
.spacing(10)
.align_y(Vertical::Center),
)
.width(Length::Fill)
.style(move |t| {
container::Style::default()
.background(Background::Color(
if let Some(items) = &self.selected_items
&& let Ok(index) = i32::try_from(index)
{
if items.contains(&(model.kind, index)) {
t.cosmic().accent.selected.into()
} else if let Some((library, hovered)) =
self.hovered_item
{
if model.kind == library
&& hovered == index
{
t.cosmic().button.hover.into()
} else {
t.cosmic().button.base.into()
}
} else {
t.cosmic().button.base.into()
}
} else if let Some((library, hovered)) =
self.hovered_item
&& let Ok(index) = i32::try_from(index)
{
if model.kind == library && hovered == index {
t.cosmic().button.hover.into()
} else {
t.cosmic().button.base.into()
}
} else {
t.cosmic().button.base.into()
},
))
.border(
Border::default()
.rounded(t.cosmic().corner_radii.radius_m),
)
})
.padding([space_xxs, space_s])
.into()
}
fn context_menu<'b>(
&self,
items: Element<'b, Message>,
) -> Element<'b, Message> {
if self.context_menu.is_some() {
let menu_items = vec![
menu::Item::Button("Open", None, MenuMessage::Open),
menu::Item::Button(
"Delete",
None,
MenuMessage::Delete,
),
];
let context_menu = context_menu(
items,
self.context_menu.map_or_else(
|| None,
|_| {
Some(menu::items(&self.menu_keys, menu_items))
},
),
);
Element::from(context_menu)
} else {
items
}
}
#[allow(clippy::unused_async)]
pub async fn search_items(
&self,
query: String,
) -> Vec<ServiceItemKind> {
let query = query.to_lowercase();
let items = self
.song_library
.items
.iter()
.filter(|song| song.title.to_lowercase().contains(&query))
.map(|song| ServiceItemKind::Song(song.clone()));
let videos = self
.video_library
.items
.iter()
.filter(|vid| vid.title.to_lowercase().contains(&query))
.map(|video| ServiceItemKind::Video(video.clone()));
let images = self
.image_library
.items
.iter()
.filter(|image| {
image.title.to_lowercase().contains(&query)
})
.map(|image| ServiceItemKind::Image(image.clone()));
let presentations = self
.presentation_library
.items
.iter()
.filter(|pres| pres.title.to_lowercase().contains(&query))
.map(|pres| ServiceItemKind::Presentation(pres.clone()));
let items = items.chain(videos);
let items = items.chain(images);
let items = items.chain(presentations);
let mut items: Vec<(usize, ServiceItemKind)> = items
.map(|item| {
(
levenshtein::distance(
query.bytes(),
item.title().bytes(),
),
item,
)
})
.collect();
items.sort_by_key(|a| a.0);
items.into_iter().map(|item| item.1).collect()
}
#[must_use]
pub fn get_video(&self, index: i32) -> Option<&Video> {
self.video_library.get_item(index)
}
#[must_use]
pub fn get_image(&self, index: i32) -> Option<&Image> {
self.image_library.get_item(index)
}
#[must_use]
pub fn get_presentation(
&self,
index: i32,
) -> Option<&Presentation> {
self.presentation_library.get_item(index)
}
pub const fn set_modifiers(
&mut self,
modifiers: Option<Modifiers>,
) {
self.modifiers_pressed = modifiers;
}
#[allow(clippy::too_many_lines)]
fn delete_items(&mut self) -> Action {
// Need to make this function collect tasks to be run off of
// who should be deleted
let Some(items) = self.selected_items.as_mut() else {
return Action::None;
};
items.sort_by_key(|(_, index)| *index);
let tasks: Vec<Task<Message>> = items
.iter()
.rev()
.map(|(kind, index)| match kind {
LibraryKind::Song => {
if let Some(song) =
self.song_library.get_item(*index)
{
let songs = self
.song_library
.items
.drain(..)
.collect();
Task::perform(
songs::remove_from_db(
Arc::clone(&self.db),
songs,
song.id,
),
|r| match r {
Ok(songs) => {
Message::ReaddSongs(songs)
}
Err(e) => {
error!(?e);
Message::None
}
},
)
} else {
Task::none()
}
}
LibraryKind::Video => {
if let Some(video) =
self.video_library.get_item(*index)
{
Task::perform(
self.video_library
.remove_video(video.id, &self.db),
|r| {
if let Err(e) = r {
error!(?e);
}
Message::None
},
)
.map(|m| Ok(m))
} else {
Task::none()
}
}
LibraryKind::Image => {
if let Some(image) =
self.image_library.get_item(*index)
{
debug!(
image.id,
image.title, "let's remove this image",
);
Task::perform(
self.image_library
.remove_image(image.id, &self.db),
|r| {
if let Err(e) = r {
error!(?e);
}
Message::None
},
)
.map(|m| Ok(m))
} else {
Task::none()
}
}
LibraryKind::Presentation => {
if let Some(presentation) =
self.presentation_library.get_item(*index)
{
Task::perform(
self.presentation_library
.remove_presentation(
presentation.id,
&self.db,
),
|r| {
if let Err(e) = r {
error!(?e);
}
Message::None
},
)
.map(|m| Ok(m))
} else {
Task::none()
}
}
})
.map(|t| {
t.map(
|r| if let Ok(r) = r { r } else { Message::None },
)
})
.collect();
if !tasks.is_empty() {
self.selected_items = None;
}
Action::Task(Task::batch(tasks))
}
}
async fn add_images() -> Option<Vec<Image>> {
let paths =
Dialog::new().title("pick image").open_files().await.ok()?;
Some(
paths
.urls()
.iter()
.map(|path| {
Image::from(path.to_file_path().expect("oops"))
})
.collect(),
)
}
async fn add_videos() -> Option<Vec<Video>> {
let paths =
Dialog::new().title("pick video").open_files().await.ok()?;
Some(
paths
.urls()
.iter()
.map(|path| {
Video::from(path.to_file_path().expect("oops"))
})
.collect(),
)
}
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> {
let mut data = dirs::data_local_dir()
.expect("Should always find a data dir");
data.push("lumina");
data.push("library-db.sqlite3");
let mut db_url = String::from("sqlite://");
db_url.push_str(
data.to_str().expect("Should always be a file here"),
);
SqlitePool::connect(&db_url).await.into_diagnostic()
}
#[allow(clippy::cast_sign_loss)]
#[allow(clippy::cast_precision_loss)]
#[allow(clippy::cast_possible_truncation)]
pub fn elide_text(text: impl AsRef<str>, width: f32) -> String {
const CHAR_SIZE: f32 = 8.0;
let text: String = text.as_ref().to_owned();
let text_length = text.len() as f32 * CHAR_SIZE;
if text_length > width {
format!(
"{}...",
if let Some((first, _second)) = text.split_at_checked(
((width / CHAR_SIZE) - 3.0).floor() as usize
) {
first
} else if let Some((first, _second)) = text
.split_at_checked(
((width / CHAR_SIZE) - 5.0).floor() as usize
)
{
first
} else if let Some((first, _second)) = text
.split_at_checked(
((width / CHAR_SIZE) - 7.0).floor() as usize
)
{
first
} else {
&text
}
)
} else {
text
}
}
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>!<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>!<EFBFBD><EFBFBD>r
<EFBFBD><EFBFBD>r
<EFBFBD><EFBFBD>r
x<EFBFBD>r
l<EFBFBD>r
q<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>A<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ؖ0y<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>g<EFBFBD>