From 1633f14b64665a7c0cc77dfdf11104876311f101 Mon Sep 17 00:00:00 2001 From: Choco Date: Fri, 19 Sep 2025 23:29:42 +0800 Subject: [PATCH] Refactor playlist view and update language files Refactored playlist viewing logic for improved modularity and maintainability, introducing PlaylistViewManager and DynamicViewManager. Updated language files to standardize playlist view titles and descriptions, and removed unnecessary emojis. Updated requirements.txt to discord.py 2.6.3. Enhanced send function to support file attachments. --- cogs/playlist.py | 230 ++++++++++++------- function.py | 2 + langs/CH.json | 11 +- langs/DE.json | 7 +- langs/EN.json | 8 +- langs/ES.json | 7 +- langs/FR.json | 7 +- langs/JA.json | 7 +- langs/KO.json | 7 +- langs/PL.json | 7 +- langs/RU.json | 7 +- langs/UA.json | 7 +- langs/VN.json | 7 +- requirements.txt | 2 +- views/__init__.py | 2 +- views/playlist.py | 332 ++++++++++++++++++++-------- views/queue.py | 2 +- views/utils/__init__.py | 3 +- views/utils/dynamic_view_manager.py | 89 ++++++++ views/utils/pagination.py | 39 +++- 20 files changed, 566 insertions(+), 217 deletions(-) create mode 100644 views/utils/dynamic_view_manager.py diff --git a/cogs/playlist.py b/cogs/playlist.py index a7f3f6b..5a8fe17 100644 --- a/cogs/playlist.py +++ b/cogs/playlist.py @@ -32,60 +32,163 @@ from function import ( get_user, update_user, check_roles, - get_lang, - settings, get_aliases, cooldown_check, logger ) -from views import PlaylistView, InboxView, HelpView +from views import PlaylistViewManager, InboxView, HelpView def assign_playlist_id(existed: list) -> str: for i in range(200, 210): if str(i) not in existed: return str(i) -async def check_playlist_perms(user_id: int, author_id: int, d_id: str) -> dict: - playlist = await get_user(author_id, 'playlist') - playlist = playlist.get(d_id) +async def check_playlist_perms(user_id: int, author_id: int, playlist_id: str) -> dict: + """Check if user has read permissions for a specific playlist.""" + user_data = await get_user(author_id, 'playlist') + playlist = user_data.get(playlist_id) + if not playlist or user_id not in playlist['perms']['read']: return {} + return playlist async def check_playlist(ctx: commands.Context, name: str = None, full: bool = False, share: bool = True) -> dict: - user = await get_user(ctx.author.id, 'playlist') + """Get user's playlist data with various filtering options.""" + user_playlists = await get_user(ctx.author.id, 'playlist') - await ctx.defer() + if not ctx.interaction.response.is_done(): + await ctx.defer() + if full: - return user + return user_playlists if not name: - return {'playlist': user['200'], 'position': 1, 'id': "200"} - - for index, data in enumerate(user, start=1): - playlist = user[data] - if playlist['name'].lower() == name: + return {'playlist': user_playlists['200'], 'position': 1, 'id': "200"} + + for index, playlist_id in enumerate(user_playlists, start=1): + playlist = user_playlists[playlist_id] + + if playlist['name'].lower() == name.lower(): if playlist['type'] == 'share' and share: - playlist = await check_playlist_perms(ctx.author.id, playlist['user'], playlist['referId']) - if not playlist or ctx.author.id not in playlist['perms']['read']: - return {'playlist': None, 'position': index, 'id': data} - return {'playlist': playlist, 'position': index, 'id': data} + shared_playlist = await check_playlist_perms(ctx.author.id, playlist['user'], playlist['referId']) + + if not shared_playlist or ctx.author.id not in shared_playlist['perms']['read']: + return {'playlist': None, 'position': index, 'id': playlist_id} + + return {'playlist': shared_playlist, 'position': index, 'id': playlist_id} + + return {'playlist': playlist, 'position': index, 'id': playlist_id} + return {'playlist': None, 'position': None, 'id': None} async def search_playlist(url: str, requester: discord.Member, time_needed: bool = True) -> dict: + """Search for playlist tracks from a URL.""" try: tracks = await voicelink.NodePool.get_node().get_tracks(url, requester=requester) - tracks = {"name": tracks.name, "tracks": tracks.tracks} + result = {"name": tracks.name, "tracks": tracks.tracks} + if time_needed: - time = sum([track.length for track in tracks["tracks"]]) - except: + result["time"] = ctime(sum(track.length for track in tracks.tracks)) + + return result + except Exception: return {} - - if time_needed: - tracks["time"] = ctime(time) - return tracks +async def _process_playlist(ctx: commands.Context, playlist_data: dict, playlist_id: str, is_locked: bool): + """Process a single playlist and return its formatted data.""" + playlist_type = playlist_data['type'] + + # Get appropriate emoji + if is_locked: + emoji = '🔒' + elif playlist_type == 'link': + emoji = '🌐' + elif playlist_type == 'share': + emoji = '🤝' + else: + emoji = '❤️' + + # Handle link playlist + if playlist_type == 'link': + tracks = await search_playlist(playlist_data['uri'], requester=ctx.author) + if not tracks: + return None + + return { + 'emoji': emoji, + 'id': playlist_id, + 'time': tracks['time'], + 'name': playlist_data['name'], + 'tracks': tracks['tracks'], + 'perms': playlist_data['perms'], + 'type': playlist_data['type'] + } + + # Handle shared playlist + if playlist_type == 'share': + shared_playlist = await check_playlist_perms( + ctx.author.id, + playlist_data['user'], + playlist_data['referId'] + ) + + if not shared_playlist: + await update_user(ctx.author.id, {"$unset": {f"playlist.{playlist_id}": 1}}) + return None + + if shared_playlist['type'] == 'link': + tracks = await search_playlist(shared_playlist['uri'], requester=ctx.author) + if not tracks: + return None + + return { + 'emoji': emoji, + 'id': playlist_id, + 'time': tracks['time'], + 'name': playlist_data['name'], + 'tracks': tracks['tracks'], + 'perms': shared_playlist['perms'], + 'owner': playlist_data['user'], + 'type': 'share' + } + + decoded_tracks = [] + total_time = 0 + for track in shared_playlist['tracks']: + decoded_track = voicelink.decode(track) + total_time += decoded_track.get("length", 0) + decoded_tracks.append(decoded_track) + + return { + 'emoji': emoji, + 'id': playlist_id, + 'time': ctime(total_time), + 'name': playlist_data['name'], + 'tracks': decoded_tracks, + 'perms': shared_playlist['perms'], + 'owner': playlist_data['user'], + 'type': 'share' + } + + decoded_tracks = [] + total_time = 0 + for track in playlist_data['tracks']: + decoded_track = voicelink.decode(track) + total_time += decoded_track.get("length", 0) + decoded_tracks.append(decoded_track) + + return { + 'emoji': emoji, + 'id': playlist_id, + 'time': ctime(total_time), + 'name': playlist_data['name'], + 'tracks': decoded_tracks, + 'perms': playlist_data['perms'], + 'owner': playlist_data.get('owner', ctx.author.id), + 'type': playlist_data['type'] + } class Playlists(commands.Cog, name="playlist"): def __init__(self, bot: commands.Bot) -> None: @@ -156,63 +259,32 @@ class Playlists(commands.Cog, name="playlist"): @playlist.command(name="view", aliases=get_aliases("view")) @commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild) async def view(self, ctx: commands.Context) -> None: - "List all your playlist and all songs in your favourite playlist." - user = await check_playlist(ctx, full=True) - rank, max_p, max_t = check_roles() - - results = [] - for index, data in enumerate(user, start=1): - playlist = user[data] - time = 0 - try: - if playlist['type'] == 'link': - tracks = await search_playlist(playlist['uri'], requester=ctx.author) - results.append({'emoji': ('🔒' if max_p < index else '🔗'), 'id': data, 'time': tracks['time'], 'name': playlist['name'], 'tracks': tracks['tracks'], 'perms': playlist['perms'], 'type': playlist['type']}) - - else: - if share := playlist['type'] == 'share': - playlist = await check_playlist_perms(ctx.author.id, playlist['user'], playlist['referId']) - if not playlist: - await update_user(ctx.author.id, {"$unset": {f"playlist.{data}": 1}}) - continue - - if playlist['type'] == 'link': - tracks = await search_playlist(playlist['uri'], requester=ctx.author) - results.append({'emoji': ('🔒' if max_p < index else '🤝'), 'id': data, 'time': tracks['time'], 'name': user[data]['name'], 'tracks': tracks['tracks'], 'perms': playlist['perms'], 'owner': user[data]['user'], 'type': 'share'}) - continue - - init = [] - for track in playlist['tracks']: - dt = voicelink.decode(track) - time += dt.get("length", 0) - init.append(dt) - playlist['tracks'] = init - results.append({'emoji': ('🔒' if max_p < index else ('🤝' if share else '❤️')), 'id': data, 'time': ctime(time), 'name': user[data]['name'], 'tracks': playlist['tracks'], 'perms': playlist['perms'], 'owner': user[data].get('user', None), 'type': user[data]['type']}) + """List all your playlists and all songs in your favourite playlist.""" + user_playlists = await check_playlist(ctx, full=True) + _, max_playlists, _ = check_roles() - except: - results.append({'emoji': '⛔', 'id': data, 'time': '--:--', 'name': 'Error', 'tracks': [], 'type': 'error'}) - - text = await get_lang(ctx.guild.id, "playlistViewTitle", "playlistViewHeaders", "playlistFooter") - embed = discord.Embed( - title=text[0].format(ctx.author.display_name), - description='```prolog\n %4s %10s %12s %10s\n' % tuple(text[1].split(",")), - color=settings.embed_color - ) + playlist_results = [] - for index in range(max_p): - try: - info = results[index] - track_info = (info['emoji'], info['id'], f"[{info['time']}]", info['name'], f"{len(info['tracks'])}") - except IndexError: - track_info = ("🎵", "-"*3, "[--:--]", "-"*6, f"-") - - embed.description += '%0s %3s. %10s %12s %10s\n' % track_info + for index, playlist_id in enumerate(user_playlists, start=1): + playlist_data = user_playlists[playlist_id] + is_locked = max_playlists < index - embed.description += "```" - embed.set_footer(text=text[2]) - - view = PlaylistView(embed, results, ctx.author) - view.response = await send(ctx, embed, view=view, ephemeral=True) + try: + result = await _process_playlist(ctx, playlist_data, playlist_id, is_locked) + if result: + playlist_results.append(result) + except Exception: + playlist_results.append({ + 'emoji': '⛔', + 'id': playlist_id, + 'time': '--:--', + 'name': 'Error', + 'tracks': [], + 'type': 'error' + }) + + view = PlaylistViewManager(ctx, playlist_results) + view.response = await send(ctx, content=await view.build_embed(), view=view, ephemeral=True) @playlist.command(name="create", aliases=get_aliases("create")) @app_commands.describe( diff --git a/function.py b/function.py index b8a1b5a..980b92a 100644 --- a/function.py +++ b/function.py @@ -191,6 +191,7 @@ async def send( content: Union[str, discord.Embed] = None, *params, view: discord.ui.View = None, + file: discord.File = None, delete_after: float = None, ephemeral: bool = False, requires_fetch: bool = False @@ -223,6 +224,7 @@ async def send( send_kwargs = { "content": text, "embed": embed, + "file": file, "allowed_mentions": ALLOWED_MENTIONS, "silent": settings.get("silent_msg", False), } diff --git a/langs/CH.json b/langs/CH.json index 29b890a..160bc34 100644 --- a/langs/CH.json +++ b/langs/CH.json @@ -38,7 +38,7 @@ "addEffect": "套用音效`{0}`濾鏡。", "clearEffect": "聲音效果已清除!", "filterTagAlreadyInUse": "此聲音效果已在使用中!請使用 /cleareffect 移除它。", - "playlistViewTitle": "📜 所有 {0} 的播放清單", + "playlistViewTitle": "所有 {0} 的播放清單", "playlistViewHeaders": "ID:,時間:,名稱:,曲目數:", "playlistFooter": "輸入 /playlist play [播放清單] 加入此播放清單至隊列中。", "playlistNotFound": "找不到播放清單 [`{0}`]。輸入 /playlist view 查看所有播放清單。", @@ -68,11 +68,11 @@ "playlistRemoved": "👋 已從 {1} 的播放清單 [`{2}`] 中刪除 **{0}**。", "playlistClear": "你已成功清除播放清單 [`{0}`]。", "playlistView": "播放清單檢視器", - "playlistViewDesc": "```名稱 | ID: {0} | {1}\n總曲目數: {2}\n擁有者: {3}\n類型: {4}\n```", - "playlistViewPermsValue": "📖 讀取: ✓ ✍🏽 編輯: {0} 🗑️ 刪除: {1}", - "playlistViewPermsValue2": "📖 讀取: {0}", + "playlistViewDesc": "```名稱: {0} [{1}]\n總曲目數: {2}\n擁有者: {3}\n類型: {4}\n```", + "playlistViewPermsValue": "📖 讀取: ✓ ✍🏽 編輯: {0} 🗑️ 刪除: {1}", + "playlistViewPermsValue2": "📖 讀取: {0}", "playlistViewTrack": "音軌", - "playlistViewPage": "頁面: {0}/{1} | 總長度: {2}", + "playlistViewFooter": "總長度: {0}", "inboxFull": "抱歉!{0} 的收件匣已滿。", "inboxNoMsg": "您的收件匣中沒有任何訊息。", "invitationSent": "已發送邀請給 {0}。", @@ -129,7 +129,6 @@ "historyTitle": "歷史隊列:", "viewTitle": "音樂隊列", "viewDesc": "**現正播放:[點擊我]({0}) ⮯**\n{1}", - "viewFooter": "頁數:{0}/{1} | 總長度:{2}", "pauseError": "播放器已經暫停。", "pauseVote": "{0} 已投票暫停歌曲。[{1}/{2}]", "paused": "播放器已被 `{0}` 暫停。", diff --git a/langs/DE.json b/langs/DE.json index f33ab0c..a0bf86a 100644 --- a/langs/DE.json +++ b/langs/DE.json @@ -38,7 +38,7 @@ "addEffect": "Wende den Effekt `{0}` Filter an.", "clearEffect": "Die Soundeffekte wurden gelöscht!", "filterTagAlreadyInUse": "Diese Soundeffekte sind bereits im Einsatz! Bitte verwende /cleareffect , um sie zu entfernen.", - "playlistViewTitle": "📜 Alle Playlists von {0}", + "playlistViewTitle": "Alle Playlists von {0}", "playlistViewHeaders": "ID:,Zeit:,Name:,Tracks:", "playlistFooter": "Gebe /playlist play [playlist] ein, um die Playlist in die Warteschlange einzufügen.", "playlistNotFound": "Die Wiedergabeliste [`{0}`] wurde nicht gefunden. Gebe /playlist view ein, um Deine gesamte Wiedergabeliste anzuzeigen.", @@ -68,11 +68,11 @@ "playlistRemoved": "👋 {0} aus {1}s Playlist [{2}] entfernt.", "playlistClear": "Du hast Deine Wiedergabenliste [`{0}`] erfolgreich gelöscht.", "playlistView": "Playlist-Viewer", - "playlistViewDesc": "```Name | ID: {0} | {1}\nTitel insgesamt: {2}\nBesitzer: {3}\nTyp: {4}\n```", + "playlistViewDesc": "```Name: {0} [{1}]\nTitel insgesamt: {2}\nBesitzer: {3}\nTyp: {4}\n```", "playlistViewPermsValue": "📖 Lesen: ✓ ✍🏽 Schreiben: {0} 🗑️ Entfernen: {1}", "playlistViewPermsValue2": "📖 Lesen: {0}", "playlistViewTrack": "Tracks", - "playlistViewPage": "Seite: {0}/{1} | Gesamtdauer: {2}", + "playlistViewFooter": "Gesamtdauer: {0}", "inboxFull": "Es tut mir leid! Der Posteingang von {0} ist voll.", "inboxNoMsg": "Es sind keine Nachrichten in Deinem Posteingang.", "invitationSent": "Einladung an {0} gesendet.", @@ -129,7 +129,6 @@ "historyTitle": "Vorherige Warteschlange:", "viewTitle": "Musik-Warteschlange", "viewDesc": "**Jetzt wird abgespielt: [Hier klicken]({0}) ⮯**\n{1}", - "viewFooter": "Seite: {0}/{1} | Gesamtdauer: {2}", "pauseError": "Der Player ist bereits pausiert.", "pauseVote": "{0} hat für eine Pause des Songs abgestimmt. [{1}/{2}]", "paused": "`{0}` hat den Player pausiert.", diff --git a/langs/EN.json b/langs/EN.json index 378a207..a05f7cb 100644 --- a/langs/EN.json +++ b/langs/EN.json @@ -38,9 +38,10 @@ "addEffect": "Applied the `{0}` effect.", "clearEffect": "The sound effects have been cleared!", "filterTagAlreadyInUse": "This sound effect is already in use! Please use /cleareffect to remove it.", - "playlistViewTitle": "📜 All of {0}'s Playlists", + "playlistViewTitle": "All of {0}'s Playlists", "playlistViewHeaders": "ID:,Time:,Name:,Tracks:", "playlistFooter": "Type /playlist play [playlist] to add a playlist the into the queue.", + "emptyPlaylistMessage": "🎶 No playlists found. Create your first one with `/playlist create`!", "playlistNotFound": "Playlist [`{0}`] not found. Type /playlist view to view all your playlist.", "playlistNotAccess": "Sorry! You are not allowed to access this playlist!", "playlistNoTrack": "Sorry! There are no tracks in the playlist [`{0}`].", @@ -68,11 +69,11 @@ "playlistRemoved": "👋 Removed **{0}** from {1}'s playlist [`{2}`].", "playlistClear": "You have successfully clear your playlist [`{0}`].", "playlistView": "Playlist Viewer", - "playlistViewDesc": "```Name | ID: {0} | {1}\nTotal Tracks: {2}\nOwner: {3}\nType: {4}\n```", + "playlistViewDesc": "```Name: {0} [{1}]\nTotal Tracks: {2}\nOwner: {3}\nType: {4}\n```", "playlistViewPermsValue": "📖 Read: ✓ ✍🏽 Write: {0} 🗑️ Remove: {1}", "playlistViewPermsValue2": "📖 Read: {0}", "playlistViewTrack": "Tracks", - "playlistViewPage": "Page: {0}/{1} | Total Duration: {2}", + "playlistViewFooter": "Total Duration: {0}", "inboxFull": "Sorry! {0}'s inbox is full.", "inboxNoMsg": "There are no messages in your inbox.", "invitationSent": "Invitation sent to {0}.", @@ -129,7 +130,6 @@ "historyTitle": "History Queue:", "viewTitle": "Music Queue", "viewDesc": "**Now Playing: [Click Me]({0}) ⮯**\n{1}", - "viewFooter": "Page: {0}/{1} | Total Duration: {2}", "pauseError": "The player is already paused.", "pauseVote": "{0} has voted to pause the song. [{1}/{2}]", "paused": "`{0}` has paused the player.", diff --git a/langs/ES.json b/langs/ES.json index b2ec7bb..1b5c11d 100644 --- a/langs/ES.json +++ b/langs/ES.json @@ -38,7 +38,7 @@ "addEffect": "plica el efecto `{0}` filtro.", "clearEffect": "¡Los efectos de sonido se han borrado!", "filterTagAlreadyInUse": "¡Este efecto de sonido ya está en uso! Utilice /cleareffect para eliminarlo.", - "playlistViewTitle": "📜 Todas las listas de reproducción de {0}", + "playlistViewTitle": "Todas las listas de reproducción de {0}", "playlistViewHeaders": "ID:,Tiempo:,Nombre:,Pistas:", "playlistFooter": "Escriba /playlist play [playlist] para agregar la lista de reproducción a la cola.", "playlistNotFound": "Lista de reproducción [`{0}`] no encontrada. Escriba /playlist view para ver todas sus listas de reproducción.", @@ -68,11 +68,11 @@ "playlistRemoved": "👋 Se quitó {0} de la lista de reproducción de {1} [`{2}`].", "playlistClear": "Ha borrado con éxito su lista de reproducción [`{0}`].", "playlistView": "Visor de lista de reproducción", - "playlistViewDesc": "```Nombre | ID: {0} | {1}\nTotal de pistas: {2}\nPropietario: {3}\nTipo: {4}\n```", + "playlistViewDesc": "```Nombre: {0} [{1}]\nTotal de pistas: {2}\nPropietario: {3}\nTipo: {4}\n```", "playlistViewPermsValue": "📖 Leer: ✓ ✍🏽 Escribir: {0} 🗑️ Eliminar: {1}", "playlistViewPermsValue2": "📖 Leer: {0}", "playlistViewTrack": "Pistas", - "playlistViewPage": "Página: {0}/{1} | Duración total: {2}", + "playlistViewFooter": "Duración total: {0}", "inboxFull": "¡Lo siento! La bandeja de entrada de {0} está llena.", "inboxNoMsg": "No hay mensajes en su bandeja de entrada.", "invitationSent": "Se ha enviado una invitación a {0}.", @@ -129,7 +129,6 @@ "historyTitle": "Cola de historial:", "viewTitle": "Cola de música", "viewDesc": "**Reproduciendo ahora: [Haga clic aquí]({0}) ⮯**\n{1}", - "viewFooter": "Página: {0}/{1} | Duración total: {2}", "pauseError": "El reproductor ya está en pausa.", "pauseVote": "{0} ha votado para pausar la canción. [{1}/{2}]", "paused": "`{0}` ha pausado el reproductor.", diff --git a/langs/FR.json b/langs/FR.json index fb1df9e..663f967 100644 --- a/langs/FR.json +++ b/langs/FR.json @@ -38,7 +38,7 @@ "addEffect": "Effet `{0}` appliqué.", "clearEffect": "Les effets sonores ont été supprimés !", "filterTagAlreadyInUse": "Cet effet sonore est déjà utilisé ! Veuillez utiliser /cleareffect pour le retirer.", - "playlistViewTitle": "📜 Toutes les playlists de {0}", + "playlistViewTitle": "Toutes les playlists de {0}", "playlistViewHeaders": "ID:,Durée:,Nom:,Morceaux:", "playlistFooter": "Tapez /playlist play [playlist] pour ajouter une playlist à la file.", "playlistNotFound": "Playlist [`{0}`] introuvable. Tapez /playlist view pour voir toutes vos playlists.", @@ -68,11 +68,11 @@ "playlistRemoved": "👋 **{0}** a été retiré de la playlist [`{2}`] de {1}.", "playlistClear": "Vous avez vidé avec succès votre playlist [`{0}`].", "playlistView": "Visualiseur de playlist", - "playlistViewDesc": "```Nom | ID : {0} | {1}\nTotal Morceaux : {2}\nPropriétaire : {3}\nType : {4}\n```", + "playlistViewDesc": "```Nom: {0} [{1}]\nTotal Morceaux : {2}\nPropriétaire : {3}\nType : {4}\n```", "playlistViewPermsValue": "📖 Lecture : ✓ ✍🏽 Écriture : {0} 🗑️ Suppression : {1}", "playlistViewPermsValue2": "📖 Lecture : {0}", "playlistViewTrack": "Morceaux", - "playlistViewPage": "Page : {0}/{1} | Durée totale : {2}", + "playlistViewFooter": "Durée totale : {0}", "inboxFull": "Désolé ! La boîte de réception de {0} est pleine.", "inboxNoMsg": "Aucun message dans votre boîte de réception.", "invitationSent": "Invitation envoyée à {0}.", @@ -129,7 +129,6 @@ "historyTitle": "Historique :", "viewTitle": "File de musiques", "viewDesc": "**Lecture en cours : [Cliquez ici]({0}) ⮯**\n{1}", - "viewFooter": "Page : {0}/{1} | Durée totale : {2}", "pauseError": "Le lecteur est déjà en pause.", "pauseVote": "{0} a voté pour mettre la chanson en pause. [{1}/{2}]", "paused": "`{0}` a mis le lecteur en pause.", diff --git a/langs/JA.json b/langs/JA.json index 88660fe..0df1caa 100644 --- a/langs/JA.json +++ b/langs/JA.json @@ -38,7 +38,7 @@ "addEffect": "`{0}` フィルターを適用します。", "clearEffect": "効果音がクリアされました!", "filterTagAlreadyInUse": "このサウンドエフェクトはすでに使用されています!削除するには/cleareffect を使用してください。", - "playlistViewTitle": "📜 {0}のすべてのプレイリスト", + "playlistViewTitle": "{0}のすべてのプレイリスト", "playlistViewHeaders": "ID:,時間:,名前:,トラック:", "playlistFooter": "プレイリストをキューに追加するには、/playlist play [playlist]を入力してください。", "playlistNotFound": "プレイリスト[{0}]が見つかりません。すべてのプレイリストを表示するには、/playlist viewを入力してください。", @@ -68,11 +68,11 @@ "playlistRemoved": "👋 {1}さんのプレイリスト[{2}]から**{0}**を削除しました。", "playlistClear": "プレイリスト[{0}]を正常にクリアしました。", "playlistView": "プレイリストビューアー", - "playlistViewDesc": "```名前 | ID: {0} | {1}\nトータルトラック: {2}\nオーナー: {3}\nタイプ: {4}\n```", + "playlistViewDesc": "```名前: {0} [{1}]\nトータルトラック: {2}\nオーナー: {3}\nタイプ: {4}\n```", "playlistViewPermsValue": "📖 読み込み: ✓ ✍🏽 書き込み: {0} 🗑️ 削除: {1}", "playlistViewPermsValue2": "📖 読み込み: {0}", "playlistViewTrack": "トラック", - "playlistViewPage": "ページ: {0}/{1} | トータルダレーション: {2}", + "playlistViewFooter": "トータルダレーション: {0}", "inboxFull": "申し訳ありません!{0}さんの受信トレイはいっぱいです。", "inboxNoMsg": "受信トレイにメッセージはありません。", "invitationSent": "{0}さんに招待状を送信しました。", @@ -129,7 +129,6 @@ "historyTitle": "再生履歴:", "viewTitle": "音楽キュー", "viewDesc": "**現在再生中:[ここをクリックして聴く]({0}) ⮯**\n{1}", - "viewFooter": "ページ:{0}/{1} | 合計再生時間:{2}", "pauseError": "プレイヤーはすでに一時停止しています。", "pauseVote": "{0}が曲を一時停止することに賛成しました。[{1}/{2}]", "paused": "{0}がプレイヤーを一時停止しました。", diff --git a/langs/KO.json b/langs/KO.json index fe49dfc..6223ef1 100644 --- a/langs/KO.json +++ b/langs/KO.json @@ -38,7 +38,7 @@ "addEffect": "`{0}` 필터를 적용하세요.", "clearEffect": "효과가 삭제되었습니다!", "filterTagAlreadyInUse": "이 필터는 이미 사용 중입니다! 삭제하려면 /cleareffect 를 사용하십시오.", - "playlistViewTitle": "📜 {0}의 모든 재생 목록", + "playlistViewTitle": "{0}의 모든 재생 목록", "playlistViewHeaders": "ID:,시간:,이름:,트랙:", "playlistFooter": "/playlist play [재생 목록]을 입력하여 재생 목록을 대기열에 추가하세요.", "playlistNotFound": "재생 목록 [{0}]을(를) 찾을 수 없습니다. 모든 재생 목록을 보려면 /playlist view를 입력하세요.", @@ -68,11 +68,11 @@ "playlistRemoved": "👋 {1}님의 재생 목록 [{2}]에서 **{0}**을(를) 제거했습니다.", "playlistClear": "재생 목록 [{0}]을(를) 성공적으로 지웠습니다.", "playlistView": "재생 목록 뷰어", - "playlistViewDesc": "```이름 | ID: {0} | {1}\n총 트랙: {2}\n소유자: {3}\n유형: {4}\n```", + "playlistViewDesc": "```이름: {0} [{1}]\n총 트랙: {2}\n소유자: {3}\n유형: {4}\n```", "playlistViewPermsValue": "📖 읽기: ✓ ✍🏽 쓰기: {0} 🗑️ 삭제: {1}", "playlistViewPermsValue2": "📖 읽기: {0}", "playlistViewTrack": "트랙", - "playlistViewPage": "페이지: {0}/{1} | 총 길이: {2}", + "playlistViewFooter": "총 길이: {0}", "inboxFull": "죄송합니다! {0}님의 받은 편지함이 가득 찼습니다.", "inboxNoMsg": "받은 편지함에 메시지가 없습니다.", "invitationSent": "{0}님에게 초대장을 보냈습니다.", @@ -129,7 +129,6 @@ "historyTitle": "이전 곡 대기열:", "viewTitle": "음악 대기열", "viewDesc": "**현재 재생중: [여기를 클릭하여 듣기]({0}) ⮯**\n{1}", - "viewFooter": "페이지: {0}/{1} | 총 재생 시간: {2}", "pauseError": "플레이어가 이미 일시정지되었습니다.", "pauseVote": "{0}님이 노래 일시정지 투표를 했습니다. [{1}/{2}]", "paused": "{0}님이 플레이어를 일시정지했습니다.", diff --git a/langs/PL.json b/langs/PL.json index 1e0ff06..68c0bfe 100644 --- a/langs/PL.json +++ b/langs/PL.json @@ -38,7 +38,7 @@ "addEffect": "Nałożono efekt: `{0}`", "clearEffect": "Efekty dźwiękowe zostały wyczyszczone.", "filterTagAlreadyInUse": "Ten efekt dźwiękowy jest już w używany! Użyj /cleareffect aby go wyłączyć.", - "playlistViewTitle": "📜 Playlisty użytkownika {0}:", + "playlistViewTitle": "Playlisty użytkownika {0}:", "playlistViewHeaders": "ID:,Czas:,Nazwa:,Ilość pozycji:", "playlistFooter": "Użyj /playlist play [nazwa_playlisty] by dodać playlistę do kolejki.", "playlistNotFound": "Playlista [`{0}`] nie została znaleziona. Użyj /playlist view by zobaczyć wszystkie swoje playlisty.", @@ -68,11 +68,11 @@ "playlistRemoved": "👋 Usunięto **{0}** z playlisty [`{2}`] użytkownika {1}.", "playlistClear": "Pomyślnie wyczyściłeś swoja playlistę: [`{0}`].", "playlistView": "Szczegóły playlisty", - "playlistViewDesc": "```Nazwa | ID: {0} | {1}\nLiczba utworów: {2}\nWłaściciel: {3}\nTyp: {4}\n```", + "playlistViewDesc": "```Nazwa: {0} [{1}]\nLiczba utworów: {2}\nWłaściciel: {3}\nTyp: {4}\n```", "playlistViewPermsValue": "📖 Odczyt: ✓ ✍🏽 Zapis: {0} 🗑️ Usuwanie: {1}", "playlistViewPermsValue2": "📖 Odczyt: {0}", "playlistViewTrack": "Utwory", - "playlistViewPage": "Strona: {0}/{1} | Całkowity czas trwania: {2}", + "playlistViewFooter": "Całkowity czas trwania: {0}", "inboxFull": "Błąd! Skrzynka odbiorcza {0} jest pełna.", "inboxNoMsg": "W twojej skrzynce odbiorczej nie ma wiadomości.", "invitationSent": "Zaproszenie wysłane do {0}.", @@ -129,7 +129,6 @@ "historyTitle": "Historia kolejki:", "viewTitle": "Kolejka utworów", "viewDesc": "**Aktualnie odtwarzane: [Kliknij mnie]({0}) ⮯**\n{1}", - "viewFooter": "Strona: {0}/{1} | Całkowity czas trwania: {2}", "pauseError": "Odtwarzacz jest już zapauzowany.", "pauseVote": "{0} zagłosował nad zapauzowaniem odtwarzania. [{1}/{2}]", "paused": "`{0}` zapauzował odtwarznaie.", diff --git a/langs/RU.json b/langs/RU.json index 2eb340b..5b9a21b 100644 --- a/langs/RU.json +++ b/langs/RU.json @@ -38,7 +38,7 @@ "addEffect": "Применен `{0}` эффект.", "clearEffect": "Звуковые эффекты были очищены!", "filterTagAlreadyInUse": "Этот звуковой эффект уже используется! Пожалуйста, используйте /cleareffect <Тег>, чтобы удалить его.", - "playlistViewTitle": "📜 Все плейлисты пользователя {0}", + "playlistViewTitle": "Все плейлисты пользователя {0}", "playlistViewHeaders": "ID:,Время:,Название:,Треки:", "playlistFooter": "Введите /playlist play [плейлист], чтобы добавить плейлист в очередь.", "playlistNotFound": "Плейлист [`{0}`] не найден. Введите /playlist view, чтобы посмотреть все ваши плейлисты.", @@ -68,11 +68,11 @@ "playlistRemoved": "👋 Удален **{0}** из плейлиста {1} [`{2}`].", "playlistClear": "Вы успешно очистили свой плейлист [`{0}`].", "playlistView": "Просмотр плейлистов", - "playlistViewDesc": "```Имя | ID: {0} | {1}\nВсего треков: {2}\nВладелец: {3}\nТип: {4}\n```", + "playlistViewDesc": "```Имя: {0} [{1}]\nВсего треков: {2}\nВладелец: {3}\nТип: {4}\n```", "playlistViewPermsValue": "📖 Чтение: ✓ ✍🏽 Запись: {0} 🗑️ Удаление: {1}", "playlistViewPermsValue2": "📖 Чтение: {0}", "playlistViewTrack": "Треки", - "playlistViewPage": "Страница: {0}/{1} | Общая продолжительность: {2}", + "playlistViewFooter": "Общая продолжительность: {0}", "inboxFull": "Извините! Почтовый ящик {0} переполнен.", "inboxNoMsg": "В вашем почтовом ящике нет сообщений.", "invitationSent": "Приглашение отправлено {0}.", @@ -129,7 +129,6 @@ "historyTitle": "История очереди:", "viewTitle": "Текущая очередь", "viewDesc": "**Сейчас играет: [*ссылка*]({0}) ⮯**\n{1}", - "viewFooter": "Страница: {0}/{1} | Общая продолжительность: {2}", "pauseError": "Плеер уже на паузе.", "pauseVote": "Пользователь `{0}` проголосовал за паузу плеера. [{1}/{2}]", "paused": "Пользователь `{0}` поставил плеер на паузу.", diff --git a/langs/UA.json b/langs/UA.json index cecbf4b..db0641a 100644 --- a/langs/UA.json +++ b/langs/UA.json @@ -38,7 +38,7 @@ "addEffect": "Застосуйте ефект `{0}` фільтр.", "clearEffect": "Звукові ефекти були очищені!", "filterTagAlreadyInUse": "Цей звуковий ефект уже використовується! Будь ласка, використовуйте /cleareffect <Тег>, щоб видалити його.", - "playlistViewTitle": "📜 Усі плейлисти користувача {0}", + "playlistViewTitle": "Усі плейлисти користувача {0}", "playlistViewHeaders": "ID:,Час:,Назва:,Треки:", "playlistFooter": "Введите /playlist play [плейлист], чтобы добавить плейлист в очередь.", "playlistNotFound": "Плейлист [`{0}`] не знайдено. Введіть /playlist view, щоб подивитися всі ваші плейлисти.", @@ -68,11 +68,11 @@ "playlistRemoved": "👋 Видалено **{0}** з плейлиста {1} [`{2}`].", "playlistClear": "Ви успішно очистили свій плейлист [`{0}`].", "playlistView": "Перегляд плейлистів", - "playlistViewDesc": "```Ім'я | ID: {0} | {1}\nУсього треків: {2}\nВласник: {3}\nТип: {4}\n```", + "playlistViewDesc": "```Ім'я: {0} [{1}]\nУсього треків: {2}\nВласник: {3}\nТип: {4}\n```", "playlistViewPermsValue": "📖 Читання: ✓ ✍🏽 Запис: {0} 🗑️ Видалення: {1}", "playlistViewPermsValue2": "📖 Читання: {0}", "playlistViewTrack": "Треки", - "playlistViewPage": "Сторінка: {0}/{1} | Загальна тривалість: {2}", + "playlistViewFooter": "Загальна тривалість: {0}", "inboxFull": "Вибачте! Поштова скринька {0} переповнена.", "inboxNoMsg": "У вашій поштовій скриньці немає повідомлень.", "invitationSent": "Запрошення надіслано {0}.", @@ -129,7 +129,6 @@ "historyTitle": "Історія черги:", "viewTitle": "Поточна черга", "viewDesc": "**Заразом грає: [*посилання*]({0}) ⮯**\n{1}", - "viewFooter": "Сторінка: {0}/{1} | Загальна тривалість: {2}", "pauseError": "Плеєр уже на паузі.", "pauseVote": "{0} проголосував за паузу пісні. [{1}/{2}]", "paused": "`{0}` Поставив плеєр на паузу.", diff --git a/langs/VN.json b/langs/VN.json index 6fef020..2440917 100644 --- a/langs/VN.json +++ b/langs/VN.json @@ -38,7 +38,7 @@ "addEffect": "Đã áp dụng hiệu ứng `{0}`.", "clearEffect": "Các hiệu ứng âm thanh đã được xóa!", "filterTagAlreadyInUse": "Hiệu ứng âm thanh này đã được sử dụng! Vui lòng sử dụng /cleareffect để xóa nó.", - "playlistViewTitle": "📜 Tất Cả Playlist Của {0}", + "playlistViewTitle": "Tất Cả Playlist Của {0}", "playlistViewHeaders": "ID:,Thời Gian:,Tên:,Bài Hát:", "playlistFooter": "Gõ /playlist play [playlist] để thêm playlist vào hàng đợi.", "playlistNotFound": "Không tìm thấy playlist [`{0}`]. Gõ /playlist view để xem tất cả playlist của bạn.", @@ -68,11 +68,11 @@ "playlistRemoved": "👋 Đã xóa **{0}** khỏi playlist [`{2}`] của {1}.", "playlistClear": "Bạn đã xóa thành công playlist [`{0}`] của mình.", "playlistView": "Trình Xem Playlist", - "playlistViewDesc": "```Tên | ID: {0} | {1}\nTổng Số Bài Hát: {2}\nChủ Sở Hữu: {3}\nLoại: {4}\n```", + "playlistViewDesc": "```Tên: {0} [{1}]\nTổng Số Bài Hát: {2}\nChủ Sở Hữu: {3}\nLoại: {4}\n```", "playlistViewPermsValue": "📖 Đọc: ✓ ✍🏽 Viết: {0} 🗑️ Xóa: {1}", "playlistViewPermsValue2": "📖 Đọc: {0}", "playlistViewTrack": "Bài Hát", - "playlistViewPage": "Trang: {0}/{1} | Tổng Thời Lượng: {2}", + "playlistViewFooter": "Tổng Thời Lượng: {0}", "inboxFull": "Xin lỗi! Hộp thư của {0} đã đầy.", "inboxNoMsg": "Không có tin nhắn nào trong hộp thư của bạn.", "invitationSent": "Đã gửi lời mời đến {0}.", @@ -129,7 +129,6 @@ "historyTitle": "Lịch Sử Hàng Đợi:", "viewTitle": "Hàng Đợi Nhạc", "viewDesc": "**Đang Phát: [Nhấp Vào Đây]({0}) ⮯**\n{1}", - "viewFooter": "Trang: {0}/{1} | Tổng Thời Lượng: {2}", "pauseError": "Trình phát đã được tạm dừng.", "pauseVote": "{0} đã bình chọn tạm dừng bài hát. [{1}/{2}]", "paused": "`{0}` đã tạm dừng trình phát.", diff --git a/requirements.txt b/requirements.txt index 30dac47..0cbe8f1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -discord.py==2.5.2 +discord.py==2.6.3 motor==3.6.0 dnspython==2.2.1 tldextract==3.2.1 diff --git a/views/__init__.py b/views/__init__.py index db8e424..d8bca40 100644 --- a/views/__init__.py +++ b/views/__init__.py @@ -32,7 +32,7 @@ from .search import SearchView from .help import HelpView from .queue import QueueView from .lyrics import LyricsView -from .playlist import PlaylistView +from .playlist import PlaylistViewManager from .inbox import InboxView from .link import LinkView from .debug import DebugView diff --git a/views/playlist.py b/views/playlist.py index d78ad31..3c22448 100644 --- a/views/playlist.py +++ b/views/playlist.py @@ -26,139 +26,297 @@ import function as func from math import ceil from tldextract import extract -from typing import Any, TYPE_CHECKING +from discord.ext import commands +from typing import Any, TYPE_CHECKING, reveal_type + +from .utils import DynamicViewManager, Pagination, BaseModal if TYPE_CHECKING: from voicelink import Track + class Select_playlist(discord.ui.Select): - def __init__(self, results): - self.view: PlaylistView + def __init__(self, results: list[dict[str, Any]]) -> None: + self.view: PlaylistViewManager super().__init__( placeholder="Select a playlist to view ..", custom_id="selector", - options=[discord.SelectOption(emoji='🌎', label='All Playlist')] + - [ - discord.SelectOption(emoji=playlist['emoji'], label=f'{index}. {playlist["name"]}', description=f"{playlist['time']} · {playlist['type']}") - for index, playlist in enumerate(results, start=1) if playlist['type'] != 'error' - ] + options=[ + discord.SelectOption( + emoji=playlist['emoji'], + label=f'{index}. {playlist["name"]}', + value=playlist["id"], + description=f"{playlist['time']} · {playlist['type']}" + ) for index, playlist in enumerate(results, start=1) if playlist['type'] != 'error' + ] ) - + async def callback(self, interaction: discord.Interaction) -> None: - if self.values[0] == 'All Playlist': - self.view.current = None - self.view.toggle_btn(True) - return await interaction.response.edit_message(embed=self.view.viewEmbed, view=self.view) - - self.view.current = self.view.results[int(self.values[0].split(". ")[0]) - 1] - self.view.page = ceil(len(self.view.current['tracks']) / 7) - self.view.current_page = 1 - self.view.toggle_btn(False) - await interaction.response.edit_message(embed=await self.view.build_embed(), view=self.view) + view: PlaylistView = self.view.change_view(self.values[0]) + await interaction.response.edit_message(embed=await view.build_embed(), view=view) + class PlaylistView(discord.ui.View): def __init__( - self, - viewEmbed: discord.Embed, - results: list[dict[str, Any]], - author: discord.Message + self, + primary_view: "PlaylistViewManager", + playlist_data: dict[str, Any] ) -> None: - super().__init__(timeout=60) + super().__init__(timeout=180) - self.viewEmbed: discord.Embed = viewEmbed - self.results: list[dict[str, Any]] = results - self.author: discord.Member = author - self.response: discord.Message = None + self.primary_view: PlaylistViewManager = primary_view + self.author: discord.Member = primary_view.ctx.author - self.current: dict[str, Any] = None - self.page: int = 0 - self.current_page: int = 1 + self.id: str = playlist_data.get("id") + self.emoji: str = playlist_data.get("emoji") + self.name: str = playlist_data.get("name") + self.time: str = playlist_data.get("time") + self.type: str = playlist_data.get("type") + self.owner_id: int = playlist_data.get("owner") + self.perms: dict[str, list[int]] = playlist_data.get("perms") + self.pagination: Pagination = Pagination[dict[str, Any]](playlist_data.get("tracks"), page_size=7) - self.add_item(Select_playlist(results)) + self.update_view() + + def update_view(self) -> None: + """Update button states and page number display based on current pagination state.""" + button_states = { + "fast_back": self.pagination.current_page <= 2, + "back": not self.pagination.has_previous_page, + "fast_next": self.pagination.current_page >= self.pagination.total_pages - 1, + "next": not self.pagination.has_next_page, + } + + for child in self.children: + if child.custom_id in button_states: + child.disabled = button_states[child.custom_id] + if child.custom_id == "page_number": + child.label = f"{self.pagination.current_page:02}/{self.pagination.total_pages:02}" async def interaction_check(self, interaction: discord.Interaction) -> bool: return interaction.user == self.author - + async def on_error(self, error, item, interaction) -> None: return - - def toggle_btn(self, action: bool) -> None: - for child in self.children: - if child.custom_id not in ("delete", "selector"): - child.disabled = action - + async def build_embed(self) -> discord.Embed: - offset: int = self.current_page * 7 - tracks: list[Track] = self.current['tracks'][(offset-7):offset] - texts = await func.get_lang(self.author.guild.id, "playlistView", "playlistViewDesc", "settingsPermTitle", "playlistViewPermsValue", "playlistViewPermsValue2", "playlistViewTrack", "playlistNoTrack", "playlistViewPage") + """Build the embed for the current page of tracks.""" + tracks = self.pagination.get_current_page_items() + texts = await func.get_lang( + self.author.guild.id, + "playlistView", "playlistViewDesc", "settingsPermTitle", + "playlistViewPermsValue", "playlistViewPermsValue2", + "playlistViewTrack", "playlistNoTrack", "playlistViewFooter" + ) embed = discord.Embed(title=texts[0], color=func.settings.embed_color) - embed.description = texts[1].format(self.current['name'], self.current['id'], len(self.current['tracks']), owner if (owner := self.current.get('owner')) else f"{self.author.id} (You)", self.current['type']) + "\n" - - perms = self.current['perms'] - if self.current['type'] == 'share': - embed.description += texts[2] + "\n" + texts[3].format('✓' if 'write' in perms and self.author.id in perms['write'] else '✘', '✓' if 'remove' in perms and self.author.id in perms['remove'] else '✘') - else: - embed.description += texts[2] + "\n" + texts[4].format(', '.join(f'<@{user}>' for user in perms['read'])) + embed.description = texts[1].format( + self.name, + self.id, + self.pagination.total_items, + self.primary_view.ctx.bot.get_user(self.owner_id), + self.type.upper() + ) + "\n" + embed.description += texts[2] + "\n" + if self.type == 'share': + write_perm = '✓' if 'write' in self.perms and self.author.id in self.perms['write'] else '✘' + remove_perm = '✓' if 'remove' in self.perms and self.author.id in self.perms['remove'] else '✘' + embed.description += texts[3].format(write_perm, remove_perm) + else: + readable_users = ', '.join(f'<@{user}>' for user in self.perms['read']) + embed.description += texts[4].format(readable_users) + + # Add track information embed.description += f"\n\n**{texts[5]}:**\n" if tracks: - if self.current.get("type") == "playlist": - embed.description += "\n".join(f"{func.get_source(track['sourceName'], 'emoji')} `{index:>2}.` `[{func.time(track['length'])}]` [{func.truncate_string(track['title'])}]({track['uri']})" for index, track in enumerate(tracks, start=offset - 6)) - else: - embed.description += '\n'.join(f"{func.get_source(extract(track.info['uri']).domain, 'emoji')} `{index:>2}.` `[{func.time(track.length)}]` [{func.truncate_string(track.title)}]({track.uri})" for index, track in enumerate(tracks, start=offset - 6)) + for index, track in enumerate(tracks, start=self.pagination.start_index + 1): + if self.type == "playlist": + source_emoji = func.get_source(track['sourceName'], 'emoji') + track_info = f"{source_emoji} `{index:>2}.` `[{func.time(track['length'])}]` [{func.truncate_string(track['title'])}]({track['uri']})" + else: + source_emoji = func.get_source(extract(track.info['uri']).domain, 'emoji') + track_info = f"{source_emoji} `{index:>2}.` `[{func.time(track.length)}]` [{func.truncate_string(track.title)}]({track.uri})" + embed.description += track_info + "\n" else: - embed.description += texts[6].format(self.current['name']) + embed.description += texts[6].format(self.name) - embed.set_footer(text=texts[7].format(self.current_page, self.page, self.current['time'])) + # Set the footer + embed.set_footer(text=texts[7].format(self.time)) return embed async def on_timeout(self) -> None: for child in self.children: child.disabled = True try: - await self.response.edit(view=self) + await self.primary_view.response.edit(view=self) except: pass - @discord.ui.button(label='<<', style=discord.ButtonStyle.grey, disabled=True) + async def update_and_edit_message(self, interaction: discord.Interaction) -> None: + """Update the view and edit the message with the new embed.""" + self.update_view() + + if interaction.response.is_done(): + await interaction.followup.edit_message(self.primary_view.response.id, embed=await self.build_embed(), view=self) + else: + await interaction.response.edit_message(embed=await self.build_embed(), view=self) + + async def on_error(self, error: Exception, item: discord.ui.Item, interaction: discord.Interaction) -> None: + """Handle errors that occur during interaction.""" + func.logger.error(f"Error in PlaylistView: {error}", exc_info=error) + + @discord.ui.button(label='<<', custom_id="fast_back") async def fast_back_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None: - if not self.current: - return - if self.current_page != 1: - self.current_page = 1 - return await interaction.response.edit_message(embed=await self.build_embed()) - await interaction.response.defer() + """Jump to the first page.""" + self.pagination.go_page(0) + await self.update_and_edit_message(interaction) - @discord.ui.button(label='Back', style=discord.ButtonStyle.blurple, disabled=True) + @discord.ui.button(label='Back', custom_id="back", style=discord.ButtonStyle.blurple) async def back_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None: - if not self.current: - return - if self.current_page > 1: - self.current_page -= 1 - return await interaction.response.edit_message(embed=await self.build_embed()) - await interaction.response.defer() + """Go to the previous page if it exists.""" + self.pagination.go_back() + await self.update_and_edit_message(interaction) - @discord.ui.button(label='Next', style=discord.ButtonStyle.blurple, disabled=True) + @discord.ui.button(label="--/--", custom_id="page_number", style=discord.ButtonStyle.blurple) + async def page_number(self, interaction: discord.Interaction, button: discord.ui.Button) -> None: + """Display current page number.""" + modal = BaseModal( + title="Page Number", + custom_id="page_number_modal", + items=[ + discord.ui.TextInput( + label="Page Number", + custom_id="page_number", + placeholder="Enter the page number to navigate.", + default=str(self.pagination.current_page), + max_length=5, + required=True + ) + ] + ) + await interaction.response.send_modal(modal) + await modal.wait() + + page_number = modal.values.get("page_number") + if not page_number or not page_number.isdigit(): + return + + self.pagination.go_page(int(page_number) - 1) + await self.update_and_edit_message(interaction) + + @discord.ui.button(label='Next', custom_id="next", style=discord.ButtonStyle.blurple) async def next_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None: - if not self.current: - return - if self.current_page < self.page: - self.current_page += 1 - return await interaction.response.edit_message(embed=await self.build_embed()) - await interaction.response.defer() + """Go to the next page if it exists.""" + self.pagination.go_next() + await self.update_and_edit_message(interaction) - @discord.ui.button(label='>>', style=discord.ButtonStyle.grey, disabled=True) + @discord.ui.button(label='>>', custom_id="fast_next") async def fast_next_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None: - if not self.current: - return - if self.current_page != self.page: - self.current_page = self.page - return await interaction.response.edit_message(embed=await self.build_embed()) + """Jump to the last page.""" + self.pagination.go_page(self.pagination.total_pages - 1) + await self.update_and_edit_message(interaction) + + @discord.ui.button(label="<") + async def back_to_home(self, interaction: discord.Interaction, button: discord.ui.Button) -> None: + """Return to the main playlist view.""" + view: PlaylistViewManager = self.primary_view.change_view("home") + await interaction.response.edit_message(embed=await view.build_embed(), view=view) + + @discord.ui.button(label="Play", style=discord.ButtonStyle.green) + async def play_all(self, interaction: discord.Interaction[commands.Bot], button: discord.ui.Button) -> None: + await interaction.response.defer() + cmd = interaction.client.get_command("playlist play") + await cmd(self.primary_view.ctx, self.name) + # Need to handle error + + @discord.ui.button(label="Share", style=discord.ButtonStyle.blurple) + async def share(self, interaction: discord.Interaction[commands.Bot], button: discord.ui.Button) -> None: await interaction.response.defer() - @discord.ui.button(emoji='🗑️', custom_id="delete", style=discord.ButtonStyle.red) - async def stop_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None: - await self.response.delete() - self.stop() \ No newline at end of file + @discord.ui.button(label="Export", style=discord.ButtonStyle.gray) + async def export(self, interaction: discord.Interaction[commands.Bot], button: discord.ui.Button) -> None: + await interaction.response.defer() + cmd = interaction.client.get_command("playlist export") + await cmd(self.primary_view.ctx, self.name) + + @discord.ui.button(label="Delete", style=discord.ButtonStyle.red) + async def delete(self, interaction: discord.Interaction[commands.Bot], button: discord.ui.Button) -> None: + await interaction.response.defer() + cmd = interaction.client.get_command("playlist delete") + await cmd(self.primary_view.ctx, name=self.name) + + +class PlaylistViewManager(DynamicViewManager): + def __init__(self, ctx: commands.Context, results: list[dict[str, Any]]): + self.ctx: commands.Context = ctx + self.results: list[dict[str, Any]] = results + + views = {"home": self} + views.update({result["id"]: PlaylistView(self, result) for result in results}) + + super().__init__(views=views, timeout=None) + + self.response: discord.Message = None + self.add_item(Select_playlist(results)) + + def get_width(self, s): + import unicodedata + width = 0 + for char in str(s): + if unicodedata.east_asian_width(char) in ('F', 'W'): + width += 2 + else: + width += 1 + return width + + def pad_string(self, s, width): + s = str(s) + current_width = self.get_width(s) + padding = width - current_width + return s + " " * padding + + async def build_embed(self) -> discord.Embed: + """ + Build the embed for the playlist overview. + + Returns: + discord.Embed: The constructed embed with playlist details. + """ + _, max_p, _ = func.check_roles() + text = await func.get_lang(self.ctx.guild.id, "playlistViewTitle", "playlistViewHeaders", "playlistFooter") + + headers = text[1].split(",") + headers.insert(0, "") + content = [headers] + + description = "" + for index in range(max_p): + info = self.results[index] if index < len(self.results) else {} + if info: + content.append([ + info.get('emoji', ' '), + info.get('id', "-" * 3), + f"[{info.get('time', '--:--')}]", + info.get('name', "-" * 6), + len(info.get('tracks', [])) + ]) + + column_widths = [max(self.get_width(str(item)) for item in column) for column in zip(*content)] + + for row in content: + formatted_row = " ".join(self.pad_string(item, width) for item, width in zip(row, column_widths)) + description += formatted_row + "\n" + + embed = discord.Embed( + description=f'```{description}```', + color=func.settings.embed_color + ) + + embed.set_author( + name=text[0].format(self.ctx.author.display_name), + icon_url=self.ctx.author.display_avatar.url + ) + embed.set_footer(text=text[2]) + return embed \ No newline at end of file diff --git a/views/queue.py b/views/queue.py index ff984c5..ad921c3 100644 --- a/views/queue.py +++ b/views/queue.py @@ -122,7 +122,7 @@ class QueueView(discord.ui.View): "live", "queueTitle", "historyTitle", - "viewFooter", + "playlistViewFooter", ) embed = discord.Embed(title=texts[0], color=func.settings.embed_color) diff --git a/views/utils/__init__.py b/views/utils/__init__.py index c2902b7..a0d3fda 100644 --- a/views/utils/__init__.py +++ b/views/utils/__init__.py @@ -22,4 +22,5 @@ SOFTWARE. """ from .pagination import Pagination -from .modal import BaseModal \ No newline at end of file +from .modal import BaseModal +from .dynamic_view_manager import DynamicViewManager diff --git a/views/utils/dynamic_view_manager.py b/views/utils/dynamic_view_manager.py new file mode 100644 index 0000000..2a9bb4c --- /dev/null +++ b/views/utils/dynamic_view_manager.py @@ -0,0 +1,89 @@ +"""MIT License + +Copyright (c) 2023 - present Vocard Development + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import discord + + +class DynamicViewManager(discord.ui.View): + def __init__(self, views: dict[str, discord.ui.View], *, timeout: float = 180): + """A manager for multiple dynamic views.""" + super().__init__(timeout=timeout) + + self._views: dict[str, discord.ui.View] = views + self._current_view: discord.ui.View | None = None + + def current_view(self) -> discord.ui.View | None: + """Get the current active view.""" + return self._current_view + + def add_view(self, name: str, view: discord.ui.View) -> None: + """ + Add a view to the manager. + + Args: + name (str): The name of the view. + view (discord.ui.View): The view instance to add. + """ + if name in self._views: + raise ValueError(f"A view with the name '{name}' already exists.") + + self._views[name] = view + + def get_view(self, key: str) -> discord.ui.View | None: + """ + Get a view from the manager. + + Args: + key (str): The name of the view to retrieve. + + Returns: + discord.ui.View | None: The requested view or None if not found. + """ + return self._views.get(key) + + def remove_view(self, key: str) -> None: + """ + Remove a view from the manager. + + Args: + key (str): The name of the view to remove. + """ + if key in self._views: + del self._views[key] + + def change_view(self, name: str) -> discord.ui.View: + """ + Change the current active view. + + Args: + name (str): The name of the view to switch to. + + Returns: + discord.ui.View: The newly activated view. + """ + view = self.get_view(name) + if not view: + raise ValueError(f"No view found with the name '{name}'.") + + self._current_view = view + return view diff --git a/views/utils/pagination.py b/views/utils/pagination.py index 7bfdbd1..3a48070 100644 --- a/views/utils/pagination.py +++ b/views/utils/pagination.py @@ -57,6 +57,28 @@ class Pagination(Generic[T]): self._current_page: int = 0 self.total_pages: int = ceil(len(items) / page_size) + def add_item(self, item: T) -> None: + """ + Adds an item to the pagination and updates total pages. + + Args: + item (T): The item to add. + """ + self._items.append(item) + self.total_pages = ceil(len(self._items) / self._page_size) + + def remove_item(self, item: T) -> None: + """ + Removes an item from the pagination and updates total pages. + + Args: + item (T): The item to remove. + """ + self._items.remove(item) + self.total_pages = ceil(len(self._items) / self._page_size) + if self._current_page >= self.total_pages: + self._current_page = max(0, self.total_pages - 1) + def get_current_page_items(self) -> List[T]: """ Retrieves the items for the current page. @@ -77,7 +99,12 @@ class Pagination(Generic[T]): self._current_page += 1 def go_page(self, page_number: int) -> None: - """Navigate to a specific page, clamped between 0 and total_pages.""" + """ + Navigate to a specific page, clamped between 0 and total_pages. + + Args: + page_number (int): The page number to navigate to (0-based). + """ self._current_page = max(0, min(page_number, self.total_pages - 1)) @property @@ -129,3 +156,13 @@ class Pagination(Generic[T]): int: The current page number, starting from 1. """ return self._current_page + 1 + + @property + def total_items(self) -> int: + """ + Gets the total number of items in the pagination. + + Returns: + int: The total number of items across all pages. + """ + return len(self._items) \ No newline at end of file