Refactor queue view and add pagination utilities
Replaced ListView with QueueView for queue/history display, introducing improved pagination and modal support via new utilities in views/utils/. Updated imports and references in cogs/basic.py and views/__init__.py. Removed views/list.py and added views/queue.py, views/utils/pagination.py, and views/utils/modal.py for modular pagination and modal handling.
This commit is contained in:
@@ -42,7 +42,7 @@ from function import (
|
||||
|
||||
from voicelink import SearchType, LoopType
|
||||
from addons import LYRICS_PLATFORMS
|
||||
from views import SearchView, ListView, LinkView, LyricsView, HelpView
|
||||
from views import SearchView, QueueView, LinkView, LyricsView, HelpView
|
||||
from validators import url
|
||||
|
||||
async def nowplay(ctx: commands.Context, player: voicelink.Player):
|
||||
@@ -493,7 +493,7 @@ class Basic(commands.Cog):
|
||||
|
||||
if player.queue.is_empty:
|
||||
return await nowplay(ctx, player)
|
||||
view = ListView(player=player, author=ctx.author)
|
||||
view = QueueView(player=player, author=ctx.author)
|
||||
view.response = await send(ctx, await view.build_embed(), view=view)
|
||||
|
||||
@queue.command(name="export", aliases=get_aliases("export"))
|
||||
@@ -577,7 +577,7 @@ class Basic(commands.Cog):
|
||||
if not player.queue.history():
|
||||
return await nowplay(ctx, player)
|
||||
|
||||
view = ListView(player=player, author=ctx.author, is_queue=False)
|
||||
view = QueueView(player=player, author=ctx.author, is_queue=False)
|
||||
view.response = await send(ctx, await view.build_embed(), view=view)
|
||||
|
||||
@commands.hybrid_command(name="leave", aliases=get_aliases("leave"))
|
||||
|
||||
@@ -30,7 +30,7 @@ class ButtonOnCooldown(commands.CommandError):
|
||||
from .controller import InteractiveController
|
||||
from .search import SearchView
|
||||
from .help import HelpView
|
||||
from .list import ListView
|
||||
from .queue import QueueView
|
||||
from .lyrics import LyricsView
|
||||
from .playlist import PlaylistView
|
||||
from .inbox import InboxView
|
||||
|
||||
@@ -27,7 +27,7 @@ from discord.ext import commands
|
||||
import function as func
|
||||
|
||||
class HelpDropdown(discord.ui.Select):
|
||||
def __init__(self, categories:list):
|
||||
def __init__(self, categories: list[str]) -> None:
|
||||
self.view: HelpView
|
||||
|
||||
super().__init__(
|
||||
|
||||
120
views/list.py
120
views/list.py
@@ -1,120 +0,0 @@
|
||||
"""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
|
||||
import function as func
|
||||
|
||||
from math import ceil
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from voicelink import Player, Track
|
||||
|
||||
class ListView(discord.ui.View):
|
||||
def __init__(
|
||||
self,
|
||||
player: "Player",
|
||||
author: discord.Member,
|
||||
is_queue: bool = True
|
||||
) -> None:
|
||||
super().__init__(timeout=60)
|
||||
self.player: Player = player
|
||||
|
||||
self.is_queue: bool = is_queue
|
||||
self.tracks: list[Track] = player.queue.tracks() if is_queue else player.queue.history()
|
||||
self.response: discord.Message = None
|
||||
|
||||
if not is_queue:
|
||||
self.tracks.reverse()
|
||||
self.author: discord.Member = author
|
||||
|
||||
self.page: int = ceil(len(self.tracks) / 7)
|
||||
self.current_page: int = 1
|
||||
|
||||
try:
|
||||
self.time: str = func.time(sum([track.length for track in self.tracks]))
|
||||
except Exception as _:
|
||||
self.time = "∞"
|
||||
|
||||
async def on_timeout(self) -> None:
|
||||
for child in self.children:
|
||||
child.disabled = True
|
||||
try:
|
||||
await self.response.edit(view=self)
|
||||
except:
|
||||
pass
|
||||
|
||||
async def on_error(self, error, item, interaction) -> None:
|
||||
return
|
||||
|
||||
async def interaction_check(self, interaction: discord.Interaction) -> bool:
|
||||
return interaction.user == self.author
|
||||
|
||||
async def build_embed(self) -> discord.Embed:
|
||||
offset: int = self.current_page * 7
|
||||
tracks: list[Track] = self.tracks[(offset-7):offset]
|
||||
texts = await func.get_lang(self.author.guild.id, "viewTitle", "viewDesc", "nowplayingDesc", "live", "queueTitle", "historyTitle", "viewFooter")
|
||||
|
||||
embed = discord.Embed(title=texts[0], color=func.settings.embed_color)
|
||||
embed.description=texts[1].format(self.player.current.uri, f"```{self.player.current.title}```") if self.player.current else texts[2].format("None")
|
||||
|
||||
embed.description += "\n**" + (texts[4] if self.is_queue else texts[5]) + "**\n" + "\n".join([
|
||||
f"{track.emoji} `{i:>2}.` `[" + (texts[3] if track.is_stream else func.time(track.length)) + f']` [{func.truncate_string(track.title)}]({track.uri})' + (track.requester.mention)
|
||||
for i, track in enumerate(tracks, start=offset-6)
|
||||
])
|
||||
embed.set_footer(text=texts[6].format(self.current_page, self.page, self.time))
|
||||
|
||||
return embed
|
||||
|
||||
@discord.ui.button(label='<<', style=discord.ButtonStyle.grey)
|
||||
async def fast_back_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
|
||||
if self.current_page != 1:
|
||||
self.current_page = 1
|
||||
return await interaction.response.edit_message(embed=await self.build_embed())
|
||||
await interaction.response.defer()
|
||||
|
||||
@discord.ui.button(label='Back', style=discord.ButtonStyle.blurple)
|
||||
async def back_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
|
||||
if self.current_page > 1:
|
||||
self.current_page -= 1
|
||||
return await interaction.response.edit_message(embed=await self.build_embed())
|
||||
await interaction.response.defer()
|
||||
|
||||
@discord.ui.button(label='Next', style=discord.ButtonStyle.blurple)
|
||||
async def next_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
|
||||
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()
|
||||
|
||||
@discord.ui.button(label='>>', style=discord.ButtonStyle.grey)
|
||||
async def fast_next_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
|
||||
if self.current_page != self.page:
|
||||
self.current_page = self.page
|
||||
return await interaction.response.edit_message(embed=await self.build_embed())
|
||||
await interaction.response.defer()
|
||||
|
||||
@discord.ui.button(emoji='🗑️', style=discord.ButtonStyle.red)
|
||||
async def stop_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
|
||||
await self.response.delete()
|
||||
self.stop()
|
||||
199
views/queue.py
Normal file
199
views/queue.py
Normal file
@@ -0,0 +1,199 @@
|
||||
"""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
|
||||
import function as func
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .utils import Pagination, BaseModal
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from voicelink import Player, Track
|
||||
|
||||
|
||||
class QueueView(discord.ui.View):
|
||||
"""
|
||||
A Discord UI view for displaying and interacting with a paginated list of tracks.
|
||||
|
||||
Attributes:
|
||||
player (Player): The player containing the track queue or history.
|
||||
author (discord.Member): The member who initiated the view.
|
||||
is_queue (bool): Indicates if the list is a queue or history.
|
||||
pagination (Pagination[Track]): Manages pagination of track lists.
|
||||
response (discord.Message): The message containing the view.
|
||||
total_duration (str): The total duration of the tracks.
|
||||
"""
|
||||
|
||||
def __init__(self, player: "Player", author: discord.Member, is_queue: bool = True) -> None:
|
||||
super().__init__(timeout=60)
|
||||
self.player: Player = player
|
||||
self.author: discord.Member = author
|
||||
self.is_queue: bool = is_queue
|
||||
self.response: discord.Message = None
|
||||
|
||||
self.pagination = Pagination["Track"](
|
||||
items=player.queue.tracks() if is_queue else list(reversed(player.queue.history())),
|
||||
page_size=7,
|
||||
)
|
||||
|
||||
self.total_duration = self.calculate_total_duration()
|
||||
self.update_view()
|
||||
|
||||
def calculate_total_duration(self) -> str:
|
||||
"""Calculate the total duration of the tracks."""
|
||||
try:
|
||||
return func.time(sum(track.length for track in self.pagination._items))
|
||||
except Exception:
|
||||
return "∞"
|
||||
|
||||
def format_description(self, tracks: list["Track"], texts: list[str]) -> str:
|
||||
"""Format the description for the embed based on current tracks."""
|
||||
now_playing = (
|
||||
texts[1].format(self.player.current.uri,
|
||||
f"```{self.player.current.title}```")
|
||||
if self.player.current else texts[2].format("None")
|
||||
)
|
||||
|
||||
track_list = "\n".join([
|
||||
f"{track.emoji} `{i:>2}.` `[{texts[3] if track.is_stream else func.time(track.length)}]` "
|
||||
f"[{func.truncate_string(track.title)}]({track.uri}) {track.requester.mention}"
|
||||
for i, track in enumerate(tracks, start=self.pagination.start_index + 1)
|
||||
])
|
||||
|
||||
return f"{now_playing}\n**{texts[4] if self.is_queue else texts[5]}**\n{track_list}"
|
||||
|
||||
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 on_timeout(self) -> None:
|
||||
"""Disable all buttons when the view times out."""
|
||||
for child in self.children:
|
||||
child.disabled = True
|
||||
try:
|
||||
await self.response.edit(view=self)
|
||||
except discord.HTTPException:
|
||||
pass
|
||||
|
||||
async def interaction_check(self, interaction: discord.Interaction) -> bool:
|
||||
"""Ensure only the author of the view can interact with it."""
|
||||
return interaction.user == self.author
|
||||
|
||||
async def build_embed(self) -> discord.Embed:
|
||||
"""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,
|
||||
"viewTitle",
|
||||
"viewDesc",
|
||||
"nowplayingDesc",
|
||||
"live",
|
||||
"queueTitle",
|
||||
"historyTitle",
|
||||
"viewFooter",
|
||||
)
|
||||
|
||||
embed = discord.Embed(title=texts[0], color=func.settings.embed_color)
|
||||
embed.description = self.format_description(tracks, texts)
|
||||
|
||||
embed.set_footer(
|
||||
text=texts[6].format(
|
||||
self.pagination.current_page,
|
||||
self.pagination.total_pages,
|
||||
self.total_duration,
|
||||
)
|
||||
)
|
||||
|
||||
return embed
|
||||
|
||||
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.response.id, embed=await self.build_embed(), view=self)
|
||||
else:
|
||||
await interaction.response.edit_message(embed=await self.build_embed(), view=self)
|
||||
|
||||
@discord.ui.button(label='<<', custom_id="fast_back")
|
||||
async def fast_back_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
|
||||
"""Jump to the first page."""
|
||||
self.pagination.go_page(0)
|
||||
await self.update_and_edit_message(interaction)
|
||||
|
||||
@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:
|
||||
"""Go to the previous page if it exists."""
|
||||
self.pagination.go_back()
|
||||
await self.update_and_edit_message(interaction)
|
||||
|
||||
@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:
|
||||
"""Go to the next page if it exists."""
|
||||
self.pagination.go_next()
|
||||
await self.update_and_edit_message(interaction)
|
||||
|
||||
@discord.ui.button(label='>>', custom_id="fast_next")
|
||||
async def fast_next_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
|
||||
"""Jump to the last page."""
|
||||
self.pagination.go_page(self.pagination.total_pages - 1)
|
||||
await self.update_and_edit_message(interaction)
|
||||
25
views/utils/__init__.py
Normal file
25
views/utils/__init__.py
Normal file
@@ -0,0 +1,25 @@
|
||||
"""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.
|
||||
"""
|
||||
|
||||
from .pagination import Pagination
|
||||
from .modal import BaseModal
|
||||
44
views/utils/modal.py
Normal file
44
views/utils/modal.py
Normal file
@@ -0,0 +1,44 @@
|
||||
"""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 BaseModal(discord.ui.Modal):
|
||||
def __init__(self, title: str, items: list[discord.ui.Item], timeout: float = None, custom_id: str = None) -> None:
|
||||
super().__init__(title=title, timeout=timeout, custom_id=custom_id)
|
||||
self._add_items(items)
|
||||
self.values: dict[str, str] = {}
|
||||
|
||||
def _add_items(self, items: list[discord.ui.Item]) -> None:
|
||||
"""Add items to the modal."""
|
||||
for item in items:
|
||||
self.add_item(item)
|
||||
|
||||
async def on_submit(self, interaction: discord.Interaction) -> None:
|
||||
"""Handle the modal submission."""
|
||||
await interaction.response.defer()
|
||||
|
||||
for item in self.walk_children():
|
||||
if isinstance(item, discord.ui.TextInput):
|
||||
self.values[item.custom_id] = item.value
|
||||
131
views/utils/pagination.py
Normal file
131
views/utils/pagination.py
Normal file
@@ -0,0 +1,131 @@
|
||||
"""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.
|
||||
"""
|
||||
|
||||
from typing import List, Generic, TypeVar
|
||||
from math import ceil
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class Pagination(Generic[T]):
|
||||
"""
|
||||
A class to manage pagination for a list of items.
|
||||
|
||||
Attributes:
|
||||
items (List[T]): The list of items to paginate.
|
||||
page_size (int): The number of items per page.
|
||||
current_page (int): The current page index (0-based).
|
||||
total_pages (int): The total number of pages.
|
||||
"""
|
||||
|
||||
def __init__(self, items: List[T], page_size: int):
|
||||
"""
|
||||
Initializes the Pagination class.
|
||||
|
||||
Args:
|
||||
items (List[T]): The list of items to paginate.
|
||||
page_size (int): The number of items per page.
|
||||
|
||||
Raises:
|
||||
ValueError: If page_size is less than or equal to 0.
|
||||
"""
|
||||
if page_size <= 0:
|
||||
raise ValueError("page_size must be greater than 0")
|
||||
|
||||
self._items: List[T] = items
|
||||
self._page_size: int = page_size
|
||||
self._current_page: int = 0
|
||||
self.total_pages: int = ceil(len(items) / page_size)
|
||||
|
||||
def get_current_page_items(self) -> List[T]:
|
||||
"""
|
||||
Retrieves the items for the current page.
|
||||
|
||||
Returns:
|
||||
List[T]: The items on the current page.
|
||||
"""
|
||||
return self._items[self.start_index:self.end_index]
|
||||
|
||||
def go_back(self) -> None:
|
||||
"""Moves to the previous page if available."""
|
||||
if self.has_previous_page:
|
||||
self._current_page -= 1
|
||||
|
||||
def go_next(self) -> None:
|
||||
"""Moves to the next page if available."""
|
||||
if self.has_next_page:
|
||||
self._current_page += 1
|
||||
|
||||
def go_page(self, page_number: int) -> None:
|
||||
"""Navigate to a specific page, clamped between 0 and total_pages."""
|
||||
self._current_page = max(0, min(page_number, self.total_pages - 1))
|
||||
|
||||
@property
|
||||
def has_next_page(self) -> bool:
|
||||
"""
|
||||
Checks if there is a next page.
|
||||
|
||||
Returns:
|
||||
bool: True if there is a next page, False otherwise.
|
||||
"""
|
||||
return self._current_page < self.total_pages - 1
|
||||
|
||||
@property
|
||||
def has_previous_page(self) -> bool:
|
||||
"""
|
||||
Checks if there is a previous page.
|
||||
|
||||
Returns:
|
||||
bool: True if there is a previous page, False otherwise.
|
||||
"""
|
||||
return self._current_page > 0
|
||||
|
||||
@property
|
||||
def start_index(self) -> int:
|
||||
"""
|
||||
Gets the start index of the items for the current page.
|
||||
|
||||
Returns:
|
||||
int: The start index (0-based) of the current page.
|
||||
"""
|
||||
return self._current_page * self._page_size
|
||||
|
||||
@property
|
||||
def end_index(self) -> int:
|
||||
"""
|
||||
Gets the end index of the items for the current page.
|
||||
|
||||
Returns:
|
||||
int: The end index (exclusive) of the current page.
|
||||
"""
|
||||
return min(self.start_index + self._page_size, len(self._items))
|
||||
|
||||
@property
|
||||
def current_page(self) -> int:
|
||||
"""
|
||||
Returns the current page number (1-based).
|
||||
|
||||
Returns:
|
||||
int: The current page number, starting from 1.
|
||||
"""
|
||||
return self._current_page + 1
|
||||
Reference in New Issue
Block a user