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