mirror of
https://github.com/MCV-Software/TWBlue.git
synced 2026-08-18 02:48:11 +02:00
Merge branch 'next-gen' of github.com:mcv-software/twblue into next-gen
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import wx
|
||||
import languageHandler
|
||||
from multiplatform_widgets import widgets
|
||||
|
||||
class HomePanel(wx.Panel):
|
||||
def __init__(self, parent, name, account="Unknown"):
|
||||
super().__init__(parent, name=name)
|
||||
self.name = name
|
||||
self.account = account
|
||||
self.type = "home_timeline"
|
||||
|
||||
self.sizer = wx.BoxSizer(wx.VERTICAL)
|
||||
|
||||
# List
|
||||
self.list = widgets.list(self, _("Author"), _("Post"), _("Date"), style=wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.LC_VRULES)
|
||||
self.list.set_windows_size(0, 120)
|
||||
self.list.set_windows_size(1, 400)
|
||||
self.list.set_windows_size(2, 120)
|
||||
self.list.set_size()
|
||||
|
||||
# Buttons
|
||||
self.post = wx.Button(self, -1, _("Post"))
|
||||
self.repost = wx.Button(self, -1, _("Repost"))
|
||||
self.reply = wx.Button(self, -1, _("Reply"))
|
||||
self.like = wx.Button(self, wx.ID_ANY, _("Like"))
|
||||
# self.bookmark = wx.Button(self, wx.ID_ANY, _("Bookmark")) # Not yet common in Bsky API usage here
|
||||
self.dm = wx.Button(self, -1, _("Chat"))
|
||||
|
||||
btnSizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
btnSizer.Add(self.post, 0, wx.ALL, 5)
|
||||
btnSizer.Add(self.repost, 0, wx.ALL, 5)
|
||||
btnSizer.Add(self.reply, 0, wx.ALL, 5)
|
||||
btnSizer.Add(self.like, 0, wx.ALL, 5)
|
||||
# btnSizer.Add(self.bookmark, 0, wx.ALL, 5)
|
||||
btnSizer.Add(self.dm, 0, wx.ALL, 5)
|
||||
|
||||
self.sizer.Add(btnSizer, 0, wx.ALL, 5)
|
||||
|
||||
self.sizer.Add(self.list.list, 1, wx.EXPAND | wx.ALL, 5)
|
||||
self.SetSizer(self.sizer)
|
||||
|
||||
# Some helper methods expected by controller might be needed?
|
||||
# Controller accesses self.buffer.list directly.
|
||||
# Some older code expected .set_position, .post, .message, .actions attributes or buttons on the panel?
|
||||
# Mastodon panels usually have bottom buttons (Post, Reply, etc).
|
||||
# I should add them if I want to "reuse Mastodon".
|
||||
|
||||
# But for now, simple list is what the previous code had.
|
||||
|
||||
def set_focus_function(self, func):
|
||||
self.list.list.Bind(wx.EVT_LIST_ITEM_FOCUSED, func)
|
||||
|
||||
def set_position(self, reverse):
|
||||
if reverse:
|
||||
self.list.select_item(0)
|
||||
else:
|
||||
self.list.select_item(self.list.get_count() - 1)
|
||||
|
||||
def set_focus_in_list(self):
|
||||
self.list.list.SetFocus()
|
||||
|
||||
class NotificationPanel(HomePanel):
|
||||
pass
|
||||
|
||||
class UserPanel(wx.Panel):
|
||||
def __init__(self, parent, name, account="Unknown"):
|
||||
super().__init__(parent, name=name)
|
||||
self.name = name
|
||||
self.account = account
|
||||
self.type = "user"
|
||||
|
||||
self.sizer = wx.BoxSizer(wx.VERTICAL)
|
||||
|
||||
# List: User
|
||||
self.list = widgets.list(self, _("User"), style=wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.LC_VRULES)
|
||||
self.list.set_windows_size(0, 600)
|
||||
self.list.set_size()
|
||||
|
||||
# Buttons
|
||||
self.post = wx.Button(self, -1, _("Post"))
|
||||
self.actions = wx.Button(self, -1, _("Actions"))
|
||||
self.message = wx.Button(self, -1, _("Message"))
|
||||
|
||||
btnSizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
btnSizer.Add(self.post, 0, wx.ALL, 5)
|
||||
btnSizer.Add(self.actions, 0, wx.ALL, 5)
|
||||
btnSizer.Add(self.message, 0, wx.ALL, 5)
|
||||
|
||||
self.sizer.Add(btnSizer, 0, wx.ALL, 5)
|
||||
|
||||
self.sizer.Add(self.list.list, 1, wx.EXPAND | wx.ALL, 5)
|
||||
self.SetSizer(self.sizer)
|
||||
|
||||
def set_focus_function(self, func):
|
||||
self.list.list.Bind(wx.EVT_LIST_ITEM_FOCUSED, func)
|
||||
|
||||
def set_position(self, reverse):
|
||||
if reverse:
|
||||
self.list.select_item(0)
|
||||
else:
|
||||
self.list.select_item(self.list.get_count() - 1)
|
||||
|
||||
def set_focus_in_list(self):
|
||||
self.list.list.SetFocus()
|
||||
|
||||
class ChatPanel(wx.Panel):
|
||||
"""Panel for conversation list, similar to Mastodon's conversationListPanel."""
|
||||
def __init__(self, parent, name, account="Unknown"):
|
||||
super().__init__(parent, name=name)
|
||||
self.name = name
|
||||
self.account = account
|
||||
self.type = "chat"
|
||||
|
||||
self.sizer = wx.BoxSizer(wx.VERTICAL)
|
||||
|
||||
# List: User, Text, Date (like Mastodon)
|
||||
self.list = widgets.list(self, _("User"), _("Text"), _("Date"), style=wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.LC_VRULES)
|
||||
self.list.set_windows_size(0, 200)
|
||||
self.list.set_windows_size(1, 600)
|
||||
self.list.set_windows_size(2, 200)
|
||||
self.list.set_size()
|
||||
|
||||
# Buttons (like Mastodon: Post, Reply)
|
||||
self.post = wx.Button(self, -1, _("Post"))
|
||||
self.reply = wx.Button(self, -1, _("Reply"))
|
||||
self.new_chat = wx.Button(self, -1, _("New Chat"))
|
||||
btnSizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
btnSizer.Add(self.post, 0, wx.ALL, 5)
|
||||
btnSizer.Add(self.reply, 0, wx.ALL, 5)
|
||||
btnSizer.Add(self.new_chat, 0, wx.ALL, 5)
|
||||
self.sizer.Add(btnSizer, 0, wx.ALL, 5)
|
||||
|
||||
self.sizer.Add(self.list.list, 1, wx.EXPAND | wx.ALL, 5)
|
||||
self.SetSizer(self.sizer)
|
||||
|
||||
def set_focus_function(self, func):
|
||||
self.list.list.Bind(wx.EVT_LIST_ITEM_FOCUSED, func)
|
||||
|
||||
def set_position(self, reversed=False):
|
||||
if reversed == False:
|
||||
self.list.select_item(self.list.get_count()-1)
|
||||
else:
|
||||
self.list.select_item(0)
|
||||
|
||||
def set_focus_in_list(self):
|
||||
self.list.list.SetFocus()
|
||||
|
||||
class ChatMessagePanel(HomePanel):
|
||||
def __init__(self, parent, name, account="Unknown"):
|
||||
super().__init__(parent, name, account)
|
||||
self.type = "chat_messages"
|
||||
# Adjust buttons for chat
|
||||
self.repost.Hide()
|
||||
self.like.Hide()
|
||||
self.dm.Hide() # Hide Chat button since we're already in a chat
|
||||
self.reply.SetLabel(_("Send Message"))
|
||||
|
||||
# Refresh columns
|
||||
self.list.list.ClearAll()
|
||||
self.list.list.InsertColumn(0, _("Sender"))
|
||||
self.list.list.InsertColumn(1, _("Message"))
|
||||
self.list.list.InsertColumn(2, _("Date"))
|
||||
self.list.set_windows_size(0, 100)
|
||||
self.list.set_windows_size(1, 400)
|
||||
self.list.set_windows_size(2, 100)
|
||||
self.list.set_size()
|
||||
@@ -59,3 +59,8 @@ def remove_filter():
|
||||
return dlg.ShowModal()
|
||||
def error_removing_filters():
|
||||
return wx.MessageDialog(None, _("TWBlue was unable to remove the filter you specified. Please try again."), _("Error"), wx.ICON_ERROR).ShowModal()
|
||||
|
||||
def common_error(message):
|
||||
"""Show a generic error dialog with the provided message."""
|
||||
dlg = wx.MessageDialog(None, message, _("Error"), wx.OK | wx.ICON_ERROR)
|
||||
return dlg.ShowModal()
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import wx
|
||||
import languageHandler
|
||||
|
||||
|
||||
class AccountSettingsDialog(wx.Dialog):
|
||||
def __init__(self, parent=None, ask_before_boost=True):
|
||||
super(AccountSettingsDialog, self).__init__(parent, title=_("Bluesky Account Settings"))
|
||||
panel = wx.Panel(self)
|
||||
|
||||
sizer = wx.BoxSizer(wx.VERTICAL)
|
||||
|
||||
# Ask before boost/share
|
||||
self.ask_before_boost = wx.CheckBox(panel, wx.ID_ANY, _("Ask confirmation before sharing a post"))
|
||||
self.ask_before_boost.SetValue(bool(ask_before_boost))
|
||||
sizer.Add(self.ask_before_boost, 0, wx.ALL, 8)
|
||||
|
||||
templates_box = wx.StaticBoxSizer(wx.StaticBox(panel, wx.ID_ANY, _("Templates")), wx.VERTICAL)
|
||||
self.template_post = wx.Button(panel, wx.ID_ANY, _("Edit template for posts"))
|
||||
self.template_person = wx.Button(panel, wx.ID_ANY, _("Edit template for persons"))
|
||||
self.template_notification = wx.Button(panel, wx.ID_ANY, _("Edit template for notifications"))
|
||||
templates_box.Add(self.template_post, 0, wx.ALL, 4)
|
||||
templates_box.Add(self.template_person, 0, wx.ALL, 4)
|
||||
templates_box.Add(self.template_notification, 0, wx.ALL, 4)
|
||||
sizer.Add(templates_box, 0, wx.EXPAND | wx.ALL, 8)
|
||||
|
||||
# Buttons
|
||||
btn_sizer = self.CreateSeparatedButtonSizer(wx.OK | wx.CANCEL)
|
||||
|
||||
panel.SetSizer(sizer)
|
||||
|
||||
main = wx.BoxSizer(wx.VERTICAL)
|
||||
main.Add(panel, 1, wx.EXPAND | wx.ALL, 10)
|
||||
if btn_sizer:
|
||||
main.Add(btn_sizer, 0, wx.EXPAND | wx.ALL, 10)
|
||||
self.SetSizerAndFit(main)
|
||||
|
||||
def get_values(self):
|
||||
return {
|
||||
"ask_before_boost": self.ask_before_boost.GetValue(),
|
||||
}
|
||||
|
||||
def set_template_labels(self, post_template, person_template, notification_template):
|
||||
self.template_post.SetLabel(_("Edit template for posts. Current template: {}").format(post_template))
|
||||
self.template_person.SetLabel(_("Edit template for persons. Current template: {}").format(person_template))
|
||||
self.template_notification.SetLabel(_("Edit template for notifications. Current template: {}").format(notification_template))
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Context menus for Bluesky buffers."""
|
||||
|
||||
import wx
|
||||
|
||||
|
||||
class baseMenu(wx.Menu):
|
||||
"""Base context menu for Bluesky posts."""
|
||||
|
||||
def __init__(self):
|
||||
super(baseMenu, self).__init__()
|
||||
self.repost = wx.MenuItem(self, wx.ID_ANY, _("&Repost"))
|
||||
self.Append(self.repost)
|
||||
self.quote = wx.MenuItem(self, wx.ID_ANY, _("&Quote"))
|
||||
self.Append(self.quote)
|
||||
self.reply = wx.MenuItem(self, wx.ID_ANY, _("Re&ply"))
|
||||
self.Append(self.reply)
|
||||
self.like = wx.MenuItem(self, wx.ID_ANY, _("&Like"))
|
||||
self.Append(self.like)
|
||||
self.unlike = wx.MenuItem(self, wx.ID_ANY, _("&Unlike"))
|
||||
self.Append(self.unlike)
|
||||
self.openUrl = wx.MenuItem(self, wx.ID_ANY, _("&Open URL"))
|
||||
self.Append(self.openUrl)
|
||||
self.openInBrowser = wx.MenuItem(self, wx.ID_ANY, _("Open in &browser"))
|
||||
self.Append(self.openInBrowser)
|
||||
self.view = wx.MenuItem(self, wx.ID_ANY, _("&Show post"))
|
||||
self.Append(self.view)
|
||||
self.copy = wx.MenuItem(self, wx.ID_ANY, _("&Copy to clipboard"))
|
||||
self.Append(self.copy)
|
||||
self.remove = wx.MenuItem(self, wx.ID_ANY, _("&Delete"))
|
||||
self.Append(self.remove)
|
||||
self.userActions = wx.MenuItem(self, wx.ID_ANY, _("&User actions..."))
|
||||
self.Append(self.userActions)
|
||||
|
||||
|
||||
class notificationMenu(wx.Menu):
|
||||
"""Context menu for Bluesky notifications."""
|
||||
|
||||
def __init__(self, notification_type="like"):
|
||||
super(notificationMenu, self).__init__()
|
||||
# Notification types that have associated posts
|
||||
post_types = ["like", "repost", "mention", "reply", "quote"]
|
||||
|
||||
if notification_type in post_types:
|
||||
self.repost = wx.MenuItem(self, wx.ID_ANY, _("&Repost"))
|
||||
self.Append(self.repost)
|
||||
self.reply = wx.MenuItem(self, wx.ID_ANY, _("Re&ply"))
|
||||
self.Append(self.reply)
|
||||
self.like = wx.MenuItem(self, wx.ID_ANY, _("&Like"))
|
||||
self.Append(self.like)
|
||||
self.openUrl = wx.MenuItem(self, wx.ID_ANY, _("&Open URL"))
|
||||
self.Append(self.openUrl)
|
||||
|
||||
self.openInBrowser = wx.MenuItem(self, wx.ID_ANY, _("Open in &browser"))
|
||||
self.Append(self.openInBrowser)
|
||||
self.view = wx.MenuItem(self, wx.ID_ANY, _("&Show post"))
|
||||
self.Append(self.view)
|
||||
self.copy = wx.MenuItem(self, wx.ID_ANY, _("&Copy to clipboard"))
|
||||
self.Append(self.copy)
|
||||
self.userActions = wx.MenuItem(self, wx.ID_ANY, _("&User actions..."))
|
||||
self.Append(self.userActions)
|
||||
|
||||
|
||||
class userMenu(wx.Menu):
|
||||
"""Context menu for Bluesky user lists."""
|
||||
|
||||
def __init__(self):
|
||||
super(userMenu, self).__init__()
|
||||
self.timeline = wx.MenuItem(self, wx.ID_ANY, _("View &timeline"))
|
||||
self.Append(self.timeline)
|
||||
self.followers = wx.MenuItem(self, wx.ID_ANY, _("View f&ollowers"))
|
||||
self.Append(self.followers)
|
||||
self.following = wx.MenuItem(self, wx.ID_ANY, _("View &following"))
|
||||
self.Append(self.following)
|
||||
self.dm = wx.MenuItem(self, wx.ID_ANY, _("Send &message"))
|
||||
self.Append(self.dm)
|
||||
self.view = wx.MenuItem(self, wx.ID_ANY, _("View &profile"))
|
||||
self.Append(self.view)
|
||||
self.copy = wx.MenuItem(self, wx.ID_ANY, _("&Copy to clipboard"))
|
||||
self.Append(self.copy)
|
||||
self.userActions = wx.MenuItem(self, wx.ID_ANY, _("&User actions..."))
|
||||
self.Append(self.userActions)
|
||||
|
||||
|
||||
class chatMenu(wx.Menu):
|
||||
"""Context menu for Bluesky chat messages."""
|
||||
|
||||
def __init__(self):
|
||||
super(chatMenu, self).__init__()
|
||||
self.reply = wx.MenuItem(self, wx.ID_ANY, _("&Reply"))
|
||||
self.Append(self.reply)
|
||||
self.copy = wx.MenuItem(self, wx.ID_ANY, _("&Copy to clipboard"))
|
||||
self.Append(self.copy)
|
||||
self.view = wx.MenuItem(self, wx.ID_ANY, _("&Show message"))
|
||||
self.Append(self.view)
|
||||
self.userActions = wx.MenuItem(self, wx.ID_ANY, _("&User actions..."))
|
||||
self.Append(self.userActions)
|
||||
@@ -0,0 +1,292 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import wx
|
||||
|
||||
|
||||
class Post(wx.Dialog):
|
||||
def __init__(self, caption=_("Post"), text="", languages=[], *args, **kwds):
|
||||
super(Post, self).__init__(parent=None, id=wx.ID_ANY, *args, **kwds)
|
||||
self.SetTitle(caption)
|
||||
main_sizer = wx.BoxSizer(wx.VERTICAL)
|
||||
|
||||
# Text
|
||||
post_label = wx.StaticText(self, wx.ID_ANY, caption)
|
||||
main_sizer.Add(post_label, 0, wx.ALL, 6)
|
||||
self.text = wx.TextCtrl(self, wx.ID_ANY, text, style=wx.TE_MULTILINE)
|
||||
self.Bind(wx.EVT_CHAR_HOOK, self.handle_keys, self.text)
|
||||
self.text.SetMinSize((400, 160))
|
||||
main_sizer.Add(self.text, 1, wx.EXPAND | wx.ALL, 6)
|
||||
|
||||
# Sensitive + CW
|
||||
self.sensitive = wx.CheckBox(self, wx.ID_ANY, _("S&ensitive content"))
|
||||
self.sensitive.SetValue(False)
|
||||
self.sensitive.Bind(wx.EVT_CHECKBOX, self.on_sensitivity_changed)
|
||||
main_sizer.Add(self.sensitive, 0, wx.ALL, 5)
|
||||
|
||||
spoiler_box = wx.BoxSizer(wx.HORIZONTAL)
|
||||
spoiler_label = wx.StaticText(self, wx.ID_ANY, _("Content warning"))
|
||||
self.spoiler = wx.TextCtrl(self, wx.ID_ANY)
|
||||
self.spoiler.Enable(False)
|
||||
spoiler_box.Add(spoiler_label, 0, wx.ALL, 5)
|
||||
spoiler_box.Add(self.spoiler, 1, wx.ALL, 10)
|
||||
main_sizer.Add(spoiler_box, 0, wx.EXPAND | wx.ALL, 5)
|
||||
|
||||
# Attachments (images only)
|
||||
attach_box = wx.StaticBoxSizer(wx.VERTICAL, self, _("Attachments (images)"))
|
||||
self.attach_list = wx.ListCtrl(self, style=wx.LC_REPORT | wx.LC_SINGLE_SEL)
|
||||
self.attach_list.InsertColumn(0, _("File"))
|
||||
self.attach_list.InsertColumn(1, _("Alt"))
|
||||
attach_box.Add(self.attach_list, 1, wx.EXPAND | wx.ALL, 5)
|
||||
btn_row = wx.BoxSizer(wx.HORIZONTAL)
|
||||
self.btn_add = wx.Button(self, wx.ID_ADD, _("Add image..."))
|
||||
self.btn_remove = wx.Button(self, wx.ID_REMOVE, _("Remove"))
|
||||
self.btn_remove.Enable(False)
|
||||
btn_row.Add(self.btn_add, 0, wx.ALL, 2)
|
||||
btn_row.Add(self.btn_remove, 0, wx.ALL, 2)
|
||||
attach_box.Add(btn_row, 0, wx.ALIGN_LEFT)
|
||||
main_sizer.Add(attach_box, 0, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 6)
|
||||
|
||||
# Language (single optional)
|
||||
lang_row = wx.BoxSizer(wx.HORIZONTAL)
|
||||
lang_row.Add(wx.StaticText(self, label=_("&Language")), 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 4)
|
||||
self.language = wx.ComboBox(self, wx.ID_ANY, choices=languages, style=wx.CB_DROPDOWN | wx.CB_READONLY)
|
||||
self.language.SetSelection(0)
|
||||
lang_row.Add(self.language, 0, wx.ALIGN_CENTER_VERTICAL)
|
||||
main_sizer.Add(lang_row, 0, wx.LEFT | wx.RIGHT | wx.BOTTOM, 6)
|
||||
|
||||
# Text actions (spellcheck, translate, autocomplete)
|
||||
text_actions_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
self.autocomplete_users = wx.Button(self, wx.ID_ANY, _("Auto&complete users"))
|
||||
text_actions_sizer.Add(self.autocomplete_users, 0, wx.ALL, 2)
|
||||
self.spellcheck = wx.Button(self, wx.ID_ANY, _("Check &spelling"))
|
||||
text_actions_sizer.Add(self.spellcheck, 0, wx.ALL, 2)
|
||||
self.translate = wx.Button(self, wx.ID_ANY, _("&Translate"))
|
||||
text_actions_sizer.Add(self.translate, 0, wx.ALL, 2)
|
||||
main_sizer.Add(text_actions_sizer, 0, wx.LEFT | wx.RIGHT | wx.BOTTOM, 6)
|
||||
|
||||
# Buttons
|
||||
btn_sizer = wx.StdDialogButtonSizer()
|
||||
self.send = wx.Button(self, wx.ID_ANY, _("&Send"))
|
||||
self.send.SetDefault()
|
||||
self.send.Bind(wx.EVT_BUTTON, lambda evt: self.EndModal(wx.ID_OK))
|
||||
btn_sizer.AddButton(self.send)
|
||||
self.close = wx.Button(self, wx.ID_CLOSE, "")
|
||||
btn_sizer.AddButton(self.close)
|
||||
btn_sizer.Realize()
|
||||
main_sizer.Add(btn_sizer, 0, wx.ALIGN_RIGHT | wx.ALL, 4)
|
||||
|
||||
self.SetSizer(main_sizer)
|
||||
main_sizer.Fit(self)
|
||||
self.SetEscapeId(self.close.GetId())
|
||||
self.Layout()
|
||||
|
||||
# Bindings
|
||||
self.btn_add.Bind(wx.EVT_BUTTON, self.on_add)
|
||||
self.btn_remove.Bind(wx.EVT_BUTTON, self.on_remove)
|
||||
self.attach_list.Bind(wx.EVT_LIST_ITEM_SELECTED, lambda evt: self.btn_remove.Enable(True))
|
||||
self.attach_list.Bind(wx.EVT_LIST_ITEM_DESELECTED, lambda evt: self.btn_remove.Enable(False))
|
||||
|
||||
def handle_keys(self, event):
|
||||
shift = event.ShiftDown()
|
||||
if event.GetKeyCode() == wx.WXK_RETURN and not shift and hasattr(self, "send"):
|
||||
self.EndModal(wx.ID_OK)
|
||||
else:
|
||||
event.Skip()
|
||||
|
||||
def on_sensitivity_changed(self, *args, **kwargs):
|
||||
self.spoiler.Enable(self.sensitive.GetValue())
|
||||
|
||||
def on_add(self, evt):
|
||||
if self.attach_list.GetItemCount() >= 4:
|
||||
wx.MessageBox(_("You can attach up to 4 images."), _("Attachment limit"), wx.ICON_INFORMATION)
|
||||
return
|
||||
fd = wx.FileDialog(self, _("Select image"), wildcard=_("Image files (*.png;*.jpg;*.jpeg;*.gif)|*.png;*.jpg;*.jpeg;*.gif"), style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST)
|
||||
if fd.ShowModal() != wx.ID_OK:
|
||||
fd.Destroy()
|
||||
return
|
||||
path = fd.GetPath()
|
||||
fd.Destroy()
|
||||
alt_dlg = wx.TextEntryDialog(self, _("Alternative text (optional)"), _("Description"))
|
||||
alt = ""
|
||||
if alt_dlg.ShowModal() == wx.ID_OK:
|
||||
alt = alt_dlg.GetValue()
|
||||
alt_dlg.Destroy()
|
||||
idx = self.attach_list.InsertItem(self.attach_list.GetItemCount(), path)
|
||||
self.attach_list.SetItem(idx, 1, alt)
|
||||
|
||||
def on_remove(self, evt):
|
||||
sel = self.attach_list.GetFirstSelected()
|
||||
if sel != -1:
|
||||
self.attach_list.DeleteItem(sel)
|
||||
|
||||
def get_payload(self):
|
||||
text = self.text.GetValue().strip()
|
||||
cw_text = self.spoiler.GetValue().strip() if self.sensitive.GetValue() else None
|
||||
lang_index = self.language.GetSelection()
|
||||
files = []
|
||||
for i in range(self.attach_list.GetItemCount()):
|
||||
files.append({
|
||||
"path": self.attach_list.GetItemText(i, 0),
|
||||
"alt": self.attach_list.GetItemText(i, 1),
|
||||
})
|
||||
return text, files, cw_text, lang_index
|
||||
|
||||
|
||||
class viewPost(wx.Dialog):
|
||||
def set_title(self, length):
|
||||
self.SetTitle(_("Post - %i characters ") % length)
|
||||
|
||||
def __init__(self, text="", reposts_count=0, likes_count=0, source="", date="", privacy="", *args, **kwargs):
|
||||
super(viewPost, self).__init__(parent=None, id=wx.ID_ANY, size=(850, 850))
|
||||
self.init_ui(text, reposts_count, likes_count, source, date, privacy)
|
||||
|
||||
def init_ui(self, text, reposts_count, likes_count, source, date, privacy):
|
||||
panel = wx.Panel(self)
|
||||
main_sizer = wx.BoxSizer(wx.VERTICAL)
|
||||
main_sizer.Add(self.create_text_section(panel, text), 1, wx.EXPAND | wx.ALL, 5)
|
||||
main_sizer.Add(self.create_image_description_section(panel), 1, wx.EXPAND | wx.ALL, 5)
|
||||
main_sizer.Add(self.create_info_section(panel, privacy, reposts_count, likes_count, source, date), 0, wx.EXPAND | wx.ALL, 5)
|
||||
main_sizer.Add(self.create_buttons_section(panel), 0, wx.ALIGN_RIGHT | wx.ALL, 5)
|
||||
panel.SetSizer(main_sizer)
|
||||
self.SetClientSize(main_sizer.CalcMin())
|
||||
|
||||
def create_text_section(self, panel, text):
|
||||
sizer = wx.StaticBoxSizer(wx.StaticBox(panel, wx.ID_ANY, _("Post")), wx.VERTICAL)
|
||||
self.text = wx.TextCtrl(panel, -1, text, style=wx.TE_READONLY | wx.TE_MULTILINE)
|
||||
sizer.Add(self.text, 1, wx.EXPAND | wx.ALL, 5)
|
||||
return sizer
|
||||
|
||||
def create_image_description_section(self, panel):
|
||||
sizer = wx.StaticBoxSizer(wx.StaticBox(panel, wx.ID_ANY, _("Image description")), wx.VERTICAL)
|
||||
self.image_description = wx.TextCtrl(panel, -1, style=wx.TE_READONLY | wx.TE_MULTILINE)
|
||||
self.image_description.Enable(False)
|
||||
sizer.Add(self.image_description, 1, wx.EXPAND | wx.ALL, 5)
|
||||
return sizer
|
||||
|
||||
def create_info_section(self, panel, privacy, reposts_count, likes_count, source, date):
|
||||
sizer = wx.StaticBoxSizer(wx.StaticBox(panel, wx.ID_ANY, _("Information")), wx.VERTICAL)
|
||||
flex_sizer = wx.FlexGridSizer(cols=3, hgap=10, vgap=10)
|
||||
flex_sizer.AddGrowableCol(1)
|
||||
flex_sizer.Add(wx.StaticText(panel, -1, _("Privacy")), 0, wx.ALIGN_CENTER_VERTICAL)
|
||||
flex_sizer.Add(wx.TextCtrl(panel, -1, privacy, style=wx.TE_READONLY | wx.TE_MULTILINE), 1, wx.EXPAND)
|
||||
flex_sizer.Add(self.create_reposts_section(panel, reposts_count), 1, wx.EXPAND | wx.ALL, 5)
|
||||
flex_sizer.Add(self.create_likes_section(panel, likes_count), 1, wx.EXPAND | wx.ALL, 5)
|
||||
flex_sizer.Add(wx.StaticText(panel, -1, _("Source")), 0, wx.ALIGN_CENTER_VERTICAL)
|
||||
flex_sizer.Add(wx.TextCtrl(panel, -1, source, style=wx.TE_READONLY | wx.TE_MULTILINE), 1, wx.EXPAND)
|
||||
flex_sizer.Add(wx.StaticText(panel, -1, _("Date")), 0, wx.ALIGN_CENTER_VERTICAL)
|
||||
flex_sizer.Add(wx.TextCtrl(panel, -1, date, style=wx.TE_READONLY | wx.TE_MULTILINE), 1, wx.EXPAND)
|
||||
sizer.Add(flex_sizer, 1, wx.EXPAND | wx.ALL, 5)
|
||||
return sizer
|
||||
|
||||
def create_reposts_section(self, panel, reposts_count):
|
||||
sizer = wx.StaticBoxSizer(wx.StaticBox(panel, wx.ID_ANY, _("Reposts")), wx.VERTICAL)
|
||||
self.reposts_button = wx.Button(panel, -1, str(reposts_count))
|
||||
self.reposts_button.Enable(False)
|
||||
sizer.Add(self.reposts_button, 1, wx.EXPAND | wx.ALL, 5)
|
||||
return sizer
|
||||
|
||||
def create_likes_section(self, panel, likes_count):
|
||||
sizer = wx.StaticBoxSizer(wx.StaticBox(panel, wx.ID_ANY, _("Likes")), wx.VERTICAL)
|
||||
self.likes_button = wx.Button(panel, -1, str(likes_count))
|
||||
self.likes_button.Enable(False)
|
||||
sizer.Add(self.likes_button, 1, wx.EXPAND | wx.ALL, 5)
|
||||
return sizer
|
||||
|
||||
def create_buttons_section(self, panel):
|
||||
sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
self.share = wx.Button(panel, wx.ID_ANY, _("&Copy link to clipboard"))
|
||||
self.share.Enable(False)
|
||||
self.spellcheck = wx.Button(panel, wx.ID_ANY, _("Check &spelling..."))
|
||||
self.translateButton = wx.Button(panel, wx.ID_ANY, _("&Translate..."))
|
||||
cancelButton = wx.Button(panel, wx.ID_CANCEL, _("C&lose"))
|
||||
cancelButton.SetDefault()
|
||||
sizer.Add(self.share, 0, wx.ALL, 5)
|
||||
sizer.Add(self.spellcheck, 0, wx.ALL, 5)
|
||||
sizer.Add(self.translateButton, 0, wx.ALL, 5)
|
||||
sizer.Add(cancelButton, 0, wx.ALL, 5)
|
||||
return sizer
|
||||
|
||||
def set_text(self, text):
|
||||
self.text.ChangeValue(text)
|
||||
|
||||
def get_text(self):
|
||||
return self.text.GetValue()
|
||||
|
||||
def text_focus(self):
|
||||
self.text.SetFocus()
|
||||
|
||||
def onSelect(self, ev):
|
||||
self.text.SelectAll()
|
||||
|
||||
def enable_button(self, buttonName):
|
||||
if hasattr(self, buttonName):
|
||||
return getattr(self, buttonName).Enable()
|
||||
|
||||
|
||||
class viewText(wx.Dialog):
|
||||
def __init__(self, title="", text="", *args, **kwargs):
|
||||
super(viewText, self).__init__(parent=None, id=wx.ID_ANY, size=(850, 850), title=title)
|
||||
panel = wx.Panel(self)
|
||||
label = wx.StaticText(panel, -1, _("Text"))
|
||||
self.text = wx.TextCtrl(panel, -1, text, style=wx.TE_READONLY | wx.TE_MULTILINE)
|
||||
self.text.SetMinSize((500, 300))
|
||||
self.text.SetFocus()
|
||||
textBox = wx.BoxSizer(wx.HORIZONTAL)
|
||||
textBox.Add(label, 0, wx.ALL, 5)
|
||||
textBox.Add(self.text, 1, wx.EXPAND, 5)
|
||||
mainBox = wx.BoxSizer(wx.VERTICAL)
|
||||
mainBox.Add(textBox, 1, wx.EXPAND | wx.ALL, 5)
|
||||
self.spellcheck = wx.Button(panel, -1, _("Check &spelling..."), size=wx.DefaultSize)
|
||||
self.translateButton = wx.Button(panel, -1, _("&Translate..."), size=wx.DefaultSize)
|
||||
cancelButton = wx.Button(panel, wx.ID_CANCEL, _("C&lose"), size=wx.DefaultSize)
|
||||
cancelButton.SetDefault()
|
||||
buttonsBox = wx.BoxSizer(wx.HORIZONTAL)
|
||||
buttonsBox.Add(self.spellcheck, 0, wx.ALL, 5)
|
||||
buttonsBox.Add(self.translateButton, 0, wx.ALL, 5)
|
||||
buttonsBox.Add(cancelButton, 0, wx.ALL, 5)
|
||||
mainBox.Add(buttonsBox, 0, wx.ALL, 5)
|
||||
panel.SetSizer(mainBox)
|
||||
self.SetMinSize((600, 400))
|
||||
self.Layout()
|
||||
|
||||
|
||||
class RepostDialog(wx.Dialog):
|
||||
def __init__(self):
|
||||
super(RepostDialog, self).__init__(None, title=_("Repost"))
|
||||
p = wx.Panel(self)
|
||||
sizer = wx.BoxSizer(wx.VERTICAL)
|
||||
lbl = wx.StaticText(p, wx.ID_ANY, _("What would you like to do with this post?"))
|
||||
sizer.Add(lbl, 0, wx.ALL, 10)
|
||||
|
||||
btn_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
self.btn_repost = wx.Button(p, wx.ID_ANY, _("Repost"))
|
||||
self.btn_quote = wx.Button(p, wx.ID_ANY, _("Quote"))
|
||||
self.btn_cancel = wx.Button(p, wx.ID_CANCEL, _("Cancel"))
|
||||
|
||||
btn_sizer.Add(self.btn_repost, 0, wx.ALL, 5)
|
||||
btn_sizer.Add(self.btn_quote, 0, wx.ALL, 5)
|
||||
btn_sizer.Add(self.btn_cancel, 0, wx.ALL, 5)
|
||||
|
||||
sizer.Add(btn_sizer, 0, wx.ALIGN_CENTER)
|
||||
p.SetSizer(sizer)
|
||||
sizer.Fit(self)
|
||||
|
||||
self.btn_repost.Bind(wx.EVT_BUTTON, self.on_repost)
|
||||
self.btn_quote.Bind(wx.EVT_BUTTON, self.on_quote)
|
||||
self.result = 0
|
||||
|
||||
def on_repost(self, event):
|
||||
self.result = 1
|
||||
self.EndModal(wx.ID_OK)
|
||||
|
||||
def on_quote(self, event):
|
||||
self.result = 2
|
||||
self.EndModal(wx.ID_OK)
|
||||
|
||||
|
||||
def repost_question():
|
||||
dlg = RepostDialog()
|
||||
dlg.ShowModal()
|
||||
result = dlg.result
|
||||
dlg.Destroy()
|
||||
return result
|
||||
|
||||
@@ -0,0 +1,412 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
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)
|
||||
|
||||
self.session = session
|
||||
self.user_identifier = user_identifier
|
||||
self.profile_data = None # Will store the formatted profile dict
|
||||
self.target_user_did = None # Will store the resolved DID of the profile being viewed
|
||||
|
||||
self._init_ui()
|
||||
self.SetMinSize((400, 300))
|
||||
self.CentreOnParent()
|
||||
|
||||
Thread(target=self.load_profile_data, daemon=True).start()
|
||||
|
||||
def _init_ui(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"),
|
||||
(_("&Bio:"), "description")
|
||||
]
|
||||
self.profile_field_ctrls = {}
|
||||
|
||||
for label_text, data_key in fields:
|
||||
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(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
|
||||
|
||||
# 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)
|
||||
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.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)
|
||||
main_sizer.Add(actions_sizer, 0, wx.ALIGN_CENTER | wx.TOP | wx.BOTTOM, 10)
|
||||
|
||||
# Close Button
|
||||
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())
|
||||
|
||||
self.panel.SetSizer(main_sizer)
|
||||
self.Fit()
|
||||
|
||||
def load_profile_data(self):
|
||||
wx.CallAfter(self.SetStatusText, _("Loading profile..."))
|
||||
for ctrl in self.profile_field_ctrls.values():
|
||||
wx.CallAfter(ctrl.SetValue, _("Loading..."))
|
||||
|
||||
# Initially hide all action buttons until state is known
|
||||
wx.CallAfter(self.follow_btn.Hide)
|
||||
wx.CallAfter(self.unfollow_btn.Hide)
|
||||
wx.CallAfter(self.mute_btn.Hide)
|
||||
wx.CallAfter(self.unmute_btn.Hide)
|
||||
wx.CallAfter(self.block_btn.Hide)
|
||||
wx.CallAfter(self.unblock_btn.Hide)
|
||||
|
||||
try:
|
||||
api = self.session._ensure_client()
|
||||
try:
|
||||
raw_profile = api.app.bsky.actor.get_profile({"actor": self.user_identifier})
|
||||
except Exception:
|
||||
raw_profile = None
|
||||
wx.CallAfter(self._apply_profile_data, raw_profile)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading profile for {self.user_identifier}: {e}", exc_info=True)
|
||||
wx.CallAfter(self._apply_profile_error, e)
|
||||
|
||||
def _apply_profile_data(self, raw_profile):
|
||||
if raw_profile:
|
||||
self.profile_data = self._format_profile_data(raw_profile)
|
||||
self.target_user_did = self.profile_data.get("did")
|
||||
self.user_identifier = self.target_user_did or self.user_identifier
|
||||
|
||||
self.update_ui_fields()
|
||||
self.update_action_buttons_state()
|
||||
self.SetTitle(_("Profile: {handle}").format(handle=self.profile_data.get("handle", "")))
|
||||
self.SetStatusText(_("Profile loaded."))
|
||||
else:
|
||||
for ctrl in self.profile_field_ctrls.values():
|
||||
ctrl.SetValue(_("Not found."))
|
||||
self.SetStatusText(_("Profile not found for '{ident}'.").format(ident=self.user_identifier))
|
||||
wx.MessageBox(_("User profile for '{ident}' not found.").format(ident=self.user_identifier), _("Error"), wx.OK | wx.ICON_ERROR, self)
|
||||
self.Layout()
|
||||
|
||||
def _apply_profile_error(self, err):
|
||||
for ctrl in self.profile_field_ctrls.values():
|
||||
ctrl.SetValue(_("Error loading."))
|
||||
self.SetStatusText(_("Error loading profile."))
|
||||
wx.MessageBox(_("Error loading profile: {error}").format(error=str(err)), _("Error"), wx.OK | wx.ICON_ERROR, self)
|
||||
self.Layout()
|
||||
|
||||
def update_ui_fields(self):
|
||||
if not self.profile_data:
|
||||
return
|
||||
|
||||
for key, ctrl in self.profile_field_ctrls.items():
|
||||
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:
|
||||
ctrl.SetValue(value or _("N/A"))
|
||||
|
||||
# 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()
|
||||
self.unfollow_btn.Hide()
|
||||
self.mute_btn.Hide()
|
||||
self.unmute_btn.Hide()
|
||||
self.block_btn.Hide()
|
||||
self.unblock_btn.Hide()
|
||||
self.Layout()
|
||||
return
|
||||
|
||||
viewer_state = self.profile_data.get("viewer", {})
|
||||
is_following = bool(viewer_state.get("following"))
|
||||
is_muted = bool(viewer_state.get("muted"))
|
||||
# 'blocking' in viewer state is the URI of *our* block record, if we are blocking them.
|
||||
is_blocking_them = bool(viewer_state.get("blocking"))
|
||||
# 'blockedBy' means *they* are blocking us. If true, most actions might fail or be hidden.
|
||||
is_blocked_by_them = bool(viewer_state.get("blockedBy"))
|
||||
|
||||
if is_blocked_by_them: # If they block us, we can't do much.
|
||||
self.follow_btn.Hide()
|
||||
self.unfollow_btn.Hide()
|
||||
self.mute_btn.Hide()
|
||||
self.unmute_btn.Hide()
|
||||
# We can still block them, or unblock them if we previously did.
|
||||
self.block_btn.Show(not is_blocking_them)
|
||||
self.unblock_btn.Show(is_blocking_them)
|
||||
self.Layout()
|
||||
return
|
||||
|
||||
self.follow_btn.Show(not is_following and not is_blocking_them)
|
||||
self.unfollow_btn.Show(is_following and not is_blocking_them)
|
||||
|
||||
self.mute_btn.Show(not is_muted and not is_blocking_them)
|
||||
self.unmute_btn.Show(is_muted and not is_blocking_them)
|
||||
|
||||
self.block_btn.Show(not is_blocking_them) # Show block if we are not currently blocking them (even if they block us)
|
||||
self.unblock_btn.Show(is_blocking_them) # Show unblock if we are currently blocking them
|
||||
|
||||
self.Layout() # Refresh sizer to show/hide buttons correctly
|
||||
|
||||
|
||||
def on_user_action(self, event, command: str):
|
||||
if not self.target_user_did: # Should be set by load_profile_data
|
||||
wx.MessageBox(_("User identifier (DID) not available for this action."), _("Error"), wx.OK | wx.ICON_ERROR)
|
||||
return
|
||||
|
||||
# Confirmation for sensitive actions
|
||||
confirmation_map = {
|
||||
"unfollow_user": _("Are you sure you want to unfollow @{handle}?").format(handle=self.profile_data.get("handle","this user")),
|
||||
"block_user": _("Are you sure you want to block @{handle}? This will prevent them from interacting with you and hide their content.").format(handle=self.profile_data.get("handle","this user")),
|
||||
# Unblock usually doesn't need confirmation, but can be added if desired.
|
||||
}
|
||||
if command in confirmation_map:
|
||||
dlg = wx.MessageDialog(self, confirmation_map[command], _("Confirm Action"), wx.YES_NO | wx.ICON_QUESTION)
|
||||
if dlg.ShowModal() != wx.ID_YES:
|
||||
dlg.Destroy()
|
||||
return
|
||||
dlg.Destroy()
|
||||
|
||||
wx.BeginBusyCursor()
|
||||
self.SetStatusText(_("Performing action: {action}...").format(action=command))
|
||||
action_button = event.GetEventObject()
|
||||
if action_button:
|
||||
action_button.Disable()
|
||||
|
||||
try:
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
# 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:
|
||||
action_button.Enable()
|
||||
self.SetStatusText(_("Action failed."))
|
||||
wx.MessageBox(str(e), _("Error"), wx.OK | wx.ICON_ERROR, self)
|
||||
|
||||
def _get_own_did(self):
|
||||
if isinstance(self.session.db, dict):
|
||||
did = self.session.db.get("user_id")
|
||||
if did:
|
||||
return did
|
||||
try:
|
||||
api = self.session._ensure_client()
|
||||
if getattr(api, "me", None):
|
||||
return api.me.did
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
def _format_profile_data(self, profile_model):
|
||||
def g(obj, key, default=None):
|
||||
if isinstance(obj, dict):
|
||||
return obj.get(key, default)
|
||||
return getattr(obj, key, default)
|
||||
|
||||
def get_count(*keys):
|
||||
for k in keys:
|
||||
val = g(profile_model, k)
|
||||
if val is not None:
|
||||
return val
|
||||
return None
|
||||
|
||||
return {
|
||||
"did": g(profile_model, "did"),
|
||||
"handle": g(profile_model, "handle"),
|
||||
"displayName": g(profile_model, "displayName") or g(profile_model, "display_name") or g(profile_model, "handle"),
|
||||
"description": g(profile_model, "description"),
|
||||
"avatar": g(profile_model, "avatar"),
|
||||
"banner": g(profile_model, "banner"),
|
||||
"followersCount": get_count("followersCount", "followers_count"),
|
||||
"followsCount": get_count("followsCount", "follows_count", "followingCount", "following_count"),
|
||||
"postsCount": get_count("postsCount", "posts_count"),
|
||||
"viewer": g(profile_model, "viewer") or {},
|
||||
}
|
||||
|
||||
def SetStatusText(self, text): # Simple status text for dialog title
|
||||
self.SetTitle(f"{_('User Profile')} - {text}")
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import wx
|
||||
|
||||
|
||||
class UserActionsDialog(wx.Dialog):
|
||||
def __init__(self, users=None, default="follow", *args, **kwargs):
|
||||
super(UserActionsDialog, self).__init__(parent=None, *args, **kwargs)
|
||||
users = users or []
|
||||
panel = wx.Panel(self)
|
||||
self.SetTitle(_(u"Action"))
|
||||
|
||||
userSizer = wx.BoxSizer()
|
||||
userLabel = wx.StaticText(panel, -1, _(u"&User"))
|
||||
default_user = users[0] if users else ""
|
||||
self.cb = wx.ComboBox(panel, -1, choices=users, value=default_user)
|
||||
self.cb.SetFocus()
|
||||
self.autocompletion = wx.Button(panel, -1, _(u"&Autocomplete users"))
|
||||
userSizer.Add(userLabel, 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)
|
||||
label2 = wx.StaticText(panel, -1, _(u"Action"))
|
||||
self.follow = wx.RadioButton(panel, -1, _(u"&Follow"), name=_(u"Action"), style=wx.RB_GROUP)
|
||||
self.unfollow = wx.RadioButton(panel, -1, _(u"U&nfollow"))
|
||||
self.mute = wx.RadioButton(panel, -1, _(u"&Mute"))
|
||||
self.unmute = wx.RadioButton(panel, -1, _(u"Unmu&te"))
|
||||
self.block = wx.RadioButton(panel, -1, _(u"&Block"))
|
||||
self.unblock = wx.RadioButton(panel, -1, _(u"Unbl&ock"))
|
||||
self.setup_default(default)
|
||||
|
||||
hSizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
hSizer.Add(label2, 0, wx.ALL, 5)
|
||||
actionSizer.Add(self.follow, 0, wx.ALL, 5)
|
||||
actionSizer.Add(self.unfollow, 0, wx.ALL, 5)
|
||||
actionSizer.Add(self.mute, 0, wx.ALL, 5)
|
||||
actionSizer.Add(self.unmute, 0, wx.ALL, 5)
|
||||
actionSizer.Add(self.block, 0, wx.ALL, 5)
|
||||
actionSizer.Add(self.unblock, 0, wx.ALL, 5)
|
||||
hSizer.Add(actionSizer, 0, wx.ALL, 5)
|
||||
|
||||
sizer = wx.BoxSizer(wx.VERTICAL)
|
||||
ok = wx.Button(panel, wx.ID_OK, _(u"&OK"))
|
||||
ok.SetDefault()
|
||||
cancel = wx.Button(panel, wx.ID_CANCEL, _(u"&Close"))
|
||||
btnsizer = wx.BoxSizer()
|
||||
btnsizer.Add(ok)
|
||||
btnsizer.Add(cancel)
|
||||
sizer.Add(userSizer)
|
||||
sizer.Add(hSizer, 0, wx.ALL, 5)
|
||||
sizer.Add(btnsizer)
|
||||
panel.SetSizer(sizer)
|
||||
|
||||
def get_action(self):
|
||||
if self.follow.GetValue() == True:
|
||||
return "follow"
|
||||
elif self.unfollow.GetValue() == True:
|
||||
return "unfollow"
|
||||
elif self.mute.GetValue() == True:
|
||||
return "mute"
|
||||
elif self.unmute.GetValue() == True:
|
||||
return "unmute"
|
||||
elif self.block.GetValue() == True:
|
||||
return "block"
|
||||
elif self.unblock.GetValue() == True:
|
||||
return "unblock"
|
||||
|
||||
def setup_default(self, default):
|
||||
if default == "follow":
|
||||
self.follow.SetValue(True)
|
||||
elif default == "unfollow":
|
||||
self.unfollow.SetValue(True)
|
||||
elif default == "mute":
|
||||
self.mute.SetValue(True)
|
||||
elif default == "unmute":
|
||||
self.unmute.SetValue(True)
|
||||
elif default == "block":
|
||||
self.block.SetValue(True)
|
||||
elif default == "unblock":
|
||||
self.unblock.SetValue(True)
|
||||
|
||||
def get_response(self):
|
||||
return self.ShowModal()
|
||||
|
||||
def get_user(self):
|
||||
return self.cb.GetValue()
|
||||
|
||||
def get_position(self):
|
||||
return self.cb.GetPosition()
|
||||
|
||||
def popup_menu(self, menu):
|
||||
self.PopupMenu(menu, self.cb.GetPosition())
|
||||
|
||||
@@ -0,0 +1,394 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import wx
|
||||
import logging
|
||||
from pubsub import pub
|
||||
from multiplatform_widgets import widgets # Assuming this provides generic widgets
|
||||
from approve.translation import translate as _ # For Approve's _ shortcut
|
||||
from approve.notifications import NotificationError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Supported languages for posts (ISO 639-1 codes) - can be expanded
|
||||
# This might ideally come from the session or a global config
|
||||
SUPPORTED_LANG_CHOICES = {
|
||||
_("English"): "en",
|
||||
_("Spanish"): "es",
|
||||
_("French"): "fr",
|
||||
_("German"): "de",
|
||||
_("Japanese"): "ja",
|
||||
_("Portuguese"): "pt",
|
||||
_("Russian"): "ru",
|
||||
_("Chinese"): "zh",
|
||||
# Add more as needed
|
||||
}
|
||||
|
||||
class ComposeDialog(wx.Dialog):
|
||||
def __init__(self, parent, session, reply_to_uri: str | None = None, quote_uri: str | None = None, initial_text: str = ""):
|
||||
super(ComposeDialog, self).__init__(parent, title=_("Compose Post"), style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
|
||||
|
||||
self.session = session
|
||||
self.panel_config = self.session.compose_panel.get_panel_configuration()
|
||||
self.reply_to_uri = reply_to_uri
|
||||
self.initial_quote_uri = quote_uri # Store initial quote URI
|
||||
self.current_quote_uri = quote_uri # Mutable quote URI
|
||||
self.attached_files_info = [] # List of dicts: {"path": str, "alt_text": str}
|
||||
|
||||
self._init_ui(initial_text)
|
||||
self.SetMinSize((550, 450)) # Increased min size
|
||||
self.CentreOnParent()
|
||||
|
||||
def _init_ui(self, initial_text: str):
|
||||
panel = wx.Panel(self)
|
||||
main_sizer = wx.BoxSizer(wx.VERTICAL)
|
||||
|
||||
# Reply Info (if applicable)
|
||||
if self.reply_to_uri:
|
||||
# In a real app, fetch & show post snippet or author
|
||||
reply_info_label = wx.StaticText(panel, label=_("Replying to: {uri_placeholder}").format(uri_placeholder=self.reply_to_uri[-10:]))
|
||||
reply_info_label.SetToolTip(self.reply_to_uri)
|
||||
main_sizer.Add(reply_info_label, 0, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, 5)
|
||||
|
||||
# Text Area
|
||||
self.text_ctrl = wx.TextCtrl(panel, style=wx.TE_MULTILINE | wx.TE_RICH2 | wx.HSCROLL)
|
||||
self.text_ctrl.SetValue(initial_text)
|
||||
self.text_ctrl.Bind(wx.EVT_TEXT, self.on_text_changed)
|
||||
main_sizer.Add(self.text_ctrl, 1, wx.EXPAND | wx.ALL, 5)
|
||||
|
||||
# Character Counter
|
||||
self.max_chars = self.panel_config.get("max_chars", 0)
|
||||
self.char_count_label = wx.StaticText(panel, label=f"0 / {self.max_chars if self.max_chars > 0 else 'N/A'}")
|
||||
main_sizer.Add(self.char_count_label, 0, wx.ALIGN_RIGHT | wx.RIGHT | wx.BOTTOM, 5)
|
||||
self.on_text_changed(None)
|
||||
|
||||
# Attachments Area
|
||||
self.max_media_attachments = self.panel_config.get("max_media_attachments", 0)
|
||||
if self.max_media_attachments > 0:
|
||||
attachment_sizer = wx.StaticBoxSizer(wx.VERTICAL, panel, _("Media Attachments") + f" (Max: {self.max_media_attachments})")
|
||||
self.attachment_list = wx.ListBox(attachment_sizer.GetStaticBox(), style=wx.LB_SINGLE, size=(-1, 60)) # Fixed height for listbox
|
||||
attachment_sizer.Add(self.attachment_list, 1, wx.EXPAND | wx.ALL, 5)
|
||||
|
||||
attach_btn_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
self.add_attachment_btn = wx.Button(attachment_sizer.GetStaticBox(), label=_("Add Media..."))
|
||||
self.add_attachment_btn.Bind(wx.EVT_BUTTON, self.on_add_attachment)
|
||||
attach_btn_sizer.Add(self.add_attachment_btn, 0, wx.ALL, 2)
|
||||
|
||||
self.remove_attachment_btn = wx.Button(attachment_sizer.GetStaticBox(), label=_("Remove Selected"))
|
||||
self.remove_attachment_btn.Bind(wx.EVT_BUTTON, self.on_remove_attachment)
|
||||
self.remove_attachment_btn.Enable(False)
|
||||
self.attachment_list.Bind(wx.EVT_LISTBOX, lambda evt: self.remove_attachment_btn.Enable(self.attachment_list.GetSelection() != wx.NOT_FOUND))
|
||||
attach_btn_sizer.Add(self.remove_attachment_btn, 0, wx.ALL, 2)
|
||||
attachment_sizer.Add(attach_btn_sizer, 0, wx.ALIGN_LEFT)
|
||||
main_sizer.Add(attachment_sizer, 0, wx.EXPAND | wx.ALL, 5)
|
||||
|
||||
# Quoting Area
|
||||
if self.panel_config.get("supports_quoting", False):
|
||||
quote_box_sizer = wx.StaticBoxSizer(wx.VERTICAL, panel, _("Quoting Post"))
|
||||
quote_display_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
self.quote_uri_text_display = wx.TextCtrl(quote_box_sizer.GetStaticBox(), value=self.current_quote_uri or _("None"), style=wx.TE_READONLY | wx.BORDER_NONE)
|
||||
self.quote_uri_text_display.SetBackgroundColour(panel.GetBackgroundColour())
|
||||
quote_display_sizer.Add(wx.StaticText(quote_box_sizer.GetStaticBox(), label=_("Quoting URI: ")), 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 2)
|
||||
quote_display_sizer.Add(self.quote_uri_text_display, 1, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 2)
|
||||
quote_box_sizer.Add(quote_display_sizer, 0, wx.EXPAND | wx.LEFT | wx.RIGHT, 2)
|
||||
|
||||
quote_btn_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
self.add_quote_btn = wx.Button(quote_box_sizer.GetStaticBox(), label=_("Set/Change Quote..."))
|
||||
self.add_quote_btn.Bind(wx.EVT_BUTTON, self.on_add_quote)
|
||||
quote_btn_sizer.Add(self.add_quote_btn, 0, wx.ALL, 2)
|
||||
|
||||
self.remove_quote_btn = wx.Button(quote_box_sizer.GetStaticBox(), label=_("Remove Quote"))
|
||||
self.remove_quote_btn.Bind(wx.EVT_BUTTON, self.on_remove_quote)
|
||||
self.remove_quote_btn.Enable(bool(self.current_quote_uri))
|
||||
quote_btn_sizer.Add(self.remove_quote_btn, 0, wx.ALL, 2)
|
||||
quote_box_sizer.Add(quote_btn_sizer, 0, wx.ALIGN_LEFT)
|
||||
main_sizer.Add(quote_box_sizer, 0, wx.EXPAND | wx.ALL, 5)
|
||||
|
||||
# Options (Content Warning, Language)
|
||||
options_box = wx.StaticBoxSizer(wx.VERTICAL, panel, _("Options"))
|
||||
options_grid_sizer = wx.FlexGridSizer(cols=2, vgap=5, hgap=5)
|
||||
options_grid_sizer.AddGrowableCol(1, 1)
|
||||
|
||||
if self.panel_config.get("supports_content_warning", False):
|
||||
self.sensitive_checkbox = wx.CheckBox(options_box.GetStaticBox(), label=_("Sensitive content (CW)"))
|
||||
self.sensitive_checkbox.Bind(wx.EVT_CHECKBOX, self.on_sensitive_changed)
|
||||
options_grid_sizer.Add(self.sensitive_checkbox, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 2)
|
||||
|
||||
self.spoiler_text_ctrl = wx.TextCtrl(options_box.GetStaticBox())
|
||||
self.spoiler_text_ctrl.SetHint(_("Content warning text (optional)"))
|
||||
self.spoiler_text_ctrl.Enable(False)
|
||||
options_grid_sizer.Add(self.spoiler_text_ctrl, 1, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL | wx.ALL, 2)
|
||||
|
||||
if self.panel_config.get("supports_language_selection", False):
|
||||
lang_label = wx.StaticText(options_box.GetStaticBox(), label=_("Languages:"))
|
||||
options_grid_sizer.Add(lang_label, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 2)
|
||||
|
||||
self.max_langs = self.panel_config.get("max_languages", 1)
|
||||
self.lang_choices_map = SUPPORTED_LANG_CHOICES # Using global for now
|
||||
lang_display_names = list(self.lang_choices_map.keys())
|
||||
|
||||
if self.max_langs == 1: # Single choice
|
||||
choices = [_("Automatic")] + lang_display_names
|
||||
self.lang_choice_ctrl = wx.Choice(options_box.GetStaticBox(), choices=choices)
|
||||
self.lang_choice_ctrl.SetSelection(0) # Default to Automatic/None
|
||||
else: # Multiple choices
|
||||
self.lang_choice_ctrl = wx.CheckListBox(options_box.GetStaticBox(), choices=lang_display_names, size=(-1, 70))
|
||||
self.lang_choice_ctrl.Bind(wx.EVT_CHECKLISTBOX, self.on_lang_checklist_changed)
|
||||
options_grid_sizer.Add(self.lang_choice_ctrl, 1, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL | wx.ALL, 2)
|
||||
|
||||
if options_grid_sizer.GetChildren():
|
||||
options_box.Add(options_grid_sizer, 1, wx.EXPAND | wx.ALL, 0) # No border for grid sizer itself
|
||||
main_sizer.Add(options_box, 0, wx.EXPAND | wx.ALL, 5)
|
||||
|
||||
# Buttons (Send, Cancel)
|
||||
btn_sizer = wx.StdDialogButtonSizer()
|
||||
self.send_btn = wx.Button(panel, wx.ID_OK, _("Send"))
|
||||
self.send_btn.SetDefault()
|
||||
self.send_btn.Bind(wx.EVT_BUTTON, self.on_send)
|
||||
btn_sizer.AddButton(self.send_btn)
|
||||
|
||||
cancel_btn = wx.Button(panel, wx.ID_CANCEL, _("Cancel"))
|
||||
btn_sizer.AddButton(cancel_btn)
|
||||
btn_sizer.Realize()
|
||||
main_sizer.Add(btn_sizer, 0, wx.ALIGN_CENTER | wx.ALL, 5)
|
||||
|
||||
panel.SetSizer(main_sizer)
|
||||
self.Fit()
|
||||
|
||||
|
||||
def on_text_changed(self, event):
|
||||
text_length = len(self.text_ctrl.GetValue())
|
||||
self.char_count_label.SetLabel(f"{text_length} / {self.max_chars}")
|
||||
if self.max_chars > 0 and text_length > self.max_chars:
|
||||
self.char_count_label.SetForegroundColour(wx.RED)
|
||||
else:
|
||||
self.char_count_label.SetForegroundColour(wx.BLACK) # System default
|
||||
|
||||
def on_add_attachment(self, event):
|
||||
max_attachments = self.panel_config.get("max_media_attachments", 0)
|
||||
if len(self.attached_files_info) >= self.max_media_attachments:
|
||||
wx.MessageBox(_("Maximum number of attachments ({max}) reached.").format(max=self.max_media_attachments), _("Attachment Limit"), wx.OK | wx.ICON_INFORMATION)
|
||||
return
|
||||
|
||||
supported_mimes = self.panel_config.get("supported_media_types", [])
|
||||
wildcard_parts = []
|
||||
if not supported_mimes: # Default if none specified by session
|
||||
wildcard_parts.append("All files (*.*)|*.*")
|
||||
else:
|
||||
for mime_type in supported_mimes:
|
||||
# Example: "image/jpeg" -> "JPEG files (*.jpg;*.jpeg)|*.jpg;*.jpeg"
|
||||
name = mime_type.split('/')[0].capitalize() + " " + mime_type.split('/')[1].upper()
|
||||
if mime_type == "image/jpeg": exts = "*.jpg;*.jpeg"
|
||||
elif mime_type == "image/png": exts = "*.png"
|
||||
elif mime_type == "image/gif": exts = "*.gif" # If supported
|
||||
else: exts = "*." + mime_type.split('/')[-1]
|
||||
wildcard_parts.append(f"{name} ({exts})|{exts}")
|
||||
|
||||
wildcard = "|".join(wildcard_parts) if wildcard_parts else wx.FileSelectorDefaultWildcardStr
|
||||
|
||||
dialog = wx.FileDialog(self, _("Select Media File"), wildcard=wildcard, style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST)
|
||||
if dialog.ShowModal() == wx.ID_OK:
|
||||
path = dialog.GetPath()
|
||||
alt_text = ""
|
||||
if self.panel_config.get("supports_alternative_text", False) and \
|
||||
any(pt in path.lower() for pt in ['.jpg', '.jpeg', '.png']): # crude check for image
|
||||
alt_text_dialog = wx.TextEntryDialog(self, _("Enter accessibility description (alt text) for the image:"), _("Image Description"))
|
||||
if alt_text_dialog.ShowModal() == wx.ID_OK:
|
||||
alt_text = alt_text_dialog.GetValue()
|
||||
alt_text_dialog.Destroy()
|
||||
|
||||
self.attached_files_info.append({"path": path, "alt_text": alt_text})
|
||||
self.attachment_list.Append(os.path.basename(path) + (f" ({_('Alt:')} {alt_text})" if alt_text else ""))
|
||||
dialog.Destroy()
|
||||
|
||||
def on_remove_attachment(self, event):
|
||||
selected_index = self.attachment_list.GetSelection()
|
||||
if selected_index != wx.NOT_FOUND:
|
||||
self.attachment_list.Delete(selected_index)
|
||||
del self.attached_files_info[selected_index]
|
||||
|
||||
def on_add_quote(self, event):
|
||||
dialog = wx.TextEntryDialog(self, _("Enter the AT-URI of the Bluesky post to quote:"), _("Quote Post"), self.current_quote_uri or "")
|
||||
if dialog.ShowModal() == wx.ID_OK:
|
||||
self.current_quote_uri = dialog.GetValue().strip()
|
||||
self.quote_uri_text_display.SetValue(self.current_quote_uri or _("None"))
|
||||
self.remove_quote_btn.Enable(bool(self.current_quote_uri))
|
||||
dialog.Destroy()
|
||||
|
||||
def on_remove_quote(self, event):
|
||||
self.current_quote_uri = None
|
||||
self.quote_uri_text_display.SetValue(_("None"))
|
||||
self.remove_quote_btn.Enable(False)
|
||||
|
||||
|
||||
def on_sensitive_changed(self, event):
|
||||
if hasattr(self, 'spoiler_text_ctrl'):
|
||||
self.spoiler_text_ctrl.Enable(event.IsChecked())
|
||||
if event.IsChecked():
|
||||
self.spoiler_text_ctrl.SetFocus()
|
||||
|
||||
def on_lang_checklist_changed(self, event):
|
||||
"""Ensure no more than max_languages are selected for CheckListBox."""
|
||||
if isinstance(self.lang_choice_ctrl, wx.CheckListBox):
|
||||
checked_indices = self.lang_choice_ctrl.GetCheckedItems()
|
||||
if len(checked_indices) > self.max_langs:
|
||||
# Find the item that was just checked to cause the overflow
|
||||
# This is a bit tricky as EVT_CHECKLISTBOX triggers after the change.
|
||||
# A simpler approach is to inform the user and let them uncheck.
|
||||
wx.MessageBox(
|
||||
_("You can select a maximum of {num} languages.").format(num=self.max_langs),
|
||||
_("Language Selection Limit"), wx.OK | wx.ICON_EXCLAMATION
|
||||
)
|
||||
# Optionally, uncheck the last checked item if possible to determine
|
||||
# For now, just warn. User has to manually correct.
|
||||
|
||||
|
||||
def on_send(self, event): # Renamed from async on_send
|
||||
text_content = self.text_ctrl.GetValue()
|
||||
if not text_content.strip() and not self.attached_files_info and not self.current_quote_uri:
|
||||
wx.MessageBox(_("Cannot send an empty post."), _("Error"), wx.OK | wx.ICON_ERROR)
|
||||
return
|
||||
|
||||
# Language processing
|
||||
langs = []
|
||||
if hasattr(self, 'lang_choice_ctrl'):
|
||||
if isinstance(self.lang_choice_ctrl, wx.Choice):
|
||||
sel_idx = self.lang_choice_ctrl.GetSelection()
|
||||
if sel_idx > 0: # Index 0 is empty/no selection
|
||||
lang_display_name = self.lang_choice_ctrl.GetString(sel_idx)
|
||||
langs.append(self.lang_choices_map[lang_display_name])
|
||||
elif isinstance(self.lang_choice_ctrl, wx.CheckListBox):
|
||||
checked_indices = self.lang_choice_ctrl.GetCheckedItems()
|
||||
if len(checked_indices) > self.max_langs:
|
||||
wx.MessageBox(_("Please select no more than {num} languages.").format(num=self.max_langs), _("Language Error"), wx.OK | wx.ICON_ERROR)
|
||||
return
|
||||
for idx in checked_indices:
|
||||
lang_display_name = self.lang_choice_ctrl.GetString(idx)
|
||||
langs.append(self.lang_choices_map[lang_display_name])
|
||||
|
||||
# Files and Alt Texts
|
||||
files_to_send = [f_info["path"] for f_info in self.attached_files_info]
|
||||
alt_texts_to_send = [f_info["alt_text"] for f_info in self.attached_files_info]
|
||||
|
||||
# Content Warning
|
||||
cw_text = None
|
||||
is_sensitive_flag = False
|
||||
if hasattr(self, 'sensitive_checkbox') and self.sensitive_checkbox.IsChecked():
|
||||
is_sensitive_flag = True
|
||||
if hasattr(self, 'spoiler_text_ctrl'):
|
||||
cw_text = self.spoiler_text_ctrl.GetValue().strip() or None # Use None if empty for Bluesky
|
||||
|
||||
kwargs_for_send = {
|
||||
"quote_uri": self.current_quote_uri,
|
||||
"langs": langs if langs else None,
|
||||
"media_alt_texts": alt_texts_to_send if alt_texts_to_send else None,
|
||||
# "tags" could be extracted from text server-side or client-side (not implemented here)
|
||||
}
|
||||
|
||||
# Filter out None values from kwargs to avoid sending them if not set
|
||||
kwargs_for_send = {k: v for k, v in kwargs_for_send.items() if v is not None}
|
||||
|
||||
try:
|
||||
self.send_btn.Disable()
|
||||
# This is an async call, so it should be handled appropriately in wxPython
|
||||
# For simplicity in this step, assuming it's handled by the caller or a wrapper
|
||||
# In a real wxPython app, this would involve asyncio.create_task and wx.CallAfter
|
||||
# or running the send in a separate thread and using wx.CallAfter for UI updates.
|
||||
# For now, we'll make this method async and let the caller handle it.
|
||||
|
||||
# wx.BeginBusyCursor() # Indicate work
|
||||
# Using pubsub to decouple UI from direct async call to session
|
||||
pub.sendMessage(
|
||||
"compose_dialog.send_post",
|
||||
session=self.session,
|
||||
text=text_content,
|
||||
files=files_to_send if files_to_send else None,
|
||||
reply_to=self.reply_to_uri,
|
||||
cw_text=cw_text,
|
||||
is_sensitive=is_sensitive_flag,
|
||||
kwargs=kwargs_for_send
|
||||
)
|
||||
# Success will be signaled by another pubsub message if needed, or just close.
|
||||
# self.EndModal(wx.ID_OK) # Moved to controller after successful send via pubsub
|
||||
|
||||
except NotificationError as e:
|
||||
wx.MessageBox(str(e), _("Post Error"), wx.OK | wx.ICON_ERROR)
|
||||
except Exception as e:
|
||||
logger.error("Error sending post from compose dialog: %s", e, exc_info=True)
|
||||
wx.MessageBox(_("An unexpected error occurred: {error}").format(error=str(e)), _("Error"), wx.OK | wx.ICON_ERROR)
|
||||
finally:
|
||||
# wx.EndBusyCursor()
|
||||
if not self.IsBeingDeleted(): # Ensure dialog still exists
|
||||
self.send_btn.Enable()
|
||||
# Do not automatically close here; let the controller do it on success signal.
|
||||
# self.EndModal(wx.ID_OK) # if successful and no further UI feedback needed in dialog
|
||||
|
||||
def get_data(self):
|
||||
"""Helper to get all data, though on_send handles it directly."""
|
||||
# This method isn't strictly necessary if on_send does all the work,
|
||||
# but can be useful for other patterns.
|
||||
pass
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Example usage (requires a mock session and panel_config)
|
||||
app = wx.App(False)
|
||||
|
||||
class MockComposePanel:
|
||||
def get_panel_configuration(self):
|
||||
return {
|
||||
"max_chars": 300,
|
||||
"max_media_attachments": 4,
|
||||
"supported_media_types": ["image/jpeg", "image/png"],
|
||||
"supports_alternative_text": True,
|
||||
"supports_content_warning": True,
|
||||
"supports_language_selection": True,
|
||||
"max_languages": 3,
|
||||
"supports_quoting": True,
|
||||
}
|
||||
|
||||
class MockSession:
|
||||
def __init__(self):
|
||||
self.compose_panel = MockComposePanel()
|
||||
self.uid = "mock_user" # Needed by some base methods if called
|
||||
|
||||
async def send_message(self, message, files=None, reply_to=None, cw_text=None, is_sensitive=False, **kwargs):
|
||||
print("MockSession.send_message called:")
|
||||
print(f" Text: {message}")
|
||||
print(f" Files: {files}")
|
||||
print(f" Reply To: {reply_to}")
|
||||
print(f" CW: {cw_text}, Sensitive: {is_sensitive}")
|
||||
print(f" kwargs: {kwargs}")
|
||||
# Simulate success or failure
|
||||
# raise NotificationError("This is a mock send error!")
|
||||
return "at://did:plc:mockposturi/app.bsky.feed.post/mockrkey"
|
||||
|
||||
# Pubsub listener for the send_post event (simulates what mainController would do)
|
||||
def on_actual_send(session, text, files, reply_to, cw_text, is_sensitive, kwargs):
|
||||
print("Pubsub: compose_dialog.send_post received. Calling session.send_message...")
|
||||
async def do_send():
|
||||
try:
|
||||
uri = await session.send_message(
|
||||
message=text,
|
||||
files=files,
|
||||
reply_to=reply_to,
|
||||
cw_text=cw_text,
|
||||
is_sensitive=is_sensitive,
|
||||
**kwargs
|
||||
)
|
||||
print(f"Pubsub: Send successful, URI: {uri}")
|
||||
# In real app, would call dialog.EndModal(wx.ID_OK) via wx.CallAfter
|
||||
wx.CallAfter(dialog.EndModal, wx.ID_OK)
|
||||
except Exception as e:
|
||||
print(f"Pubsub: Send failed: {e}")
|
||||
# In real app, show error and re-enable send button in dialog via wx.CallAfter
|
||||
wx.CallAfter(wx.MessageBox, str(e), "Error", wx.OK | wx.ICON_ERROR, dialog)
|
||||
wx.CallAfter(dialog.send_btn.Enable, True)
|
||||
|
||||
asyncio.create_task(do_send())
|
||||
|
||||
pub.subscribe(on_actual_send, "compose_dialog.send_post")
|
||||
|
||||
session = MockSession()
|
||||
# Example: dialog = ComposeDialog(None, session, reply_to_uri="at://reply_uri", quote_uri="at://quote_uri", initial_text="Hello")
|
||||
dialog = ComposeDialog(None, session, initial_text="Hello Bluesky!")
|
||||
dialog.ShowModal()
|
||||
dialog.Destroy()
|
||||
app.MainLoop()
|
||||
@@ -26,8 +26,15 @@ class UserListDialog(wx.Dialog):
|
||||
buttons_sizer.Add(self.actions_button, 0, wx.RIGHT, 10)
|
||||
self.details_button = wx.Button(panel, wx.ID_ANY, _("&View profile"))
|
||||
buttons_sizer.Add(self.details_button, 0, wx.RIGHT, 10)
|
||||
self.load_more_button = wx.Button(panel, wx.ID_ANY, _("&Load more"))
|
||||
self.load_more_button.Hide()
|
||||
buttons_sizer.Add(self.load_more_button, 0, wx.RIGHT, 10)
|
||||
close_button = wx.Button(panel, wx.ID_CANCEL, "&Close")
|
||||
buttons_sizer.Add(close_button, 0)
|
||||
main_sizer.Add(buttons_sizer, 0, wx.ALIGN_CENTER | wx.BOTTOM, 15)
|
||||
panel.SetSizer(main_sizer)
|
||||
# self.SetSizerAndFit(main_sizer)
|
||||
|
||||
def add_users(self, users):
|
||||
for user in users:
|
||||
self.user_list.Append(user)
|
||||
|
||||
Reference in New Issue
Block a user