mirror of
https://github.com/MCV-Software/TWBlue.git
synced 2026-10-16 10:16:53 +02:00
wx only decodes the image formats its own handlers support. Mastodon instances commonly serve avatars and headers as WebP, so wx.Image failed and raised a modal "Unknown image format" dialog, which blocked the user from updating their profile or from reading someone's profile. Images are now decoded with Pillow and their raw pixel data handed over to wx, through a new mysc.image_utils module shared by the Mastodon and Bluesky profile dialogs. Undecodable or unreachable images log the problem and leave an empty placeholder instead of raising. Also accept WebP and JPEG files in the header/avatar file pickers, warn the user with a common message dialog when the picked file cannot be read, and fix the parentless wx.StaticBitmap placeholders that were created when a profile image could not be downloaded.
240 lines
9.6 KiB
Python
240 lines
9.6 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Wx dialogs for showing a user's profile."""
|
|
|
|
from pubsub import pub
|
|
from typing import Tuple
|
|
import requests
|
|
import wx
|
|
from logging import getLogger
|
|
from threading import Thread
|
|
|
|
from sessions.mastodon.utils import html_filter
|
|
from mysc.image_utils import load_scaled_image
|
|
|
|
|
|
log = getLogger(__name__)
|
|
|
|
|
|
def selectUserDialog(users: list) -> tuple:
|
|
"""Choose a user from a possible list of users"""
|
|
if len(users) == 1:
|
|
return users[0]
|
|
dlg = wx.Dialog(None, title=_("Select user"))
|
|
label = wx.StaticText(dlg, label="Select a user: ")
|
|
choiceList = []
|
|
for user in users:
|
|
if len(user) == 3: # (display_name, username, id)
|
|
choiceList.append(f"{user[0]}: @{user[1]}")
|
|
else: # (acct, id)
|
|
choiceList.append(f"{user[0]}")
|
|
choice = wx.Choice(dlg, choices=choiceList)
|
|
ok = wx.Button(dlg, wx.ID_OK, _("OK"))
|
|
ok.SetDefault()
|
|
cancel = wx.Button(dlg, wx.ID_CANCEL, _("Cancel"))
|
|
dlg.SetEscapeId(cancel.GetId())
|
|
|
|
#sizers
|
|
sizer = wx.GridSizer(2, 2, 5, 5)
|
|
sizer.Add(label, wx.SizerFlags().Center())
|
|
sizer.Add(choice, wx.SizerFlags().Center())
|
|
sizer.Add(ok, wx.SizerFlags().Center())
|
|
sizer.Add(cancel, wx.SizerFlags().Center())
|
|
|
|
if dlg.ShowModal() == wx.ID_CANCEL:
|
|
return ()
|
|
# return the selected user
|
|
return users[choice.GetSelection()]
|
|
|
|
|
|
def returnTrue():
|
|
return True
|
|
|
|
|
|
class ShowUserProfile(wx.Dialog):
|
|
"""
|
|
A dialog for Showing user profile
|
|
layout is:
|
|
```
|
|
header
|
|
avatar
|
|
name
|
|
bio
|
|
meta data
|
|
```
|
|
"""
|
|
|
|
def __init__(self, user):
|
|
"""Initialize update profile dialog
|
|
Parameters:
|
|
- user: user dictionary
|
|
"""
|
|
super().__init__(parent=None)
|
|
self.user = user
|
|
self.SetTitle(_("{}'s Profile").format(user.display_name))
|
|
self.panel = wx.Panel(self)
|
|
wrapperSizer = wx.BoxSizer(wx.VERTICAL)
|
|
mainSizer = wx.GridSizer(2, 5, 5)
|
|
|
|
# create widgets
|
|
nameLabel = wx.StaticText(self.panel, label=_("&Name: "))
|
|
name = self.createTextCtrl(user.display_name, size=(200, 30))
|
|
mainSizer.Add(nameLabel, wx.SizerFlags().Center())
|
|
mainSizer.Add(name, wx.SizerFlags().Center())
|
|
|
|
urlLabel = wx.StaticText(self.panel, label=_("&URL: "))
|
|
url = self.createTextCtrl(user.url, size=(200, 30))
|
|
mainSizer.Add(urlLabel, wx.SizerFlags().Center())
|
|
mainSizer.Add(url, wx.SizerFlags().Center())
|
|
|
|
bioLabel = wx.StaticText(self.panel, label=_("&Bio: "))
|
|
bio = self.createTextCtrl(html_filter(user.note), (400, 60))
|
|
mainSizer.Add(bioLabel, wx.SizerFlags().Center())
|
|
mainSizer.Add(bio, wx.SizerFlags().Center())
|
|
|
|
joinLabel = wx.StaticText(self.panel, label=_("&Joined at: "))
|
|
joinText = self.createTextCtrl(user.created_at.strftime('%d %B, %Y'), (80, 30))
|
|
mainSizer.Add(joinLabel, wx.SizerFlags().Center())
|
|
mainSizer.Add(joinText, wx.SizerFlags().Center())
|
|
|
|
actions = wx.Button(self.panel, label=_("&Actions"))
|
|
actions.Bind(wx.EVT_BUTTON, self.onAction)
|
|
mainSizer.Add(actions, wx.SizerFlags().Center())
|
|
|
|
# header
|
|
headerLabel = wx.StaticText(self.panel, label=_("Header: "))
|
|
# Create empty image
|
|
self.headerImage = wx.StaticBitmap(self.panel)
|
|
self.headerImage.AcceptsFocusFromKeyboard = returnTrue
|
|
mainSizer.Add(headerLabel, wx.SizerFlags().Center())
|
|
mainSizer.Add(self.headerImage, wx.SizerFlags().Center())
|
|
|
|
# avatar
|
|
avatarLabel = wx.StaticText(self.panel, label=_("Avatar: "))
|
|
# Create empty image
|
|
self.avatarImage = wx.StaticBitmap(self.panel)
|
|
self.avatarImage.AcceptsFocusFromKeyboard = returnTrue
|
|
mainSizer.Add(avatarLabel, wx.SizerFlags().Center())
|
|
mainSizer.Add(self.avatarImage, wx.SizerFlags().Center())
|
|
|
|
self.fields = []
|
|
for num, field in enumerate(user.fields):
|
|
labelSizer = wx.BoxSizer(wx.HORIZONTAL)
|
|
labelLabel = wx.StaticText(self.panel, label=_("Field &{} - Label: ").format(num + 1))
|
|
labelSizer.Add(labelLabel, wx.SizerFlags().Center().Border(wx.ALL, 5))
|
|
labelText = self.createTextCtrl(html_filter(field.name), (230, 30), True)
|
|
labelSizer.Add(labelText, wx.SizerFlags().Expand().Border(wx.ALL, 5))
|
|
mainSizer.Add(labelSizer, 0, wx.CENTER)
|
|
|
|
contentSizer = wx.BoxSizer(wx.HORIZONTAL)
|
|
contentLabel = wx.StaticText(self.panel, label=_("Content: "))
|
|
contentSizer.Add(contentLabel, wx.SizerFlags().Center())
|
|
contentText = self.createTextCtrl(html_filter(field.value), (400, 60), True)
|
|
contentSizer.Add(contentText, wx.SizerFlags().Center())
|
|
mainSizer.Add(contentSizer, 0, wx.CENTER | wx.LEFT, 10)
|
|
|
|
bullSwitch = {True: _('Yes'), False: _('No'), None: _('No')}
|
|
privateSizer = wx.BoxSizer(wx.HORIZONTAL)
|
|
privateLabel = wx.StaticText(self.panel, label=_("&Private account: "))
|
|
private = self.createTextCtrl(bullSwitch[user.locked], (30, 30))
|
|
privateSizer.Add(privateLabel, wx.SizerFlags().Center())
|
|
privateSizer.Add(private, wx.SizerFlags().Center())
|
|
mainSizer.Add(privateSizer, 0, wx.ALL | wx.CENTER)
|
|
|
|
botSizer = wx.BoxSizer(wx.HORIZONTAL)
|
|
botLabel = wx.StaticText(self.panel, label=_("B&ot account: "))
|
|
botText = self.createTextCtrl(bullSwitch[user.bot], (30, 30))
|
|
botSizer.Add(botLabel, wx.SizerFlags().Center())
|
|
botSizer.Add(botText, wx.SizerFlags().Center())
|
|
mainSizer.Add(botSizer, 0, wx.ALL | wx.CENTER)
|
|
|
|
discoverSizer = wx.BoxSizer(wx.HORIZONTAL)
|
|
discoverLabel = wx.StaticText(self.panel, label=_("&Discoverable account: "))
|
|
discoverText = self.createTextCtrl(bullSwitch[user.discoverable], (30, 30))
|
|
discoverSizer.Add(discoverLabel, wx.SizerFlags().Center())
|
|
discoverSizer.Add(discoverText, wx.SizerFlags().Center())
|
|
mainSizer.Add(discoverSizer, 0, wx.ALL | wx.CENTER)
|
|
|
|
posts = wx.Button(self.panel, label=_("{} pos&ts. Click to open posts timeline").format(user.statuses_count))
|
|
# posts.SetToolTip(_("Click to open {}'s posts").format(user.display_name))
|
|
posts.Bind(wx.EVT_BUTTON, self.onPost)
|
|
mainSizer.Add(posts, wx.SizerFlags().Center())
|
|
|
|
following = wx.Button(self.panel, label=_("{} &following. Click to open Following timeline").format(user.following_count))
|
|
mainSizer.Add(following, wx.SizerFlags().Center())
|
|
following.Bind(wx.EVT_BUTTON, self.onFollowing)
|
|
|
|
followers = wx.Button(self.panel, label=_("{} fo&llowers. Click to open followers timeline").format(user.followers_count))
|
|
mainSizer.Add(followers, wx.SizerFlags().Center())
|
|
followers.Bind(wx.EVT_BUTTON, self.onFollowers)
|
|
|
|
close = wx.Button(self.panel, wx.ID_CLOSE, _("&Close"))
|
|
self.SetEscapeId(close.GetId())
|
|
close.SetDefault()
|
|
wrapperSizer.Add(mainSizer, 0, wx.CENTER)
|
|
wrapperSizer.Add(close, wx.SizerFlags().Center())
|
|
self.panel.SetSizer(wrapperSizer)
|
|
wrapperSizer.Fit(self.panel)
|
|
self.panel.Center()
|
|
mainSizer.Fit(self)
|
|
self.Center()
|
|
imageDownloaderThread = Thread(target=self._getImages)
|
|
imageDownloaderThread.start()
|
|
|
|
|
|
def createTextCtrl(self, text: str, size: Tuple[int, int], multiline: bool = False) -> wx.TextCtrl:
|
|
"""Creates a wx.TextCtrl and returns it
|
|
Parameters:
|
|
text: The value of the wx.TextCtrl
|
|
size: The size of the wx.TextCtrl
|
|
Returns: the created wx.TextCtrl object
|
|
"""
|
|
if not multiline:
|
|
style= wx.TE_PROCESS_ENTER | wx.TE_PROCESS_TAB | wx.TE_READONLY
|
|
else:
|
|
style= wx.TE_PROCESS_ENTER | wx.TE_PROCESS_TAB | wx.TE_READONLY | wx.TE_MULTILINE
|
|
textCtrl = wx.TextCtrl(self.panel, value=text, size=size, style=style)
|
|
textCtrl.AcceptsFocusFromKeyboard = returnTrue
|
|
return textCtrl
|
|
|
|
def onAction(self, *args):
|
|
"""Opens the Open timeline dialog"""
|
|
pub.sendMessage('execute-action', action='follow')
|
|
|
|
def onPost(self, *args):
|
|
"""Open this user's timeline"""
|
|
pub.sendMessage('execute-action', action='openPostTimeline', kwargs=dict(user=self.user))
|
|
|
|
def onFollowing(self, *args):
|
|
"""Open following timeline for this user"""
|
|
pub.sendMessage('execute-action', action='openFollowingTimeline', kwargs=dict(user=self.user))
|
|
|
|
def onFollowers(self, *args):
|
|
"""Open followers timeline for this user"""
|
|
pub.sendMessage('execute-action', action='openFollowersTimeline', kwargs=dict(user=self.user))
|
|
|
|
def _getImages(self):
|
|
"""Downloads image from mastodon server
|
|
This method should run on a separate thread
|
|
"""
|
|
log.debug("Downloading user's header and avatar images...")
|
|
try:
|
|
header = requests.get(self.user.header)
|
|
avatar = requests.get(self.user.avatar)
|
|
except requests.exceptions.RequestException as mess:
|
|
log.exception("An exception was raised while downloading images:", mess)
|
|
return
|
|
wx.CallAfter(self._drawImages, header.content, avatar.content)
|
|
|
|
def _drawImages(self, headerImageBytes, avatarImageBytes):
|
|
"""Draws images on the bitmap ui"""
|
|
# log.debug("Drawing images...")
|
|
# Header
|
|
headerImage = load_scaled_image(headerImageBytes, 300, 100)
|
|
if headerImage is not None:
|
|
self.headerImage.SetBitmap(headerImage.ConvertToBitmap())
|
|
|
|
# Avatar
|
|
avatarImage = load_scaled_image(avatarImageBytes, 150, 150)
|
|
if avatarImage is not None:
|
|
self.avatarImage.SetBitmap(avatarImage.ConvertToBitmap())
|