From 47ec3d212a71b978cff3fd41f8b96e47cbf4e9e6 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Wed, 15 Apr 2026 11:57:08 +0800 Subject: [PATCH] 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. --- cogs/basic.py | 2 +- update.py | 2 +- voicelink/views/controller.py | 82 +++++++++++++++++++++++++---------- 3 files changed, 60 insertions(+), 26 deletions(-) diff --git a/cogs/basic.py b/cogs/basic.py index dc98a71..2984f10 100644 --- a/cogs/basic.py +++ b/cogs/basic.py @@ -790,7 +790,7 @@ class Basic(commands.Cog): @commands.hybrid_command(name="lyrics", aliases=get_aliases("lyrics")) @app_commands.describe(title="Searches for your query and displays the reutned lyrics.") @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." if not title: player: voicelink.Player = ctx.guild.voice_client diff --git a/update.py b/update.py index 424bf56..d14586f 100644 --- a/update.py +++ b/update.py @@ -31,7 +31,7 @@ import subprocess from io import BytesIO ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) -__version__ = "v2.7.3b2" +__version__ = "v2.7.3b3" # URLs for update and migration PYTHON_CMD_NAME = os.path.basename(sys.executable) diff --git a/voicelink/views/controller.py b/voicelink/views/controller.py index 7ac2189..b15685c 100644 --- a/voicelink/views/controller.py +++ b/voicelink/views/controller.py @@ -29,7 +29,7 @@ from discord.ext import commands from typing import Optional, Dict, Type, Union, Any 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 ..mongodb import MongoDBHandler @@ -87,6 +87,34 @@ class ControlButton(discord.ui.Button): 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): def __init__(self, **kwargs): super().__init__( @@ -319,7 +347,7 @@ class Forward(ControlButton): position = int(self.player.position + 10000) 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): def __init__(self, **kwargs): @@ -338,7 +366,7 @@ class Rewind(ControlButton): position = 0 if (value := int(self.player.position - 30000)) <= 0 else value 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): def __init__(self, **kwargs): @@ -352,6 +380,8 @@ class Lyrics(ControlButton): if not self.player or not self.player.is_playing: return await self.send(interaction, "player.errors.noTrackPlaying", ephemeral=True) + await interaction.response.defer() + title = self.player.current.title artist = self.player.current.author @@ -362,23 +392,22 @@ class Lyrics(ControlButton): 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.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): - self.player: voicelink.Player = player - if player.queue.is_empty: raise ValueError("Player queue is empty, cannot create Tracks row instance.") 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): 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)) super().__init__( - placeholder=self.player._ph.replace(btn_data.get("label"), {}), + placeholder=player._ph.replace(btn_data.get("label"), {}), + player=player, options=options, disabled=player.queue.is_empty, **kwargs @@ -386,47 +415,49 @@ class Tracks(discord.ui.Select): async def callback(self, interaction: discord.Interaction): 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])) await self.player.stop() 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): - - self.player: voicelink.Player = player - options = [discord.SelectOption(label="None", value="None")] for name in voicelink.Filters.get_available_filters(): options.append(discord.SelectOption(label=name.capitalize(), value=name)) super().__init__( - placeholder=self.player._ph.replace(btn_data.get("label"), {}), + placeholder=player._ph.replace(btn_data.get("label"), {}), + player=player, options=options, row=row ) async def callback(self, interaction: discord.Interaction): 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() if self.values[0] == "None": 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): 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: 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, "play-pause": PlayPause, "skip": Skip, @@ -446,7 +477,7 @@ BUTTON_TYPE: Dict[str, Type[Union[ControlButton, discord.ui.Select]]] = { } class InteractiveController(discord.ui.View): - def __init__(self, player): + def __init__(self, player: "voicelink.Player"): super().__init__(timeout=None) self.player: voicelink.Player = player @@ -485,4 +516,7 @@ class InteractiveController(discord.ui.View): 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) - super().on_error(interaction, error, item) \ No newline at end of file + if isinstance(error, voicelink.VoicelinkException): + return await interaction.response.send_message(error, ephemeral=True) + + await super().on_error(interaction, error, item) \ No newline at end of file