Refactorización. Casi finalizado a falta de testeo profundo

This commit is contained in:
Jesús Pavón Abián
2026-02-01 14:57:17 +01:00
parent ca3ee06738
commit de10c927d9
3 changed files with 234 additions and 12 deletions

View File

@@ -12,6 +12,7 @@ Se está siguiendo `falta.md` por orden. Los puntos 1 a 8 y 10-11 están marcado
- Autocompletado añadido al diálogo de acciones de usuario. - Autocompletado añadido al diálogo de acciones de usuario.
- Atajos de teclado (&) añadidos a botones del perfil. - Atajos de teclado (&) añadidos a botones del perfil.
- Persistencia de búsquedas implementada (se guardan y restauran al reiniciar). - Persistencia de búsquedas implementada (se guardan y restauran al reiniciar).
- Paginación completa en todos los buffers: HomeTimeline, FollowingTimeline, NotificationBuffer, LikesBuffer, MentionsBuffer, SentBuffer, UserTimeline, SearchBuffer.
## Cambios anteriores ## Cambios anteriores
- Activado autocompletado en el diálogo "Ver timeline..." y validación de usuario. - Activado autocompletado en el diálogo "Ver timeline..." y validación de usuario.
@@ -21,11 +22,13 @@ Se está siguiendo `falta.md` por orden. Los puntos 1 a 8 y 10-11 están marcado
- Menús: para Bluesky, las opciones no aplicables se ocultan usando el sentinel "HIDE". - Menús: para Bluesky, las opciones no aplicables se ocultan usando el sentinel "HIDE".
## Puntos pendientes ## Puntos pendientes
- 9) Paginación en timelines principales (home, notifications, user timelines, search) - parcial. Ninguno. Todos los puntos de falta.md están completados.
## Notas técnicas ## Notas técnicas
- `update_menus` en `src/controller/mainController.py` interpreta `"HIDE"` para ocultar entradas. - `update_menus` en `src/controller/mainController.py` interpreta `"HIDE"` para ocultar entradas.
- Buffers de Reposts/Likes usan `PostUserListBuffer` con cursor para paginación. - Buffers de Reposts/Likes usan `PostUserListBuffer` con cursor para paginación.
- Las búsquedas ahora se guardan en `session.settings["other_buffers"]["searches"]`. - Las búsquedas ahora se guardan en `session.settings["other_buffers"]["searches"]`.
- Perfil de usuario descarga imágenes en thread separado para no bloquear UI. - Perfil de usuario descarga imágenes en thread separado para no bloquear UI.
- Paginación usa patrón: `self.next_cursor` guardado en `start_stream()`, usado en `get_more_items()`.
- El menú "load_previous_items" activa `get_more_items()` en el buffer actual.

View File

