Retro Bulletin Board Systems on atproto. Web app and TUI. atbbs.xyz
python tui atproto bbs

tui: properly paginate replies

+42 -21
+6
tui/app.tcss
··· 108 108 padding: 1 4; 109 109 } 110 110 111 + .page-status { 112 + color: #8a8a8a; 113 + text-align: center; 114 + margin: 1 0; 115 + } 116 + 111 117 /* home screen */ 112 118 HomeScreen { 113 119 align: center middle;
+36 -21
tui/screens/thread.py
··· 3 3 from textual.binding import Binding 4 4 from textual.containers import VerticalScroll 5 5 from textual.screen import Screen 6 - from textual.widgets import Button, Footer 6 + from textual.widgets import Footer, Static 7 7 8 8 from core import lexicon 9 9 from core.models import AtUri, BBS, Thread ··· 17 17 Binding("escape", "app.pop_screen", "back"), 18 18 Binding("ctrl+e", "reply", "reply"), 19 19 Binding("ctrl+d", "delete", "delete"), 20 + Binding("[", "prev_page", "prev page"), 21 + Binding("]", "next_page", "next page"), 20 22 Binding("ctrl+s", "save_attachment", "save attachments", show=False), 21 23 ] 22 24 ··· 54 56 collection=lexicon.THREAD, 55 57 attachments=self.thread.attachments, 56 58 ) 59 + yield Static("", id="page-status-top", classes="page-status") 60 + yield Static("", id="page-status-bottom", classes="page-status") 57 61 yield Footer() 58 62 59 63 def on_mount(self) -> None: ··· 63 67 pass 64 68 self.load_replies() 65 69 70 + def _update_page_status(self) -> None: 71 + text = f"page {self._page} of {self._total_pages}" if self._total_pages > 1 else "" 72 + self.query_one("#page-status-top", Static).update(text) 73 + self.query_one("#page-status-bottom", Static).update(text) 74 + 75 + def _clear_replies(self) -> None: 76 + for post in self.query(Post): 77 + if post.collection == lexicon.REPLY: 78 + post.remove() 79 + self._replies_map.clear() 80 + 66 81 @work(exclusive=True) 67 82 async def load_replies(self, page: int = 1) -> None: 68 83 client = self.app.http_client ··· 79 94 80 95 self._page = result.page 81 96 self._total_pages = result.total_pages 97 + self._update_page_status() 98 + 82 99 scroll = self.query_one("#thread-scroll") 83 100 84 - # Store for quote lookup 85 101 for r in result.replies: 86 102 self._replies_map[r.uri] = r 87 103 ··· 103 119 collection=lexicon.REPLY, 104 120 attachments=r.attachments, 105 121 quote_text=quote_text, 106 - ) 122 + ), 123 + before=self.query_one("#page-status-bottom"), 107 124 ) 108 125 109 - if self._page < self._total_pages: 110 - await scroll.mount(Button(f"page {self._page + 1} of {self._total_pages} →", id="next-page")) 111 - 112 - def refresh_data(self) -> None: 113 - self._do_refresh() 114 - 115 - @work(exclusive=True) 116 - async def _do_refresh(self) -> None: 117 - for post in self.query(Post): 118 - if post.collection == lexicon.REPLY: 119 - await post.remove() 126 + # Focus first reply 120 127 try: 121 - await self.query_one("#next-page", Button).remove() 128 + replies = [p for p in self.query(Post) if p.collection == lexicon.REPLY] 129 + if replies: 130 + replies[0].focus() 122 131 except Exception: 123 132 pass 124 133 125 - self._replies_map.clear() 134 + def action_next_page(self) -> None: 135 + if self._page < self._total_pages: 136 + self._clear_replies() 137 + self.load_replies(page=self._page + 1) 138 + 139 + def action_prev_page(self) -> None: 140 + if self._page > 1: 141 + self._clear_replies() 142 + self.load_replies(page=self._page - 1) 143 + 144 + def refresh_data(self) -> None: 145 + self._clear_replies() 126 146 self._page = 1 127 147 self.load_replies(page=1) 128 - 129 - def on_button_pressed(self, event: Button.Pressed) -> None: 130 - if event.button.id == "next-page" and self._page < self._total_pages: 131 - event.button.remove() 132 - self.load_replies(page=self._page + 1) 133 148 134 149 def action_reply(self) -> None: 135 150 session = require_session(self)