diff --git a/command_attributes/src/lib.rs b/command_attributes/src/lib.rs index 1bd1ad0..cce792e 100644 --- a/command_attributes/src/lib.rs +++ b/command_attributes/src/lib.rs @@ -81,21 +81,28 @@ pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream { options.subcommand_groups.push(new_group); } "arg" => { - if let Some(subcommand_group) = options.subcommand_groups.last_mut() { - if let Some(subcommand) = subcommand_group.subcommands.last_mut() { - subcommand.cmd_args.push(propagate_err!(attributes::parse(values))); - } else { - if let Some(subcommand) = options.subcommands.last_mut() { - subcommand.cmd_args.push(propagate_err!(attributes::parse(values))); - } else { - options.cmd_args.push(propagate_err!(attributes::parse(values))); - } + let arg = propagate_err!(attributes::parse(values)); + + match last_desc { + LastItem::Fun => { + options.cmd_args.push(arg); } - } else { - if let Some(subcommand) = options.subcommands.last_mut() { - subcommand.cmd_args.push(propagate_err!(attributes::parse(values))); - } else { - options.cmd_args.push(propagate_err!(attributes::parse(values))); + LastItem::SubFun => { + options.subcommands.last_mut().unwrap().cmd_args.push(arg); + } + LastItem::SubGroup => { + panic!("Argument not expected under subcommand group"); + } + LastItem::SubGroupFun => { + options + .subcommand_groups + .last_mut() + .unwrap() + .subcommands + .last_mut() + .unwrap() + .cmd_args + .push(arg); } } } diff --git a/src/commands/reminder_cmds.rs b/src/commands/reminder_cmds.rs index 6f9f8ae..7e42d41 100644 --- a/src/commands/reminder_cmds.rs +++ b/src/commands/reminder_cmds.rs @@ -369,7 +369,7 @@ async fn delete(ctx: &Context, invoke: CommandInvoke, _args: CommandOptions) { let reminders = Reminder::from_guild(ctx, interaction.guild_id, interaction.user.id).await; - let resp = show_delete_page(&reminders, 0, timezone).await; + let resp = show_delete_page(&reminders, 0, timezone); let _ = interaction .create_interaction_response(&ctx, |r| { @@ -402,7 +402,7 @@ pub fn max_delete_page(reminders: &[Reminder], timezone: &Tz) -> usize { }) } -pub async fn show_delete_page( +pub fn show_delete_page( reminders: &[Reminder], page: usize, timezone: Tz, diff --git a/src/commands/todo_cmds.rs b/src/commands/todo_cmds.rs index ce7f26f..6432608 100644 --- a/src/commands/todo_cmds.rs +++ b/src/commands/todo_cmds.rs @@ -1,7 +1,13 @@ use regex_command_attr::command; -use serenity::client::Context; +use serenity::{ + builder::{CreateEmbed, CreateInteractionResponse}, + client::Context, + model::interactions::InteractionResponseType, +}; use crate::{ + component_models::pager::TodoPager, + consts::{EMBED_DESCRIPTION_MAX_LENGTH, SELECT_MAX_ENTRIES, THEME_COLOR}, framework::{CommandInvoke, CommandOptions, CreateGenericResponse}, SQLPool, }; @@ -61,6 +67,8 @@ async fn todo(ctx: &Context, invoke: CommandInvoke, args: CommandOptions) { _ => (Some(invoke.author_id().0), None, None), }; + println!("{:?}", keys); + match args.get("task") { Some(task) => { let task = task.to_string(); @@ -82,7 +90,8 @@ async fn todo(ctx: &Context, invoke: CommandInvoke, args: CommandOptions) { } None => { let values = sqlx::query!( - "SELECT value FROM todos WHERE user_id = ? AND channel_id = ? AND guild_id = ?", + // fucking braindead mysql use <=> instead of = for null comparison + "SELECT value FROM todos WHERE user_id <=> ? AND channel_id <=> ? AND guild_id <=> ?", keys.0, keys.1, keys.2, @@ -91,8 +100,115 @@ async fn todo(ctx: &Context, invoke: CommandInvoke, args: CommandOptions) { .await .unwrap() .iter() - .map(|row| &row.value); + .map(|row| row.value.clone()) + .collect::>(); + + let resp = show_todo_page(&values, 0, keys.0, keys.1, keys.2); + + let interaction = invoke.interaction().unwrap(); + + let _ = interaction + .create_interaction_response(&ctx, |r| { + *r = resp; + r.kind(InteractionResponseType::ChannelMessageWithSource) + }) + .await + .unwrap(); } } } } + +pub fn max_todo_page(todo_values: &[String]) -> usize { + let mut rows = 0; + let mut char_count = 0; + + todo_values.iter().enumerate().map(|(c, v)| format!("{}: {}", c, v)).fold( + 1, + |mut pages, text| { + rows += 1; + char_count += text.len(); + + if char_count > EMBED_DESCRIPTION_MAX_LENGTH || rows > SELECT_MAX_ENTRIES { + rows = 1; + char_count = text.len(); + pages += 1; + } + + pages + }, + ) +} + +pub fn show_todo_page( + todo_values: &[String], + page: usize, + user_id: Option, + channel_id: Option, + guild_id: Option, +) -> CreateInteractionResponse { + // let pager = TodoPager::new(page, user_id, channel_id, guild_id); + + let pages = max_todo_page(todo_values); + let mut page = page; + if page >= pages { + page = pages - 1; + } + + let mut char_count = 0; + let mut rows = 0; + let mut skipped_rows = 0; + let mut skipped_char_count = 0; + let mut first_num = 0; + + let mut skipped_pages = 0; + + let display_vec: Vec = todo_values + .iter() + .enumerate() + .map(|(c, v)| format!("`{}`: {}", c + 1, v)) + .skip_while(|p| { + first_num += 1; + skipped_rows += 1; + skipped_char_count += p.len(); + + if skipped_char_count > EMBED_DESCRIPTION_MAX_LENGTH + || skipped_rows > SELECT_MAX_ENTRIES + { + skipped_rows = 1; + skipped_char_count = p.len(); + skipped_pages += 1; + } + + skipped_pages < page + }) + .take_while(|p| { + rows += 1; + char_count += p.len(); + + char_count < EMBED_DESCRIPTION_MAX_LENGTH && rows <= SELECT_MAX_ENTRIES + }) + .collect(); + + let display = display_vec.join("\n"); + + let title = if user_id.is_some() { + "Your" + } else if channel_id.is_some() { + "Channel" + } else { + "Server" + }; + + let mut embed = CreateEmbed::default(); + embed + .title(format!("{} Todo List", title)) + .description(display) + .footer(|f| f.text(format!("Page {} of {}", page + 1, pages))) + .color(*THEME_COLOR); + + let mut response = CreateInteractionResponse::default(); + response.interaction_response_data(|d| d.embeds(vec![embed])); + + response +} diff --git a/src/component_models/mod.rs b/src/component_models/mod.rs index f5cbb2a..31eba86 100644 --- a/src/component_models/mod.rs +++ b/src/component_models/mod.rs @@ -18,7 +18,7 @@ use serenity::{ use crate::{ commands::reminder_cmds::{max_delete_page, show_delete_page}, - component_models::pager::{DelPager, LookPager, Pager}, + component_models::pager::{DelPager, LookPager, Pager, TodoPager}, consts::{EMBED_DESCRIPTION_MAX_LENGTH, THEME_COLOR}, models::reminder::Reminder, SQLPool, @@ -31,6 +31,7 @@ pub enum ComponentDataModel { Restrict(Restrict), LookPager(LookPager), DelPager(DelPager), + TodoPager(TodoPager), DelSelector(DelSelector), } @@ -168,8 +169,7 @@ INSERT IGNORE INTO roles (role, name, guild_id) VALUES (?, \"Role\", (SELECT id let max_pages = max_delete_page(&reminders, &pager.timezone); - let resp = - show_delete_page(&reminders, pager.next_page(max_pages), pager.timezone).await; + let resp = show_delete_page(&reminders, pager.next_page(max_pages), pager.timezone); let _ = component .create_interaction_response(&ctx, move |r| { @@ -190,7 +190,7 @@ INSERT IGNORE INTO roles (role, name, guild_id) VALUES (?, \"Role\", (SELECT id let reminders = Reminder::from_guild(ctx, component.guild_id, component.user.id).await; - let resp = show_delete_page(&reminders, selector.page, selector.timezone).await; + let resp = show_delete_page(&reminders, selector.page, selector.timezone); let _ = component .create_interaction_response(&ctx, move |r| { @@ -199,6 +199,7 @@ INSERT IGNORE INTO roles (role, name, guild_id) VALUES (?, \"Role\", (SELECT id }) .await; } + ComponentDataModel::TodoPager(pager) => {} } } } diff --git a/src/component_models/pager.rs b/src/component_models/pager.rs index eb2eed7..172ba96 100644 --- a/src/component_models/pager.rs +++ b/src/component_models/pager.rs @@ -1,7 +1,14 @@ +// todo split pager out into a single struct use chrono_tz::Tz; use serde::{Deserialize, Serialize}; use serde_repr::*; -use serenity::{builder::CreateComponents, model::interactions::message_component::ButtonStyle}; +use serenity::{ + builder::CreateComponents, + model::{ + id::{ChannelId, GuildId, UserId}, + interactions::message_component::ButtonStyle, + }, +}; use crate::{component_models::ComponentDataModel, models::reminder::look_flags::LookFlags}; @@ -209,3 +216,123 @@ impl DelPager { ) } } + +#[derive(Deserialize, Serialize)] +pub struct TodoPager { + pub page: usize, + action: PageAction, + pub user_id: Option, + pub channel_id: Option, + pub guild_id: Option, +} + +impl Pager for TodoPager { + fn next_page(&self, max_pages: usize) -> usize { + match self.action { + PageAction::First => 0, + PageAction::Previous => 0.max(self.page - 1), + PageAction::Refresh => self.page, + PageAction::Next => (max_pages - 1).min(self.page + 1), + PageAction::Last => max_pages - 1, + } + } + + fn create_button_row(&self, max_pages: usize, comp: &mut CreateComponents) { + let next_page = self.next_page(max_pages); + + let (page_first, page_prev, page_refresh, page_next, page_last) = + TodoPager::buttons(next_page, self.user_id, self.channel_id, self.guild_id); + + comp.create_action_row(|row| { + row.create_button(|b| { + b.label("⏮️") + .style(ButtonStyle::Primary) + .custom_id(page_first.to_custom_id()) + .disabled(next_page == 0) + }) + .create_button(|b| { + b.label("◀️") + .style(ButtonStyle::Secondary) + .custom_id(page_prev.to_custom_id()) + .disabled(next_page == 0) + }) + .create_button(|b| { + b.label("🔁").style(ButtonStyle::Secondary).custom_id(page_refresh.to_custom_id()) + }) + .create_button(|b| { + b.label("▶️") + .style(ButtonStyle::Secondary) + .custom_id(page_next.to_custom_id()) + .disabled(next_page + 1 == max_pages) + }) + .create_button(|b| { + b.label("⏭️") + .style(ButtonStyle::Primary) + .custom_id(page_last.to_custom_id()) + .disabled(next_page + 1 == max_pages) + }) + }); + } +} + +impl TodoPager { + pub fn new( + page: usize, + user_id: Option, + channel_id: Option, + guild_id: Option, + ) -> Self { + Self { page, action: PageAction::Refresh, user_id, channel_id, guild_id } + } + + pub fn buttons( + page: usize, + user_id: Option, + channel_id: Option, + guild_id: Option, + ) -> ( + ComponentDataModel, + ComponentDataModel, + ComponentDataModel, + ComponentDataModel, + ComponentDataModel, + ) { + ( + ComponentDataModel::TodoPager(TodoPager { + page, + action: PageAction::First, + user_id, + channel_id, + guild_id, + }), + ComponentDataModel::TodoPager(TodoPager { + page, + action: PageAction::Previous, + user_id, + channel_id, + guild_id, + }), + ComponentDataModel::TodoPager(TodoPager { + page, + action: PageAction::Refresh, + user_id, + channel_id, + guild_id, + }), + ComponentDataModel::TodoPager(TodoPager { + page, + action: PageAction::Next, + user_id, + channel_id, + guild_id, + }), + ComponentDataModel::TodoPager(TodoPager { + page, + action: PageAction::Last, + user_id, + channel_id, + guild_id, + }), + ) + } +} diff --git a/src/framework.rs b/src/framework.rs index 823fe7c..b3752d3 100644 --- a/src/framework.rs +++ b/src/framework.rs @@ -614,7 +614,18 @@ impl RegexFramework { s.name(option.name) .description(option.description) .kind(option.kind) - .required(option.required) + .required(option.required); + + for sub_option in option.options { + s.create_sub_option(|ss| { + ss.name(sub_option.name) + .description(sub_option.description) + .kind(sub_option.kind) + .required(sub_option.required) + }); + } + + s }); } diff --git a/src/models/reminder/mod.rs b/src/models/reminder/mod.rs index 25fe205..2ac11f6 100644 --- a/src/models/reminder/mod.rs +++ b/src/models/reminder/mod.rs @@ -318,18 +318,4 @@ WHERE ) } } - - pub async fn delete(&self, ctx: &Context) { - let pool = ctx.data.read().await.get::().cloned().unwrap(); - - sqlx::query!( - " -DELETE FROM reminders WHERE id = ? - ", - self.id - ) - .execute(&pool) - .await - .unwrap(); - } }