Refactor controller UI and bump version
Refactor interactive controller components and tweak command signatures. - Make lyrics command accept keyword-only args (adds a leading * for title/artist). - Bump version to v2.7.3b3. - Import dispatch_message and add send_embed helper to ControlButton for embed responses. - Introduce ControlSelect (base for selects) and refactor Tracks/Effects to inherit it, pass player explicitly and fix placeholder/player usage. - Namespace localization keys (e.g. player.controls.forward/rewind) and defer interaction responses where appropriate. - Replace direct send_localized_message calls with ControlSelect/ControlButton send wrappers to honor controller message settings. - Add additional on_error handling for VoicelinkException and ensure non-ephemeral message deletion behavior follows controller settings. These changes improve consistency for UI components, interaction handling, and localization usage.
This commit is contained in:
@@ -790,7 +790,7 @@ class Basic(commands.Cog):
|
|||||||
@commands.hybrid_command(name="lyrics", aliases=get_aliases("lyrics"))
|
@commands.hybrid_command(name="lyrics", aliases=get_aliases("lyrics"))
|
||||||
@app_commands.describe(title="Searches for your query and displays the reutned lyrics.")
|
@app_commands.describe(title="Searches for your query and displays the reutned lyrics.")
|
||||||
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
|
@commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild)
|
||||||
async def lyrics(self, ctx: commands.Context, title: str = "", artist: str = ""):
|
async def lyrics(self, ctx: commands.Context, *, title: str = "", artist: str = ""):
|
||||||
"Displays lyrics for the playing track."
|
"Displays lyrics for the playing track."
|
||||||
if not title:
|
if not title:
|
||||||
player: voicelink.Player = ctx.guild.voice_client
|
player: voicelink.Player = ctx.guild.voice_client
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import subprocess
|
|||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
|
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
__version__ = "v2.7.3b2"
|
__version__ = "v2.7.3b3"
|
||||||
|
|
||||||
# URLs for update and migration
|
# URLs for update and migration
|
||||||
PYTHON_CMD_NAME = os.path.basename(sys.executable)
|
PYTHON_CMD_NAME = os.path.basename(sys.executable)
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ from discord.ext import commands
|
|||||||
from typing import Optional, Dict, Type, Union, Any
|
from typing import Optional, Dict, Type, Union, Any
|
||||||
|
|
||||||
from ..config import Config
|
from ..config import Config
|
||||||
from ..utils import format_ms, send_localized_message
|
from ..utils import format_ms, send_localized_message, dispatch_message
|
||||||
from ..language import LangHandler
|
from ..language import LangHandler
|
||||||
from ..mongodb import MongoDBHandler
|
from ..mongodb import MongoDBHandler
|
||||||
|
|
||||||
@@ -87,6 +87,34 @@ class ControlButton(discord.ui.Button):
|
|||||||
ephemeral=ephemeral
|
ephemeral=ephemeral
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def send_embed(self, interaction: discord.Interaction, embed: discord.Embed, *params, view: discord.ui.View = None, ephemeral: bool = False) -> None:
|
||||||
|
stay = self.player.settings.get("controller_msg", True)
|
||||||
|
return await dispatch_message(
|
||||||
|
interaction, embed, *params,
|
||||||
|
view=view,
|
||||||
|
delete_after=None if ephemeral or stay else 10,
|
||||||
|
ephemeral=ephemeral
|
||||||
|
)
|
||||||
|
|
||||||
|
class ControlSelect(discord.ui.Select):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
player: "voicelink.Player",
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
self.player: voicelink.Player = player
|
||||||
|
|
||||||
|
async def send(self, interaction: discord.Interaction, key: str, *params, view: discord.ui.View = None, ephemeral: bool = False) -> None:
|
||||||
|
stay = self.player.settings.get("controller_msg", True)
|
||||||
|
return await send_localized_message(
|
||||||
|
interaction, key, *params,
|
||||||
|
view=view,
|
||||||
|
delete_after=None if ephemeral or stay else 10,
|
||||||
|
ephemeral=ephemeral
|
||||||
|
)
|
||||||
|
|
||||||
class Back(ControlButton):
|
class Back(ControlButton):
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
@@ -319,7 +347,7 @@ class Forward(ControlButton):
|
|||||||
position = int(self.player.position + 10000)
|
position = int(self.player.position + 10000)
|
||||||
|
|
||||||
await self.player.seek(position)
|
await self.player.seek(position)
|
||||||
await self.send(interaction, 'forward', format_ms(position))
|
await self.send(interaction, 'player.controls.forward', format_ms(position))
|
||||||
|
|
||||||
class Rewind(ControlButton):
|
class Rewind(ControlButton):
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
@@ -338,7 +366,7 @@ class Rewind(ControlButton):
|
|||||||
position = 0 if (value := int(self.player.position - 30000)) <= 0 else value
|
position = 0 if (value := int(self.player.position - 30000)) <= 0 else value
|
||||||
|
|
||||||
await self.player.seek(position)
|
await self.player.seek(position)
|
||||||
await self.send(interaction, 'rewind', format_ms(position))
|
await self.send(interaction, 'player.controls.rewind', format_ms(position))
|
||||||
|
|
||||||
class Lyrics(ControlButton):
|
class Lyrics(ControlButton):
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
@@ -352,6 +380,8 @@ class Lyrics(ControlButton):
|
|||||||
if not self.player or not self.player.is_playing:
|
if not self.player or not self.player.is_playing:
|
||||||
return await self.send(interaction, "player.errors.noTrackPlaying", ephemeral=True)
|
return await self.send(interaction, "player.errors.noTrackPlaying", ephemeral=True)
|
||||||
|
|
||||||
|
await interaction.response.defer()
|
||||||
|
|
||||||
title = self.player.current.title
|
title = self.player.current.title
|
||||||
artist = self.player.current.author
|
artist = self.player.current.author
|
||||||
|
|
||||||
@@ -362,23 +392,22 @@ class Lyrics(ControlButton):
|
|||||||
return await self.send(interaction, "lyrics.notFound", ephemeral=True)
|
return await self.send(interaction, "lyrics.notFound", ephemeral=True)
|
||||||
|
|
||||||
view = LyricsView(name=title, source={_: re.findall(r'.*\n(?:.*\n){,22}', v or "") for _, v in lyrics.items()}, author=interaction.user)
|
view = LyricsView(name=title, source={_: re.findall(r'.*\n(?:.*\n){,22}', v or "") for _, v in lyrics.items()}, author=interaction.user)
|
||||||
view.response = await self.send(interaction, view.build_embed(), view=view, ephemeral=True)
|
view.response = await self.send_embed(interaction, await view.build_embed(), view=view, ephemeral=True)
|
||||||
|
|
||||||
class Tracks(discord.ui.Select):
|
class Tracks(ControlSelect):
|
||||||
def __init__(self, player: "voicelink.Player", btn_data, **kwargs):
|
def __init__(self, player: "voicelink.Player", btn_data, **kwargs):
|
||||||
self.player: voicelink.Player = player
|
|
||||||
|
|
||||||
if player.queue.is_empty:
|
if player.queue.is_empty:
|
||||||
raise ValueError("Player queue is empty, cannot create Tracks row instance.")
|
raise ValueError("Player queue is empty, cannot create Tracks row instance.")
|
||||||
|
|
||||||
options = []
|
options = []
|
||||||
for index, track in enumerate(self.player.queue.tracks(), start=1):
|
for index, track in enumerate(player.queue.tracks(), start=1):
|
||||||
if index > min(max(btn_data.get("max_options", 10), 1), 25):
|
if index > min(max(btn_data.get("max_options", 10), 1), 25):
|
||||||
break
|
break
|
||||||
options.append(discord.SelectOption(label=f"{index}. {track.title[:40]}", description=f"{track.author[:30]} · " + ("Live" if track.is_stream else track.formatted_length), emoji=track.emoji))
|
options.append(discord.SelectOption(label=f"{index}. {track.title[:40]}", description=f"{track.author[:30]} · " + ("Live" if track.is_stream else track.formatted_length), emoji=track.emoji))
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
placeholder=self.player._ph.replace(btn_data.get("label"), {}),
|
placeholder=player._ph.replace(btn_data.get("label"), {}),
|
||||||
|
player=player,
|
||||||
options=options,
|
options=options,
|
||||||
disabled=player.queue.is_empty,
|
disabled=player.queue.is_empty,
|
||||||
**kwargs
|
**kwargs
|
||||||
@@ -386,47 +415,49 @@ class Tracks(discord.ui.Select):
|
|||||||
|
|
||||||
async def callback(self, interaction: discord.Interaction):
|
async def callback(self, interaction: discord.Interaction):
|
||||||
if not self.player.is_privileged(interaction.user):
|
if not self.player.is_privileged(interaction.user):
|
||||||
return await send_localized_message(interaction, "permissions.missingFunction", ephemeral=True)
|
return await self.send(interaction, "permissions.missingFunction", ephemeral=True)
|
||||||
|
|
||||||
|
await interaction.response.defer()
|
||||||
|
|
||||||
self.player.queue.skipto(int(self.values[0].split(". ")[0]))
|
self.player.queue.skipto(int(self.values[0].split(". ")[0]))
|
||||||
await self.player.stop()
|
await self.player.stop()
|
||||||
|
|
||||||
if self.player.settings.get("controller_msg", True):
|
if self.player.settings.get("controller_msg", True):
|
||||||
await send_localized_message(interaction, "player.controls.skip.success", interaction.user)
|
await self.send(interaction, "player.controls.skip.success", interaction.user)
|
||||||
|
|
||||||
class Effects(discord.ui.Select):
|
class Effects(ControlSelect):
|
||||||
def __init__(self, player: "voicelink.Player", btn_data, row):
|
def __init__(self, player: "voicelink.Player", btn_data, row):
|
||||||
|
|
||||||
self.player: voicelink.Player = player
|
|
||||||
|
|
||||||
options = [discord.SelectOption(label="None", value="None")]
|
options = [discord.SelectOption(label="None", value="None")]
|
||||||
for name in voicelink.Filters.get_available_filters():
|
for name in voicelink.Filters.get_available_filters():
|
||||||
options.append(discord.SelectOption(label=name.capitalize(), value=name))
|
options.append(discord.SelectOption(label=name.capitalize(), value=name))
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
placeholder=self.player._ph.replace(btn_data.get("label"), {}),
|
placeholder=player._ph.replace(btn_data.get("label"), {}),
|
||||||
|
player=player,
|
||||||
options=options,
|
options=options,
|
||||||
row=row
|
row=row
|
||||||
)
|
)
|
||||||
|
|
||||||
async def callback(self, interaction: discord.Interaction):
|
async def callback(self, interaction: discord.Interaction):
|
||||||
if not self.player.is_privileged(interaction.user):
|
if not self.player.is_privileged(interaction.user):
|
||||||
return await send_localized_message(interaction, "permissions.missingFunction", ephemeral=True)
|
return await self.send(interaction, "permissions.missingFunction", ephemeral=True)
|
||||||
|
|
||||||
|
await interaction.response.defer()
|
||||||
|
|
||||||
avalibable_filters = voicelink.Filters.get_available_filters()
|
avalibable_filters = voicelink.Filters.get_available_filters()
|
||||||
if self.values[0] == "None":
|
if self.values[0] == "None":
|
||||||
await self.player.reset_filter(requester=interaction.user)
|
await self.player.reset_filter(requester=interaction.user)
|
||||||
return await send_localized_message(interaction, "effects.cleared")
|
return await self.send(interaction, "effects.cleared")
|
||||||
|
|
||||||
selected_filter = avalibable_filters.get(self.values[0].lower())()
|
selected_filter: voicelink.Filter = avalibable_filters.get(self.values[0].lower())()
|
||||||
if self.player.filters.has_filter(filter_tag=selected_filter.tag):
|
if self.player.filters.has_filter(filter_tag=selected_filter.tag):
|
||||||
await self.player.remove_filter(filter_tag=selected_filter.tag, requester=interaction.user)
|
await self.player.remove_filter(filter_tag=selected_filter.tag, requester=interaction.user)
|
||||||
await send_localized_message(interaction, "effects.cleared")
|
await self.send(interaction, "effects.cleared")
|
||||||
else:
|
else:
|
||||||
await self.player.add_filter(selected_filter, requester=interaction.user)
|
await self.player.add_filter(selected_filter, requester=interaction.user)
|
||||||
await send_localized_message(interaction, "effects.added", selected_filter.tag)
|
await self.send(interaction, "effects.added", selected_filter.tag)
|
||||||
|
|
||||||
BUTTON_TYPE: Dict[str, Type[Union[ControlButton, discord.ui.Select]]] = {
|
BUTTON_TYPE: Dict[str, Type[Union[ControlButton, ControlSelect]]] = {
|
||||||
"back": Back,
|
"back": Back,
|
||||||
"play-pause": PlayPause,
|
"play-pause": PlayPause,
|
||||||
"skip": Skip,
|
"skip": Skip,
|
||||||
@@ -446,7 +477,7 @@ BUTTON_TYPE: Dict[str, Type[Union[ControlButton, discord.ui.Select]]] = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class InteractiveController(discord.ui.View):
|
class InteractiveController(discord.ui.View):
|
||||||
def __init__(self, player):
|
def __init__(self, player: "voicelink.Player"):
|
||||||
super().__init__(timeout=None)
|
super().__init__(timeout=None)
|
||||||
|
|
||||||
self.player: voicelink.Player = player
|
self.player: voicelink.Player = player
|
||||||
@@ -485,4 +516,7 @@ class InteractiveController(discord.ui.View):
|
|||||||
sec = int(error.retry_after)
|
sec = int(error.retry_after)
|
||||||
return await interaction.response.send_message(f"You're on cooldown for {sec} second{'' if sec == 1 else 's'}!", ephemeral=True)
|
return await interaction.response.send_message(f"You're on cooldown for {sec} second{'' if sec == 1 else 's'}!", ephemeral=True)
|
||||||
|
|
||||||
super().on_error(interaction, error, item)
|
if isinstance(error, voicelink.VoicelinkException):
|
||||||
|
return await interaction.response.send_message(error, ephemeral=True)
|
||||||
|
|
||||||
|
await super().on_error(interaction, error, item)
|
||||||
Reference in New Issue
Block a user