Remove old selector implementation

This commit is contained in:
Ev-1 committed 2022-10-02 21:41:07 +02:00
1 parent 8ccab9afea
commit 75c283e959
7 files changed
+145 -305

No files matched your search

+1
View File
@@ -12,6 +12,7 @@ from argparse import ArgumentParser, RawTextHelpFormatter
import aiohttp
import yaml
# Bot Utilities
from musicbot.utils.localisation import Aliaser, LocalizedContext, Localizer, LocalizerWrapper
from musicbot.utils.logger import BotLogger
from musicbot.utils.settingsmanager import Settings
+1 -1
View File
@@ -62,7 +62,7 @@ scrub:
enqueue:
position: position
playing_in: Playing in
enqueued: Song enqueued
enqueued: Song queued
estimated: estimated
toolong: Track too long. `{_length}>{_max}`
history:
+71 -63
View File
@@ -12,11 +12,12 @@ import urllib
from bs4 import BeautifulSoup
# Bot Utilities
from musicbot.utils.mixplayer.player import MixPlayer
from ...utils import checks, thumbnailer, timeformatter
from ...utils.userinteraction import ClearOn, QueuePaginator, Scroller, Selector, TextPaginator
from ...utils.userinteraction.selector import Selector2, SelectorButton, SelectorItem
from ...utils.userinteraction.paginators import QueuePaginator, TextPaginator
from ...utils.userinteraction.scroller import ClearOn, Scroller
from ...utils.userinteraction.selector import SelectMode, Selector, SelectorButton, SelectorItem
from .decorators import BasicVoiceClient, require_playing, require_queue, require_voice_connection, voteable
from .music_errors import WrongTextChannelError
@@ -63,6 +64,7 @@ class Music(commands.Cog):
return embed, False
track.extra["thumbnail_url"] = await thumbnailer.ThumbNailer.identify(self, track.identifier, track.uri)
track.requester = ctx.author.id
# Add to player
track, pos_global, pos_local = player.add(requester=ctx.author.id, track=track)
@@ -90,25 +92,27 @@ class Music(commands.Cog):
return embed, True
def get_current_song_embed(self, ctx, include_time=False):
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id)
if not player.current:
return
song = f'**[{player.current.title}]({player.current.uri})**'
if include_time:
position = timeformatter.format_ms(player.position)
if player.current.stream:
duration = '{live}'
else:
duration = timeformatter.format_ms(player.current.duration)
song = f'**[{player.current.title}]({player.current.uri})**\n({position}/{duration})'
else:
song = f'**[{player.current.title}]({player.current.uri})**'
song += f'\n({position}/{duration})'
embed = discord.Embed(color=ctx.me.color, description=song, title='{now}')
member = ctx.guild.get_member(player.current.requester)
if thumbnail_url := player.current.extra["thumbnail_url"]:
embed.set_thumbnail(url=thumbnail_url)
embed.set_footer(text=f'{{requested_by}} {member.display_name}', icon_url=member.display_avatar.url)
if member := ctx.guild.get_member(player.current.requester):
embed.set_footer(text=f'{{requested_by}} {member.display_name}', icon_url=member.display_avatar.url)
embed = ctx.localizer.format_embed(embed)
return embed
@@ -189,11 +193,8 @@ class Music(commands.Cog):
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
await player.skip()
if player.current:
embed = self.get_current_song_embed(ctx)
await ctx.send(ctx.localizer.format_str("{skip.skipped}"), embed=embed)
else:
await ctx.send(ctx.localizer.format_str("{skip.skipped}"))
embed = self.get_current_song_embed(ctx)
await ctx.send(ctx.localizer.format_str("{skip.skipped}"), embed=embed)
@commands.command(name='skipto')
@checks.dj_or(alone=True)
@@ -229,9 +230,9 @@ class Music(commands.Cog):
@commands.command(name='now')
@require_playing()
async def _now(self, ctx):
embed = self.get_current_song_embed(ctx, include_time=True)
embed = ctx.localizer.format_embed(embed)
await ctx.send(embed=embed)
if embed := self.get_current_song_embed(ctx, include_time=True):
embed = ctx.localizer.format_embed(embed)
await ctx.send(embed=embed)
@commands.command(name='queue')
@require_queue(require_member_queue=True)
@@ -275,57 +276,63 @@ class Music(commands.Cog):
@require_queue(require_author_queue=True)
async def _move(self, ctx):
""" Moves a song in your queue. """
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id)
user_queue = player.user_queue(ctx.author.id, dual=True)
# We create a new selector for each selection
def move_selector(ctx, queue, title, first: bool):
async def return_track(track):
return track
# Setup for selector
identifiers = []
for index, temp in enumerate(user_queue):
track, globpos = temp
identifiers.append(ctx.localizer.format_str("{queue.usertrack}", _index=index+1,
_globalindex=globpos+1, _title=track.title,
_uri=track.uri))
choices = []
for index, (track, _) in enumerate(queue, start=1):
blank = " " * 6
if first:
prefix = f'`{blank}`\n`{index:<6}`'
else:
prefix = f'`{index:<3}-> `\n`{blank}`'
label = f'{prefix} [{track.title}]({track.uri})'
selection = SelectorItem(label, str(index), wrap_in_button_callback(return_track, track))
choices.append(selection)
def ret_first_arg(*args):
return args[0]
return Selector(ctx, choices, select_mode=SelectMode.SpanningMultiSelect, use_tick_for_stop_emoji=True,
color=ctx.me.color, title=ctx.localizer.format_str(title))
functions = [ret_first_arg]*len(user_queue)
arguments = [(i,) for i in range(len(user_queue))]
message = None
page = 0
while True:
# Prompt the user for which track to move
selector = move_selector(ctx, player.user_queue(ctx.author.id, dual=True), "{moved.choose_pos}", True)
message, track_to_move = await selector.start_scrolling(ClearOn.Timeout, message, page)
page = selector.page_number
# Set the different titles for the different selections
round_titles = ["{moved.choose_song}", "{moved.choose_pos}"]
round_titles = [ctx.localizer.format_str(i) for i in round_titles]
if not track_to_move:
break
selector = Selector(ctx, identifiers, functions, arguments, num_selections=5, round_titles=round_titles,
color=ctx.me.color, title=ctx.localizer.format_str("{moved.choose}"))
# Prompt the user for where to move it
selector = move_selector(ctx, player.user_queue(ctx.author.id, dual=True), "{moved.choose_song}", False)
message, track_to_replace = await selector.start_scrolling(ClearOn.Timeout, message, page)
page = selector.page_number
# Get the initial and final position
message, page, selections = await selector.start_scrolling()
if not track_to_replace:
break
try:
pos_initial, pos_final = selections[0], selections[1]
except (IndexError, TypeError):
return await message.delete()
# At this point the user has chosen two songs, we will move the first song to be in front of the second
# one in the queue. Find the index of both songs in the current queue
# Move the track
moved = player.move_user_track(ctx.author.id, pos_initial, pos_final)
# Keep updating the user queue in case it changes while the user is selecting songs
user_queue = player.user_queue(ctx.author.id, dual=True)
user_queue_identifiers = [track for (track, _) in user_queue]
try:
pos_initial = user_queue_identifiers.index(track_to_move[0])
pos_final = user_queue_identifiers.index(track_to_replace[0])
player.move_user_track(ctx.author.id, pos_initial, pos_final)
except ValueError:
# Track not found in queue. Queue could have changed during selection
# if a song was skipped or the current song finished
pass
# Create a nice embed explaining what happened
song = f'**[{moved.title}]({moved.uri})**'
embed = discord.Embed(color=ctx.me.color, description=song, title='{moved.moved}')
member = ctx.guild.get_member(moved.requester)
if thumbnail_url := player.current.extra["thumbnail_url"]:
embed.set_thumbnail(url=thumbnail_url)
embed.add_field(name="{moved.local}", value=f"`{pos_initial + 1} → {pos_final + 1}`", inline=True)
embed.add_field(name="{moved.global}", value=f"`{player.queue._loc_to_glob(ctx.author.id, pos_initial) + 1}\
→ {player.queue._loc_to_glob(ctx.author.id, pos_final) + 1}`", inline=True)
embed.set_footer(text=f'{{requested_by}} {member.display_name}', icon_url=member.display_avatar.url)
embed = ctx.localizer.format_embed(embed)
await message.edit(embed=embed)
if message:
await message.delete()
@commands.command(name='remove')
@require_voice_connection()
@@ -361,8 +368,8 @@ class Music(commands.Cog):
SelectorItem(f'`{index}` [{track.title}]({track.uri})', str(index),
add_change_button_color(update_remove_list, tracks_to_remove, track)))
remove_selector = Selector2(ctx, selector_buttons, terminate_on_select=False, use_tick_for_stop_emoji=True,
color=ctx.me.color, title='Select songs to remove')
remove_selector = Selector(ctx, selector_buttons, select_mode=SelectMode.MultiSelect,
use_tick_for_stop_emoji=True, color=ctx.me.color, title='Select songs to remove')
await remove_selector.start_scrolling(ClearOn.AnyExit)
# If any tracks were removed create a scroller for navigating them
@@ -442,7 +449,8 @@ class Music(commands.Cog):
self.enqueue, ctx, track, discord.Embed(color=ctx.me.color)))
buttons.append(interaction)
search_selector = Selector2(ctx, buttons, color=ctx.me.color, title=ctx.localizer.format_str('{results}'))
search_selector = Selector(ctx, buttons, select_mode=SelectMode.SingleSelect,
color=ctx.me.color, title=ctx.localizer.format_str('{results}'))
message, callback_results = await search_selector.start_scrolling()
if len(callback_results) == 0:
@@ -732,8 +740,8 @@ class Music(commands.Cog):
SelectorItem("", '\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE}', seek_callback(player, 15)),
SelectorItem("", '\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}', seek_callback(player, 1000))
]
scrubber = Selector2(ctx, scrubber_controls, terminate_on_select=False, use_tick_for_stop_emoji=True,
default_text=ctx.localizer.format_str(controls), color=ctx.me.color)
scrubber = Selector(ctx, scrubber_controls, select_mode=SelectMode.MultiSelect, use_tick_for_stop_emoji=True,
default_text=ctx.localizer.format_str(controls), color=ctx.me.color)
await scrubber.start_scrolling(ClearOn.AnyExit)
@commands.group(name='loop')
+1 -1
View File
@@ -1 +1 @@
bot_version = "0.2.13"
bot_version = "0.3.0"
+32 -16
View File
@@ -2,8 +2,8 @@ from __future__ import annotations
# Discord Packages
import discord
from lavalink.client import asyncio
import asyncio
from enum import Flag, auto
from typing import List, Union
@@ -88,21 +88,31 @@ class Scroller:
if not self.permissions.send_messages:
raise CantScrollException('Bot cannot send messages.')
async def start_scrolling(self, clear_mode: ClearOn) -> discord.Message:
async def start_scrolling(self, clear_mode: ClearOn,
message: Union[discord.Message, None] = None,
start_page: int = 0) -> discord.Message:
self.clear_mode = clear_mode
self.current_page_number = 0
self.page_number = start_page
self.build_view()
self.message = await self.channel.send(embed=self.paginator.pages[0], view=self.view)
self.update_view()
if message:
self.message = message
await self.message.edit(embed=self.current_page_embed, view=self.view)
else:
self.message = await self.channel.send(embed=self.current_page_embed, view=self.view)
await self.scrolling_done.wait()
return self.message
def update_view(self, interaction: discord.Interaction):
def update_view_on_interaction(self, interaction: discord.Interaction):
self.update_view()
def update_view(self):
if self.use_nav_bar:
self.navigator.placeholder = f"Page: {self.current_page_number + 1}/{len(self.paginator.pages)}"
self.navigator.placeholder = f"Page: {self.page_number + 1}/{len(self.paginator.pages)}"
if self.is_scrolling_paginator:
self.back_button.disabled = self.current_page_number == 0
self.forward_button.disabled = self.current_page_number == len(self.paginator.pages)-1
self.back_button.disabled = self.page_number == 0
self.forward_button.disabled = self.page_number == len(self.paginator.pages)-1
def build_view(self):
for button in self.control_buttons:
@@ -114,10 +124,11 @@ class Scroller:
for (i, _) in enumerate(self.paginator.pages):
self.navigator.add_option(label=str(i+1), value=str(i))
async def stop(self, user_stopped: bool):
async def stop(self, user_stopped: bool, clear_scroller_view: bool = True):
self.is_scrolling_paginator = False
self.view.stop()
self.view.clear_items()
if clear_scroller_view:
self.view.clear_items()
if (user_stopped and self.clear_mode & ClearOn.ManualExit or
not user_stopped and self.clear_mode & ClearOn.Timeout):
if self.message:
@@ -131,7 +142,8 @@ class Scroller:
except discord.HTTPException:
pass
else:
await self.update_message()
if clear_scroller_view:
await self.update_message()
# Notify the start_scrolling function that we're done
self.scrolling_done.set()
@@ -142,17 +154,17 @@ class Scroller:
if page < 0 or page >= len(self.paginator.pages):
return
self.current_page_number = page
self.page_number = page
if interaction.message:
# Update the view before we edit the message
self.update_view(interaction)
self.update_view_on_interaction(interaction)
await self.update_message()
await interaction.response.defer()
async def update_message(self):
if self.message:
await self.message.edit(embed=self.paginator.pages[self.current_page_number], view=self.view)
await self.message.edit(embed=self.current_page_embed, view=self.view)
async def first_page(self, interaction: discord.Interaction):
await self._scroll(0, interaction)
@@ -161,10 +173,10 @@ class Scroller:
await self._scroll(len(self.paginator.pages) - 1, interaction)
async def next_page(self, interaction: discord.Interaction):
await self._scroll(self.current_page_number + 1, interaction)
await self._scroll(self.page_number + 1, interaction)
async def previous_page(self, interaction: discord.Interaction):
await self._scroll(self.current_page_number - 1, interaction)
await self._scroll(self.page_number - 1, interaction)
async def navigate(self, interaction: discord.Interaction):
await self._scroll(int(self.navigator.values[0]), interaction)
@@ -175,3 +187,7 @@ class Scroller:
async def on_timeout(self):
await self.stop(user_stopped=False)
@property
def current_page_embed(self):
return self.paginator.pages[self.page_number]
+38 -223
View File
@@ -3,14 +3,19 @@ from __future__ import annotations
# Discord Packages
import discord
import asyncio
import inspect
from typing import Callable, Coroutine, List
from enum import Enum, auto
from typing import Callable, Coroutine, List, Union
from .paginators import TextPaginator
from .scroller import ClearOn, Scroller
class SelectMode(Enum):
SingleSelect = auto()
MultiSelect = auto()
SpanningMultiSelect = auto() # Used to keep the view around after select
class SelectorButton(discord.ui.Button):
def __init__(self, label, callback: Callable[[discord.Interaction, SelectorButton], Coroutine],
style=discord.ButtonStyle.gray, **kwargs):
@@ -34,15 +39,11 @@ class SelectorItem:
self.callback = callback
class Selector2(TextPaginator, Scroller):
def __init__(self, ctx, choices: List[SelectorItem], round_titles=None, use_tick_for_stop_emoji: bool = False,
terminate_on_select: bool = True, max_size=2000, default_text=" ", **embed_base):
self.match = None
if round_titles is None:
round_titles = []
self.match = None
self.terminate_on_select = terminate_on_select
self.round_titles = round_titles
class Selector(TextPaginator, Scroller):
def __init__(self, ctx, choices: List[SelectorItem], select_mode: SelectMode,
use_tick_for_stop_emoji: bool = False, max_size=2000, default_text=" ", **embed_base):
self.selector_mode: SelectMode = select_mode
self.selections_per_page = 5
self.selections = choices
@@ -63,10 +64,10 @@ class Selector2(TextPaginator, Scroller):
self.buttons = []
# The list of currently visible buttons, gets cleared upon scroll.
self.visible_buttons = []
self.currently_visible_buttons = []
# Depending on the selector we might want the result of callbacks
# after we have finished interacting with the scroller
# Depending on the selector we might want the result of calculations in
# the callbacks after we have finished interacting with the scroller
self.callback_results = []
# Wrap the provided so that we can both update the view if needed
@@ -78,12 +79,17 @@ class Selector2(TextPaginator, Scroller):
result = await func(interaction, button)
self.callback_results.append(result)
self.update_view(interaction)
self.update_view_on_interaction(interaction)
await interaction.response.defer()
if terminate_on_select:
await self.stop(True)
else:
await self.update_message()
match self.selector_mode:
case SelectMode.SingleSelect:
await self.stop(True, clear_scroller_view=True)
case SelectMode.SpanningMultiSelect:
await self.stop(True, clear_scroller_view=False)
case _:
await self.update_message()
return wrapped_callback
for choice in self.selections:
@@ -93,216 +99,25 @@ class Selector2(TextPaginator, Scroller):
super().build_view()
self.update_buttons()
def update_view(self, interaction: discord.Interaction):
super().update_view(interaction)
def update_view(self):
super().update_view()
self.update_buttons()
def update_buttons(self):
for button in self.visible_buttons:
for button in self.currently_visible_buttons:
self.view.remove_item(item=button)
self.visible_buttons = []
self.current_page_number
start = self.current_page_number * self.selections_per_page
end = min((self.current_page_number + 1) * self.selections_per_page, len(self.selections))
self.currently_visible_buttons = []
self.page_number
start = self.page_number * self.selections_per_page
end = min((self.page_number + 1) * self.selections_per_page, len(self.selections))
for button in self.buttons[start:end]:
self.view.add_item(item=button)
self.visible_buttons.append(button)
self.currently_visible_buttons.append(button)
async def start_scrolling(self, clear_mode: ClearOn = ClearOn.Timeout):
message = await super().start_scrolling(clear_mode)
async def start_scrolling(self, clear_mode: ClearOn = ClearOn.Timeout,
message: Union[discord.Message, None] = None,
start_page: int = 0):
message = await super().start_scrolling(clear_mode, message, start_page)
return message, self.callback_results
class Selector(TextPaginator):
def __init__(self, ctx, identifiers, functions, arguments, terminate_on_select=True, num_selections=3,
round_titles=None, max_size=2000, **embed_base):
"""
Selector, tool for making interactive menus.
:param ctx: discord.py context
:param identifiers: A list of strings, one for each menu item
:param functions: List of functions, one for each menu item. Gets run when the corresponding
menu item is selected
:param arguments: List of arguments to pass to the corresponding function in functions.
:param terminate_on_select:
:param num_selections: the number of selectable items per page
:param round_titles: A list of embed titles, updates each time a menu selection is made
:param max_size: The max amount of characters per page.
:param embed_base: Arguments that specify the look of the scroller embeds
"""
if round_titles is None:
round_titles = []
self.match = None
self.current_page = 0
self.ctx = ctx
self.bot = ctx.bot
self.cmdmsg = ctx.message
self.message = ctx.message
self.channel = ctx.channel
self.author = ctx.author
self.localizer = ctx.localizer
self.num_selections = num_selections
self.terminate_on_select = terminate_on_select
self.round_titles = round_titles
assert len(identifiers) == len(functions) == len(arguments)
super().__init__(max_size=max_size, max_lines=num_selections, **embed_base)
self.selections = list(zip(identifiers, functions, arguments))
self.max_selections = min(len(identifiers), num_selections)
for i, selection in enumerate(self.selections):
self.add_line(f"{i%num_selections+1}\N{combining enclosing keycap} {selection[0]}")
self.close_page()
if len(self.pages) > 1:
self.multipage = True
else:
self.multipage = False
assert num_selections <= 10
self.select_emojis = [f"{i+1}\N{combining enclosing keycap}" for i in range(num_selections)]
self.control_emojis = [
('\N{BLACK LEFT-POINTING TRIANGLE}', self.previous_page),
('\N{BLACK RIGHT-POINTING TRIANGLE}', self.next_page),
('❌', self.stop_scrolling),
]
self.timeout = 30
self.args = None
self.control_input = False
self.scrolling = True
self.stopped = False
self.add_page_indicator(self.localizer, "{queue.pageindicator}")
if self.round_titles:
self.update_embed_title(self.round_titles.pop(0))
async def scroll(self, page):
if page < 0 or page >= len(self.pages):
return
self.current_page = page
await self.message.edit(embed=self.pages[page])
async def next_page(self):
await self.scroll(self.current_page + 1)
async def previous_page(self):
await self.scroll(self.current_page - 1)
async def stop_scrolling(self):
self.scrolling = False
await self.message.clear_reactions()
def update_embed_title(self, updated_title):
for page in self.pages:
page.title = updated_title
def _react_check(self, reaction, user):
if user is None or user.id != self.author.id:
return False
if reaction.message.id != self.message.id:
return False
for i, emoji in enumerate(self.select_emojis):
if emoji == reaction.emoji:
choice = i + self.current_page * self.num_selections
if choice >= len(self.selections):
return True
self.match = self.selections[choice][1]
self.args = self.selections[choice][2]
return True
for (emoji, func) in self.control_emojis:
if reaction.emoji == emoji:
self.match = func
self.control_input = True
return True
return False
async def send(self):
# No embeds to scroll through
if not self.pages:
return
self.message = await self.channel.send(embed=self.pages[0])
for reaction in self.select_emojis[:self.max_selections]:
if not self.stopped:
await self.message.add_reaction(reaction)
if self.multipage:
for reaction in list(zip(*self.control_emojis))[0]:
if not self.stopped:
await self.message.add_reaction(reaction)
else:
if not self.stopped:
await self.message.add_reaction('❌')
async def start_scrolling(self):
result = None
if not self.scrolling:
await self.send()
else:
self.bot.loop.create_task(self.send())
current_round = 0
results = []
while self.scrolling:
try:
reaction, user = await self.bot.wait_for('reaction_add', check=self._react_check, timeout=self.timeout)
except asyncio.TimeoutError:
self.scrolling = False
try:
await self.message.clear_reactions()
except (discord.Forbidden, discord.HTTPException):
pass
finally:
break
try:
await self.message.remove_reaction(reaction, user)
except (discord.Forbidden, discord.HTTPException, discord.NotFound, discord.InvalidArgument):
pass
if self.match:
if inspect.iscoroutinefunction(self.match):
if self.args:
result = await self.match(*self.args)
else:
result = await self.match()
else:
if self.args:
result = self.match(*self.args)
else:
result = self.match()
else:
continue
if self.terminate_on_select and not self.control_input:
current_round += 1
results.append(result)
if not self.round_titles:
self.stopped = True
try:
await self.message.clear_reactions()
except (discord.Forbidden, discord.HTTPException):
pass
break
if self.round_titles:
self.update_embed_title(self.round_titles.pop(0))
await self.scroll(self.current_page)
self.args = None
self.control_input = False
if len(results) == 1:
self.stopped = True
await self.message.clear_reactions()
return self.message, self.pages[self.current_page], results[0]
else:
self.stopped = True
await self.message.clear_reactions()
return self.message, self.pages[self.current_page], results
+1 -1
View File
@@ -5,7 +5,7 @@ statistics = True
[isort]
known_discord = discord,lavalink
known_utils = cogs.utils,cogs.helpformatter
known_utils = cogs.utils,cogs.helpformatter,musicbot.utils
import_heading_discord = Discord Packages
import_heading_utils = Bot Utilities
sections = FUTURE,DISCORD,STDLIB,THIRDPARTY,FIRSTPARTY,UTILS,LOCALFOLDER