added informationa and url yank
This commit is contained in:
+72
-8
@@ -2,10 +2,10 @@ use super::{App, InputMode};
|
||||
use crate::radio::station::StationInfo;
|
||||
use ratatui::{
|
||||
Frame,
|
||||
layout::{Constraint, Layout},
|
||||
layout::{Constraint, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, List, ListItem, Paragraph},
|
||||
widgets::{Block, Clear, List, ListItem, Paragraph, Wrap},
|
||||
};
|
||||
|
||||
impl App {
|
||||
@@ -31,6 +31,12 @@ impl App {
|
||||
frame.render_widget(self.info_panel(), top_right);
|
||||
frame.render_widget(self.played_songs_list(), bottom_right);
|
||||
frame.render_widget(Paragraph::new(self.footer_hints()), footer);
|
||||
|
||||
if self.input_mode == InputMode::Info {
|
||||
let popup_area = centered_rect(area, 62, 8);
|
||||
frame.render_widget(Clear, popup_area);
|
||||
frame.render_widget(self.station_info_popup(), popup_area);
|
||||
}
|
||||
}
|
||||
|
||||
fn info_panel(&self) -> Paragraph<'static> {
|
||||
@@ -153,24 +159,71 @@ impl App {
|
||||
List::new(items).block(Block::bordered().title("Played Songs"))
|
||||
}
|
||||
|
||||
fn station_info_popup(&self) -> Paragraph<'static> {
|
||||
let station = self
|
||||
.station_list_state
|
||||
.selected()
|
||||
.and_then(|i| self.filtered_stations.get(i));
|
||||
|
||||
let lines = if let Some(s) = station {
|
||||
let status_line = if let Some(msg) = &self.clipboard_message {
|
||||
let color = if msg.starts_with("Error") { Color::Red } else { Color::Green };
|
||||
Line::from(Span::styled(msg.clone(), Style::default().fg(color)))
|
||||
} else {
|
||||
Line::from(Span::styled(
|
||||
"c copy url Esc / Enter / i close",
|
||||
Style::default().add_modifier(Modifier::DIM),
|
||||
))
|
||||
};
|
||||
vec![
|
||||
Line::from(Span::styled(
|
||||
s.name.clone(),
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
)),
|
||||
Line::from(""),
|
||||
Line::from(Span::styled(s.url.clone(), Style::default().fg(Color::Cyan))),
|
||||
Line::from(""),
|
||||
status_line,
|
||||
]
|
||||
} else {
|
||||
vec![Line::from(Span::styled(
|
||||
"No station selected",
|
||||
Style::default().add_modifier(Modifier::DIM),
|
||||
))]
|
||||
};
|
||||
|
||||
Paragraph::new(lines)
|
||||
.wrap(Wrap { trim: false })
|
||||
.block(
|
||||
Block::bordered()
|
||||
.title("Station Info [i]")
|
||||
.border_style(Style::default().fg(Color::Cyan)),
|
||||
)
|
||||
}
|
||||
|
||||
fn footer_hints(&self) -> Line<'static> {
|
||||
if self.input_mode == InputMode::Search {
|
||||
Line::from(vec![
|
||||
match self.input_mode {
|
||||
InputMode::Search => Line::from(vec![
|
||||
key_hint(" Esc "), Span::raw(" cancel "),
|
||||
key_hint(" Enter "), Span::raw(" confirm"),
|
||||
])
|
||||
} else {
|
||||
Line::from(vec![
|
||||
]),
|
||||
InputMode::Info => Line::from(vec![
|
||||
key_hint(" Esc "), Span::raw(" / "),
|
||||
key_hint(" Enter "), Span::raw(" / "),
|
||||
key_hint(" i "), Span::raw(" close"),
|
||||
]),
|
||||
InputMode::Normal => Line::from(vec![
|
||||
key_hint(" / "), Span::raw(" search "),
|
||||
key_hint(" Esc "), Span::raw(" clear "),
|
||||
key_hint(" j/k "), Span::raw(" navigate "),
|
||||
key_hint(" ^U/^D "), Span::raw(" half page "),
|
||||
key_hint(" Enter "), Span::raw(" play "),
|
||||
key_hint(" s "), Span::raw(" stop "),
|
||||
key_hint(" i "), Span::raw(" info "),
|
||||
key_hint(" f "), Span::raw(" favorite "),
|
||||
key_hint(" Tab "), Span::raw(" all/favs "),
|
||||
key_hint(" q "), Span::raw(" quit"),
|
||||
])
|
||||
]),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -184,3 +237,14 @@ fn key_hint(label: &'static str) -> Span<'static> {
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)
|
||||
}
|
||||
|
||||
fn centered_rect(area: Rect, width: u16, height: u16) -> Rect {
|
||||
let x = area.x + area.width.saturating_sub(width) / 2;
|
||||
let y = area.y + area.height.saturating_sub(height) / 2;
|
||||
Rect {
|
||||
x,
|
||||
y,
|
||||
width: width.min(area.width),
|
||||
height: height.min(area.height),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ impl App {
|
||||
match self.input_mode {
|
||||
InputMode::Normal => self.handle_normal_key(key_event).await,
|
||||
InputMode::Search => self.handle_search_key(key_event),
|
||||
InputMode::Info => self.handle_info_key(key_event),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +34,7 @@ impl App {
|
||||
self.show_favorites_only = !self.show_favorites_only;
|
||||
self.update_filter();
|
||||
}
|
||||
KeyCode::Char('i') => self.input_mode = InputMode::Info,
|
||||
KeyCode::Char('f') => self.toggle_favorite(),
|
||||
KeyCode::Char('s') => self.stop_playback().await,
|
||||
KeyCode::Esc if !self.search_query.is_empty() => {
|
||||
@@ -75,6 +77,30 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_info_key(&mut self, key_event: KeyEvent) {
|
||||
match key_event.code {
|
||||
KeyCode::Esc | KeyCode::Enter | KeyCode::Char('i') | KeyCode::Char('q') => {
|
||||
self.clipboard_message = None;
|
||||
self.input_mode = InputMode::Normal;
|
||||
self.needs_clear = true;
|
||||
}
|
||||
KeyCode::Char('c') => {
|
||||
let url = self
|
||||
.station_list_state
|
||||
.selected()
|
||||
.and_then(|i| self.filtered_stations.get(i))
|
||||
.map(|s| s.url.clone());
|
||||
if let Some(url) = url {
|
||||
self.clipboard_message = Some(match self.copy_url_to_clipboard(&url) {
|
||||
Ok(()) => "Copied!".into(),
|
||||
Err(e) => format!("Error: {e}"),
|
||||
});
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_search_key(&mut self, key_event: KeyEvent) {
|
||||
match key_event.code {
|
||||
KeyCode::Char(c) => {
|
||||
|
||||
@@ -17,10 +17,12 @@ mod player;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum InputMode {
|
||||
Normal,
|
||||
Search,
|
||||
Info,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -40,6 +42,9 @@ pub struct App {
|
||||
spawn_player: Option<player::SpawnFn>,
|
||||
favorites: HashSet<String>,
|
||||
show_favorites_only: bool,
|
||||
clipboard_message: Option<String>,
|
||||
clipboard_child: Option<std::process::Child>,
|
||||
needs_clear: bool,
|
||||
}
|
||||
|
||||
impl Default for App {
|
||||
@@ -64,11 +69,48 @@ impl Default for App {
|
||||
spawn_player: Some(player::new_mpv_spawn()),
|
||||
favorites: favorites::load_favorites(),
|
||||
show_favorites_only: false,
|
||||
clipboard_message: None,
|
||||
clipboard_child: None,
|
||||
needs_clear: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub(super) fn copy_url_to_clipboard(&mut self, url: &str) -> Result<(), String> {
|
||||
use std::io::Write;
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
if let Some(mut old) = self.clipboard_child.take() {
|
||||
old.kill().ok();
|
||||
}
|
||||
|
||||
let tools: &[(&str, &[&str])] = &[
|
||||
("wl-copy", &[]),
|
||||
("xclip", &["-selection", "clipboard"]),
|
||||
("xsel", &["--clipboard", "--input"]),
|
||||
];
|
||||
|
||||
for (cmd, args) in tools {
|
||||
let Ok(mut child) = Command::new(cmd)
|
||||
.args(*args)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if let Some(mut stdin) = child.stdin.take() {
|
||||
stdin.write_all(url.as_bytes()).ok();
|
||||
}
|
||||
self.clipboard_child = Some(child);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err("No clipboard tool found — install wl-copy, xclip, or xsel".into())
|
||||
}
|
||||
|
||||
pub fn without_player(mut self) -> Self {
|
||||
self.spawn_player = None;
|
||||
self
|
||||
@@ -76,6 +118,10 @@ impl App {
|
||||
|
||||
pub async fn run(&mut self, terminal: &mut DefaultTerminal) -> Result<()> {
|
||||
while !self.exit {
|
||||
if self.needs_clear {
|
||||
terminal.clear().wrap_err("terminal clear failed")?;
|
||||
self.needs_clear = false;
|
||||
}
|
||||
terminal.draw(|frame| self.draw(frame))?;
|
||||
self.handle_events()
|
||||
.await
|
||||
|
||||
Reference in New Issue
Block a user