Skip to content

Generic UI Components

This page documents the reusable UI components used across various views of the bot. These components encapsulate logic for state management, navigation, and database interaction.

Interactive Buttons

The button logic is split between src.oscar.ui.buttons (generic toggles) and src.oscar.ui.select_button (domain-specific actions).

1. ToggleButton

A general-purpose button that switches between two states (On/Off) and updates its appearance automatically.

  • Logic: Executes an async on_changed callback when clicked, passing the new boolean state.
  • Usage: Used in TranslatedView to switch languages.

src.oscar.ui.buttons.ToggleButton

ToggleButton(state: bool = True, label_on: str = '✔', label_off: str = '✘', style_on: ButtonStyle = discord.ButtonStyle.success, style_off: ButtonStyle = discord.ButtonStyle.danger, on_changed: Callable[[Interaction, bool], Awaitable[None]] | None = None)

Bases: Button[LayoutView]

Toggle button class that switches between two states

Source code in src/oscar/ui/buttons.py
def __init__(
    self,
    state: bool = True,
    label_on: str = "✔",
    label_off: str = "✘",
    style_on: discord.ButtonStyle = discord.ButtonStyle.success,
    style_off: discord.ButtonStyle = discord.ButtonStyle.danger,
    on_changed: Callable[[discord.Interaction, bool], Awaitable[None]] | None = None,
):
    self.toggle_state = state
    self.label_on = label_on
    self.label_off = label_off
    self.style_on = style_on
    self.style_off = style_off
    self.on_changed = on_changed

    super().__init__(
        label=label_on if state else label_off,
        style=style_on if state else style_off,
    )

2. SelectButton (Radio Group)

A button designed for single-choice selections within a group (Radio Button behavior).

  • Group Logic: It iterates recursively through the parent view's children. When clicked, it deselects all other SelectButton instances that share the same group string.
  • Side Effect: If the group is named "Semester", it automatically writes the selection to the user's preferences in the database.

src.oscar.ui.select_button.SelectButton

SelectButton(label: str, group: str = 'default')

Bases: Button[LayoutView]

Class for a select button

Source code in src/oscar/ui/select_button.py
def __init__(self, label: str, group:str ="default"):
    super().__init__(label=label, style=discord.ButtonStyle.secondary)
    self.selected: bool = False
    self.group: str = group

3. FilterButton (Multi-Select)

A specialized button for the filter mask (SelectView). Unlike SelectButton, it allows multiple active selections.

  • Visual Feedback: Changes style to Success (Green) when selected and back to Secondary (Grey) when deselected.
  • Data Flow: The parent view iterates over these buttons to build the filter query.

src.oscar.ui.select_button.FilterButton

FilterButton(label: str, group: str = 'default', value: str | None = None)

Bases: Button[LayoutView]

Class for a filter button.

The label is what the student reads and therefore changes with the language. The value is what the search reads and stays the same in every language. It falls back to the label, so an older button without a value still works.

Source code in src/oscar/ui/select_button.py
def __init__(self, label: str, group:str ="default", value: str | None = None):
    super().__init__(label=label, style=discord.ButtonStyle.secondary)
    self.selected: bool = False
    self.group:str = group
    self.value: str = label if value is None else value

4. Action Buttons (Info & Delete)

Buttons that perform specific operations on Module objects.

  • InfoButton: Fetches a module by name (Module.from_name) and opens a ModuleView.
  • DeleteButton: Removes a module from the semester plan via the database.
    • Requirement: Must be attached to a CustomView. It calls view.build_view() to trigger a full UI refresh after deletion.

src.oscar.ui.select_button.InfoButton

InfoButton(label: str, module: str)

Bases: Button[LayoutView]

Class for an info button

Source code in src/oscar/ui/select_button.py
def __init__(self,label:str,module:str):
    super().__init__(label=label,style=discord.ButtonStyle.secondary)
    self.module_name = module

src.oscar.ui.select_button.DeleteButton

DeleteButton(view: CustomView, module)

Bases: Button[LayoutView]

Class for a delete button

