feat(models): Created the StatefulTree struct for displaying seasons and episodes (and any other structured data) for the UI.
This commit is contained in:
Generated
+706
-441
File diff suppressed because it is too large
Load Diff
+6
-5
@@ -21,14 +21,14 @@ chrono = { version = "0.4.38", features = ["serde"] }
|
|||||||
confy = { version = "0.6.0", default-features = false, features = [
|
confy = { version = "0.6.0", default-features = false, features = [
|
||||||
"yaml_conf",
|
"yaml_conf",
|
||||||
] }
|
] }
|
||||||
crossterm = "0.27.0"
|
crossterm = "0.28.1"
|
||||||
derivative = "2.2.0"
|
derivative = "2.2.0"
|
||||||
human-panic = "1.1.3"
|
human-panic = "2.0.2"
|
||||||
indoc = "2.0.0"
|
indoc = "2.0.0"
|
||||||
log = "0.4.17"
|
log = "0.4.17"
|
||||||
log4rs = { version = "1.2.0", features = ["file_appender"] }
|
log4rs = { version = "1.2.0", features = ["file_appender"] }
|
||||||
regex = "1.11.1"
|
regex = "1.11.1"
|
||||||
reqwest = { version = "0.11.14", features = ["json"] }
|
reqwest = { version = "0.12.9", features = ["json"] }
|
||||||
serde_yaml = "0.9.16"
|
serde_yaml = "0.9.16"
|
||||||
serde_json = "1.0.91"
|
serde_json = "1.0.91"
|
||||||
serde = { version = "1.0.214", features = ["derive"] }
|
serde = { version = "1.0.214", features = ["derive"] }
|
||||||
@@ -36,7 +36,7 @@ strum = { version = "0.26.3", features = ["derive"] }
|
|||||||
strum_macros = "0.26.4"
|
strum_macros = "0.26.4"
|
||||||
tokio = { version = "1.36.0", features = ["full"] }
|
tokio = { version = "1.36.0", features = ["full"] }
|
||||||
tokio-util = "0.7.8"
|
tokio-util = "0.7.8"
|
||||||
ratatui = { version = "0.28.0", features = ["all-widgets"] }
|
ratatui = { version = "0.29.0", features = ["all-widgets"] }
|
||||||
urlencoding = "2.1.2"
|
urlencoding = "2.1.2"
|
||||||
clap = { version = "4.5.20", features = ["derive", "cargo", "env"] }
|
clap = { version = "4.5.20", features = ["derive", "cargo", "env"] }
|
||||||
clap_complete = "4.5.33"
|
clap_complete = "4.5.33"
|
||||||
@@ -45,13 +45,14 @@ ctrlc = "3.4.5"
|
|||||||
colored = "2.1.0"
|
colored = "2.1.0"
|
||||||
async-trait = "0.1.83"
|
async-trait = "0.1.83"
|
||||||
dirs-next = "2.0.0"
|
dirs-next = "2.0.0"
|
||||||
|
managarr-tree-widget = { git = "https://github.com/Dark-Alex-17/managarr-tree-widget.git" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
assert_cmd = "2.0.16"
|
assert_cmd = "2.0.16"
|
||||||
mockall = "0.13.0"
|
mockall = "0.13.0"
|
||||||
mockito = "1.0.0"
|
mockito = "1.0.0"
|
||||||
pretty_assertions = "1.3.0"
|
pretty_assertions = "1.3.0"
|
||||||
rstest = "0.18.2"
|
rstest = "0.23.0"
|
||||||
|
|
||||||
[dev-dependencies.cargo-husky]
|
[dev-dependencies.cargo-husky]
|
||||||
version = "1"
|
version = "1"
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ pub mod servarr_data;
|
|||||||
pub mod sonarr_models;
|
pub mod sonarr_models;
|
||||||
pub mod stateful_list;
|
pub mod stateful_list;
|
||||||
pub mod stateful_table;
|
pub mod stateful_table;
|
||||||
|
pub mod stateful_tree;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[path = "model_tests.rs"]
|
#[path = "model_tests.rs"]
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
use managarr_tree_widget::{TreeItem, TreeState};
|
||||||
|
use ratatui::text::ToText;
|
||||||
|
|
||||||
|
use super::Scrollable;
|
||||||
|
use core::hash::Hash;
|
||||||
|
use std::fmt::{Debug, Display};
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "stateful_tree_tests.rs"]
|
||||||
|
mod stateful_tree_tests;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct StatefulTree<T>
|
||||||
|
where
|
||||||
|
T: ToText + Hash + Clone + PartialEq + Eq + Debug + Default + Display,
|
||||||
|
{
|
||||||
|
pub state: TreeState,
|
||||||
|
pub items: Vec<TreeItem<T>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> StatefulTree<T>
|
||||||
|
where
|
||||||
|
T: ToText + Hash + Clone + PartialEq + Eq + Debug + Default + Display,
|
||||||
|
{
|
||||||
|
pub fn set_items(&mut self, items: Vec<TreeItem<T>>) {
|
||||||
|
self.items = items;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current_selection(&self) -> Option<&T> {
|
||||||
|
self
|
||||||
|
.state
|
||||||
|
.flatten(&self.items)
|
||||||
|
.into_iter()
|
||||||
|
.find(|i| self.state.selected() == i.identifier)
|
||||||
|
.map(|item| item.item.content())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.items.is_empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Scrollable for StatefulTree<T>
|
||||||
|
where
|
||||||
|
T: ToText + Hash + Clone + PartialEq + Eq + Debug + Default + Display,
|
||||||
|
{
|
||||||
|
fn scroll_down(&mut self) {
|
||||||
|
self.state.key_down();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scroll_up(&mut self) {
|
||||||
|
self.state.key_up();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scroll_to_top(&mut self) {
|
||||||
|
self.state.select_first();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scroll_to_bottom(&mut self) {
|
||||||
|
self.state.select_last();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||||
|
|
||||||
|
use crate::models::stateful_tree::StatefulTree;
|
||||||
|
use managarr_tree_widget::{Tree, TreeItem, TreeState};
|
||||||
|
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||||
|
use ratatui::buffer::Buffer;
|
||||||
|
use ratatui::layout::Rect;
|
||||||
|
use ratatui::widgets::StatefulWidget;
|
||||||
|
use crate::models::Scrollable;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_stateful_tree_scrolling_on_empty_tree_performs_no_op() {
|
||||||
|
let mut stateful_tree: StatefulTree<&str> = StatefulTree::default();
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
stateful_tree.state.key_down();
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
|
||||||
|
assert_eq!(stateful_tree.state.selected(), Vec::<u64>::new());
|
||||||
|
|
||||||
|
stateful_tree.scroll_up();
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
|
||||||
|
assert_eq!(stateful_tree.state.selected(), Vec::<u64>::new());
|
||||||
|
|
||||||
|
stateful_tree.scroll_down();
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
|
||||||
|
assert_eq!(stateful_tree.state.selected(), Vec::<u64>::new());
|
||||||
|
|
||||||
|
stateful_tree.scroll_to_top();
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
|
||||||
|
assert_eq!(stateful_tree.state.selected(), Vec::<u64>::new());
|
||||||
|
|
||||||
|
stateful_tree.scroll_to_bottom();
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_stateful_tree_scroll() {
|
||||||
|
let mut stateful_tree = create_test_stateful_tree();
|
||||||
|
let hash = |s: &str| {
|
||||||
|
let mut hasher = DefaultHasher::new();
|
||||||
|
s.hash(&mut hasher);
|
||||||
|
hasher.finish()
|
||||||
|
};
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
stateful_tree.scroll_down();
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
|
||||||
|
assert_eq!(stateful_tree.state.selected(), &[hash("Test 1")]);
|
||||||
|
|
||||||
|
stateful_tree.scroll_down();
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
|
||||||
|
assert_eq!(stateful_tree.state.selected(), &[hash("Test 2")]);
|
||||||
|
|
||||||
|
stateful_tree.scroll_down();
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
|
||||||
|
assert_eq!(stateful_tree.state.selected(), &[hash("Test 3")]);
|
||||||
|
|
||||||
|
stateful_tree.scroll_down();
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
|
||||||
|
assert_eq!(stateful_tree.state.selected(), &[hash("Test 3")]);
|
||||||
|
|
||||||
|
stateful_tree.scroll_up();
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
|
||||||
|
assert_eq!(stateful_tree.state.selected(), &[hash("Test 2")]);
|
||||||
|
|
||||||
|
stateful_tree.scroll_up();
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
|
||||||
|
assert_eq!(stateful_tree.state.selected(), &[hash("Test 1")]);
|
||||||
|
|
||||||
|
stateful_tree.scroll_to_bottom();
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
|
||||||
|
assert_eq!(stateful_tree.state.selected(), &[hash("Test 3")]);
|
||||||
|
|
||||||
|
stateful_tree.scroll_to_top();
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
|
||||||
|
assert_eq!(stateful_tree.state.selected(), &[hash("Test 1")]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_stateful_tree_set_items() {
|
||||||
|
let items_vec = vec![
|
||||||
|
TreeItem::new_leaf("Test 1"),
|
||||||
|
TreeItem::new_leaf("Test 2"),
|
||||||
|
TreeItem::new_leaf("Test 3"),
|
||||||
|
];
|
||||||
|
let hash = |s: &str| {
|
||||||
|
let mut hasher = DefaultHasher::new();
|
||||||
|
s.hash(&mut hasher);
|
||||||
|
hasher.finish()
|
||||||
|
};
|
||||||
|
let mut stateful_tree: StatefulTree<&str> = StatefulTree::default();
|
||||||
|
|
||||||
|
stateful_tree.set_items(items_vec.clone());
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
stateful_tree.state.key_down();
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
|
||||||
|
assert_eq!(stateful_tree.state.selected(), &[hash("Test 1")]);
|
||||||
|
|
||||||
|
stateful_tree.state.key_down();
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
stateful_tree.set_items(items_vec.clone());
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
|
||||||
|
assert_eq!(stateful_tree.state.selected(), &[hash("Test 2")]);
|
||||||
|
|
||||||
|
stateful_tree.state.key_down();
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
stateful_tree.set_items(items_vec);
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
|
||||||
|
assert_eq!(stateful_tree.state.selected(), &[hash("Test 3")]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_stateful_tree_current_selection() {
|
||||||
|
let mut stateful_tree = create_test_stateful_tree();
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
stateful_tree.state.key_down();
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
|
||||||
|
let current_selection = stateful_tree.current_selection();
|
||||||
|
|
||||||
|
assert!(current_selection.is_some());
|
||||||
|
assert_str_eq!(current_selection.unwrap(), stateful_tree.items[0].content());
|
||||||
|
|
||||||
|
stateful_tree.state.key_down();
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
let current_selection = stateful_tree.current_selection();
|
||||||
|
|
||||||
|
assert!(current_selection.is_some());
|
||||||
|
assert_str_eq!(current_selection.unwrap(), stateful_tree.items[1].content());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_stateful_tree_is_empty() {
|
||||||
|
let mut stateful_tree = create_test_stateful_tree();
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
|
||||||
|
assert!(!stateful_tree.is_empty());
|
||||||
|
|
||||||
|
stateful_tree = StatefulTree::default();
|
||||||
|
render(&mut stateful_tree.state, &stateful_tree.items);
|
||||||
|
|
||||||
|
assert!(stateful_tree.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(state: &mut TreeState, items: &[TreeItem<&str>]) {
|
||||||
|
let tree = Tree::new(items).unwrap();
|
||||||
|
let area = Rect::new(0, 0, 10, 4);
|
||||||
|
let mut buffer = Buffer::empty(area);
|
||||||
|
StatefulWidget::render(tree, area, &mut buffer, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_test_stateful_tree() -> StatefulTree<&'static str> {
|
||||||
|
let mut stateful_tree = StatefulTree::default();
|
||||||
|
stateful_tree.set_items(vec![
|
||||||
|
TreeItem::new_leaf("Test 1"),
|
||||||
|
TreeItem::new_leaf("Test 2"),
|
||||||
|
TreeItem::new_leaf("Test 3"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
stateful_tree
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -146,7 +146,7 @@ where
|
|||||||
|
|
||||||
if self.highlight_rows {
|
if self.highlight_rows {
|
||||||
table = table
|
table = table
|
||||||
.highlight_style(Style::new().highlight())
|
.row_highlight_style(Style::new().highlight())
|
||||||
.highlight_symbol(HIGHLIGHT_SYMBOL);
|
.highlight_symbol(HIGHLIGHT_SYMBOL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user