[feat] filtering and work for sorting
Some checks failed
/ clippy (push) Failing after 26m51s
/ test (push) Failing after 8m58s

This commit is contained in:
Chris Cochrun 2026-05-29 14:33:20 -05:00
parent a05b918729
commit 4479a11dff
6 changed files with 275 additions and 88 deletions

View file

@ -1,4 +1,4 @@
use crate::core::model::Sort;
use crate::core::model::{Sort, SortDirection};
use crate::{Background, Slide, SlideBuilder, TextAlignment};
use super::content::Content;
@ -158,7 +158,7 @@ impl Model<Image> {
let mut model = Self {
items: vec![],
kind: LibraryKind::Image,
sorting_method: Sort::AccessTime,
sorting_method: Sort::AccessTime(SortDirection::Descending),
};
let mut db = db.acquire().await.expect("probs");
@ -188,12 +188,30 @@ impl Model<Image> {
pub fn sort(&mut self) {
match self.sorting_method {
Sort::AccessTime => {
Sort::AccessTime(SortDirection::Descending) => {
self.items.sort_by(|a, b| b.accessed_at.cmp(&a.accessed_at))
}
Sort::CreatedTime => todo!(),
Sort::Title => todo!(),
Sort::Secondary => todo!(),
Sort::AccessTime(SortDirection::Ascending) => {
self.items.sort_by(|a, b| a.accessed_at.cmp(&b.accessed_at))
}
Sort::Title(SortDirection::Descending) => {
self.items.sort_by(|a, b| b.title.cmp(&a.title))
}
Sort::Title(SortDirection::Ascending) => {
self.items.sort_by(|a, b| a.title.cmp(&b.title))
}
Sort::CreatedTime(SortDirection::Descending) => {
self.items.sort_by(|a, b| b.created_at.cmp(&a.created_at))
}
Sort::CreatedTime(SortDirection::Ascending) => {
self.items.sort_by(|a, b| a.created_at.cmp(&b.created_at))
}
Sort::Secondary(SortDirection::Descending) => {
self.items.sort_by(|a, b| b.path.cmp(&a.path))
}
Sort::Secondary(SortDirection::Ascending) => {
self.items.sort_by(|a, b| a.path.cmp(&b.path))
}
}
}
@ -327,7 +345,7 @@ mod test {
let mut image_model: Model<Image> = Model {
items: vec![],
kind: LibraryKind::Image,
sorting_method: Sort::AccessTime,
sorting_method: Sort::AccessTime(SortDirection::Descending),
};
let mut db = add_db()
.await
@ -350,7 +368,7 @@ mod test {
let mut image_model: Model<Image> = Model {
items: vec![],
kind: LibraryKind::Image,
sorting_method: Sort::AccessTime,
sorting_method: Sort::AccessTime(SortDirection::Descending),
};
let result = image_model.add_item(image.clone());
let new_image = test_image("A newer image".into());

View file

@ -26,10 +26,16 @@ pub enum LibraryKind {
#[derive(Debug, Clone, Eq, PartialEq, Copy, Serialize, Deserialize)]
pub enum Sort {
AccessTime,
CreatedTime,
Title,
Secondary, // This can be author or file name
AccessTime(SortDirection),
CreatedTime(SortDirection),
Title(SortDirection),
Secondary(SortDirection), // This can be author or file name
}
#[derive(Debug, Clone, Eq, PartialEq, Copy, Serialize, Deserialize)]
pub enum SortDirection {
Ascending,
Descending,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]

View file

@ -13,7 +13,7 @@ use std::path::{Path, PathBuf};
use std::sync::Arc;
use tracing::{debug, error};
use crate::core::model::Sort;
use crate::core::model::{Sort, SortDirection};
use crate::{Background, Slide, SlideBuilder, TextAlignment};
use super::content::Content;
@ -299,7 +299,7 @@ impl Model<Presentation> {
let mut model = Self {
items: vec![],
kind: LibraryKind::Presentation,
sorting_method: Sort::AccessTime,
sorting_method: Sort::AccessTime(SortDirection::Descending),
};
model.load_from_db(db).await;
model
@ -363,12 +363,30 @@ impl Model<Presentation> {
pub fn sort(&mut self) {
match self.sorting_method {
Sort::AccessTime => {
Sort::AccessTime(SortDirection::Descending) => {
self.items.sort_by(|a, b| b.accessed_at.cmp(&a.accessed_at))
}
Sort::CreatedTime => todo!(),
Sort::Title => todo!(),
Sort::Secondary => todo!(),
Sort::AccessTime(SortDirection::Ascending) => {
self.items.sort_by(|a, b| a.accessed_at.cmp(&b.accessed_at))
}
Sort::Title(SortDirection::Descending) => {
self.items.sort_by(|a, b| b.title.cmp(&a.title))
}
Sort::Title(SortDirection::Ascending) => {
self.items.sort_by(|a, b| a.title.cmp(&b.title))
}
Sort::CreatedTime(SortDirection::Descending) => {
self.items.sort_by(|a, b| b.created_at.cmp(&a.created_at))
}
Sort::CreatedTime(SortDirection::Ascending) => {
self.items.sort_by(|a, b| a.created_at.cmp(&b.created_at))
}
Sort::Secondary(SortDirection::Descending) => {
self.items.sort_by(|a, b| b.path.cmp(&a.path))
}
Sort::Secondary(SortDirection::Ascending) => {
self.items.sort_by(|a, b| a.path.cmp(&b.path))
}
}
}
@ -549,7 +567,7 @@ mod test {
let mut presentation_model: Model<Presentation> = Model {
items: vec![],
kind: LibraryKind::Presentation,
sorting_method: Sort::AccessTime,
sorting_method: Sort::AccessTime(SortDirection::Descending),
};
let db = Arc::new(add_db().await.expect("Getting db error"));
presentation_model.load_from_db(db).await;

View file

@ -19,7 +19,7 @@ use tracing::{debug, error};
use crate::core::content::Content;
use crate::core::kinds::ServiceItemKind;
use crate::core::model::{LibraryKind, Model, Sort};
use crate::core::model::{LibraryKind, Model, Sort, SortDirection};
use crate::core::service_items::ServiceTrait;
use crate::core::slide::{self, Background, TextAlignment};
use crate::ui::text_svg::{Color, Font, Stroke, shadow, stroke};
@ -667,7 +667,7 @@ impl Model<Song> {
let mut model = Self {
items: vec![],
kind: LibraryKind::Song,
sorting_method: Sort::AccessTime,
sorting_method: Sort::AccessTime(SortDirection::Descending),
};
model.load_from_db(db).await;
@ -702,12 +702,32 @@ impl Model<Song> {
pub fn sort(&mut self) {
match self.sorting_method {
Sort::AccessTime => {
Sort::AccessTime(SortDirection::Descending) => {
self.items.sort_by(|a, b| b.accessed_at.cmp(&a.accessed_at))
}
Sort::CreatedTime => todo!(),
Sort::Title => todo!(),
Sort::Secondary => todo!(),
Sort::AccessTime(SortDirection::Ascending) => {
self.items.sort_by(|a, b| a.accessed_at.cmp(&b.accessed_at))
}
Sort::Title(SortDirection::Descending) => {
self.items.sort_by(|a, b| b.title.cmp(&a.title))
}
Sort::Title(SortDirection::Ascending) => {
self.items.sort_by(|a, b| a.title.cmp(&b.title))
}
Sort::CreatedTime(SortDirection::Descending) => {
self.items.sort_by(|a, b| b.created_at.cmp(&a.created_at))
}
Sort::CreatedTime(SortDirection::Ascending) => {
self.items.sort_by(|a, b| a.created_at.cmp(&b.created_at))
}
Sort::Secondary(SortDirection::Descending) => {
self.items.sort_by(|a, b| b.author.cmp(&a.author))
}
Sort::Secondary(SortDirection::Ascending) => {
self.items.sort_by(|a, b| a.author.cmp(&b.author))
} // Sort::CreatedTime => todo!(),
// Sort::Title => todo!(),
// Sort::Secondary => todo!(),
}
}
@ -1315,7 +1335,7 @@ You saved my soul"
let song_model: Model<Song> = Model {
items: vec![],
kind: LibraryKind::Song,
sorting_method: Sort::AccessTime,
sorting_method: Sort::AccessTime(SortDirection::Descending),
// db: crate::core::model::get_db().await,
};
song_model

View file

@ -1,4 +1,4 @@
use crate::core::model::Sort;
use crate::core::model::{Sort, SortDirection};
use crate::{Background, SlideBuilder, TextAlignment};
use super::content::Content;
@ -184,7 +184,7 @@ impl Model<Video> {
let mut model = Self {
items: vec![],
kind: LibraryKind::Video,
sorting_method: Sort::AccessTime,
sorting_method: Sort::AccessTime(SortDirection::Descending),
};
model.load_from_db(db).await;
model
@ -206,12 +206,30 @@ impl Model<Video> {
pub fn sort(&mut self) {
match self.sorting_method {
Sort::AccessTime => {
Sort::AccessTime(SortDirection::Descending) => {
self.items.sort_by(|a, b| b.accessed_at.cmp(&a.accessed_at))
}
Sort::CreatedTime => todo!(),
Sort::Title => todo!(),
Sort::Secondary => todo!(),
Sort::AccessTime(SortDirection::Ascending) => {
self.items.sort_by(|a, b| a.accessed_at.cmp(&b.accessed_at))
}
Sort::Title(SortDirection::Descending) => {
self.items.sort_by(|a, b| b.title.cmp(&a.title))
}
Sort::Title(SortDirection::Ascending) => {
self.items.sort_by(|a, b| a.title.cmp(&b.title))
}
Sort::CreatedTime(SortDirection::Descending) => {
self.items.sort_by(|a, b| b.created_at.cmp(&a.created_at))
}
Sort::CreatedTime(SortDirection::Ascending) => {
self.items.sort_by(|a, b| a.created_at.cmp(&b.created_at))
}
Sort::Secondary(SortDirection::Descending) => {
self.items.sort_by(|a, b| b.path.cmp(&a.path))
}
Sort::Secondary(SortDirection::Ascending) => {
self.items.sort_by(|a, b| a.path.cmp(&b.path))
}
}
}
@ -353,7 +371,7 @@ mod test {
let mut video_model: Model<Video> = Model {
items: vec![],
kind: LibraryKind::Video,
sorting_method: Sort::AccessTime,
sorting_method: Sort::AccessTime(SortDirection::Descending),
};
let db = Arc::new(add_db().await.expect(""));
video_model.load_from_db(db).await;
@ -371,7 +389,7 @@ mod test {
let mut video_model: Model<Video> = Model {
items: vec![],
kind: LibraryKind::Video,
sorting_method: Sort::AccessTime,
sorting_method: Sort::AccessTime(SortDirection::Descending),
};
let result = video_model.add_item(video.clone());
let new_video = test_video("A newer video".into());

View file

@ -30,7 +30,7 @@ use tracing::{debug, error, warn};
use crate::core::content::Content;
use crate::core::images::{self, Image};
use crate::core::kinds::ServiceItemKind;
use crate::core::model::{KindWrapper, LibraryKind, Model, Sort};
use crate::core::model::{KindWrapper, LibraryKind, Model, Sort, SortDirection};
use crate::core::presentations::{self, Presentation};
use crate::core::service_items::ServiceItem;
use crate::core::songs::{self, Song, insert_song};
@ -56,6 +56,10 @@ pub struct Library {
video_popup_input: String,
hovered_point: cosmic::iced::Point,
context_point: cosmic::iced::Point,
song_search_query: Option<String>,
image_search_query: Option<String>,
video_search_query: Option<String>,
presentation_search_query: Option<String>,
}
#[derive(Debug, Clone, PartialEq)]
@ -102,6 +106,7 @@ pub enum Message {
OpenLibrary(Option<LibraryKind>),
HoverItem(Option<(LibraryKind, i32)>),
SelectItem(Option<(LibraryKind, i32)>),
SearchLibrary(String),
DragItem(ServiceItem),
UpdateSong(Song),
SongChanged,
@ -152,16 +157,16 @@ impl<'a> Library {
Self {
song_library: Model::new_song_model(Arc::clone(&db))
.await
.set_sort(Sort::AccessTime),
.set_sort(Sort::AccessTime(SortDirection::Descending)),
image_library: Model::new_image_model(Arc::clone(&db))
.await
.set_sort(Sort::AccessTime),
.set_sort(Sort::AccessTime(SortDirection::Descending)),
video_library: Model::new_video_model(Arc::clone(&db))
.await
.set_sort(Sort::AccessTime),
.set_sort(Sort::AccessTime(SortDirection::Descending)),
presentation_library: Model::new_presentation_model(Arc::clone(&db))
.await
.set_sort(Sort::AccessTime),
.set_sort(Sort::AccessTime(SortDirection::Descending)),
library_open: None,
library_hovered: None,
selected_items: None,
@ -175,6 +180,10 @@ impl<'a> Library {
video_popup_input: String::new(),
hovered_point: Point::ORIGIN,
context_point: Point::ORIGIN,
song_search_query: None,
image_search_query: None,
video_search_query: None,
presentation_search_query: None,
}
}
@ -216,6 +225,37 @@ impl<'a> Library {
self.context_menu = None;
return self.delete_items();
}
Message::SearchLibrary(search) => match self.library_open {
Some(LibraryKind::Song) => {
self.song_search_query = if search.is_empty() {
None
} else {
Some(search)
}
}
Some(LibraryKind::Image) => {
self.image_search_query = if search.is_empty() {
None
} else {
Some(search)
}
}
Some(LibraryKind::Video) => {
self.video_search_query = if search.is_empty() {
None
} else {
Some(search)
}
}
Some(LibraryKind::Presentation) => {
self.presentation_search_query = if search.is_empty() {
None
} else {
Some(search)
}
}
None => (),
},
Message::ReaddSongs(songs) => {
self.song_library.items = songs;
}
@ -804,57 +844,99 @@ impl<'a> Library {
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);
let item = DndSource::<Message, KindWrapper>::new({
let mouse_area = mouse_area(visual_item);
let mouse_area = mouse_area
.on_move(|point| Message::HoverPoint(point))
.on_enter(Message::HoverItem(Some((
model.kind, i32_index,
))))
.on_double_click(Message::AccessItem(Some((
model.kind, i32_index,
))))
.on_right_press(Message::OpenContext(Some(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 = TreeState::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)
model
.items
.iter()
.enumerate()
.filter(|(_, item)| match &model.kind {
LibraryKind::Song => {
if let Some(search) = &self.song_search_query {
item.title()
.to_lowercase()
.contains(&search.to_lowercase())
} else {
true
}
}
LibraryKind::Video => {
if let Some(search) = &self.video_search_query {
item.title()
.to_lowercase()
.contains(&search.to_lowercase())
} else {
true
}
}
LibraryKind::Image => {
if let Some(search) = &self.image_search_query {
item.title()
.to_lowercase()
.contains(&search.to_lowercase())
} else {
true
}
}
LibraryKind::Presentation => {
if let Some(search) = &self.presentation_search_query {
item.title()
.to_lowercase()
.contains(&search.to_lowercase())
} else {
true
}
}
})
.drag_content(move || KindWrapper((kind, i32_index)));
.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);
self.context_menu(item.into(), kind, i32_index).into()
})
let item = DndSource::<Message, KindWrapper>::new({
let mouse_area = mouse_area(visual_item);
let mouse_area = mouse_area
.on_move(|point| Message::HoverPoint(point))
.on_enter(Message::HoverItem(Some((
model.kind, i32_index,
))))
.on_double_click(Message::AccessItem(Some((
model.kind, i32_index,
))))
.on_right_press(Message::OpenContext(Some(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 = TreeState::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)));
self.context_menu(item.into(), kind, i32_index).into()
})
})
.spacing(2)
.width(Length::Fill),
@ -862,8 +944,33 @@ impl<'a> Library {
.spacing(5)
.height(Length::Fill);
let search_bar = match &model.kind {
LibraryKind::Song => text_input(
"Search...",
self.song_search_query.clone().unwrap_or(String::new()),
)
.on_input(Message::SearchLibrary),
LibraryKind::Video => text_input(
"Search...",
self.video_search_query.clone().unwrap_or(String::new()),
)
.on_input(Message::SearchLibrary),
LibraryKind::Image => text_input(
"Search...",
self.image_search_query.clone().unwrap_or(String::new()),
)
.on_input(Message::SearchLibrary),
LibraryKind::Presentation => text_input(
"Search...",
self.presentation_search_query
.clone()
.unwrap_or(String::new()),
)
.on_input(Message::SearchLibrary),
};
let library_toolbar = rowm!(
text_input("Search...", ""),
search_bar,
button::icon(icon::from_name("list-add-symbolic"))
.icon_size(theme::spacing().space_l)
.on_press(Message::AddItem)