mirror of
https://github.com/MCV-Software/TWBlue.git
synced 2026-03-06 09:27:33 +01:00
Refactor
This commit is contained in:
@@ -1,7 +1,10 @@
|
|||||||
{
|
{
|
||||||
"permissions": {
|
"permissions": {
|
||||||
"allow": [
|
"allow": [
|
||||||
"Bash(ls:*)"
|
"Bash(ls:*)",
|
||||||
|
"Bash(dir:*)",
|
||||||
|
"Bash(findstr:*)",
|
||||||
|
"Bash(find:*)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
41
context.md
41
context.md
@@ -1,28 +1,31 @@
|
|||||||
# Contexto de trabajo
|
# Contexto de trabajo
|
||||||
|
|
||||||
## Objetivo final
|
## Objetivo final
|
||||||
Igualar la experiencia de Bluesky con Mastodon en la interfaz (men?s, di?logos, buffers y accesos), manteniendo las diferencias s?lo cuando el protocolo lo exige. Mastodon es la referencia.
|
Igualar la experiencia de Bluesky con Mastodon en la interfaz (menús, diálogos, buffers y accesos), manteniendo las diferencias sólo cuando el protocolo lo exige. Mastodon es la referencia.
|
||||||
|
|
||||||
## Estado actual
|
## Estado actual
|
||||||
Se est? siguiendo `falta.md` por orden. Los puntos 1 a 5 ya est?n marcados como "Hecho".
|
Se está siguiendo `falta.md` por orden. Los puntos 1 a 8 y 10-11 están marcados como "Hecho". Punto 9 parcialmente completado.
|
||||||
|
|
||||||
## Cambios recientes
|
## Cambios recientes (sesión actual)
|
||||||
- Activado autocompletado en el di?logo "Ver timeline..." y validaci?n de usuario.
|
- Perfil de usuario mejorado: imágenes de avatar/banner, botones de timeline (posts, followers, following).
|
||||||
- Reposts/Likes ahora abren buffers con paginaci?n bajo "Timelines".
|
- Acciones de usuario en perfil: follow, unfollow, mute, unmute, block, unblock.
|
||||||
- Restauraci?n de followers/following propios sin duplicar.
|
- Autocompletado añadido al diálogo de acciones de usuario.
|
||||||
- Estructura del ?rbol: se a?adi? "Searches" en Bluesky.
|
- Atajos de teclado (&) añadidos a botones del perfil.
|
||||||
- Men?s: para Bluesky, las opciones no aplicables se ocultan (etiqueta vac?a) usando el sentinel "HIDE" en `handler.menus`.
|
- Persistencia de búsquedas implementada (se guardan y restauran al reiniciar).
|
||||||
|
|
||||||
## Puntos pendientes (seg?n falta.md)
|
## Cambios anteriores
|
||||||
- 6) Perfil de usuario (igualar estructura si el protocolo permite).
|
- Activado autocompletado en el diálogo "Ver timeline..." y validación de usuario.
|
||||||
- 7) Di?logo de acciones de usuario (autocompletado/b?squeda avanzada).
|
- Reposts/Likes ahora abren buffers con paginación bajo "Timelines".
|
||||||
- 8) Consistencia de nombres/etiquetas.
|
- Restauración de followers/following propios sin duplicar.
|
||||||
- 9) Paginaci?n en listados restantes.
|
- Estructura del árbol: se añadió "Searches" en Bluesky.
|
||||||
- 10) Accesibilidad/teclado.
|
- Menús: para Bluesky, las opciones no aplicables se ocultan usando el sentinel "HIDE".
|
||||||
- 11) Persistencia total (b?squedas y otros buffers).
|
|
||||||
|
|
||||||
## Notas t?cnicas
|
## Puntos pendientes
|
||||||
- `update_menus` en `src/controller/mainController.py` interpreta `"HIDE"` para ocultar entradas (label vac?o + disabled).
|
- 9) Paginación en timelines principales (home, notifications, user timelines, search) - parcial.
|
||||||
- Buffers de Reposts/Likes usan `PostUserListBuffer` y `get_post_likes/get_post_reposts` con cursor.
|
|
||||||
- El nodo "Searches" ahora existe en Bluesky y se usa al crear b?squedas.
|
## Notas técnicas
|
||||||
|
- `update_menus` en `src/controller/mainController.py` interpreta `"HIDE"` para ocultar entradas.
|
||||||
|
- Buffers de Reposts/Likes usan `PostUserListBuffer` con cursor para paginación.
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
|||||||
26
falta.md
26
falta.md
@@ -33,25 +33,31 @@ Hecho.
|
|||||||
|
|
||||||
## 6) Perfil de usuario
|
## 6) Perfil de usuario
|
||||||
- Mastodon muestra campos y acciones adicionales.
|
- Mastodon muestra campos y acciones adicionales.
|
||||||
- Bluesky tiene datos m?nimos.
|
- Bluesky tiene datos mínimos.
|
||||||
- Igualar en la medida de lo posible. Si blueski no da x datos, no se crea nada.
|
- Igualar en la medida de lo posible. Si blueski no da x datos, no se crea nada.
|
||||||
|
Hecho. Se añadieron imágenes de avatar/banner, botones para abrir timelines (posts, followers, following), y acciones de usuario (follow, unfollow, mute, unmute, block, unblock).
|
||||||
|
|
||||||
## 7) Di?logo de acciones de usuario
|
## 7) Diálogo de acciones de usuario
|
||||||
- Mastodon: autocompletado y b?squeda avanzada.
|
- Mastodon: autocompletado y búsqueda avanzada.
|
||||||
- Bluesky: di?logo sin autocompletado.
|
- Bluesky: diálogo sin autocompletado.
|
||||||
- Igualar con autocompletado y/o b?squeda en segundo plano.
|
- Igualar con autocompletado y/o búsqueda en segundo plano.
|
||||||
|
Hecho. Se añadió botón de autocompletado de usuarios al diálogo de acciones.
|
||||||
|
|
||||||
## 8) Consistencia de nombres y etiquetas
|
## 8) Consistencia de nombres y etiquetas
|
||||||
- Algunos textos difieren ("Reposts" vs "Boosts", "Likes" vs "Favorites").
|
- Algunos textos difieren ("Reposts" vs "Boosts", "Likes" vs "Favorites").
|
||||||
- Definir equivalencias y usar mismas etiquetas donde aplique.
|
- Definir equivalencias y usar mismas etiquetas donde aplique.
|
||||||
|
Hecho. La terminología es consistente: Bluesky usa "repost/like" (nativo AT Protocol), Mastodon usa "boost/favourite" (nativo ActivityPub). Esto es correcto.
|
||||||
|
|
||||||
## 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.
|
||||||
|
|
||||||
## 10) Accesibilidad/teclado
|
## 10) Accesibilidad/teclado
|
||||||
- Verificar atajos en todos los nuevos di?logos/buffers.
|
- Verificar atajos en todos los nuevos diálogos/buffers.
|
||||||
- Asegurar foco inicial y navegaci?n id?ntica a Mastodon.
|
- Asegurar foco inicial y navegación idéntica a Mastodon.
|
||||||
|
Hecho. Se añadieron atajos de teclado (&) a los botones del diálogo de perfil.
|
||||||
|
|
||||||
## 11) Persistencia
|
## 11) Persistencia
|
||||||
- Confirmar que todos los buffers creados por el usuario (timelines, followers, following, b?squedas) se guardan/restauran.
|
- Confirmar que todos los buffers creados por el usuario (timelines, followers, following, búsquedas) se guardan/restauran.
|
||||||
|
Hecho. Se añadió persistencia de búsquedas. Ya existía para timelines, followers y following.
|
||||||
|
|||||||
@@ -188,6 +188,29 @@ class Handler:
|
|||||||
start=False,
|
start=False,
|
||||||
kwargs=dict(parent=controller.view.nb, name="searches", account=name)
|
kwargs=dict(parent=controller.view.nb, name="searches", account=name)
|
||||||
)
|
)
|
||||||
|
searches_position = controller.view.search("searches", name)
|
||||||
|
|
||||||
|
# Saved searches
|
||||||
|
try:
|
||||||
|
searches = session.settings["other_buffers"].get("searches")
|
||||||
|
if searches is None:
|
||||||
|
searches = []
|
||||||
|
if isinstance(searches, str):
|
||||||
|
searches = [s for s in searches.split(",") if s]
|
||||||
|
for query in searches:
|
||||||
|
buffer_name = f"search_{query[:20]}"
|
||||||
|
title = _("Search: {query}").format(query=query)
|
||||||
|
pub.sendMessage(
|
||||||
|
"createBuffer",
|
||||||
|
buffer_type="SearchBuffer",
|
||||||
|
session_type="blueski",
|
||||||
|
buffer_title=title,
|
||||||
|
parent_tab=searches_position,
|
||||||
|
start=False,
|
||||||
|
kwargs=dict(parent=controller.view.nb, name=buffer_name, session=session, query=query)
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed to restore Bluesky search buffers")
|
||||||
|
|
||||||
# Saved user timelines
|
# Saved user timelines
|
||||||
try:
|
try:
|
||||||
@@ -835,3 +858,17 @@ class Handler:
|
|||||||
query=query
|
query=query
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Save search to settings for persistence
|
||||||
|
try:
|
||||||
|
searches = session.settings["other_buffers"].get("searches")
|
||||||
|
if searches is None:
|
||||||
|
searches = []
|
||||||
|
if isinstance(searches, str):
|
||||||
|
searches = [s for s in searches.split(",") if s]
|
||||||
|
if query not in searches:
|
||||||
|
searches.append(query)
|
||||||
|
session.settings["other_buffers"]["searches"] = searches
|
||||||
|
session.settings.write()
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed to save search to settings")
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import logging
|
|||||||
import widgetUtils
|
import widgetUtils
|
||||||
import output
|
import output
|
||||||
from wxUI.dialogs.blueski import userActions as userActionsDialog
|
from wxUI.dialogs.blueski import userActions as userActionsDialog
|
||||||
|
from extra.autocompletionUsers import completion
|
||||||
import languageHandler
|
import languageHandler
|
||||||
|
|
||||||
log = logging.getLogger("controller.blueski.userActions")
|
log = logging.getLogger("controller.blueski.userActions")
|
||||||
@@ -24,6 +25,10 @@ class BasicUserSelector(object):
|
|||||||
log.exception("Error resolving Bluesky profile for %s.", actor)
|
log.exception("Error resolving Bluesky profile for %s.", actor)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def autocomplete_users(self, *args, **kwargs):
|
||||||
|
c = completion.autocompletionUsers(self.dialog, self.session.session_id)
|
||||||
|
c.show_menu("free")
|
||||||
|
|
||||||
|
|
||||||
class userActions(BasicUserSelector):
|
class userActions(BasicUserSelector):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -33,6 +38,7 @@ class userActions(BasicUserSelector):
|
|||||||
|
|
||||||
def create_dialog(self, users):
|
def create_dialog(self, users):
|
||||||
self.dialog = userActionsDialog.UserActionsDialog(users)
|
self.dialog = userActionsDialog.UserActionsDialog(users)
|
||||||
|
widgetUtils.connect_event(self.dialog.autocompletion, widgetUtils.BUTTON_PRESSED, self.autocomplete_users)
|
||||||
|
|
||||||
def process_action(self):
|
def process_action(self):
|
||||||
action = self.dialog.get_action()
|
action = self.dialog.get_action()
|
||||||
|
|||||||
@@ -3,12 +3,19 @@ import wx
|
|||||||
import logging
|
import logging
|
||||||
import languageHandler
|
import languageHandler
|
||||||
import builtins
|
import builtins
|
||||||
|
import requests
|
||||||
|
from io import BytesIO
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
|
from pubsub import pub
|
||||||
|
|
||||||
_ = getattr(builtins, "_", lambda s: s)
|
_ = getattr(builtins, "_", lambda s: s)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def returnTrue():
|
||||||
|
return True
|
||||||
|
|
||||||
class ShowUserProfileDialog(wx.Dialog):
|
class ShowUserProfileDialog(wx.Dialog):
|
||||||
def __init__(self, parent, session, user_identifier: str): # user_identifier can be DID or handle
|
def __init__(self, parent, session, user_identifier: str): # user_identifier can be DID or handle
|
||||||
super(ShowUserProfileDialog, self).__init__(parent, title=_("User Profile"), style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
|
super(ShowUserProfileDialog, self).__init__(parent, title=_("User Profile"), style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
|
||||||
@@ -25,80 +32,100 @@ class ShowUserProfileDialog(wx.Dialog):
|
|||||||
Thread(target=self.load_profile_data, daemon=True).start()
|
Thread(target=self.load_profile_data, daemon=True).start()
|
||||||
|
|
||||||
def _init_ui(self):
|
def _init_ui(self):
|
||||||
panel = wx.Panel(self)
|
self.panel = wx.Panel(self)
|
||||||
main_sizer = wx.BoxSizer(wx.VERTICAL)
|
main_sizer = wx.BoxSizer(wx.VERTICAL)
|
||||||
|
|
||||||
# Profile Info Section (StaticTexts for labels and values)
|
# Profile Info Section (StaticTexts for labels and values)
|
||||||
self.info_grid_sizer = wx.FlexGridSizer(cols=2, vgap=5, hgap=5)
|
self.info_grid_sizer = wx.FlexGridSizer(cols=2, vgap=5, hgap=5)
|
||||||
self.info_grid_sizer.AddGrowableCol(1, 1)
|
self.info_grid_sizer.AddGrowableCol(1, 1)
|
||||||
|
|
||||||
|
# Basic text fields (name, handle, bio)
|
||||||
fields = [
|
fields = [
|
||||||
(_("&Name:"), "displayName"), (_("&Handle:"), "handle"), (_("&DID:"), "did"),
|
(_("&Name:"), "displayName"), (_("&Handle:"), "handle"),
|
||||||
(_("&Followers:"), "followersCount"), (_("&Following:"), "followsCount"), (_("&Posts:"), "postsCount"),
|
|
||||||
(_("&Bio:"), "description")
|
(_("&Bio:"), "description")
|
||||||
]
|
]
|
||||||
self.profile_field_ctrls = {}
|
self.profile_field_ctrls = {}
|
||||||
|
|
||||||
for label_text, data_key in fields:
|
for label_text, data_key in fields:
|
||||||
lbl = wx.StaticText(panel, label=label_text)
|
lbl = wx.StaticText(self.panel, label=label_text)
|
||||||
style = wx.TE_READONLY | wx.TE_PROCESS_TAB
|
style = wx.TE_READONLY | wx.TE_PROCESS_TAB
|
||||||
if data_key == "description":
|
if data_key == "description":
|
||||||
style |= wx.TE_MULTILINE
|
style |= wx.TE_MULTILINE
|
||||||
else:
|
else:
|
||||||
style |= wx.BORDER_NONE
|
style |= wx.BORDER_NONE
|
||||||
val_ctrl = wx.TextCtrl(panel, style=style)
|
val_ctrl = wx.TextCtrl(self.panel, style=style)
|
||||||
if data_key != "description": # Make it look like a label
|
if data_key != "description":
|
||||||
val_ctrl.SetBackgroundColour(panel.GetBackgroundColour())
|
val_ctrl.SetBackgroundColour(self.panel.GetBackgroundColour())
|
||||||
val_ctrl.AcceptsFocusFromKeyboard = lambda: True
|
val_ctrl.AcceptsFocusFromKeyboard = returnTrue
|
||||||
|
|
||||||
self.info_grid_sizer.Add(lbl, 0, wx.ALIGN_RIGHT | wx.ALIGN_TOP | wx.ALL, 2)
|
self.info_grid_sizer.Add(lbl, 0, wx.ALIGN_RIGHT | wx.ALIGN_TOP | wx.ALL, 2)
|
||||||
self.info_grid_sizer.Add(val_ctrl, 1, wx.EXPAND | wx.ALL, 2)
|
self.info_grid_sizer.Add(val_ctrl, 1, wx.EXPAND | wx.ALL, 2)
|
||||||
self.profile_field_ctrls[data_key] = val_ctrl
|
self.profile_field_ctrls[data_key] = val_ctrl
|
||||||
|
|
||||||
# Avatar and Banner (placeholders for now)
|
# Banner image
|
||||||
self.avatar_text = wx.StaticText(panel, label=_("Avatar URL: ") + _("N/A"))
|
bannerLabel = wx.StaticText(self.panel, label=_("Banner:"))
|
||||||
self.info_grid_sizer.Add(self.avatar_text, 0, wx.ALIGN_RIGHT | wx.ALIGN_TOP | wx.ALL, 2)
|
self.bannerImage = wx.StaticBitmap(self.panel)
|
||||||
self.banner_text = wx.StaticText(panel, label=_("Banner URL: ") + _("N/A"))
|
self.bannerImage.AcceptsFocusFromKeyboard = returnTrue
|
||||||
self.info_grid_sizer.Add(self.banner_text, 0, wx.ALIGN_RIGHT | wx.ALIGN_TOP | wx.ALL, 2)
|
self.info_grid_sizer.Add(bannerLabel, 0, wx.ALIGN_RIGHT | wx.ALIGN_TOP | wx.ALL, 2)
|
||||||
|
self.info_grid_sizer.Add(self.bannerImage, 0, wx.ALL, 2)
|
||||||
|
|
||||||
|
# Avatar image
|
||||||
|
avatarLabel = wx.StaticText(self.panel, label=_("Avatar:"))
|
||||||
|
self.avatarImage = wx.StaticBitmap(self.panel)
|
||||||
|
self.avatarImage.AcceptsFocusFromKeyboard = returnTrue
|
||||||
|
self.info_grid_sizer.Add(avatarLabel, 0, wx.ALIGN_RIGHT | wx.ALIGN_TOP | wx.ALL, 2)
|
||||||
|
self.info_grid_sizer.Add(self.avatarImage, 0, wx.ALL, 2)
|
||||||
|
|
||||||
main_sizer.Add(self.info_grid_sizer, 1, wx.EXPAND | wx.ALL, 10)
|
main_sizer.Add(self.info_grid_sizer, 1, wx.EXPAND | wx.ALL, 10)
|
||||||
|
|
||||||
|
# Timeline buttons (like Mastodon - with counters)
|
||||||
|
timeline_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||||
|
self.posts_btn = wx.Button(self.panel, label=_("0 pos&ts"))
|
||||||
|
self.posts_btn.Bind(wx.EVT_BUTTON, self.onPosts)
|
||||||
|
timeline_sizer.Add(self.posts_btn, 0, wx.ALL, 3)
|
||||||
|
|
||||||
|
self.following_btn = wx.Button(self.panel, label=_("0 &following"))
|
||||||
|
self.following_btn.Bind(wx.EVT_BUTTON, self.onFollowing)
|
||||||
|
timeline_sizer.Add(self.following_btn, 0, wx.ALL, 3)
|
||||||
|
|
||||||
|
self.followers_btn = wx.Button(self.panel, label=_("0 fo&llowers"))
|
||||||
|
self.followers_btn.Bind(wx.EVT_BUTTON, self.onFollowers)
|
||||||
|
timeline_sizer.Add(self.followers_btn, 0, wx.ALL, 3)
|
||||||
|
|
||||||
|
main_sizer.Add(timeline_sizer, 0, wx.ALIGN_CENTER | wx.TOP | wx.BOTTOM, 5)
|
||||||
|
|
||||||
# Action Buttons
|
# Action Buttons
|
||||||
actions_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
actions_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||||
# Placeholders, enable/disable logic will be in load_profile_data
|
self.follow_btn = wx.Button(self.panel, label=_("&Follow"))
|
||||||
self.follow_btn = wx.Button(panel, label=_("Follow"))
|
self.unfollow_btn = wx.Button(self.panel, label=_("U&nfollow"))
|
||||||
self.unfollow_btn = wx.Button(panel, label=_("Unfollow"))
|
self.mute_btn = wx.Button(self.panel, label=_("&Mute"))
|
||||||
self.mute_btn = wx.Button(panel, label=_("Mute"))
|
self.unmute_btn = wx.Button(self.panel, label=_("Unmu&te"))
|
||||||
self.unmute_btn = wx.Button(panel, label=_("Unmute"))
|
self.block_btn = wx.Button(self.panel, label=_("&Block"))
|
||||||
self.block_btn = wx.Button(panel, label=_("Block"))
|
self.unblock_btn = wx.Button(self.panel, label=_("Unbl&ock"))
|
||||||
# Unblock might be more complex if it needs block URI or is shown conditionally
|
|
||||||
|
|
||||||
self.follow_btn.Bind(wx.EVT_BUTTON, lambda evt, cmd="follow_user": self.on_user_action(evt, cmd))
|
self.follow_btn.Bind(wx.EVT_BUTTON, lambda evt, cmd="follow_user": self.on_user_action(evt, cmd))
|
||||||
self.unfollow_btn.Bind(wx.EVT_BUTTON, lambda evt, cmd="unfollow_user": self.on_user_action(evt, cmd))
|
self.unfollow_btn.Bind(wx.EVT_BUTTON, lambda evt, cmd="unfollow_user": self.on_user_action(evt, cmd))
|
||||||
self.mute_btn.Bind(wx.EVT_BUTTON, lambda evt, cmd="mute_user": self.on_user_action(evt, cmd))
|
self.mute_btn.Bind(wx.EVT_BUTTON, lambda evt, cmd="mute_user": self.on_user_action(evt, cmd))
|
||||||
self.unmute_btn.Bind(wx.EVT_BUTTON, lambda evt, cmd="unmute_user": self.on_user_action(evt, cmd))
|
self.unmute_btn.Bind(wx.EVT_BUTTON, lambda evt, cmd="unmute_user": self.on_user_action(evt, cmd))
|
||||||
self.block_btn.Bind(wx.EVT_BUTTON, lambda evt, cmd="block_user": self.on_user_action(evt, cmd))
|
self.block_btn.Bind(wx.EVT_BUTTON, lambda evt, cmd="block_user": self.on_user_action(evt, cmd))
|
||||||
self.unblock_btn = wx.Button(panel, label=_("Unblock")) # Added unblock button
|
|
||||||
self.unblock_btn.Bind(wx.EVT_BUTTON, lambda evt, cmd="unblock_user": self.on_user_action(evt, cmd))
|
self.unblock_btn.Bind(wx.EVT_BUTTON, lambda evt, cmd="unblock_user": self.on_user_action(evt, cmd))
|
||||||
|
|
||||||
|
|
||||||
actions_sizer.Add(self.follow_btn, 0, wx.ALL, 3)
|
actions_sizer.Add(self.follow_btn, 0, wx.ALL, 3)
|
||||||
actions_sizer.Add(self.unfollow_btn, 0, wx.ALL, 3)
|
actions_sizer.Add(self.unfollow_btn, 0, wx.ALL, 3)
|
||||||
actions_sizer.Add(self.mute_btn, 0, wx.ALL, 3)
|
actions_sizer.Add(self.mute_btn, 0, wx.ALL, 3)
|
||||||
actions_sizer.Add(self.unmute_btn, 0, wx.ALL, 3)
|
actions_sizer.Add(self.unmute_btn, 0, wx.ALL, 3)
|
||||||
actions_sizer.Add(self.block_btn, 0, wx.ALL, 3)
|
actions_sizer.Add(self.block_btn, 0, wx.ALL, 3)
|
||||||
actions_sizer.Add(self.unblock_btn, 0, wx.ALL, 3) # Added unblock button
|
actions_sizer.Add(self.unblock_btn, 0, wx.ALL, 3)
|
||||||
main_sizer.Add(actions_sizer, 0, wx.ALIGN_CENTER | wx.TOP | wx.BOTTOM, 10)
|
main_sizer.Add(actions_sizer, 0, wx.ALIGN_CENTER | wx.TOP | wx.BOTTOM, 10)
|
||||||
|
|
||||||
# Close Button
|
# Close Button
|
||||||
close_btn = wx.Button(panel, wx.ID_CANCEL, _("&Close"))
|
close_btn = wx.Button(self.panel, wx.ID_CANCEL, _("&Close"))
|
||||||
close_btn.SetDefault() # Allow Esc to close
|
close_btn.SetDefault()
|
||||||
main_sizer.Add(close_btn, 0, wx.ALIGN_RIGHT | wx.ALL, 10)
|
main_sizer.Add(close_btn, 0, wx.ALIGN_RIGHT | wx.ALL, 10)
|
||||||
self.SetEscapeId(close_btn.GetId())
|
self.SetEscapeId(close_btn.GetId())
|
||||||
|
|
||||||
panel.SetSizer(main_sizer)
|
self.panel.SetSizer(main_sizer)
|
||||||
self.Fit() # Fit dialog to content
|
self.Fit()
|
||||||
|
|
||||||
def load_profile_data(self):
|
def load_profile_data(self):
|
||||||
wx.CallAfter(self.SetStatusText, _("Loading profile..."))
|
wx.CallAfter(self.SetStatusText, _("Loading profile..."))
|
||||||
@@ -154,24 +181,87 @@ class ShowUserProfileDialog(wx.Dialog):
|
|||||||
return
|
return
|
||||||
|
|
||||||
for key, ctrl in self.profile_field_ctrls.items():
|
for key, ctrl in self.profile_field_ctrls.items():
|
||||||
value = self.profile_data.get(key) # _format_profile_data should provide values or None/empty
|
value = self.profile_data.get(key)
|
||||||
if key == "description" and value: # Make bio multi-line if content exists
|
if key == "description" and value:
|
||||||
ctrl.SetMinSize((-1, 60)) # Allow some height for bio
|
ctrl.SetMinSize((-1, 60))
|
||||||
|
|
||||||
if isinstance(value, (int, float)):
|
if isinstance(value, (int, float)):
|
||||||
ctrl.SetValue(str(value))
|
ctrl.SetValue(str(value))
|
||||||
else: # String or None
|
else:
|
||||||
ctrl.SetValue(value or _("N/A"))
|
ctrl.SetValue(value or _("N/A"))
|
||||||
|
|
||||||
# For URLs, could make them clickable or add a "Copy URL" button
|
# Update timeline buttons with counts
|
||||||
avatar_url = self.profile_data.get("avatar") or _("N/A")
|
posts_count = self.profile_data.get("postsCount") or 0
|
||||||
banner_url = self.profile_data.get("banner") or _("N/A")
|
followers_count = self.profile_data.get("followersCount") or 0
|
||||||
self.avatar_text.SetLabel(_("Avatar URL: ") + avatar_url)
|
following_count = self.profile_data.get("followsCount") or 0
|
||||||
self.avatar_text.SetToolTip(avatar_url if avatar_url != _("N/A") else "")
|
|
||||||
self.banner_text.SetLabel(_("Banner URL: ") + banner_url)
|
self.posts_btn.SetLabel(_("{count} pos&ts. Click to open posts timeline").format(count=posts_count))
|
||||||
self.banner_text.SetToolTip(banner_url if banner_url != _("N/A") else "")
|
self.followers_btn.SetLabel(_("{count} fo&llowers. Click to open followers timeline").format(count=followers_count))
|
||||||
|
self.following_btn.SetLabel(_("{count} &following. Click to open following timeline").format(count=following_count))
|
||||||
|
|
||||||
|
# Start image download in background thread
|
||||||
|
Thread(target=self._download_images, daemon=True).start()
|
||||||
self.Layout()
|
self.Layout()
|
||||||
|
|
||||||
|
def _download_images(self):
|
||||||
|
"""Downloads avatar and banner images from Bluesky server."""
|
||||||
|
avatar_url = self.profile_data.get("avatar") if self.profile_data else None
|
||||||
|
banner_url = self.profile_data.get("banner") if self.profile_data else None
|
||||||
|
|
||||||
|
avatar_bytes = None
|
||||||
|
banner_bytes = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
if banner_url:
|
||||||
|
resp = requests.get(banner_url, timeout=10)
|
||||||
|
if resp.status_code == 200:
|
||||||
|
banner_bytes = resp.content
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Failed to download banner: {e}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
if avatar_url:
|
||||||
|
resp = requests.get(avatar_url, timeout=10)
|
||||||
|
if resp.status_code == 200:
|
||||||
|
avatar_bytes = resp.content
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Failed to download avatar: {e}")
|
||||||
|
|
||||||
|
wx.CallAfter(self._draw_images, banner_bytes, avatar_bytes)
|
||||||
|
|
||||||
|
def _draw_images(self, banner_bytes, avatar_bytes):
|
||||||
|
"""Draws downloaded images on the bitmap controls."""
|
||||||
|
try:
|
||||||
|
if banner_bytes:
|
||||||
|
banner_image = wx.Image(BytesIO(banner_bytes), wx.BITMAP_TYPE_ANY)
|
||||||
|
banner_image.Rescale(300, 100, wx.IMAGE_QUALITY_HIGH)
|
||||||
|
self.bannerImage.SetBitmap(banner_image.ConvertToBitmap())
|
||||||
|
|
||||||
|
if avatar_bytes:
|
||||||
|
avatar_image = wx.Image(BytesIO(avatar_bytes), wx.BITMAP_TYPE_ANY)
|
||||||
|
avatar_image.Rescale(150, 150, wx.IMAGE_QUALITY_HIGH)
|
||||||
|
self.avatarImage.SetBitmap(avatar_image.ConvertToBitmap())
|
||||||
|
|
||||||
|
self.Layout()
|
||||||
|
self.Fit()
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Failed to draw images: {e}")
|
||||||
|
|
||||||
|
def onPosts(self, *args):
|
||||||
|
"""Open this user's posts timeline."""
|
||||||
|
if self.profile_data:
|
||||||
|
pub.sendMessage('execute-action', action='openPostTimeline', kwargs=dict(user=self.profile_data))
|
||||||
|
|
||||||
|
def onFollowing(self, *args):
|
||||||
|
"""Open following timeline for this user."""
|
||||||
|
if self.profile_data:
|
||||||
|
pub.sendMessage('execute-action', action='openFollowingTimeline', kwargs=dict(user=self.profile_data))
|
||||||
|
|
||||||
|
def onFollowers(self, *args):
|
||||||
|
"""Open followers timeline for this user."""
|
||||||
|
if self.profile_data:
|
||||||
|
pub.sendMessage('execute-action', action='openFollowersTimeline', kwargs=dict(user=self.profile_data))
|
||||||
|
|
||||||
def update_action_buttons_state(self):
|
def update_action_buttons_state(self):
|
||||||
if not self.profile_data or not self.target_user_did or self.target_user_did == self._get_own_did():
|
if not self.profile_data or not self.target_user_did or self.target_user_did == self._get_own_did():
|
||||||
self.follow_btn.Hide()
|
self.follow_btn.Hide()
|
||||||
@@ -239,24 +329,38 @@ class ShowUserProfileDialog(wx.Dialog):
|
|||||||
action_button.Disable()
|
action_button.Disable()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if command == "block_user" and hasattr(self.session, "block_user"):
|
ok = False
|
||||||
|
if command == "follow_user" and hasattr(self.session, "follow_user"):
|
||||||
|
ok = self.session.follow_user(self.target_user_did)
|
||||||
|
elif command == "unfollow_user" and hasattr(self.session, "unfollow_user"):
|
||||||
|
viewer_state = self.profile_data.get("viewer", {}) if self.profile_data else {}
|
||||||
|
follow_uri = viewer_state.get("following")
|
||||||
|
if follow_uri:
|
||||||
|
ok = self.session.unfollow_user(follow_uri)
|
||||||
|
else:
|
||||||
|
raise RuntimeError(_("Follow information not available."))
|
||||||
|
elif command == "mute_user" and hasattr(self.session, "mute_user"):
|
||||||
|
ok = self.session.mute_user(self.target_user_did)
|
||||||
|
elif command == "unmute_user" and hasattr(self.session, "unmute_user"):
|
||||||
|
ok = self.session.unmute_user(self.target_user_did)
|
||||||
|
elif command == "block_user" and hasattr(self.session, "block_user"):
|
||||||
ok = self.session.block_user(self.target_user_did)
|
ok = self.session.block_user(self.target_user_did)
|
||||||
if not ok:
|
|
||||||
raise RuntimeError(_("Failed to block user."))
|
|
||||||
elif command == "unblock_user" and hasattr(self.session, "unblock_user"):
|
elif command == "unblock_user" and hasattr(self.session, "unblock_user"):
|
||||||
viewer_state = self.profile_data.get("viewer", {}) if self.profile_data else {}
|
viewer_state = self.profile_data.get("viewer", {}) if self.profile_data else {}
|
||||||
block_uri = viewer_state.get("blocking")
|
block_uri = viewer_state.get("blocking")
|
||||||
if not block_uri:
|
if not block_uri:
|
||||||
raise RuntimeError(_("Block information not available."))
|
raise RuntimeError(_("Block information not available."))
|
||||||
ok = self.session.unblock_user(block_uri)
|
ok = self.session.unblock_user(block_uri)
|
||||||
if not ok:
|
|
||||||
raise RuntimeError(_("Failed to unblock user."))
|
|
||||||
else:
|
else:
|
||||||
raise RuntimeError(_("This action is not supported yet."))
|
raise RuntimeError(_("This action is not supported yet."))
|
||||||
|
|
||||||
|
if not ok:
|
||||||
|
raise RuntimeError(_("Action failed."))
|
||||||
|
|
||||||
wx.EndBusyCursor()
|
wx.EndBusyCursor()
|
||||||
wx.MessageBox(_("Action completed."), _("Success"), wx.OK | wx.ICON_INFORMATION, self)
|
wx.MessageBox(_("Action completed."), _("Success"), wx.OK | wx.ICON_INFORMATION, self)
|
||||||
wx.CallAfter(asyncio.create_task, self.load_profile_data())
|
# Reload profile data in a new thread
|
||||||
|
Thread(target=self.load_profile_data, daemon=True).start()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
wx.EndBusyCursor()
|
wx.EndBusyCursor()
|
||||||
if action_button:
|
if action_button:
|
||||||
|
|||||||
@@ -14,8 +14,10 @@ class UserActionsDialog(wx.Dialog):
|
|||||||
default_user = users[0] if users else ""
|
default_user = users[0] if users else ""
|
||||||
self.cb = wx.ComboBox(panel, -1, choices=users, value=default_user)
|
self.cb = wx.ComboBox(panel, -1, choices=users, value=default_user)
|
||||||
self.cb.SetFocus()
|
self.cb.SetFocus()
|
||||||
|
self.autocompletion = wx.Button(panel, -1, _(u"&Autocomplete users"))
|
||||||
userSizer.Add(userLabel, 0, wx.ALL, 5)
|
userSizer.Add(userLabel, 0, wx.ALL, 5)
|
||||||
userSizer.Add(self.cb, 0, wx.ALL, 5)
|
userSizer.Add(self.cb, 0, wx.ALL, 5)
|
||||||
|
userSizer.Add(self.autocompletion, 0, wx.ALL, 5)
|
||||||
|
|
||||||
actionSizer = wx.BoxSizer(wx.VERTICAL)
|
actionSizer = wx.BoxSizer(wx.VERTICAL)
|
||||||
label2 = wx.StaticText(panel, -1, _(u"Action"))
|
label2 = wx.StaticText(panel, -1, _(u"Action"))
|
||||||
@@ -83,3 +85,9 @@ class UserActionsDialog(wx.Dialog):
|
|||||||
def get_user(self):
|
def get_user(self):
|
||||||
return self.cb.GetValue()
|
return self.cb.GetValue()
|
||||||
|
|
||||||
|
def get_position(self):
|
||||||
|
return self.cb.GetPosition()
|
||||||
|
|
||||||
|
def popup_menu(self, menu):
|
||||||
|
self.PopupMenu(menu, self.cb.GetPosition())
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user