From 28efb157eec0993a3e47e54bd33f6709237dd7d9 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sun, 2 Mar 2025 13:42:37 +0100 Subject: [PATCH] chore: Finished first components v2 impl --- discord/components.py | 1 - discord/ui/file.py | 125 +++++++++++++++++++++++++ discord/ui/item.py | 5 +- discord/ui/media_gallery.py | 177 ++++++++++++++++++++++++++++++++++++ discord/ui/section.py | 8 ++ discord/ui/separator.py | 110 ++++++++++++++++++++++ discord/ui/text_display.py | 14 ++- discord/ui/thumbnail.py | 24 ++++- discord/ui/view.py | 7 +- 9 files changed, 462 insertions(+), 9 deletions(-) create mode 100644 discord/ui/file.py create mode 100644 discord/ui/media_gallery.py create mode 100644 discord/ui/separator.py diff --git a/discord/components.py b/discord/components.py index 7330b82e9..f13646910 100644 --- a/discord/components.py +++ b/discord/components.py @@ -986,7 +986,6 @@ class MediaGalleryComponent(Component): def to_dict(self) -> MediaGalleryComponentPayload: return { - 'id': self.id, 'type': self.type.value, 'items': [item.to_dict() for item in self.items], } diff --git a/discord/ui/file.py b/discord/ui/file.py new file mode 100644 index 000000000..b4285a654 --- /dev/null +++ b/discord/ui/file.py @@ -0,0 +1,125 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-present Rapptz + +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. +""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Literal, Optional, TypeVar, Union + +from .item import Item +from ..components import FileComponent, UnfurledMediaItem +from ..enums import ComponentType + +if TYPE_CHECKING: + from typing_extensions import Self + + from .view import View + +V = TypeVar('V', bound='View', covariant=True) + +__all__ = ('File',) + + +class File(Item[V]): + """Represents a UI file component. + + .. versionadded:: 2.6 + + Parameters + ---------- + media: Union[:class:`str`, :class:`UnfurledMediaItem`] + This file's media. If this is a string itmust point to a local + file uploaded within the parent view of this item, and must + meet the ``attachment://file-name.extension`` structure. + spoiler: :class:`bool` + Whether to flag this file as a spoiler. Defaults to ``False``. + row: Optional[:class:`int`] + The relative row this file component belongs to. By default + items are arranged automatically into those rows. If you'd + like to control the relative positioning of the row then + passing an index is advised. For example, row=1 will show + up before row=2. Defaults to ``None``, which is automatic + ordering. The row number must be between 0 and 9 (i.e. zero indexed) + """ + + def __init__( + self, + media: Union[str, UnfurledMediaItem], + *, + spoiler: bool = False, + row: Optional[int] = None, + ) -> None: + super().__init__() + self._underlying = FileComponent._raw_construct( + media=UnfurledMediaItem(media) if isinstance(media, str) else media, + spoiler=spoiler, + ) + + self.row = row + + def _is_v2(self): + return True + + @property + def width(self): + return 5 + + @property + def type(self) -> Literal[ComponentType.file]: + return self._underlying.type + + @property + def media(self) -> UnfurledMediaItem: + """:class:`UnfurledMediaItem`: Returns this file media.""" + return self._underlying.media + + @media.setter + def media(self, value: UnfurledMediaItem) -> None: + self._underlying.media = value + + @property + def url(self) -> str: + """:class:`str`: Returns this file's url.""" + return self._underlying.media.url + + @url.setter + def url(self, value: str) -> None: + self._underlying.media = UnfurledMediaItem(value) + + @property + def spoiler(self) -> bool: + """:class:`bool`: Returns whether this file should be flagged as a spoiler.""" + return self._underlying.spoiler + + @spoiler.setter + def spoiler(self, value: bool) -> None: + self._underlying.spoiler = value + + def to_component_dict(self): + return self._underlying.to_dict() + + @classmethod + def from_component(cls, component: FileComponent) -> Self: + return cls( + media=component.media, + spoiler=component.spoiler, + ) diff --git a/discord/ui/item.py b/discord/ui/item.py index 2d2a3aaa6..aaf15cee6 100644 --- a/discord/ui/item.py +++ b/discord/ui/item.py @@ -70,6 +70,7 @@ class Item(Generic[V]): # actually affect the intended purpose of this check because from_component is # only called upon edit and we're mainly interested during initial creation time. self._provided_custom_id: bool = False + self._max_row: int = 5 if not self._is_v2() else 10 def to_component_dict(self) -> Dict[str, Any]: raise NotImplementedError @@ -109,10 +110,10 @@ class Item(Generic[V]): def row(self, value: Optional[int]) -> None: if value is None: self._row = None - elif 5 > value >= 0: + elif self._max_row > value >= 0: self._row = value else: - raise ValueError('row cannot be negative or greater than or equal to 5') + raise ValueError('row cannot be negative or greater than or equal to 10') @property def width(self) -> int: diff --git a/discord/ui/media_gallery.py b/discord/ui/media_gallery.py new file mode 100644 index 000000000..fa20af740 --- /dev/null +++ b/discord/ui/media_gallery.py @@ -0,0 +1,177 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-present Rapptz + +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. +""" +from __future__ import annotations + +from typing import TYPE_CHECKING, List, Literal, Optional, TypeVar + +from .item import Item +from ..enums import ComponentType +from ..components import ( + MediaGalleryItem, + MediaGalleryComponent, +) + +if TYPE_CHECKING: + from typing_extensions import Self + + from .view import View + +V = TypeVar('V', bound='View', covariant=True) + +__all__ = ('MediaGallery',) + + +class MediaGallery(Item[V]): + """Represents a UI media gallery. + + This can contain up to 10 :class:`MediaGalleryItem`s. + + .. versionadded:: 2.6 + + Parameters + ---------- + items: List[:class:`MediaGalleryItem`] + The initial items of this gallery. + row: Optional[:class:`int`] + The relative row this media gallery belongs to. By default + items are arranged automatically into those rows. If you'd + like to control the relative positioning of the row then + passing an index is advised. For example, row=1 will show + up before row=2. Defaults to ``None``, which is automatic + ordering. The row number must be between 0 and 9 (i.e. zero indexed) + """ + + def __init__(self, items: List[MediaGalleryItem], *, row: Optional[int] = None) -> None: + super().__init__() + + self._underlying = MediaGalleryComponent._raw_construct( + items=items, + ) + + self.row = row + + @property + def items(self) -> List[MediaGalleryItem]: + """List[:class:`MediaGalleryItem`]: Returns a read-only list of this gallery's items.""" + return self._underlying.items.copy() + + @items.setter + def items(self, value: List[MediaGalleryItem]) -> None: + if len(value) > 10: + raise ValueError('media gallery only accepts up to 10 items') + + self._underlying.items = value + + def to_component_dict(self): + return self._underlying.to_dict() + + def _is_v2(self) -> bool: + return True + + def add_item(self, item: MediaGalleryItem) -> Self: + """Adds an item to this gallery. + + This function returns the class instance to allow for fluent-style + chaining. + + Parameters + ---------- + item: :class:`MediaGalleryItem` + The item to add to the gallery. + + Raises + ------ + TypeError + A :class:`MediaGalleryItem` was not passed. + ValueError + Maximum number of items has been exceeded (10). + """ + + if len(self._underlying.items) >= 10: + raise ValueError('maximum number of items has been exceeded') + + if not isinstance(item, MediaGalleryItem): + raise TypeError(f'expected MediaGalleryItem not {item.__class__.__name__}') + + self._underlying.items.append(item) + return self + + def remove_item(self, item: MediaGalleryItem) -> Self: + """Removes an item from the gallery. + + This function returns the class instance to allow for fluent-style + chaining. + + Parameters + ---------- + item: :class:`MediaGalleryItem` + The item to remove from the gallery. + """ + + try: + self._underlying.items.remove(item) + except ValueError: + pass + return self + + def insert_item_at(self, index: int, item: MediaGalleryItem) -> Self: + """Inserts an item before a specified index to the gallery. + + This function returns the class instance to allow for fluent-style + chaining. + + Parameters + ---------- + index: :class:`int` + The index of where to insert the item. + item: :class:`MediaGalleryItem` + The item to insert. + """ + + self._underlying.items.insert(index, item) + return self + + def clear_items(self) -> Self: + """Removes all items from the gallery. + + This function returns the class instance to allow for fluent-style + chaining. + """ + + self._underlying.items.clear() + return self + + @property + def type(self) -> Literal[ComponentType.media_gallery]: + return self._underlying.type + + @property + def width(self): + return 5 + + @classmethod + def from_component(cls, component: MediaGalleryComponent) -> Self: + return cls( + items=component.items, + ) diff --git a/discord/ui/section.py b/discord/ui/section.py index 0012d0118..4da36f86f 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -49,6 +49,13 @@ class Section(Item[V]): The text displays of this section. Up to 3. accessory: Optional[:class:`Item`] The section accessory. Defaults to ``None``. + row: Optional[:class:`int`] + The relative row this section belongs to. By default + items are arranged automatically into those rows. If you'd + like to control the relative positioning of the row then + passing an index is advised. For example, row=1 will show + up before row=2. Defaults to ``None``, which is automatic + ordering. The row number must be between 0 and 9 (i.e. zero indexed) """ __slots__ = ( @@ -61,6 +68,7 @@ class Section(Item[V]): children: List[Union[Item[Any], str]], *, accessory: Optional[Item[Any]] = None, + row: Optional[int] = None, ) -> None: super().__init__() if len(children) > 3: diff --git a/discord/ui/separator.py b/discord/ui/separator.py new file mode 100644 index 000000000..c275fad82 --- /dev/null +++ b/discord/ui/separator.py @@ -0,0 +1,110 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-present Rapptz + +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. +""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Literal, Optional, TypeVar + +from .item import Item +from ..components import SeparatorComponent +from ..enums import SeparatorSize, ComponentType + +if TYPE_CHECKING: + from .view import View + +V = TypeVar('V', bound='View', covariant=True) + +__all__ = ('Separator',) + + +class Separator(Item[V]): + """Represents a UI separator. + + .. versionadded:: 2.6 + + Parameters + ---------- + visible: :class:`bool` + Whether this separator is visible. On the client side this + is whether a divider line should be shown or not. + spacing: :class:`SeparatorSize` + The spacing of this separator. + row: Optional[:class:`int`] + The relative row this separator belongs to. By default + items are arranged automatically into those rows. If you'd + like to control the relative positioning of the row then + passing an index is advised. For example, row=1 will show + up before row=2. Defaults to ``None``, which is automatic + ordering. The row number must be between 0 and 9 (i.e. zero indexed) + """ + + def __init__( + self, + *, + visible: bool = True, + spacing: SeparatorSize = SeparatorSize.small, + row: Optional[int] = None, + ) -> None: + super().__init__() + self._underlying = SeparatorComponent._raw_construct( + spacing=spacing, + visible=visible, + ) + + self.row = row + + def _is_v2(self): + return True + + @property + def visible(self) -> bool: + """:class:`bool`: Whether this separator is visible. + + On the client side this is whether a divider line should + be shown or not. + """ + return self._underlying.visible + + @visible.setter + def visible(self, value: bool) -> None: + self._underlying.visible = value + + @property + def spacing(self) -> SeparatorSize: + """:class:`SeparatorSize`: The spacing of this separator.""" + return self._underlying.spacing + + @spacing.setter + def spacing(self, value: SeparatorSize) -> None: + self._underlying.spacing = value + + @property + def width(self): + return 5 + + @property + def type(self) -> Literal[ComponentType.separator]: + return self._underlying.type + + def to_component_dict(self): + return self._underlying.to_dict() diff --git a/discord/ui/text_display.py b/discord/ui/text_display.py index 0daff9c89..a51d60493 100644 --- a/discord/ui/text_display.py +++ b/discord/ui/text_display.py @@ -23,7 +23,7 @@ DEALINGS IN THE SOFTWARE. """ from __future__ import annotations -from typing import TYPE_CHECKING, Literal, TypeVar +from typing import TYPE_CHECKING, Literal, Optional, TypeVar from .item import Item from ..components import TextDisplay as TextDisplayComponent @@ -46,16 +46,24 @@ class TextDisplay(Item[V]): ---------- content: :class:`str` The content of this text display. + row: Optional[:class:`int`] + The relative row this text display belongs to. By default + items are arranged automatically into those rows. If you'd + like to control the relative positioning of the row then + passing an index is advised. For example, row=1 will show + up before row=2. Defaults to ``None``, which is automatic + ordering. The row number must be between 0 and 9 (i.e. zero indexed) """ - def __init__(self, content: str) -> None: + def __init__(self, content: str, *, row: Optional[int] = None) -> None: super().__init__() self.content: str = content - self._underlying = TextDisplayComponent._raw_construct( content=content, ) + self.row = row + def to_component_dict(self): return self._underlying.to_dict() diff --git a/discord/ui/thumbnail.py b/discord/ui/thumbnail.py index 67e380f65..fe1d96221 100644 --- a/discord/ui/thumbnail.py +++ b/discord/ui/thumbnail.py @@ -56,14 +56,34 @@ class Thumbnail(Item[V]): The description of this thumbnail. Defaults to ``None``. spoiler: :class:`bool` Whether to flag this thumbnail as a spoiler. Defaults to ``False``. + row: Optional[:class:`int`] + The relative row this thumbnail belongs to. By default + items are arranged automatically into those rows. If you'd + like to control the relative positioning of the row then + passing an index is advised. For example, row=1 will show + up before row=2. Defaults to ``None``, which is automatic + ordering. The row number must be between 0 and 9 (i.e. zero indexed) """ - def __init__(self, media: Union[str, UnfurledMediaItem], *, description: Optional[str] = None, spoiler: bool = False) -> None: + def __init__( + self, + media: Union[str, UnfurledMediaItem], + *, + description: Optional[str] = None, + spoiler: bool = False, + row: Optional[int] = None, + ) -> None: + super().__init__() + self.media: UnfurledMediaItem = UnfurledMediaItem(media) if isinstance(media, str) else media self.description: Optional[str] = description self.spoiler: bool = spoiler - self._underlying = ThumbnailComponent._raw_construct() + self.row = row + + @property + def width(self): + return 5 @property def type(self) -> Literal[ComponentType.thumbnail]: diff --git a/discord/ui/view.py b/discord/ui/view.py index c2cef2248..08714bf3a 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -685,7 +685,12 @@ class ViewStore: item._view = view item._rendered_row = base_item._rendered_row item._refresh_state(interaction, interaction.data) # type: ignore - +The relative row this text display belongs to. By default + items are arranged automatically into those rows. If you'd + like to control the relative positioning of the row then + passing an index is advised. For example, row=1 will show + up before row=2. Defaults to ``None``, which is automatic + ordering. The row number must be between 0 and 9 (i.e. zero indexed) try: allow = await item.interaction_check(interaction) except Exception: