mirror of
https://github.com/MCV-Software/TWBlue.git
synced 2026-05-09 11:27:36 +02:00
Refactor
This commit is contained in:
@@ -3,12 +3,19 @@ import wx
|
||||
import logging
|
||||
import languageHandler
|
||||
import builtins
|
||||
import requests
|
||||
from io import BytesIO
|
||||
from threading import Thread
|
||||
from pubsub import pub
|
||||
|
||||
_ = getattr(builtins, "_", lambda s: s)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def returnTrue():
|
||||
return True
|
||||
|
||||
class ShowUserProfileDialog(wx.Dialog):
|
||||
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)
|
||||
@@ -25,80 +32,100 @@ class ShowUserProfileDialog(wx.Dialog):
|
||||
Thread(target=self.load_profile_data, daemon=True).start()
|
||||
|
||||
def _init_ui(self):
|
||||
panel = wx.Panel(self)
|
||||
self.panel = wx.Panel(self)
|
||||
main_sizer = wx.BoxSizer(wx.VERTICAL)
|
||||
|
||||
# Profile Info Section (StaticTexts for labels and values)
|
||||
self.info_grid_sizer = wx.FlexGridSizer(cols=2, vgap=5, hgap=5)
|
||||
self.info_grid_sizer.AddGrowableCol(1, 1)
|
||||
|
||||
# Basic text fields (name, handle, bio)
|
||||
fields = [
|
||||
(_("&Name:"), "displayName"), (_("&Handle:"), "handle"), (_("&DID:"), "did"),
|
||||
(_("&Followers:"), "followersCount"), (_("&Following:"), "followsCount"), (_("&Posts:"), "postsCount"),
|
||||
(_("&Name:"), "displayName"), (_("&Handle:"), "handle"),
|
||||
(_("&Bio:"), "description")
|
||||
]
|
||||
self.profile_field_ctrls = {}
|
||||
|
||||
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
|
||||
if data_key == "description":
|
||||
style |= wx.TE_MULTILINE
|
||||
else:
|
||||
style |= wx.BORDER_NONE
|
||||
val_ctrl = wx.TextCtrl(panel, style=style)
|
||||
if data_key != "description": # Make it look like a label
|
||||
val_ctrl.SetBackgroundColour(panel.GetBackgroundColour())
|
||||
val_ctrl.AcceptsFocusFromKeyboard = lambda: True
|
||||
val_ctrl = wx.TextCtrl(self.panel, style=style)
|
||||
if data_key != "description":
|
||||
val_ctrl.SetBackgroundColour(self.panel.GetBackgroundColour())
|
||||
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(val_ctrl, 1, wx.EXPAND | wx.ALL, 2)
|
||||
self.profile_field_ctrls[data_key] = val_ctrl
|
||||
|
||||
# Avatar and Banner (placeholders for now)
|
||||
self.avatar_text = wx.StaticText(panel, label=_("Avatar URL: ") + _("N/A"))
|
||||
self.info_grid_sizer.Add(self.avatar_text, 0, wx.ALIGN_RIGHT | wx.ALIGN_TOP | wx.ALL, 2)
|
||||
self.banner_text = wx.StaticText(panel, label=_("Banner URL: ") + _("N/A"))
|
||||
self.info_grid_sizer.Add(self.banner_text, 0, wx.ALIGN_RIGHT | wx.ALIGN_TOP | wx.ALL, 2)
|
||||
# Banner image
|
||||
bannerLabel = wx.StaticText(self.panel, label=_("Banner:"))
|
||||
self.bannerImage = wx.StaticBitmap(self.panel)
|
||||
self.bannerImage.AcceptsFocusFromKeyboard = returnTrue
|
||||
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)
|
||||
|
||||
# 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
|
||||
actions_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
# Placeholders, enable/disable logic will be in load_profile_data
|
||||
self.follow_btn = wx.Button(panel, label=_("Follow"))
|
||||
self.unfollow_btn = wx.Button(panel, label=_("Unfollow"))
|
||||
self.mute_btn = wx.Button(panel, label=_("Mute"))
|
||||
self.unmute_btn = wx.Button(panel, label=_("Unmute"))
|
||||
self.block_btn = wx.Button(panel, label=_("Block"))
|
||||
# Unblock might be more complex if it needs block URI or is shown conditionally
|
||||
self.follow_btn = wx.Button(self.panel, label=_("&Follow"))
|
||||
self.unfollow_btn = wx.Button(self.panel, label=_("U&nfollow"))
|
||||
self.mute_btn = wx.Button(self.panel, label=_("&Mute"))
|
||||
self.unmute_btn = wx.Button(self.panel, label=_("Unmu&te"))
|
||||
self.block_btn = wx.Button(self.panel, label=_("&Block"))
|
||||
self.unblock_btn = wx.Button(self.panel, label=_("Unbl&ock"))
|
||||
|
||||
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.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.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))
|
||||
|
||||
|
||||
actions_sizer.Add(self.follow_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.unmute_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)
|
||||
|
||||
# Close Button
|
||||
close_btn = wx.Button(panel, wx.ID_CANCEL, _("&Close"))
|
||||
close_btn.SetDefault() # Allow Esc to close
|
||||
close_btn = wx.Button(self.panel, wx.ID_CANCEL, _("&Close"))
|
||||
close_btn.SetDefault()
|
||||
main_sizer.Add(close_btn, 0, wx.ALIGN_RIGHT | wx.ALL, 10)
|
||||
self.SetEscapeId(close_btn.GetId())
|
||||
|
||||
panel.SetSizer(main_sizer)
|
||||
self.Fit() # Fit dialog to content
|
||||
self.panel.SetSizer(main_sizer)
|
||||
self.Fit()
|
||||
|
||||
def load_profile_data(self):
|
||||
wx.CallAfter(self.SetStatusText, _("Loading profile..."))
|
||||
@@ -154,24 +181,87 @@ class ShowUserProfileDialog(wx.Dialog):
|
||||
return
|
||||
|
||||
for key, ctrl in self.profile_field_ctrls.items():
|
||||
value = self.profile_data.get(key) # _format_profile_data should provide values or None/empty
|
||||
if key == "description" and value: # Make bio multi-line if content exists
|
||||
ctrl.SetMinSize((-1, 60)) # Allow some height for bio
|
||||
value = self.profile_data.get(key)
|
||||
if key == "description" and value:
|
||||
ctrl.SetMinSize((-1, 60))
|
||||
|
||||
if isinstance(value, (int, float)):
|
||||
ctrl.SetValue(str(value))
|
||||
else: # String or None
|
||||
else:
|
||||
ctrl.SetValue(value or _("N/A"))
|
||||
|
||||
# For URLs, could make them clickable or add a "Copy URL" button
|
||||
avatar_url = self.profile_data.get("avatar") or _("N/A")
|
||||
banner_url = self.profile_data.get("banner") or _("N/A")
|
||||
self.avatar_text.SetLabel(_("Avatar URL: ") + avatar_url)
|
||||
self.avatar_text.SetToolTip(avatar_url if avatar_url != _("N/A") else "")
|
||||
self.banner_text.SetLabel(_("Banner URL: ") + banner_url)
|
||||
self.banner_text.SetToolTip(banner_url if banner_url != _("N/A") else "")
|
||||
# Update timeline buttons with counts
|
||||
posts_count = self.profile_data.get("postsCount") or 0
|
||||
followers_count = self.profile_data.get("followersCount") or 0
|
||||
following_count = self.profile_data.get("followsCount") or 0
|
||||
|
||||
self.posts_btn.SetLabel(_("{count} pos&ts. Click to open posts timeline").format(count=posts_count))
|
||||
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()
|
||||
|
||||
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):
|
||||
if not self.profile_data or not self.target_user_did or self.target_user_did == self._get_own_did():
|
||||
self.follow_btn.Hide()
|
||||
@@ -239,24 +329,38 @@ class ShowUserProfileDialog(wx.Dialog):
|
||||
action_button.Disable()
|
||||
|
||||
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)
|
||||
if not ok:
|
||||
raise RuntimeError(_("Failed to block user."))
|
||||
elif command == "unblock_user" and hasattr(self.session, "unblock_user"):
|
||||
viewer_state = self.profile_data.get("viewer", {}) if self.profile_data else {}
|
||||
block_uri = viewer_state.get("blocking")
|
||||
if not block_uri:
|
||||
raise RuntimeError(_("Block information not available."))
|
||||
ok = self.session.unblock_user(block_uri)
|
||||
if not ok:
|
||||
raise RuntimeError(_("Failed to unblock user."))
|
||||
else:
|
||||
raise RuntimeError(_("This action is not supported yet."))
|
||||
|
||||
if not ok:
|
||||
raise RuntimeError(_("Action failed."))
|
||||
|
||||
wx.EndBusyCursor()
|
||||
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:
|
||||
wx.EndBusyCursor()
|
||||
if action_button:
|
||||
|
||||
Reference in New Issue
Block a user