git/grrs/main

fix: replace integrated history with browser redirect

removed all of the background polling/reqwest logic for the /history/ endpoint because cloudflare is cloudcucking us by tarpitting/rate-limiting us. sigh. deleted related models, appstate fields, appevent variants. removed the internal history tab from the ui. added a global `h` keybind that opens https://app.gensokyoradio.net/history if someone would like to access the history. this closes 9fa0f3b. really unfortunate, this. kinda bummed. regardless, i've also added a retry function for retrying the connection
diff --git a/src/app.rs b/src/app.rs
index 234457c..a2912df 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -1,7 +1,6 @@
use std::{
- collections::{HashMap, hash_map::DefaultHasher},
+ collections::HashMap,
env, fs,
- hash::{Hash, Hasher},
path::PathBuf,
time::{Instant, SystemTime, UNIX_EPOCH},
};
@@ -14,7 +13,7 @@ use souvlaki::MediaControlEvent;
use crate::{
config::AppConfig,
- models::{HistorySong, LikedSong, MpvSong, SongData},
+ models::{LikedSong, MpvSong, SongData},
};
pub const REFRESH_WINDOW: std::time::Duration = std::time::Duration::from_secs(30);
@@ -23,7 +22,6 @@ pub const REFRESH_WINDOW: std::time::Duration = std::time::Duration::from_secs(3
pub enum AppMode {
Playing,
LikedSongs,
- History,
ConfigError(Vec<String>),
}
@@ -36,8 +34,6 @@ pub enum AppEvent {
LocalArtLoaded(i64, Option<StatefulProtocol>),
MediaControl(MediaControlEvent),
ApiUpdate { listeners: u64, circle_link: String, rating: String, times_rated: u64 },
- HistoryUpdate(Vec<HistorySong>),
- HistoryArtLoaded(usize, Option<StatefulProtocol>),
MpvUpdate(MpvSong),
KosuzuBusy(bool),
}
@@ -51,9 +47,6 @@ pub struct AppState {
pub likes: HashMap<i64, LikedSong>,
pub filtered_likes: Vec<LikedSong>,
pub likes_file: PathBuf,
- pub history: Vec<HistorySong>,
- pub history_list_state: ListState,
- pub selected_history_art: Option<StatefulProtocol>,
pub status_message: String,
pub status_message_time: Option<Instant>,
pub is_muted: bool,
@@ -78,7 +71,7 @@ pub struct AppState {
pub config_scroll: u16,
pub notified_song_id: i64,
pub nav_time: Option<Instant>,
- pub cache_dir: PathBuf,
+ pub mpv_retries: u8,
}
impl AppState {
@@ -120,7 +113,6 @@ impl AppState {
fs::create_dir_all(&data_dir).ok();
fs::create_dir_all(&cache_dir).ok();
- fs::create_dir_all(cache_dir.join("history_art")).ok();
let old_likes = config_dir.join("likes.json");
let new_likes = data_dir.join("likes.json");
@@ -160,6 +152,7 @@ impl AppState {
let secondary_color = config.get_secondary_text();
let startup_volume = config.player.startup_volume.min(100);
+ let mpv_retries = config.player.connection_retries;
Self {
config,
@@ -170,9 +163,6 @@ impl AppState {
likes,
filtered_likes,
likes_file,
- history: Vec::new(),
- history_list_state: ListState::default(),
- selected_history_art: None,
status_message: "Connecting to Gensokyo Radio...".to_string(),
status_message_time: Some(Instant::now()),
is_muted: false,
@@ -197,17 +187,10 @@ impl AppState {
config_scroll: 0,
notified_song_id: 0,
nav_time: None,
- cache_dir,
+ mpv_retries,
}
}
- pub fn generate_id(title: &str, artist: &str) -> i64 {
- let mut hasher = DefaultHasher::new();
- title.hash(&mut hasher);
- artist.hash(&mut hasher);
- hasher.finish() as i64
- }
-
pub fn toggle_like(&mut self) {
if self.song.id == 0 {
return;
diff --git a/src/config.rs b/src/config.rs
index 435ee93..1eb173b 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -18,6 +18,7 @@ pub struct PlayerConfig {
pub startup_volume: u8,
pub loudness_normalization: bool,
pub stream_url: String,
+ pub connection_retries: u8,
}
impl Default for PlayerConfig {
@@ -26,6 +27,7 @@ impl Default for PlayerConfig {
startup_volume: 100,
loudness_normalization: false,
stream_url: "https://stream.gensokyoradio.net/3".to_string(),
+ connection_retries: 15,
}
}
}
diff --git a/src/input.rs b/src/input.rs
index 3d2594a..d7aa402 100644
--- a/src/input.rs
+++ b/src/input.rs
@@ -1,18 +1,10 @@
-use std::{
- fs,
- io::Write,
- os::unix::net::UnixStream,
- time::{SystemTime, UNIX_EPOCH},
-};
+use std::{fs, io::Write, os::unix::net::UnixStream};
use crossterm::event::{Event as CEvent, KeyCode, KeyModifiers, MouseButton, MouseEventKind};
use ratatui_image::picker::Picker;
use tokio::sync::mpsc;
-use crate::{
- app::{AppEvent, AppMode, AppState},
- models::LikedSong,
-};
+use crate::app::{AppEvent, AppMode, AppState};
pub struct InputOutcome {
pub should_quit: bool,
@@ -177,6 +169,14 @@ pub async fn handle_input(
let _ = stream.write_all(cmd.as_bytes());
}
return InputOutcome { should_quit: false, should_clear: clear };
+ } else if key.code == KeyCode::Char('h') {
+ if webbrowser::open("https://app.gensokyoradio.net/history").is_ok() {
+ state.status_message = "opened history in browser!".to_string();
+ } else {
+ state.status_message = "failed to open browser".to_string();
+ }
+ state.status_message_time = Some(std::time::Instant::now());
+ return InputOutcome { should_quit: false, should_clear: clear };
}
if state.mode == AppMode::Playing {
@@ -246,26 +246,7 @@ pub async fn handle_input(
state.status_message_time = Some(std::time::Instant::now());
}
} else if state.mode == AppMode::LikedSongs {
- if key.code == KeyCode::Tab {
- state.mode = AppMode::History;
- state.selected_art = None;
- state.selected_history_art = None;
- state.nav_time = Some(std::time::Instant::now());
- if !state.history.is_empty() {
- if state.history_list_state.selected().is_none() {
- state.history_list_state.select(Some(0));
- }
- let idx = state.history_list_state.selected().unwrap_or(0);
- fetch_history_art(
- idx,
- state.history[idx].album_art.clone(),
- state.cache_dir.join("history_art"),
- tx.clone(),
- picker.clone(),
- http_client.clone(),
- );
- }
- } else if key.code == KeyCode::Esc {
+ if key.code == KeyCode::Tab || key.code == KeyCode::Esc {
state.mode = AppMode::Playing;
state.selected_art = None;
state.nav_time = Some(std::time::Instant::now());
@@ -349,143 +330,6 @@ pub async fn handle_input(
state.status_message_time = Some(std::time::Instant::now());
}
}
- } else if state.mode == AppMode::History {
- if key.code == KeyCode::Tab || key.code == KeyCode::Esc {
- state.mode = AppMode::Playing;
- state.nav_time = Some(std::time::Instant::now());
- state.selected_history_art = None;
- } else if key.code == KeyCode::Up || key.code == KeyCode::Char('k') {
- if !state.history.is_empty() {
- let current = state.history_list_state.selected().unwrap_or(0);
- let next = if current == 0 { state.history.len() - 1 } else { current - 1 };
- state.history_list_state.select(Some(next));
- if current != next {
- state.selected_history_art = None;
- fetch_history_art(
- next,
- state.history[next].album_art.clone(),
- state.cache_dir.join("history_art"),
- tx.clone(),
- picker.clone(),
- http_client.clone(),
- );
- }
- }
- } else if key.code == KeyCode::Down || key.code == KeyCode::Char('j') {
- if !state.history.is_empty() {
- let current = state.history_list_state.selected().unwrap_or(0);
- let next = if current == state.history.len() - 1 { 0 } else { current + 1 };
- state.history_list_state.select(Some(next));
- if current != next {
- state.selected_history_art = None;
- fetch_history_art(
- next,
- state.history[next].album_art.clone(),
- state.cache_dir.join("history_art"),
- tx.clone(),
- picker.clone(),
- http_client.clone(),
- );
- }
- }
- } else if key.code == KeyCode::Char('l') {
- if let Some(idx) = state.history_list_state.selected() {
- if let Some(hist_song) = state.history.get(idx).cloned() {
- let fake_id = AppState::generate_id(&hist_song.title, &hist_song.artist);
- if state.likes.contains_key(&fake_id) {
- state.show_unlike_confirm = Some(fake_id);
- } else {
- let ts = SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .expect("system time went backwards")
- .as_secs();
- state.likes.insert(
- fake_id,
- LikedSong {
- id: fake_id,
- title: hist_song.title.clone(),
- artist: hist_song.artist.clone(),
- album: hist_song.album.clone(),
- duration: 0,
- timestamp: ts,
- circle: hist_song.circle.clone(),
- vocal: String::new(),
- original: String::new(),
- year: String::new(),
- },
- );
- if let Ok(data) = serde_json::to_string_pretty(&state.likes) {
- fs::write(&state.likes_file, data).ok();
- }
- state.update_search();
-
- state.status_message = "song liked!".to_string();
- state.status_message_time = Some(std::time::Instant::now());
-
- if !hist_song.album_art.is_empty() {
- let art_dir = state
- .likes_file
- .parent()
- .expect("likes_file has no parent directory")
- .join("art");
- fs::create_dir_all(&art_dir).ok();
- let dest_path = state.art_path(fake_id);
- let src_path = state.cache_dir.join("history_art").join(&hist_song.album_art);
- let client = http_client.clone();
- let art_url = hist_song.album_art.clone();
- tokio::spawn(async move {
- if src_path.exists() {
- let _ = tokio::fs::copy(src_path, dest_path).await;
- } else {
- let url =
- format!("https://gensokyoradio.net/images/albums/500/{}", art_url);
- if let Ok(resp) = client.get(&url).send().await
- && let Ok(bytes) = resp.bytes().await
- {
- let _ = tokio::fs::write(dest_path, bytes).await;
- }
- }
- });
- }
- }
- }
- }
- } else if key.code == KeyCode::Char('c') || key.code == KeyCode::Char('C') {
- if let Some(idx) = state.history_list_state.selected() {
- if let Some(song) = state.history.get(idx)
- && let Ok(mut clipboard) = arboard::Clipboard::new()
- {
- let text = if key.code == KeyCode::Char('c') {
- format!("{} - {}", song.artist, song.title)
- } else {
- format!(
- "Title: {}\nArtist: {}\nAlbum: {}\nCircle: {}",
- song.title, song.artist, song.album, song.circle
- )
- };
- let _ = clipboard.set_text(text);
- state.status_message = if key.code == KeyCode::Char('c') {
- "copied track/artist!".to_string()
- } else {
- "copied full metadata!".to_string()
- };
- state.status_message_time = Some(std::time::Instant::now());
- }
- }
- } else if (key.code == KeyCode::Char('o') || key.code == KeyCode::Enter)
- && let Some(idx) = state.history_list_state.selected()
- {
- if let Some(song) = state.history.get(idx) {
- let query = urlencoding::encode(&song.album);
- let url = format!("https://touhoudb.com/Search?searchType=Album&filter={query}");
- if webbrowser::open(&url).is_ok() {
- state.status_message = "opened touhoudb in browser!".to_string();
- } else {
- state.status_message = "failed to open browser".to_string();
- }
- state.status_message_time = Some(std::time::Instant::now());
- }
- }
}
},
CEvent::Mouse(mouse) => {
@@ -529,56 +373,3 @@ fn load_local_art(id: i64, path: std::path::PathBuf, tx: mpsc::Sender<AppEvent>,
let _ = tx.send(AppEvent::LocalArtLoaded(id, None)).await;
});
}
-
-pub fn fetch_history_art(
- idx: usize,
- filename: String,
- cache_dir: std::path::PathBuf,
- tx: mpsc::Sender<AppEvent>,
- picker: Picker,
- client: reqwest::Client,
-) {
- if filename.is_empty() {
- tokio::spawn(async move {
- let _ = tx.send(AppEvent::HistoryArtLoaded(idx, None)).await;
- });
- return;
- }
-
- tokio::spawn(async move {
- let path = cache_dir.join(&filename);
- let mut bytes = vec![];
-
- if path.exists() {
- if let Ok(b) = tokio::fs::read(&path).await {
- bytes = b;
- }
- }
-
- if bytes.is_empty() {
- let url = format!("https://gensokyoradio.net/images/albums/500/{}", filename);
- if let Ok(resp) = client.get(&url).send().await
- && let Ok(b) = resp.bytes().await
- {
- bytes = b.to_vec();
- let _ = tokio::fs::write(&path, &bytes).await;
- }
- }
-
- if !bytes.is_empty() {
- let p_clone = picker.clone();
- let decode_result = tokio::task::spawn_blocking(move || {
- if let Ok(img) = image::load_from_memory(&bytes) {
- return Some(p_clone.new_resize_protocol(img));
- }
- None
- })
- .await
- .unwrap_or(None);
-
- let _ = tx.send(AppEvent::HistoryArtLoaded(idx, decode_result)).await;
- return;
- }
- let _ = tx.send(AppEvent::HistoryArtLoaded(idx, None)).await;
- });
-}
diff --git a/src/main.rs b/src/main.rs
index 9a22276..4943c89 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -30,7 +30,7 @@ use tokio_tungstenite::{connect_async, tungstenite::protocol::Message};
use crate::{
app::{AppEvent, AppMode, AppState, REFRESH_WINDOW},
input::handle_input,
- models::{ApiInfo, HistorySong, SongData, WsPayload, parse_mpv_media_title},
+ models::{ApiInfo, SongData, WsPayload, parse_mpv_media_title},
ui::draw_ui,
};
@@ -57,6 +57,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
"--no-video".to_string(),
"--really-quiet".to_string(),
"--no-terminal".to_string(),
+ "--user-agent=curl/8.4.0".to_string(),
volume_arg,
format!("--input-ipc-server={socket_path}"),
];
@@ -130,30 +131,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let (ws_nudge_tx, mut ws_nudge_rx) = watch::channel(());
if !is_config_error {
- let tx_hist_init = tx.clone();
- let hist_client_init = http_client.clone();
- tokio::spawn(async move {
- if let Ok(resp) = hist_client_init.get("https://gensokyoradio.net/api/station/history/").send().await
- && let Ok(text) = resp.text().await
- {
- if let Ok(payloads) = serde_json::from_str::<Vec<serde_json::Value>>(&text) {
- let hist: Vec<HistorySong> = payloads
- .into_iter()
- .map(|p| HistorySong {
- title: p.get("TITLE").and_then(|v| v.as_str()).unwrap_or_default().to_string(),
- artist: p.get("ARTIST").and_then(|v| v.as_str()).unwrap_or_default().to_string(),
- album: p.get("ALBUM").and_then(|v| v.as_str()).unwrap_or_default().to_string(),
- circle: p.get("CIRCLE").and_then(|v| v.as_str()).unwrap_or_default().to_string(),
- album_art: p.get("ALBUMART").and_then(|v| v.as_str()).unwrap_or_default().to_string(),
- })
- .collect();
- if !hist.is_empty() {
- let _ = tx_hist_init.send(AppEvent::HistoryUpdate(hist)).await;
- }
- }
- }
- });
-
let tx_api = tx.clone();
let api_client = http_client.clone();
tokio::spawn(async move {
@@ -172,29 +149,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.await;
}
- tokio::time::sleep(Duration::from_secs(2)).await;
-
- if let Ok(resp) = api_client.get("https://gensokyoradio.net/api/station/history/").send().await
- && let Ok(text) = resp.text().await
- {
- if let Ok(payloads) = serde_json::from_str::<Vec<serde_json::Value>>(&text) {
- let hist: Vec<HistorySong> = payloads
- .into_iter()
- .map(|p| HistorySong {
- title: p.get("TITLE").and_then(|v| v.as_str()).unwrap_or_default().to_string(),
- artist: p.get("ARTIST").and_then(|v| v.as_str()).unwrap_or_default().to_string(),
- album: p.get("ALBUM").and_then(|v| v.as_str()).unwrap_or_default().to_string(),
- circle: p.get("CIRCLE").and_then(|v| v.as_str()).unwrap_or_default().to_string(),
- album_art: p.get("ALBUMART").and_then(|v| v.as_str()).unwrap_or_default().to_string(),
- })
- .collect();
- if !hist.is_empty() {
- let _ = tx_api.send(AppEvent::HistoryUpdate(hist)).await;
- }
- }
- }
-
- tokio::time::sleep(Duration::from_secs(13)).await;
+ tokio::time::sleep(Duration::from_secs(15)).await;
}
});
@@ -357,6 +312,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut last_nudge = Instant::now().checked_sub(Duration::from_secs(10)).unwrap_or_else(Instant::now);
let mut hide_art = false;
+ let mut mpv_respawn_time: Option<Instant> = None;
loop {
terminal.draw(|f| draw_ui(f, &mut state, hide_art))?;
@@ -387,77 +343,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
break;
}
},
- AppEvent::HistoryUpdate(history) => {
- let first_load = state.history.is_empty() && !history.is_empty();
- let old_first = state.history.first().map(|s| s.title.clone());
- let new_first = history.first().map(|s| s.title.clone());
-
- let shifted = old_first != new_first && !first_load;
-
- state.history = history.clone();
-
- if shifted {
- if let Some(idx) = state.history_list_state.selected() {
- if idx + 1 < state.history.len() {
- state.history_list_state.select(Some(idx + 1));
- }
- }
- }
-
- if first_load && state.mode == AppMode::History {
- if state.history_list_state.selected().is_none() {
- state.history_list_state.select(Some(0));
- }
- let idx = state.history_list_state.selected().unwrap_or(0);
- crate::input::fetch_history_art(
- idx,
- history[idx].album_art.clone(),
- state.cache_dir.join("history_art"),
- tx.clone(),
- picker.clone(),
- http_client.clone(),
- );
- }
-
- if first_load || shifted {
- let cache_dir = state.cache_dir.join("history_art");
- let hist_client = http_client.clone();
-
- tokio::spawn(async move {
- let mut current_files = std::collections::HashSet::new();
- for song in history {
- if song.album_art.is_empty() {
- continue;
- }
- current_files.insert(song.album_art.clone());
- let path = cache_dir.join(&song.album_art);
- if !path.exists() {
- let url = format!("https://gensokyoradio.net/images/albums/500/{}", song.album_art);
- if let Ok(resp) = hist_client.get(&url).send().await
- && let Ok(bytes) = resp.bytes().await
- {
- let _ = tokio::fs::write(&path, bytes).await;
- }
- }
- }
-
- if let Ok(mut entries) = tokio::fs::read_dir(&cache_dir).await {
- while let Ok(Some(entry)) = entries.next_entry().await {
- if let Ok(name) = entry.file_name().into_string() {
- if !current_files.contains(&name) {
- let _ = tokio::fs::remove_file(entry.path()).await;
- }
- }
- }
- }
- });
- }
- },
- AppEvent::HistoryArtLoaded(idx, art) => {
- if state.history_list_state.selected() == Some(idx) {
- state.selected_history_art = art;
- }
- },
AppEvent::SongUpdate(song) => {
let cached_listeners = state.song.listeners;
if song.id != state.song.id {
@@ -602,6 +487,50 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
}
},
AppEvent::Tick => {
+ if let Some(ref mut child) = mpv_process {
+ if let Ok(Some(_status)) = child.try_wait() {
+ mpv_process = None;
+
+ if state.mpv_retries > 0 {
+ state.mpv_retries -= 1;
+ state.status_message =
+ format!("stream dropped. reconnecting... ({} left)", state.mpv_retries);
+ state.status_message_time = Some(Instant::now());
+ mpv_respawn_time = Some(Instant::now() + Duration::from_secs(5));
+ } else {
+ state.status_message = "stream failed. max retries reached.".to_string();
+ state.status_message_time = Some(Instant::now());
+ }
+ }
+ } else if let Some(respawn_time) = mpv_respawn_time {
+ if Instant::now() >= respawn_time {
+ mpv_respawn_time = None;
+ let _ = fs::remove_file(&state.config.system.socket_path);
+
+ let mut args = vec![
+ "--no-video".to_string(),
+ "--really-quiet".to_string(),
+ "--no-terminal".to_string(),
+ "--user-agent=curl/8.4.0".to_string(),
+ format!("--volume={}", state.volume),
+ format!("--input-ipc-server={}", state.config.system.socket_path),
+ ];
+
+ if state.config.player.loudness_normalization {
+ args.push("--af=loudnorm=I=-16:TP=-1.5:LRA=11".to_string());
+ }
+ args.push(state.config.player.stream_url.clone());
+
+ mpv_process = Command::new("mpv")
+ .args(args)
+ .stdout(Stdio::null())
+ .stderr(Stdio::null())
+ .stdin(Stdio::null())
+ .spawn()
+ .ok();
+ }
+ }
+
if let Some(t) = state.status_message_time
&& t.elapsed().as_secs() >= 3
{
diff --git a/src/models.rs b/src/models.rs
index 722970c..7de82c9 100644
--- a/src/models.rs
+++ b/src/models.rs
@@ -106,12 +106,3 @@ pub struct LikedSong {
#[serde(default)]
pub year: String,
}
-
-#[derive(Default, Clone, Debug)]
-pub struct HistorySong {
- pub title: String,
- pub artist: String,
- pub album: String,
- pub circle: String,
- pub album_art: String,
-}
diff --git a/src/ui.rs b/src/ui.rs
index c231b05..63a4323 100644
--- a/src/ui.rs
+++ b/src/ui.rs
@@ -152,15 +152,6 @@ pub fn draw_ui(f: &mut Frame, state: &mut AppState, hide_art: bool) {
Style::default().fg(state.secondary_color)
},
),
- Span::raw(" "),
- Span::styled(
- "[history]",
- if state.mode == AppMode::History {
- Style::default().fg(state.theme_color).bold()
- } else {
- Style::default().fg(state.secondary_color)
- },
- ),
]);
f.render_widget(Paragraph::new(nav_text).alignment(Alignment::Center), main_chunks[0]);
@@ -182,9 +173,6 @@ pub fn draw_ui(f: &mut Frame, state: &mut AppState, hide_art: bool) {
AppMode::LikedSongs => {
draw_library(f, state, content_chunks.clone(), bottom_chunk, hide_art);
},
- AppMode::History => {
- draw_history(f, state, content_chunks.clone(), bottom_chunk, hide_art);
- },
_ => {},
}
@@ -406,67 +394,6 @@ fn draw_library(f: &mut Frame, state: &mut AppState, content: Rc<[Rect]>, bottom
}
}
-fn draw_history(f: &mut Frame, state: &mut AppState, content: Rc<[Rect]>, bottom: Rect, hide_art: bool) {
- let image_height = content[0].width / 2;
- let left_col = Layout::default()
- .direction(Direction::Vertical)
- .constraints([Constraint::Length(image_height), Constraint::Min(0)])
- .split(content[0]);
-
- let has_art = state
- .history_list_state
- .selected()
- .and_then(|i| state.history.get(i))
- .map(|s| !s.album_art.is_empty())
- .unwrap_or(false);
-
- if hide_art {
- f.render_widget(Clear, left_col[0]);
- } else if let Some(ref mut protocol) = state.selected_history_art {
- f.render_stateful_widget(
- StatefulImage::default().resize(Resize::Fit(Some(FilterType::CatmullRom))),
- left_col[0],
- protocol,
- );
- } else if has_art {
- f.render_widget(Paragraph::new("Loading Art...").alignment(Alignment::Center), left_col[0]);
- } else {
- f.render_widget(Paragraph::new("No Art Saved").alignment(Alignment::Center), left_col[0]);
- }
-
- let mut items: Vec<ListItem> = state
- .history
- .iter()
- .enumerate()
- .map(|(i, s)| {
- if Some(i) == state.history_list_state.selected() {
- ListItem::new(Line::from(vec![Span::styled(
- format!("{} - {}", s.title, s.artist),
- Style::default().fg(state.highlight_fg_color).bold(),
- )]))
- } else {
- ListItem::new(Line::from(vec![
- Span::styled(format!("{} ", s.title), Style::default().fg(state.theme_color).bold()),
- Span::styled(format!("- {}", s.artist), Style::default().fg(state.secondary_color)),
- ]))
- }
- })
- .collect();
-
- if items.is_empty() {
- items.push(ListItem::new(Line::from(vec![Span::styled(
- "Fetching history...",
- Style::default().fg(state.secondary_color),
- )])));
- }
- f.render_stateful_widget(
- List::new(items).highlight_style(Style::default().bg(state.highlight_bg_color)).highlight_symbol("> "),
- content[2],
- &mut state.history_list_state,
- );
- f.render_widget(Paragraph::new(build_status_bar(state)).alignment(Alignment::Left), bottom);
-}
-
fn draw_help_popup(f: &mut Frame, state: &AppState, area: Rect) {
let popup_area = centered_rect(44, 14, area);
f.render_widget(Clear, popup_area);
@@ -487,7 +414,7 @@ fn draw_help_popup(f: &mut Frame, state: &AppState, area: Rect) {
.margin(1)
.split(inner_area);
- let tab_titles: Vec<_> = vec![" General ", " Lib / Hist "].into_iter().map(Line::from).collect();
+ let tab_titles: Vec<_> = vec![" General ", " Library "].into_iter().map(Line::from).collect();
let tabs = Tabs::new(tab_titles)
.select(state.help_tab)
.highlight_style(Style::default().fg(Color::Black).bg(state.theme_color).bold())
@@ -509,6 +436,10 @@ fn draw_help_popup(f: &mut Frame, state: &AppState, area: Rect) {
Span::styled("[-/=]", Style::default().bold().fg(state.theme_color)),
Span::raw("Volume down / up"),
]),
+ Row::new(vec![
+ Span::styled("[h]", Style::default().bold().fg(state.theme_color)),
+ Span::raw("Open history in browser"),
+ ]),
Row::new(vec![
Span::styled("[?]", Style::default().bold().fg(state.theme_color)),
Span::raw("Close help menu"),