Source code in src/oscar/ui/select_button.py
def __init__(self, view: CustomView, module):
    super().__init__(label="X", style=discord.ButtonStyle.danger)
    self.view_ref = view  # Referenz auf das View
    self.module = module  # Das zugehörige Modulobjekt

Pagination System

The PaginatedView is an abstract base class for navigating large datasets (e.g., search results or example lists).

Core Logic

  • Navigation: Implements 4 buttons: First (❙❮), Previous (❮), Next (❯), Last (❯❙).
  • State Management: Automatically disables navigation buttons when reaching the start or end of the list (e.g., disables "Next" on the last page).
  • Security: Includes a check_user method to ensure only the user who initiated the command can navigate the view.

Implementation

To use this, you must provide a get_page callback function that returns a discord.Embed for a given index.

src.oscar.ui.paginated_view.PaginatedView

PaginatedView(interaction: Interaction, total_pages: int, get_page: Callable[[int], Embed])

Bases: View

Class holding a Paginated View

Source code in src/oscar/ui/paginated_view.py
def __init__(
        self,
        interaction: discord.Interaction,
        total_pages: int,
        get_page: Callable[[int], discord.Embed]
    ):
    self.interaction: Interaction[Client] = interaction
    self.total_pages: int = total_pages
    self.get_page: Callable[[int], discord.Embed] = get_page
    self.index:int = 0
    super().__init__(timeout=300)  # doesn't accept new interaction after <timeout> seconds

setup async

setup()

Setup the embed and button callbacks

Source code in src/oscar/ui/paginated_view.py
async def setup(self):
    """ Setup the embed and button callbacks"""
    embed:discord.Embed = self.get_page(self.index)

    if self.total_pages <=1:
        _ = await self.interaction.response.send_message(embed=embed)
    else:
        self.__update_buttons()
        _ = await self.interaction.response.send_message(embed=embed, view=self)

check_user async

check_user(interaction: Interaction) -> bool

Chec weather the user has the right to perform the interaction

Source code in src/oscar/ui/paginated_view.py
async def check_user(self, interaction: discord.Interaction) -> bool:
    """ Chec weather the user has the right to perform the interaction"""
    if interaction.user == self.interaction.user:
        return True

    _ = await interaction.response.send_message(
        'Only the original auther can interact',
        ephemeral=True
    )
    return False

first async

first(interaction: Interaction, _button: Button[LayoutView])

Callback for the skip to the first page button

Source code in src/oscar/ui/paginated_view.py
@discord.ui.button(label='❙❮', style=discord.ButtonStyle.secondary)
async def first(self, interaction: discord.Interaction, _button: Button[LayoutView]):
    """ Callback for the skip to the first page button"""
    self.index = 0
    await self.__update(interaction=interaction)

previous async

previous(interaction: Interaction, _button: Button[LayoutView])

Callback for the previous page button

Source code in src/oscar/ui/paginated_view.py
@discord.ui.button(label='❮', style=discord.ButtonStyle.blurple)
async def previous(self, interaction: discord.Interaction, _button: Button[LayoutView]):
    """ Callback for the previous page button"""
    self.index -= 1
    await self.__update(interaction=interaction)

next async

next(interaction: Interaction, _button: Button[LayoutView])

Callback for the next page button

Source code in src/oscar/ui/paginated_view.py
@discord.ui.button(label='❯', style=discord.ButtonStyle.blurple)
async def next(self, interaction: discord.Interaction, _button: Button[LayoutView]):
    """ Callback for the next page button"""
    self.index += 1
    await self.__update(interaction=interaction)

last async

last(interaction: Interaction, _button: Button[LayoutView])

Callback for the skip to the last page button

Source code in src/oscar/ui/paginated_view.py
@discord.ui.button(label='❯❙', style=discord.ButtonStyle.secondary)
async def last(self, interaction: discord.Interaction, _button: Button[LayoutView]):
    """ Callback for the skip to the last page button"""
    self.index = self.total_pages-1
    await self.__update(interaction=interaction)

see Views