Skip to content

Commit

Permalink
Add filters for un/played and un/downloaded
Browse files Browse the repository at this point in the history
  • Loading branch information
jeff-hughes committed Oct 21, 2021
1 parent 8daa1b5 commit c672b56
Show file tree
Hide file tree
Showing 7 changed files with 182 additions and 45 deletions.
3 changes: 3 additions & 0 deletions config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ delete_all = [ "X" ]
remove = [ "r" ]
remove_all = [ "R" ]

filter_played = [ "1" ]
filter_downloaded = [ "2" ]

help = [ "?" ]
quit = [ "q" ]

Expand Down
4 changes: 4 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ pub struct KeybindingsFromToml {
pub delete_all: Option<Vec<String>>,
pub remove: Option<Vec<String>>,
pub remove_all: Option<Vec<String>>,
pub filter_played: Option<Vec<String>>,
pub filter_downloaded: Option<Vec<String>>,
pub help: Option<Vec<String>>,
pub quit: Option<Vec<String>>,
}
Expand Down Expand Up @@ -157,6 +159,8 @@ impl Config {
delete_all: None,
remove: None,
remove_all: None,
filter_played: None,
filter_downloaded: None,
help: None,
quit: None,
};
Expand Down
7 changes: 7 additions & 0 deletions src/keymap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ pub enum UserAction {
Remove,
RemoveAll,

FilterPlayed,
FilterDownloaded,

Help,
Quit,
}
Expand Down Expand Up @@ -87,6 +90,8 @@ impl Keybindings {
(config.delete_all, UserAction::DeleteAll),
(config.remove, UserAction::Remove),
(config.remove_all, UserAction::RemoveAll),
(config.filter_played, UserAction::FilterPlayed),
(config.filter_downloaded, UserAction::FilterDownloaded),
(config.help, UserAction::Help),
(config.quit, UserAction::Quit),
];
Expand Down Expand Up @@ -167,6 +172,8 @@ impl Keybindings {
(UserAction::DeleteAll, vec!["X".to_string()]),
(UserAction::Remove, vec!["r".to_string()]),
(UserAction::RemoveAll, vec!["R".to_string()]),
(UserAction::FilterPlayed, vec!["1".to_string()]),
(UserAction::FilterDownloaded, vec!["2".to_string()]),
(UserAction::Help, vec!["?".to_string()]),
(UserAction::Quit, vec!["q".to_string()]),
];
Expand Down
52 changes: 45 additions & 7 deletions src/main_controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use crate::feeds::{self, FeedMsg, PodcastFeed};
use crate::play_file;
use crate::threadpool::Threadpool;
use crate::types::*;
use crate::ui::{Ui, UiMsg};
use crate::ui::{FilterStatus, Filters, Ui, UiMsg};

/// Enum used for communicating with other threads.
#[allow(clippy::enum_variant_names)]
Expand Down Expand Up @@ -161,6 +161,8 @@ impl MainController {
self.remove_all_episodes(pod_id, delete_files)
}

Message::Ui(UiMsg::FilterChange(filters)) => self.update_filters(filters),

Message::Ui(UiMsg::Noop) => (),
}
}
Expand Down Expand Up @@ -248,9 +250,10 @@ impl MainController {
),
// get all of 'em!
None => {
pod_data = self.podcasts.map(|pod| {
PodcastFeed::new(Some(pod.id), pod.url.clone(), Some(pod.title.clone()))
})
pod_data = self.podcasts.map(
|pod| PodcastFeed::new(Some(pod.id), pod.url.clone(), Some(pod.title.clone())),
false,
)
}
}
for feed in pod_data.into_iter() {
Expand Down Expand Up @@ -682,14 +685,49 @@ impl MainController {
}

let mut podcast = self.podcasts.clone_podcast(pod_id).unwrap();
podcast.episodes.map(|ep| {
let _ = self.db.hide_episode(ep.id, true);
});
podcast.episodes.map(
|ep| {
let _ = self.db.hide_episode(ep.id, true);
},
false,
);
podcast.episodes = LockVec::new(Vec::new());
self.podcasts.replace(pod_id, podcast);

self.tx_to_ui
.send(MainMessage::UiUpdateMenus)
.expect("Thread messaging error");
}

