303 lines
8.1 KiB
Rust
303 lines
8.1 KiB
Rust
use crate::app::App;
|
|
use crate::models::servarr_models::{DiskSpace, RootFolder};
|
|
use crate::ui::THEME;
|
|
use crate::ui::styles::{
|
|
ManagarrStyle, default_style, failure_style, primary_style, secondary_style,
|
|
system_function_style,
|
|
};
|
|
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
|
|
use ratatui::style::{Style, Stylize};
|
|
use ratatui::text::{Line, Span, Text};
|
|
use ratatui::widgets::{Block, BorderType, Borders, LineGauge, ListItem, Paragraph, Wrap};
|
|
use std::collections::{HashMap, HashSet};
|
|
use std::path::{Path, PathBuf};
|
|
|
|
#[cfg(test)]
|
|
#[path = "utils_tests.rs"]
|
|
mod utils_tests;
|
|
|
|
pub fn background_block<'a>() -> Block<'a> {
|
|
THEME.with(|theme| {
|
|
let background = theme.get().background.unwrap();
|
|
|
|
if background.enabled.unwrap() {
|
|
Block::new().white().bg(background.color.unwrap())
|
|
} else {
|
|
Block::new().white()
|
|
}
|
|
})
|
|
}
|
|
|
|
pub fn layout_block<'a>() -> Block<'a> {
|
|
Block::new()
|
|
.borders(Borders::ALL)
|
|
.border_type(BorderType::Rounded)
|
|
}
|
|
|
|
pub fn layout_block_with_title(title_span: Span<'_>) -> Block<'_> {
|
|
layout_block().title(title_span)
|
|
}
|
|
|
|
pub fn layout_block_top_border_with_title(title_span: Span<'_>) -> Block<'_> {
|
|
layout_block_top_border().title(title_span)
|
|
}
|
|
|
|
pub fn layout_block_top_border<'a>() -> Block<'a> {
|
|
Block::new().borders(Borders::TOP).default_color()
|
|
}
|
|
|
|
pub fn layout_block_bottom_border<'a>() -> Block<'a> {
|
|
Block::new().borders(Borders::BOTTOM).default_color()
|
|
}
|
|
|
|
pub fn layout_paragraph_borderless(string: &str) -> Paragraph<'_> {
|
|
Paragraph::new(Text::from(string))
|
|
.block(borderless_block())
|
|
.primary()
|
|
.bold()
|
|
.wrap(Wrap { trim: false })
|
|
.centered()
|
|
}
|
|
|
|
pub fn borderless_block<'a>() -> Block<'a> {
|
|
Block::new().default_color()
|
|
}
|
|
|
|
pub fn style_block_highlight(is_selected: bool) -> Style {
|
|
if is_selected {
|
|
system_function_style().bold()
|
|
} else {
|
|
default_style().bold()
|
|
}
|
|
}
|
|
|
|
pub fn title_style(title: &str) -> Span<'_> {
|
|
format!(" {title} ").bold()
|
|
}
|
|
|
|
pub fn unstyled_title_block(title: &str) -> Block<'_> {
|
|
layout_block_with_title(title_style(title))
|
|
}
|
|
|
|
pub fn title_block(title: &str) -> Block<'_> {
|
|
unstyled_title_block(title).default_color()
|
|
}
|
|
|
|
pub fn title_block_centered(title: &str) -> Block<'_> {
|
|
title_block(title).title_alignment(Alignment::Center)
|
|
}
|
|
|
|
pub fn logo_block<'a>() -> Block<'a> {
|
|
layout_block().default_color().title(Span::styled(
|
|
" Managarr - A Servarr management TUI ",
|
|
Style::new().magenta().bold().italic(),
|
|
))
|
|
}
|
|
|
|
pub fn centered_rect(percent_x: u16, percent_y: u16, area: Rect) -> Rect {
|
|
let [_, vertical_area, _] = Layout::vertical([
|
|
Constraint::Percentage((100 - percent_y) / 2),
|
|
Constraint::Percentage(percent_y),
|
|
Constraint::Percentage((100 - percent_y) / 2),
|
|
])
|
|
.areas(area);
|
|
|
|
let [_, horizontal_layout, _] = Layout::horizontal([
|
|
Constraint::Percentage((100 - percent_x) / 2),
|
|
Constraint::Percentage(percent_x),
|
|
Constraint::Percentage((100 - percent_x) / 2),
|
|
])
|
|
.areas(vertical_area);
|
|
|
|
horizontal_layout
|
|
}
|
|
|
|
pub fn line_gauge_with_title(title: &str, ratio: f64) -> LineGauge<'_> {
|
|
LineGauge::default()
|
|
.block(Block::new().title(title))
|
|
.filled_style(primary_style())
|
|
.filled_symbol("━")
|
|
.unfilled_symbol("━")
|
|
.ratio(ratio)
|
|
.label(Line::from(format!("{:.0}%", ratio * 100.0)))
|
|
}
|
|
|
|
pub fn line_gauge_with_label(title: &str, ratio: f64) -> LineGauge<'_> {
|
|
LineGauge::default()
|
|
.block(Block::new())
|
|
.filled_style(primary_style())
|
|
.filled_symbol("━")
|
|
.unfilled_symbol("━")
|
|
.ratio(ratio)
|
|
.label(Line::from(format!("{title}: {:.0}%", ratio * 100.0)))
|
|
}
|
|
|
|
pub fn get_width_from_percentage(area: Rect, percentage: u16) -> usize {
|
|
(area.width as f64 * (percentage as f64 / 100.0)) as usize
|
|
}
|
|
|
|
pub(super) fn style_log_list_item(list_item: ListItem<'_>, level: String) -> ListItem<'_> {
|
|
match level.to_lowercase().as_str() {
|
|
"trace" => list_item.gray(),
|
|
"debug" => list_item.blue(),
|
|
"info" => list_item.style(default_style()),
|
|
"warn" => list_item.style(secondary_style()),
|
|
"error" => list_item.style(failure_style()),
|
|
"fatal" => list_item.style(failure_style().bold()),
|
|
_ => list_item.style(default_style()),
|
|
}
|
|
}
|
|
|
|
pub(super) fn convert_to_minutes_hours_days(time: i64) -> String {
|
|
if time < 60 {
|
|
if time == 0 {
|
|
"now".to_owned()
|
|
} else if time == 1 {
|
|
format!("{time} minute")
|
|
} else {
|
|
format!("{time} minutes")
|
|
}
|
|
} else if time / 60 < 24 {
|
|
let hours = time / 60;
|
|
if hours == 1 {
|
|
format!("{hours} hour")
|
|
} else {
|
|
format!("{hours} hours")
|
|
}
|
|
} else {
|
|
let days = time / (60 * 24);
|
|
if days == 1 {
|
|
format!("{days} day")
|
|
} else {
|
|
format!("{days} days")
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(super) fn decorate_peer_style(seeders: u64, leechers: u64, text: Text<'_>) -> Text<'_> {
|
|
if seeders == 0 {
|
|
text.failure()
|
|
} else if seeders < leechers {
|
|
text.warning()
|
|
} else {
|
|
text.success()
|
|
}
|
|
}
|
|
|
|
pub(super) fn extract_monitored_root_folders(
|
|
app: &App<'_>,
|
|
root_folders: Vec<RootFolder>,
|
|
) -> Vec<RootFolder> {
|
|
let monitored_paths = app
|
|
.server_tabs
|
|
.get_active_config()
|
|
.as_ref()
|
|
.unwrap()
|
|
.monitored_storage_paths
|
|
.as_ref();
|
|
|
|
if let Some(monitored_paths) = monitored_paths
|
|
&& !monitored_paths.is_empty()
|
|
{
|
|
let monitored_paths: Vec<PathBuf> = monitored_paths.iter().map(PathBuf::from).collect();
|
|
|
|
let mut collapsed_folders: HashMap<PathBuf, (RootFolder, Vec<String>)> = HashMap::new();
|
|
let mut unmatched_folders: Vec<RootFolder> = Vec::new();
|
|
|
|
for root_folder in root_folders {
|
|
let root_path = Path::new(&root_folder.path);
|
|
|
|
let matching_monitored_path = monitored_paths
|
|
.iter()
|
|
.filter(|mp| root_path.starts_with(mp))
|
|
.max_by_key(|mp| mp.components().count());
|
|
|
|
if let Some(monitored_path) = matching_monitored_path {
|
|
let subfolder_name = root_path
|
|
.strip_prefix(monitored_path)
|
|
.ok()
|
|
.and_then(|p| p.components().next())
|
|
.map(|c| c.as_os_str().to_string_lossy().to_string())
|
|
.unwrap_or_default();
|
|
|
|
collapsed_folders
|
|
.entry(monitored_path.clone())
|
|
.and_modify(|(_, subfolders)| {
|
|
if !subfolder_name.is_empty() && !subfolders.contains(&subfolder_name) {
|
|
subfolders.push(subfolder_name.clone());
|
|
}
|
|
})
|
|
.or_insert_with(|| {
|
|
let subfolders = if subfolder_name.is_empty() {
|
|
vec![]
|
|
} else {
|
|
vec![subfolder_name]
|
|
};
|
|
(root_folder.clone(), subfolders)
|
|
});
|
|
} else {
|
|
unmatched_folders.push(root_folder);
|
|
}
|
|
}
|
|
|
|
let mut result: Vec<RootFolder> = collapsed_folders
|
|
.into_iter()
|
|
.map(|(monitored_path, (mut root_folder, mut subfolders))| {
|
|
subfolders.sort();
|
|
let path_str = monitored_path.to_string_lossy();
|
|
root_folder.path = if subfolders.is_empty() {
|
|
path_str.to_string()
|
|
} else {
|
|
format!(
|
|
"{}/[{}]",
|
|
path_str.trim_end_matches('/'),
|
|
subfolders.join(",")
|
|
)
|
|
};
|
|
root_folder
|
|
})
|
|
.collect();
|
|
|
|
result.extend(unmatched_folders);
|
|
result.sort_by(|a, b| a.path.cmp(&b.path));
|
|
result
|
|
} else {
|
|
root_folders
|
|
}
|
|
}
|
|
|
|
pub(super) fn extract_monitored_disk_space_vec(
|
|
app: &App<'_>,
|
|
disk_space_vec: Vec<DiskSpace>,
|
|
) -> Vec<DiskSpace> {
|
|
let monitored_paths = app
|
|
.server_tabs
|
|
.get_active_config()
|
|
.as_ref()
|
|
.unwrap()
|
|
.monitored_storage_paths
|
|
.as_ref();
|
|
if let Some(monitored_paths) = monitored_paths
|
|
&& !monitored_paths.is_empty()
|
|
{
|
|
let monitored: HashSet<&str> = monitored_paths.iter().map(|s| s.as_str()).collect();
|
|
let mut seen_paths = HashSet::new();
|
|
let mut filtered_disk_space_vec = Vec::with_capacity(disk_space_vec.len());
|
|
|
|
for ds in disk_space_vec {
|
|
match ds.path.as_deref() {
|
|
None => filtered_disk_space_vec.push(ds),
|
|
Some(p) => {
|
|
if monitored.contains(p) && seen_paths.insert(p.to_owned()) {
|
|
filtered_disk_space_vec.push(ds)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
filtered_disk_space_vec
|
|
} else {
|
|
disk_space_vec
|
|
}
|
|
}
|