feat: add gRPC client with config-based server address and album support
- Add tonic/prost gRPC client connecting to music-agregator service - Add config.yaml for configurable server host/port - Add build.rs for proto compilation from music-agregator - Update Artist/Album models to match proto with MonitorState enum - Convert album list from GetArtists response - Fix album click selection with correct layout offsets - Improve monitor state icons for better visibility
This commit is contained in:
Generated
+1050
-3
File diff suppressed because it is too large
Load Diff
@@ -8,3 +8,12 @@ ratatui = "0.29"
|
|||||||
crossterm = "0.28"
|
crossterm = "0.28"
|
||||||
color-eyre = "0.6"
|
color-eyre = "0.6"
|
||||||
nix = { version = "0.29", features = ["fs"] }
|
nix = { version = "0.29", features = ["fs"] }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_yaml = "0.9"
|
||||||
|
|
||||||
|
tonic = "0.12"
|
||||||
|
prost = "0.13"
|
||||||
|
tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "time"] }
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tonic-build = "0.12"
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let proto_root = "../music-agregator/proto";
|
||||||
|
|
||||||
|
tonic_build::configure()
|
||||||
|
.build_server(false)
|
||||||
|
.build_client(true)
|
||||||
|
.out_dir("src/proto")
|
||||||
|
.compile_protos(
|
||||||
|
&[format!(
|
||||||
|
"{}/music_agregator/v1/music_agregator.proto",
|
||||||
|
proto_root
|
||||||
|
)],
|
||||||
|
&[proto_root],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
server:
|
||||||
|
host: localhost
|
||||||
|
port: 3000
|
||||||
@@ -61,6 +61,8 @@
|
|||||||
clippy
|
clippy
|
||||||
rustfmt
|
rustfmt
|
||||||
|
|
||||||
|
protobuf
|
||||||
|
|
||||||
opencode
|
opencode
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|||||||
+122
-24
@@ -8,11 +8,14 @@ use ratatui::{
|
|||||||
widgets::{ListState, Paragraph},
|
widgets::{ListState, Paragraph},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::data::{Artist, CalendarEntry, HistoryEntry, QueueEntry, WantedEntry};
|
use crate::data::{
|
||||||
|
Album, AlbumStatus, Artist, CalendarEntry, HistoryEntry, MonitorState, QueueEntry, WantedEntry,
|
||||||
|
};
|
||||||
|
use crate::grpc::{AlbumDetail, ArtistSummary, GrpcResponse};
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
use crate::ui::library::{LibraryFocus, LibraryState, render_library};
|
use crate::ui::library::{LibraryFocus, LibraryState, render_library};
|
||||||
use crate::ui::modals::{ModalKind, render_help_modal, render_quit_modal};
|
use crate::ui::modals::{ModalKind, render_help_modal, render_quit_modal};
|
||||||
use crate::ui::notifications::NotificationManager;
|
use crate::ui::notifications::{NotifKind, NotificationManager};
|
||||||
use crate::ui::statusbar::render_statusbar;
|
use crate::ui::statusbar::render_statusbar;
|
||||||
use crate::ui::topbar::render_topbar;
|
use crate::ui::topbar::render_topbar;
|
||||||
use crate::ui::views::{
|
use crate::ui::views::{
|
||||||
@@ -286,46 +289,44 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn handle_library_click(&mut self, x: u16, rel_y: usize) {
|
fn handle_library_click(&mut self, x: u16, rel_y: usize) {
|
||||||
let artists_width = 32u16;
|
const ARTISTS_PANE_WIDTH: u16 = 32;
|
||||||
|
const BORDER_TOP: usize = 1;
|
||||||
|
const HEADER_HEIGHT: usize = 6;
|
||||||
|
const DIVIDER_HEIGHT: usize = 1;
|
||||||
|
const ALBUMS_START_ROW: usize = BORDER_TOP + HEADER_HEIGHT + DIVIDER_HEIGHT;
|
||||||
|
|
||||||
if x < artists_width {
|
if x < ARTISTS_PANE_WIDTH {
|
||||||
if rel_y > 0 && rel_y <= self.library.artists.len() {
|
if rel_y > 0 && rel_y <= self.library.artists.len() {
|
||||||
self.library.artist_state.select(Some(rel_y - 1));
|
self.library.artist_state.select(Some(rel_y - 1));
|
||||||
self.library.album_state.select(Some(0));
|
self.library.album_state.select(Some(0));
|
||||||
self.library.track_state.select(Some(0));
|
self.library.track_state.select(Some(0));
|
||||||
self.library.focus = LibraryFocus::Artists;
|
self.library.focus = LibraryFocus::Artists;
|
||||||
}
|
}
|
||||||
} else {
|
} else if rel_y >= ALBUMS_START_ROW {
|
||||||
let detail_y = rel_y;
|
let album_row = rel_y - ALBUMS_START_ROW;
|
||||||
let header_height = 6;
|
let content_height = self.main_area.height.saturating_sub(10) as usize;
|
||||||
let divider1 = 1;
|
let albums_section_height = (content_height * 40) / 100;
|
||||||
let _albums_section_height = 40;
|
let tracks_start_row = ALBUMS_START_ROW + albums_section_height + DIVIDER_HEIGHT;
|
||||||
|
|
||||||
if detail_y > header_height + divider1 {
|
|
||||||
let albums_start = header_height + divider1;
|
|
||||||
let albums_rel = detail_y - albums_start;
|
|
||||||
|
|
||||||
|
if rel_y < tracks_start_row {
|
||||||
if let Some(artist) = self.library.selected_artist() {
|
if let Some(artist) = self.library.selected_artist() {
|
||||||
let album_count = artist.albums.len();
|
if album_row < artist.albums.len() {
|
||||||
let tracks_start = albums_start + album_count.min(6) + 1;
|
self.library.album_state.select(Some(album_row));
|
||||||
|
|
||||||
if detail_y < tracks_start && albums_rel < album_count {
|
|
||||||
self.library.album_state.select(Some(albums_rel));
|
|
||||||
self.library.track_state.select(Some(0));
|
self.library.track_state.select(Some(0));
|
||||||
self.library.focus = LibraryFocus::Albums;
|
self.library.focus = LibraryFocus::Albums;
|
||||||
} else if detail_y >= tracks_start {
|
}
|
||||||
let tracks_rel = detail_y - tracks_start;
|
}
|
||||||
|
} else {
|
||||||
|
let track_row = rel_y - tracks_start_row;
|
||||||
if let Some(album) = self.library.selected_album() {
|
if let Some(album) = self.library.selected_album() {
|
||||||
if tracks_rel < album.total as usize {
|
if track_row < album.total as usize {
|
||||||
self.library.track_state.select(Some(tracks_rel));
|
self.library.track_state.select(Some(track_row));
|
||||||
self.library.focus = LibraryFocus::Tracks;
|
self.library.focus = LibraryFocus::Tracks;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn select_list_item(&self, _state: &mut ListState, _len: usize, _rel_y: usize) {}
|
fn select_list_item(&self, _state: &mut ListState, _len: usize, _rel_y: usize) {}
|
||||||
|
|
||||||
@@ -412,6 +413,103 @@ impl App {
|
|||||||
pub fn handle_tick(&mut self) {
|
pub fn handle_tick(&mut self) {
|
||||||
self.notifications.tick();
|
self.notifications.tick();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_error(&mut self, msg: String) {
|
||||||
|
self.notifications
|
||||||
|
.push("Error", Some(msg), NotifKind::Error, "✗");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_grpc_response(&mut self, response: GrpcResponse) {
|
||||||
|
match response {
|
||||||
|
GrpcResponse::Artists(artists) => {
|
||||||
|
let converted: Vec<Artist> = artists.into_iter().map(convert_artist).collect();
|
||||||
|
let count = converted.len();
|
||||||
|
self.library = LibraryState::new(converted);
|
||||||
|
self.notifications.push(
|
||||||
|
"Library loaded",
|
||||||
|
Some(format!("{} artists", count)),
|
||||||
|
NotifKind::Success,
|
||||||
|
"✓",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
GrpcResponse::Error(msg) => {
|
||||||
|
self.set_error(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convert_artist(summary: ArtistSummary) -> Artist {
|
||||||
|
let albums: Vec<Album> = summary.albums.into_iter().map(convert_album).collect();
|
||||||
|
Artist {
|
||||||
|
id: summary.id,
|
||||||
|
name: summary.name,
|
||||||
|
country: summary.country,
|
||||||
|
genres: summary.genres,
|
||||||
|
monitor_state: MonitorState::from_proto(summary.monitor_state),
|
||||||
|
path: String::new(),
|
||||||
|
quality: "FLAC".to_string(),
|
||||||
|
size_gb: 0.0,
|
||||||
|
albums,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convert_album(detail: AlbumDetail) -> Album {
|
||||||
|
let year = parse_year(&detail.release_date);
|
||||||
|
let monitor_state = MonitorState::from_proto(detail.monitor_state);
|
||||||
|
let monitored = monitor_state.is_monitored();
|
||||||
|
|
||||||
|
let (have, status, quality) = if let Some(download) = detail.download {
|
||||||
|
let have = if download.state == "downloaded" {
|
||||||
|
detail.total_tracks as u16
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
let status = match download.state.as_str() {
|
||||||
|
"downloaded" => AlbumStatus::Complete,
|
||||||
|
"downloading" => AlbumStatus::Partial,
|
||||||
|
_ => {
|
||||||
|
if monitored {
|
||||||
|
AlbumStatus::Wanted
|
||||||
|
} else {
|
||||||
|
AlbumStatus::Unmonitored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let quality = if download.quality.is_empty() {
|
||||||
|
"—".to_string()
|
||||||
|
} else {
|
||||||
|
download.quality
|
||||||
|
};
|
||||||
|
(have, status, quality)
|
||||||
|
} else {
|
||||||
|
let status = if monitored {
|
||||||
|
AlbumStatus::Wanted
|
||||||
|
} else {
|
||||||
|
AlbumStatus::Unmonitored
|
||||||
|
};
|
||||||
|
(0, status, "—".to_string())
|
||||||
|
};
|
||||||
|
|
||||||
|
Album {
|
||||||
|
id: detail.id,
|
||||||
|
title: detail.title,
|
||||||
|
year,
|
||||||
|
album_type: detail.album_type,
|
||||||
|
monitored,
|
||||||
|
total: detail.total_tracks as u16,
|
||||||
|
have,
|
||||||
|
quality,
|
||||||
|
status,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_year(date_str: &str) -> u16 {
|
||||||
|
date_str
|
||||||
|
.split('-')
|
||||||
|
.next()
|
||||||
|
.and_then(|y| y.parse().ok())
|
||||||
|
.unwrap_or(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn scroll_list_state(state: &mut ListState, len: usize, delta: i32) {
|
fn scroll_list_state(state: &mut ListState, len: usize, delta: i32) {
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
use serde::Deserialize;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct Config {
|
||||||
|
pub server: ServerConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct ServerConfig {
|
||||||
|
pub host: String,
|
||||||
|
pub port: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
|
let contents = fs::read_to_string(path)?;
|
||||||
|
let config: Config = serde_yaml::from_str(&contents)?;
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn grpc_addr(&self) -> String {
|
||||||
|
format!("http://{}:{}", self.server.host, self.server.port)
|
||||||
|
}
|
||||||
|
}
|
||||||
+27
-5
@@ -1,23 +1,45 @@
|
|||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
//! Data models for harmony music library.
|
|
||||||
|
|
||||||
/// Album completion status.
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum AlbumStatus {
|
pub enum AlbumStatus {
|
||||||
|
#[default]
|
||||||
Complete,
|
Complete,
|
||||||
Partial,
|
Partial,
|
||||||
Wanted,
|
Wanted,
|
||||||
Unmonitored,
|
Unmonitored,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A music artist in the library.
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub enum MonitorState {
|
||||||
|
#[default]
|
||||||
|
Unspecified,
|
||||||
|
Monitored,
|
||||||
|
Unmonitored,
|
||||||
|
Excluded,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MonitorState {
|
||||||
|
pub fn from_proto(value: i32) -> Self {
|
||||||
|
match value {
|
||||||
|
1 => MonitorState::Monitored,
|
||||||
|
2 => MonitorState::Unmonitored,
|
||||||
|
3 => MonitorState::Excluded,
|
||||||
|
_ => MonitorState::Unspecified,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_monitored(&self) -> bool {
|
||||||
|
matches!(self, MonitorState::Monitored)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Artist {
|
pub struct Artist {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub country: String,
|
pub country: String,
|
||||||
pub genres: Vec<String>,
|
pub genres: Vec<String>,
|
||||||
pub monitored: bool,
|
pub monitor_state: MonitorState,
|
||||||
pub path: String,
|
pub path: String,
|
||||||
pub quality: String,
|
pub quality: String,
|
||||||
pub size_gb: f64,
|
pub size_gb: f64,
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
use tokio::sync::mpsc;
|
||||||
|
use tonic::transport::Channel;
|
||||||
|
|
||||||
|
use crate::proto::music_agregator_v1::music_agregator_service_client::MusicAgregatorServiceClient;
|
||||||
|
|
||||||
|
pub use crate::proto::music_agregator_v1::{AlbumDetail, ArtistSummary, GetArtistsRequest};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum GrpcRequest {
|
||||||
|
GetArtists,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum GrpcResponse {
|
||||||
|
Artists(Vec<ArtistSummary>),
|
||||||
|
Error(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GrpcClient {
|
||||||
|
music: MusicAgregatorServiceClient<Channel>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GrpcClient {
|
||||||
|
pub async fn connect(addr: &str) -> Result<Self, tonic::transport::Error> {
|
||||||
|
let channel = Channel::from_shared(addr.to_string())
|
||||||
|
.expect("valid uri")
|
||||||
|
.connect()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
music: MusicAgregatorServiceClient::new(channel),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_artists(&mut self) -> Result<Vec<ArtistSummary>, tonic::Status> {
|
||||||
|
let response = self.music.get_artists(GetArtistsRequest {}).await?;
|
||||||
|
Ok(response.into_inner().artists)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn spawn_grpc_worker(
|
||||||
|
addr: String,
|
||||||
|
) -> (mpsc::Sender<GrpcRequest>, mpsc::Receiver<GrpcResponse>) {
|
||||||
|
let (req_tx, mut req_rx) = mpsc::channel::<GrpcRequest>(32);
|
||||||
|
let (resp_tx, resp_rx) = mpsc::channel::<GrpcResponse>(32);
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let client = match GrpcClient::connect(&addr).await {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => {
|
||||||
|
let _ = resp_tx.send(GrpcResponse::Error(e.to_string())).await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut client = client;
|
||||||
|
|
||||||
|
while let Some(request) = req_rx.recv().await {
|
||||||
|
let response = match request {
|
||||||
|
GrpcRequest::GetArtists => match client.get_artists().await {
|
||||||
|
Ok(artists) => GrpcResponse::Artists(artists),
|
||||||
|
Err(e) => GrpcResponse::Error(e.to_string()),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if resp_tx.send(response).await.is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
(req_tx, resp_rx)
|
||||||
|
}
|
||||||
+29
-5
@@ -14,15 +14,21 @@ use crossterm::{
|
|||||||
use ratatui::prelude::*;
|
use ratatui::prelude::*;
|
||||||
|
|
||||||
mod app;
|
mod app;
|
||||||
|
mod config;
|
||||||
mod data;
|
mod data;
|
||||||
|
mod grpc;
|
||||||
|
mod proto;
|
||||||
mod theme;
|
mod theme;
|
||||||
mod ui;
|
mod ui;
|
||||||
|
|
||||||
use app::App;
|
use app::App;
|
||||||
|
use config::Config;
|
||||||
|
use grpc::{GrpcRequest, spawn_grpc_worker};
|
||||||
|
|
||||||
const TICK_RATE: Duration = Duration::from_millis(250);
|
const TICK_RATE: Duration = Duration::from_millis(100);
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
color_eyre::install()?;
|
color_eyre::install()?;
|
||||||
|
|
||||||
let hook = panic::take_hook();
|
let hook = panic::take_hook();
|
||||||
@@ -32,7 +38,7 @@ fn main() -> Result<()> {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
setup_terminal()?;
|
setup_terminal()?;
|
||||||
let result = run();
|
let result = run().await;
|
||||||
restore_terminal()?;
|
restore_terminal()?;
|
||||||
|
|
||||||
result
|
result
|
||||||
@@ -52,23 +58,41 @@ fn restore_terminal() -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run() -> Result<()> {
|
async fn run() -> Result<()> {
|
||||||
|
let config = Config::load("config.yaml").unwrap_or_else(|e| {
|
||||||
|
eprintln!("Failed to load config.yaml: {}", e);
|
||||||
|
std::process::exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
|
let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
|
||||||
let mut app = App::new();
|
let mut app = App::new();
|
||||||
|
|
||||||
|
let (grpc_tx, mut grpc_rx) = spawn_grpc_worker(config.grpc_addr());
|
||||||
|
|
||||||
|
if grpc_tx.send(GrpcRequest::GetArtists).await.is_err() {
|
||||||
|
app.set_error("Failed to send initial request".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
while app.running {
|
while app.running {
|
||||||
terminal.draw(|frame| app.draw(frame))?;
|
terminal.draw(|frame| app.draw(frame))?;
|
||||||
|
|
||||||
|
if let Ok(response) = grpc_rx.try_recv() {
|
||||||
|
app.handle_grpc_response(response);
|
||||||
|
}
|
||||||
|
|
||||||
if event::poll(TICK_RATE)? {
|
if event::poll(TICK_RATE)? {
|
||||||
match event::read()? {
|
match event::read()? {
|
||||||
Event::Key(key) if key.kind == KeyEventKind::Press => {
|
Event::Key(key) if key.kind == KeyEventKind::Press => {
|
||||||
// Only handle Ctrl+C and Escape for quit
|
|
||||||
if key.modifiers.contains(KeyModifiers::CONTROL)
|
if key.modifiers.contains(KeyModifiers::CONTROL)
|
||||||
&& key.code == KeyCode::Char('c')
|
&& key.code == KeyCode::Char('c')
|
||||||
{
|
{
|
||||||
app.running = false;
|
app.running = false;
|
||||||
} else if key.code == KeyCode::Esc {
|
} else if key.code == KeyCode::Esc {
|
||||||
app.handle_escape();
|
app.handle_escape();
|
||||||
|
} else if key.code == KeyCode::Char('r')
|
||||||
|
&& key.modifiers.contains(KeyModifiers::CONTROL)
|
||||||
|
{
|
||||||
|
let _ = grpc_tx.send(GrpcRequest::GetArtists).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::Mouse(mouse) => match mouse.kind {
|
Event::Mouse(mouse) => match mouse.kind {
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod music_agregator_v1 {
|
||||||
|
include!("music_agregator.v1.rs");
|
||||||
|
}
|
||||||
@@ -0,0 +1,417 @@
|
|||||||
|
// This file is @generated by prost-build.
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct MonitorAlbumRequest {
|
||||||
|
#[prost(string, tag = "1")]
|
||||||
|
pub album_id: ::prost::alloc::string::String,
|
||||||
|
#[prost(message, optional, tag = "2")]
|
||||||
|
pub indexer_options: ::core::option::Option<IndexerOptions>,
|
||||||
|
#[prost(enumeration = "QualityType", tag = "3")]
|
||||||
|
pub quality: i32,
|
||||||
|
}
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct IndexerOptions {
|
||||||
|
#[prost(string, tag = "1")]
|
||||||
|
pub tracker: ::prost::alloc::string::String,
|
||||||
|
}
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct MonitorAlbumResponse {
|
||||||
|
#[prost(message, optional, tag = "1")]
|
||||||
|
pub release: ::core::option::Option<MonitoredRelease>,
|
||||||
|
}
|
||||||
|
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
|
||||||
|
pub struct GetArtistsRequest {}
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct GetArtistsResponse {
|
||||||
|
#[prost(message, repeated, tag = "1")]
|
||||||
|
pub artists: ::prost::alloc::vec::Vec<ArtistSummary>,
|
||||||
|
}
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct ArtistSummary {
|
||||||
|
#[prost(string, tag = "1")]
|
||||||
|
pub id: ::prost::alloc::string::String,
|
||||||
|
#[prost(string, tag = "2")]
|
||||||
|
pub external_id: ::prost::alloc::string::String,
|
||||||
|
#[prost(string, tag = "3")]
|
||||||
|
pub name: ::prost::alloc::string::String,
|
||||||
|
#[prost(string, tag = "4")]
|
||||||
|
pub artist_type: ::prost::alloc::string::String,
|
||||||
|
#[prost(string, tag = "5")]
|
||||||
|
pub country: ::prost::alloc::string::String,
|
||||||
|
#[prost(string, repeated, tag = "6")]
|
||||||
|
pub genres: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
|
||||||
|
#[prost(string, tag = "7")]
|
||||||
|
pub image_url: ::prost::alloc::string::String,
|
||||||
|
#[prost(enumeration = "MonitorState", tag = "8")]
|
||||||
|
pub monitor_state: i32,
|
||||||
|
#[prost(message, repeated, tag = "9")]
|
||||||
|
pub albums: ::prost::alloc::vec::Vec<AlbumDetail>,
|
||||||
|
}
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct AlbumDetail {
|
||||||
|
#[prost(string, tag = "1")]
|
||||||
|
pub id: ::prost::alloc::string::String,
|
||||||
|
#[prost(string, tag = "2")]
|
||||||
|
pub external_id: ::prost::alloc::string::String,
|
||||||
|
#[prost(string, tag = "3")]
|
||||||
|
pub title: ::prost::alloc::string::String,
|
||||||
|
#[prost(string, tag = "4")]
|
||||||
|
pub album_type: ::prost::alloc::string::String,
|
||||||
|
#[prost(string, tag = "5")]
|
||||||
|
pub release_date: ::prost::alloc::string::String,
|
||||||
|
#[prost(int32, tag = "6")]
|
||||||
|
pub total_tracks: i32,
|
||||||
|
#[prost(int32, tag = "7")]
|
||||||
|
pub total_discs: i32,
|
||||||
|
#[prost(string, tag = "8")]
|
||||||
|
pub cover_url: ::prost::alloc::string::String,
|
||||||
|
#[prost(string, repeated, tag = "9")]
|
||||||
|
pub genres: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
|
||||||
|
#[prost(string, tag = "10")]
|
||||||
|
pub label: ::prost::alloc::string::String,
|
||||||
|
#[prost(enumeration = "MonitorState", tag = "11")]
|
||||||
|
pub monitor_state: i32,
|
||||||
|
#[prost(message, optional, tag = "12")]
|
||||||
|
pub download: ::core::option::Option<DownloadInfo>,
|
||||||
|
}
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct DownloadInfo {
|
||||||
|
#[prost(string, tag = "1")]
|
||||||
|
pub state: ::prost::alloc::string::String,
|
||||||
|
#[prost(string, tag = "2")]
|
||||||
|
pub format: ::prost::alloc::string::String,
|
||||||
|
#[prost(string, tag = "3")]
|
||||||
|
pub quality: ::prost::alloc::string::String,
|
||||||
|
#[prost(string, tag = "4")]
|
||||||
|
pub save_path: ::prost::alloc::string::String,
|
||||||
|
}
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct GetAlbumRequest {
|
||||||
|
#[prost(string, tag = "1")]
|
||||||
|
pub album_id: ::prost::alloc::string::String,
|
||||||
|
}
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct GetAlbumResponse {
|
||||||
|
#[prost(message, optional, tag = "1")]
|
||||||
|
pub album: ::core::option::Option<AlbumDetail>,
|
||||||
|
#[prost(message, repeated, tag = "2")]
|
||||||
|
pub tracks: ::prost::alloc::vec::Vec<TrackDetail>,
|
||||||
|
}
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct TrackDetail {
|
||||||
|
#[prost(string, tag = "1")]
|
||||||
|
pub id: ::prost::alloc::string::String,
|
||||||
|
#[prost(string, tag = "2")]
|
||||||
|
pub external_id: ::prost::alloc::string::String,
|
||||||
|
#[prost(string, tag = "3")]
|
||||||
|
pub title: ::prost::alloc::string::String,
|
||||||
|
#[prost(int32, tag = "4")]
|
||||||
|
pub duration_ms: i32,
|
||||||
|
#[prost(int32, tag = "5")]
|
||||||
|
pub disc_number: i32,
|
||||||
|
#[prost(int32, tag = "6")]
|
||||||
|
pub track_number: i32,
|
||||||
|
#[prost(string, tag = "7")]
|
||||||
|
pub isrc: ::prost::alloc::string::String,
|
||||||
|
#[prost(bool, tag = "8")]
|
||||||
|
pub explicit: bool,
|
||||||
|
#[prost(message, repeated, tag = "9")]
|
||||||
|
pub artists: ::prost::alloc::vec::Vec<ArtistCredit>,
|
||||||
|
#[prost(message, optional, tag = "10")]
|
||||||
|
pub file: ::core::option::Option<TrackFile>,
|
||||||
|
}
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct ArtistCredit {
|
||||||
|
#[prost(string, tag = "1")]
|
||||||
|
pub id: ::prost::alloc::string::String,
|
||||||
|
#[prost(string, tag = "2")]
|
||||||
|
pub name: ::prost::alloc::string::String,
|
||||||
|
}
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct TrackFile {
|
||||||
|
#[prost(string, tag = "1")]
|
||||||
|
pub path: ::prost::alloc::string::String,
|
||||||
|
#[prost(string, tag = "2")]
|
||||||
|
pub format: ::prost::alloc::string::String,
|
||||||
|
#[prost(int64, tag = "3")]
|
||||||
|
pub size: i64,
|
||||||
|
}
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct MonitoredRelease {
|
||||||
|
#[prost(string, tag = "1")]
|
||||||
|
pub info_hash: ::prost::alloc::string::String,
|
||||||
|
#[prost(string, tag = "2")]
|
||||||
|
pub artist: ::prost::alloc::string::String,
|
||||||
|
#[prost(string, tag = "3")]
|
||||||
|
pub album: ::prost::alloc::string::String,
|
||||||
|
#[prost(int32, tag = "4")]
|
||||||
|
pub year: i32,
|
||||||
|
#[prost(string, tag = "5")]
|
||||||
|
pub format: ::prost::alloc::string::String,
|
||||||
|
#[prost(bool, tag = "6")]
|
||||||
|
pub lossless: bool,
|
||||||
|
#[prost(int32, tag = "7")]
|
||||||
|
pub bit_depth: i32,
|
||||||
|
#[prost(int32, tag = "8")]
|
||||||
|
pub sample_rate: i32,
|
||||||
|
#[prost(string, tag = "9")]
|
||||||
|
pub source: ::prost::alloc::string::String,
|
||||||
|
#[prost(int32, tag = "10")]
|
||||||
|
pub track_count: i32,
|
||||||
|
#[prost(string, repeated, tag = "11")]
|
||||||
|
pub track_names: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
|
||||||
|
#[prost(bool, tag = "12")]
|
||||||
|
pub has_cover_art: bool,
|
||||||
|
#[prost(bool, tag = "13")]
|
||||||
|
pub has_cue_sheet: bool,
|
||||||
|
#[prost(bool, tag = "14")]
|
||||||
|
pub has_rip_log: bool,
|
||||||
|
#[prost(int64, tag = "15")]
|
||||||
|
pub total_audio_size: i64,
|
||||||
|
#[prost(string, tag = "16")]
|
||||||
|
pub download_link: ::prost::alloc::string::String,
|
||||||
|
#[prost(int32, tag = "17")]
|
||||||
|
pub seeders: i32,
|
||||||
|
#[prost(string, tag = "18")]
|
||||||
|
pub tracker: ::prost::alloc::string::String,
|
||||||
|
}
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
|
||||||
|
#[repr(i32)]
|
||||||
|
pub enum QualityType {
|
||||||
|
QualityUnspecified = 0,
|
||||||
|
QualityLossless = 1,
|
||||||
|
QualityLossy = 2,
|
||||||
|
}
|
||||||
|
impl QualityType {
|
||||||
|
/// String value of the enum field names used in the ProtoBuf definition.
|
||||||
|
///
|
||||||
|
/// The values are not transformed in any way and thus are considered stable
|
||||||
|
/// (if the ProtoBuf definition does not change) and safe for programmatic use.
|
||||||
|
pub fn as_str_name(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::QualityUnspecified => "QUALITY_UNSPECIFIED",
|
||||||
|
Self::QualityLossless => "QUALITY_LOSSLESS",
|
||||||
|
Self::QualityLossy => "QUALITY_LOSSY",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Creates an enum from field names used in the ProtoBuf definition.
|
||||||
|
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
|
||||||
|
match value {
|
||||||
|
"QUALITY_UNSPECIFIED" => Some(Self::QualityUnspecified),
|
||||||
|
"QUALITY_LOSSLESS" => Some(Self::QualityLossless),
|
||||||
|
"QUALITY_LOSSY" => Some(Self::QualityLossy),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
|
||||||
|
#[repr(i32)]
|
||||||
|
pub enum MonitorState {
|
||||||
|
Unspecified = 0,
|
||||||
|
Monitored = 1,
|
||||||
|
Unmonitored = 2,
|
||||||
|
Excluded = 3,
|
||||||
|
}
|
||||||
|
impl MonitorState {
|
||||||
|
/// String value of the enum field names used in the ProtoBuf definition.
|
||||||
|
///
|
||||||
|
/// The values are not transformed in any way and thus are considered stable
|
||||||
|
/// (if the ProtoBuf definition does not change) and safe for programmatic use.
|
||||||
|
pub fn as_str_name(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Unspecified => "MONITOR_STATE_UNSPECIFIED",
|
||||||
|
Self::Monitored => "MONITOR_STATE_MONITORED",
|
||||||
|
Self::Unmonitored => "MONITOR_STATE_UNMONITORED",
|
||||||
|
Self::Excluded => "MONITOR_STATE_EXCLUDED",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Creates an enum from field names used in the ProtoBuf definition.
|
||||||
|
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
|
||||||
|
match value {
|
||||||
|
"MONITOR_STATE_UNSPECIFIED" => Some(Self::Unspecified),
|
||||||
|
"MONITOR_STATE_MONITORED" => Some(Self::Monitored),
|
||||||
|
"MONITOR_STATE_UNMONITORED" => Some(Self::Unmonitored),
|
||||||
|
"MONITOR_STATE_EXCLUDED" => Some(Self::Excluded),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Generated client implementations.
|
||||||
|
pub mod music_agregator_service_client {
|
||||||
|
#![allow(
|
||||||
|
unused_variables,
|
||||||
|
dead_code,
|
||||||
|
missing_docs,
|
||||||
|
clippy::wildcard_imports,
|
||||||
|
clippy::let_unit_value,
|
||||||
|
)]
|
||||||
|
use tonic::codegen::*;
|
||||||
|
use tonic::codegen::http::Uri;
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct MusicAgregatorServiceClient<T> {
|
||||||
|
inner: tonic::client::Grpc<T>,
|
||||||
|
}
|
||||||
|
impl MusicAgregatorServiceClient<tonic::transport::Channel> {
|
||||||
|
/// Attempt to create a new client by connecting to a given endpoint.
|
||||||
|
pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error>
|
||||||
|
where
|
||||||
|
D: TryInto<tonic::transport::Endpoint>,
|
||||||
|
D::Error: Into<StdError>,
|
||||||
|
{
|
||||||
|
let conn = tonic::transport::Endpoint::new(dst)?.connect().await?;
|
||||||
|
Ok(Self::new(conn))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<T> MusicAgregatorServiceClient<T>
|
||||||
|
where
|
||||||
|
T: tonic::client::GrpcService<tonic::body::BoxBody>,
|
||||||
|
T::Error: Into<StdError>,
|
||||||
|
T::ResponseBody: Body<Data = Bytes> + std::marker::Send + 'static,
|
||||||
|
<T::ResponseBody as Body>::Error: Into<StdError> + std::marker::Send,
|
||||||
|
{
|
||||||
|
pub fn new(inner: T) -> Self {
|
||||||
|
let inner = tonic::client::Grpc::new(inner);
|
||||||
|
Self { inner }
|
||||||
|
}
|
||||||
|
pub fn with_origin(inner: T, origin: Uri) -> Self {
|
||||||
|
let inner = tonic::client::Grpc::with_origin(inner, origin);
|
||||||
|
Self { inner }
|
||||||
|
}
|
||||||
|
pub fn with_interceptor<F>(
|
||||||
|
inner: T,
|
||||||
|
interceptor: F,
|
||||||
|
) -> MusicAgregatorServiceClient<InterceptedService<T, F>>
|
||||||
|
where
|
||||||
|
F: tonic::service::Interceptor,
|
||||||
|
T::ResponseBody: Default,
|
||||||
|
T: tonic::codegen::Service<
|
||||||
|
http::Request<tonic::body::BoxBody>,
|
||||||
|
Response = http::Response<
|
||||||
|
<T as tonic::client::GrpcService<tonic::body::BoxBody>>::ResponseBody,
|
||||||
|
>,
|
||||||
|
>,
|
||||||
|
<T as tonic::codegen::Service<
|
||||||
|
http::Request<tonic::body::BoxBody>,
|
||||||
|
>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync,
|
||||||
|
{
|
||||||
|
MusicAgregatorServiceClient::new(InterceptedService::new(inner, interceptor))
|
||||||
|
}
|
||||||
|
/// Compress requests with the given encoding.
|
||||||
|
///
|
||||||
|
/// This requires the server to support it otherwise it might respond with an
|
||||||
|
/// error.
|
||||||
|
#[must_use]
|
||||||
|
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
||||||
|
self.inner = self.inner.send_compressed(encoding);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
/// Enable decompressing responses.
|
||||||
|
#[must_use]
|
||||||
|
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
||||||
|
self.inner = self.inner.accept_compressed(encoding);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
/// Limits the maximum size of a decoded message.
|
||||||
|
///
|
||||||
|
/// Default: `4MB`
|
||||||
|
#[must_use]
|
||||||
|
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
|
||||||
|
self.inner = self.inner.max_decoding_message_size(limit);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
/// Limits the maximum size of an encoded message.
|
||||||
|
///
|
||||||
|
/// Default: `usize::MAX`
|
||||||
|
#[must_use]
|
||||||
|
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
|
||||||
|
self.inner = self.inner.max_encoding_message_size(limit);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
pub async fn monitor_album(
|
||||||
|
&mut self,
|
||||||
|
request: impl tonic::IntoRequest<super::MonitorAlbumRequest>,
|
||||||
|
) -> std::result::Result<
|
||||||
|
tonic::Response<super::MonitorAlbumResponse>,
|
||||||
|
tonic::Status,
|
||||||
|
> {
|
||||||
|
self.inner
|
||||||
|
.ready()
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tonic::Status::unknown(
|
||||||
|
format!("Service was not ready: {}", e.into()),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let codec = tonic::codec::ProstCodec::default();
|
||||||
|
let path = http::uri::PathAndQuery::from_static(
|
||||||
|
"/music_agregator.v1.MusicAgregatorService/MonitorAlbum",
|
||||||
|
);
|
||||||
|
let mut req = request.into_request();
|
||||||
|
req.extensions_mut()
|
||||||
|
.insert(
|
||||||
|
GrpcMethod::new(
|
||||||
|
"music_agregator.v1.MusicAgregatorService",
|
||||||
|
"MonitorAlbum",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
self.inner.unary(req, path, codec).await
|
||||||
|
}
|
||||||
|
pub async fn get_artists(
|
||||||
|
&mut self,
|
||||||
|
request: impl tonic::IntoRequest<super::GetArtistsRequest>,
|
||||||
|
) -> std::result::Result<
|
||||||
|
tonic::Response<super::GetArtistsResponse>,
|
||||||
|
tonic::Status,
|
||||||
|
> {
|
||||||
|
self.inner
|
||||||
|
.ready()
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tonic::Status::unknown(
|
||||||
|
format!("Service was not ready: {}", e.into()),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let codec = tonic::codec::ProstCodec::default();
|
||||||
|
let path = http::uri::PathAndQuery::from_static(
|
||||||
|
"/music_agregator.v1.MusicAgregatorService/GetArtists",
|
||||||
|
);
|
||||||
|
let mut req = request.into_request();
|
||||||
|
req.extensions_mut()
|
||||||
|
.insert(
|
||||||
|
GrpcMethod::new(
|
||||||
|
"music_agregator.v1.MusicAgregatorService",
|
||||||
|
"GetArtists",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
self.inner.unary(req, path, codec).await
|
||||||
|
}
|
||||||
|
pub async fn get_album(
|
||||||
|
&mut self,
|
||||||
|
request: impl tonic::IntoRequest<super::GetAlbumRequest>,
|
||||||
|
) -> std::result::Result<
|
||||||
|
tonic::Response<super::GetAlbumResponse>,
|
||||||
|
tonic::Status,
|
||||||
|
> {
|
||||||
|
self.inner
|
||||||
|
.ready()
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tonic::Status::unknown(
|
||||||
|
format!("Service was not ready: {}", e.into()),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let codec = tonic::codec::ProstCodec::default();
|
||||||
|
let path = http::uri::PathAndQuery::from_static(
|
||||||
|
"/music_agregator.v1.MusicAgregatorService/GetAlbum",
|
||||||
|
);
|
||||||
|
let mut req = request.into_request();
|
||||||
|
req.extensions_mut()
|
||||||
|
.insert(
|
||||||
|
GrpcMethod::new(
|
||||||
|
"music_agregator.v1.MusicAgregatorService",
|
||||||
|
"GetAlbum",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
self.inner.unary(req, path, codec).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+31
-11
@@ -8,7 +8,7 @@ use ratatui::{
|
|||||||
widgets::{List, ListItem, ListState, Paragraph},
|
widgets::{List, ListItem, ListState, Paragraph},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::data::{Album, AlbumStatus, Artist, Track};
|
use crate::data::{Album, AlbumStatus, Artist, MonitorState, Track};
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
use crate::ui::pane::{Pane, section_divider};
|
use crate::ui::pane::{Pane, section_divider};
|
||||||
use crate::ui::progress_bar::progress_bar;
|
use crate::ui::progress_bar::progress_bar;
|
||||||
@@ -201,6 +201,20 @@ fn status_icon(status: AlbumStatus, monitored: bool) -> (char, Style) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn monitor_state_icon(state: MonitorState, status: AlbumStatus) -> (char, Style) {
|
||||||
|
match state {
|
||||||
|
MonitorState::Monitored => match status {
|
||||||
|
AlbumStatus::Complete => ('✓', Style::default().fg(theme::GREEN)),
|
||||||
|
AlbumStatus::Partial => ('◐', Style::default().fg(theme::YELLOW)),
|
||||||
|
AlbumStatus::Wanted => ('!', Style::default().fg(theme::RED)),
|
||||||
|
AlbumStatus::Unmonitored => ('-', Style::default().fg(theme::GRAY)),
|
||||||
|
},
|
||||||
|
MonitorState::Unmonitored => ('-', Style::default().fg(theme::GRAY)),
|
||||||
|
MonitorState::Excluded => ('x', Style::default().fg(theme::RED)),
|
||||||
|
MonitorState::Unspecified => ('?', Style::default().fg(theme::GRAY)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn track_icon(have: bool) -> (char, Style) {
|
fn track_icon(have: bool) -> (char, Style) {
|
||||||
if have {
|
if have {
|
||||||
('✓', Style::default().fg(theme::GREEN))
|
('✓', Style::default().fg(theme::GREEN))
|
||||||
@@ -267,15 +281,12 @@ fn render_artists_pane(frame: &mut Frame, area: Rect, state: &mut LibraryState)
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|artist| {
|
.map(|artist| {
|
||||||
let status = artist_status(artist);
|
let status = artist_status(artist);
|
||||||
let (icon_char, icon_style) = status_icon(status, artist.monitored);
|
let (icon_char, icon_style) = monitor_state_icon(artist.monitor_state, status);
|
||||||
|
|
||||||
let total: u16 = artist.albums.iter().map(|a| a.total).sum();
|
let total: u16 = artist.albums.iter().map(|a| a.total).sum();
|
||||||
let have: u16 = artist.albums.iter().map(|a| a.have).sum();
|
let have: u16 = artist.albums.iter().map(|a| a.have).sum();
|
||||||
|
|
||||||
let mut name_text = artist.name.clone();
|
let mut name_text = artist.name.clone();
|
||||||
if !artist.monitored {
|
|
||||||
name_text.push_str(" ·unm");
|
|
||||||
}
|
|
||||||
|
|
||||||
let count_str = format!("{}/{}", have, total);
|
let count_str = format!("{}/{}", have, total);
|
||||||
let name_width = inner.width as usize - 2 - count_str.len() - 2;
|
let name_width = inner.width as usize - 2 - count_str.len() - 2;
|
||||||
@@ -399,18 +410,27 @@ fn render_artist_header(frame: &mut Frame, area: Rect, artist: &Artist) {
|
|||||||
let have: u16 = artist.albums.iter().map(|a| a.have).sum();
|
let have: u16 = artist.albums.iter().map(|a| a.have).sum();
|
||||||
let total: u16 = artist.albums.iter().map(|a| a.total).sum();
|
let total: u16 = artist.albums.iter().map(|a| a.total).sum();
|
||||||
|
|
||||||
let (status_icon, status_text, status_style) = if artist.monitored {
|
let (status_icon, status_text, status_style) = match artist.monitor_state {
|
||||||
(
|
MonitorState::Monitored => (
|
||||||
Span::styled("● ", Style::default().fg(theme::GREEN)),
|
Span::styled("● ", Style::default().fg(theme::GREEN)),
|
||||||
"Monitored",
|
"Monitored",
|
||||||
Style::default().fg(theme::FG2),
|
Style::default().fg(theme::FG2),
|
||||||
)
|
),
|
||||||
} else {
|
MonitorState::Unmonitored => (
|
||||||
(
|
|
||||||
Span::styled("◌ ", Style::default().fg(theme::GRAY)),
|
Span::styled("◌ ", Style::default().fg(theme::GRAY)),
|
||||||
"Unmonitored",
|
"Unmonitored",
|
||||||
Style::default().fg(theme::GRAY),
|
Style::default().fg(theme::GRAY),
|
||||||
)
|
),
|
||||||
|
MonitorState::Excluded => (
|
||||||
|
Span::styled("⊘ ", Style::default().fg(theme::RED)),
|
||||||
|
"Excluded",
|
||||||
|
Style::default().fg(theme::RED),
|
||||||
|
),
|
||||||
|
MonitorState::Unspecified => (
|
||||||
|
Span::styled("? ", Style::default().fg(theme::GRAY)),
|
||||||
|
"Unknown",
|
||||||
|
Style::default().fg(theme::GRAY),
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
let lines = vec![
|
let lines = vec![
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ pub use help::render_help_modal;
|
|||||||
pub use quit::render_quit_modal;
|
pub use quit::render_quit_modal;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[allow(dead_code)]
|
||||||
pub enum ModalKind {
|
pub enum ModalKind {
|
||||||
Help,
|
Help,
|
||||||
Quit,
|
Quit,
|
||||||
|
|||||||
Reference in New Issue
Block a user