diff --git a/src/commands/search/vndb/character.rs b/src/commands/search/vndb/character.rs index 8d8d1d28..66376a50 100644 --- a/src/commands/search/vndb/character.rs +++ b/src/commands/search/vndb/character.rs @@ -1,10 +1,14 @@ -use crate::structs::{ - api::vndb::Vndb, - command_context::{AeonCommandContext, AeonCommandInput}, - select_menu::SelectMenu, +use crate::{ + functions::limit_strings, + structs::{ + api::vndb::Vndb, + command_context::{AeonCommandContext, AeonCommandInput}, + components_v2::ComponentsV2Embed, + select_menu::SelectMenu, + }, }; use anyhow::Result; -use slashook::commands::MessageResponse; +use slashook::structs::components::{ActionRow, Button, Components, Section, Separator, TextDisplay, Thumbnail}; use std::sync::Arc; pub async fn run(ctx: Arc) -> Result<()> { @@ -12,10 +16,46 @@ pub async fn run(ctx: Arc) -> Result<()> { && ctx.get_bool_arg("search").unwrap_or(false) { let characters = Vndb::search_character(ctx.get_string_arg("character", 0, true)?).await?; - let options = characters.iter().map(|character| (&character.name, &character.id, Some(&character.vns[0].title))); - let select_menu = SelectMenu::new("vndb", "character", "Select a character…", None::).add_options(options); + let mut embed = ComponentsV2Embed::new().set_title("Select a character"); + let mut components = Components::empty(); - return ctx.respond(select_menu, false).await; + let iter = characters.into_iter().take(6).enumerate(); + let total = iter.len(); + + for (i, character) in iter { + let aliases = character.aliases.iter().map(|alias| format!("_{alias}_")).collect::>(); + let text_display = TextDisplay::new(format!( + "### [{}](https://vndb.org/{}){}\n-# {}", + limit_strings(character.name.split(""), "", 300), + character.id, + if aliases.is_empty() { "".into() } else { format!("\n{}", aliases.join("\n")) }, + character.vns[0].title, + )); + + if let Some(image) = &character.image + && image.sexual == 0. + && image.violence == 0. + { + let thumbnail = Thumbnail::new(&image.url); + let section = Section::new().add_component(text_display).set_accessory(thumbnail); + components = components.add_component(section); + } else { + components = components.add_component(text_display); + } + + let button = Button::new().set_id("vndb", format!("character/{}", character.id)).set_label("Select"); + let action_row = ActionRow::new().add_component(button); + components = components.add_component(action_row); + + if i != total - 1 { + let separator = Separator::new(); + components = components.add_component(separator); + } + } + + embed = embed.set_components(components); + + return ctx.respond(embed, false).await; } let (query, section) = ctx.get_query_and_section("character")?; @@ -31,5 +71,5 @@ pub async fn run(ctx: Arc) -> Result<()> { _ => character.format(), }; - ctx.respond(MessageResponse::from(select_menu).add_embed(embed), false).await + ctx.respond(embed.set_select_menu(select_menu), false).await } diff --git a/src/commands/search/vndb/character_trait.rs b/src/commands/search/vndb/character_trait.rs index 1999e43c..d4581e82 100644 --- a/src/commands/search/vndb/character_trait.rs +++ b/src/commands/search/vndb/character_trait.rs @@ -4,19 +4,20 @@ use crate::structs::{ select_menu::SelectMenu, }; use anyhow::Result; -use slashook::commands::MessageResponse; use std::sync::Arc; pub async fn run(ctx: Arc) -> Result<()> { if let AeonCommandInput::ApplicationCommand(input, _) = &ctx.command_input && input.is_string_select() { - return ctx.respond(Vndb::search_trait(&input.values.as_ref().unwrap()[0]).await?[0].format(), false).await; + return ctx + .respond(Vndb::search_trait(&input.values.as_ref().unwrap()[0]).await?[0].format().set_select_menu_from_input(input), false) + .await; } let results = Vndb::search_trait(ctx.get_string_arg("trait", 0, true)?).await?; let options = results.iter().map(|result| (&result.name, &result.id, result.group_name.as_ref())); let select_menu = SelectMenu::new("vndb", "trait", "View other traits…", None::).add_options(options); - ctx.respond(MessageResponse::from(select_menu).add_embed(results[0].format()), false).await + ctx.respond(results[0].format().set_select_menu(select_menu), false).await } diff --git a/src/commands/search/vndb/tag.rs b/src/commands/search/vndb/tag.rs index 64f931e0..449d2866 100644 --- a/src/commands/search/vndb/tag.rs +++ b/src/commands/search/vndb/tag.rs @@ -4,19 +4,19 @@ use crate::structs::{ select_menu::SelectMenu, }; use anyhow::Result; -use slashook::commands::MessageResponse; use std::sync::Arc; pub async fn run(ctx: Arc) -> Result<()> { if let AeonCommandInput::ApplicationCommand(input, _) = &ctx.command_input && input.is_string_select() { - return ctx.respond(Vndb::search_tag(&input.values.as_ref().unwrap()[0]).await?[0].format(), false).await; + let embed = Vndb::search_tag(&input.values.as_ref().unwrap()[0]).await?[0].format(); + return ctx.respond(embed.set_select_menu_from_input(input), false).await; } let tags = Vndb::search_tag(ctx.get_string_arg("tag", 0, true)?).await?; let options = tags.iter().map(|tag| (&tag.name, &tag.id, Some(&tag.category))); let select_menu = SelectMenu::new("vndb", "tag", "View other tags…", None::).add_options(options); - ctx.respond(MessageResponse::from(select_menu).add_embed(tags[0].format()), false).await + ctx.respond(tags[0].format().set_select_menu(select_menu), false).await } diff --git a/src/commands/search/vndb/visual_novel.rs b/src/commands/search/vndb/visual_novel.rs index c0364749..a086d289 100644 --- a/src/commands/search/vndb/visual_novel.rs +++ b/src/commands/search/vndb/visual_novel.rs @@ -1,10 +1,14 @@ -use crate::structs::{ - api::vndb::Vndb, - command_context::{AeonCommandContext, AeonCommandInput}, - select_menu::SelectMenu, +use crate::{ + functions::limit_strings, + structs::{ + api::vndb::Vndb, + command_context::{AeonCommandContext, AeonCommandInput}, + components_v2::ComponentsV2Embed, + select_menu::SelectMenu, + }, }; use anyhow::Result; -use slashook::commands::MessageResponse; +use slashook::structs::components::{ActionRow, Button, Components, Section, Separator, TextDisplay, Thumbnail}; use std::sync::Arc; pub async fn run(ctx: Arc) -> Result<()> { @@ -12,13 +16,56 @@ pub async fn run(ctx: Arc) -> Result<()> { && ctx.get_bool_arg("search").unwrap_or(false) { let visual_novels = Vndb::search_visual_novel(ctx.get_string_arg("visual-novel", 0, true)?).await?; - let options = visual_novels.iter().map(|visual_novel| (&visual_novel.title, &visual_novel.id, Some(&visual_novel.dev_status))); - let select_menu = SelectMenu::new("vndb", "visual-novel", "Select a visual novel…", None::).add_options(options); + let mut embed = ComponentsV2Embed::new().set_title("Select a visual novel"); + let mut components = Components::empty(); - return ctx.respond(select_menu, false).await; + let iter = visual_novels.into_iter().take(7).enumerate(); + let total = iter.len(); + + for (i, visual_novel) in iter { + let mut aliases = vec![]; + if let Some(alt_title) = &visual_novel.alt_title { + aliases.push(format!("_{alt_title}_")); + } + for alias in &visual_novel.aliases { + aliases.push(format!("_{alias}_")); + } + let text_display = TextDisplay::new(format!( + "### [{} ({})](https://vndb.org/{}){}", + limit_strings(visual_novel.title.split(""), "", 300), + visual_novel.dev_status, + visual_novel.id, + if aliases.is_empty() { "".into() } else { format!("\n{}", aliases.join("\n")) }, + )); + + if let Some(image) = &visual_novel.image + && image.sexual == 0. + && image.violence == 0. + { + let thumbnail = Thumbnail::new(&image.url); + let section = Section::new().add_component(text_display).set_accessory(thumbnail); + components = components.add_component(section); + } else { + components = components.add_component(text_display); + } + + let button = Button::new().set_id("vndb", format!("visual-novel/{}", visual_novel.id)).set_label("Select"); + let action_row = ActionRow::new().add_component(button); + components = components.add_component(action_row); + + if i != total - 1 { + let separator = Separator::new(); + components = components.add_component(separator); + } + } + + embed = embed.set_components(components); + + return ctx.respond(embed, false).await; } let (query, section) = ctx.get_query_and_section("visual-novel")?; + let visual_novel = Vndb::search_visual_novel(query).await?.remove(0); let id = &visual_novel.id; let select_menu = SelectMenu::new("vndb", "visual-novel", "View other sections…", Some(§ion)) @@ -31,5 +78,5 @@ pub async fn run(ctx: Arc) -> Result<()> { _ => visual_novel.format(), }; - ctx.respond(MessageResponse::from(select_menu).add_embed(embed), false).await + ctx.respond(embed.set_select_menu(select_menu), false).await } diff --git a/src/functions.rs b/src/functions.rs index 883a7f09..254e5b51 100644 --- a/src/functions.rs +++ b/src/functions.rs @@ -63,11 +63,21 @@ pub fn limit_strings, U: Display, V: Display>(iterable let delimiter = delimiter.to_string(); let mut strings = iterable.into_iter().map(|to_string| to_string.to_string()).collect::>(); - while strings.join(&delimiter).len() > limit { - strings.pop(); + let join_full = strings.join(&delimiter); + + if join_full.len() > limit { + while strings.join(&delimiter).len() > limit - delimiter.len() - 1 { + strings.pop(); + } + } + + let mut join_limited = strings.join(&delimiter); + + if join_full.len() != join_limited.len() { + join_limited += &format!("{delimiter}…"); } - strings.join(&delimiter) + join_limited } pub fn label_num(amount: T, singular: U, plural: V) -> String { diff --git a/src/structs/api/vndb/character.rs b/src/structs/api/vndb/character.rs index ec5736ea..6f67f3ff 100644 --- a/src/structs/api/vndb/character.rs +++ b/src/structs/api/vndb/character.rs @@ -1,9 +1,12 @@ use crate::{ functions::limit_strings, - structs::api::vndb::{ - Vndb, - statics::{VNDB_CHARACTER_FIELDS, VNDB_EMBED_AUTHOR_ICON_URL, VNDB_EMBED_AUTHOR_URL, VNDB_EMBED_COLOR}, - visual_novel::VndbImage, + structs::{ + api::vndb::{ + Vndb, + statics::{VNDB_CHARACTER_FIELDS, VNDB_EMBED_COLOR}, + visual_novel::VndbImage, + }, + components_v2::ComponentsV2Embed, }, traits::Commas, }; @@ -11,7 +14,7 @@ use anyhow::{Result, bail}; use serde::Deserialize; use serde_json::json; use serde_repr::Deserialize_repr; -use slashook::structs::embeds::Embed; +use slashook::structs::components::{Components, Separator, TextDisplay}; use std::{ collections::HashMap, fmt::{Display, Formatter, Result as FmtResult}, @@ -117,53 +120,94 @@ pub struct VndbCharacter { } impl VndbCharacter { - fn _format(&self) -> Embed { + fn _format(&self) -> ComponentsV2Embed { let thumbnail = self.image.as_ref().map_or("", |image| if image.sexual > 1.0 { "" } else { image.url.as_str() }); let title = self.name.chars().take(256).collect::(); let url = format!("https://vndb.org/{}", self.id); + let aliases = self.aliases.iter().map(|alias| format!("_{alias}_")).collect::>(); - Embed::new() - .set_color(VNDB_EMBED_COLOR) - .unwrap_or_default() - .set_thumbnail(thumbnail) - .set_author("vndb • Character", Some(VNDB_EMBED_AUTHOR_URL), Some(VNDB_EMBED_AUTHOR_ICON_URL)) - .set_title(title) - .set_url(url) + let mut embed = ComponentsV2Embed::new().set_color(VNDB_EMBED_COLOR).set_thumbnail(thumbnail).set_title(title).set_url(url); + + if !aliases.is_empty() { + embed = embed.set_description(aliases.join("\n")); + } + + embed } - pub fn format(&self) -> Embed { - let aliases = self.aliases.iter().map(|alias| format!("_{alias}_")).collect::>().join("\n"); - let sex = self.sex.as_ref().map(|(sex, spoiler_sex)| { - format!( - "{}{}", + pub fn format(&self) -> ComponentsV2Embed { + let mut text_displays = vec![]; + + let mut group = vec![]; + if let Some((sex, spoiler_sex)) = &self.sex { + group.push(format!( + "**Sex**: {}{}", sex.as_ref().map(|sex| format!("{sex:?}")).as_deref().unwrap_or("N/A"), spoiler_sex.as_ref().map(|spoiler_sex| format!(" (||actually {spoiler_sex:?}||)")).as_deref().unwrap_or_default(), - ) - }); - let age = self.age.map(|age| age.commas()); - let birthday = self.birthday.map(|birthday| format!("{}/{}", birthday.0, birthday.1)); - let blood_type = self.blood_type.as_ref().map(|blood_type| format!("{blood_type:?}")); - let height = self.height.map(|height| format!("{} cm", height.commas())); - let weight = self.weight.map(|weight| format!("{} kg", weight.commas())); - let bust = - self.bust.map(|bust| format!("{bust} cm{}", self.cup.as_ref().map(|cup| format!(" - Cup Size {cup}")).unwrap_or_default())); - let waist = self.waist.map(|waist| format!("{waist} cm")); - let hips = self.hips.map(|hips| format!("{hips} cm")); - - self._format() - .set_description(aliases) - .add_field("Sex", sex.as_deref().unwrap_or("N/A"), true) - .add_field("Age", age.as_deref().unwrap_or("N/A"), true) - .add_field("Birthday", birthday.as_deref().unwrap_or("N/A"), true) - .add_field("Blood Type", blood_type.as_deref().unwrap_or("N/A"), true) - .add_field("Height", height.as_deref().unwrap_or("N/A"), true) - .add_field("Weight", weight.as_deref().unwrap_or("N/A"), true) - .add_field("Bust", bust.as_deref().unwrap_or("N/A"), true) - .add_field("Waist", waist.as_deref().unwrap_or("N/A"), true) - .add_field("Hips", hips.as_deref().unwrap_or("N/A"), true) + )); + } + if let Some(blood_type) = &self.blood_type { + group.push(format!("**Blood Type**: {blood_type:?}")); + } + if !group.is_empty() { + text_displays.push(TextDisplay::new(group.join("\n"))); + } + + let mut group = vec![]; + if let Some(age) = self.age { + group.push(format!("**Age**: {}", age.commas())); + } + if let Some(birthday) = self.birthday { + group.push(format!("**Birthday**: {}/{}", birthday.0, birthday.1)); + } + if !group.is_empty() { + text_displays.push(TextDisplay::new(group.join("\n"))); + } + + let mut group = vec![]; + if let Some(height) = self.height { + group.push(format!("**Height**: {} cm", height.commas())); + } + if let Some(weight) = self.weight { + group.push(format!("**Weight**: {} kg", weight.commas())); + } + if !group.is_empty() { + text_displays.push(TextDisplay::new(group.join("\n"))); + } + + let mut group = vec![]; + if let Some(bust) = &self.bust { + group.push(format!("**Bust**: {bust} cm")); + } + if let Some(waist) = self.waist { + group.push(format!("**Waist**: {waist} cm")); + } + if let Some(hips) = self.hips { + group.push(format!("**Hips**: {hips} cm")); + } + if let Some(cup_size) = &self.cup { + group.push(format!("**Cup Size**: {cup_size}")); + } + if !group.is_empty() { + text_displays.push(TextDisplay::new(group.join("\n"))); + } + + let total = text_displays.len(); + let mut components = Components::empty(); + + for (i, text_display) in text_displays.into_iter().enumerate() { + components = components.add_component(text_display); + + if i != total - 1 { + let separator = Separator::new(); + components = components.add_component(separator); + } + } + + self._format().set_components(components) } - pub fn format_traits(&self) -> Embed { + pub fn format_traits(&self) -> ComponentsV2Embed { let mut groups = HashMap::new(); for character_trait in &self.traits { @@ -177,26 +221,43 @@ impl VndbCharacter { text = format!("||{text}||"); } - groups.get_mut(&character_trait.group_name).unwrap().push(text); + if let Some(traits) = groups.get_mut(&character_trait.group_name) { + traits.push(text); + } } - let mut embed = self._format(); + let mut components = Components::empty(); - for (group_name, traits) in groups { - embed = embed.add_field(group_name, limit_strings(traits, ", ", 1024), false); + for (i, (group_name, traits)) in groups.iter().enumerate() { + let text_display = TextDisplay::new(format!("### {group_name}\n{}", limit_strings(traits, ", ", 512))); + components = components.add_component(text_display); + + if i != groups.len() - 1 { + let separator = Separator::new(); + components = components.add_component(separator); + } } - embed + self._format().set_components(components) } - pub fn format_visual_novels(&self) -> Embed { - self._format().set_description(limit_strings( - self.vns - .iter() - .map(|visual_novel| format!("[{}](https://vndb.org/{}) ({})", visual_novel.title, visual_novel.id, visual_novel.role)), - "\n", - 4096, - )) + pub fn format_visual_novels(&self) -> ComponentsV2Embed { + let mut components = Components::empty(); + + let iter = self.vns.iter().take(20); + let total = iter.len(); + + for (i, vn) in iter.enumerate() { + let text_display = TextDisplay::new(format!("### [{}](https://vndb.org/{})\n-# {}", vn.title, vn.id, vn.role)); + components = components.add_component(text_display); + + if i != total - 1 { + let separator = Separator::new(); + components = components.add_component(separator); + } + } + + self._format().set_components(components) } } diff --git a/src/structs/api/vndb/character_trait.rs b/src/structs/api/vndb/character_trait.rs index a46aebfd..72b2aa78 100644 --- a/src/structs/api/vndb/character_trait.rs +++ b/src/structs/api/vndb/character_trait.rs @@ -1,16 +1,19 @@ use crate::{ functions::limit_strings, macros::yes_no, - structs::api::vndb::{ - Vndb, - statics::{VNDB_EMBED_AUTHOR_ICON_URL, VNDB_EMBED_AUTHOR_URL, VNDB_EMBED_COLOR, VNDB_TRAIT_FIELDS}, + structs::{ + api::vndb::{ + Vndb, + statics::{VNDB_EMBED_COLOR, VNDB_TRAIT_FIELDS}, + }, + components_v2::ComponentsV2Embed, }, traits::Commas, }; use anyhow::{Result, bail}; use serde::Deserialize; use serde_json::json; -use slashook::structs::embeds::Embed; +use slashook::structs::components::{Components, TextDisplay}; use std::fmt::Display; #[derive(Deserialize, Debug)] @@ -28,7 +31,7 @@ pub struct VndbTrait { } impl VndbTrait { - pub fn format(&self) -> Embed { + pub fn format(&self) -> ComponentsV2Embed { let group_name = self.group_name.as_ref().map(|group_name| format!("{group_name}: ")); let title = format!("{}{}", group_name.as_deref().unwrap_or_default(), self.name); let url = format!("https://vndb.org/{}", self.id); @@ -37,18 +40,15 @@ impl VndbTrait { let searchable = yes_no!(self.searchable); let applicable = yes_no!(self.applicable); let char_count = self.char_count.commas(); + let footer = format!("Character Count: {char_count} • Applicable: {applicable} • Searchable: {searchable}"); - Embed::new() + ComponentsV2Embed::new() .set_color(VNDB_EMBED_COLOR) - .unwrap_or_default() - .set_author("vndb • Trait", Some(VNDB_EMBED_AUTHOR_URL), Some(VNDB_EMBED_AUTHOR_ICON_URL)) .set_title(title) .set_url(url) .set_description(aliases) - .add_field("Description", description, false) - .add_field("Searchable", searchable, true) - .add_field("Applicable", applicable, true) - .add_field("Character Count", char_count, true) + .set_components(Components::empty().add_component(TextDisplay::new(description))) + .set_footer(footer) } } diff --git a/src/structs/api/vndb/tag.rs b/src/structs/api/vndb/tag.rs index 92e649e7..54c58055 100644 --- a/src/structs/api/vndb/tag.rs +++ b/src/structs/api/vndb/tag.rs @@ -1,16 +1,19 @@ use crate::{ functions::limit_strings, macros::yes_no, - structs::api::vndb::{ - Vndb, - statics::{VNDB_EMBED_AUTHOR_ICON_URL, VNDB_EMBED_AUTHOR_URL, VNDB_EMBED_COLOR, VNDB_TAG_FIELDS}, + structs::{ + api::vndb::{ + Vndb, + statics::{VNDB_EMBED_COLOR, VNDB_TAG_FIELDS}, + }, + components_v2::ComponentsV2Embed, }, traits::Commas, }; use anyhow::{Result, bail}; use serde::Deserialize; use serde_json::json; -use slashook::structs::embeds::Embed; +use slashook::structs::components::{Components, TextDisplay}; use std::fmt::{Display, Formatter, Result as FmtResult}; #[derive(Deserialize, Debug)] @@ -51,7 +54,7 @@ pub struct VndbTag { } impl VndbTag { - pub fn format(&self) -> Embed { + pub fn format(&self) -> ComponentsV2Embed { let title = format!("{} ({})", self.name, self.category); let url = format!("https://vndb.org/{}", self.id); let aliases = self.aliases.iter().map(|alias| format!("_{alias}_")).collect::>().join("\n"); @@ -59,18 +62,15 @@ impl VndbTag { let searchable = yes_no!(self.searchable); let applicable = yes_no!(self.applicable); let vn_count = self.vn_count.commas(); + let footer = format!("Visual Novel Count: {vn_count} • Applicable: {applicable} • Searchable: {searchable}"); - Embed::new() + ComponentsV2Embed::new() .set_color(VNDB_EMBED_COLOR) - .unwrap_or_default() - .set_author("vndb • Tag", Some(VNDB_EMBED_AUTHOR_URL), Some(VNDB_EMBED_AUTHOR_ICON_URL)) .set_title(title) .set_url(url) .set_description(aliases) - .add_field("Description", description, false) - .add_field("Searchable", searchable, true) - .add_field("Applicable", applicable, true) - .add_field("Visual Novel Count", vn_count, true) + .set_components(Components::empty().add_component(TextDisplay::new(description))) + .set_footer(footer) } } diff --git a/src/structs/api/vndb/visual_novel.rs b/src/structs/api/vndb/visual_novel.rs index b4c545cc..a369a4ec 100644 --- a/src/structs/api/vndb/visual_novel.rs +++ b/src/structs/api/vndb/visual_novel.rs @@ -1,15 +1,18 @@ use crate::{ functions::{label_num, limit_strings}, - structs::api::vndb::{ - Vndb, - statics::{VNDB_EMBED_AUTHOR_ICON_URL, VNDB_EMBED_AUTHOR_URL, VNDB_EMBED_COLOR, VNDB_VISUAL_NOVEL_FIELDS}, + structs::{ + api::vndb::{ + Vndb, + statics::{VNDB_EMBED_COLOR, VNDB_VISUAL_NOVEL_FIELDS}, + }, + components_v2::ComponentsV2Embed, }, }; use anyhow::{Result, bail}; use serde::Deserialize; use serde_json::json; use serde_repr::Deserialize_repr; -use slashook::structs::embeds::Embed; +use slashook::structs::components::{Components, TextDisplay}; use std::fmt::{Display, Formatter, Result as FmtResult}; // Enum reference: https://code.blicky.net/yorhel/vndb/src/branch/master/lib/VNDB/Types.pm @@ -522,61 +525,64 @@ pub struct VndbVisualNovel { } impl VndbVisualNovel { - fn _format(&self) -> Embed { - let thumbnail = self.image.as_ref().map_or("", |image| if image.sexual > 1.0 { "" } else { image.url.as_str() }); - let title = format!( - "{} ({})", - if self.title.len() > 230 { - format!("{}…", self.title.chars().take(229).collect::().trim()) - } else { - self.title.clone() - }, - self.dev_status, - ); + fn _format(&self) -> ComponentsV2Embed { + let thumbnail = self.image.as_ref().map_or("", |image| if image.sexual > 1. { "" } else { image.url.as_str() }); + let title = format!("{} ({})", limit_strings(self.title.split(""), "", 230), self.dev_status); let url = format!("https://vndb.org/{}", self.id); - - Embed::new() - .set_color(VNDB_EMBED_COLOR) - .unwrap_or_default() - .set_thumbnail(thumbnail) - .set_author("vndb • Visual Novel", Some(VNDB_EMBED_AUTHOR_URL), Some(VNDB_EMBED_AUTHOR_ICON_URL)) - .set_title(title) - .set_url(url) - } - - pub fn format(&self) -> Embed { - let aliases = self.aliases.iter().map(|alias| format!("_{alias}_")).collect::>().join("\n"); - let popularity = format!("{:.0}%", self.popularity); - let rating = format!( - "{} ({})", + let footer = format!( + "Released: {} • Rating: {} ({}) • Popularity: {:.0}%", + self.released.as_deref().unwrap_or("TBA"), self.rating.map(|rating| format!("{rating:.0}%")).as_deref().unwrap_or("N/A"), label_num(self.vote_count, "vote", "votes"), + self.popularity, ); - let length = format!( - "{} ({})", - self.length.as_ref().map(|length| length.to_string()).as_deref().unwrap_or("N/A"), - label_num(self.length_votes, "vote", "votes"), - ); + + ComponentsV2Embed::new().set_color(VNDB_EMBED_COLOR).set_thumbnail(thumbnail).set_title(title).set_url(url).set_footer(footer) + } + + pub fn format(&self) -> ComponentsV2Embed { + let mut aliases = vec![]; + if let Some(alt_title) = &self.alt_title { + aliases.push(format!("_{alt_title}_")); + } + for alias in &self.aliases { + aliases.push(format!("_{alias}_")); + } let languages = self.languages.iter().map(|language| language.to_string()).collect::>().join(", "); let platforms = self.platforms.iter().map(|platform| platform.to_string()).collect::>().join(", "); - let release_date = self.released.as_ref().map(|released| format!("Released {released}")); - - self._format() - .set_description(aliases) - .add_field("Popularity", popularity, true) - .add_field("Rating", rating, true) - .add_field("Length", length, true) - .add_field("Languages", languages, false) - .add_field("Platforms", platforms, false) - .set_footer(release_date.as_deref().unwrap_or_default(), None::) + + let mut embed = self._format(); + + if !aliases.is_empty() { + embed = embed.set_description(aliases.join("\n")) + } + + let mut components = Components::empty(); + + if !languages.is_empty() { + let text_display = TextDisplay::new(format!("### Languages\n{languages}")); + components = components.add_component(text_display); + } + + if !platforms.is_empty() { + let text_display = TextDisplay::new(format!("### Platforms\n{platforms}")); + components = components.add_component(text_display); + } + + if let Some(length) = &self.length { + let text_display = TextDisplay::new(format!("### Length\n{length} ({})", label_num(self.length_votes, "vote", "votes"))); + components = components.add_component(text_display); + } + + embed.set_components(components) } - pub fn format_description(&self) -> Embed { - let description = limit_strings(Vndb::clean_bbcode(self.description.as_deref().unwrap_or("N/A")).split('\n'), "\n", 4096); + pub fn format_description(&self) -> ComponentsV2Embed { + let description = limit_strings(Vndb::clean_bbcode(self.description.as_deref().unwrap_or("N/A")).split('\n'), "\n", 3000); self._format().set_description(description) } - pub fn format_tags(&self) -> Embed { + pub fn format_tags(&self) -> ComponentsV2Embed { let tags = limit_strings( self.tags.iter().map(|tag| { let mut text = format!("[{}](https://vndb.org/{})", tag.name, tag.id); @@ -588,7 +594,7 @@ impl VndbVisualNovel { text }), ", ", - 4096, + 3000, ); self._format().set_description(tags) } diff --git a/src/structs/command.rs b/src/structs/command.rs index b51b1e82..58c25fe6 100644 --- a/src/structs/command.rs +++ b/src/structs/command.rs @@ -105,10 +105,13 @@ impl AeonCommand { } }, AeonCommandInput::ApplicationCommand(input, _) => { - let subcommand = self - .subcommands - .iter() - .find(|entry| entry.name == input.subcommand.as_ref().or(input.custom_id.as_ref()).cloned().unwrap_or_default()); + let subcommand = self.subcommands.iter().find(|entry| { + let subcommand_input = input.subcommand.as_deref().unwrap_or_default(); + let custom_id_subcommand_input = + input.custom_id.as_deref().unwrap_or_default().split('/').next().unwrap_or_default(); + + entry.name == subcommand_input || entry.name == custom_id_subcommand_input + }); if let Some(subcommand) = subcommand { func = Some(&subcommand.func); diff --git a/src/structs/command_context.rs b/src/structs/command_context.rs index 39ee9d8e..d04d68e7 100644 --- a/src/structs/command_context.rs +++ b/src/structs/command_context.rs @@ -177,10 +177,19 @@ impl AeonCommandContext { AeonCommandInput::ApplicationCommand(input, _) => { if input.is_string_select() { let mut split = input.values.as_ref().unwrap()[0].split('/'); - Ok((split.next().unwrap().into(), split.next().unwrap_or_default().into())) - } else { - Ok((self.get_string_arg(option_name, 0, true)?, "".into())) + let query = split.next().unwrap_or_default().into(); + let section = split.next().unwrap_or_default().into(); + return Ok((query, section)); + } + + if input.is_button() { + let mut split = input.custom_id.as_deref().unwrap_or_default().split('/'); + let query = split.next_back().unwrap_or_default().into(); + let section = "".into(); + return Ok((query, section)); } + + Ok((self.get_string_arg(option_name, 0, true)?, "".into())) }, AeonCommandInput::MessageCommand(..) => Ok((self.get_string_arg(option_name, 0, true)?, "".into())), } diff --git a/src/structs/components_v2.rs b/src/structs/components_v2.rs new file mode 100644 index 00000000..36bae7c0 --- /dev/null +++ b/src/structs/components_v2.rs @@ -0,0 +1,221 @@ +use crate::statics::colors::PRIMARY_EMBED_COLOR; +use serde_json::{Value, json}; +use slashook::{ + commands::{CommandInput, MessageResponse}, + structs::{ + components::{ActionRow, Button, Component, Components, Container, Section, SelectMenu, Separator, TextDisplay, Thumbnail}, + messages::MessageFlags, + }, +}; +use std::fmt::Display; + +pub struct ComponentsV2Embed { + color: Option, + title: Option, + url: Option, + thumbnail: Option, + description: Option, + components: Components, + footer: Option, + buttons: Vec