diff --git a/doc/changelog.md b/doc/changelog.md index e075ef1d..fed7255a 100644 --- a/doc/changelog.md +++ b/doc/changelog.md @@ -7,6 +7,7 @@ TWBlue Changelog * The translation module has been rewritten. Now, instead of offering translations with Google Translator, the user can choose between [LibreTranslate,](https://github.com/LibreTranslate/LibreTranslate) which requires no configuration thanks to the [instance of the NVDA Spanish community;](https://translate.nvda.es) or translate using [DeepL,](https://deepl.com) for which it is necessary to create an account on DeepL and [subscribe to a DeepL API Free plan](https://support.deepl.com/hc/en-us/articles/360021200939-DeepL-API-Free) to obtain the API key which can be used to translate up to 500000 characters every month. The API key can be entered in the global options dialog, under a new tab called translation services. When translating a text, the translation engine can be changed. When changing the translation engine, the target language must be selected again before translation takes place. * TWBlue should be able to switch to Windows 11 Keymap when running under Windows 11. ([#494](https://github.com/mcv-software/twblue/issues/494)) * Mastodon: + * When viewing a post, a button displays the number of boosts and times it has been added to favorites. Clicking on that button will open a list of users who have interacted with the post. From that list, it is possible to view profiles and perform common user actions. * Fixed an error that caused TWBlue to be unable to properly display the user action dialog from the followers or following buffer. ([#575](https://github.com/mcv-software/twblue/issues/575)) ## changes in version 2024.01.05 diff --git a/src/controller/buffers/mastodon/base.py b/src/controller/buffers/mastodon/base.py index 370d3233..cc10f517 100644 --- a/src/controller/buffers/mastodon/base.py +++ b/src/controller/buffers/mastodon/base.py @@ -550,7 +550,7 @@ class BaseBuffer(base.Buffer): output.speak(_("No status found with that ID")) return # print(post) - msg = messages.viewPost(item, offset_hours=self.session.db["utc_offset"], item_url=self.get_item_url(item=item)) + msg = messages.viewPost(self.session, item, offset_hours=self.session.db["utc_offset"], item_url=self.get_item_url(item=item)) def ocr_image(self): post = self.get_item() diff --git a/src/controller/mastodon/messages.py b/src/controller/mastodon/messages.py index 016f7f9f..106aaa64 100644 --- a/src/controller/mastodon/messages.py +++ b/src/controller/mastodon/messages.py @@ -10,6 +10,7 @@ from controller import messages from sessions.mastodon import templates from wxUI.dialogs.mastodon import postDialogs from extra.autocompletionUsers import completion +from . import userList def character_count(post_text, post_cw, character_limit=500): # We will use text for counting character limit only. @@ -235,9 +236,11 @@ class post(messages.basicMessage): self.message.visibility.SetSelection(setting) class viewPost(post): - def __init__(self, post, offset_hours=0, date="", item_url=""): + def __init__(self, session, post, offset_hours=0, date="", item_url=""): + self.session = session if post.reblog != None: post = post.reblog + self.post_id = post.id author = post.account.display_name if post.account.display_name != "" else post.account.username title = _(u"Post from {}").format(author) image_description = templates.process_image_descriptions(post.media_attachments) @@ -264,12 +267,24 @@ class viewPost(post): widgetUtils.connect_event(self.message.share, widgetUtils.BUTTON_PRESSED, self.share) self.item_url = item_url widgetUtils.connect_event(self.message.translateButton, widgetUtils.BUTTON_PRESSED, self.translate) + widgetUtils.connect_event(self.message.boosts_button, widgetUtils.BUTTON_PRESSED, self.on_boosts) + widgetUtils.connect_event(self.message.favorites_button, widgetUtils.BUTTON_PRESSED, self.on_favorites) self.message.ShowModal() # We won't need text_processor in this dialog, so let's avoid it. def text_processor(self): pass + def on_boosts(self, *args, **kwargs): + users = self.session.api.status_reblogged_by(self.post_id) + title = _("people who boosted this post") + user_list = userList.MastodonUserList(session=self.session, users=users, title=title) + + def on_favorites(self, *args, **kwargs): + users = self.session.api.status_favourited_by(self.post_id) + title = _("people who favorited this post") + user_list = userList.MastodonUserList(session=self.session, users=users, title=title) + def share(self, *args, **kwargs): if hasattr(self, "item_url"): output.copy(self.item_url) diff --git a/src/controller/mastodon/userList.py b/src/controller/mastodon/userList.py new file mode 100644 index 00000000..1be5a8f0 --- /dev/null +++ b/src/controller/mastodon/userList.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +from mastodon import MastodonError +from wxUI.dialogs.mastodon import showUserProfile +from controller.userList import UserListController +from . import userActions + +class MastodonUserList(UserListController): + + def process_users(self, users): + return [dict(id=user.id, display_name=f"{user.display_name} (@{user.acct})", acct=user.acct) for user in users] + + def on_actions(self, *args, **kwargs): + user = self.dialog.user_list.GetSelection() + user_account = self.users[user] + u = userActions.userActions(self.session, [user_account.get("acct")]) + + def on_details(self, *args, **kwargs): + user = self.dialog.user_list.GetSelection() + user_id = self.users[user].get("id") + try: + user_object = self.session.api.account(user_id) + except MastodonError: + return + dlg = showUserProfile.ShowUserProfile(user_object) + dlg.ShowModal() diff --git a/src/controller/userList.py b/src/controller/userList.py new file mode 100644 index 00000000..340a9823 --- /dev/null +++ b/src/controller/userList.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +import widgetUtils +from pubsub import pub +from wxUI.dialogs import userList + +class UserListController(object): + def __init__(self, users, session, title): + super(UserListController, self).__init__() + self.session = session + self.users = self.process_users(users) + self.dialog = userList.UserListDialog(title=title, users=[user.get("display_name", user.get("acct")) for user in self.users]) + widgetUtils.connect_event(self.dialog.actions_button, widgetUtils.BUTTON_PRESSED, self.on_actions) + widgetUtils.connect_event(self.dialog.details_button, widgetUtils.BUTTON_PRESSED, self.on_details) + self.dialog.ShowModal() + + def process_users(self, users): + return {} + + def on_actions(self): + pass + + def on_details(self, *args, **kwargs): + pass \ No newline at end of file diff --git a/src/wxUI/dialogs/mastodon/convert to 480p.txt b/src/wxUI/dialogs/mastodon/convert to 480p.txt deleted file mode 100644 index 8ccf82ac..00000000 --- a/src/wxUI/dialogs/mastodon/convert to 480p.txt +++ /dev/null @@ -1,7 +0,0 @@ -ffmpeg -hwaccel qsv -c:v h264_qsv -i "$1" -map 0 -c copy -c:v hevc_qsv -preset slow -global_quality 22 -look_ahead 1 "final/${1%.*}.mkv" - -#!/bin/bash -for i in *.wmv; -do - ffmpeg -i "$i" -c:v libx264 -c:a aac -vf scale=640:480 -crf 20 "final/$i.mp4" -done diff --git a/src/wxUI/dialogs/mastodon/postDialogs.py b/src/wxUI/dialogs/mastodon/postDialogs.py index 2e2f1ef8..e627a0cb 100644 --- a/src/wxUI/dialogs/mastodon/postDialogs.py +++ b/src/wxUI/dialogs/mastodon/postDialogs.py @@ -164,80 +164,81 @@ class Post(wx.Dialog): def unable_to_attach_poll(self, *args, **kwargs): return wx.MessageDialog(self, _("You can add a poll or media files. In order to add your poll, please remove other attachments first."), _("Error adding poll"), wx.ICON_ERROR).ShowModal() +import wx + class viewPost(wx.Dialog): - def set_title(self, lenght): - self.SetTitle(_("Post - %i characters ") % (lenght,)) + def set_title(self, length): + self.SetTitle(_("Post - %i characters ") % length) def __init__(self, text="", boosts_count=0, favs_count=0, source="", date="", privacy="", *args, **kwargs): - super(viewPost, self).__init__(parent=None, id=wx.ID_ANY, size=(850,850)) + super(viewPost, self).__init__(parent=None, id=wx.ID_ANY, size=(850, 850)) + self.init_ui(text, boosts_count, favs_count, source, date, privacy) + + def init_ui(self, text, boosts_count, favs_count, source, date, privacy): panel = wx.Panel(self) - label = wx.StaticText(panel, -1, _("Post")) - self.text = wx.TextCtrl(panel, -1, text, style=wx.TE_READONLY|wx.TE_MULTILINE, size=(250, 180)) - 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, 0, wx.ALL, 5) - label2 = wx.StaticText(panel, -1, _("Image description")) - self.image_description = wx.TextCtrl(panel, -1, style=wx.TE_READONLY|wx.TE_MULTILINE, size=(250, 180)) + 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, boosts_count, favs_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) - iBox = wx.BoxSizer(wx.HORIZONTAL) - iBox.Add(label2, 0, wx.ALL, 5) - iBox.Add(self.image_description, 1, wx.EXPAND, 5) - mainBox.Add(iBox, 0, wx.ALL, 5) - privacyLabel = wx.StaticText(panel, -1, _("Privacy")) - privacy = wx.TextCtrl(panel, -1, privacy, size=wx.DefaultSize, style=wx.TE_READONLY|wx.TE_MULTILINE) - privacyBox = wx.BoxSizer(wx.HORIZONTAL) - privacyBox.Add(privacyLabel, 0, wx.ALL, 5) - privacyBox.Add(privacy, 0, wx.ALL, 5) - boostsCountLabel = wx.StaticText(panel, -1, _(u"Boosts: ")) - boostsCount = wx.TextCtrl(panel, -1, str(boosts_count), size=wx.DefaultSize, style=wx.TE_READONLY|wx.TE_MULTILINE) - boostBox = wx.BoxSizer(wx.HORIZONTAL) - boostBox.Add(boostsCountLabel, 0, wx.ALL, 5) - boostBox.Add(boostsCount, 0, wx.ALL, 5) - favsCountLabel = wx.StaticText(panel, -1, _("Favorites: ")) - favsCount = wx.TextCtrl(panel, -1, str(favs_count), size=wx.DefaultSize, style=wx.TE_READONLY|wx.TE_MULTILINE) - favsBox = wx.BoxSizer(wx.HORIZONTAL) - favsBox.Add(favsCountLabel, 0, wx.ALL, 5) - favsBox.Add(favsCount, 0, wx.ALL, 5) - sourceLabel = wx.StaticText(panel, -1, _("Source: ")) - source = wx.TextCtrl(panel, -1, source, size=wx.DefaultSize, style=wx.TE_READONLY|wx.TE_MULTILINE) - sourceBox = wx.BoxSizer(wx.HORIZONTAL) - sourceBox.Add(sourceLabel, 0, wx.ALL, 5) - sourceBox.Add(source, 0, wx.ALL, 5) - dateLabel = wx.StaticText(panel, -1, _(u"Date: ")) - date = wx.TextCtrl(panel, -1, date, size=wx.DefaultSize, style=wx.TE_READONLY|wx.TE_MULTILINE) - dateBox = wx.BoxSizer(wx.HORIZONTAL) - dateBox.Add(dateLabel, 0, wx.ALL, 5) - dateBox.Add(date, 0, wx.ALL, 5) - infoBox = wx.BoxSizer(wx.HORIZONTAL) - infoBox.Add(privacyBox, 0, wx.ALL, 5) - infoBox.Add(boostBox, 0, wx.ALL, 5) - infoBox.Add(favsBox, 0, wx.ALL, 5) - infoBox.Add(sourceBox, 0, wx.ALL, 5) - mainBox.Add(infoBox, 0, wx.ALL, 5) - mainBox.Add(dateBox, 0, wx.ALL, 5) + sizer.Add(self.image_description, 1, wx.EXPAND | wx.ALL, 5) + return sizer + + def create_info_section(self, panel, privacy, boosts_count, favs_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_boosts_section(panel, boosts_count), 1, wx.EXPAND | wx.ALL, 5) + flex_sizer.Add(self.create_favorites_section(panel, favs_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_boosts_section(self, panel, boosts_count): + sizer = wx.StaticBoxSizer(wx.StaticBox(panel, wx.ID_ANY, _("Boosts")), wx.VERTICAL) + self.boosts_button = wx.Button(panel, -1, str(boosts_count)) + self.boosts_button.SetToolTip(_("View users who boosted this post")) + sizer.Add(self.boosts_button, 1, wx.EXPAND | wx.ALL, 5) + return sizer + + def create_favorites_section(self, panel, favs_count): + sizer = wx.StaticBoxSizer(wx.StaticBox(panel, wx.ID_ANY, _("Favorites")), wx.VERTICAL) + self.favorites_button = wx.Button(panel, -1, str(favs_count)) + self.favorites_button.SetToolTip(_("View users who favorited this post")) + sizer.Add(self.favorites_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, -1, _("Check &spelling..."), size=wx.DefaultSize) - self.translateButton = wx.Button(panel, -1, _(u"&Translate..."), size=wx.DefaultSize) - cancelButton = wx.Button(panel, wx.ID_CANCEL, _(u"C&lose"), size=wx.DefaultSize) + 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() - buttonsBox = wx.BoxSizer(wx.HORIZONTAL) - buttonsBox.Add(self.share, 0, wx.ALL, 5) - 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) - selectId = wx.ID_ANY - self.Bind(wx.EVT_MENU, self.onSelect, id=selectId) - self.accel_tbl = wx.AcceleratorTable([ - (wx.ACCEL_CTRL, ord('A'), selectId), - ]) - self.SetAcceleratorTable(self.accel_tbl) - panel.SetSizer(mainBox) - self.SetClientSize(mainBox.CalcMin()) + 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) diff --git a/src/wxUI/dialogs/userList.py b/src/wxUI/dialogs/userList.py new file mode 100644 index 00000000..5a8b01c2 --- /dev/null +++ b/src/wxUI/dialogs/userList.py @@ -0,0 +1,32 @@ +import wx + +class UserListDialog(wx.Dialog): + def __init__(self, parent=None, title="", users=[]): + super(UserListDialog, self).__init__(parent=parent, title=title, size=(400, 300)) + self.users = users + self.init_ui() + + def init_ui(self): + panel = wx.Panel(self) + main_sizer = wx.BoxSizer(wx.VERTICAL) + title_text = wx.StaticText(panel, label=self.GetTitle()) + title_font = title_text.GetFont() + title_font.PointSize += 2 + title_font = title_font.Bold() + title_text.SetFont(title_font) + main_sizer.Add(title_text, 0, wx.ALIGN_CENTER | wx.TOP, 10) + user_list_box = wx.StaticBox(panel, wx.ID_ANY, "Users") + user_list_sizer = wx.StaticBoxSizer(user_list_box, wx.VERTICAL) + self.user_list = wx.ListBox(panel, wx.ID_ANY, choices=self.users, style=wx.LB_SINGLE) + user_list_sizer.Add(self.user_list, 1, wx.EXPAND | wx.ALL, 10) + main_sizer.Add(user_list_sizer, 1, wx.EXPAND | wx.ALL, 15) + buttons_sizer = wx.BoxSizer(wx.HORIZONTAL) + self.actions_button = wx.Button(panel, wx.ID_ANY, "Actions") + 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) + 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)