Skip to content

Commit

Permalink
Merge pull request #59 from sushi-chaaaan/feature/pagination
Browse files Browse the repository at this point in the history
feature: add pagination
  • Loading branch information
sushichan044 authored Oct 24, 2023
2 parents 070e0ab + be9683f commit eb4ceff
Show file tree
Hide file tree
Showing 6 changed files with 256 additions and 1 deletion.
89 changes: 89 additions & 0 deletions examples/paginaton.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# This example requires the 'message_content' privileged intent to function.


import discord
from discord.ext import commands
from ductile import View, ViewObject
from ductile.controller import MessageableController
from ductile.pagination import Paginator
from ductile.ui import Button

PAGES = [
"Page 1",
"Page 2",
"Page 3",
"Page 4",
"Page 5",
]


class Bot(commands.Bot):
def __init__(self) -> None:
super().__init__(command_prefix="!", intents=discord.Intents.all())

async def on_ready(self) -> None:
print(f"Logged in as {self.user}") # noqa: T201
print("Ready!") # noqa: T201


class PaginationView(View):
def __init__(self) -> None:
super().__init__()
# initialize Paginator with source data and configuration.
self.page = Paginator(self, source=PAGES, config={"page_size": 2})

def render(self) -> ViewObject:
# you can define component callback inside render method.
e = discord.Embed(title="Pagination").add_field(
name="page content",
# you can get current page chunk data.
value="\n".join(self.page.data),
)

# Define UI using ViewObject
return ViewObject(
embeds=[e],
components=[
Button(
"<<",
# you can use at_first and at_last property: returns whether the current page is the first/last page.
style={"color": "grey", "disabled": self.page.at_first},
# go_first, go_previous, go_next, go_last methods: go to first/previous/next/last page.
# these will automatically call `View.sync`.
on_click=self.page.go_first,
),
Button(
"<",
style={"color": "grey", "disabled": self.page.at_first},
on_click=self.page.go_previous,
),
Button(
# you can get current page number and max page number.
f"{self.page.current_page}/{self.page.max_page}",
style={"color": "grey", "disabled": True},
),
Button(
">",
style={"color": "grey", "disabled": self.page.at_last},
on_click=self.page.go_next,
),
Button(
">>",
style={"color": "grey", "disabled": self.page.at_last},
on_click=self.page.go_last,
),
],
)


bot = Bot()


@bot.command(name="page")
async def send_counter(ctx: commands.Context) -> None:
# use controller to send and control View.
controller = MessageableController(PaginationView(), messageable=ctx.channel)
await controller.send()


bot.run("MY_COOL_BOT_TOKEN")
3 changes: 2 additions & 1 deletion src/ductile/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from . import controller, types, ui
from . import controller, pagination, types, ui
from .state import State
from .view import View, ViewObject

__all__ = [
"controller",
"pagination",
"types",
"ui",
"State",
Expand Down
6 changes: 6 additions & 0 deletions src/ductile/pagination/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from .page import Paginator, PaginatorConfig

__all__ = [
"Paginator",
"PaginatorConfig",
]
147 changes: 147 additions & 0 deletions src/ductile/pagination/page.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
from typing import TYPE_CHECKING, Generic, TypeVar

from typing_extensions import NotRequired, Required, TypedDict

from ..utils import chunks # noqa: TID252

if TYPE_CHECKING:
from discord import Interaction

from ..view import View # noqa: TID252


_T = TypeVar("_T")


class PaginatorConfig(TypedDict):
"""
A paginator configuration.
Parameters
----------
TypedDict : _type_
_description_
"""

page_size: Required[int]
initial_page: NotRequired[int]


class Paginator(Generic[_T]):
"""
A paginator.
Parameters
----------
view : `View`
The view to attach.
source: `list[_T]`
The source data. This can be any iterable.
config: `PaginatorConfig`
The paginator configuration.
"""

def __init__(self, view: "View", *, source: list[_T], config: PaginatorConfig) -> None:
self.__view = view
self.__CHUNKS = list(chunks(source, config["page_size"]))
self.__MAX_INDEX: int = len(self.__CHUNKS) - 1
self.__current_index: int = c if (self._is_valid_index(c := (config.get("initial_page", 0)))) else 0

@property
def current_page(self) -> int:
"""
Return the current page number.
Returns
-------
int
The current page number.
"""
return self.__current_index + 1

@property
def max_page(self) -> int:
"""
Return the maximum page number.
Returns
-------
int
The maximum page number.
"""
return self.__MAX_INDEX + 1

@property
def at_first(self) -> bool:
"""
Return whether the current page is the first page.
Returns
-------
bool
Whether the current page is the first page.
"""
return self.__current_index == 0

@property
def at_last(self) -> bool:
"""
Return whether the current page is the last page.
Returns
-------
bool
Whether the current page is the last page.
"""
return self.__current_index == self.__MAX_INDEX

def _is_valid_index(self, index: int) -> bool:
return 0 <= index <= self.__MAX_INDEX

def go_next(self, _: "Interaction") -> None:
"""Go to the next page. This method will call `View.sync`."""
next_index = self.__current_index + 1
if not self._is_valid_index(next_index):
return

self.__current_index = next_index
self.__view.sync()

def go_previous(self, _: "Interaction") -> None:
"""Go to the previous page. This method will call `View.sync`."""
previous_index = self.__current_index - 1
if not self._is_valid_index(previous_index):
return

self.__current_index = previous_index
self.__view.sync()

def go_first(self, _: "Interaction") -> None:
"""Go to the first page. This method will call `View.sync`."""
if not self._is_valid_index(self.__current_index) or self.at_first:
return

self.__current_index = 0
self.__view.sync()

def go_last(self, _: "Interaction") -> None:
"""Go to the last page. This method will call `View.sync`."""
if not self._is_valid_index(self.__current_index) or self.at_last:
return

self.__current_index = self.__MAX_INDEX
self.__view.sync()

@property
def data(self) -> list[_T]:
"""
Return the current page data.
Returns
-------
_T
The current page data.
"""
return self.__CHUNKS[self.__current_index]
2 changes: 2 additions & 0 deletions src/ductile/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from .async_helper import get_all_tasks, wait_tasks_by_name
from .call import call_any_function
from .chunk import chunks
from .logger import get_logger
from .type_helper import is_async_func, is_sync_func

__all__ = [
"chunks",
"get_all_tasks",
"wait_tasks_by_name",
"call_any_function",
Expand Down
10 changes: 10 additions & 0 deletions src/ductile/utils/chunk.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from collections.abc import Generator
from typing import TypeVar

_T = TypeVar("_T")


def chunks(iterable: list[_T], size: int) -> Generator[list[_T], None, None]:
"""Yield successive chunks from iterable of size."""
for i in range(0, len(iterable), size):
yield iterable[i : i + size]

0 comments on commit eb4ceff

Please sign in to comment.