added song updates
This commit is contained in:
+77
-29
@@ -12,8 +12,9 @@ use ratatui::{
|
||||
style::Stylize,
|
||||
widgets::{Block, Paragraph},
|
||||
};
|
||||
use std::io::{self};
|
||||
use std::io;
|
||||
use tokio::process::Child;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum InputMode {
|
||||
@@ -30,6 +31,9 @@ pub struct App {
|
||||
search_query: String,
|
||||
input_mode: InputMode,
|
||||
player: Option<Child>,
|
||||
current_song: Option<String>,
|
||||
played_songs: Vec<String>,
|
||||
song_rx: Option<mpsc::UnboundedReceiver<String>>,
|
||||
}
|
||||
|
||||
impl Default for App {
|
||||
@@ -46,6 +50,9 @@ impl Default for App {
|
||||
input_mode: InputMode::Normal,
|
||||
search_query: String::new(),
|
||||
player: None,
|
||||
current_song: None,
|
||||
played_songs: Vec::new(),
|
||||
song_rx: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,6 +64,7 @@ impl App {
|
||||
self.handle_events()
|
||||
.await
|
||||
.wrap_err("handle events failed")?;
|
||||
self.check_song_updates();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -72,10 +80,10 @@ impl App {
|
||||
Layout::vertical([Constraint::Percentage(10), Constraint::Percentage(90)]).areas(right);
|
||||
|
||||
let [top_left, bottom_left] =
|
||||
Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)]).areas(left);
|
||||
Layout::vertical([Constraint::Percentage(10), Constraint::Percentage(90)]).areas(left);
|
||||
|
||||
// Render the left block (e.g., station selection)
|
||||
let max_name_len = top_left.width.saturating_sub(4) as usize;
|
||||
// Station list
|
||||
let max_name_len = bottom_left.width.saturating_sub(4) as usize;
|
||||
let station_items: Vec<ListItem> = self
|
||||
.filtered_stations
|
||||
.iter()
|
||||
@@ -96,30 +104,34 @@ impl App {
|
||||
|
||||
frame.render_stateful_widget(
|
||||
stations_list,
|
||||
top_left,
|
||||
bottom_left,
|
||||
&mut self.station_list_state.clone(),
|
||||
);
|
||||
|
||||
let station = self
|
||||
.station_list_state
|
||||
.selected()
|
||||
.and_then(|i| self.filtered_stations.get(i));
|
||||
let paragraph = Paragraph::new(
|
||||
station
|
||||
.map(|s| format!("Now selected: {}\n{}", s.name, s.url))
|
||||
.unwrap_or_else(|| "No station selected".into()),
|
||||
)
|
||||
.block(Block::bordered().title("Info"));
|
||||
frame.render_widget(paragraph, bottom_left);
|
||||
// Info: show current song
|
||||
let info_text = match &self.current_song {
|
||||
Some(song) => format!("Now playing: {}", song),
|
||||
None => "No song playing".to_string(),
|
||||
};
|
||||
let paragraph = Paragraph::new(info_text).block(Block::bordered().title("Info"));
|
||||
frame.render_widget(paragraph, top_left);
|
||||
|
||||
// Search box
|
||||
let mut search_string = self.search_query.clone();
|
||||
if self.input_mode == InputMode::Search {
|
||||
search_string.push('█');
|
||||
}
|
||||
let search = Paragraph::new(search_string).block(Block::bordered().title("Search"));
|
||||
|
||||
frame.render_widget(search, top_right);
|
||||
frame.render_widget(Block::bordered().title("Played Songs"), bottom_right);
|
||||
|
||||
// Played Songs list
|
||||
let song_items: Vec<ListItem> = self
|
||||
.played_songs
|
||||
.iter()
|
||||
.map(|s| ListItem::new(s.clone()))
|
||||
.collect();
|
||||
let songs_list = List::new(song_items).block(Block::bordered().title("Played Songs"));
|
||||
frame.render_widget(songs_list, bottom_right);
|
||||
}
|
||||
|
||||
async fn handle_events(&mut self) -> io::Result<()> {
|
||||
@@ -132,6 +144,32 @@ impl App {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_song_updates(&mut self) {
|
||||
let mut new_songs = Vec::new();
|
||||
|
||||
if let Some(rx) = &mut self.song_rx {
|
||||
while let Ok(song) = rx.try_recv() {
|
||||
new_songs.push(song);
|
||||
}
|
||||
}
|
||||
|
||||
for song in new_songs {
|
||||
self.update_current_song(song);
|
||||
}
|
||||
}
|
||||
|
||||
fn update_current_song(&mut self, song: String) {
|
||||
if let Some(prev) = &self.current_song {
|
||||
if prev != &song {
|
||||
self.played_songs.insert(0, prev.clone());
|
||||
if self.played_songs.len() > 20 {
|
||||
self.played_songs.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
self.current_song = Some(song);
|
||||
}
|
||||
|
||||
pub async fn handle_key_event(&mut self, key_event: KeyEvent) {
|
||||
match self.input_mode {
|
||||
InputMode::Normal => match key_event.code {
|
||||
@@ -192,7 +230,6 @@ impl App {
|
||||
let stations = api
|
||||
.get_stations()
|
||||
.country("Germany")
|
||||
.limit("50")
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| eyre!("Failed to fetch station: {e}"))?;
|
||||
@@ -242,20 +279,31 @@ impl App {
|
||||
let _ = child.kill().await;
|
||||
}
|
||||
|
||||
let child = tokio::process::Command::new("mpv")
|
||||
let (tx, rx) = mpsc::unbounded_channel();
|
||||
self.song_rx = Some(rx);
|
||||
|
||||
let mut child = tokio::process::Command::new("mpv")
|
||||
.arg(&url)
|
||||
.arg("--no-video")
|
||||
.stdout(std::process::Stdio::null())
|
||||
.arg("--quiet")
|
||||
.arg("--term-playing-msg=${media-title}")
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.spawn();
|
||||
.spawn()
|
||||
.expect("Failed to spawn mpv");
|
||||
|
||||
let mut stdout = child.stdout.take().expect("No stdout");
|
||||
tokio::spawn(async move {
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
let mut reader = BufReader::new(&mut stdout).lines();
|
||||
while let Ok(Some(line)) = reader.next_line().await {
|
||||
let title = line.trim().to_string();
|
||||
if !title.is_empty() {
|
||||
let _ = tx.send(title);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
match child {
|
||||
Ok(child) => {
|
||||
self.player = Some(child);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to play station: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user