Spaces:
Running
Running
| """ | |
| keyboards/inline.py β All inline keyboard builders for the bot UI. | |
| Design principles: | |
| - Folders use a 2-column grid layout. | |
| - Every nested menu has Back and Home buttons. | |
| - Pagination is included for large lists (> ITEMS_PER_PAGE). | |
| - Callback data uses a structured format: "action:param1:param2" | |
| """ | |
| from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton | |
| from aiogram.utils.keyboard import InlineKeyboardBuilder | |
| from config import config | |
| def _back_home_row(back_callback: str, home_callback: str = "home") -> list[InlineKeyboardButton]: | |
| """Returns a standard [π Back] [π Home] button row.""" | |
| return [ | |
| InlineKeyboardButton(text="π Back", callback_data=back_callback), | |
| InlineKeyboardButton(text="π Home", callback_data=home_callback), | |
| ] | |
| # βββββββββββββββββββββββββββ MAIN MENUS βββββββββββββββββββββββββββ | |
| def main_menu_user() -> InlineKeyboardMarkup: | |
| """The main menu for regular users.""" | |
| builder = InlineKeyboardBuilder() | |
| builder.row(InlineKeyboardButton(text="π Browse Knowledge Base", callback_data="browse:root:0")) | |
| builder.row(InlineKeyboardButton(text="βΉοΈ About", callback_data="about")) | |
| return builder.as_markup() | |
| def main_menu_admin(view_as_user: bool = False) -> InlineKeyboardMarkup: | |
| """ | |
| Admin main menu. Includes a toggle to preview as a regular user. | |
| When view_as_user=True, management buttons are hidden. | |
| """ | |
| builder = InlineKeyboardBuilder() | |
| builder.row(InlineKeyboardButton(text="π Browse Knowledge Base", callback_data="browse:root:0")) | |
| if not view_as_user: | |
| # Show full admin controls | |
| builder.row(InlineKeyboardButton(text="π Manage Folders", callback_data="manage:folders:0")) | |
| builder.row(InlineKeyboardButton(text="π Admin Dashboard", callback_data="admin:dashboard")) | |
| builder.row(InlineKeyboardButton(text="ποΈ View as User", callback_data="toggle:user_view")) | |
| else: | |
| # Preview mode β only show the return button | |
| builder.row(InlineKeyboardButton(text="ποΈ Return to Admin View", callback_data="toggle:admin_view")) | |
| return builder.as_markup() | |
| def main_menu_owner(view_as_user: bool = False) -> InlineKeyboardMarkup: | |
| """Owner main menu β extends admin menu with owner-only controls.""" | |
| builder = InlineKeyboardBuilder() | |
| builder.row(InlineKeyboardButton(text="π Browse Knowledge Base", callback_data="browse:root:0")) | |
| if not view_as_user: | |
| builder.row(InlineKeyboardButton(text="π Manage Folders", callback_data="manage:folders:0")) | |
| builder.row( | |
| InlineKeyboardButton(text="π Admin Dashboard", callback_data="admin:dashboard"), | |
| InlineKeyboardButton(text="π Owner Dashboard", callback_data="owner:dashboard"), | |
| ) | |
| builder.row( | |
| InlineKeyboardButton(text="π₯ Manage Admins", callback_data="owner:admins"), | |
| InlineKeyboardButton(text="ποΈ View as User", callback_data="toggle:user_view"), | |
| ) | |
| else: | |
| builder.row(InlineKeyboardButton(text="ποΈ Return to Admin View", callback_data="toggle:admin_view")) | |
| return builder.as_markup() | |
| # βββββββββββββββββββββββββββ FOLDER BROWSING βββββββββββββββββββββββββββ | |
| def folder_list_keyboard( | |
| folders: list, | |
| items: list, | |
| parent_id: int | None, | |
| page: int, | |
| is_admin: bool = False, | |
| is_owner: bool = False, | |
| ) -> InlineKeyboardMarkup: | |
| """ | |
| Builds a paginated keyboard for browsing folders and items inside a folder. | |
| Folders appear in a 2-column grid, items appear as single rows below. | |
| """ | |
| builder = InlineKeyboardBuilder() | |
| per_page = config.ITEMS_PER_PAGE | |
| total_content = folders + items # Combined for pagination | |
| total_pages = max(1, (len(total_content) + per_page - 1) // per_page) | |
| page_content = total_content[page * per_page: (page + 1) * per_page] | |
| # Separate paginated content back into folders and items | |
| page_folders = [c for c in page_content if isinstance(c, type(folders[0])) if folders] if folders else [] | |
| page_items = [c for c in page_content if hasattr(c, 'content_type')] | |
| # Folders in 2-column grid | |
| folder_buttons = [ | |
| InlineKeyboardButton( | |
| text=f"{f.emoji} {f.name}", | |
| callback_data=f"browse:folder:{f.id}:0" | |
| ) | |
| for f in page_folders | |
| ] | |
| # Add folder buttons in pairs (2 per row) | |
| for i in range(0, len(folder_buttons), 2): | |
| row = folder_buttons[i:i+2] | |
| builder.row(*row) | |
| # Items in single rows | |
| content_type_emoji = { | |
| "photo": "πΌοΈ", "video": "π¬", "document": "π", | |
| "audio": "π΅", "link": "π", "text": "π" | |
| } | |
| for item in page_items: | |
| emoji = content_type_emoji.get(item.content_type, "π") | |
| builder.row(InlineKeyboardButton( | |
| text=f"{emoji} {item.title}", | |
| callback_data=f"view:item:{item.id}" | |
| )) | |
| # Admin/Owner action buttons (if not in user-view mode) | |
| if is_admin or is_owner: | |
| folder_param = parent_id if parent_id else "root" | |
| builder.row( | |
| InlineKeyboardButton(text="β Add New Item", callback_data=f"add:start:{folder_param}"), | |
| InlineKeyboardButton(text="βοΈ Edit Folder", callback_data=f"edit:folder:{parent_id}" if parent_id else "home"), | |
| ) | |
| if parent_id: # Can only delete non-root folders | |
| builder.row(InlineKeyboardButton( | |
| text="ποΈ Delete Folder", | |
| callback_data=f"delete:folder:{parent_id}" | |
| )) | |
| # Pagination row | |
| nav_buttons = [] | |
| if page > 0: | |
| nav_buttons.append(InlineKeyboardButton(text="β¬ οΈ Prev", callback_data=f"browse:folder:{parent_id or 'root'}:{page-1}")) | |
| if page < total_pages - 1: | |
| nav_buttons.append(InlineKeyboardButton(text="β‘οΈ Next", callback_data=f"browse:folder:{parent_id or 'root'}:{page+1}")) | |
| if nav_buttons: | |
| builder.row(*nav_buttons) | |
| # Back and Home navigation | |
| if parent_id is not None: | |
| builder.row(*_back_home_row(back_callback="browse:root:0")) | |
| else: | |
| builder.row(InlineKeyboardButton(text="π Home", callback_data="home")) | |
| return builder.as_markup() | |
| def item_view_keyboard(item_id: int, folder_id: int, is_admin: bool = False) -> InlineKeyboardMarkup: | |
| """Keyboard shown when a user views a specific item.""" | |
| builder = InlineKeyboardBuilder() | |
| if is_admin: | |
| builder.row( | |
| InlineKeyboardButton(text="βοΈ Edit", callback_data=f"edit:item:{item_id}"), | |
| InlineKeyboardButton(text="ποΈ Delete", callback_data=f"delete:item:{item_id}"), | |
| ) | |
| builder.row(*_back_home_row(back_callback=f"browse:folder:{folder_id}:0")) | |
| return builder.as_markup() | |
| # βββββββββββββββββββββββββββ ADMIN MANAGEMENT βββββββββββββββββββββββββββ | |
| def add_item_type_keyboard(folder_id: int | str) -> InlineKeyboardMarkup: | |
| """Asks whether to add a Folder or Content item.""" | |
| builder = InlineKeyboardBuilder() | |
| builder.row( | |
| InlineKeyboardButton(text="π New Folder", callback_data=f"add:folder:{folder_id}"), | |
| InlineKeyboardButton(text="π New Content", callback_data=f"add:content:{folder_id}"), | |
| ) | |
| builder.row(InlineKeyboardButton(text="π Cancel", callback_data="home")) | |
| return builder.as_markup() | |
| def content_type_keyboard(folder_id: int | str) -> InlineKeyboardMarkup: | |
| """Lets admin choose the type of content to add.""" | |
| builder = InlineKeyboardBuilder() | |
| builder.row( | |
| InlineKeyboardButton(text="πΌοΈ Photo", callback_data=f"ctype:photo:{folder_id}"), | |
| InlineKeyboardButton(text="π¬ Video", callback_data=f"ctype:video:{folder_id}"), | |
| ) | |
| builder.row( | |
| InlineKeyboardButton(text="π Document", callback_data=f"ctype:document:{folder_id}"), | |
| InlineKeyboardButton(text="π΅ Audio", callback_data=f"ctype:audio:{folder_id}"), | |
| ) | |
| builder.row( | |
| InlineKeyboardButton(text="π Link", callback_data=f"ctype:link:{folder_id}"), | |
| InlineKeyboardButton(text="π Text", callback_data=f"ctype:text:{folder_id}"), | |
| ) | |
| builder.row(InlineKeyboardButton(text="π Cancel", callback_data="home")) | |
| return builder.as_markup() | |
| def confirm_delete_keyboard(entity_type: str, entity_id: int) -> InlineKeyboardMarkup: | |
| """Confirmation dialog before deleting a folder or item.""" | |
| builder = InlineKeyboardBuilder() | |
| builder.row( | |
| InlineKeyboardButton( | |
| text="β Yes, Delete", | |
| callback_data=f"confirm_delete:{entity_type}:{entity_id}" | |
| ), | |
| InlineKeyboardButton(text="β Cancel", callback_data="home"), | |
| ) | |
| return builder.as_markup() | |
| def admin_list_keyboard(admins: list, page: int = 0) -> InlineKeyboardMarkup: | |
| """Lists all admins with a remove button for each.""" | |
| builder = InlineKeyboardBuilder() | |
| per_page = 5 | |
| total_pages = max(1, (len(admins) + per_page - 1) // per_page) | |
| page_admins = admins[page * per_page: (page + 1) * per_page] | |
| for admin in page_admins: | |
| name = admin.full_name or admin.username or f"ID:{admin.user_id}" | |
| builder.row(InlineKeyboardButton( | |
| text=f"π€ {name}", | |
| callback_data=f"owner:admin_info:{admin.user_id}" | |
| )) | |
| # Pagination | |
| nav = [] | |
| if page > 0: | |
| nav.append(InlineKeyboardButton(text="β¬ οΈ", callback_data=f"owner:admins_page:{page-1}")) | |
| if page < total_pages - 1: | |
| nav.append(InlineKeyboardButton(text="β‘οΈ", callback_data=f"owner:admins_page:{page+1}")) | |
| if nav: | |
| builder.row(*nav) | |
| builder.row(InlineKeyboardButton(text="β Add Admin", callback_data="owner:add_admin")) | |
| builder.row(InlineKeyboardButton(text="π Home", callback_data="home")) | |
| return builder.as_markup() | |
| def admin_info_keyboard(admin_user_id: int) -> InlineKeyboardMarkup: | |
| """Shows options for a specific admin.""" | |
| builder = InlineKeyboardBuilder() | |
| builder.row(InlineKeyboardButton( | |
| text="ποΈ Remove Admin", | |
| callback_data=f"owner:remove_admin:{admin_user_id}" | |
| )) | |
| builder.row(*_back_home_row(back_callback="owner:admins")) | |
| return builder.as_markup() | |
| def back_home_keyboard(back_callback: str = "home") -> InlineKeyboardMarkup: | |
| """Simple back + home keyboard for generic use.""" | |
| builder = InlineKeyboardBuilder() | |
| builder.row(*_back_home_row(back_callback=back_callback)) | |
| return builder.as_markup() |