Refactor view states

This commit is contained in:
chmanie 2023-05-27 01:01:47 +02:00
parent 9e1efb886a
commit 5b80868872
2 changed files with 267 additions and 111 deletions

View File

@ -2,8 +2,8 @@ mod rpc;
use crabidy_core::proto::crabidy::{ use crabidy_core::proto::crabidy::{
crabidy_service_client::CrabidyServiceClient, get_queue_updates_response::QueueUpdateResult, crabidy_service_client::CrabidyServiceClient, get_queue_updates_response::QueueUpdateResult,
GetLibraryNodeRequest, GetQueueUpdatesRequest, GetQueueUpdatesResponse, ActiveTrack, GetLibraryNodeRequest, GetQueueUpdatesRequest, GetQueueUpdatesResponse,
GetTrackUpdatesResponse, LibraryNode, LibraryNodeState, GetTrackUpdatesResponse, LibraryNode, LibraryNodeState, Queue,
}; };
use crossterm::{ use crossterm::{
@ -30,71 +30,44 @@ use std::{
}; };
use tokio::{select, signal, task}; use tokio::{select, signal, task};
use tokio_stream::StreamExt; use tokio_stream::StreamExt;
// use
use tonic::{transport::Channel, Request, Streaming}; use tonic::{transport::Channel, Request, Streaming};
struct StatefulList<T> { trait ListView {
state: ListState, fn get_size(&self) -> usize;
items: Vec<T>, fn select(&mut self, idx: Option<usize>);
prev_selected: usize, fn selected(&self) -> Option<usize>;
} fn prev_selected(&self) -> usize;
impl<T> StatefulList<T> {
fn default() -> Self {
let mut state = ListState::default();
Self {
state,
items: Vec::default(),
prev_selected: 0,
}
}
fn next(&mut self) { fn next(&mut self) {
if let Some(i) = self.state.selected() { if self.is_empty() {
let next = if i == self.items.len() - 1 { 0 } else { i + 1 }; return;
self.state.select(Some(next)); }
if let Some(i) = self.selected() {
let next = if i == self.get_size() - 1 { 0 } else { i + 1 };
self.select(Some(next));
} else { } else {
self.state.select(Some(0)); self.select(Some(0));
} }
} }
fn prev(&mut self) { fn prev(&mut self) {
if let Some(i) = self.state.selected() { if self.is_empty() {
let prev = if i == 0 { self.items.len() - 1 } else { i - 1 };
self.state.select(Some(prev));
} else {
self.state.select(Some(0));
}
}
fn is_focused(&self) -> bool {
self.state.selected().is_some()
}
fn focus(&mut self) {
if self.is_focused() {
return; return;
} }
self.state.select(Some(self.prev_selected)); if let Some(i) = self.selected() {
} let prev = if i == 0 { self.get_size() - 1 } else { i - 1 };
self.select(Some(prev));
fn blur(&mut self) {
if !self.is_focused() {
return;
}
if let Some(i) = self.state.selected() {
self.prev_selected = i;
} else { } else {
self.prev_selected = 0; self.select(Some(0));
} }
self.state.select(None);
} }
fn get_selected(&self) -> Option<&T> { fn is_selected(&self) -> bool {
if let Some(idx) = self.state.selected() { self.selected().is_some()
return Some(&self.items[idx]);
} }
None
fn is_empty(&self) -> bool {
self.get_size() == 0
} }
} }
@ -103,25 +76,134 @@ struct UiItem {
title: String, title: String,
} }
struct QueueView {
list: Vec<UiItem>,
list_state: ListState,
prev_selected: usize,
}
impl ListView for QueueView {
fn get_size(&self) -> usize {
self.list.len()
}
fn select(&mut self, idx: Option<usize>) {
if let Some(pos) = idx {
self.prev_selected = pos;
}
self.list_state.select(idx);
}
fn selected(&self) -> Option<usize> {
self.list_state.selected()
}
fn prev_selected(&self) -> usize {
self.prev_selected
}
}
#[derive(Clone, Copy)]
enum UiFocus {
Library,
Queue,
}
impl QueueView {
fn check_focus(&mut self, focus: UiFocus) {
if !self.is_selected() && matches!(focus, UiFocus::Queue) {
self.select(Some(self.prev_selected()));
} else if self.is_selected() && !matches!(focus, UiFocus::Queue) {
self.select(None);
}
}
fn update(&mut self, queue: Queue) {
self.list = queue
.tracks
.iter()
.map(|t| UiItem {
uuid: t.uuid.clone(),
title: t.title.clone(),
})
.collect();
}
}
struct LibraryView { struct LibraryView {
title: String, title: String,
uuid: String, uuid: String,
list: StatefulList<UiItem>, list: Vec<UiItem>,
list_state: ListState,
parent: Option<String>, parent: Option<String>,
positions: HashMap<String, usize>,
}
impl ListView for LibraryView {
fn get_size(&self) -> usize {
self.list.len()
}
fn select(&mut self, idx: Option<usize>) {
if let Some(pos) = idx {
self.positions
.entry(self.uuid.clone())
.and_modify(|e| *e = pos)
.or_insert(pos);
}
self.list_state.select(idx);
}
fn selected(&self) -> Option<usize> {
self.list_state.selected()
}
fn prev_selected(&self) -> usize {
*self.positions.get(&self.uuid).unwrap_or(&0)
}
} }
impl LibraryView { impl LibraryView {
fn check_focus(&mut self, focus: UiFocus) {
if !self.is_selected() && matches!(focus, UiFocus::Library) {
self.select(Some(self.prev_selected()));
} else if self.is_selected() && !matches!(focus, UiFocus::Library) {
self.select(None);
}
}
fn get_selected(&self) -> Option<&UiItem> {
if let Some(idx) = self.list_state.selected() {
return Some(&self.list[idx]);
}
None
}
fn ascend(&mut self, tx: &Sender<MessageFromUi>) {
if let Some(parent) = self.parent.as_ref() {
tx.send(MessageFromUi::GetLibraryNode(parent.clone()));
}
}
fn dive(&mut self, tx: &Sender<MessageFromUi>) {
if let Some(item) = self.get_selected() {
tx.send(MessageFromUi::GetLibraryNode(item.uuid.clone()));
}
}
fn queue_replace_with_selected(&mut self, tx: &Sender<MessageFromUi>) {
if let Some(item) = self.get_selected() {
tx.send(MessageFromUi::ReplaceWithNode(item.uuid.clone()));
}
}
fn update(&mut self, node: LibraryNode) { fn update(&mut self, node: LibraryNode) {
if node.tracks.is_empty() && node.children.is_empty() { if node.tracks.is_empty() && node.children.is_empty() {
return; return;
} }
// if children empty and tracks empty return // if children empty and tracks empty return
self.uuid = node.uuid; self.uuid = node.uuid;
self.title = node.name; self.title = node.title;
self.parent = node.parent; self.parent = node.parent;
self.select(Some(self.prev_selected()));
if !node.tracks.is_empty() { if !node.tracks.is_empty() {
self.list.items = node self.list = node
.tracks .tracks
.iter() .iter()
.map(|t| UiItem { .map(|t| UiItem {
@ -131,21 +213,35 @@ impl LibraryView {
.collect(); .collect();
} else { } else {
// if tracks not empty use tracks instead // if tracks not empty use tracks instead
self.list.items = node self.list = node
.children .children
.iter() .iter()
.map(|c| UiItem { .map(|c| UiItem {
uuid: c.to_string(), uuid: c.uuid.clone(),
title: c.to_string(), title: c.title.clone(),
}) })
.collect(); .collect();
} }
} }
} }
struct NowPlayingView {
text: String,
}
impl NowPlayingView {
fn update(&mut self, active_track: ActiveTrack) {
if let Some(track_info) = active_track.track {
self.text = format!("Playing {} - {}", track_info.title, active_track.play_state);
}
}
}
struct App { struct App {
focus: UiFocus,
library: LibraryView, library: LibraryView,
queue: StatefulList<UiItem>, now_playing: NowPlayingView,
queue: QueueView,
} }
impl App { impl App {
@ -153,22 +249,38 @@ impl App {
let mut library = LibraryView { let mut library = LibraryView {
title: "Library".to_string(), title: "Library".to_string(),
uuid: "/".to_string(), uuid: "/".to_string(),
list: StatefulList::default(), list: Vec::new(),
list_state: ListState::default(),
positions: HashMap::new(),
parent: None, parent: None,
}; };
library.list.focus(); let queue = QueueView {
let mut queue = StatefulList::default(); list: Vec::new(),
App { library, queue } list_state: ListState::default(),
prev_selected: 0,
};
let now_playing = NowPlayingView {
text: "Not playing".to_string(),
};
App {
focus: UiFocus::Library,
library,
now_playing,
queue,
}
}
fn check_focus(&mut self) {
self.library.check_focus(self.focus);
self.queue.check_focus(self.focus);
} }
fn cycle_active(&mut self) { fn cycle_active(&mut self) {
if self.library.list.is_focused() { self.focus = match (self.focus, self.queue.is_empty()) {
self.library.list.blur(); (UiFocus::Library, false) => UiFocus::Queue,
self.queue.focus(); (UiFocus::Library, true) => UiFocus::Library,
} else { (UiFocus::Queue, _) => UiFocus::Library,
self.library.list.focus(); };
self.queue.blur();
}
} }
} }
@ -176,13 +288,15 @@ impl App {
enum MessageToUi { enum MessageToUi {
ReplaceLibraryNode(LibraryNode), ReplaceLibraryNode(LibraryNode),
QueueStreamUpdate(QueueUpdateResult), QueueStreamUpdate(QueueUpdateResult),
TrackStreamUpdate(GetTrackUpdatesResponse), TrackStreamUpdate(ActiveTrack),
} }
// FIXME: Rename this // FIXME: Rename this
enum MessageFromUi { enum MessageFromUi {
Quit, Quit,
GetLibraryNode(String), GetLibraryNode(String),
ReplaceWithNode(String),
TogglePlay,
} }
async fn orchestrate<'a>( async fn orchestrate<'a>(
@ -191,7 +305,6 @@ async fn orchestrate<'a>(
let mut rpc_client = rpc::RpcClient::connect("http://[::1]:50051").await?; let mut rpc_client = rpc::RpcClient::connect("http://[::1]:50051").await?;
if let Some(root_node) = rpc_client.get_library_node("/").await? { if let Some(root_node) = rpc_client.get_library_node("/").await? {
// FIXME: Is it ok to clone here?
tx.send(MessageToUi::ReplaceLibraryNode(root_node.clone())); tx.send(MessageToUi::ReplaceLibraryNode(root_node.clone()));
} }
@ -210,6 +323,12 @@ async fn orchestrate<'a>(
if let Some(node) = rpc_client.get_library_node(&uuid).await? { if let Some(node) = rpc_client.get_library_node(&uuid).await? {
tx.send(MessageToUi::ReplaceLibraryNode(node.clone())); tx.send(MessageToUi::ReplaceLibraryNode(node.clone()));
} }
},
MessageFromUi::ReplaceWithNode(uuid) => {
rpc_client.replace_queue_with(&uuid).await?
}
MessageFromUi::TogglePlay => {
rpc_client.toggle_play().await?
} }
} }
} }
@ -219,7 +338,10 @@ async fn orchestrate<'a>(
} }
} }
Some(Ok(resp)) = track_update_stream.next() => { Some(Ok(resp)) = track_update_stream.next() => {
tx.send(MessageToUi::TrackStreamUpdate(resp)); if let Some(active_track) = resp.active_track {
tx.send_async(MessageToUi::TrackStreamUpdate(active_track)).await;
}
} }
} }
} }
@ -262,15 +384,14 @@ fn run_ui(tx: Sender<MessageFromUi>, rx: Receiver<MessageToUi>) {
app.library.update(node); app.library.update(node);
} }
MessageToUi::QueueStreamUpdate(queue_update) => match queue_update { MessageToUi::QueueStreamUpdate(queue_update) => match queue_update {
QueueUpdateResult::Full(queue) => {} QueueUpdateResult::Full(queue) => {
QueueUpdateResult::PositionChange(pos) => { app.queue.update(queue);
app.queue.items.push(UiItem {
uuid: pos.timestamp.to_string(),
title: pos.timestamp.to_string(),
});
} }
QueueUpdateResult::PositionChange(pos) => {}
}, },
_ => {} MessageToUi::TrackStreamUpdate(active_track) => {
app.now_playing.update(active_track);
}
} }
} }
@ -283,36 +404,36 @@ fn run_ui(tx: Sender<MessageFromUi>, rx: Receiver<MessageToUi>) {
if crossterm::event::poll(timeout).unwrap() { if crossterm::event::poll(timeout).unwrap() {
if let Event::Key(key) = event::read().unwrap() { if let Event::Key(key) = event::read().unwrap() {
if key.kind == KeyEventKind::Press { if key.kind == KeyEventKind::Press {
match key.code { match (app.focus, key.code) {
KeyCode::Char('q') => { (_, KeyCode::Char('q')) => {
tx.send(MessageFromUi::Quit); tx.send(MessageFromUi::Quit);
break; break;
} }
KeyCode::Char('j') => { (_, KeyCode::Tab) => app.cycle_active(),
if app.library.list.is_focused() { (_, KeyCode::Char(' ')) => {
app.library.list.next(); tx.send(MessageFromUi::TogglePlay);
} else { }
(UiFocus::Library, KeyCode::Char('j')) => {
app.library.next();
}
(UiFocus::Library, KeyCode::Char('k')) => {
app.library.prev();
}
(UiFocus::Library, KeyCode::Char('h')) => {
app.library.ascend(&tx);
}
(UiFocus::Library, KeyCode::Char('l')) => {
app.library.dive(&tx);
}
(UiFocus::Library, KeyCode::Enter) => {
app.library.queue_replace_with_selected(&tx);
}
(UiFocus::Queue, KeyCode::Char('j')) => {
app.queue.next(); app.queue.next();
} }
} (UiFocus::Queue, KeyCode::Char('k')) => {
KeyCode::Char('k') => {
if app.library.list.is_focused() {
app.library.list.prev();
} else {
app.queue.prev(); app.queue.prev();
} }
}
KeyCode::Tab => app.cycle_active(),
KeyCode::Char('h') => {
if let Some(parent) = app.library.parent.as_ref() {
tx.send(MessageFromUi::GetLibraryNode(parent.clone()));
}
}
KeyCode::Char('l') => {
if let Some(item) = app.library.list.get_selected() {
tx.send(MessageFromUi::GetLibraryNode(item.uuid.clone()));
}
}
_ => {} _ => {}
} }
} }
@ -320,6 +441,7 @@ fn run_ui(tx: Sender<MessageFromUi>, rx: Receiver<MessageToUi>) {
} }
if last_tick.elapsed() >= tick_rate { if last_tick.elapsed() >= tick_rate {
app.check_focus();
last_tick = Instant::now(); last_tick = Instant::now();
} }
} }
@ -346,21 +468,24 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
let library_items: Vec<ListItem> = app let library_items: Vec<ListItem> = app
.library .library
.list .list
.items
.iter() .iter()
// FIXME: why to_string() ?? // FIXME: why to_string() ??
.map(|i| ListItem::new(Span::from(i.title.to_string()))) .map(|i| ListItem::new(Span::from(i.title.to_string())))
.collect(); .collect();
let library_list = List::new(library_items) let library_list = List::new(library_items)
.block(Block::default().borders(Borders::ALL).title(app.library.title.clone())) .block(
Block::default()
.borders(Borders::ALL)
.title(app.library.title.clone()),
)
.highlight_style( .highlight_style(
Style::default() Style::default()
.bg(Color::LightBlue) .bg(Color::LightBlue)
.add_modifier(Modifier::BOLD), .add_modifier(Modifier::BOLD),
); );
f.render_stateful_widget(library_list, main[0], &mut app.library.list.state); f.render_stateful_widget(library_list, main[0], &mut app.library.list_state);
let now_playing = Layout::default() let now_playing = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
@ -369,7 +494,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
let queue_items: Vec<ListItem> = app let queue_items: Vec<ListItem> = app
.queue .queue
.items .list
.iter() .iter()
// FIXME: why to_string() ?? // FIXME: why to_string() ??
.map(|i| ListItem::new(Span::from(i.title.to_string()))) .map(|i| ListItem::new(Span::from(i.title.to_string())))
@ -383,11 +508,16 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
.add_modifier(Modifier::BOLD), .add_modifier(Modifier::BOLD),
); );
f.render_stateful_widget(queue_list, now_playing[0], &mut app.queue.state); f.render_stateful_widget(queue_list, now_playing[0], &mut app.queue.list_state);
let media_info = Block::default() let media_info = Block::default()
.title("Now playing") .title("Now playing")
.borders(Borders::ALL) .borders(Borders::ALL)
.style(Style::default().bg(Color::Black)); .style(Style::default());
f.render_widget(media_info, now_playing[1]);
let now_playing_text = Paragraph::new(app.now_playing.text.to_string())
.block(media_info)
.alignment(Alignment::Center);
// f.render_widget(media_info, now_playing[1]);
f.render_widget(now_playing_text, now_playing[1]);
} }

