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:
Choco
2026-04-15 11:57:08 +08:00
parent 629eb041f6
commit 47ec3d212a
3 changed files with 60 additions and 26 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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)