/// Updates the user-selected filters to show only played/unplayed
/// or downloaded/not downloaded episodes.
pub fn update_filters(&self, filters: Filters) {
{
let brrw_map = self.podcasts.borrow_map();
for (_, pod) in brrw_map.iter() {
let new_filter = pod.episodes.filter_map(|ep| {
let play_filter = match filters.played {
FilterStatus::All => false,
FilterStatus::PositiveCases => !ep.is_played(),
FilterStatus::NegativeCases => ep.is_played(),
};
let download_filter = match filters.downloaded {
FilterStatus::All => false,
FilterStatus::PositiveCases => ep.path.is_none(),
FilterStatus::NegativeCases => ep.path.is_some(),
};
if !(play_filter | download_filter) {
return Some(ep.id);
} else {
return None;
}
});
let mut filtered_order = pod.episodes.borrow_filtered_order();
*filtered_order = new_filter;
}
}
self.tx_to_ui
.send(MainMessage::UiUpdateMenus)
.expect("Thread messaging error");
}
}
77 changes: 46 additions & 31 deletions src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ impl Podcast {
fn num_unplayed(&self) -> usize {
return self
.episodes
.map(|ep| !ep.is_played() as usize)
.map(|ep| !ep.is_played() as usize, false)
.iter()
.sum();
}
Expand Down Expand Up @@ -279,12 +279,22 @@ impl Menuable for NewEpisode {
/// Primarily, the LockVec is used to provide methods that abstract
/// away some of the logic necessary for borrowing and locking the
/// Arc<Mutex<_>>.
///
/// The data is structured in a way to allow for quick access both by
/// item ID (using a hash map), as well as by the order of an item in
/// the list (using a vector of the item IDs). The `order` vector
/// provides the full order of all the podcasts/episodes that are
/// present in the hash map; the `filtered_order` vector provides the
/// order only for the items that are currently filtered in, if the
/// user has set an active filter for played/unplayed or downloaded/
/// undownloaded.
#[derive(Debug)]
pub struct LockVec<T>
where T: Clone + Menuable
{
data: Arc<Mutex<HashMap<i64, T, BuildNoHashHasher<i64>>>>,
order: Arc<Mutex<Vec<i64>>>,
filtered_order: Arc<Mutex<Vec<i64>>>,
}

impl<T: Clone + Menuable> LockVec<T> {
Expand All @@ -300,7 +310,8 @@ impl<T: Clone + Menuable> LockVec<T> {

return LockVec {
data: Arc::new(Mutex::new(hm)),
order: Arc::new(Mutex::new(order)),
order: Arc::new(Mutex::new(order.clone())),
filtered_order: Arc::new(Mutex::new(order)),
};
}

Expand All @@ -314,17 +325,24 @@ impl<T: Clone + Menuable> LockVec<T> {
return self.order.lock().expect("Mutex error");
}

/// Lock the LockVec filtered order vector for reading/writing.
pub fn borrow_filtered_order(&self) -> MutexGuard<Vec<i64>> {
return self.filtered_order.lock().expect("Mutex error");
}

/// Lock the LockVec hashmap for reading/writing.
#[allow(clippy::type_complexity)]
pub fn borrow(
&self,
) -> (
MutexGuard<HashMap<i64, T, BuildNoHashHasher<i64>>>,
MutexGuard<Vec<i64>>,
MutexGuard<Vec<i64>>,
) {
return (
self.data.lock().expect("Mutex error"),
self.order.lock().expect("Mutex error"),
self.filtered_order.lock().expect("Mutex error"),
);
}

Expand All @@ -337,27 +355,36 @@ impl<T: Clone + Menuable> LockVec<T> {

/// Empty out and replace all the data in the LockVec.
pub fn replace_all(&self, data: Vec<T>) {
let (mut map, mut order) = self.borrow();
let (mut map, mut order, mut filtered_order) = self.borrow();
map.clear();
order.clear();
filtered_order.clear();
for i in data.into_iter() {
let id = i.get_id();
map.insert(i.get_id(), i);
order.push(id);
filtered_order.push(id);
}
}

/// Maps a closure to every element in the LockVec, in the same way
/// as an Iterator. However, to avoid issues with keeping the borrow
/// alive, the function returns a Vec of the collected results,
/// rather than an iterator.
pub fn map<B, F>(&self, mut f: F) -> Vec<B>
pub fn map<B, F>(&self, mut f: F, filtered: bool) -> Vec<B>
where F: FnMut(&T) -> B {
let (map, order) = self.borrow();
return order
.iter()
.map(|id| f(map.get(id).expect("Index error in LockVec")))
.collect();
let (map, order, filtered_order) = self.borrow();
if filtered {
return filtered_order
.iter()
.map(|id| f(map.get(id).expect("Index error in LockVec")))
.collect();
} else {
return order
.iter()
.map(|id| f(map.get(id).expect("Index error in LockVec")))
.collect();
}
}

/// Maps a closure to a single element in the LockVec, specified by
Expand All @@ -376,41 +403,28 @@ impl<T: Clone + Menuable> LockVec<T> {
/// this returns None.
pub fn map_single_by_index<B, F>(&self, index: usize, f: F) -> Option<B>
where F: FnOnce(&T) -> B {
let order = self.borrow_order();
let order = self.borrow_filtered_order();
return match order.get(index) {
Some(id) => self.map_single(*id, f),
None => None,
};
}

/// Maps a closure to every element in the LockVec, in the same way
/// as the `filter_map()` does on an Iterator, both mapping and
/// filtering, over a specified range.
/// Does not check if the range is valid!
/// However, to avoid issues with keeping the borrow
/// alive, the function returns a Vec of the collected results,
/// rather than an iterator.
// pub fn map_by_range<B, F>(&self, start: usize, end: usize, mut f: F) -> Vec<B>
// where F: FnMut(&T) -> Option<B> {
// let (map, order) = self.borrow();
// return (start..end)
// .into_iter()
// .filter_map(|id| {
// f(map
// .get(order.get(id).expect("Index error in LockVec"))
// .expect("Index error in LockVec"))
// })
// .collect();
// }

/// Maps a closure to every element in the LockVec, in the same way
/// as the `filter_map()` does on an Iterator, both mapping and
/// filtering. However, to avoid issues with keeping the borrow
/// alive, the function returns a Vec of the collected results,
/// rather than an iterator.
///
/// Note that the word "filter" in this sense represents the concept
/// from functional programming, providing a function that evaluates
/// items in the list and returns a boolean value. The word "filter"
/// is used elsewhere in the code to represent user-selected
/// filters to show only selected podcasts/episodes, but this is
/// *not* the sense of the word here.
pub fn filter_map<B, F>(&self, mut f: F) -> Vec<B>
where F: FnMut(&T) -> Option<B> {
let (map, order) = self.borrow();
let (map, order, _) = self.borrow();
return order
.iter()
.filter_map(|id| f(map.get(id).expect("Index error in LockVec")))
Expand All @@ -433,6 +447,7 @@ impl<T: Clone + Menuable> Clone for LockVec<T> {
return LockVec {
data: Arc::clone(&self.data),
order: Arc::clone(&self.order),
filtered_order: Arc::clone(&self.filtered_order),
};
}
}
Expand Down
8 changes: 4 additions & 4 deletions src/ui/menu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ impl<T: Clone + Menuable> Menu<T> {
self.selected = self.start_row;
}

let (map, order) = self.items.borrow();
let (map, _, order) = self.items.borrow();
if !order.is_empty() {
// update selected item if list has gotten shorter
let current_selected = self.get_menu_idx(self.selected);
Expand Down Expand Up @@ -288,7 +288,7 @@ impl Menu<Podcast> {
/// currently selected podcast.
pub fn get_episodes(&self) -> LockVec<Episode> {
let index = self.get_menu_idx(self.selected);
let (borrowed_map, borrowed_order) = self.items.borrow();
let (borrowed_map, _, borrowed_order) = self.items.borrow();
let pod_id = borrowed_order
.get(index)
.expect("Could not retrieve podcast.");
Expand Down Expand Up @@ -347,7 +347,7 @@ impl Menu<NewEpisode> {
/// selected; if all are selected already, only then will it convert
/// all to unselected.
pub fn select_all_items(&mut self) {
let all_selected = self.items.map(|ep| ep.selected).iter().all(|x| *x);
let all_selected = self.items.map(|ep| ep.selected, false).iter().all(|x| *x);
let changed =
self.change_item_selections((0..self.items.len()).collect(), Some(!all_selected));
if changed {
Expand All @@ -364,7 +364,7 @@ impl Menu<NewEpisode> {
fn change_item_selections(&mut self, indexes: Vec<usize>, selection: Option<bool>) -> bool {
let mut changed = false;
{
let (mut borrowed_map, borrowed_order) = self.items.borrow();
let (mut borrowed_map, borrowed_order, _) = self.items.borrow();
for idx in indexes {
if let Some(ep_id) = borrowed_order.get(idx) {
if let Entry::Occupied(mut ep) = borrowed_map.entry(*ep_id) {
Expand Down
Loading

0 comments on commit c672b56

Please sign in to comment.