Initial Radarr ui!
This commit is contained in:
@@ -11,6 +11,7 @@ chrono = { version = "0.4", features = ["serde"] }
|
|||||||
clap = { version = "4.0.30", features = ["help", "usage", "error-context", "derive"] }
|
clap = { version = "4.0.30", features = ["help", "usage", "error-context", "derive"] }
|
||||||
confy = { version = "0.5.1", default_features = false, features = ["yaml_conf"] }
|
confy = { version = "0.5.1", default_features = false, features = ["yaml_conf"] }
|
||||||
crossterm = "0.25.0"
|
crossterm = "0.25.0"
|
||||||
|
derivative = "2.2.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"] }
|
||||||
reqwest = { version = "0.11.13", features = ["json"] }
|
reqwest = { version = "0.11.13", features = ["json"] }
|
||||||
|
|||||||
+13
-3
@@ -9,17 +9,27 @@ macro_rules! generate_keybindings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
generate_keybindings! {
|
generate_keybindings! {
|
||||||
quit
|
quit,
|
||||||
|
up,
|
||||||
|
down
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct KeyBinding {
|
pub struct KeyBinding {
|
||||||
key: Key,
|
pub key: Key,
|
||||||
desc: &'static str
|
pub desc: &'static str
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const DEFAULT_KEYBINDINGS: KeyBindings = KeyBindings {
|
pub const DEFAULT_KEYBINDINGS: KeyBindings = KeyBindings {
|
||||||
quit: KeyBinding {
|
quit: KeyBinding {
|
||||||
key: Key::Char('q'),
|
key: Key::Char('q'),
|
||||||
desc: "Quit",
|
desc: "Quit",
|
||||||
|
},
|
||||||
|
up: KeyBinding {
|
||||||
|
key: Key::Up,
|
||||||
|
desc: "Scroll up"
|
||||||
|
},
|
||||||
|
down: KeyBinding {
|
||||||
|
key: Key::Down,
|
||||||
|
desc: "Scroll down"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
+67
-4
@@ -2,6 +2,7 @@ use log::error;
|
|||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::sync::mpsc::Sender;
|
use tokio::sync::mpsc::Sender;
|
||||||
|
use tui::widgets::TableState;
|
||||||
|
|
||||||
use crate::app::radarr::RadarrData;
|
use crate::app::radarr::RadarrData;
|
||||||
|
|
||||||
@@ -10,7 +11,6 @@ use super::network::RadarrEvent;
|
|||||||
pub(crate) mod key_binding;
|
pub(crate) mod key_binding;
|
||||||
pub mod radarr;
|
pub mod radarr;
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct App {
|
pub struct App {
|
||||||
network_tx: Option<Sender<RadarrEvent>>,
|
network_tx: Option<Sender<RadarrEvent>>,
|
||||||
pub client: Client,
|
pub client: Client,
|
||||||
@@ -39,15 +39,15 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reset(&mut self) {
|
pub fn reset_tick_count(&mut self) {
|
||||||
self.tick_count = 0;
|
self.tick_count = 0;
|
||||||
// self.data = Data::default();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn on_tick(&mut self) {
|
pub async fn on_tick(&mut self) {
|
||||||
if self.tick_count % self.tick_until_poll == 0 {
|
if self.tick_count % self.tick_until_poll == 0 {
|
||||||
self.dispatch(RadarrEvent::GetOverview).await;
|
self.dispatch(RadarrEvent::GetOverview).await;
|
||||||
self.dispatch(RadarrEvent::GetStatus).await;
|
self.dispatch(RadarrEvent::GetStatus).await;
|
||||||
|
self.dispatch(RadarrEvent::GetMovies).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.tick_count += 1;
|
self.tick_count += 1;
|
||||||
@@ -68,7 +68,7 @@ impl Default for App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
#[derive(Default)]
|
||||||
pub struct Data {
|
pub struct Data {
|
||||||
pub radarr_data: RadarrData,
|
pub radarr_data: RadarrData,
|
||||||
}
|
}
|
||||||
@@ -94,3 +94,66 @@ impl Default for RadarrConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct StatefulTable<T> {
|
||||||
|
pub state: TableState,
|
||||||
|
pub items: Vec<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Default for StatefulTable<T> {
|
||||||
|
fn default() -> StatefulTable<T> {
|
||||||
|
StatefulTable {
|
||||||
|
state: TableState::default(),
|
||||||
|
items: Vec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> StatefulTable<T> {
|
||||||
|
pub fn set_items(&mut self, items: Vec<T>) {
|
||||||
|
let items_len = items.len();
|
||||||
|
self.items = items;
|
||||||
|
if !self.items.is_empty() {
|
||||||
|
let selected_row = self.state.selected().map_or(0, |i| {
|
||||||
|
if i > 0 && i < items_len {
|
||||||
|
i
|
||||||
|
} else if i >= items_len {
|
||||||
|
items_len - 1
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
});
|
||||||
|
self.state.select(Some(selected_row));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scroll_down(&mut self) {
|
||||||
|
let selected_row = match self.state.selected() {
|
||||||
|
Some(i) => {
|
||||||
|
if i >= self.items.len() - 1 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => 0
|
||||||
|
};
|
||||||
|
|
||||||
|
self.state.select(Some(selected_row));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scroll_up(&mut self) {
|
||||||
|
let selected_row = match self.state.selected() {
|
||||||
|
Some(i) => {
|
||||||
|
if i == 0 {
|
||||||
|
self.items.len() - 1
|
||||||
|
} else {
|
||||||
|
i - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => 0
|
||||||
|
};
|
||||||
|
|
||||||
|
self.state.select(Some(selected_row));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+6
-2
@@ -1,9 +1,13 @@
|
|||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
use crate::app::StatefulTable;
|
||||||
|
use crate::network::radarr::Movie;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
pub struct RadarrData {
|
pub struct RadarrData {
|
||||||
pub free_space: u64,
|
pub free_space: u64,
|
||||||
pub total_space: u64,
|
pub total_space: u64,
|
||||||
pub version: String,
|
pub version: String,
|
||||||
pub start_time: DateTime<Utc>
|
pub start_time: DateTime<Utc>,
|
||||||
|
pub movies: StatefulTable<Movie>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ use crossterm::event::{KeyCode, KeyEvent};
|
|||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub enum Key {
|
pub enum Key {
|
||||||
|
Up,
|
||||||
|
Down,
|
||||||
Char(char),
|
Char(char),
|
||||||
Unknown,
|
Unknown,
|
||||||
}
|
}
|
||||||
@@ -21,6 +23,14 @@ impl Display for Key {
|
|||||||
impl From<KeyEvent> for Key {
|
impl From<KeyEvent> for Key {
|
||||||
fn from(key_event: KeyEvent) -> Self {
|
fn from(key_event: KeyEvent) -> Self {
|
||||||
match key_event {
|
match key_event {
|
||||||
|
KeyEvent {
|
||||||
|
code: KeyCode::Up,
|
||||||
|
..
|
||||||
|
} => Key::Up,
|
||||||
|
KeyEvent {
|
||||||
|
code: KeyCode::Down,
|
||||||
|
..
|
||||||
|
} => Key::Down,
|
||||||
KeyEvent {
|
KeyEvent {
|
||||||
code: KeyCode::Char(c),
|
code: KeyCode::Char(c),
|
||||||
..
|
..
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
use crate::app::App;
|
||||||
|
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||||
|
use crate::event::Key;
|
||||||
|
|
||||||
|
pub async fn handle_key_events(key: Key, app: &mut App) {
|
||||||
|
match key {
|
||||||
|
_ if key == DEFAULT_KEYBINDINGS.up.key => handle_scroll_up(app).await,
|
||||||
|
_ if key == DEFAULT_KEYBINDINGS.down.key => handle_scroll_down(app).await,
|
||||||
|
_ => ()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_scroll_up(app: &mut App) {
|
||||||
|
app.data.radarr_data.movies.scroll_up();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_scroll_down(app: &mut App) {
|
||||||
|
app.data.radarr_data.movies.scroll_down();
|
||||||
|
}
|
||||||
+7
-7
@@ -8,13 +8,11 @@ use crossterm::terminal::{
|
|||||||
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
|
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
|
||||||
};
|
};
|
||||||
use log::{debug, info};
|
use log::{debug, info};
|
||||||
use tokio::sync::mpsc::Receiver;
|
|
||||||
use tokio::sync::{mpsc, Mutex};
|
use tokio::sync::{mpsc, Mutex};
|
||||||
|
use tokio::sync::mpsc::Receiver;
|
||||||
use tui::backend::CrosstermBackend;
|
use tui::backend::CrosstermBackend;
|
||||||
use tui::Terminal;
|
use tui::Terminal;
|
||||||
|
|
||||||
use utils::init_logging_config;
|
|
||||||
|
|
||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::event::input_event::{Events, InputEvent};
|
use crate::event::input_event::{Events, InputEvent};
|
||||||
use crate::event::Key;
|
use crate::event::Key;
|
||||||
@@ -35,7 +33,7 @@ struct Cli {}
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
log4rs::init_config(init_logging_config())?;
|
log4rs::init_config(utils::init_logging_config())?;
|
||||||
Cli::parse();
|
Cli::parse();
|
||||||
|
|
||||||
let config = confy::load("managarr", "config")?;
|
let config = confy::load("managarr", "config")?;
|
||||||
@@ -50,7 +48,7 @@ async fn main() -> Result<()> {
|
|||||||
info!("Checking if Radarr server is up and running...");
|
info!("Checking if Radarr server is up and running...");
|
||||||
app.lock().await.dispatch(RadarrEvent::HealthCheck).await;
|
app.lock().await.dispatch(RadarrEvent::HealthCheck).await;
|
||||||
|
|
||||||
simple_ui(&app).await?;
|
start_ui(&app).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -65,7 +63,7 @@ async fn start_networking(mut network_rx: Receiver<RadarrEvent>, app: &Arc<Mutex
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn simple_ui(app: &Arc<Mutex<App>>) -> Result<()> {
|
async fn start_ui(app: &Arc<Mutex<App>>) -> Result<()> {
|
||||||
let mut stdout = io::stdout();
|
let mut stdout = io::stdout();
|
||||||
enable_raw_mode()?;
|
enable_raw_mode()?;
|
||||||
|
|
||||||
@@ -79,13 +77,15 @@ async fn simple_ui(app: &Arc<Mutex<App>>) -> Result<()> {
|
|||||||
|
|
||||||
loop {
|
loop {
|
||||||
let mut app = app.lock().await;
|
let mut app = app.lock().await;
|
||||||
terminal.draw(|f| ui(f, &app))?;
|
terminal.draw(|f| ui(f, &mut app))?;
|
||||||
|
|
||||||
match input_events.next()? {
|
match input_events.next()? {
|
||||||
InputEvent::KeyEvent(key) => {
|
InputEvent::KeyEvent(key) => {
|
||||||
if key == Key::Char('q') {
|
if key == Key::Char('q') {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handlers::handle_key_events(key, &mut app).await;
|
||||||
}
|
}
|
||||||
InputEvent::Tick => app.on_tick().await,
|
InputEvent::Tick => app.on_tick().await,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ use tokio::sync::Mutex;
|
|||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
|
|
||||||
pub(crate) mod radarr;
|
pub(crate) mod radarr;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq, Hash)]
|
#[derive(Debug, Eq, PartialEq, Hash)]
|
||||||
pub enum RadarrEvent {
|
pub enum RadarrEvent {
|
||||||
HealthCheck,
|
HealthCheck,
|
||||||
GetOverview,
|
GetOverview,
|
||||||
GetStatus,
|
GetStatus,
|
||||||
|
GetMovies,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Network<'a> {
|
pub struct Network<'a> {
|
||||||
|
|||||||
+67
-56
@@ -1,11 +1,14 @@
|
|||||||
use anyhow::Result;
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
use derivative::Derivative;
|
||||||
use log::{debug, error};
|
use log::{debug, error};
|
||||||
use reqwest::RequestBuilder;
|
use reqwest::RequestBuilder;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::de::DeserializeOwned;
|
||||||
|
use serde::Deserialize;
|
||||||
use serde_json::Number;
|
use serde_json::Number;
|
||||||
use crate::app::RadarrConfig;
|
use tokio::sync::MutexGuard;
|
||||||
use crate::network::{Network, RadarrEvent};
|
|
||||||
|
use crate::app::{App, RadarrConfig};
|
||||||
|
use crate::network::{Network, RadarrEvent, utils};
|
||||||
|
|
||||||
impl RadarrEvent {
|
impl RadarrEvent {
|
||||||
const fn resource(self) -> &'static str {
|
const fn resource(self) -> &'static str {
|
||||||
@@ -13,11 +16,12 @@ impl RadarrEvent {
|
|||||||
RadarrEvent::HealthCheck => "/health",
|
RadarrEvent::HealthCheck => "/health",
|
||||||
RadarrEvent::GetOverview => "/diskspace",
|
RadarrEvent::GetOverview => "/diskspace",
|
||||||
RadarrEvent::GetStatus => "/system/status",
|
RadarrEvent::GetStatus => "/system/status",
|
||||||
|
RadarrEvent::GetMovies => "/movie",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct DiskSpace {
|
struct DiskSpace {
|
||||||
pub path: String,
|
pub path: String,
|
||||||
@@ -33,41 +37,30 @@ struct SystemStatus {
|
|||||||
start_time: DateTime<Utc>,
|
start_time: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Derivative, Deserialize, Debug)]
|
||||||
|
#[derivative(Default)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Movie {
|
||||||
|
#[derivative(Default(value = "Number::from(0)"))]
|
||||||
|
pub id: Number,
|
||||||
|
pub title: String,
|
||||||
|
#[derivative(Default(value = "Number::from(0)"))]
|
||||||
|
pub year: Number,
|
||||||
|
pub monitored: bool,
|
||||||
|
pub has_file: bool,
|
||||||
|
}
|
||||||
|
|
||||||
impl<'a> Network<'a> {
|
impl<'a> Network<'a> {
|
||||||
pub async fn handle_radarr_event(&self, radarr_event: RadarrEvent) {
|
pub async fn handle_radarr_event(&self, radarr_event: RadarrEvent) {
|
||||||
match radarr_event {
|
match radarr_event {
|
||||||
RadarrEvent::HealthCheck => {
|
RadarrEvent::HealthCheck => self.healthcheck(RadarrEvent::HealthCheck.resource()).await,
|
||||||
self.healthcheck(RadarrEvent::HealthCheck.resource()).await;
|
RadarrEvent::GetOverview => self.diskspace(RadarrEvent::GetOverview.resource()).await,
|
||||||
}
|
RadarrEvent::GetStatus => self.status(RadarrEvent::GetStatus.resource()).await,
|
||||||
RadarrEvent::GetOverview => match self.diskspace(RadarrEvent::GetOverview.resource()).await {
|
RadarrEvent::GetMovies => self.movies(RadarrEvent::GetMovies.resource()).await
|
||||||
Ok(disk_space_vec) => {
|
|
||||||
let mut app = self.app.lock().await;
|
|
||||||
let DiskSpace {
|
|
||||||
free_space,
|
|
||||||
total_space,
|
|
||||||
..
|
|
||||||
} = &disk_space_vec[0];
|
|
||||||
app.data.radarr_data.free_space = free_space.as_u64().unwrap();
|
|
||||||
app.data.radarr_data.total_space = total_space.as_u64().unwrap();
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error!("Failed to fetch disk space. {:?}", e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
RadarrEvent::GetStatus => match self.status(RadarrEvent::GetStatus.resource()).await {
|
|
||||||
Ok(system_status) => {
|
|
||||||
let mut app = self.app.lock().await;
|
|
||||||
app.data.radarr_data.version = system_status.version;
|
|
||||||
app.data.radarr_data.start_time = system_status.start_time;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error!("Failed to fetch system status. {:?}", e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut app = self.app.lock().await;
|
let mut app = self.app.lock().await;
|
||||||
app.reset();
|
app.reset_tick_count();
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn healthcheck(&self, resource: &str) {
|
async fn healthcheck(&self, resource: &str) {
|
||||||
@@ -76,32 +69,30 @@ impl<'a> Network<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn diskspace(&self, resource: &str) -> Result<Vec<DiskSpace>> {
|
async fn diskspace(&self, resource: &str) {
|
||||||
debug!("Handling diskspace event: {:?}", resource);
|
self.handle_get_request::<Vec<DiskSpace>>(resource, | disk_space_vec, mut app | {
|
||||||
|
let DiskSpace {
|
||||||
|
free_space,
|
||||||
|
total_space,
|
||||||
|
..
|
||||||
|
} = &disk_space_vec[0];
|
||||||
|
|
||||||
Ok(
|
app.data.radarr_data.free_space = free_space.as_u64().unwrap();
|
||||||
self
|
app.data.radarr_data.total_space = total_space.as_u64().unwrap();
|
||||||
.call_radarr_api(resource)
|
}).await;
|
||||||
.await
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.json::<Vec<DiskSpace>>()
|
|
||||||
.await?,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn status(&self, resource: &str) -> Result<SystemStatus> {
|
async fn status(&self, resource: &str) {
|
||||||
debug!("Handling system status event: {:?}", resource);
|
self.handle_get_request::<SystemStatus>(resource, | system_status, mut app | {
|
||||||
|
app.data.radarr_data.version = system_status.version;
|
||||||
|
app.data.radarr_data.start_time = system_status.start_time;
|
||||||
|
}).await;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(
|
async fn movies(&self, resource: &str) {
|
||||||
self
|
self.handle_get_request::<Vec<Movie>>(resource, |movie_vec, mut app| {
|
||||||
.call_radarr_api(resource)
|
app.data.radarr_data.movies.set_items(movie_vec);
|
||||||
.await
|
}).await;
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.json::<SystemStatus>()
|
|
||||||
.await?,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn call_radarr_api(&self, resource: &str) -> RequestBuilder {
|
async fn call_radarr_api(&self, resource: &str) -> RequestBuilder {
|
||||||
@@ -123,4 +114,24 @@ impl<'a> Network<'a> {
|
|||||||
))
|
))
|
||||||
.header("X-Api-Key", api_token)
|
.header("X-Api-Key", api_token)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn handle_get_request<T>(&self, resource: &str, mut app_update_fn: impl FnMut(T, MutexGuard<App>))
|
||||||
|
where
|
||||||
|
T: DeserializeOwned {
|
||||||
|
match self.call_radarr_api(resource)
|
||||||
|
.await
|
||||||
|
.send()
|
||||||
|
.await {
|
||||||
|
Ok(response) => {
|
||||||
|
match utils::parse_response::<T>(response).await {
|
||||||
|
Ok(value) => {
|
||||||
|
let app = self.app.lock().await;
|
||||||
|
app_update_fn(value, app);
|
||||||
|
}
|
||||||
|
Err(e) => error!("Failed to parse movie response! {:?}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => error!("Failed to fetch movies. {:?}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
use reqwest::Response;
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
|
||||||
|
pub async fn parse_response<T: DeserializeOwned>(response: Response) -> Result<T, reqwest::Error> {
|
||||||
|
response.json::<T>().await
|
||||||
|
}
|
||||||
+47
-10
@@ -1,15 +1,15 @@
|
|||||||
use std::ops::Sub;
|
use std::ops::Sub;
|
||||||
|
|
||||||
use chrono::{Duration, Utc};
|
use chrono::{Duration, Utc};
|
||||||
|
use tui::{Frame, symbols};
|
||||||
use tui::backend::Backend;
|
use tui::backend::Backend;
|
||||||
use tui::layout::{Alignment, Constraint, Rect};
|
use tui::layout::{Alignment, Constraint, Rect};
|
||||||
use tui::style::Color::Cyan;
|
use tui::style::{Color, Modifier, Style};
|
||||||
use tui::style::Style;
|
use tui::text::{Span, Spans, Text};
|
||||||
use tui::text::{Spans, Text};
|
use tui::widgets::{Block, Borders, Cell, LineGauge, Paragraph, Row, Table};
|
||||||
use tui::widgets::{Block, Borders, LineGauge, Paragraph};
|
|
||||||
use tui::{symbols, Frame};
|
|
||||||
|
|
||||||
use crate::app::radarr::RadarrData;
|
|
||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
|
use crate::app::radarr::RadarrData;
|
||||||
use crate::logos::{
|
use crate::logos::{
|
||||||
BAZARR_LOGO, LIDARR_LOGO, PROWLARR_LOGO, RADARR_LOGO, READARR_LOGO, SONARR_LOGO,
|
BAZARR_LOGO, LIDARR_LOGO, PROWLARR_LOGO, RADARR_LOGO, READARR_LOGO, SONARR_LOGO,
|
||||||
};
|
};
|
||||||
@@ -19,14 +19,17 @@ use crate::ui::utils::{
|
|||||||
|
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
pub fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
static HIGHLIGHT_SYMBOL: &str = "=> ";
|
||||||
let main_chunks = vertical_chunks(
|
|
||||||
|
pub fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
|
||||||
|
let main_chunks = vertical_chunks_with_margin(
|
||||||
vec![Constraint::Length(20), Constraint::Length(0)],
|
vec![Constraint::Length(20), Constraint::Length(0)],
|
||||||
f.size(),
|
f.size(),
|
||||||
|
1
|
||||||
);
|
);
|
||||||
|
|
||||||
draw_context_row(f, app, main_chunks[0]);
|
draw_context_row(f, app, main_chunks[0]);
|
||||||
f.render_widget(Block::default().borders(Borders::ALL), main_chunks[1]);
|
draw_radarr_ui(f, app, main_chunks[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_context_row<B: Backend>(f: &mut Frame<'_, B>, app: &App, area: Rect) {
|
fn draw_context_row<B: Backend>(f: &mut Frame<'_, B>, app: &App, area: Rect) {
|
||||||
@@ -49,6 +52,40 @@ fn draw_context_row<B: Backend>(f: &mut Frame<'_, B>, app: &App, area: Rect) {
|
|||||||
draw_logo(f, chunks[4]);
|
draw_logo(f, chunks[4]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn draw_radarr_ui<B: Backend>(f: &mut Frame<'_, B>, app: &mut App, area: Rect) {
|
||||||
|
let block = Block::default().borders(Borders::ALL).title(Spans::from(vec![
|
||||||
|
Span::styled("Movies", Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD))
|
||||||
|
]));
|
||||||
|
|
||||||
|
let row_style = Style::default().fg(Color::Cyan);
|
||||||
|
let rows = app.data.radarr_data.movies.items
|
||||||
|
.iter()
|
||||||
|
.map(|movie| Row::new(vec![
|
||||||
|
Cell::from(movie.title.to_owned()),
|
||||||
|
Cell::from(movie.year.to_string()),
|
||||||
|
Cell::from(movie.monitored.to_string()),
|
||||||
|
Cell::from(movie.has_file.to_string())
|
||||||
|
]).style(row_style));
|
||||||
|
let header_row = Row::new(vec!["Title", "Year", "Monitored", "Downloaded"])
|
||||||
|
.style(Style::default().fg(Color::White))
|
||||||
|
.bottom_margin(0);
|
||||||
|
let constraints = vec![
|
||||||
|
Constraint::Percentage(25),
|
||||||
|
Constraint::Percentage(25),
|
||||||
|
Constraint::Percentage(25),
|
||||||
|
Constraint::Percentage(25),
|
||||||
|
];
|
||||||
|
|
||||||
|
let table = Table::new(rows)
|
||||||
|
.header(header_row)
|
||||||
|
.block(block)
|
||||||
|
.highlight_style(Style::default().add_modifier(Modifier::REVERSED))
|
||||||
|
.highlight_symbol(HIGHLIGHT_SYMBOL)
|
||||||
|
.widths(&constraints);
|
||||||
|
|
||||||
|
f.render_stateful_widget(table, area, &mut app.data.radarr_data.movies.state)
|
||||||
|
}
|
||||||
|
|
||||||
fn draw_stats<B: Backend>(f: &mut Frame<'_, B>, app: &App, area: Rect) {
|
fn draw_stats<B: Backend>(f: &mut Frame<'_, B>, app: &App, area: Rect) {
|
||||||
let RadarrData {
|
let RadarrData {
|
||||||
free_space,
|
free_space,
|
||||||
@@ -92,7 +129,7 @@ fn draw_stats<B: Backend>(f: &mut Frame<'_, B>, app: &App, area: Rect) {
|
|||||||
|
|
||||||
let space_gauge = LineGauge::default()
|
let space_gauge = LineGauge::default()
|
||||||
.block(Block::default().title("Storage:"))
|
.block(Block::default().title("Storage:"))
|
||||||
.gauge_style(Style::default().fg(Cyan))
|
.gauge_style(Style::default().fg(Color::Cyan))
|
||||||
.line_set(symbols::line::THICK)
|
.line_set(symbols::line::THICK)
|
||||||
.ratio(ratio)
|
.ratio(ratio)
|
||||||
.label(Spans::from(format!("{:.0}%", ratio * 100.0)));
|
.label(Spans::from(format!("{:.0}%", ratio * 100.0)));
|
||||||
|
|||||||
Reference in New Issue
Block a user