View File

@ -1,7 +1,8 @@
use crabidy_core::proto::crabidy::{ use crabidy_core::proto::crabidy::{
crabidy_service_client::CrabidyServiceClient, get_queue_updates_response::QueueUpdateResult, crabidy_service_client::CrabidyServiceClient, get_queue_updates_response::QueueUpdateResult,
GetLibraryNodeRequest, GetQueueUpdatesRequest, GetQueueUpdatesResponse, GetTrackUpdatesRequest, GetLibraryNodeRequest, GetQueueUpdatesRequest, GetQueueUpdatesResponse, GetTrackUpdatesRequest,
GetTrackUpdatesResponse, LibraryNode, LibraryNodeState, GetTrackUpdatesResponse, LibraryNode, LibraryNodeState, ReplaceWithNodeRequest,
ReplaceWithNodeResponse, TogglePlayRequest,
}; };
use std::{ use std::{
@ -44,6 +45,7 @@ impl RpcClient {
library_node_cache, library_node_cache,
}) })
} }
pub async fn get_library_node( pub async fn get_library_node(
&mut self, &mut self,
uuid: &str, uuid: &str,
@ -102,4 +104,28 @@ impl RpcClient {
.into_inner(); .into_inner();
Ok(stream) Ok(stream)
} }
pub async fn replace_queue_with(&mut self, uuid: &str) -> Result<(), Box<dyn Error>> {
let replace_with_node_request = Request::new(ReplaceWithNodeRequest {
uuid: uuid.to_string(),
});
let response = self
.client
.replace_with_node(replace_with_node_request)
.await?;
Ok(())
}
pub async fn toggle_play(&mut self) -> Result<(), Box<dyn Error>> {
let toggle_play_request = Request::new(TogglePlayRequest {});
let response = self
.client
.toggle_play(toggle_play_request)
.await?;
Ok(())
}
} }