@@ -51,7 +51,7 @@ Hecho. La terminología es consistente: Bluesky usa "repost/like" (nativo AT Pro
## 9) Paginación en listados ## 9) Paginación en listados
- Bluesky: implementada en Reposts/Likes y Followers/Following. - Bluesky: implementada en Reposts/Likes y Followers/Following.
- Faltan otros listados equivalentes (por ejemplo, búsquedas de usuarios si se implementan). - Faltan otros listados equivalentes (por ejemplo, búsquedas de usuarios si se implementan).
Parcial. Paginación implementada en buffers de usuarios (followers/following/likes/reposts). Pendiente en timelines principales. Hecho. Paginación implementada en todos los buffers: HomeTimeline, FollowingTimeline, NotificationBuffer, LikesBuffer, MentionsBuffer, SentBuffer, UserTimeline, SearchBuffer, FollowersBuffer, FollowingBuffer, PostUserListBuffer.
## 10) Accesibilidad/teclado ## 10) Accesibilidad/teclado
- Verificar atajos en todos los nuevos diálogos/buffers. - Verificar atajos en todos los nuevos diálogos/buffers.

View File

@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import logging import logging
import output
from .base import BaseBuffer from .base import BaseBuffer
from wxUI.buffers.blueski import panels as BlueskiPanels from wxUI.buffers.blueski import panels as BlueskiPanels
from pubsub import pub from pubsub import pub
@@ -11,7 +12,8 @@ class HomeTimeline(BaseBuffer):
super(HomeTimeline, self).__init__(*args, **kwargs) super(HomeTimeline, self).__init__(*args, **kwargs)
self.type = "home_timeline" self.type = "home_timeline"
self.feed_uri = None self.feed_uri = None
self.next_cursor = None
def create_buffer(self, parent, name): def create_buffer(self, parent, name):
# Override to use HomePanel # Override to use HomePanel
self.buffer = BlueskiPanels.HomePanel(parent, name) self.buffer = BlueskiPanels.HomePanel(parent, name)
@@ -22,13 +24,13 @@ class HomeTimeline(BaseBuffer):
try: try:
count = self.session.settings["general"].get("max_posts_per_call", 50) count = self.session.settings["general"].get("max_posts_per_call", 50)
except: pass except: pass
api = self.session._ensure_client() api = self.session._ensure_client()
# Discover Logic # Discover Logic
if not self.feed_uri: if not self.feed_uri:
self.feed_uri = self._resolve_discover_feed(api) self.feed_uri = self._resolve_discover_feed(api)
items = [] items = []
try: try:
res = None res = None
@@ -38,16 +40,40 @@ class HomeTimeline(BaseBuffer):
else: else:
# Fallback to standard timeline # Fallback to standard timeline
res = api.app.bsky.feed.get_timeline({"limit": count}) res = api.app.bsky.feed.get_timeline({"limit": count})
feed = getattr(res, "feed", []) feed = getattr(res, "feed", [])
items = list(feed) items = list(feed)
self.next_cursor = getattr(res, "cursor", None)
except Exception: except Exception:
log.exception("Failed to fetch home timeline") log.exception("Failed to fetch home timeline")
return 0 return 0
return self.process_items(items, play_sound) return self.process_items(items, play_sound)
def get_more_items(self):
if not self.next_cursor:
return
count = 50
try:
count = self.session.settings["general"].get("max_posts_per_call", 50)
except:
pass
api = self.session._ensure_client()
try:
if self.feed_uri:
res = api.app.bsky.feed.get_feed({"feed": self.feed_uri, "limit": count, "cursor": self.next_cursor})
else:
res = api.app.bsky.feed.get_timeline({"limit": count, "cursor": self.next_cursor})
feed = getattr(res, "feed", [])
items = list(feed)
self.next_cursor = getattr(res, "cursor", None)
added = self.process_items(items, play_sound=False)
if added:
output.speak(_(u"%s items retrieved") % (str(added)), True)
except Exception:
log.exception("Error fetching more home timeline items")
def _resolve_discover_feed(self, api): def _resolve_discover_feed(self, api):
# Reuse logic from panels.py # Reuse logic from panels.py
try: try:
@@ -76,7 +102,8 @@ class FollowingTimeline(BaseBuffer):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(FollowingTimeline, self).__init__(*args, **kwargs) super(FollowingTimeline, self).__init__(*args, **kwargs)
self.type = "following_timeline" self.type = "following_timeline"
self.next_cursor = None
def create_buffer(self, parent, name): def create_buffer(self, parent, name):
self.buffer = BlueskiPanels.HomePanel(parent, name) # Reuse HomePanel layout self.buffer = BlueskiPanels.HomePanel(parent, name) # Reuse HomePanel layout
self.buffer.session = self.session self.buffer.session = self.session
@@ -85,19 +112,40 @@ class FollowingTimeline(BaseBuffer):
count = 50 count = 50
try: count = self.session.settings["general"].get("max_posts_per_call", 50) try: count = self.session.settings["general"].get("max_posts_per_call", 50)
except: pass except: pass
api = self.session._ensure_client() api = self.session._ensure_client()
try: try:
# Force reverse-chronological # Force reverse-chronological
res = api.app.bsky.feed.get_timeline({"limit": count, "algorithm": "reverse-chronological"}) res = api.app.bsky.feed.get_timeline({"limit": count, "algorithm": "reverse-chronological"})
feed = getattr(res, "feed", []) feed = getattr(res, "feed", [])
items = list(feed) items = list(feed)
self.next_cursor = getattr(res, "cursor", None)
except Exception: except Exception:
log.exception("Error fetching following timeline") log.exception("Error fetching following timeline")
return 0 return 0
return self.process_items(items, play_sound) return self.process_items(items, play_sound)
def get_more_items(self):
if not self.next_cursor:
return
count = 50
try:
count = self.session.settings["general"].get("max_posts_per_call", 50)
except:
pass
api = self.session._ensure_client()
try:
res = api.app.bsky.feed.get_timeline({"limit": count, "algorithm": "reverse-chronological", "cursor": self.next_cursor})
feed = getattr(res, "feed", [])
items = list(feed)
self.next_cursor = getattr(res, "cursor", None)
added = self.process_items(items, play_sound=False)
if added:
output.speak(_(u"%s items retrieved") % (str(added)), True)
except Exception:
log.exception("Error fetching more following timeline items")
class NotificationBuffer(BaseBuffer): class NotificationBuffer(BaseBuffer):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
# Override compose_func before calling super().__init__ # Override compose_func before calling super().__init__
@@ -105,6 +153,7 @@ class NotificationBuffer(BaseBuffer):
super(NotificationBuffer, self).__init__(*args, **kwargs) super(NotificationBuffer, self).__init__(*args, **kwargs)
self.type = "notifications" self.type = "notifications"
self.sound = "notification_received.ogg" self.sound = "notification_received.ogg"
self.next_cursor = None
def create_buffer(self, parent, name): def create_buffer(self, parent, name):
self.buffer = BlueskiPanels.NotificationPanel(parent, name) self.buffer = BlueskiPanels.NotificationPanel(parent, name)
@@ -124,6 +173,7 @@ class NotificationBuffer(BaseBuffer):
try: try:
res = api.app.bsky.notification.list_notifications({"limit": count}) res = api.app.bsky.notification.list_notifications({"limit": count})
notifications = getattr(res, "notifications", []) notifications = getattr(res, "notifications", [])
self.next_cursor = getattr(res, "cursor", None)
if not notifications: if not notifications:
return 0 return 0
@@ -134,6 +184,27 @@ class NotificationBuffer(BaseBuffer):
log.exception("Error fetching Bluesky notifications") log.exception("Error fetching Bluesky notifications")
return 0 return 0
def get_more_items(self):
if not self.next_cursor:
return
count = 50
try:
count = self.session.settings["general"].get("max_posts_per_call", 50)
except:
pass
api = self.session._ensure_client()
if not api:
return
try:
res = api.app.bsky.notification.list_notifications({"limit": count, "cursor": self.next_cursor})
notifications = getattr(res, "notifications", [])
self.next_cursor = getattr(res, "cursor", None)
added = self.process_items(list(notifications), play_sound=False)
if added:
output.speak(_(u"%s items retrieved") % (str(added)), True)
except Exception:
log.exception("Error fetching more notifications")
def add_new_item(self, notification): def add_new_item(self, notification):
"""Add a single new notification from streaming/polling.""" """Add a single new notification from streaming/polling."""
return self.process_items([notification], play_sound=True) return self.process_items([notification], play_sound=True)
@@ -207,6 +278,7 @@ class LikesBuffer(BaseBuffer):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(LikesBuffer, self).__init__(*args, **kwargs) super(LikesBuffer, self).__init__(*args, **kwargs)
self.type = "likes" self.type = "likes"
self.next_cursor = None
def create_buffer(self, parent, name): def create_buffer(self, parent, name):
self.buffer = BlueskiPanels.HomePanel(parent, name) self.buffer = BlueskiPanels.HomePanel(parent, name)
@@ -223,12 +295,34 @@ class LikesBuffer(BaseBuffer):
try: try:
res = api.app.bsky.feed.get_actor_likes({"actor": api.me.did, "limit": count}) res = api.app.bsky.feed.get_actor_likes({"actor": api.me.did, "limit": count})
items = getattr(res, "feed", None) or getattr(res, "items", None) or [] items = getattr(res, "feed", None) or getattr(res, "items", None) or []
self.next_cursor = getattr(res, "cursor", None)
except Exception: except Exception:
log.exception("Error fetching likes") log.exception("Error fetching likes")
return 0 return 0
return self.process_items(list(items), play_sound) return self.process_items(list(items), play_sound)
def get_more_items(self):
if not self.next_cursor:
return
count = 50
try:
count = self.session.settings["general"].get("max_posts_per_call", 50)
except:
pass
api = self.session._ensure_client()
if not api:
return
try:
res = api.app.bsky.feed.get_actor_likes({"actor": api.me.did, "limit": count, "cursor": self.next_cursor})
items = getattr(res, "feed", None) or getattr(res, "items", None) or []
self.next_cursor = getattr(res, "cursor", None)
added = self.process_items(list(items), play_sound=False)
if added:
output.speak(_(u"%s items retrieved") % (str(added)), True)
except Exception:
log.exception("Error fetching more likes")
class MentionsBuffer(BaseBuffer): class MentionsBuffer(BaseBuffer):
"""Buffer for mentions and replies to the current user.""" """Buffer for mentions and replies to the current user."""
@@ -239,6 +333,7 @@ class MentionsBuffer(BaseBuffer):
super(MentionsBuffer, self).__init__(*args, **kwargs) super(MentionsBuffer, self).__init__(*args, **kwargs)
self.type = "mentions" self.type = "mentions"
self.sound = "mention_received.ogg" self.sound = "mention_received.ogg"
self.next_cursor = None
def create_buffer(self, parent, name): def create_buffer(self, parent, name):
self.buffer = BlueskiPanels.NotificationPanel(parent, name) self.buffer = BlueskiPanels.NotificationPanel(parent, name)
@@ -258,6 +353,7 @@ class MentionsBuffer(BaseBuffer):
try: try:
res = api.app.bsky.notification.list_notifications({"limit": count}) res = api.app.bsky.notification.list_notifications({"limit": count})
notifications = getattr(res, "notifications", []) notifications = getattr(res, "notifications", [])
self.next_cursor = getattr(res, "cursor", None)
if not notifications: if not notifications:
return 0 return 0
@@ -276,6 +372,33 @@ class MentionsBuffer(BaseBuffer):
log.exception("Error fetching Bluesky mentions") log.exception("Error fetching Bluesky mentions")
return 0 return 0
def get_more_items(self):
if not self.next_cursor:
return
count = 50
try:
count = self.session.settings["general"].get("max_posts_per_call", 50)
except:
pass
api = self.session._ensure_client()
if not api:
return
try:
res = api.app.bsky.notification.list_notifications({"limit": count, "cursor": self.next_cursor})
notifications = getattr(res, "notifications", [])
self.next_cursor = getattr(res, "cursor", None)
# Filter only mentions and replies
mentions = [
n for n in notifications
if getattr(n, "reason", "") in ("mention", "reply", "quote")
]
if mentions:
added = self.process_items(mentions, play_sound=False)
if added:
output.speak(_(u"%s items retrieved") % (str(added)), True)
except Exception:
log.exception("Error fetching more mentions")
def add_new_item(self, notification): def add_new_item(self, notification):
"""Add a single new mention from streaming/polling.""" """Add a single new mention from streaming/polling."""
reason = getattr(notification, "reason", "") reason = getattr(notification, "reason", "")
@@ -290,6 +413,7 @@ class SentBuffer(BaseBuffer):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(SentBuffer, self).__init__(*args, **kwargs) super(SentBuffer, self).__init__(*args, **kwargs)
self.type = "sent" self.type = "sent"
self.next_cursor = None
def create_buffer(self, parent, name): def create_buffer(self, parent, name):
self.buffer = BlueskiPanels.HomePanel(parent, name) self.buffer = BlueskiPanels.HomePanel(parent, name)
@@ -314,6 +438,7 @@ class SentBuffer(BaseBuffer):
"filter": "posts_no_replies" "filter": "posts_no_replies"
}) })
items = getattr(res, "feed", []) items = getattr(res, "feed", [])
self.next_cursor = getattr(res, "cursor", None)
if not items: if not items:
return 0 return 0
@@ -324,6 +449,32 @@ class SentBuffer(BaseBuffer):
log.exception("Error fetching sent posts") log.exception("Error fetching sent posts")
return 0 return 0
def get_more_items(self):
if not self.next_cursor:
return
count = 50
try:
count = self.session.settings["general"].get("max_posts_per_call", 50)
except:
pass
api = self.session._ensure_client()
if not api or not api.me:
return
try:
res = api.app.bsky.feed.get_author_feed({
"actor": api.me.did,
"limit": count,
"filter": "posts_no_replies",
"cursor": self.next_cursor
})
items = getattr(res, "feed", [])
self.next_cursor = getattr(res, "cursor", None)
added = self.process_items(list(items), play_sound=False)
if added:
output.speak(_(u"%s items retrieved") % (str(added)), True)
except Exception:
log.exception("Error fetching more sent posts")
class UserTimeline(BaseBuffer): class UserTimeline(BaseBuffer):
"""Buffer for posts by a specific user.""" """Buffer for posts by a specific user."""
@@ -333,6 +484,8 @@ class UserTimeline(BaseBuffer):
self.handle = kwargs.get("handle") self.handle = kwargs.get("handle")
super(UserTimeline, self).__init__(*args, **kwargs) super(UserTimeline, self).__init__(*args, **kwargs)
self.type = "user_timeline" self.type = "user_timeline"
self.next_cursor = None
self._resolved_actor = None
def create_buffer(self, parent, name): def create_buffer(self, parent, name):
self.buffer = BlueskiPanels.HomePanel(parent, name) self.buffer = BlueskiPanels.HomePanel(parent, name)
@@ -372,17 +525,44 @@ class UserTimeline(BaseBuffer):
actor = did actor = did
except Exception: except Exception:
pass pass
self._resolved_actor = actor
res = api.app.bsky.feed.get_author_feed({ res = api.app.bsky.feed.get_author_feed({
"actor": actor, "actor": actor,
"limit": count, "limit": count,
}) })
items = getattr(res, "feed", []) or [] items = getattr(res, "feed", []) or []
self.next_cursor = getattr(res, "cursor", None)
except Exception: except Exception:
log.exception("Error fetching user timeline") log.exception("Error fetching user timeline")
return 0 return 0
return self.process_items(list(items), play_sound) return self.process_items(list(items), play_sound)
def get_more_items(self):
if not self.next_cursor or not self._resolved_actor:
return
count = 50
try:
count = self.session.settings["general"].get("max_posts_per_call", 50)
except:
pass
api = self.session._ensure_client()
if not api:
return
try:
res = api.app.bsky.feed.get_author_feed({
"actor": self._resolved_actor,
"limit": count,
"cursor": self.next_cursor
})
items = getattr(res, "feed", []) or []
self.next_cursor = getattr(res, "cursor", None)
added = self.process_items(list(items), play_sound=False)
if added:
output.speak(_(u"%s items retrieved") % (str(added)), True)
except Exception:
log.exception("Error fetching more user timeline items")
def remove_buffer(self, force=False): def remove_buffer(self, force=False):
if not force: if not force:
from wxUI import commonMessageDialogs from wxUI import commonMessageDialogs
@@ -419,6 +599,7 @@ class SearchBuffer(BaseBuffer):
self.search_query = kwargs.pop("query", "") self.search_query = kwargs.pop("query", "")
super(SearchBuffer, self).__init__(*args, **kwargs) super(SearchBuffer, self).__init__(*args, **kwargs)
self.type = "search" self.type = "search"
self.next_cursor = None
def create_buffer(self, parent, name): def create_buffer(self, parent, name):
self.buffer = BlueskiPanels.HomePanel(parent, name) self.buffer = BlueskiPanels.HomePanel(parent, name)
@@ -445,6 +626,7 @@ class SearchBuffer(BaseBuffer):
"limit": count "limit": count
}) })
posts = getattr(res, "posts", []) posts = getattr(res, "posts", [])
self.next_cursor = getattr(res, "cursor", None)
if not posts: if not posts:
return 0 return 0
@@ -459,6 +641,31 @@ class SearchBuffer(BaseBuffer):
log.exception("Error searching Bluesky posts") log.exception("Error searching Bluesky posts")
return 0 return 0
def get_more_items(self):
if not self.next_cursor or not self.search_query:
return
count = 50
try:
count = self.session.settings["general"].get("max_posts_per_call", 50)
except:
pass
api = self.session._ensure_client()
if not api:
return
try:
res = api.app.bsky.feed.search_posts({
"q": self.search_query,
"limit": count,
"cursor": self.next_cursor
})
posts = getattr(res, "posts", [])
self.next_cursor = getattr(res, "cursor", None)
added = self.process_items(list(posts), play_sound=False)
if added:
output.speak(_(u"%s items retrieved") % (str(added)), True)
except Exception:
log.exception("Error fetching more search results")
def remove_buffer(self, force=False): def remove_buffer(self, force=False):
"""Search buffers can always be removed.""" """Search buffers can always be removed."""
if not force: if not force:
@@ -471,4 +678,16 @@ class SearchBuffer(BaseBuffer):
self.session.db.pop(self.name, None) self.session.db.pop(self.name, None)
except Exception: except Exception:
pass pass
# Also remove from saved searches
try:
searches = self.session.settings["other_buffers"].get("searches")
if searches:
if isinstance(searches, str):
searches = [s for s in searches.split(",") if s]
if self.search_query in searches:
searches.remove(self.search_query)
self.session.settings["other_buffers"]["searches"] = searches
self.session.settings.write()
except Exception:
log.exception("Error updating saved searches")
return True return True