-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathui_base.py
More file actions
116 lines (102 loc) · 4.91 KB
/
ui_base.py
File metadata and controls
116 lines (102 loc) · 4.91 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
import discord
import traceback
from discord.ui import LayoutView
from ui_icons import Icons
from ui_translate import t
from ui_utils import get_feedback
from logger import log
# This is a "Decorator". It's like a wrapper around other functions.
# We use it to catch errors in button clicks so the bot doesn't crash,
# and also to check if the user is in the same voice channel as the bot.
def handle_ui_error(func):
"""Decorator to handle errors in UI callbacks and enforce permissions."""
async def wrapper(*args, **kwargs):
interaction = next((arg for arg in args if isinstance(arg, discord.Interaction)), None)
if not interaction:
return await func(*args, **kwargs)
# Enforce permission check
# Usually, component callbacks are methods: (self, interaction)
# We check if self has a 'radio' attribute
self_obj = args[0] if args else None
radio = getattr(self_obj, 'radio', None)
if radio and hasattr(radio, 'can_interact'):
if not radio.can_interact(interaction.user):
# We need to import t inside to avoid circular imports or missing refs
# but it's already at top level. Let's use it.
feedback = get_feedback('not_in_same_voice')
if not interaction.response.is_done():
await interaction.response.send_message(feedback, ephemeral=True)
else:
await interaction.followup.send(feedback, ephemeral=True)
# Auto-delete error notification
from ui_utils import delayed_delete
import asyncio
asyncio.create_task(delayed_delete(interaction, radio.config.notification_timeout))
return
try:
return await func(*args, **kwargs)
except (discord.errors.NotFound, discord.errors.HTTPException) as e:
# Handle known noise errors
code = getattr(e, 'code', 0)
if code in [10062, 40060]:
return # Silent ignore for these
log.error(f"UI Error in {func.__name__}: {e}")
await _send_error_msg(interaction)
except Exception as e:
log.error(f"UI Error in {func.__name__}: {e}")
traceback.print_exc()
await _send_error_msg(interaction)
return wrapper
async def _send_error_msg(interaction):
if not interaction: return
feedback = get_feedback('error_generic')
try:
if not interaction.response.is_done():
await interaction.response.send_message(feedback, ephemeral=True)
else:
await interaction.followup.send(feedback, ephemeral=True)
except:
pass # Final line of defense
# This is the "Father" class for all our screens.
# It makes sure every view has access to the radio object.
class BaseView(LayoutView):
"""Base class for all Radio Bot views with shared logic."""
def __init__(self, radio, timeout=None):
super().__init__(timeout=timeout)
self.radio = radio
async def on_error(self, interaction: discord.Interaction, error: Exception, item: discord.ui.Item) -> None:
log.error(f"View Error: {error} in {item}")
traceback.print_exc()
feedback = get_feedback('error_generic')
try:
if not interaction.response.is_done():
await interaction.response.send_message(feedback, ephemeral=True)
else:
await interaction.followup.send(feedback, ephemeral=True)
except discord.errors.NotFound:
log.warning(f"Could not send view error message (interaction expired): {error}")
except Exception as e:
log.error(f"Error in View.on_error: {e}")
# This is a special view for lists (like the Queue or History).
# It can split a long list into multiple "pages" that you can flip through.
class PaginatedView(BaseView):
"""Base class for views requiring pagination."""
def __init__(self, radio, data_list, items_per_page=5, timeout=None, page=0):
super().__init__(radio, timeout=timeout)
self.data_list = data_list
self.items_per_page = items_per_page
self.current_page = page
self.total_pages = max(1, (len(data_list) + items_per_page - 1) // items_per_page)
def get_page_items(self):
start = self.current_page * self.items_per_page
end = start + self.items_per_page
return self.data_list[start:end]
def update_pagination_buttons(self, prev_button, next_button):
"""Helper to update state of Prev/Next buttons."""
if prev_button:
prev_button.disabled = (self.current_page == 0)
if next_button:
next_button.disabled = (self.current_page >= self.total_pages - 1)
@property
def pagination_info(self):
return f"{t('page')} {self.current_page + 1} / {self.total_pages} ({len(self.data_list)} {t('total')})"