use poise::CreateReply; use radiobrowser::{ApiStation, StationSearchBuilder}; use regex::Regex; use serenity::all::{CreateActionRow, CreateButton, CreateEmbed, CreateEmbedFooter, CreateInteractionResponse, CreateInteractionResponseMessage}; use crate::{commands::voice_types::NumberOfEntries, types::Context}; pub async fn paginate_search_stations( ctx: &Context<'_>, search_builder: &StationSearchBuilder, limit: NumberOfEntries ) -> Result<(), serenity::Error> { // Define some unique identifiers for the navigation buttons let ctx_id = ctx.id(); let prev_button_id = format!("{}prev", ctx_id); let next_button_id = format!("{}next", ctx_id); let search_builder = search_builder; let Ok(stations) = search_builder.clone().send().await else { ctx.reply("Something went wrong, try searching again").await?; return Ok(()) }; let mut page = 0; let embed = create_station_list_embed(&stations, page); // Send the embed with the first page as content let reply = { let components = CreateActionRow::Buttons(vec![ CreateButton::new(&prev_button_id).emoji('◀'), CreateButton::new(&next_button_id).emoji('▶'), ]); CreateReply::default() .embed(embed) .components(vec![components]) }; ctx.send(reply).await?; // Loop through incoming interactions with the navigation buttons let mut offset = 0; let limit_int = limit as u32; while let Some(press) = serenity::collector::ComponentInteractionCollector::new(ctx) // We defined our button IDs to start with `ctx_id`. If they don't, some other command's // button was pressed .filter(move |press| press.data.custom_id.starts_with(&ctx_id.to_string())) // Timeout when no navigation button has been pressed for 24 hours .timeout(std::time::Duration::from_secs(3600 * 24)) .await { // Depending on which button was pressed, go to next or previous page if press.data.custom_id == next_button_id { offset += limit_int; page += 1; } else if press.data.custom_id == prev_button_id { offset = if offset < limit_int { offset } else { offset - limit_int }; page = if page == 0 { 0 } else { page - 1 }; } else { // This is an unrelated button interaction continue; } let Ok(mut stations) = search_builder.clone().offset(offset.to_string()).send().await else { ctx.reply("Something went wrong, try searching again").await?; return Ok(()) }; if stations.is_empty() { offset = 0; page = 0; let Ok(new_stations) = search_builder.clone().offset(offset.to_string()).send().await else { ctx.reply("Something went wrong, try searching again").await?; return Ok(()) }; stations = new_stations; } let embed = create_station_list_embed(&stations, page); // Update the message with the new page contents press .create_response( ctx.serenity_context(), CreateInteractionResponse::UpdateMessage( CreateInteractionResponseMessage::new() .embed(embed), ), ) .await?; } Ok(()) } fn create_station_list_embed(stations: &Vec, page: u32) -> CreateEmbed { let result = CreateEmbed::new() .fields( stations.iter().map(|station| { ( station.name.clone(), format!("Country: {} Stream: {}", station.country, station.url), false ) }) ) .footer(CreateEmbedFooter::new(format!("Page: {}", page + 1))); result } pub enum LinkString { Link, String } pub fn link_or_string(haystack: &str) -> LinkString { let Ok(re) = Regex::new(r"^https?://([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$") else { panic!("Wrong regex expression!"); }; return if re.is_match(haystack) { LinkString::Link } else { LinkString::String } } pub fn parse_radio_autocomplete(haystack: &str) -> Option<(String, String, String)> { let Ok(re) = Regex::new(r"^Name: (.*) Country: (.*) Language: (.*)") else { panic!("Wrong regex expression!"); }; let Some(captures) = re.captures(haystack) else { return None }; return Some(( String::from(&captures[1]), String::from(&captures[2]), String::from(&captures[3]) )) }