Compare commits

..

No commits in common. "next-gen" and "release" have entirely different histories.

84 changed files with 17940 additions and 32662 deletions

View File

@ -1,13 +0,0 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "pip" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"
ignore:
- dependency-name: "lief"

View File

@ -34,16 +34,11 @@ jobs:
cd scripts cd scripts
makensis twblue.nsi makensis twblue.nsi
- name: Create portable
working-directory: scripts\TWBlue64
run: |
7z a -tzip TWBlue_portable.zip .
- name: Create new release - name: Create new release
env: env:
gh_token: ${{ github.token }} gh_token: ${{ github.token }}
run: | run: |
mkdir .release-assets mkdir .release-assets
mv scripts\TWBlue_setup.exe .release-assets\TWBlue_setup_${{github.ref_name}}.exe mv scripts\TWBlue_setup.exe .release-assets\TWBlue_setup_${{github.ref_name}}.exe
mv scripts\TWBlue64\TWBlue_portable.zip .release-assets\TWBlue_portable_${{github.ref_name}}.zip 7z a -tzip .release-assets\TWBlue_portable_${{github.ref_name}}.zip scripts\TWBlue64
gh release create "${{github.ref_name}}" -F "release-notes.md" -t "${{github.ref_name}}" .release-assets\TWBlue_setup_${{github.ref_name}}.exe .release-assets\TWBlue_portable_${{github.ref_name}}.zip gh release create release -F "release-notes.md" -t "${{github.ref_name}}" .release-assets\TWBlue_setup_${{github.ref_name}}.exe .release-assets\TWBlue_portable_${{github.ref_name}}.zip

View File

@ -75,7 +75,7 @@ Now that you have installed all these packages, you can run TW Blue from source
To generate the documentation in html format, navigate to the doc folder inside this repo. After that, run these commands: To generate the documentation in html format, navigate to the doc folder inside this repo. After that, run these commands:
`python documentation_importer.py` `python document_importer.py`
`python generator.py` `python generator.py`
The documentation will be generated, placing each language in a separate folder in the doc directory. Move these folders (for example `de`, `en`, `es`, `fr`, `it`, ...) to `src/documentation`, creating the directory if necessary. The documentation will be generated, placing each language in a separate folder in the doc directory. Move these folders (for example `de`, `en`, `es`, `fr`, `it`, ...) to `src/documentation`, creating the directory if necessary.
@ -108,4 +108,4 @@ To manage translations in TWBlue, you can install the [Babel package.](https://p
pybabel extract -o twblue.pot --msgid-bugs-address "manuel@manuelcortez.net" --copyright-holder "MCV software" --input-dirs ..\src pybabel extract -o twblue.pot --msgid-bugs-address "manuel@manuelcortez.net" --copyright-holder "MCV software" --input-dirs ..\src
``` ```
Take into account, though, that we use [weblate](https://weblate.mcvsoftware.com) to track translation work for TWBlue. If you wish to be part of our translation team, please open an issue so we can create an account for you in Weblate. Take into account, though, that we use [weblate](https://weblate.mcvsoftware.com) to track translation work for TWBlue. If you wish to be part of our translation team, please open an issue so we can create an account for you in Weblate.

View File

@ -2,43 +2,6 @@ TWBlue Changelog
## changes in this version ## changes in this version
In this version, we have focused on providing initial support for Mastodon filters and pinned posts. From TWBlue, it is now possible to initially use filters for posts in most buffers, as well as manage them (create, edit, and delete filters, in addition to adding keywords). A new variable has also been added for post templates in the invisible interface that allows displaying whether a post has been pinned by its author.
* Mastodon:
* Added filters support to TWBlue. Filters are only implemented in posts for the moment. TWBlue will, depending in the selected settings, hide behind a content warning or completely ignore a post based on filters. Also it is possible to add, delete or edit filters from the buffer menu in the menu bar.
* A language selector has been added for posting in TWBlue. It is now possible to choose the language in which a post will be made, which will be useful for content filtering and other language-dependent features. The default language can be chosen based on your Mastodon accounts language, the language of the post youre replying to, or, if no automatic selection is possible, TWBlues own language will be used by default.
* TWBlue now supports announcing (via a new template variable for posts) pinned posts. You can edit your posts template and use the $pinned variable. When reading the post, TWBlue will indicate if the post is pinned. Also, when loading an user timeline, pinned posts will be loaded at the top or bottom of the buffer according to local settings.
* TWBlue should be able to display all posts in the post displayer dialog.
* reading long posts in the graphical user interface should work better.
## Changes in version 2024.5.23
* Core:
* The way sessions are named has been changed. Now the account is indicated first, followed by the social network it belongs to.
* An option has been added to the global options dialog that allows for the reading of long posts in the graphical interface. This is especially useful since, by default, the graphical interface can only display a limited number of characters in the post.
* Some options that are no longer necessary in the application have been removed from the global settings dialog.
* Mastodon:
* fixed an error that caused TWBlue to not display some posts correctly.
* Fixed name for community timelines when created during startup. Now it should be clear if it's a federated or local timeline.
* Defined shortcuts to fields on the update profile dialog so it will be easier to navigate.
* Now it is possible to load conversations directly from community timelines.
## Changes in version 2024.5.19
In this version of TWBlue, which is being released several months after the previous one, we focused on adding initial support for GoToSocial-type networks. GoToSocial is a server for creating decentralized networks similar to Mastodon. Its API is very similar but retains some differences. In this version, TWBlue can be used to log into GoToSocial accounts, although there will be some features, such as the Streaming API and Markdown support, that are not yet functional. Another significant addition is support for creating community timelines, which will allow you to load the local and public timeline of remote instances. This is useful if your instance does not federate directly with them, as it will allow you to see posts from other communities and interact directly with them. Finally, the translation module has been rewritten; it now supports using LibreTranslate by default and DeepL, for which an API key is required. Below is the detailed list of changes:
* Core:
* Added Initial Support to GoToSocial. Some features are not fully implemented yet, although GoToXocial instances should be able to be used as normal sessions in TWBlue. Streaming, poll options and markdown are not supported but planned for the near future.
* 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:
* Added support for viewing communities: A community timeline is the local or public timeline of another instance. This is especially useful when the instance one is part of does not federate with other remote instances. The posts displayed are only those that are shared publicly. It is possible to interact with the posts from community timelines, but it should be noted that TWBlue will take some time to retrieve the post one wishes to interact with.
* 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.
* Now it is possible to mute conversations in Mastodon sessions. To do this, there is a button that can be called "Mute" or "Unmute Conversation" in the dialog to display the post. Conversations that have been muted will not generate notifications or mentions when they receive new replies. Only conversations that you are a part of can be muted.
* 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
* Core: * Core:
* The TWBlue website will no longer be available on the twblue.es domain. Beginning in January 2024, TWBlue will live at https://twblue.mcvsoftware.com. Also, we will start releasing versions on [gitHub releases](https://github.com/mcv-software/twblue/releases) so it will be easier to track specific versions. * The TWBlue website will no longer be available on the twblue.es domain. Beginning in January 2024, TWBlue will live at https://twblue.mcvsoftware.com. Also, we will start releasing versions on [gitHub releases](https://github.com/mcv-software/twblue/releases) so it will be easier to track specific versions.
* As of the first release of TWBlue in 2024, we will officially stop generating 32-bit (X86) compatible binaries due to the increasing difficulty of generating versions compatible with this architecture in modern Python. * As of the first release of TWBlue in 2024, we will officially stop generating 32-bit (X86) compatible binaries due to the increasing difficulty of generating versions compatible with this architecture in modern Python.

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,17 @@
## Changelog ## Changelog
In this version, we have focused on providing initial support for Mastodon filters and pinned posts. From TWBlue, it is now possible to initially use filters for posts in most buffers, as well as manage them (create, edit, and delete filters, in addition to adding keywords). A new variable has also been added for post templates in the invisible interface that allows displaying whether a post has been pinned by its author. * Core:
* The TWBlue website will no longer be available on the twblue.es domain. Beginning in January 2024, TWBlue will live at https://twblue.mcvsoftware.com. Also, we will start releasing versions on [gitHub releases](https://github.com/mcv-software/twblue/releases) so it will be easier to track specific versions.
* As of the first release of TWBlue in 2024, we will officially stop generating 32-bit (X86) compatible binaries due to the increasing difficulty of generating versions compatible with this architecture in modern Python.
* TWBlue should be more reliable when checking for updates.
* If running from source, automatic updates will not be checked as this works only for distribution. ([#540](https://github.com/MCV-Software/TWBlue/pull/540))
* Fixed the 'report an error' item in the help menu. Now this item redirects to our gitHub issue tracker. ([#524](https://github.com/MCV-Software/TWBlue/pull/524))
* Mastodon: * Mastodon:
* Added filters support to TWBlue. Filters are only implemented in posts for the moment. TWBlue will, depending in the selected settings, hide behind a content warning or completely ignore a post based on filters. Also it is possible to add, delete or edit filters from the buffer menu in the menu bar. * Implemented actions for the notifications buffer on a mastodon instance. Actions can be performed from the contextual menu on every notification, or by using invisible keystrokes. ([#517](https://github.com/mcv-software/twblue/issues/517))
* A language selector has been added for posting in TWBlue. It is now possible to choose the language in which a post will be made, which will be useful for content filtering and other language-dependent features. The default language can be chosen based on your Mastodon accounts language, the language of the post youre replying to, or, if no automatic selection is possible, TWBlues own language will be used by default. * Implemented update profile dialog. ([#547](https://github.com/MCV-Software/TWBlue/pull/547))
* TWBlue now supports announcing (via a new template variable for posts) pinned posts. You can edit your posts template and use the $pinned variable. When reading the post, TWBlue will indicate if the post is pinned. Also, when loading an user timeline, pinned posts will be loaded at the top or bottom of the buffer according to local settings. * It is possible to display an user profile from the user menu within the menu bar, or by using the invisible keystroke for user details. ([#555](https://github.com/MCV-Software/TWBlue/pull/555))
* TWBlue should be able to display all posts in the post displayer dialog. * Added possibility to vote in polls. This is mapped to Alt+Win+Shift+V in the invisible keymaps for windows 10/11.
* reading long posts in the graphical user interface should work better. * Added posts search. Take into account that Mastodon instances should be configured with full text search enabled. Search for posts only include posts the logged-in user has interacted with. ([#541](https://github.com/MCV-Software/TWBlue/pull/541))
* Added user autocompletion settings in account settings dialog, so it is possible to ask TWBlue to scan mastodon accounts and add people from followers and following buffers. For now, user autocompletion can be used only when composing new posts or replies.
* TWBlue should be able to ignore deleted direct messages or messages from deleted accounts. Previously, a direct message that no longer existed in the instance caused errors when loading the direct messages buffer and could potentially affect notifications as well.
* TWBlue should be able to ignore notifications from deleted accounts or posts.

View File

@ -1,58 +1,55 @@
accessible_output2 @ git+https://github.com/accessibleapps/accessible_output2@57bda997d98e87dd78aa049e7021cf777871619b wxpython
arrow==1.3.0 pytest
attrs==25.3.0 coverage
backports.functools-lru-cache==2.0.0 wheel
blurhash==1.1.4 future
certifi==2025.4.26 configobj
chardet==5.2.0 markdown
charset-normalizer==3.4.2 requests
colorama==0.4.6 oauthlib
configobj==5.0.9 requests-oauthlib
coverage==7.8.0 requests-toolbelt
cx-Freeze==8.2.0 pypubsub
cx-Logging==3.2.1 arrow
decorator==5.2.1 python-dateutil
demoji==1.1.0 winpaths
deepl==1.22.0 PySocks
future==1.0.0 win_inet_pton
idna==3.10 # Install the latest RC of this lib
importlib-metadata==8.7.0 # see https://github.com/ssut/py-googletrans/issues/234
iniconfig==2.1.0 googletrans==4.0.0-rc1
libloader @ git+https://github.com/accessibleapps/libloader@bc94811c095b2e57a036acd88660be9a33260267 idna<3,>=2.5
libretranslatepy==2.1.4 chardet
lief==0.15.1 urllib3
Markdown==3.8 youtube-dl
Mastodon.py==2.0.1 python-vlc
numpy==2.2.3 pypiwin32
oauthlib==3.2.2 pywin32
packaging==25.0 certifi
pillow==11.2.1 backports.functools_lru_cache
platform_utils @ git+https://github.com/accessibleapps/platform_utils@e0d79f7b399c4ea677a633d2dde9202350d62c38 cx_freeze
pluggy==1.5.0 twitter-text-parser
psutil==7.0.0 mastodon.py
pyenchant==3.2.2 pyenchant
pypiwin32==223 sqlitedict
Pypubsub==4.0.3 cx-Logging
PySocks==1.7.1 h11
pytest==8.3.5 h2
python-dateutil==2.9.0.post0 hpack
python-magic-bin==0.4.14 hstspreload
python-vlc==3.0.21203 httpcore
pywin32==310 httpx
requests==2.32.3 hyperframe
requests-oauthlib==2.0.0 rfc3986
requests-toolbelt==1.0.0 sniffio
rfc3986==2.0.0 attrs
setuptools==69.0.0 importlib-metadata
six==1.17.0 numpy
sniffio==1.3.1 pillow
sound_lib @ git+https://github.com/accessibleapps/sound_lib@a439f0943fb95ee7b6ba24f51a686f47c4ad66b2 charset-normalizer
sqlitedict==2.1.0 demoji
twitter-text-parser==3.0.0 psutil
types-python-dateutil==2.9.0.20241206 git+https://github.com/accessibleapps/libloader
urllib3==2.4.0 git+https://github.com/accessibleapps/platform_utils
win-inet-pton==1.1.0 git+https://github.com/accessibleapps/accessible_output2
winpaths==0.2 git+https://github.com/accessibleapps/sound_lib
wxPython==4.2.3
youtube-dl==2021.12.17
zipp==3.21.0

View File

@ -27,7 +27,7 @@ var StartMenuFolder
!insertmacro MUI_PAGE_STARTMENU startmenu $StartMenuFolder !insertmacro MUI_PAGE_STARTMENU startmenu $StartMenuFolder
!insertmacro MUI_PAGE_INSTFILES !insertmacro MUI_PAGE_INSTFILES
!define MUI_FINISHPAGE_LINK "Visit TWBlue website" !define MUI_FINISHPAGE_LINK "Visit TWBlue website"
!define MUI_FINISHPAGE_LINK_LOCATION "https://twblue.mcvsoftware.com" !define MUI_FINISHPAGE_LINK_LOCATION "https://twblue.es"
!define MUI_FINISHPAGE_RUN "$INSTDIR\TWBlue.exe" !define MUI_FINISHPAGE_RUN "$INSTDIR\TWBlue.exe"
!insertmacro MUI_PAGE_FINISH !insertmacro MUI_PAGE_FINISH
!insertmacro MUI_UNPAGE_CONFIRM !insertmacro MUI_UNPAGE_CONFIRM
@ -65,7 +65,7 @@ CreateShortCut "$DESKTOP\TWBlue.lnk" "$INSTDIR\TWBlue.exe"
!insertmacro MUI_STARTMENU_WRITE_BEGIN startmenu !insertmacro MUI_STARTMENU_WRITE_BEGIN startmenu
CreateDirectory "$SMPROGRAMS\$StartMenuFolder" CreateDirectory "$SMPROGRAMS\$StartMenuFolder"
CreateShortCut "$SMPROGRAMS\$StartMenuFolder\TWBlue.lnk" "$INSTDIR\TWBlue.exe" CreateShortCut "$SMPROGRAMS\$StartMenuFolder\TWBlue.lnk" "$INSTDIR\TWBlue.exe"
CreateShortCut "$SMPROGRAMS\$StartMenuFolder\TWBlue on the web.lnk" "https://twblue.mcvsoftware.com" CreateShortCut "$SMPROGRAMS\$StartMenuFolder\TWBlue on the web.lnk" "http://twblue.es"
CreateShortCut "$SMPROGRAMS\$StartMenuFolder\Uninstall.lnk" "$INSTDIR\Uninstall.exe" CreateShortCut "$SMPROGRAMS\$StartMenuFolder\Uninstall.lnk" "$INSTDIR\Uninstall.exe"
!insertmacro MUI_STARTMENU_WRITE_END !insertmacro MUI_STARTMENU_WRITE_END
WriteUninstaller "$INSTDIR\Uninstall.exe" WriteUninstaller "$INSTDIR\Uninstall.exe"
@ -74,7 +74,7 @@ WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\twblue" "U
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall" "InstallLocation" $INSTDIR WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall" "InstallLocation" $INSTDIR
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall" "Publisher" "Manuel Cortéz" WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall" "Publisher" "Manuel Cortéz"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\twblue" "DisplayVersion" "0.95" WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\twblue" "DisplayVersion" "0.95"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\twblue" "URLInfoAbout" "https://twblue.mcvsoftware.com" WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\twblue" "URLInfoAbout" "https://twblue.es"
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\twblue" "VersionMajor" 0 WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\twblue" "VersionMajor" 0
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\twblue" "VersionMinor" 0 WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\twblue" "VersionMinor" 0
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\twblue" "NoModify" 1 WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\twblue" "NoModify" 1

View File

@ -9,7 +9,8 @@ update_period = integer(default=2)
hide_gui = boolean(default=False) hide_gui = boolean(default=False)
voice_enabled = boolean(default=False) voice_enabled = boolean(default=False)
ask_at_exit = boolean(default=True) ask_at_exit = boolean(default=True)
read_long_posts_in_gui = boolean(default=True) autostart = boolean(default=False)
handle_longtweets = boolean(default=True)
use_invisible_keyboard_shorcuts = boolean(default=True) use_invisible_keyboard_shorcuts = boolean(default=True)
play_ready_sound = boolean(default=True) play_ready_sound = boolean(default=True)
speak_ready_msg = boolean(default=True) speak_ready_msg = boolean(default=True)
@ -17,6 +18,9 @@ log_level = string(default="error")
load_keymap = string(default="default.keymap") load_keymap = string(default="default.keymap")
donation_dialog_displayed = boolean(default=False) donation_dialog_displayed = boolean(default=False)
check_for_updates = boolean(default=True) check_for_updates = boolean(default=True)
remember_mention_and_longtweet = boolean(default=False)
longtweet = boolean(default=false)
mention_all = boolean(default=False)
no_streaming = boolean(default=False) no_streaming = boolean(default=False)
[proxy] [proxy]
@ -24,10 +28,4 @@ type = integer(default=0)
server = string(default="") server = string(default="")
port = integer(default=8080) port = integer(default=8080)
user = string(default="") user = string(default="")
password = string(default="") password = string(default="")
[translator]
engine=string(default="LibreTranslate")
lt_api_url=string(default="https://translate.nvda.es")
lt_api_key=string(default="")
deepl_api_key = string(default="")

View File

@ -1,6 +1,5 @@
# -*- coding: cp1252 -*- # -*- coding: cp1252 -*-
import os import os
import sys
import config_utils import config_utils
import paths import paths
import logging import logging
@ -22,10 +21,7 @@ def setup ():
log.debug("Loading keymap...") log.debug("Loading keymap...")
global keymap global keymap
if float(platform.version()[:2]) >= 10 and app["app-settings"]["load_keymap"] == "default.keymap": if float(platform.version()[:2]) >= 10 and app["app-settings"]["load_keymap"] == "default.keymap":
if sys.getwindowsversion().build > 22000: app["app-settings"]["load_keymap"] = "Windows 10.keymap"
app["app-settings"]["load_keymap"] = "Windows11.keymap"
else:
app["app-settings"]["load_keymap"] = "Windows 10.keymap"
app.write() app.write()
global changed_keymap global changed_keymap
changed_keymap = True changed_keymap = True

View File

@ -5,4 +5,3 @@ from .conversations import ConversationBuffer, ConversationListBuffer
from .users import UserBuffer from .users import UserBuffer
from .notifications import NotificationsBuffer from .notifications import NotificationsBuffer
from .search import SearchBuffer from .search import SearchBuffer
from .community import CommunityBuffer

View File

@ -70,7 +70,7 @@ class BaseBuffer(base.Buffer):
response = post.message.ShowModal() response = post.message.ShowModal()
if response == wx.ID_OK: if response == wx.ID_OK:
post_data = post.get_data() post_data = post.get_data()
call_threaded(self.session.send_post, posts=post_data, visibility=post.get_visibility(), language=post.get_language(), **kwargs) call_threaded(self.session.send_post, posts=post_data, visibility=post.get_visibility(), **kwargs)
if hasattr(post.message, "destroy"): if hasattr(post.message, "destroy"):
post.message.destroy() post.message.destroy()
@ -108,28 +108,13 @@ class BaseBuffer(base.Buffer):
min_id = self.session.db[self.name][0].id min_id = self.session.db[self.name][0].id
else: else:
min_id = self.session.db[self.name][-1].id min_id = self.session.db[self.name][-1].id
# loads pinned posts from user accounts.
# Load those posts only when there are no items previously loaded.
if "-timeline" in self.name and "account_statuses" in self.function and len(self.session.db.get(self.name, [])) == 0:
pinned_posts = self.session.api.account_statuses(pinned=True, limit=count, *self.args, **self.kwargs)
pinned_posts.reverse()
else:
pinned_posts = None
try: try:
results = getattr(self.session.api, self.function)(min_id=min_id, limit=count, *self.args, **self.kwargs) results = getattr(self.session.api, self.function)(min_id=min_id, limit=count, *self.args, **self.kwargs)
results.reverse() results.reverse()
except Exception as e: except Exception as e:
log.exception("Error %s" % (str(e))) log.exception("Error %s" % (str(e)))
return return
if self.session.settings["general"]["reverse_timelines"]:
if pinned_posts != None and len(pinned_posts) > 0:
amount_of_pinned_posts = self.session.order_buffer(self.name, pinned_posts)
number_of_items = self.session.order_buffer(self.name, results) number_of_items = self.session.order_buffer(self.name, results)
if self.session.settings["general"]["reverse_timelines"] == False:
if pinned_posts != None and len(pinned_posts) > 0:
amount_of_pinned_posts = self.session.order_buffer(self.name, pinned_posts)
if pinned_posts != None and len(pinned_posts) > 0:
number_of_items = amount_of_pinned_posts+number_of_items
log.debug("Number of items retrieved: %d" % (number_of_items,)) log.debug("Number of items retrieved: %d" % (number_of_items,))
if hasattr(self, "finished_timeline") and self.finished_timeline == False: if hasattr(self, "finished_timeline") and self.finished_timeline == False:
if "-timeline" in self.name: if "-timeline" in self.name:
@ -172,9 +157,6 @@ class BaseBuffer(base.Buffer):
items_db = self.session.db[self.name] items_db = self.session.db[self.name]
for i in items: for i in items:
if utils.find_item(i, self.session.db[self.name]) == None: if utils.find_item(i, self.session.db[self.name]) == None:
filter_status = utils.evaluate_filters(post=i, current_context=utils.get_current_context(self.name))
if filter_status == "hide":
continue
elements.append(i) elements.append(i)
if self.session.settings["general"]["reverse_timelines"] == False: if self.session.settings["general"]["reverse_timelines"] == False:
items_db.insert(0, i) items_db.insert(0, i)
@ -338,7 +320,7 @@ class BaseBuffer(base.Buffer):
return False return False
return True return True
def reply(self, event=None, item=None, *args, **kwargs): def reply(self, item=None, *args, **kwargs):
if item == None: if item == None:
item = self.get_item() item = self.get_item()
visibility = item.visibility visibility = item.visibility
@ -353,19 +335,16 @@ class BaseBuffer(base.Buffer):
visibility = "unlisted" visibility = "unlisted"
if item.reblog != None: if item.reblog != None:
users = ["@{} ".format(user.acct) for user in item.reblog.mentions if user.id != self.session.db["user_id"]] users = ["@{} ".format(user.acct) for user in item.reblog.mentions if user.id != self.session.db["user_id"]]
language = item.reblog.language
if item.reblog.account.acct != item.account.acct and "@{} ".format(item.reblog.account.acct) not in users: if item.reblog.account.acct != item.account.acct and "@{} ".format(item.reblog.account.acct) not in users:
users.append("@{} ".format(item.reblog.account.acct)) users.append("@{} ".format(item.reblog.account.acct))
else: else:
users = ["@{} ".format(user.acct) for user in item.mentions if user.id != self.session.db["user_id"]] users = ["@{} ".format(user.acct) for user in item.mentions if user.id != self.session.db["user_id"]]
language = item.language
if "@{} ".format(item.account.acct) not in users and item.account.id != self.session.db["user_id"]: if "@{} ".format(item.account.acct) not in users and item.account.id != self.session.db["user_id"]:
users.insert(0, "@{} ".format(item.account.acct)) users.insert(0, "@{} ".format(item.account.acct))
users_str = "".join(users) users_str = "".join(users)
post = messages.post(session=self.session, title=title, caption=caption, text=users_str) post = messages.post(session=self.session, title=title, caption=caption, text=users_str)
visibility_settings = dict(public=0, unlisted=1, private=2, direct=3) visibility_settings = dict(public=0, unlisted=1, private=2, direct=3)
post.message.visibility.SetSelection(visibility_settings.get(visibility)) post.message.visibility.SetSelection(visibility_settings.get(visibility))
post.set_language(language)
# Respect content warning settings. # Respect content warning settings.
if item.sensitive: if item.sensitive:
post.message.sensitive.SetValue(item.sensitive) post.message.sensitive.SetValue(item.sensitive)
@ -374,11 +353,11 @@ class BaseBuffer(base.Buffer):
response = post.message.ShowModal() response = post.message.ShowModal()
if response == wx.ID_OK: if response == wx.ID_OK:
post_data = post.get_data() post_data = post.get_data()
call_threaded(self.session.send_post, reply_to=item.id, posts=post_data, visibility=post.get_visibility(), language=post.get_language()) call_threaded(self.session.send_post, reply_to=item.id, posts=post_data, visibility=post.get_visibility())
if hasattr(post.message, "destroy"): if hasattr(post.message, "destroy"):
post.message.destroy() post.message.destroy()
def send_message(self, event=None, item=None, *args, **kwargs): def send_message(self, item=None, *args, **kwargs):
if item == None: if item == None:
item = self.get_item() item = self.get_item()
title = _("Conversation with {}").format(item.account.username) title = _("Conversation with {}").format(item.account.username)
@ -401,11 +380,11 @@ class BaseBuffer(base.Buffer):
response = post.message.ShowModal() response = post.message.ShowModal()
if response == wx.ID_OK: if response == wx.ID_OK:
post_data = post.get_data() post_data = post.get_data()
call_threaded(self.session.send_post, posts=post_data, visibility="direct", reply_to=item.id, language=post.get_language()) call_threaded(self.session.send_post, posts=post_data, visibility="direct", reply_to=item.id)
if hasattr(post.message, "destroy"): if hasattr(post.message, "destroy"):
post.message.destroy() post.message.destroy()
def share_item(self, event=None, item=None, *args, **kwargs): def share_item(self, item=None, *args, **kwargs):
if item == None: if item == None:
item = self.get_item() item = self.get_item()
if self.can_share(item=item) == False: if self.can_share(item=item) == False:
@ -427,8 +406,6 @@ class BaseBuffer(base.Buffer):
original_date = arrow.get(self.session.db[self.name][self.buffer.list.get_selected()].created_at) original_date = arrow.get(self.session.db[self.name][self.buffer.list.get_selected()].created_at)
ts = original_date.humanize(locale=languageHandler.getLanguage()) ts = original_date.humanize(locale=languageHandler.getLanguage())
self.buffer.list.list.SetItem(self.buffer.list.get_selected(), 2, ts) self.buffer.list.list.SetItem(self.buffer.list.get_selected(), 2, ts)
if config.app["app-settings"]["read_long_posts_in_gui"] == True and self.buffer.list.list.HasFocus():
wx.CallLater(40, output.speak, self.get_message(), interrupt=True)
if self.session.settings['sound']['indicate_audio'] and utils.is_audio_or_video(post): if self.session.settings['sound']['indicate_audio'] and utils.is_audio_or_video(post):
self.session.sound.play("audio.ogg") self.session.sound.play("audio.ogg")
if self.session.settings['sound']['indicate_img'] and utils.is_image(post): if self.session.settings['sound']['indicate_img'] and utils.is_image(post):
@ -437,7 +414,7 @@ class BaseBuffer(base.Buffer):
pub.sendMessage("toggleShare", shareable=can_share) pub.sendMessage("toggleShare", shareable=can_share)
self.buffer.boost.Enable(can_share) self.buffer.boost.Enable(can_share)
def audio(self, event=None, item=None, *args, **kwargs): def audio(self, item=None, *args, **kwargs):
if sound.URLPlayer.player.is_playing(): if sound.URLPlayer.player.is_playing():
return sound.URLPlayer.stop_audio() return sound.URLPlayer.stop_audio()
if item == None: if item == None:
@ -512,7 +489,7 @@ class BaseBuffer(base.Buffer):
return item.reblog.url return item.reblog.url
return item.url return item.url
def open_in_browser(self, event=None, item=None, *args, **kwargs): def open_in_browser(self, item=None, *args, **kwargs):
if item == None: if item == None:
item = self.get_item() item = self.get_item()
url = self.get_item_url(item=item) url = self.get_item_url(item=item)
@ -533,7 +510,7 @@ class BaseBuffer(base.Buffer):
item = item.reblog item = item.reblog
call_threaded(self.session.api_call, call_name="status_unfavourite", preexec_message=_("Removing from favorites..."), _sound="favourite.ogg", id=item.id) call_threaded(self.session.api_call, call_name="status_unfavourite", preexec_message=_("Removing from favorites..."), _sound="favourite.ogg", id=item.id)
def toggle_favorite(self, event=None, item=None, *args, **kwargs): def toggle_favorite(self, item=None, *args, **kwargs):
if item == None: if item == None:
item = self.get_item() item = self.get_item()
if item.reblog != None: if item.reblog != None:
@ -548,7 +525,7 @@ class BaseBuffer(base.Buffer):
else: else:
call_threaded(self.session.api_call, call_name="status_unfavourite", preexec_message=_("Removing from favorites..."), _sound="favourite.ogg", id=item.id) call_threaded(self.session.api_call, call_name="status_unfavourite", preexec_message=_("Removing from favorites..."), _sound="favourite.ogg", id=item.id)
def toggle_bookmark(self, event=None, item=None, *args, **kwargs): def toggle_bookmark(self, item=None, *args, **kwargs):
if item == None: if item == None:
item = self.get_item() item = self.get_item()
if item.reblog != None: if item.reblog != None:
@ -572,7 +549,8 @@ class BaseBuffer(base.Buffer):
except MastodonNotFoundError: except MastodonNotFoundError:
output.speak(_("No status found with that ID")) output.speak(_("No status found with that ID"))
return return
msg = messages.viewPost(self.session, item, offset_hours=self.session.db["utc_offset"], item_url=self.get_item_url(item=item)) # print(post)
msg = messages.viewPost(item, offset_hours=self.session.db["utc_offset"], item_url=self.get_item_url(item=item))
def ocr_image(self): def ocr_image(self):
post = self.get_item() post = self.get_item()
@ -613,11 +591,8 @@ class BaseBuffer(base.Buffer):
response = viewer.message.ShowModal() response = viewer.message.ShowModal()
viewer.message.Destroy() viewer.message.Destroy()
def vote(self, item=None): def vote(self):
if item == None: post = self.get_item()
post = self.get_item()
else:
post = item
if not hasattr(post, "poll") or post.poll == None: if not hasattr(post, "poll") or post.poll == None:
return return
poll = post.poll poll = post.poll
@ -641,14 +616,14 @@ class BaseBuffer(base.Buffer):
return return
poll = self.session.api_call(call_name="poll_vote", id=poll.id, choices=options, preexec_message=_("Sending vote...")) poll = self.session.api_call(call_name="poll_vote", id=poll.id, choices=options, preexec_message=_("Sending vote..."))
def post_from_error(self, visibility, reply_to, data, lang): def post_from_error(self, visibility, reply_to, data):
title = _("Post") title = _("Post")
caption = _("Write your post here") caption = _("Write your post here")
post = messages.post(session=self.session, title=title, caption=caption) post = messages.post(session=self.session, title=title, caption=caption)
post.set_post_data(visibility=visibility, data=data, language=language) post.set_post_data(visibility=visibility, data=data)
response = post.message.ShowModal() response = post.message.ShowModal()
if response == wx.ID_OK: if response == wx.ID_OK:
post_data = post.get_data() post_data = post.get_data()
call_threaded(self.session.send_post, posts=post_data, reply_to=reply_to, visibility=post.get_visibility(), language=post.get_language()) call_threaded(self.session.send_post, posts=post_data, reply_to=reply_to, visibility=post.get_visibility())
if hasattr(post.message, "destroy"): if hasattr(post.message, "destroy"):
post.message.destroy() post.message.destroy()

View File

@ -1,160 +0,0 @@
# -*- coding: utf-8 -*-
import time
import logging
import mastodon
import widgetUtils
import output
from wxUI import commonMessageDialogs
from sessions.mastodon import utils
from . import base
log = logging.getLogger("controller.buffers.mastodon.community")
class CommunityBuffer(base.BaseBuffer):
def __init__(self, community_url, *args, **kwargs):
super(CommunityBuffer, self).__init__(*args, **kwargs)
self.community_url = community_url
self.community_api = mastodon.Mastodon(api_base_url=self.community_url)
self.timeline = kwargs.get("timeline", "local")
self.kwargs.pop("timeline")
def get_buffer_name(self):
type = _("Local") if self.timeline == "local" else _("Federated")
instance = self.community_url.replace("https://", "")
return _(f"{type} timeline for {instance}")
def start_stream(self, mandatory=False, play_sound=True, avoid_autoreading=False):
current_time = time.time()
if self.execution_time == 0 or current_time-self.execution_time >= 180 or mandatory==True:
self.execution_time = current_time
log.debug("Starting stream for buffer %s, account %s and type %s" % (self.name, self.account, self.type))
log.debug("args: %s, kwargs: %s" % (self.args, self.kwargs))
count = self.session.settings["general"]["max_posts_per_call"]
min_id = None
# toDo: Implement reverse timelines properly here.
if self.name in self.session.db and len(self.session.db[self.name]) > 0:
if self.session.settings["general"]["reverse_timelines"]:
min_id = self.session.db[self.name][0].id
else:
min_id = self.session.db[self.name][-1].id
try:
results = self.community_api.timeline(timeline=self.timeline, min_id=min_id, limit=count, *self.args, **self.kwargs)
results.reverse()
except Exception as e:
log.exception("Error %s" % (str(e)))
return
number_of_items = self.session.order_buffer(self.name, results)
log.debug("Number of items retrieved: %d" % (number_of_items,))
self.put_items_on_list(number_of_items)
if number_of_items > 0 and self.sound != None and self.session.settings["sound"]["session_mute"] == False and self.name not in self.session.settings["other_buffers"]["muted_buffers"] and play_sound == True:
self.session.sound.play(self.sound)
# Autoread settings
if avoid_autoreading == False and mandatory == True and number_of_items > 0 and self.name in self.session.settings["other_buffers"]["autoread_buffers"]:
self.auto_read(number_of_items)
return number_of_items
def get_more_items(self):
elements = []
if self.session.settings["general"]["reverse_timelines"] == False:
max_id = self.session.db[self.name][0].id
else:
max_id = self.session.db[self.name][-1].id
try:
items = self.community_api.timeline(timeline=self.timeline, max_id=max_id, limit=self.session.settings["general"]["max_posts_per_call"], *self.args, **self.kwargs)
except Exception as e:
log.exception("Error %s" % (str(e)))
return
items_db = self.session.db[self.name]
for i in items:
if utils.find_item(i, self.session.db[self.name]) == None:
elements.append(i)
if self.session.settings["general"]["reverse_timelines"] == False:
items_db.insert(0, i)
else:
items_db.append(i)
self.session.db[self.name] = items_db
selection = self.buffer.list.get_selected()
log.debug("Retrieved %d items from cursored search in function %s." % (len(elements), self.function))
safe = True
if self.session.settings["general"]["read_preferences_from_instance"]:
safe = self.session.expand_spoilers == False
if self.session.settings["general"]["reverse_timelines"] == False:
for i in elements:
post = self.compose_function(i, self.session.db, self.session.settings, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"], safe=safe)
self.buffer.list.insert_item(True, *post)
else:
for i in elements:
post = self.compose_function(i, self.session.db, self.session.settings, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"], safe=safe)
self.buffer.list.insert_item(False, *post)
self.buffer.list.select_item(selection)
output.speak(_(u"%s items retrieved") % (str(len(elements))), True)
def remove_buffer(self, force=False):
if force == False:
dlg = commonMessageDialogs.remove_buffer()
else:
dlg = widgetUtils.YES
if dlg == widgetUtils.YES:
tl_info = f"{self.timeline}@{self.community_url}"
self.session.settings["other_buffers"]["communities"].remove(tl_info)
self.session.settings.write()
if self.name in self.session.db:
self.session.db.pop(self.name)
return True
elif dlg == widgetUtils.NO:
return False
def get_item_from_instance(self, *args, **kwargs):
item = self.get_item()
try:
results = self.session.api.search(q=item.url, resolve=True, result_type="statuses")
except Exception as e:
log.exception("Error when searching for remote post.")
return None
item = results["statuses"][0]
return item
def reply(self, *args, **kwargs):
item = self.get_item_from_instance()
if item != None:
super(CommunityBuffer, self).reply(item=item)
def send_message(self, *args, **kwargs):
item = self.get_item_from_instance()
if item != None:
super(CommunityBuffer, self).send_message(item=item)
def share_item(self, *args, **kwargs):
item = self.get_item_from_instance()
if item != None:
super(CommunityBuffer, self).share_item(item=item)
def add_to_favorites(self, *args, **kwargs):
item = self.get_item_from_instance()
if item != None:
super(CommunityBuffer, self).add_to_favorite(item=item)
def remove_from_favorites(self, *args, **kwargs):
item = self.get_item_from_instance()
if item != None:
super(CommunityBuffer, self).remove_from_favorites(item=item)
def toggle_favorite(self, *args, **kwargs):
item = self.get_item_from_instance()
if item != None:
super(CommunityBuffer, self).toggle_favorite(item=item)
def toggle_bookmark(self, *args, **kwargs):
item = self.get_item_from_instance()
if item != None:
super(CommunityBuffer, self).toggle_bookmark(item=item)
def vote(self, *args, **kwargs):
item = self.get_item_from_instance()
if item != None:
super(CommunityBuffer, self).vote(item=item)
def view_item(self, *args, **kwargs):
item = self.get_item_from_instance()
if item != None:
super(CommunityBuffer, self).view_item(item=item)

View File

@ -4,7 +4,6 @@ import logging
import wx import wx
import widgetUtils import widgetUtils
import output import output
import config
from mastodon import MastodonNotFoundError from mastodon import MastodonNotFoundError
from controller.mastodon import messages from controller.mastodon import messages
from controller.buffers.mastodon.base import BaseBuffer from controller.buffers.mastodon.base import BaseBuffer
@ -163,8 +162,6 @@ class ConversationListBuffer(BaseBuffer):
def onFocus(self, *args, **kwargs): def onFocus(self, *args, **kwargs):
post = self.get_item() post = self.get_item()
if config.app["app-settings"]["read_long_posts_in_gui"] == True and self.buffer.list.list.HasFocus():
wx.CallLater(40, output.speak, self.get_message(), interrupt=True)
if self.session.settings['sound']['indicate_audio'] and utils.is_audio_or_video(post): if self.session.settings['sound']['indicate_audio'] and utils.is_audio_or_video(post):
self.session.sound.play("audio.ogg") self.session.sound.play("audio.ogg")
if self.session.settings['sound']['indicate_img'] and utils.is_image(post): if self.session.settings['sound']['indicate_img'] and utils.is_image(post):
@ -191,7 +188,7 @@ class ConversationListBuffer(BaseBuffer):
response = post.message.ShowModal() response = post.message.ShowModal()
if response == wx.ID_OK: if response == wx.ID_OK:
post_data = post.get_data() post_data = post.get_data()
call_threaded(self.session.send_post, reply_to=item.id, posts=post_data, visibility=visibility, language=post.get_language()) call_threaded(self.session.send_post, reply_to=item.id, posts=post_data, visibility=visibility)
if hasattr(post.message, "destroy"): if hasattr(post.message, "destroy"):
post.message.destroy() post.message.destroy()

View File

@ -56,9 +56,6 @@ class MentionsBuffer(BaseBuffer):
items_db = self.session.db[self.name] items_db = self.session.db[self.name]
for i in items: for i in items:
if utils.find_item(i, self.session.db[self.name]) == None: if utils.find_item(i, self.session.db[self.name]) == None:
filter_status = utils.evaluate_filters(post=i, current_context=utils.get_current_context(self.name))
if filter_status == "hide":
continue
elements.append(i) elements.append(i)
if self.session.settings["general"]["reverse_timelines"] == False: if self.session.settings["general"]["reverse_timelines"] == False:
items_db.insert(0, i) items_db.insert(0, i)

View File

@ -1,12 +1,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import time import time
import logging import logging
import arrow
import widgetUtils import widgetUtils
import wx
import output import output
import languageHandler
import config
from pubsub import pub from pubsub import pub
from controller.buffers.mastodon.base import BaseBuffer from controller.buffers.mastodon.base import BaseBuffer
from controller.mastodon import messages from controller.mastodon import messages
@ -42,11 +38,8 @@ class NotificationsBuffer(BaseBuffer):
original_date = arrow.get(self.session.db[self.name][self.buffer.list.get_selected()].created_at) original_date = arrow.get(self.session.db[self.name][self.buffer.list.get_selected()].created_at)
ts = original_date.humanize(locale=languageHandler.getLanguage()) ts = original_date.humanize(locale=languageHandler.getLanguage())
self.buffer.list.list.SetItem(self.buffer.list.get_selected(), 1, ts) self.buffer.list.list.SetItem(self.buffer.list.get_selected(), 1, ts)
if config.app["app-settings"]["read_long_posts_in_gui"] == True and self.buffer.list.list.HasFocus():
wx.CallLater(40, output.speak, self.get_message(), interrupt=True)
def bind_events(self): def bind_events(self):
self.buffer.set_focus_function(self.onFocus)
widgetUtils.connect_event(self.buffer.list.list, widgetUtils.KEYPRESS, self.get_event) widgetUtils.connect_event(self.buffer.list.list, widgetUtils.KEYPRESS, self.get_event)
widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.post_status, self.buffer.post) widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.post_status, self.buffer.post)
widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.destroy_status, self.buffer.dismiss) widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.destroy_status, self.buffer.dismiss)
@ -126,7 +119,7 @@ class NotificationsBuffer(BaseBuffer):
response = post.message.ShowModal() response = post.message.ShowModal()
if response == wx.ID_OK: if response == wx.ID_OK:
post_data = post.get_data() post_data = post.get_data()
call_threaded(self.session.send_post, posts=post_data, visibility="direct", language=post.get_language()) call_threaded(self.session.send_post, posts=post_data, visibility="direct")
if hasattr(post.message, "destroy"): if hasattr(post.message, "destroy"):
post.message.destroy() post.message.destroy()

View File

@ -153,7 +153,6 @@ class Controller(object):
widgetUtils.connect_event(self.view, widgetUtils.MENU, self.toggle_buffer_mute, self.view.mute_buffer) widgetUtils.connect_event(self.view, widgetUtils.MENU, self.toggle_buffer_mute, self.view.mute_buffer)
widgetUtils.connect_event(self.view, widgetUtils.MENU, self.open_timeline, self.view.timeline) widgetUtils.connect_event(self.view, widgetUtils.MENU, self.open_timeline, self.view.timeline)
widgetUtils.connect_event(self.view, widgetUtils.MENU, self.open_favs_timeline, self.view.favs) widgetUtils.connect_event(self.view, widgetUtils.MENU, self.open_favs_timeline, self.view.favs)
widgetUtils.connect_event(self.view, widgetUtils.MENU, self.community_timeline, self.view.community_timeline)
widgetUtils.connect_event(self.view, widgetUtils.MENU, self.open_conversation, menuitem=self.view.view_conversation) widgetUtils.connect_event(self.view, widgetUtils.MENU, self.open_conversation, menuitem=self.view.view_conversation)
widgetUtils.connect_event(self.view, widgetUtils.MENU, self.seekLeft, menuitem=self.view.seekLeft) widgetUtils.connect_event(self.view, widgetUtils.MENU, self.seekLeft, menuitem=self.view.seekLeft)
widgetUtils.connect_event(self.view, widgetUtils.MENU, self.seekRight, menuitem=self.view.seekRight) widgetUtils.connect_event(self.view, widgetUtils.MENU, self.seekRight, menuitem=self.view.seekRight)
@ -165,8 +164,6 @@ class Controller(object):
widgetUtils.connect_event(self.view, widgetUtils.MENU, self.update_buffer, self.view.update_buffer) widgetUtils.connect_event(self.view, widgetUtils.MENU, self.update_buffer, self.view.update_buffer)
widgetUtils.connect_event(self.view, widgetUtils.MENU, self.manage_aliases, self.view.manageAliases) widgetUtils.connect_event(self.view, widgetUtils.MENU, self.manage_aliases, self.view.manageAliases)
widgetUtils.connect_event(self.view, widgetUtils.MENU, self.report_error, self.view.reportError) widgetUtils.connect_event(self.view, widgetUtils.MENU, self.report_error, self.view.reportError)
widgetUtils.connect_event(self.view, widgetUtils.MENU, self.create_filter, self.view.filter)
widgetUtils.connect_event(self.view, widgetUtils.MENU, self.manage_filters, self.view.manage_filters)
def set_systray_icon(self): def set_systray_icon(self):
self.systrayIcon = sysTrayIcon.SysTrayIcon() self.systrayIcon = sysTrayIcon.SysTrayIcon()
@ -849,14 +846,7 @@ class Controller(object):
def register_invisible_keyboard_shorcuts(self, keymap): def register_invisible_keyboard_shorcuts(self, keymap):
if config.changed_keymap: if config.changed_keymap:
build_number = sys.getwindowsversion().build commonMessageDialogs.changed_keymap()
if build_number > 22000:
system = "Windows 11"
keystroke_editor_shortcut = "Control+Win+Alt+K"
else:
system = "Windows 10"
keystroke_editor_shortcut = "Win+Alt+K"
commonMessageDialogs.changed_keymap(system, keystroke_editor_shortcut)
# Make sure we pass a keymap without undefined keystrokes. # Make sure we pass a keymap without undefined keystrokes.
new_keymap = {key: keymap[key] for key in keymap.keys() if keymap[key] != ""} new_keymap = {key: keymap[key] for key in keymap.keys() if keymap[key] != ""}
self.keyboard_handler = WXKeyboardHandler(self.view) self.keyboard_handler = WXKeyboardHandler(self.view)
@ -1088,10 +1078,10 @@ class Controller(object):
# if "direct_messages" not in buffer.session.settings["other_buffers"]["muted_buffers"]: # if "direct_messages" not in buffer.session.settings["other_buffers"]["muted_buffers"]:
# self.notify(buffer.session, sound_to_play) # self.notify(buffer.session, sound_to_play)
def mastodon_error_post(self, name, reply_to, visibility, posts, language): def mastodon_error_post(self, name, reply_to, visibility, posts):
home = self.search_buffer("home_timeline", name) home = self.search_buffer("home_timeline", name)
if home != None: if home != None:
wx.CallAfter(home.post_from_error, visibility=visibility, reply_to=reply_to, data=posts, lang=language) wx.CallAfter(home.post_from_error, visibility=visibility, reply_to=reply_to, data=posts)
def change_buffer_title(self, name, buffer, title): def change_buffer_title(self, name, buffer, title):
buffer_index = self.view.search(buffer, name) buffer_index = self.view.search(buffer, name)
@ -1151,21 +1141,3 @@ class Controller(object):
handler = self.get_handler(type=buffer.session.type) handler = self.get_handler(type=buffer.session.type)
if handler and hasattr(handler, 'openFollowingTimeline'): if handler and hasattr(handler, 'openFollowingTimeline'):
handler.openFollowingTimeline(self, buffer, user) handler.openFollowingTimeline(self, buffer, user)
def community_timeline(self, *args, user=None):
buffer = self.get_best_buffer()
handler = self.get_handler(type=buffer.session.type)
if handler and hasattr(handler, 'community_timeline'):
handler.community_timeline(self, buffer)
def create_filter(self, *args, **kwargs):
buffer = self.get_best_buffer()
handler = self.get_handler(type=buffer.session.type)
if handler and hasattr(handler, 'create_filter'):
handler.create_filter(self, buffer)
def manage_filters(self, *args, **kwargs):
buffer = self.get_best_buffer()
handler = self.get_handler(type=buffer.session.type)
if handler and hasattr(handler, 'manage_filters'):
handler.manage_filters(self, buffer)

View File

@ -1 +0,0 @@
# -*- coding: utf-8 -*-

View File

@ -1,112 +0,0 @@
# -*- coding: utf-8 -*-
import widgetUtils
from wxUI.dialogs.mastodon.filters import create_filter as dialog
from mastodon import MastodonAPIError
class CreateFilterController(object):
def __init__(self, session, filter_data=None):
super(CreateFilterController, self).__init__()
self.session = session
self.filter_data = filter_data
self.dialog = dialog.CreateFilterDialog(parent=None)
if self.filter_data is not None:
self.keywords = self.filter_data.get("keywords")
self.load_filter_data()
else:
self.keywords = []
widgetUtils.connect_event(self.dialog.keyword_panel.add_button, widgetUtils.BUTTON_PRESSED, self.on_add_keyword)
widgetUtils.connect_event(self.dialog.keyword_panel.remove_button, widgetUtils.BUTTON_PRESSED, self.on_remove_keyword)
def on_add_keyword(self, event):
""" Adds a keyword to the list. """
keyword = self.dialog.keyword_panel.keyword_text.GetValue().strip()
whole_word = self.dialog.keyword_panel.whole_word_checkbox.GetValue()
if keyword:
for idx, kw in enumerate(self.keywords):
if kw['keyword'] == keyword:
return
keyword_data = {
'keyword': keyword,
'whole_word': whole_word
}
self.keywords.append(keyword_data)
self.dialog.keyword_panel.add_keyword(keyword, whole_word)
def on_remove_keyword(self, event):
removed = self.dialog.keyword_panel.remove_keyword()
if removed is not None:
self.keywords.pop(removed)
def get_expires_in_seconds(self, selection, value):
if selection == 0:
return None
if selection == 1:
return value * 3600
elif selection == 2:
return value * 86400
elif selection == 3:
return value * 604800
elif selection == 4:
return value * 2592000
return None
def set_expires_in(self, seconds):
if seconds is None:
self.dialog.expiration_choice.SetSelection(0)
self.dialog.expiration_value.Enable(False)
return
if seconds % 2592000 == 0 and seconds >= 2592000:
self.dialog.expiration_choice.SetSelection(4)
self.dialog.expiration_value.SetValue(seconds // 2592000)
elif seconds % 604800 == 0 and seconds >= 604800:
self.dialog.expiration_choice.SetSelection(3)
self.dialog.expiration_value.SetValue(seconds // 604800)
elif seconds % 86400 == 0 and seconds >= 86400:
self.dialog.expiration_choice.SetSelection(2)
self.dialog.expiration_value.SetValue(seconds // 86400)
else:
self.dialog.expiration_choice.SetSelection(1)
self.dialog.expiration_value.SetValue(max(1, seconds // 3600))
self.dialog.expiration_value.Enable(True)
def load_filter_data(self):
if 'title' in self.filter_data:
self.dialog.name_ctrl.SetValue(self.filter_data['title'])
self.dialog.SetTitle(_("Update Filter: {}").format(self.filter_data['title']))
if 'context' in self.filter_data:
for context in self.filter_data['context']:
if context in self.dialog.context_checkboxes:
self.dialog.context_checkboxes[context].SetValue(True)
if 'filter_action' in self.filter_data:
action_index = self.dialog.actions.index(self.filter_data['filter_action']) if self.filter_data['filter_action'] in self.dialog.actions else 0
self.dialog.action_choice.SetSelection(action_index)
if 'expires_in' in self.filter_data:
self.set_expires_in(self.filter_data['expires_in'])
print(self.filter_data)
if 'keywords' in self.filter_data:
self.keywords = self.filter_data['keywords']
self.dialog.keyword_panel.set_keywords(self.filter_data['keywords'])
def get_filter_data(self):
filter_data = {
'title': self.dialog.name_ctrl.GetValue(),
'context': [],
'filter_action': self.dialog.actions[self.dialog.action_choice.GetSelection()],
'expires_in': self.get_expires_in_seconds(selection=self.dialog.expiration_choice.GetSelection(), value=self.dialog.expiration_value.GetValue()),
'keywords_attributes': self.keywords
}
for context, checkbox in self.dialog.context_checkboxes.items():
if checkbox.GetValue():
filter_data['context'].append(context)
return filter_data
def get_response(self):
response = self.dialog.ShowModal()
if response == widgetUtils.OK:
filter_data = self.get_filter_data()
if self.filter_data == None:
result = self.session.api.create_filter_v2(**filter_data)
else:
result = self.session.api.update_filter_v2(filter_id=self.filter_data['id'], **filter_data)
return result
return None

View File

@ -1,99 +0,0 @@
# -*- coding: utf-8 -*-
import datetime
import wx
import widgetUtils
from wxUI import commonMessageDialogs
from wxUI.dialogs.mastodon.filters import manage_filters as dialog
from . import create_filter
from mastodon import MastodonError
class ManageFiltersController(object):
def __init__(self, session):
super(ManageFiltersController, self).__init__()
self.session = session
self.selected_filter_idx = -1
self.error_loading = False
self.dialog = dialog.ManageFiltersDialog(parent=None)
self.dialog.filter_list.Bind(wx.EVT_LIST_ITEM_SELECTED, self.on_filter_selected)
self.dialog.filter_list.Bind(wx.EVT_LIST_ITEM_DESELECTED, self.on_filter_deselected)
widgetUtils.connect_event(self.dialog.add_button, wx.EVT_BUTTON, self.on_add_filter)
widgetUtils.connect_event(self.dialog.edit_button, wx.EVT_BUTTON, self.on_edit_filter)
widgetUtils.connect_event(self.dialog.remove_button, wx.EVT_BUTTON, self.on_remove_filter)
self.load_filter_data()
def on_filter_selected(self, event):
"""Handle filter selection event."""
self.selected_filter_idx = event.GetIndex()
self.dialog.edit_button.Enable()
self.dialog.remove_button.Enable()
def on_filter_deselected(self, event):
"""Handle filter deselection event."""
self.selected_filter_idx = -1
self.dialog.edit_button.Disable()
self.dialog.remove_button.Disable()
def get_selected_filter_id(self):
"""Get the ID of the currently selected filter."""
if self.selected_filter_idx != -1:
return self.dialog.filter_list.GetItemData(self.selected_filter_idx)
return None
def load_filter_data(self):
try:
filters = self.session.api.filters_v2()
self.dialog.filter_list.DeleteAllItems()
self.on_filter_deselected(None)
for i, filter_obj in enumerate(filters):
index = self.dialog.filter_list.InsertItem(i, filter_obj.title)
keyword_count = len(filter_obj.keywords)
self.dialog.filter_list.SetItem(index, 1, str(keyword_count))
contexts = ", ".join(filter_obj.context)
self.dialog.filter_list.SetItem(index, 2, contexts)
self.dialog.filter_list.SetItem(index, 3, filter_obj.filter_action)
if filter_obj.expires_at:
expiry_str = filter_obj.expires_at.strftime("%Y-%m-%d %H:%M")
else:
expiry_str = _("Never")
self.dialog.filter_list.SetItem(index, 4, expiry_str)
self.dialog.filter_list.SetItemData(index, int(filter_obj.id) if isinstance(filter_obj.id, (int, str)) else 0)
except MastodonError as e:
commonMessageDialogs.error_loading_filters()
self.error_loading = True
def on_add_filter(self, *args, **kwargs):
filterController = create_filter.CreateFilterController(self.session)
try:
filter = filterController.get_response()
self.load_filter_data()
except MastodonError as error:
commonMessageDialogs.error_adding_filter()
return self.on_add_filter()
def on_edit_filter(self, *args, **kwargs):
filter_id = self.get_selected_filter_id()
if filter_id == None:
return
try:
filter_data = self.session.api.filter_v2(filter_id)
filterController = create_filter.CreateFilterController(self.session, filter_data=filter_data)
filterController.get_response()
self.load_filter_data()
except MastodonError as error:
commonMessageDialogs.error_adding_filter()
def on_remove_filter(self, *args, **kwargs):
filter_id = self.get_selected_filter_id()
if filter_id == None:
return
dlg = commonMessageDialogs.remove_filter()
if dlg == widgetUtils.NO:
return
try:
self.session.api.delete_filter_v2(filter_id)
self.load_filter_data()
except MastodonError as error:
commonMessageDialogs.error_removing_filter()
def get_response(self):
return self.dialog.ShowModal() == wx.ID_OK

View File

@ -1,9 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import wx import wx
import logging import logging
import mastodon
import output import output
from mastodon import MastodonError
from pubsub import pub from pubsub import pub
from mysc import restart from mysc import restart
from mysc.thread_utils import call_threaded from mysc.thread_utils import call_threaded
@ -12,10 +10,9 @@ from wxUI.dialogs.mastodon import dialogs
from wxUI.dialogs import userAliasDialogs from wxUI.dialogs import userAliasDialogs
from wxUI import commonMessageDialogs from wxUI import commonMessageDialogs
from wxUI.dialogs.mastodon import updateProfile as update_profile_dialogs from wxUI.dialogs.mastodon import updateProfile as update_profile_dialogs
from wxUI.dialogs.mastodon import showUserProfile, communityTimeline from wxUI.dialogs.mastodon import showUserProfile
from sessions.mastodon.utils import html_filter from sessions.mastodon.utils import html_filter
from . import userActions, settings from . import userActions, settings
from .filters import create_filter, manage_filters
log = logging.getLogger("controller.mastodon.handler") log = logging.getLogger("controller.mastodon.handler")
@ -51,9 +48,9 @@ class Handler(object):
details=_("Show user profile"), details=_("Show user profile"),
favs=None, favs=None,
# In buffer Menu. # In buffer Menu.
community_timeline =_("Create c&ommunity timeline"), trends=None,
filter=_("Create a &filter"), filter=None,
manage_filters=_("&Manage filters") manage_filters=None
) )
# Name for the "tweet" menu in the menu bar. # Name for the "tweet" menu in the menu bar.
self.item_menu = _("&Post") self.item_menu = _("&Post")
@ -105,16 +102,9 @@ class Handler(object):
# for i in session.settings["other_buffers"]["lists"]: # for i in session.settings["other_buffers"]["lists"]:
# pub.sendMessage("createBuffer", buffer_type="ListBuffer", session_type=session.type, buffer_title=_(u"List for {}").format(i), parent_tab=lists_position, start=False, kwargs=dict(parent=controller.view.nb, function="list_timeline", name="%s-list" % (i,), sessionObject=session, name, bufferType=None, sound="list_tweet.ogg", list_id=utils.find_list(i, session.db["lists"]), include_ext_alt_text=True, tweet_mode="extended")) # pub.sendMessage("createBuffer", buffer_type="ListBuffer", session_type=session.type, buffer_title=_(u"List for {}").format(i), parent_tab=lists_position, start=False, kwargs=dict(parent=controller.view.nb, function="list_timeline", name="%s-list" % (i,), sessionObject=session, name, bufferType=None, sound="list_tweet.ogg", list_id=utils.find_list(i, session.db["lists"]), include_ext_alt_text=True, tweet_mode="extended"))
pub.sendMessage("createBuffer", buffer_type="EmptyBuffer", session_type="base", buffer_title=_("Searches"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, name="searches", account=name)) pub.sendMessage("createBuffer", buffer_type="EmptyBuffer", session_type="base", buffer_title=_("Searches"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, name="searches", account=name))
searches_position =controller.view.search("searches", name) searches_position =controller.view.search("searches", session.db["user_name"])
for term in session.settings["other_buffers"]["post_searches"]: for term in session.settings["other_buffers"]["post_searches"]:
pub.sendMessage("createBuffer", buffer_type="SearchBuffer", session_type=session.type, buffer_title=_("Search for {}").format(term), parent_tab=searches_position, start=True, kwargs=dict(parent=controller.view.nb, compose_func="compose_post", function="search", name="%s-searchterm" % (term,), sessionObject=session, account=session.get_name(), sound="search_updated.ogg", q=term, result_type="statuses")) pub.sendMessage("createBuffer", buffer_type="SearchBuffer", session_type=session.type, buffer_title=_("Search for {}").format(term), parent_tab=searches_position, start=True, kwargs=dict(parent=controller.view.nb, compose_func="compose_post", function="search", name="%s-searchterm" % (term,), sessionObject=session, account=session.get_name(), sound="search_updated.ogg", q=term, result_type="statuses"))
pub.sendMessage("createBuffer", buffer_type="EmptyBuffer", session_type="base", buffer_title=_("Communities"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, name="communities", account=name))
communities_position =controller.view.search("communities", name)
for community in session.settings["other_buffers"]["communities"]:
bufftype = _("Local") if community.split("@")[0] == "local" else _("federated")
community_name = community.split("@")[1].replace("https://", "")
title = _(f"{bufftype} timeline for {community_name}")
pub.sendMessage("createBuffer", buffer_type="CommunityBuffer", session_type=session.type, buffer_title=title, parent_tab=communities_position, start=True, kwargs=dict(parent=controller.view.nb, function="timeline", compose_func="compose_post", name=community, sessionObject=session, community_url=community.split("@")[1], account=session.get_name(), sound="search_updated.ogg", timeline=community.split("@")[0]))
# for i in session.settings["other_buffers"]["trending_topic_buffers"]: # for i in session.settings["other_buffers"]["trending_topic_buffers"]:
# pub.sendMessage("createBuffer", buffer_type="TrendsBuffer", session_type=session.type, buffer_title=_("Trending topics for %s") % (i), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, name="%s_tt" % (i,), sessionObject=session, name, trendsFor=i, sound="trends_updated.ogg")) # pub.sendMessage("createBuffer", buffer_type="TrendsBuffer", session_type=session.type, buffer_title=_("Trending topics for %s") % (i), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, name="%s_tt" % (i,), sessionObject=session, name, trendsFor=i, sound="trends_updated.ogg"))
@ -131,12 +121,7 @@ class Handler(object):
pub.sendMessage("buffer-title-changed", buffer=buffer) pub.sendMessage("buffer-title-changed", buffer=buffer)
def open_conversation(self, controller, buffer): def open_conversation(self, controller, buffer):
# detect if we are in a community buffer. post = buffer.get_item()
# Community buffers are special because we'll need to retrieve the object locally at first.
if hasattr(buffer, "community_url"):
post = buffer.get_item_from_instance()
else:
post = buffer.get_item()
if post.reblog != None: if post.reblog != None:
post = post.reblog post = post.reblog
conversations_position =controller.view.search("direct_messages", buffer.session.get_name()) conversations_position =controller.view.search("direct_messages", buffer.session.get_name())
@ -145,11 +130,7 @@ class Handler(object):
def follow(self, buffer): def follow(self, buffer):
if not hasattr(buffer, "get_item"): if not hasattr(buffer, "get_item"):
return return
# Community buffers are special because we'll need to retrieve the object locally at first. item = buffer.get_item()
if hasattr(buffer, "community_url"):
item = buffer.get_item_from_instance()
else:
item = buffer.get_item()
if buffer.type == "user": if buffer.type == "user":
users = [item.acct] users = [item.acct]
elif buffer.type == "baseBuffer": elif buffer.type == "baseBuffer":
@ -170,11 +151,7 @@ class Handler(object):
users.insert(0, status.reblog.account.acct) users.insert(0, status.reblog.account.acct)
else: else:
users = [user.acct for user in status.mentions if user.id != buffer.session.db["user_id"]] users = [user.acct for user in status.mentions if user.id != buffer.session.db["user_id"]]
if hasattr(item, "account"): if item.account.acct not in users:
acct = item.account.acct
else:
acct = item.acct
if acct not in users:
users.insert(0, item.account.acct) users.insert(0, item.account.acct)
u = userActions.userActions(buffer.session, users) u = userActions.userActions(buffer.session, users)
@ -201,10 +178,7 @@ class Handler(object):
def open_timeline(self, controller, buffer): def open_timeline(self, controller, buffer):
if not hasattr(buffer, "get_item"): if not hasattr(buffer, "get_item"):
return return
if hasattr(buffer, "community_url"): item = buffer.get_item()
item = buffer.get_item_from_instance()
else:
item = buffer.get_item()
if buffer.type == "user": if buffer.type == "user":
users = [item.acct] users = [item.acct]
elif buffer.type == "baseBuffer": elif buffer.type == "baseBuffer":
@ -381,42 +355,3 @@ class Handler(object):
user = buffer.session.api.account(selectedUser[-1]) user = buffer.session.api.account(selectedUser[-1])
dlg = showUserProfile.ShowUserProfile(user) dlg = showUserProfile.ShowUserProfile(user)
dlg.ShowModal() dlg.ShowModal()
def community_timeline(self, controller, buffer):
dlg = communityTimeline.CommunityTimeline()
if dlg.ShowModal() != wx.ID_OK:
return
url = dlg.url.GetValue()
bufftype = dlg.get_action()
local_api = mastodon.Mastodon(api_base_url=url)
try:
instance = local_api.instance()
except MastodonError:
commonMessageDialogs.invalid_instance()
return
if bufftype == "local":
title = _(f"Local timeline for {url.replace('https://', '')}")
else:
title = _(f"Federated timeline for {url}")
bufftype = "public"
dlg.Destroy()
tl_info = f"{bufftype}@{url}"
if tl_info in buffer.session.settings["other_buffers"]["communities"]:
return # buffer already exists.
buffer.session.settings["other_buffers"]["communities"].append(tl_info)
buffer.session.settings.write()
communities_position =controller.view.search("communities", buffer.session.get_name())
pub.sendMessage("createBuffer", buffer_type="CommunityBuffer", session_type=buffer.session.type, buffer_title=title, parent_tab=communities_position, start=True, kwargs=dict(parent=controller.view.nb, function="timeline", name=tl_info, sessionObject=buffer.session, account=buffer.session.get_name(), sound="tweet_timeline.ogg", community_url=url, timeline=bufftype))
def create_filter(self, controller, buffer):
filterController = create_filter.CreateFilterController(buffer.session)
try:
filter = filterController.get_response()
except MastodonError as error:
log.exception("Error adding filter.")
commonMessageDialogs.error_adding_filter()
return self.create_filter(controller=controller, buffer=buffer)
def manage_filters(self, controller, buffer):
manageFiltersController = manage_filters.ManageFiltersController(buffer.session)
manageFiltersController.get_response()

View File

@ -5,14 +5,11 @@ import wx
import widgetUtils import widgetUtils
import config import config
import output import output
import languageHandler
from twitter_text import parse_tweet, config from twitter_text import parse_tweet, config
from mastodon import MastodonError
from controller import messages from controller import messages
from sessions.mastodon import templates from sessions.mastodon import templates
from wxUI.dialogs.mastodon import postDialogs from wxUI.dialogs.mastodon import postDialogs
from extra.autocompletionUsers import completion from extra.autocompletionUsers import completion
from . import userList
def character_count(post_text, post_cw, character_limit=500): def character_count(post_text, post_cw, character_limit=500):
# We will use text for counting character limit only. # We will use text for counting character limit only.
@ -33,12 +30,9 @@ class post(messages.basicMessage):
self.max = session.char_limit self.max = session.char_limit
self.title = title self.title = title
self.session = session self.session = session
langs = self.session.supported_languages self.message = postDialogs.Post(caption=caption, text=text, *args, **kwargs)
display_langs = [l.name for l in langs]
self.message = postDialogs.Post(caption=caption, text=text, languages=display_langs, *args, **kwargs)
self.message.SetTitle(title) self.message.SetTitle(title)
self.message.text.SetInsertionPoint(len(self.message.text.GetValue())) self.message.text.SetInsertionPoint(len(self.message.text.GetValue()))
self.set_language(self.session.default_language)
widgetUtils.connect_event(self.message.spellcheck, widgetUtils.BUTTON_PRESSED, self.spellcheck) widgetUtils.connect_event(self.message.spellcheck, widgetUtils.BUTTON_PRESSED, self.spellcheck)
widgetUtils.connect_event(self.message.text, widgetUtils.ENTERED_TEXT, self.text_processor) widgetUtils.connect_event(self.message.text, widgetUtils.ENTERED_TEXT, self.text_processor)
widgetUtils.connect_event(self.message.spoiler, widgetUtils.ENTERED_TEXT, self.text_processor) widgetUtils.connect_event(self.message.spoiler, widgetUtils.ENTERED_TEXT, self.text_processor)
@ -74,21 +68,7 @@ class post(messages.basicMessage):
self.add_post(event=None, update_gui=False) self.add_post(event=None, update_gui=False)
return self.thread return self.thread
def set_language(self, language=None): def set_post_data(self, visibility, data):
""" Attempt to set the default language for a post. """
# language can be provided in a post (replying or recovering from errors).
# Also it can be provided in user preferences (retrieved in the session).
# If no language is provided, let's fallback to TWBlue's user language.
if language != None:
language_code = language
else:
# Let's cut langcode_VARIANT to ISO-639 two letter code only.
language_code = languageHandler.curLang[:2]
for lang in self.session.supported_languages:
if lang.code == language_code:
self.message.language.SetStringSelection(lang.name)
def set_post_data(self, visibility, data, language):
if len(data) == 0: if len(data) == 0:
return return
if len(data) > 1: if len(data) > 1:
@ -105,7 +85,6 @@ class post(messages.basicMessage):
self.message.on_sensitivity_changed() self.message.on_sensitivity_changed()
for attachment in self.attachments: for attachment in self.attachments:
self.message.add_item(item=[attachment["file"], attachment["type"], attachment["description"]]) self.message.add_item(item=[attachment["file"], attachment["type"], attachment["description"]])
self.set_language(language)
self.text_processor() self.text_processor()
def text_processor(self, *args, **kwargs): def text_processor(self, *args, **kwargs):
@ -250,24 +229,15 @@ class post(messages.basicMessage):
visibility_settings = ["public", "unlisted", "private", "direct"] visibility_settings = ["public", "unlisted", "private", "direct"]
return visibility_settings[self.message.visibility.GetSelection()] return visibility_settings[self.message.visibility.GetSelection()]
def get_language(self):
langs = self.session.supported_languages
lang = self.message.language.GetSelection()
if lang >= 0:
return langs[lang].code
return None
def set_visibility(self, setting): def set_visibility(self, setting):
visibility_settings = ["public", "unlisted", "private", "direct"] visibility_settings = ["public", "unlisted", "private", "direct"]
visibility_setting = visibility_settings.index(setting) visibility_setting = visibility_settings.index(setting)
self.message.visibility.SetSelection(setting) self.message.visibility.SetSelection(setting)
class viewPost(post): class viewPost(post):
def __init__(self, session, post, offset_hours=0, date="", item_url=""): def __init__(self, post, offset_hours=0, date="", item_url=""):
self.session = session
if post.reblog != None: if post.reblog != None:
post = post.reblog post = post.reblog
self.post_id = post.id
author = post.account.display_name if post.account.display_name != "" else post.account.username author = post.account.display_name if post.account.display_name != "" else post.account.username
title = _(u"Post from {}").format(author) title = _(u"Post from {}").format(author)
image_description = templates.process_image_descriptions(post.media_attachments) image_description = templates.process_image_descriptions(post.media_attachments)
@ -284,12 +254,6 @@ class viewPost(post):
else: else:
source = source_obj.get("name") source = source_obj.get("name")
self.message = postDialogs.viewPost(text=text, boosts_count=boost_count, favs_count=favs_count, source=source, date=date, privacy=privacy) self.message = postDialogs.viewPost(text=text, boosts_count=boost_count, favs_count=favs_count, source=source, date=date, privacy=privacy)
participants = [post.account.id] + [account.id for account in post.mentions]
if self.session.db["user_id"] in participants:
self.message.mute.Enable(True)
if post.muted:
self.message.mute.SetLabel(_("Unmute conversation"))
widgetUtils.connect_event(self.message.mute, widgetUtils.BUTTON_PRESSED, self.mute_unmute)
self.message.SetTitle(title) self.message.SetTitle(title)
if image_description != "": if image_description != "":
self.message.image_description.Enable(True) self.message.image_description.Enable(True)
@ -300,42 +264,12 @@ class viewPost(post):
widgetUtils.connect_event(self.message.share, widgetUtils.BUTTON_PRESSED, self.share) widgetUtils.connect_event(self.message.share, widgetUtils.BUTTON_PRESSED, self.share)
self.item_url = item_url self.item_url = item_url
widgetUtils.connect_event(self.message.translateButton, widgetUtils.BUTTON_PRESSED, self.translate) 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() self.message.ShowModal()
# We won't need text_processor in this dialog, so let's avoid it. # We won't need text_processor in this dialog, so let's avoid it.
def text_processor(self): def text_processor(self):
pass pass
def mute_unmute(self, *args, **kwargs):
post = self.session.api.status(self.post_id)
if post.muted == True:
action = "status_unmute"
new_label = _("Mute conversation")
msg = _("Conversation unmuted.")
else:
action = "status_mute"
new_label = _("Unmute conversation")
msg = _("Conversation muted.")
try:
getattr(self.session.api, action)(self.post_id)
self.message.mute.SetLabel(new_label)
output.speak(msg)
except MastodonError:
return
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): def share(self, *args, **kwargs):
if hasattr(self, "item_url"): if hasattr(self, "item_url"):
output.copy(self.item_url) output.copy(self.item_url)

View File

@ -1,25 +0,0 @@
# -*- 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()

View File

@ -1,21 +1,25 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import widgetUtils import widgetUtils
import output import output
import config from extra import translator, SpellChecker
from extra import SpellChecker
from extra.translator import TranslatorController
class basicMessage(object): class basicMessage(object):
def translate(self, event=None): def translate(self, event=None):
t = TranslatorController(self.message.text.GetValue()) dlg = translator.gui.translateDialog()
if t.response == False: if dlg.get_response() == widgetUtils.OK:
text_to_translate = self.message.text.GetValue()
language_dict = translator.translator.available_languages()
for k in language_dict:
if language_dict[k] == dlg.dest_lang.GetStringSelection():
dst = k
msg = translator.translator.translate(text=text_to_translate, target=dst)
self.message.text.ChangeValue(msg)
self.message.text.SetInsertionPoint(len(self.message.text.GetValue()))
self.text_processor()
self.message.text.SetFocus()
output.speak(_(u"Translated"))
else:
return return
msg = t.translate()
self.message.text.ChangeValue(msg)
self.message.text.SetInsertionPoint(len(self.message.text.GetValue()))
self.text_processor()
self.message.text.SetFocus()
output.speak(_(u"Translated"))
def text_processor(self, *args, **kwargs): def text_processor(self, *args, **kwargs):
pass pass

View File

@ -6,6 +6,7 @@ import config
import languageHandler import languageHandler
import application import application
from pubsub import pub from pubsub import pub
from mysc import autostart as autostart_windows
from wxUI.dialogs import configuration from wxUI.dialogs import configuration
from wxUI import commonMessageDialogs from wxUI import commonMessageDialogs
@ -47,16 +48,21 @@ class globalSettingsController(object):
self.dialog.create_general(langs,self.kmfriendlies) self.dialog.create_general(langs,self.kmfriendlies)
self.dialog.general.language.SetSelection(id) self.dialog.general.language.SetSelection(id)
self.dialog.general.km.SetSelection(self.kmid) self.dialog.general.km.SetSelection(self.kmid)
if paths.mode == "installed":
self.dialog.set_value("general", "autostart", config.app["app-settings"]["autostart"])
else:
self.dialog.general.autostart.Enable(False)
self.dialog.set_value("general", "ask_at_exit", config.app["app-settings"]["ask_at_exit"]) self.dialog.set_value("general", "ask_at_exit", config.app["app-settings"]["ask_at_exit"])
self.dialog.set_value("general", "no_streaming", config.app["app-settings"]["no_streaming"]) self.dialog.set_value("general", "no_streaming", config.app["app-settings"]["no_streaming"])
self.dialog.set_value("general", "play_ready_sound", config.app["app-settings"]["play_ready_sound"]) self.dialog.set_value("general", "play_ready_sound", config.app["app-settings"]["play_ready_sound"])
self.dialog.set_value("general", "speak_ready_msg", config.app["app-settings"]["speak_ready_msg"]) self.dialog.set_value("general", "speak_ready_msg", config.app["app-settings"]["speak_ready_msg"])
self.dialog.set_value("general", "read_long_posts_in_gui", config.app["app-settings"]["read_long_posts_in_gui"]) self.dialog.set_value("general", "handle_longtweets", config.app["app-settings"]["handle_longtweets"])
self.dialog.set_value("general", "use_invisible_shorcuts", config.app["app-settings"]["use_invisible_keyboard_shorcuts"]) self.dialog.set_value("general", "use_invisible_shorcuts", config.app["app-settings"]["use_invisible_keyboard_shorcuts"])
self.dialog.set_value("general", "disable_sapi5", config.app["app-settings"]["voice_enabled"]) self.dialog.set_value("general", "disable_sapi5", config.app["app-settings"]["voice_enabled"])
self.dialog.set_value("general", "hide_gui", config.app["app-settings"]["hide_gui"]) self.dialog.set_value("general", "hide_gui", config.app["app-settings"]["hide_gui"])
self.dialog.set_value("general", "update_period", config.app["app-settings"]["update_period"]) self.dialog.set_value("general", "update_period", config.app["app-settings"]["update_period"])
self.dialog.set_value("general", "check_for_updates", config.app["app-settings"]["check_for_updates"]) self.dialog.set_value("general", "check_for_updates", config.app["app-settings"]["check_for_updates"])
self.dialog.set_value("general", "remember_mention_and_longtweet", config.app["app-settings"]["remember_mention_and_longtweet"])
proxyTypes = [_("System default"), _("HTTP"), _("SOCKS v4"), _("SOCKS v4 with DNS support"), _("SOCKS v5"), _("SOCKS v5 with DNS support")] proxyTypes = [_("System default"), _("HTTP"), _("SOCKS v4"), _("SOCKS v4 with DNS support"), _("SOCKS v5"), _("SOCKS v5 with DNS support")]
self.dialog.create_proxy(proxyTypes) self.dialog.create_proxy(proxyTypes)
try: try:
@ -67,10 +73,7 @@ class globalSettingsController(object):
self.dialog.set_value("proxy", "port", config.app["proxy"]["port"]) self.dialog.set_value("proxy", "port", config.app["proxy"]["port"])
self.dialog.set_value("proxy", "user", config.app["proxy"]["user"]) self.dialog.set_value("proxy", "user", config.app["proxy"]["user"])
self.dialog.set_value("proxy", "password", config.app["proxy"]["password"]) self.dialog.set_value("proxy", "password", config.app["proxy"]["password"])
self.dialog.create_translator_panel()
self.dialog.set_value("translator_panel", "libre_api_url", config.app["translator"]["lt_api_url"])
self.dialog.set_value("translator_panel", "libre_api_key", config.app["translator"]["lt_api_key"])
self.dialog.set_value("translator_panel", "deepL_api_key", config.app["translator"]["deepl_api_key"])
self.dialog.realize() self.dialog.realize()
self.response = self.dialog.get_response() self.response = self.dialog.get_response()
@ -86,6 +89,9 @@ class globalSettingsController(object):
kmFile.close() kmFile.close()
log.debug("Triggered app restart due to a keymap change.") log.debug("Triggered app restart due to a keymap change.")
self.needs_restart = True self.needs_restart = True
if config.app["app-settings"]["autostart"] != self.dialog.get_value("general", "autostart") and paths.mode == "installed":
config.app["app-settings"]["autostart"] = self.dialog.get_value("general", "autostart")
autostart_windows.setAutoStart(application.name, enable=self.dialog.get_value("general", "autostart"))
if config.app["app-settings"]["use_invisible_keyboard_shorcuts"] != self.dialog.get_value("general", "use_invisible_shorcuts"): if config.app["app-settings"]["use_invisible_keyboard_shorcuts"] != self.dialog.get_value("general", "use_invisible_shorcuts"):
config.app["app-settings"]["use_invisible_keyboard_shorcuts"] = self.dialog.get_value("general", "use_invisible_shorcuts") config.app["app-settings"]["use_invisible_keyboard_shorcuts"] = self.dialog.get_value("general", "use_invisible_shorcuts")
pub.sendMessage("invisible-shorcuts-changed", registered=self.dialog.get_value("general", "use_invisible_shorcuts")) pub.sendMessage("invisible-shorcuts-changed", registered=self.dialog.get_value("general", "use_invisible_shorcuts"))
@ -100,10 +106,11 @@ class globalSettingsController(object):
config.app["app-settings"]["voice_enabled"] = self.dialog.get_value("general", "disable_sapi5") config.app["app-settings"]["voice_enabled"] = self.dialog.get_value("general", "disable_sapi5")
config.app["app-settings"]["hide_gui"] = self.dialog.get_value("general", "hide_gui") config.app["app-settings"]["hide_gui"] = self.dialog.get_value("general", "hide_gui")
config.app["app-settings"]["ask_at_exit"] = self.dialog.get_value("general", "ask_at_exit") config.app["app-settings"]["ask_at_exit"] = self.dialog.get_value("general", "ask_at_exit")
config.app["app-settings"]["read_long_posts_in_gui"] = self.dialog.get_value("general", "read_long_posts_in_gui") config.app["app-settings"]["handle_longtweets"] = self.dialog.get_value("general", "handle_longtweets")
config.app["app-settings"]["play_ready_sound"] = self.dialog.get_value("general", "play_ready_sound") config.app["app-settings"]["play_ready_sound"] = self.dialog.get_value("general", "play_ready_sound")
config.app["app-settings"]["speak_ready_msg"] = self.dialog.get_value("general", "speak_ready_msg") config.app["app-settings"]["speak_ready_msg"] = self.dialog.get_value("general", "speak_ready_msg")
config.app["app-settings"]["check_for_updates"] = self.dialog.get_value("general", "check_for_updates") config.app["app-settings"]["check_for_updates"] = self.dialog.get_value("general", "check_for_updates")
config.app["app-settings"]["remember_mention_and_longtweet"] = self.dialog.get_value("general", "remember_mention_and_longtweet")
if config.app["proxy"]["type"]!=self.dialog.get_value("proxy", "type") or config.app["proxy"]["server"] != self.dialog.get_value("proxy", "server") or config.app["proxy"]["port"] != self.dialog.get_value("proxy", "port") or config.app["proxy"]["user"] != self.dialog.get_value("proxy", "user") or config.app["proxy"]["password"] != self.dialog.get_value("proxy", "password"): if config.app["proxy"]["type"]!=self.dialog.get_value("proxy", "type") or config.app["proxy"]["server"] != self.dialog.get_value("proxy", "server") or config.app["proxy"]["port"] != self.dialog.get_value("proxy", "port") or config.app["proxy"]["user"] != self.dialog.get_value("proxy", "user") or config.app["proxy"]["password"] != self.dialog.get_value("proxy", "password"):
if self.is_started == True: if self.is_started == True:
self.needs_restart = True self.needs_restart = True
@ -113,7 +120,4 @@ class globalSettingsController(object):
config.app["proxy"]["port"] = self.dialog.get_value("proxy", "port") config.app["proxy"]["port"] = self.dialog.get_value("proxy", "port")
config.app["proxy"]["user"] = self.dialog.get_value("proxy", "user") config.app["proxy"]["user"] = self.dialog.get_value("proxy", "user")
config.app["proxy"]["password"] = self.dialog.get_value("proxy", "password") config.app["proxy"]["password"] = self.dialog.get_value("proxy", "password")
config.app["translator"]["lt_api_url"] = self.dialog.get_value("translator_panel", "libre_api_url")
config.app["translator"]["lt_api_key"] = self.dialog.get_value("translator_panel", "libre_api_key")
config.app["translator"]["deepl_api_key"] = self.dialog.get_value("translator_panel", "deepL_api_key")
config.app.write() config.app.write()

View File

@ -1,23 +0,0 @@
# -*- 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

View File

@ -8,8 +8,8 @@ class autocompletionScanDialog(widgetUtils.BaseDialog):
super(autocompletionScanDialog, self).__init__(parent=None, id=-1, title=_(u"Autocomplete users' settings")) super(autocompletionScanDialog, self).__init__(parent=None, id=-1, title=_(u"Autocomplete users' settings"))
panel = wx.Panel(self) panel = wx.Panel(self)
sizer = wx.BoxSizer(wx.VERTICAL) sizer = wx.BoxSizer(wx.VERTICAL)
self.followers = wx.CheckBox(panel, -1, _("Add &followers to database")) self.followers = wx.CheckBox(panel, -1, _("Add followers to database"))
self.friends = wx.CheckBox(panel, -1, _("Add f&ollowing to database")) self.friends = wx.CheckBox(panel, -1, _("Add following to database"))
sizer.Add(self.followers, 0, wx.ALL, 5) sizer.Add(self.followers, 0, wx.ALL, 5)
sizer.Add(self.friends, 0, wx.ALL, 5) sizer.Add(self.friends, 0, wx.ALL, 5)
ok = wx.Button(panel, wx.ID_OK) ok = wx.Button(panel, wx.ID_OK)

View File

@ -1,2 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from .translator import TranslatorController from __future__ import absolute_import
from __future__ import unicode_literals
from . import translator
from . import wx_ui as gui

View File

@ -1 +0,0 @@
# -*- coding: utf-8 -*-

View File

@ -1,14 +0,0 @@
# -*- coding: utf-8 -*-
import config
from deepl import Translator
def translate(text: str, target_language: str) -> str:
key = config.app["translator"]["deepl_api_key"]
t = Translator(key)
return t.translate_text(text, target_lang=target_language).text
def languages():
key = config.app["translator"]["deepl_api_key"]
t = Translator(key)
langs = t.get_target_languages()
return langs

View File

@ -1,45 +0,0 @@
# -*- coding: utf-8 -*-
""" Modified Libretranslatepy module which adds an user agent for making requests against more instances. """
import json
from typing import Any, Dict
from urllib import request, parse
from libretranslatepy import LibreTranslateAPI
class CustomLibreTranslateAPI(LibreTranslateAPI):
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
def _create_request(self, url: str, method: str, data: Dict[str, str]) -> request.Request:
url_params = parse.urlencode(data)
req = request.Request(url, method=method, data=url_params.encode())
req.add_header("User-Agent", self.USER_AGENT)
return req
def translate(self, q: str, source: str = "en", target: str = "es", timeout: int | None = None) -> Any:
url = self.url + "translate"
params: Dict[str, str] = {"q": q, "source": source, "target": target}
if self.api_key is not None:
params["api_key"] = self.api_key
req = self._create_request(url=url, method="POST", data=params)
response = request.urlopen(req, timeout=timeout)
response_str = response.read().decode()
return json.loads(response_str)["translatedText"]
def detect(self, q: str, timeout: int | None = None) -> Any:
url = self.url + "detect"
params: Dict[str, str] = {"q": q}
if self.api_key is not None:
params["api_key"] = self.api_key
req = self._create_request(url=url, method="POST", data=params)
response = request.urlopen(req, timeout=timeout)
response_str = response.read().decode()
return json.loads(response_str)
def languages(self, timeout: int | None = None) -> Any:
url = self.url + "languages"
params: Dict[str, str] = dict()
if self.api_key is not None:
params["api_key"] = self.api_key
req = self._create_request(url=url, method="GET", data=params)
response = request.urlopen(req, timeout=timeout)
response_str = response.read().decode()
return json.loads(response_str)

View File

@ -1,58 +1,116 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import logging import logging
import threading from googletrans import Translator, LANGUAGES
import wx
import config
from pubsub import pub
from . engines import libre_translate, deep_l
from .wx_ui import translateDialog
log = logging.getLogger("extras.translator") log = logging.getLogger("extras.translator")
class TranslatorController(object): # create a single translator instance
def __init__(self, text): # see https://github.com/ssut/py-googletrans/issues/234
super(TranslatorController, self).__init__() t = None
self.text = text
self.languages = []
self.response = False
self.dialog = translateDialog()
pub.subscribe(self.on_engine_changed, "translator.engine_changed")
if config.app["translator"]["engine"] == "LibreTranslate":
self.dialog.engine_select.SetSelection(0)
elif config.app["translator"]["engine"] == "DeepL":
self.dialog.engine_select.SetSelection(1)
threading.Thread(target=self.load_languages).start()
if self.dialog.ShowModal() == wx.ID_OK:
self.response = True
for k in self.language_dict:
if self.language_dict[k] == self.dialog.dest_lang.GetStringSelection():
self.target_language= k
pub.unsubscribe(self.on_engine_changed, "translator.engine_changed")
def load_languages(self): def translate(text="", target="en"):
self.language_dict = self.get_languages() global t
self.languages = [self.language_dict[k] for k in self.language_dict] log.debug("Received translation request for language %s, text=%s" % (target, text))
self.dialog.set_languages(self.languages) if t == None:
t = Translator()
vars = dict(text=text, dest=target)
return t.translate(**vars).text
def on_engine_changed(self, engine): supported_langs = None
config.app["translator"]["engine"] = engine
config.app.write()
threading.Thread(target=self.load_languages).start()
def translate(self): languages = {
log.debug("Received translation request for language %s, text=%s" % (self.target_language, self.text)) "af": _(u"Afrikaans"),
if config.app["translator"].get("engine") == "LibreTranslate": "sq": _(u"Albanian"),
translator = libre_translate.CustomLibreTranslateAPI(config.app["translator"]["lt_api_url"], config.app["translator"]["lt_api_key"]) "am": _(u"Amharic"),
vars = dict(q=self.text, target=self.target_language) "ar": _(u"Arabic"),
return translator.translate(**vars) "hy": _(u"Armenian"),
elif config.app["translator"]["engine"] == "DeepL" and config.app["translator"]["deepl_api_key"] != "": "az": _(u"Azerbaijani"),
return deep_l.translate(text=self.text, target_language=self.target_language) "eu": _(u"Basque"),
"be": _(u"Belarusian"),
"bn": _(u"Bengali"),
"bh": _(u"Bihari"),
"bg": _(u"Bulgarian"),
"my": _(u"Burmese"),
"ca": _(u"Catalan"),
"chr": _(u"Cherokee"),
"zh": _(u"Chinese"),
"zh-CN": _(u"Chinese_simplified"),
"zh-TW": _(u"Chinese_traditional"),
"hr": _(u"Croatian"),
"cs": _(u"Czech"),
"da": _(u"Danish"),
"dv": _(u"Dhivehi"),
"nl": _(u"Dutch"),
"en": _(u"English"),
"eo": _(u"Esperanto"),
"et": _(u"Estonian"),
"tl": _(u"Filipino"),
"fi": _(u"Finnish"),
"fr": _(u"French"),
"gl": _(u"Galician"),
"ka": _(u"Georgian"),
"de": _(u"German"),
"el": _(u"Greek"),
"gn": _(u"Guarani"),
"gu": _(u"Gujarati"),
"iw": _(u"Hebrew"),
"hi": _(u"Hindi"),
"hu": _(u"Hungarian"),
"is": _(u"Icelandic"),
"id": _(u"Indonesian"),
"iu": _(u"Inuktitut"),
"ga": _(u"Irish"),
"it": _(u"Italian"),
"ja": _(u"Japanese"),
"kn": _(u"Kannada"),
"kk": _(u"Kazakh"),
"km": _(u"Khmer"),
"ko": _(u"Korean"),
"ku": _(u"Kurdish"),
"ky": _(u"Kyrgyz"),
"lo": _(u"Laothian"),
"lv": _(u"Latvian"),
"lt": _(u"Lithuanian"),
"mk": _(u"Macedonian"),
"ms": _(u"Malay"),
"ml": _(u"Malayalam"),
"mt": _(u"Maltese"),
"mr": _(u"Marathi"),
"mn": _(u"Mongolian"),
"ne": _(u"Nepali"),
"no": _(u"Norwegian"),
"or": _(u"Oriya"),
"ps": _(u"Pashto"),
"fa": _(u"Persian"),
"pl": _(u"Polish"),
"pt": _(u"Portuguese"),
"pa": _(u"Punjabi"),
"ro": _(u"Romanian"),
"ru": _(u"Russian"),
"sa": _(u"Sanskrit"),
"sr": _(u"Serbian"),
"sd": _(u"Sindhi"),
"si": _(u"Sinhalese"),
"sk": _(u"Slovak"),
"sl": _(u"Slovenian"),
"es": _(u"Spanish"),
"sw": _(u"Swahili"),
"sv": _(u"Swedish"),
"tg": _(u"Tajik"),
"ta": _(u"Tamil"),
"tl": _(u"Tagalog"),
"te": _(u"Telugu"),
"th": _(u"Thai"),
"bo": _(u"Tibetan"),
"tr": _(u"Turkish"),
"uk": _(u"Ukrainian"),
"ur": _(u"Urdu"),
"uz": _(u"Uzbek"),
"ug": _(u"Uighur"),
"vi": _(u"Vietnamese"),
"cy": _(u"Welsh"),
"yi": _(u"Yiddish")
}
def get_languages(self): def available_languages():
languages = {} return dict(sorted(languages.items(), key=lambda x: x[1]))
if config.app["translator"].get("engine") == "LibreTranslate":
translator = libre_translate.CustomLibreTranslateAPI(config.app["translator"]["lt_api_url"], config.app["translator"]["lt_api_key"])
languages = {l.get("code"): l.get("name") for l in translator.languages()}
elif config.app["translator"]["engine"] == "DeepL" and config.app["translator"]["deepl_api_key"] != "":
languages = {language.code: language.name for language in deep_l.languages()}
return dict(sorted(languages.items(), key=lambda x: x[1]))

View File

@ -16,26 +16,23 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
############################################################ ############################################################
from . import translator
import wx import wx
from pubsub import pub
from wxUI.dialogs import baseDialog from wxUI.dialogs import baseDialog
class translateDialog(baseDialog.BaseWXDialog): class translateDialog(baseDialog.BaseWXDialog):
def __init__(self): def __init__(self):
languages = []
language_dict = translator.available_languages()
for k in language_dict:
languages.append(language_dict[k])
super(translateDialog, self).__init__(None, -1, title=_(u"Translate message")) super(translateDialog, self).__init__(None, -1, title=_(u"Translate message"))
self.engines = ["LibreTranslate", "DeepL"]
panel = wx.Panel(self) panel = wx.Panel(self)
sizer = wx.BoxSizer(wx.VERTICAL) sizer = wx.BoxSizer(wx.VERTICAL)
staticEngine = wx.StaticText(panel, -1, _(u"Translation engine"))
self.engine_select = wx.ComboBox(panel, -1, choices=self.engines, style=wx.CB_READONLY)
self.engine_select.Bind(wx.EVT_COMBOBOX, lambda event: pub.sendMessage("translator.engine_changed", engine=self.engine_select.GetValue()))
staticDest = wx.StaticText(panel, -1, _(u"Target language")) staticDest = wx.StaticText(panel, -1, _(u"Target language"))
self.dest_lang = wx.ComboBox(panel, -1, style = wx.CB_READONLY) self.dest_lang = wx.ComboBox(panel, -1, choices=languages, style = wx.CB_READONLY)
self.dest_lang.SetFocus() self.dest_lang.SetFocus()
self.dest_lang.SetSelection(0) self.dest_lang.SetSelection(0)
engineSizer = wx.BoxSizer(wx.HORIZONTAL)
engineSizer.Add(staticEngine)
engineSizer.Add(self.engine_select)
listSizer = wx.BoxSizer(wx.HORIZONTAL) listSizer = wx.BoxSizer(wx.HORIZONTAL)
listSizer.Add(staticDest) listSizer.Add(staticDest)
listSizer.Add(self.dest_lang) listSizer.Add(self.dest_lang)
@ -43,14 +40,6 @@ class translateDialog(baseDialog.BaseWXDialog):
ok.SetDefault() ok.SetDefault()
cancel = wx.Button(panel, wx.ID_CANCEL) cancel = wx.Button(panel, wx.ID_CANCEL)
self.SetEscapeId(wx.ID_CANCEL) self.SetEscapeId(wx.ID_CANCEL)
sizer.Add(engineSizer, 0, wx.EXPAND | wx.ALL, 5)
sizer.Add(listSizer, 0, wx.EXPAND | wx.ALL, 5)
sizer.Add(ok, 0, wx.ALIGN_CENTER | wx.ALL, 5)
sizer.Add(cancel, 0, wx.ALIGN_CENTER | wx.ALL, 5)
panel.SetSizer(sizer)
def set_languages(self, languages):
wx.CallAfter(self.dest_lang.SetItems, languages)
def get(self, control): def get(self, control):
return getattr(self, control).GetSelection() return getattr(self, control).GetSelection()

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import httpcore
httpcore.SyncHTTPTransport = httpcore.AsyncHTTPProxy
import sys import sys
import os import os
import platform import platform

View File

@ -3,7 +3,6 @@ access_token = string(default="")
instance = string(default="") instance = string(default="")
user_name = string(default="") user_name = string(default="")
ignored_clients = list(default=list()) ignored_clients = list(default=list())
type = string(default="mastodon")
[general] [general]
relative_times = boolean(default=True) relative_times = boolean(default=True)
@ -30,7 +29,6 @@ indicate_img = boolean(default=True)
[other_buffers] [other_buffers]
timelines = list(default=list()) timelines = list(default=list())
searches = list(default=list()) searches = list(default=list())
communities = list(default=list())
lists = list(default=list()) lists = list(default=list())
followers_timelines = list(default=list()) followers_timelines = list(default=list())
following_timelines = list(default=list()) following_timelines = list(default=list())

44
src/mysc/autostart.py Normal file
View File

@ -0,0 +1,44 @@
from __future__ import unicode_literals
from future import standard_library
standard_library.install_aliases()
from builtins import str
import winreg
import os
import sys
from platform_utils import paths
RUN_REGKEY = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Run"
def is_installed(app_subkey):
"""Checks if the currently running copy is installed or portable variant. Requires the name of the application subkey found under the uninstall section in Windows registry."""
try:
key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\%s" % app_subkey)
inst_dir = winreg.QueryValueEx(key,"InstallLocation")[0]
except WindowsError:
return False
winreg.CloseKey(key)
try:
return os.stat(inst_dir) == os.stat(paths.app_path())
except WindowsError:
return False
def getAutoStart(app_name):
"""Queries if the automatic startup should be set for the application or not, depending on it's current state."""
try:
key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, RUN_REGKEY)
val = winreg.QueryValueEx(key, str(app_name))[0]
return os.stat(val) == os.stat(sys.argv[0])
except (WindowsError, OSError):
return False
def setAutoStart(app_name, enable=True):
"""Configures automatic startup for the application, if the enable argument is set to True. If set to False, deletes the application AutoStart value."""
if getAutoStart(app_name) == enable:
return
key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, RUN_REGKEY, 0, winreg.KEY_WRITE)
if enable:
winreg.SetValueEx(key, str(app_name), None, winreg.REG_SZ, paths.get_executable())
else:
winreg.DeleteValue(key, str(app_name))

View File

@ -14,7 +14,6 @@ import application
from pubsub import pub from pubsub import pub
from controller import settings from controller import settings
from sessions.mastodon import session as MastodonSession from sessions.mastodon import session as MastodonSession
from sessions.gotosocial import session as GotosocialSession
from . import manager from . import manager
from . import wxUI as view from . import wxUI as view
@ -69,7 +68,7 @@ class sessionManagerController(object):
name = _("{account_name}@{instance} (Mastodon)").format(account_name=config_test["mastodon"]["user_name"], instance=config_test["mastodon"]["instance"].replace("https://", "")) name = _("{account_name}@{instance} (Mastodon)").format(account_name=config_test["mastodon"]["user_name"], instance=config_test["mastodon"]["instance"].replace("https://", ""))
if config_test["mastodon"]["instance"] != "" and config_test["mastodon"]["access_token"] != "": if config_test["mastodon"]["instance"] != "" and config_test["mastodon"]["access_token"] != "":
sessionsList.append(name) sessionsList.append(name)
self.sessions.append(dict(type=config_test["mastodon"].get("type", "mastodon"), id=i)) self.sessions.append(dict(type="mastodon", id=i))
else: else:
try: try:
log.debug("Deleting session %s" % (i,)) log.debug("Deleting session %s" % (i,))
@ -95,8 +94,6 @@ class sessionManagerController(object):
# Create the session object based in session type. # Create the session object based in session type.
if i.get("type") == "mastodon": if i.get("type") == "mastodon":
s = MastodonSession.Session(i.get("id")) s = MastodonSession.Session(i.get("id"))
elif i.get("type") == "gotosocial":
s = GotosocialSession.Session(i.get("id"))
s.get_configuration() s.get_configuration()
if i.get("id") not in config.app["sessions"]["ignored_sessions"]: if i.get("id") not in config.app["sessions"]["ignored_sessions"]:
try: try:
@ -119,7 +116,7 @@ class sessionManagerController(object):
s = MastodonSession.Session(location) s = MastodonSession.Session(location)
result = s.authorise() result = s.authorise()
if result == True: if result == True:
self.sessions.append(dict(id=location, type=s.settings["mastodon"].get("type"))) self.sessions.append(dict(id=location, type=type))
self.view.add_new_session_to_list() self.view.add_new_session_to_list()
def remove_account(self, index): def remove_account(self, index):

View File

@ -1 +0,0 @@
# -*- coding: utf-8 -*-

View File

@ -1,13 +0,0 @@
from sessions.mastodon import session
class Session(session.Session):
# disable version check so Mastodon.py won't throw exceptions.
version_check_mode = "none"
name = "GoToSocial"
def get_lists(self):
""" Gets the lists that the user is subscribed to and stores them in the database. Returns None."""
self.db["lists"] = []
def get_muted_users(self):
self.db["muted_users"] = []

View File

@ -17,9 +17,6 @@ def compose_post(post, db, settings, relative_times, show_screen_names, safe=Tru
text = _("Boosted from @{}: {}").format(post.reblog.account.acct, templates.process_text(post.reblog, safe=safe)) text = _("Boosted from @{}: {}").format(post.reblog.account.acct, templates.process_text(post.reblog, safe=safe))
else: else:
text = templates.process_text(post, safe=safe) text = templates.process_text(post, safe=safe)
filtered = utils.evaluate_filters(post=post, current_context="home")
if filtered != None:
text = _("hidden by filter {}").format(filtered)
source = post.get("application", "") source = post.get("application", "")
# "" means remote user, None for legacy apps so we should cover both sides. # "" means remote user, None for legacy apps so we should cover both sides.
if source != None and source != "": if source != None and source != "":
@ -76,7 +73,4 @@ def compose_notification(notification, db, settings, relative_times, show_screen
text = _("A poll in which you have voted has expired: {status}").format(status=",".join(compose_post(notification.status, db, settings, relative_times, show_screen_names, safe=safe))) text = _("A poll in which you have voted has expired: {status}").format(status=",".join(compose_post(notification.status, db, settings, relative_times, show_screen_names, safe=safe)))
elif notification.type == "follow_request": elif notification.type == "follow_request":
text = _("{username} wants to follow you.").format(username=user) text = _("{username} wants to follow you.").format(username=user)
filtered = utils.evaluate_filters(post=notification, current_context="notifications")
if filtered != None:
text = _("hidden by filter {}").format(filtered)
return [user, text, ts] return [user, text, ts]

View File

@ -11,7 +11,7 @@ import config
import config_utils import config_utils
import output import output
import application import application
from mastodon import MastodonError, MastodonAPIError, MastodonNotFoundError, MastodonUnauthorizedError, MastodonVersionError from mastodon import MastodonError, MastodonAPIError, MastodonNotFoundError, MastodonUnauthorizedError
from pubsub import pub from pubsub import pub
from mysc.thread_utils import call_threaded from mysc.thread_utils import call_threaded
from sessions import base from sessions import base
@ -19,23 +19,19 @@ from sessions.mastodon import utils, streaming
log = logging.getLogger("sessions.mastodonSession") log = logging.getLogger("sessions.mastodonSession")
MASTODON_VERSION = "4.3.2" MASTODON_VERSION = "4.0.1"
class Session(base.baseSession): class Session(base.baseSession):
version_check_mode = "created"
name = "Mastodon"
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(Session, self).__init__(*args, **kwargs) super(Session, self).__init__(*args, **kwargs)
self.config_spec = "mastodon.defaults" self.config_spec = "mastodon.defaults"
self.supported_languages = [] self.supported_languages = []
self.default_language = None
self.type = "mastodon" self.type = "mastodon"
self.db["pagination_info"] = dict() self.db["pagination_info"] = dict()
self.char_limit = 500 self.char_limit = 500
self.post_visibility = "public" self.post_visibility = "public"
self.expand_spoilers = False self.expand_spoilers = False
self.software = "mastodon"
pub.subscribe(self.on_status, "mastodon.status_received") pub.subscribe(self.on_status, "mastodon.status_received")
pub.subscribe(self.on_status_updated, "mastodon.status_updated") pub.subscribe(self.on_status_updated, "mastodon.status_updated")
pub.subscribe(self.on_notification, "mastodon.notification_received") pub.subscribe(self.on_notification, "mastodon.notification_received")
@ -44,20 +40,17 @@ class Session(base.baseSession):
if self.settings["mastodon"]["access_token"] != None and self.settings["mastodon"]["instance"] != None: if self.settings["mastodon"]["access_token"] != None and self.settings["mastodon"]["instance"] != None:
try: try:
log.debug("Logging in to Mastodon instance {}...".format(self.settings["mastodon"]["instance"])) log.debug("Logging in to Mastodon instance {}...".format(self.settings["mastodon"]["instance"]))
self.api = mastodon.Mastodon(access_token=self.settings["mastodon"]["access_token"], api_base_url=self.settings["mastodon"]["instance"], mastodon_version=MASTODON_VERSION, user_agent="TWBlue/{}".format(application.version), version_check_mode=self.version_check_mode) self.api = mastodon.Mastodon(access_token=self.settings["mastodon"]["access_token"], api_base_url=self.settings["mastodon"]["instance"], mastodon_version=MASTODON_VERSION, user_agent="TWBlue/{}".format(application.version))
if verify_credentials == True: if verify_credentials == True:
credentials = self.api.account_verify_credentials() credentials = self.api.account_verify_credentials()
self.db["user_name"] = credentials["username"] self.db["user_name"] = credentials["username"]
self.db["user_id"] = credentials["id"] self.db["user_id"] = credentials["id"]
if hasattr(credentials, "source") and hasattr(credentials.source, "language"):
log.info(f"Setting default language on account {credentials.username} to {credentials.source.language}")
self.default_language = credentials.source.language
self.settings["mastodon"]["user_name"] = credentials["username"] self.settings["mastodon"]["user_name"] = credentials["username"]
self.logged = True self.logged = True
log.debug("Logged.") log.debug("Logged.")
self.counter = 0 self.counter = 0
except MastodonError: except MastodonError:
log.exception(f"The login attempt failed on instance {self.settings['mastodon']['instance']}.") log.exception("The login attempt failed.")
self.logged = False self.logged = False
else: else:
self.logged = False self.logged = False
@ -74,7 +67,7 @@ class Session(base.baseSession):
return return
try: try:
client_id, client_secret = mastodon.Mastodon.create_app("TWBlue", api_base_url=authorisation_dialog.GetValue(), website="https://twblue.es") client_id, client_secret = mastodon.Mastodon.create_app("TWBlue", api_base_url=authorisation_dialog.GetValue(), website="https://twblue.es")
temporary_api = mastodon.Mastodon(client_id=client_id, client_secret=client_secret, api_base_url=instance, mastodon_version=MASTODON_VERSION, user_agent="TWBlue/{}".format(application.version), version_check_mode="none") # disable version check so we can handle more platforms than Mastodon. temporary_api = mastodon.Mastodon(client_id=client_id, client_secret=client_secret, api_base_url=instance, mastodon_version=MASTODON_VERSION, user_agent="TWBlue/{}".format(application.version))
auth_url = temporary_api.auth_request_url() auth_url = temporary_api.auth_request_url()
except MastodonError: except MastodonError:
dlg = wx.MessageDialog(None, _("We could not connect to your mastodon instance. Please verify that the domain exists and the instance is accessible via a web browser."), _("Instance error"), wx.ICON_ERROR) dlg = wx.MessageDialog(None, _("We could not connect to your mastodon instance. Please verify that the domain exists and the instance is accessible via a web browser."), _("Instance error"), wx.ICON_ERROR)
@ -97,13 +90,6 @@ class Session(base.baseSession):
return return
self.create_session_folder() self.create_session_folder()
self.get_configuration() self.get_configuration()
# handle when the instance is GoTosocial.
# this might be extended for other activity pub software later on.
nodeinfo = temporary_api.instance_nodeinfo()
if nodeinfo.software.get("name") == "gotosocial":
self.settings["mastodon"]["type"] = nodeinfo.software.get("name")
# GoToSocial doesn't support certain buffers so we redefine all of them here.
self.settings["general"]["buffer_order"] = ['home', 'local', 'mentions', 'sent', 'favorites', 'bookmarks', 'followers', 'following', 'blocked', 'notifications']
self.settings["mastodon"]["access_token"] = access_token self.settings["mastodon"]["access_token"] = access_token
self.settings["mastodon"]["instance"] = instance self.settings["mastodon"]["instance"] = instance
self.settings.write() self.settings.write()
@ -117,10 +103,7 @@ class Session(base.baseSession):
self.db["utc_offset"] = offset self.db["utc_offset"] = offset
instance = self.api.instance() instance = self.api.instance()
if len(self.supported_languages) == 0: if len(self.supported_languages) == 0:
try: self.supported_languages = instance.languages
self.supported_languages = self.api.instance_languages()
except (Exception, MastodonVersionError):
pass
self.get_lists() self.get_lists()
self.get_muted_users() self.get_muted_users()
# determine instance custom characters limit. # determine instance custom characters limit.
@ -134,20 +117,11 @@ class Session(base.baseSession):
def get_lists(self): def get_lists(self):
""" Gets the lists that the user is subscribed to and stores them in the database. Returns None.""" """ Gets the lists that the user is subscribed to and stores them in the database. Returns None."""
if self.software == "gotosocial":
self.db["lists"] = []
return
self.db["lists"] = self.api.lists() self.db["lists"] = self.api.lists()
def get_muted_users(self): def get_muted_users(self):
### ToDo: Use a function to retrieve all muted users. ### ToDo: Use a function to retrieve all muted users.
if self.software == "gotosocial": self.db["muted_users"] = self.api.mutes()
self.db["muted_users"] = []
return
try:
self.db["muted_users"] = self.api.mutes()
except MastodonNotFoundError:
self.db["muted_users"] = []
def order_buffer(self, name, data, ignore_older=False): def order_buffer(self, name, data, ignore_older=False):
num = 0 num = 0
@ -170,9 +144,6 @@ class Session(base.baseSession):
log.error("Ignoring an older tweet... Last id: {0}, tweet id: {1}".format(last_id, i.id)) log.error("Ignoring an older tweet... Last id: {0}, tweet id: {1}".format(last_id, i.id))
continue continue
if utils.find_item(i, self.db[name]) == None: if utils.find_item(i, self.db[name]) == None:
filter_status = utils.evaluate_filters(post=i, current_context=utils.get_current_context(name))
if filter_status == "hide":
continue
if self.settings["general"]["reverse_timelines"] == False: objects.append(i) if self.settings["general"]["reverse_timelines"] == False: objects.append(i)
else: objects.insert(0, i) else: objects.insert(0, i)
num = num+1 num = num+1
@ -217,17 +188,17 @@ class Session(base.baseSession):
self.sound.play(_sound) self.sound.play(_sound)
return val return val
def send_post(self, reply_to=None, visibility=None, language=None, posts=[]): def send_post(self, reply_to=None, visibility=None, posts=[]):
""" Convenience function to send a thread. """ """ Convenience function to send a thread. """
in_reply_to_id = reply_to in_reply_to_id = reply_to
for obj in posts: for obj in posts:
text = obj.get("text") text = obj.get("text")
if len(obj["attachments"]) == 0: if len(obj["attachments"]) == 0:
try: try:
item = self.api_call(call_name="status_post", status=text, _sound="tweet_send.ogg", in_reply_to_id=in_reply_to_id, visibility=visibility, sensitive=obj["sensitive"], spoiler_text=obj["spoiler_text"], language=language) item = self.api_call(call_name="status_post", status=text, _sound="tweet_send.ogg", in_reply_to_id=in_reply_to_id, visibility=visibility, sensitive=obj["sensitive"], spoiler_text=obj["spoiler_text"])
# If it fails, let's basically send an event with all passed info so we will catch it later. # If it fails, let's basically send an event with all passed info so we will catch it later.
except Exception as e: except Exception as e:
pub.sendMessage("mastodon.error_post", name=self.get_name(), reply_to=reply_to, visibility=visibility, posts=posts, lang=language) pub.sendMessage("mastodon.error_post", name=self.get_name(), reply_to=reply_to, visibility=visibility, posts=posts)
return return
if item != None: if item != None:
in_reply_to_id = item["id"] in_reply_to_id = item["id"]
@ -241,25 +212,23 @@ class Session(base.baseSession):
for i in obj["attachments"]: for i in obj["attachments"]:
media = self.api_call("media_post", media_file=i["file"], description=i["description"], synchronous=True) media = self.api_call("media_post", media_file=i["file"], description=i["description"], synchronous=True)
media_ids.append(media.id) media_ids.append(media.id)
item = self.api_call(call_name="status_post", status=text, _sound="tweet_send.ogg", in_reply_to_id=in_reply_to_id, media_ids=media_ids, visibility=visibility, poll=poll, sensitive=obj["sensitive"], spoiler_text=obj["spoiler_text"], language=language) item = self.api_call(call_name="status_post", status=text, _sound="tweet_send.ogg", in_reply_to_id=in_reply_to_id, media_ids=media_ids, visibility=visibility, poll=poll, sensitive=obj["sensitive"], spoiler_text=obj["spoiler_text"])
if item != None: if item != None:
in_reply_to_id = item["id"] in_reply_to_id = item["id"]
except Exception as e: except Exception as e:
pub.sendMessage("mastodon.error_post", name=self.get_name(), reply_to=reply_to, visibility=visibility, posts=posts, lang=language) pub.sendMessage("mastodon.error_post", name=self.get_name(), reply_to=reply_to, visibility=visibility, posts=posts)
return return
def get_name(self): def get_name(self):
instance = self.settings["mastodon"]["instance"] instance = self.settings["mastodon"]["instance"]
instance = instance.replace("https://", "") instance = instance.replace("https://", "")
user = self.settings["mastodon"]["user_name"] user = self.settings["mastodon"]["user_name"]
return "{}@{} ({})".format(user, instance, self.name) return "Mastodon: {}@{}".format(user, instance)
def start_streaming(self): def start_streaming(self):
if self.settings["general"]["disable_streaming"]: if self.settings["general"]["disable_streaming"]:
log.info("Streaming is disabled for session {}. Skipping...".format(self.get_name())) log.info("Streaming is disabled for session {}. Skipping...".format(self.get_name()))
return return
if self.software == "gotosocial":
return
listener = streaming.StreamListener(session_name=self.get_name(), user_id=self.db["user_id"]) listener = streaming.StreamListener(session_name=self.get_name(), user_id=self.db["user_id"])
try: try:
stream_healthy = self.api.stream_healthy() stream_healthy = self.api.stream_healthy()

View File

@ -9,7 +9,7 @@ from . import utils, compose
# This will be used for the edit template dialog. # This will be used for the edit template dialog.
# Available variables for post objects. # Available variables for post objects.
# safe_text will be the content warning in case a post contains one, text will always be the full text, no matter if has a content warning or not. # safe_text will be the content warning in case a post contains one, text will always be the full text, no matter if has a content warning or not.
post_variables = ["date", "display_name", "screen_name", "source", "lang", "safe_text", "text", "image_descriptions", "visibility", "pinned"] post_variables = ["date", "display_name", "screen_name", "source", "lang", "safe_text", "text", "image_descriptions", "visibility"]
person_variables = ["display_name", "screen_name", "description", "followers", "following", "favorites", "posts", "created_at"] person_variables = ["display_name", "screen_name", "description", "followers", "following", "favorites", "posts", "created_at"]
conversation_variables = ["users", "last_post"] conversation_variables = ["users", "last_post"]
notification_variables = ["display_name", "screen_name", "text", "date"] notification_variables = ["display_name", "screen_name", "text", "date"]
@ -57,7 +57,6 @@ def render_post(post, template, settings, relative_times=False, offset_hours=0):
$text: Toot text. This always displays the full text, even if there is a content warning present. $text: Toot text. This always displays the full text, even if there is a content warning present.
$image_descriptions: Information regarding image descriptions added by twitter users. $image_descriptions: Information regarding image descriptions added by twitter users.
$visibility: post's visibility: public, not listed, followers only or direct. $visibility: post's visibility: public, not listed, followers only or direct.
$pinned: Wether the post is pinned or not (if not pinned, this will be blank).
""" """
global post_variables global post_variables
available_data = dict(source="") available_data = dict(source="")
@ -76,9 +75,6 @@ def render_post(post, template, settings, relative_times=False, offset_hours=0):
else: else:
text = process_text(post, safe=False) text = process_text(post, safe=False)
safe_text = process_text(post) safe_text = process_text(post)
filtered = utils.evaluate_filters(post=post, current_context="home")
if filtered != None:
text = _("hidden by filter {}").format(filtered)
visibility_settings = dict(public=_("Public"), unlisted=_("Not listed"), private=_("Followers only"), direct=_("Direct")) visibility_settings = dict(public=_("Public"), unlisted=_("Not listed"), private=_("Followers only"), direct=_("Direct"))
visibility = visibility_settings.get(post.visibility) visibility = visibility_settings.get(post.visibility)
available_data.update(lang=post.language, text=text, safe_text=safe_text, visibility=visibility) available_data.update(lang=post.language, text=text, safe_text=safe_text, visibility=visibility)
@ -89,12 +85,6 @@ def render_post(post, template, settings, relative_times=False, offset_hours=0):
else: else:
image_descriptions = process_image_descriptions(post.media_attachments) image_descriptions = process_image_descriptions(post.media_attachments)
available_data.update(image_descriptions=image_descriptions) available_data.update(image_descriptions=image_descriptions)
# Process if the post is pinned
if post.get("pinned", False):
pinned = _("Pinned.")
else:
pinned = ""
available_data.update(pinned=pinned)
result = Template(_(template)).safe_substitute(**available_data) result = Template(_(template)).safe_substitute(**available_data)
return result return result
@ -171,9 +161,6 @@ def render_notification(notification, template, post_template, settings, relativ
text = _("A poll in which you have voted has expired: {status}").format(status=render_post(notification.status, post_template, settings, relative_times, offset_hours)) text = _("A poll in which you have voted has expired: {status}").format(status=render_post(notification.status, post_template, settings, relative_times, offset_hours))
elif notification.type == "follow_request": elif notification.type == "follow_request":
text = _("wants to follow you.") text = _("wants to follow you.")
filtered = utils.evaluate_filters(post=notification, current_context="notifications")
if filtered != None:
text = _("hidden by filter {}").format(filtered)
available_data.update(text=text) available_data.update(text=text)
result = Template(_(template)).safe_substitute(**available_data) result = Template(_(template)).safe_substitute(**available_data)
result = result.replace(" . ", "") result = result.replace(" . ", "")

View File

@ -1,7 +1,6 @@
import re import re
import demoji import demoji
from html.parser import HTMLParser from html.parser import HTMLParser
from datetime import datetime, timezone
url_re = re.compile('<a\s*href=[\'|"](.*?)[\'"].*?>') url_re = re.compile('<a\s*href=[\'|"](.*?)[\'"].*?>')
@ -92,59 +91,3 @@ def demoji_user(name, settings):
user = re.sub(r":(.*?):", "", user) user = re.sub(r":(.*?):", "", user)
return user return user
return name return name
def get_current_context(buffer: str) -> str:
""" Gets the name of a buffer and returns the context it belongs to. useful for filtering. """
if buffer == "home_timeline":
return "home"
elif buffer == "mentions" or buffer == "notifications":
return "notifications"
def evaluate_filters(post: dict, current_context: str) -> str | None:
"""
Evaluates the 'filtered' attribute of a Mastodon post to determine its visibility,
considering the current context, expiration, and matches (keywords or status).
Parameters:
post (dict): A dictionary representing a Mastodon post.
current_context (str): The context in which the post is displayed
(e.g., "home", "notifications", "public", "thread", or "profile").
Returns:
- "hide" if any applicable filter indicates the post should be hidden.
- A string with the filter's title if an applicable "warn" filter is present.
- None if no applicable filters are found, meaning the post should be shown normally.
"""
filters = post.get("filtered", None)
if filters == None:
return
warn_filter_title = None
now = datetime.now(timezone.utc)
for result in filters:
filter_data = result.get("filter", {})
# Check if the filter applies to the current context.
filter_contexts = filter_data.get("context", [])
if current_context not in filter_contexts:
continue # Skip filters not applicable in this context
# Check if the filter has expired.
expires_at = filter_data.get("expires_at")
if expires_at is not None:
# If expires_at is a string, attempt to parse it.
if isinstance(expires_at, str):
try:
expiration_dt = datetime.fromisoformat(expires_at)
except ValueError:
continue # Skip if the date format is invalid
else:
expiration_dt = expires_at
if expiration_dt < now:
continue # Skip expired filters
action = filter_data.get("filter_action", "")
if action == "hide":
return "hide"
elif action == "warn":
title = filter_data.get("title", "")
warn_filter_title = title if title else "warn"
if warn_filter_title:
return warn_filter_title
return None

View File

@ -29,8 +29,8 @@ def donation():
dlg = wx.MessageDialog(None, _(u"If you like {0} we need your help to keep it going. Help us by donating to the project. This will help us pay for the server, the domain and some other things to ensure that {0} will be actively maintained. Your donation will give us the means to continue the development of {0}, and to keep {0} free. Would you like to donate now?").format(application.name), _(u"We need your help"), wx.ICON_QUESTION|wx.YES_NO) dlg = wx.MessageDialog(None, _(u"If you like {0} we need your help to keep it going. Help us by donating to the project. This will help us pay for the server, the domain and some other things to ensure that {0} will be actively maintained. Your donation will give us the means to continue the development of {0}, and to keep {0} free. Would you like to donate now?").format(application.name), _(u"We need your help"), wx.ICON_QUESTION|wx.YES_NO)
return dlg.ShowModal() return dlg.ShowModal()
def changed_keymap(system, keystroke_editor_shortcut): def changed_keymap():
return wx.MessageDialog(None, _(f"TWBlue has detected that you're running {system} and has changed the default keymap to the {system} keymap. It means that some keyboard shorcuts could be different. Please check the keystroke editor by pressing {keystroke_editor_shortcut} to see all available keystrokes for this keymap."), _(u"Information"), wx.OK).ShowModal() return wx.MessageDialog(None, _(u"TWBlue has detected that you're running windows 10 and has changed the default keymap to the Windows 10 keymap. It means that some keyboard shorcuts could be different. Please check the keystroke editor by pressing Alt+Win+K to see all available keystrokes for this keymap."), _(u"Information"), wx.OK).ShowModal()
def invalid_configuration(): def invalid_configuration():
return wx.MessageDialog(None, _("The configuration file is invalid."), _("Error"), wx.ICON_ERROR).ShowModal() return wx.MessageDialog(None, _("The configuration file is invalid."), _("Error"), wx.ICON_ERROR).ShowModal()
@ -44,18 +44,3 @@ def cant_update_source() -> wx.MessageDialog:
""" """
dlg = wx.MessageDialog(None, _("Sorry, you can't update while running {} from source.").format(application.name), _("Error"), wx.OK) dlg = wx.MessageDialog(None, _("Sorry, you can't update while running {} from source.").format(application.name), _("Error"), wx.OK)
return dlg.ShowModal() return dlg.ShowModal()
def invalid_instance():
return wx.MessageDialog(None, _("the provided instance is invalid. Please try again."), _("Invalid instance"), wx.ICON_ERROR).ShowModal()
def error_adding_filter():
return wx.MessageDialog(None, _("TWBlue was unable to add or update the filter with the specified settings. Please try again."), _("Error"), wx.ICON_ERROR).ShowModal()
def error_loading_filters():
return wx.MessageDialog(None, _("TWBlue was unable to load your filters from the instance. Please try again."), _("Error"), wx.ICON_ERROR).ShowModal()
def remove_filter():
dlg = wx.MessageDialog(None, _("Do you really want to delete this filter ?"), _("Delete filter"), wx.ICON_QUESTION|wx.YES_NO)
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()

View File

@ -1,4 +1,4 @@
# -*- coding: utf-8 -*- from __future__ import unicode_literals
import wx import wx
class BaseWXDialog(wx.Dialog): class BaseWXDialog(wx.Dialog):

View File

@ -12,43 +12,47 @@ class general(wx.Panel, baseDialog.BaseWXDialog):
def __init__(self, parent, languages,keymaps): def __init__(self, parent, languages,keymaps):
super(general, self).__init__(parent) super(general, self).__init__(parent)
sizer = wx.BoxSizer(wx.VERTICAL) sizer = wx.BoxSizer(wx.VERTICAL)
language = wx.StaticText(self, -1, _(u"&Language")) language = wx.StaticText(self, -1, _(u"Language"))
self.language = wx.ListBox(self, -1, choices=languages) self.language = wx.ListBox(self, -1, choices=languages)
self.language.SetSize(self.language.GetBestSize()) self.language.SetSize(self.language.GetBestSize())
langBox = wx.BoxSizer(wx.HORIZONTAL) langBox = wx.BoxSizer(wx.HORIZONTAL)
langBox.Add(language, 0, wx.ALL, 5) langBox.Add(language, 0, wx.ALL, 5)
langBox.Add(self.language, 0, wx.ALL, 5) langBox.Add(self.language, 0, wx.ALL, 5)
sizer.Add(langBox, 0, wx.ALL, 5) sizer.Add(langBox, 0, wx.ALL, 5)
self.ask_at_exit = wx.CheckBox(self, -1, _(U"&Ask before exiting {0}").format(application.name,)) self.autostart = wx.CheckBox(self, -1, _(u"Run {0} at Windows startup").format(application.name,))
self.ask_at_exit = wx.CheckBox(self, -1, _(U"ask before exiting {0}").format(application.name,))
sizer.Add(self.autostart, 0, wx.ALL, 5)
sizer.Add(self.ask_at_exit, 0, wx.ALL, 5) sizer.Add(self.ask_at_exit, 0, wx.ALL, 5)
self.no_streaming = wx.CheckBox(self, -1, _(U"&Disable Streaming functions")) self.no_streaming = wx.CheckBox(self, -1, _(U"Disable Streaming functions"))
sizer.Add(self.no_streaming, 0, wx.ALL, 5) sizer.Add(self.no_streaming, 0, wx.ALL, 5)
updatePeriodBox = wx.BoxSizer(wx.HORIZONTAL) updatePeriodBox = wx.BoxSizer(wx.HORIZONTAL)
updatePeriodBox.Add(wx.StaticText(self, -1, _(u"&Buffer update interval, in minutes")), 0, wx.ALL, 5) updatePeriodBox.Add(wx.StaticText(self, -1, _(u"Buffer update interval, in minutes")), 0, wx.ALL, 5)
self.update_period = wx.SpinCtrl(self, wx.ID_ANY) self.update_period = wx.SpinCtrl(self, wx.ID_ANY)
self.update_period.SetRange(1, 30) self.update_period.SetRange(1, 30)
self.update_period.SetSize(self.update_period.GetBestSize()) self.update_period.SetSize(self.update_period.GetBestSize())
updatePeriodBox.Add(self.update_period, 0, wx.ALL, 5) updatePeriodBox.Add(self.update_period, 0, wx.ALL, 5)
sizer.Add(updatePeriodBox, 0, wx.ALL, 5) sizer.Add(updatePeriodBox, 0, wx.ALL, 5)
self.play_ready_sound = wx.CheckBox(self, -1, _(U"Pla&y a sound when {0} launches").format(application.name,)) self.play_ready_sound = wx.CheckBox(self, -1, _(U"Play a sound when {0} launches").format(application.name,))
sizer.Add(self.play_ready_sound, 0, wx.ALL, 5) sizer.Add(self.play_ready_sound, 0, wx.ALL, 5)
self.speak_ready_msg = wx.CheckBox(self, -1, _(U"Sp&eak a message when {0} launches").format(application.name,)) self.speak_ready_msg = wx.CheckBox(self, -1, _(U"Speak a message when {0} launches").format(application.name,))
sizer.Add(self.speak_ready_msg, 0, wx.ALL, 5) sizer.Add(self.speak_ready_msg, 0, wx.ALL, 5)
self.use_invisible_shorcuts = wx.CheckBox(self, -1, _(u"&Use invisible interface's keyboard shortcuts while GUI is visible")) self.use_invisible_shorcuts = wx.CheckBox(self, -1, _(u"Use invisible interface's keyboard shortcuts while GUI is visible"))
sizer.Add(self.use_invisible_shorcuts, 0, wx.ALL, 5) sizer.Add(self.use_invisible_shorcuts, 0, wx.ALL, 5)
self.disable_sapi5 = wx.CheckBox(self, -1, _(u"A&ctivate Sapi5 when any other screen reader is not being run")) self.disable_sapi5 = wx.CheckBox(self, -1, _(u"Activate Sapi5 when any other screen reader is not being run"))
sizer.Add(self.disable_sapi5, 0, wx.ALL, 5) sizer.Add(self.disable_sapi5, 0, wx.ALL, 5)
self.hide_gui = wx.CheckBox(self, -1, _(u"&Hide GUI on launch")) self.hide_gui = wx.CheckBox(self, -1, _(u"Hide GUI on launch"))
sizer.Add(self.hide_gui, 0, wx.ALL, 5) sizer.Add(self.hide_gui, 0, wx.ALL, 5)
self.read_long_posts_in_gui = wx.CheckBox(self, wx.ID_ANY, _("&Read long posts in GUI")) self.handle_longtweets = wx.CheckBox(self, wx.ID_ANY, _(u"Use Codeofdusk's longtweet handlers (may decrease client performance)"))
sizer.Add(self.read_long_posts_in_gui, 0, wx.ALL, 5) sizer.Add(self.handle_longtweets, 0, wx.ALL, 5)
self.remember_mention_and_longtweet = wx.CheckBox(self, -1, _(u"Remember state for mention all and long tweet"))
sizer.Add(self.remember_mention_and_longtweet, 0, wx.ALL, 5)
kmbox = wx.BoxSizer(wx.VERTICAL) kmbox = wx.BoxSizer(wx.VERTICAL)
km_label = wx.StaticText(self, -1, _(u"&Keymap")) km_label = wx.StaticText(self, -1, _(u"Keymap"))
self.km = wx.ComboBox(self, -1, choices=keymaps, style=wx.CB_READONLY) self.km = wx.ComboBox(self, -1, choices=keymaps, style=wx.CB_READONLY)
self.km.SetSize(self.km.GetBestSize()) self.km.SetSize(self.km.GetBestSize())
kmbox.Add(km_label, 0, wx.ALL, 5) kmbox.Add(km_label, 0, wx.ALL, 5)
kmbox.Add(self.km, 0, wx.ALL, 5) kmbox.Add(self.km, 0, wx.ALL, 5)
self.check_for_updates = wx.CheckBox(self, -1, _(U"Check for u&pdates when {0} launches").format(application.name,)) self.check_for_updates = wx.CheckBox(self, -1, _(U"Check for updates when {0} launches").format(application.name,))
sizer.Add(self.check_for_updates, 0, wx.ALL, 5) sizer.Add(self.check_for_updates, 0, wx.ALL, 5)
sizer.Add(kmbox, 0, wx.ALL, 5) sizer.Add(kmbox, 0, wx.ALL, 5)
self.SetSizer(sizer) self.SetSizer(sizer)
@ -58,32 +62,32 @@ class proxy(wx.Panel, baseDialog.BaseWXDialog):
def __init__(self, parent, proxyTypes): def __init__(self, parent, proxyTypes):
super(proxy, self).__init__(parent) super(proxy, self).__init__(parent)
sizer = wx.BoxSizer(wx.VERTICAL) sizer = wx.BoxSizer(wx.VERTICAL)
type=wx.StaticText(self, wx.ID_ANY, _(u"Proxy &type: ")) type=wx.StaticText(self, wx.ID_ANY, _(u"Proxy type: "))
self.type=wx.ComboBox(self, -1, choices=proxyTypes, style=wx.CB_READONLY) self.type=wx.ComboBox(self, -1, choices=proxyTypes, style=wx.CB_READONLY)
self.type.SetSize(self.type.GetBestSize()) self.type.SetSize(self.type.GetBestSize())
typeBox = wx.BoxSizer(wx.HORIZONTAL) typeBox = wx.BoxSizer(wx.HORIZONTAL)
typeBox.Add(type, 0, wx.ALL, 5) typeBox.Add(type, 0, wx.ALL, 5)
typeBox.Add(self.type, 0, wx.ALL, 5) typeBox.Add(self.type, 0, wx.ALL, 5)
sizer.Add(typeBox, 0, wx.ALL, 5) sizer.Add(typeBox, 0, wx.ALL, 5)
lbl = wx.StaticText(self, wx.ID_ANY, _(u"Proxy s&erver: ")) lbl = wx.StaticText(self, wx.ID_ANY, _(u"Proxy server: "))
self.server = wx.TextCtrl(self, -1) self.server = wx.TextCtrl(self, -1)
serverBox = wx.BoxSizer(wx.HORIZONTAL) serverBox = wx.BoxSizer(wx.HORIZONTAL)
serverBox.Add(lbl, 0, wx.ALL, 5) serverBox.Add(lbl, 0, wx.ALL, 5)
serverBox.Add(self.server, 0, wx.ALL, 5) serverBox.Add(self.server, 0, wx.ALL, 5)
sizer.Add(serverBox, 0, wx.ALL, 5) sizer.Add(serverBox, 0, wx.ALL, 5)
lbl = wx.StaticText(self, wx.ID_ANY, _(u"&Port: ")) lbl = wx.StaticText(self, wx.ID_ANY, _(u"Port: "))
self.port = wx.SpinCtrl(self, wx.ID_ANY, min=1, max=65535) self.port = wx.SpinCtrl(self, wx.ID_ANY, min=1, max=65535)
portBox = wx.BoxSizer(wx.HORIZONTAL) portBox = wx.BoxSizer(wx.HORIZONTAL)
portBox.Add(lbl, 0, wx.ALL, 5) portBox.Add(lbl, 0, wx.ALL, 5)
portBox.Add(self.port, 0, wx.ALL, 5) portBox.Add(self.port, 0, wx.ALL, 5)
sizer.Add(portBox, 0, wx.ALL, 5) sizer.Add(portBox, 0, wx.ALL, 5)
lbl = wx.StaticText(self, wx.ID_ANY, _(u"&User: ")) lbl = wx.StaticText(self, wx.ID_ANY, _(u"User: "))
self.user = wx.TextCtrl(self, wx.ID_ANY) self.user = wx.TextCtrl(self, wx.ID_ANY)
userBox = wx.BoxSizer(wx.HORIZONTAL) userBox = wx.BoxSizer(wx.HORIZONTAL)
userBox.Add(lbl, 0, wx.ALL, 5) userBox.Add(lbl, 0, wx.ALL, 5)
userBox.Add(self.user, 0, wx.ALL, 5) userBox.Add(self.user, 0, wx.ALL, 5)
sizer.Add(userBox, 0, wx.ALL, 5) sizer.Add(userBox, 0, wx.ALL, 5)
lbl = wx.StaticText(self, wx.ID_ANY, _(u"P&assword: ")) lbl = wx.StaticText(self, wx.ID_ANY, _(u"Password: "))
self.password = wx.TextCtrl(self, wx.ID_ANY, style=wx.TE_PASSWORD) self.password = wx.TextCtrl(self, wx.ID_ANY, style=wx.TE_PASSWORD)
passwordBox = wx.BoxSizer(wx.HORIZONTAL) passwordBox = wx.BoxSizer(wx.HORIZONTAL)
passwordBox.Add(lbl, 0, wx.ALL, 5) passwordBox.Add(lbl, 0, wx.ALL, 5)
@ -95,9 +99,9 @@ class reporting(wx.Panel, baseDialog.BaseWXDialog):
def __init__(self, parent): def __init__(self, parent):
super(reporting, self).__init__(parent) super(reporting, self).__init__(parent)
sizer = wx.BoxSizer(wx.VERTICAL) sizer = wx.BoxSizer(wx.VERTICAL)
self.speech_reporting = wx.CheckBox(self, wx.ID_ANY, _(U"Enable automatic s&peech feedback")) self.speech_reporting = wx.CheckBox(self, wx.ID_ANY, _(U"Enable automatic speech feedback"))
sizer.Add(self.speech_reporting, 0, wx.ALL, 5) sizer.Add(self.speech_reporting, 0, wx.ALL, 5)
self.braille_reporting = wx.CheckBox(self, wx.ID_ANY, _(U"Enable automatic &Braille feedback")) self.braille_reporting = wx.CheckBox(self, wx.ID_ANY, _(U"Enable automatic Braille feedback"))
sizer.Add(self.braille_reporting, 0, wx.ALL, 5) sizer.Add(self.braille_reporting, 0, wx.ALL, 5)
self.SetSizer(sizer) self.SetSizer(sizer)
@ -108,9 +112,9 @@ class other_buffers(wx.Panel):
self.buffers = widgets.list(self, _(u"Buffer"), _(u"Name"), _(u"Status"), style=wx.LC_SINGLE_SEL|wx.LC_REPORT) self.buffers = widgets.list(self, _(u"Buffer"), _(u"Name"), _(u"Status"), style=wx.LC_SINGLE_SEL|wx.LC_REPORT)
sizer.Add(self.buffers.list, 0, wx.ALL, 5) sizer.Add(self.buffers.list, 0, wx.ALL, 5)
btnSizer = wx.BoxSizer(wx.HORIZONTAL) btnSizer = wx.BoxSizer(wx.HORIZONTAL)
self.toggle_state = wx.Button(self, -1, _(u"S&how/hide")) self.toggle_state = wx.Button(self, -1, _(u"Show/hide"))
self.up = wx.Button(self, -1, _(u"Move &up")) self.up = wx.Button(self, -1, _(u"Move up"))
self.down = wx.Button(self, -1, _(u"Move &down")) self.down = wx.Button(self, -1, _(u"Move down"))
btnSizer.Add(self.toggle_state, 0, wx.ALL, 5) btnSizer.Add(self.toggle_state, 0, wx.ALL, 5)
btnSizer.Add(self.up, 0, wx.ALL, 5) btnSizer.Add(self.up, 0, wx.ALL, 5)
btnSizer.Add(self.down, 0, wx.ALL, 5) btnSizer.Add(self.down, 0, wx.ALL, 5)
@ -197,30 +201,6 @@ class other_buffers(wx.Panel):
buffers_list.append(self.buffers.get_text_column(i, 0)) buffers_list.append(self.buffers.get_text_column(i, 0))
return buffers_list return buffers_list
class TranslatorPanel(wx.Panel, baseDialog.BaseWXDialog):
def __init__(self, parent):
super(TranslatorPanel, self).__init__(parent)
sizer = wx.BoxSizer(wx.VERTICAL)
lbl_libre_url = wx.StaticText(self, wx.ID_ANY, _(u"&LibreTranslate API URL: "))
self.libre_api_url = wx.TextCtrl(self, wx.ID_ANY)
libreUrlBox = wx.BoxSizer(wx.HORIZONTAL)
libreUrlBox.Add(lbl_libre_url, 0, wx.ALL, 5)
libreUrlBox.Add(self.libre_api_url, 1, wx.ALL | wx.EXPAND, 5)
sizer.Add(libreUrlBox, 0, wx.ALL | wx.EXPAND, 5)
lbl_libre_api_key = wx.StaticText(self, wx.ID_ANY, _(u"LibreTranslate API &Key (optional): "))
self.libre_api_key = wx.TextCtrl(self, wx.ID_ANY)
libreApiKeyBox = wx.BoxSizer(wx.HORIZONTAL)
libreApiKeyBox.Add(lbl_libre_api_key, 0, wx.ALL, 5)
libreApiKeyBox.Add(self.libre_api_key, 1, wx.ALL | wx.EXPAND, 5)
sizer.Add(libreApiKeyBox, 0, wx.ALL | wx.EXPAND, 5)
lbl_deepL_api_key = wx.StaticText(self, wx.ID_ANY, _(u"&DeepL API Key: "))
self.deepL_api_key = wx.TextCtrl(self, wx.ID_ANY)
deepLApiKeyBox = wx.BoxSizer(wx.HORIZONTAL)
deepLApiKeyBox.Add(lbl_deepL_api_key, 0, wx.ALL, 5)
deepLApiKeyBox.Add(self.deepL_api_key, 1, wx.ALL | wx.EXPAND, 5)
sizer.Add(deepLApiKeyBox, 0, wx.ALL | wx.EXPAND, 5)
self.SetSizer(sizer)
class configurationDialog(baseDialog.BaseWXDialog): class configurationDialog(baseDialog.BaseWXDialog):
def set_title(self, title): def set_title(self, title):
self.SetTitle(title) self.SetTitle(title)
@ -241,16 +221,12 @@ class configurationDialog(baseDialog.BaseWXDialog):
self.proxy = proxy(self.notebook, proxyTypes) self.proxy = proxy(self.notebook, proxyTypes)
self.notebook.AddPage(self.proxy, _(u"Proxy")) self.notebook.AddPage(self.proxy, _(u"Proxy"))
def create_translator_panel(self):
self.translator_panel= TranslatorPanel(self.notebook)
self.notebook.AddPage(self.translator_panel, _("Translation services"))
def realize(self): def realize(self):
self.sizer.Add(self.notebook, 0, wx.ALL, 5) self.sizer.Add(self.notebook, 0, wx.ALL, 5)
ok_cancel_box = wx.BoxSizer(wx.HORIZONTAL) ok_cancel_box = wx.BoxSizer(wx.HORIZONTAL)
ok = wx.Button(self.panel, wx.ID_OK, _(u"&Save")) ok = wx.Button(self.panel, wx.ID_OK, _(u"Save"))
ok.SetDefault() ok.SetDefault()
cancel = wx.Button(self.panel, wx.ID_CANCEL, _(u"&Close")) cancel = wx.Button(self.panel, wx.ID_CANCEL, _(u"Close"))
self.SetEscapeId(cancel.GetId()) self.SetEscapeId(cancel.GetId())
ok_cancel_box.Add(ok, 0, wx.ALL, 5) ok_cancel_box.Add(ok, 0, wx.ALL, 5)
ok_cancel_box.Add(cancel, 0, wx.ALL, 5) ok_cancel_box.Add(cancel, 0, wx.ALL, 5)

View File

@ -1,38 +0,0 @@
# -*- coding: utf-8 -*-
import wx
class CommunityTimeline(wx.Dialog):
def __init__(self, *args, **kwargs):
super(CommunityTimeline, self).__init__(parent=None, *args, **kwargs)
panel = wx.Panel(self)
communitySizer = wx.BoxSizer()
self.SetTitle(_("Create community timeline"))
communityLabel = wx.StaticText(panel, -1, _("Community URL"))
self.url = wx.TextCtrl(panel, -1)
self.url.SetFocus()
communitySizer.Add(communityLabel, 0, wx.ALL, 5)
communitySizer.Add(self.url, 0, wx.ALL, 5)
actionSizer = wx.BoxSizer(wx.VERTICAL)
label2 = wx.StaticText(panel, -1, _(u"Buffer type"))
self.local= wx.RadioButton(panel, -1, _("&Local timeline"), style=wx.RB_GROUP)
self.federated= wx.RadioButton(panel, -1, _("&Federated Timeline"))
hSizer = wx.BoxSizer(wx.HORIZONTAL)
hSizer.Add(label2, 0, wx.ALL, 5)
actionSizer.Add(self.local, 0, wx.ALL, 5)
actionSizer.Add(self.federated, 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(communitySizer)
sizer.Add(hSizer, 0, wx.ALL, 5)
sizer.Add(btnsizer)
panel.SetSizer(sizer)
def get_action(self):
if self.local.GetValue() == True: return "local"
elif self.federated.GetValue() == True: return "federated"

View File

@ -12,49 +12,49 @@ class generalAccount(wx.Panel, baseDialog.BaseWXDialog):
super(generalAccount, self).__init__(parent) super(generalAccount, self).__init__(parent)
sizer = wx.BoxSizer(wx.VERTICAL) sizer = wx.BoxSizer(wx.VERTICAL)
userAutocompletionBox = wx.StaticBox(self, label=_("User autocompletion settings")) userAutocompletionBox = wx.StaticBox(self, label=_("User autocompletion settings"))
self.userAutocompletionScan = wx.Button(self, wx.ID_ANY, _("Scan acc&ount and add followers and following users to the user autocompletion database")) self.userAutocompletionScan = wx.Button(self, wx.ID_ANY, _("Scan account and add followers and following users to the user autocompletion database"))
self.userAutocompletionManage = wx.Button(self, wx.ID_ANY, _("&Manage autocompletion database")) self.userAutocompletionManage = wx.Button(self, wx.ID_ANY, _("Manage autocompletion database"))
autocompletionSizer = wx.StaticBoxSizer(userAutocompletionBox, wx.HORIZONTAL) autocompletionSizer = wx.StaticBoxSizer(userAutocompletionBox, wx.HORIZONTAL)
autocompletionSizer.Add(self.userAutocompletionScan, 0, wx.ALL, 5) autocompletionSizer.Add(self.userAutocompletionScan, 0, wx.ALL, 5)
autocompletionSizer.Add(self.userAutocompletionManage, 0, wx.ALL, 5) autocompletionSizer.Add(self.userAutocompletionManage, 0, wx.ALL, 5)
sizer.Add(autocompletionSizer, 0, wx.ALL, 5) sizer.Add(autocompletionSizer, 0, wx.ALL, 5)
self.disable_streaming = wx.CheckBox(self, wx.ID_ANY, _("&Disable Streaming API endpoints")) self.disable_streaming = wx.CheckBox(self, wx.ID_ANY, _("Disable Streaming API endpoints"))
sizer.Add(self.disable_streaming, 0, wx.ALL, 5) sizer.Add(self.disable_streaming, 0, wx.ALL, 5)
self.relative_time = wx.CheckBox(self, wx.ID_ANY, _("&Relative timestamps")) self.relative_time = wx.CheckBox(self, wx.ID_ANY, _("Relative timestamps"))
sizer.Add(self.relative_time, 0, wx.ALL, 5) sizer.Add(self.relative_time, 0, wx.ALL, 5)
self.read_preferences_from_instance = wx.CheckBox(self, wx.ID_ANY, _("R&ead preferences from instance (default visibility when publishing and displaying sensitive content)")) self.read_preferences_from_instance = wx.CheckBox(self, wx.ID_ANY, _("Read preferences from instance (default visibility when publishing and displaying sensitive content)"))
sizer.Add(self.read_preferences_from_instance, 0, wx.ALL, 5) sizer.Add(self.read_preferences_from_instance, 0, wx.ALL, 5)
itemsPerCallBox = wx.BoxSizer(wx.HORIZONTAL) itemsPerCallBox = wx.BoxSizer(wx.HORIZONTAL)
itemsPerCallBox.Add(wx.StaticText(self, -1, _("&Items on each API call")), 0, wx.ALL, 5) itemsPerCallBox.Add(wx.StaticText(self, -1, _("Items on each API call")), 0, wx.ALL, 5)
self.itemsPerApiCall = wx.SpinCtrl(self, wx.ID_ANY) self.itemsPerApiCall = wx.SpinCtrl(self, wx.ID_ANY)
self.itemsPerApiCall.SetRange(0, 40) self.itemsPerApiCall.SetRange(0, 40)
self.itemsPerApiCall.SetSize(self.itemsPerApiCall.GetBestSize()) self.itemsPerApiCall.SetSize(self.itemsPerApiCall.GetBestSize())
itemsPerCallBox.Add(self.itemsPerApiCall, 0, wx.ALL, 5) itemsPerCallBox.Add(self.itemsPerApiCall, 0, wx.ALL, 5)
sizer.Add(itemsPerCallBox, 0, wx.ALL, 5) sizer.Add(itemsPerCallBox, 0, wx.ALL, 5)
self.reverse_timelines = wx.CheckBox(self, wx.ID_ANY, _("I&nverted buffers: The newest items will be shown at the beginning while the oldest at the end")) self.reverse_timelines = wx.CheckBox(self, wx.ID_ANY, _("Inverted buffers: The newest items will be shown at the beginning while the oldest at the end"))
sizer.Add(self.reverse_timelines, 0, wx.ALL, 5) sizer.Add(self.reverse_timelines, 0, wx.ALL, 5)
self.ask_before_boost = wx.CheckBox(self, wx.ID_ANY, _("&Ask confirmation before boosting a post")) self.ask_before_boost = wx.CheckBox(self, wx.ID_ANY, _("Ask confirmation before boosting a post"))
sizer.Add(self.ask_before_boost, 0, wx.ALL, 5) sizer.Add(self.ask_before_boost, 0, wx.ALL, 5)
self.show_screen_names = wx.CheckBox(self, wx.ID_ANY, _("S&how screen names instead of full names")) self.show_screen_names = wx.CheckBox(self, wx.ID_ANY, _("Show screen names instead of full names"))
sizer.Add(self.show_screen_names, 0, wx.ALL, 5) sizer.Add(self.show_screen_names, 0, wx.ALL, 5)
self.hide_emojis = wx.CheckBox(self, wx.ID_ANY, _("Hide e&mojis in usernames")) self.hide_emojis = wx.CheckBox(self, wx.ID_ANY, _("hide emojis in usernames"))
sizer.Add(self.hide_emojis, 0, wx.ALL, 5) sizer.Add(self.hide_emojis, 0, wx.ALL, 5)
PersistSizeLabel = wx.StaticText(self, -1, _("&Number of items per buffer to cache in database (0 to disable caching, blank for unlimited)")) PersistSizeLabel = wx.StaticText(self, -1, _("Number of items per buffer to cache in database (0 to disable caching, blank for unlimited)"))
self.persist_size = wx.TextCtrl(self, -1) self.persist_size = wx.TextCtrl(self, -1)
sizer.Add(PersistSizeLabel, 0, wx.ALL, 5) sizer.Add(PersistSizeLabel, 0, wx.ALL, 5)
sizer.Add(self.persist_size, 0, wx.ALL, 5) sizer.Add(self.persist_size, 0, wx.ALL, 5)
self.load_cache_in_memory = wx.CheckBox(self, wx.NewId(), _("&Load cache for items in memory (much faster in big datasets but requires more RAM)")) self.load_cache_in_memory = wx.CheckBox(self, wx.NewId(), _("Load cache for items in memory (much faster in big datasets but requires more RAM)"))
self.SetSizer(sizer) self.SetSizer(sizer)
class templates(wx.Panel, baseDialog.BaseWXDialog): class templates(wx.Panel, baseDialog.BaseWXDialog):
def __init__(self, parent, post_template, conversation_template, person_template): def __init__(self, parent, post_template, conversation_template, person_template):
super(templates, self).__init__(parent) super(templates, self).__init__(parent)
sizer = wx.BoxSizer(wx.VERTICAL) sizer = wx.BoxSizer(wx.VERTICAL)
self.post = wx.Button(self, wx.ID_ANY, _("Edit template for &posts. Current template: {}").format(post_template)) self.post = wx.Button(self, wx.ID_ANY, _("Edit template for posts. Current template: {}").format(post_template))
sizer.Add(self.post, 0, wx.ALL, 5) sizer.Add(self.post, 0, wx.ALL, 5)
self.conversation = wx.Button(self, wx.ID_ANY, _("Edit template for c&onversations. Current template: {}").format(conversation_template)) self.conversation = wx.Button(self, wx.ID_ANY, _("Edit template for conversations. Current template: {}").format(conversation_template))
sizer.Add(self.conversation, 0, wx.ALL, 5) sizer.Add(self.conversation, 0, wx.ALL, 5)
self.person = wx.Button(self, wx.ID_ANY, _("Edit template for p&ersons. Current template: {}").format(person_template)) self.person = wx.Button(self, wx.ID_ANY, _("Edit template for persons. Current template: {}").format(person_template))
sizer.Add(self.person, 0, wx.ALL, 5) sizer.Add(self.person, 0, wx.ALL, 5)
self.SetSizer(sizer) self.SetSizer(sizer)
@ -62,7 +62,7 @@ class sound(wx.Panel):
def __init__(self, parent, input_devices, output_devices, soundpacks): def __init__(self, parent, input_devices, output_devices, soundpacks):
wx.Panel.__init__(self, parent) wx.Panel.__init__(self, parent)
sizer = wx.BoxSizer(wx.VERTICAL) sizer = wx.BoxSizer(wx.VERTICAL)
volume = wx.StaticText(self, -1, _(u"&Volume")) volume = wx.StaticText(self, -1, _(u"Volume"))
self.volumeCtrl = wx.Slider(self) self.volumeCtrl = wx.Slider(self)
# Connect a key handler here to handle volume slider being inverted when moving with up and down arrows. # Connect a key handler here to handle volume slider being inverted when moving with up and down arrows.
# see https://github.com/manuelcortez/TWBlue/issues/261 # see https://github.com/manuelcortez/TWBlue/issues/261
@ -73,16 +73,16 @@ class sound(wx.Panel):
volumeBox.Add(volume, 0, wx.ALL, 5) volumeBox.Add(volume, 0, wx.ALL, 5)
volumeBox.Add(self.volumeCtrl, 0, wx.ALL, 5) volumeBox.Add(self.volumeCtrl, 0, wx.ALL, 5)
sizer.Add(volumeBox, 0, wx.ALL, 5) sizer.Add(volumeBox, 0, wx.ALL, 5)
self.session_mute = wx.CheckBox(self, -1, _(u"S&ession mute")) self.session_mute = wx.CheckBox(self, -1, _(u"Session mute"))
sizer.Add(self.session_mute, 0, wx.ALL, 5) sizer.Add(self.session_mute, 0, wx.ALL, 5)
output_label = wx.StaticText(self, -1, _(u"&Output device")) output_label = wx.StaticText(self, -1, _(u"Output device"))
self.output = wx.ComboBox(self, -1, choices=output_devices, style=wx.CB_READONLY) self.output = wx.ComboBox(self, -1, choices=output_devices, style=wx.CB_READONLY)
self.output.SetSize(self.output.GetBestSize()) self.output.SetSize(self.output.GetBestSize())
outputBox = wx.BoxSizer(wx.HORIZONTAL) outputBox = wx.BoxSizer(wx.HORIZONTAL)
outputBox.Add(output_label, 0, wx.ALL, 5) outputBox.Add(output_label, 0, wx.ALL, 5)
outputBox.Add(self.output, 0, wx.ALL, 5) outputBox.Add(self.output, 0, wx.ALL, 5)
sizer.Add(outputBox, 0, wx.ALL, 5) sizer.Add(outputBox, 0, wx.ALL, 5)
input_label = wx.StaticText(self, -1, _(u"&Input device")) input_label = wx.StaticText(self, -1, _(u"Input device"))
self.input = wx.ComboBox(self, -1, choices=input_devices, style=wx.CB_READONLY) self.input = wx.ComboBox(self, -1, choices=input_devices, style=wx.CB_READONLY)
self.input.SetSize(self.input.GetBestSize()) self.input.SetSize(self.input.GetBestSize())
inputBox = wx.BoxSizer(wx.HORIZONTAL) inputBox = wx.BoxSizer(wx.HORIZONTAL)
@ -90,15 +90,15 @@ class sound(wx.Panel):
inputBox.Add(self.input, 0, wx.ALL, 5) inputBox.Add(self.input, 0, wx.ALL, 5)
sizer.Add(inputBox, 0, wx.ALL, 5) sizer.Add(inputBox, 0, wx.ALL, 5)
soundBox = wx.BoxSizer(wx.VERTICAL) soundBox = wx.BoxSizer(wx.VERTICAL)
soundpack_label = wx.StaticText(self, -1, _(u"Sound &pack")) soundpack_label = wx.StaticText(self, -1, _(u"Sound pack"))
self.soundpack = wx.ComboBox(self, -1, choices=soundpacks, style=wx.CB_READONLY) self.soundpack = wx.ComboBox(self, -1, choices=soundpacks, style=wx.CB_READONLY)
self.soundpack.SetSize(self.soundpack.GetBestSize()) self.soundpack.SetSize(self.soundpack.GetBestSize())
soundBox.Add(soundpack_label, 0, wx.ALL, 5) soundBox.Add(soundpack_label, 0, wx.ALL, 5)
soundBox.Add(self.soundpack, 0, wx.ALL, 5) soundBox.Add(self.soundpack, 0, wx.ALL, 5)
sizer.Add(soundBox, 0, wx.ALL, 5) sizer.Add(soundBox, 0, wx.ALL, 5)
self.indicate_audio = wx.CheckBox(self, -1, _("Indicate &audio or video in posts with sound")) self.indicate_audio = wx.CheckBox(self, -1, _("Indicate audio or video in posts with sound"))
sizer.Add(self.indicate_audio, 0, wx.ALL, 5) sizer.Add(self.indicate_audio, 0, wx.ALL, 5)
self.indicate_img = wx.CheckBox(self, -1, _("Indicate posts containing i&mages with sound")) self.indicate_img = wx.CheckBox(self, -1, _("Indicate posts containing images with sound"))
sizer.Add(self.indicate_img, 0, wx.ALL, 5) sizer.Add(self.indicate_img, 0, wx.ALL, 5)
self.SetSizer(sizer) self.SetSizer(sizer)
@ -121,7 +121,7 @@ class extrasPanel(wx.Panel):
def __init__(self, parent, ocr_languages=[], translation_languages=[]): def __init__(self, parent, ocr_languages=[], translation_languages=[]):
super(extrasPanel, self).__init__(parent) super(extrasPanel, self).__init__(parent)
mainSizer = wx.BoxSizer(wx.VERTICAL) mainSizer = wx.BoxSizer(wx.VERTICAL)
OCRBox = wx.StaticBox(self, label=_(u"&Language for OCR")) OCRBox = wx.StaticBox(self, label=_(u"Language for OCR"))
self.ocr_lang = wx.ListBox(self, -1, choices=ocr_languages) self.ocr_lang = wx.ListBox(self, -1, choices=ocr_languages)
self.ocr_lang.SetSize(self.ocr_lang.GetBestSize()) self.ocr_lang.SetSize(self.ocr_lang.GetBestSize())
ocrLanguageSizer = wx.StaticBoxSizer(OCRBox, wx.HORIZONTAL) ocrLanguageSizer = wx.StaticBoxSizer(OCRBox, wx.HORIZONTAL)
@ -167,9 +167,9 @@ class configurationDialog(baseDialog.BaseWXDialog):
def realize(self): def realize(self):
self.sizer.Add(self.notebook, 0, wx.ALL, 5) self.sizer.Add(self.notebook, 0, wx.ALL, 5)
ok_cancel_box = wx.BoxSizer(wx.HORIZONTAL) ok_cancel_box = wx.BoxSizer(wx.HORIZONTAL)
ok = wx.Button(self.panel, wx.ID_OK, _(u"&Save")) ok = wx.Button(self.panel, wx.ID_OK, _(u"Save"))
ok.SetDefault() ok.SetDefault()
cancel = wx.Button(self.panel, wx.ID_CANCEL, _(u"&Close")) cancel = wx.Button(self.panel, wx.ID_CANCEL, _(u"Close"))
self.SetEscapeId(cancel.GetId()) self.SetEscapeId(cancel.GetId())
ok_cancel_box.Add(ok, 0, wx.ALL, 5) ok_cancel_box.Add(ok, 0, wx.ALL, 5)
ok_cancel_box.Add(cancel, 0, wx.ALL, 5) ok_cancel_box.Add(cancel, 0, wx.ALL, 5)

View File

@ -0,0 +1,7 @@
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

View File

@ -1,146 +0,0 @@
# -*- coding: utf-8 -*-
import wx
class FilterKeywordPanel(wx.Panel):
"""panel to handle filter's keywords. """
def __init__(self, parent):
super(FilterKeywordPanel, self).__init__(parent)
self.keywords = []
# Add widgets
list_panel = wx.Panel(self)
self.keyword_list = wx.ListCtrl(list_panel, wx.ID_ANY, style=wx.LC_REPORT | wx.LC_SINGLE_SEL)
self.keyword_list.InsertColumn(0, _("Keyword"))
self.keyword_list.InsertColumn(1, _("Whole word"))
self.keyword_list.SetColumnWidth(0, wx.LIST_AUTOSIZE_USEHEADER)
self.keyword_list.SetColumnWidth(1, wx.LIST_AUTOSIZE_USEHEADER)
list_sizer = wx.BoxSizer(wx.VERTICAL)
list_sizer.Add(self.keyword_list, 1, wx.EXPAND)
list_panel.SetSizer(list_sizer)
keyword_label = wx.StaticText(self, wx.ID_ANY, label=_("Keyword:"))
self.keyword_text = wx.TextCtrl(self, wx.ID_ANY)
keyword_sizer = wx.BoxSizer(wx.VERTICAL)
keyword_sizer.Add(keyword_label, 0, wx.RIGHT, 5)
keyword_sizer.Add(self.keyword_text, 0, wx.EXPAND)
self.whole_word_checkbox = wx.CheckBox(self, wx.ID_ANY, label=_("Whole word"))
self.add_button = wx.Button(self, wx.ID_ANY, label=_("Add"))
self.remove_button = wx.Button(self, wx.ID_ANY, label=_("Remove"))
input_sizer = wx.BoxSizer(wx.HORIZONTAL)
input_sizer.Add(keyword_sizer, 1, wx.RIGHT, 5)
input_sizer.Add(self.whole_word_checkbox, 0)
button_sizer = wx.BoxSizer(wx.HORIZONTAL)
button_sizer.Add(self.add_button, 0, wx.RIGHT, 5)
button_sizer.Add(self.remove_button, 0)
main_sizer = wx.BoxSizer(wx.VERTICAL)
main_sizer.Add(wx.StaticText(self, label=_("Palabras clave a filtrar:")), 0, wx.BOTTOM, 5)
main_sizer.Add(list_panel, 1, wx.EXPAND | wx.BOTTOM, 5)
main_sizer.Add(input_sizer, 0, wx.EXPAND | wx.BOTTOM, 5)
main_sizer.Add(button_sizer, 0, wx.ALIGN_RIGHT)
self.SetSizer(main_sizer)
def add_keyword(self, keyword, whole_word=False):
""" Adds a keyword to the list. """
index = self.keyword_list.InsertItem(self.keyword_list.GetItemCount(), keyword)
self.keyword_list.SetItem(index, 1, _("Yes") if whole_word else _("No"))
self.keyword_list.SetColumnWidth(0, wx.LIST_AUTOSIZE)
self.keyword_list.SetColumnWidth(1, wx.LIST_AUTOSIZE_USEHEADER)
self.keyword_text.Clear()
self.whole_word_checkbox.SetValue(False)
def remove_keyword(self):
""" Remove a keyword from the list. """
selection = self.keyword_list.GetFirstSelected()
if selection != -1:
self.keyword_list.DeleteItem(selection)
return selection
def set_keywords(self, keywords):
""" Set the list of keyword. """
self.keyword_list.DeleteAllItems()
for keyword_data in keywords:
if isinstance(keyword_data, dict):
kw = keyword_data.get('keyword', '')
whole_word = keyword_data.get('whole_word', False)
self.keywords.append({'keyword': kw, 'whole_word': whole_word})
index = self.keyword_list.InsertItem(self.keyword_list.GetItemCount(), kw)
self.keyword_list.SetItem(index, 1, _("Yes") if whole_word else _("No"))
else:
self.keywords.append({'keyword': keyword_data, 'whole_word': False})
index = self.keyword_list.InsertItem(self.keyword_list.GetItemCount(), keyword_data)
self.keyword_list.SetItem(index, 1, _("No"))
self.keyword_list.SetColumnWidth(0, wx.LIST_AUTOSIZE)
self.keyword_list.SetColumnWidth(1, wx.LIST_AUTOSIZE_USEHEADER)
class CreateFilterDialog(wx.Dialog):
def __init__(self, parent, title=_("New filter")):
super(CreateFilterDialog, self).__init__(parent, title=title, style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
self.contexts = ["home", "public", "notifications", "thread", "account"]
self.context_labels = {
"home": _("Home timeline"),
"public": _("Public statuses"),
"notifications": _("Notifications"),
"thread": _("Threads"),
"account": _("Profiles")
}
self.actions = ["hide", "warn"]
self.action_labels = {
"hide": _("Hide posts"),
"warn": _("Set a content warning to posts")
}
self.expiration_options = [
("never", _("Never")),
("hours", _("Hours")),
("days", _("Days")),
("weeks", _("Weeks")),
("months", _("months"))
]
main_sizer = wx.BoxSizer(wx.VERTICAL)
name_label = wx.StaticText(self, wx.ID_ANY, label=_("Title:"))
self.name_ctrl = wx.TextCtrl(self, wx.ID_ANY)
name_sizer = wx.BoxSizer(wx.HORIZONTAL)
name_sizer.Add(name_label, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5)
name_sizer.Add(self.name_ctrl, 1, wx.EXPAND)
main_sizer.Add(name_sizer, 0, wx.EXPAND | wx.ALL, 10)
static_box = wx.StaticBox(self, wx.ID_ANY, label=_("Apply to:"))
context_sizer = wx.StaticBoxSizer(static_box, wx.VERTICAL)
self.context_checkboxes = {}
context_grid = wx.FlexGridSizer(rows=3, cols=2, vgap=5, hgap=10)
for context in self.contexts:
checkbox = wx.CheckBox(static_box, wx.ID_ANY, label=self.context_labels[context])
self.context_checkboxes[context] = checkbox
context_grid.Add(checkbox)
context_sizer.Add(context_grid, 0, wx.ALL, 10)
main_sizer.Add(context_sizer, 0, wx.EXPAND | wx.ALL, 10)
action_label = wx.StaticText(self, wx.ID_ANY, label=_("Action:"))
self.action_choice = wx.Choice(self, wx.ID_ANY)
for action in self.actions:
self.action_choice.Append(self.action_labels[action])
action_sizer = wx.BoxSizer(wx.HORIZONTAL)
action_sizer.Add(action_label, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5)
action_sizer.Add(self.action_choice, 1)
main_sizer.Add(action_sizer, 0, wx.EXPAND | wx.ALL, 10)
expiration_label = wx.StaticText(self, wx.ID_ANY, label=_("Expires in:"))
self.expiration_choice = wx.Choice(self, wx.ID_ANY)
for e, label in self.expiration_options:
self.expiration_choice.Append(label)
self.expiration_value = wx.SpinCtrl(self, wx.ID_ANY, min=1, max=9999, initial=1)
self.expiration_value.Enable(False)
self.expiration_choice.Bind(wx.EVT_CHOICE, self.on_expiration_changed)
expiration_sizer = wx.BoxSizer(wx.HORIZONTAL)
expiration_sizer.Add(expiration_label, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5)
expiration_sizer.Add(self.expiration_choice, 1, wx.RIGHT, 5)
expiration_sizer.Add(self.expiration_value, 0)
main_sizer.Add(expiration_sizer, 0, wx.EXPAND | wx.ALL, 10)
self.keyword_panel = FilterKeywordPanel(self)
main_sizer.Add(self.keyword_panel, 1, wx.EXPAND | wx.ALL, 10)
button_sizer = self.CreateButtonSizer(wx.OK | wx.CANCEL)
main_sizer.Add(button_sizer, 0, wx.EXPAND | wx.ALL, 10)
self.SetSizer(main_sizer)
self.SetSize((450, 550))
self.action_choice.SetSelection(0)
self.expiration_choice.SetSelection(0)
wx.CallAfter(self.name_ctrl.SetFocus)
def on_expiration_changed(self, event):
selection = self.expiration_choice.GetSelection()
self.expiration_value.Enable(selection != 0)

View File

@ -1,35 +0,0 @@
# -*- coding: utf-8 -*-
import wx
class ManageFiltersDialog(wx.Dialog):
"""
A dialog that displays a list of Mastodon filters and provides controls
to add, edit and remove them.
"""
def __init__(self, parent, title=_("Filters"), *args, **kwargs):
"""Initialize the filters view dialog. """
super(ManageFiltersDialog, self).__init__(parent, title=title, *args, **kwargs)
main_sizer = wx.BoxSizer(wx.VERTICAL)
self.filter_list = wx.ListCtrl(self, style=wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.BORDER_SUNKEN)
self.filter_list.InsertColumn(0, _("Title"), width=150)
self.filter_list.InsertColumn(1, _("Keywords"), width=80)
self.filter_list.InsertColumn(2, _("Contexts"), width=150)
self.filter_list.InsertColumn(3, _("Action"), width=100)
self.filter_list.InsertColumn(4, _("Expires"), width=150)
main_sizer.Add(self.filter_list, 1, wx.EXPAND | wx.ALL, 10)
button_sizer = wx.BoxSizer(wx.HORIZONTAL)
self.add_button = wx.Button(self, label=_("Add"))
self.edit_button = wx.Button(self, label=_("Edit"))
self.remove_button = wx.Button(self, label=_("Remove"))
close_button = wx.Button(self, wx.ID_CLOSE)
self.edit_button.Disable()
self.remove_button.Disable()
button_sizer.Add(self.add_button, 0, wx.RIGHT, 5)
button_sizer.Add(self.edit_button, 0, wx.RIGHT, 5)
button_sizer.Add(self.remove_button, 0, wx.RIGHT, 5)
button_sizer.Add((0, 0), 1, wx.EXPAND) # Spacer to push close button to right
button_sizer.Add(close_button, 0)
self.SetEscapeId(close_button.GetId())
main_sizer.Add(button_sizer, 0, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 10)
self.SetSizer(main_sizer)

View File

@ -1,8 +1,7 @@
# -*- coding: utf-8 -*-
import wx import wx
class Post(wx.Dialog): class Post(wx.Dialog):
def __init__(self, caption=_("Post"), text="", languages=[], *args, **kwds): def __init__(self, caption=_("Post"), text="", *args, **kwds):
super(Post, self).__init__(parent=None, id=wx.ID_ANY, *args, **kwds) super(Post, self).__init__(parent=None, id=wx.ID_ANY, *args, **kwds)
main_sizer = wx.BoxSizer(wx.VERTICAL) main_sizer = wx.BoxSizer(wx.VERTICAL)
post_sizer = wx.WrapSizer(wx.VERTICAL) post_sizer = wx.WrapSizer(wx.VERTICAL)
@ -49,12 +48,6 @@ class Post(wx.Dialog):
self.visibility = wx.ComboBox(self, wx.ID_ANY, choices=[_("Public"), _("Not listed"), _("Followers only"), _("Direct")], style=wx.CB_DROPDOWN | wx.CB_READONLY | wx.CB_SIMPLE) self.visibility = wx.ComboBox(self, wx.ID_ANY, choices=[_("Public"), _("Not listed"), _("Followers only"), _("Direct")], style=wx.CB_DROPDOWN | wx.CB_READONLY | wx.CB_SIMPLE)
self.visibility.SetSelection(0) self.visibility.SetSelection(0)
visibility_sizer.Add(self.visibility, 0, 0, 0) visibility_sizer.Add(self.visibility, 0, 0, 0)
language_sizer = wx.BoxSizer(wx.HORIZONTAL)
post_actions_sizer.Add(language_sizer, 0, wx.RIGHT, 20)
lang_label = wx.StaticText(self, wx.ID_ANY, _("Language"))
language_sizer.Add(lang_label, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5)
self.language = wx.ComboBox(self, wx.ID_ANY, choices=languages, style=wx.CB_DROPDOWN | wx.CB_READONLY)
language_sizer.Add(self.language, 0, wx.ALIGN_CENTER_VERTICAL, 0)
self.add = wx.Button(self, wx.ID_ANY, _("A&dd")) self.add = wx.Button(self, wx.ID_ANY, _("A&dd"))
self.sensitive = wx.CheckBox(self, wx.ID_ANY, _("S&ensitive content")) self.sensitive = wx.CheckBox(self, wx.ID_ANY, _("S&ensitive content"))
self.sensitive.SetValue(False) self.sensitive.SetValue(False)
@ -171,83 +164,80 @@ class Post(wx.Dialog):
def unable_to_attach_poll(self, *args, **kwargs): 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() 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()
class viewPost(wx.Dialog): class viewPost(wx.Dialog):
def set_title(self, length): def set_title(self, lenght):
self.SetTitle(_("Post - %i characters ") % length) self.SetTitle(_("Post - %i characters ") % (lenght,))
def __init__(self, text="", boosts_count=0, favs_count=0, source="", date="", privacy="", *args, **kwargs): 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) panel = wx.Panel(self)
main_sizer = wx.BoxSizer(wx.VERTICAL) label = wx.StaticText(panel, -1, _("Post"))
main_sizer.Add(self.create_text_section(panel, text), 1, wx.EXPAND | wx.ALL, 5) self.text = wx.TextCtrl(panel, -1, text, style=wx.TE_READONLY|wx.TE_MULTILINE, size=(250, 180))
main_sizer.Add(self.create_image_description_section(panel), 1, wx.EXPAND | wx.ALL, 5) self.text.SetFocus()
main_sizer.Add(self.create_info_section(panel, privacy, boosts_count, favs_count, source, date), 0, wx.EXPAND | wx.ALL, 5) textBox = wx.BoxSizer(wx.HORIZONTAL)
main_sizer.Add(self.create_buttons_section(panel), 0, wx.ALIGN_RIGHT | wx.ALL, 5) textBox.Add(label, 0, wx.ALL, 5)
panel.SetSizer(main_sizer) textBox.Add(self.text, 1, wx.EXPAND, 5)
self.SetClientSize(main_sizer.CalcMin()) mainBox = wx.BoxSizer(wx.VERTICAL)
mainBox.Add(textBox, 0, wx.ALL, 5)
def create_text_section(self, panel, text): label2 = wx.StaticText(panel, -1, _("Image description"))
sizer = wx.StaticBoxSizer(wx.StaticBox(panel, wx.ID_ANY, _("Post")), wx.VERTICAL) self.image_description = wx.TextCtrl(panel, -1, style=wx.TE_READONLY|wx.TE_MULTILINE, size=(250, 180))
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) self.image_description.Enable(False)
sizer.Add(self.image_description, 1, wx.EXPAND | wx.ALL, 5) iBox = wx.BoxSizer(wx.HORIZONTAL)
return sizer iBox.Add(label2, 0, wx.ALL, 5)
iBox.Add(self.image_description, 1, wx.EXPAND, 5)
def create_info_section(self, panel, privacy, boosts_count, favs_count, source, date): mainBox.Add(iBox, 0, wx.ALL, 5)
sizer = wx.StaticBoxSizer(wx.StaticBox(panel, wx.ID_ANY, _("Information")), wx.VERTICAL) privacyLabel = wx.StaticText(panel, -1, _("Privacy"))
flex_sizer = wx.FlexGridSizer(cols=3, hgap=10, vgap=10) privacy = wx.TextCtrl(panel, -1, privacy, size=wx.DefaultSize, style=wx.TE_READONLY|wx.TE_MULTILINE)
flex_sizer.AddGrowableCol(1) privacyBox = wx.BoxSizer(wx.HORIZONTAL)
flex_sizer.Add(wx.StaticText(panel, -1, _("Privacy")), 0, wx.ALIGN_CENTER_VERTICAL) privacyBox.Add(privacyLabel, 0, wx.ALL, 5)
flex_sizer.Add(wx.TextCtrl(panel, -1, privacy, style=wx.TE_READONLY | wx.TE_MULTILINE), 1, wx.EXPAND) privacyBox.Add(privacy, 0, wx.ALL, 5)
flex_sizer.Add(self.create_boosts_section(panel, boosts_count), 1, wx.EXPAND | wx.ALL, 5) boostsCountLabel = wx.StaticText(panel, -1, _(u"Boosts: "))
flex_sizer.Add(self.create_favorites_section(panel, favs_count), 1, wx.EXPAND | wx.ALL, 5) boostsCount = wx.TextCtrl(panel, -1, str(boosts_count), size=wx.DefaultSize, style=wx.TE_READONLY|wx.TE_MULTILINE)
flex_sizer.Add(wx.StaticText(panel, -1, _("Source")), 0, wx.ALIGN_CENTER_VERTICAL) boostBox = wx.BoxSizer(wx.HORIZONTAL)
flex_sizer.Add(wx.TextCtrl(panel, -1, source, style=wx.TE_READONLY | wx.TE_MULTILINE), 1, wx.EXPAND) boostBox.Add(boostsCountLabel, 0, wx.ALL, 5)
flex_sizer.Add(wx.StaticText(panel, -1, _("Date")), 0, wx.ALIGN_CENTER_VERTICAL) boostBox.Add(boostsCount, 0, wx.ALL, 5)
flex_sizer.Add(wx.TextCtrl(panel, -1, date, style=wx.TE_READONLY | wx.TE_MULTILINE), 1, wx.EXPAND) favsCountLabel = wx.StaticText(panel, -1, _("Favorites: "))
sizer.Add(flex_sizer, 1, wx.EXPAND | wx.ALL, 5) favsCount = wx.TextCtrl(panel, -1, str(favs_count), size=wx.DefaultSize, style=wx.TE_READONLY|wx.TE_MULTILINE)
return sizer favsBox = wx.BoxSizer(wx.HORIZONTAL)
favsBox.Add(favsCountLabel, 0, wx.ALL, 5)
def create_boosts_section(self, panel, boosts_count): favsBox.Add(favsCount, 0, wx.ALL, 5)
sizer = wx.StaticBoxSizer(wx.StaticBox(panel, wx.ID_ANY, _("Boosts")), wx.VERTICAL) sourceLabel = wx.StaticText(panel, -1, _("Source: "))
self.boosts_button = wx.Button(panel, -1, str(boosts_count)) source = wx.TextCtrl(panel, -1, source, size=wx.DefaultSize, style=wx.TE_READONLY|wx.TE_MULTILINE)
self.boosts_button.SetToolTip(_("View users who boosted this post")) sourceBox = wx.BoxSizer(wx.HORIZONTAL)
sizer.Add(self.boosts_button, 1, wx.EXPAND | wx.ALL, 5) sourceBox.Add(sourceLabel, 0, wx.ALL, 5)
return sizer sourceBox.Add(source, 0, wx.ALL, 5)
dateLabel = wx.StaticText(panel, -1, _(u"Date: "))
def create_favorites_section(self, panel, favs_count): date = wx.TextCtrl(panel, -1, date, size=wx.DefaultSize, style=wx.TE_READONLY|wx.TE_MULTILINE)
sizer = wx.StaticBoxSizer(wx.StaticBox(panel, wx.ID_ANY, _("Favorites")), wx.VERTICAL) dateBox = wx.BoxSizer(wx.HORIZONTAL)
self.favorites_button = wx.Button(panel, -1, str(favs_count)) dateBox.Add(dateLabel, 0, wx.ALL, 5)
self.favorites_button.SetToolTip(_("View users who favorited this post")) dateBox.Add(date, 0, wx.ALL, 5)
sizer.Add(self.favorites_button, 1, wx.EXPAND | wx.ALL, 5) infoBox = wx.BoxSizer(wx.HORIZONTAL)
return sizer infoBox.Add(privacyBox, 0, wx.ALL, 5)
infoBox.Add(boostBox, 0, wx.ALL, 5)
def create_buttons_section(self, panel): infoBox.Add(favsBox, 0, wx.ALL, 5)
sizer = wx.BoxSizer(wx.HORIZONTAL) infoBox.Add(sourceBox, 0, wx.ALL, 5)
self.mute = wx.Button(panel, wx.ID_ANY, _("Mute conversation")) mainBox.Add(infoBox, 0, wx.ALL, 5)
self.mute.Enable(False) mainBox.Add(dateBox, 0, wx.ALL, 5)
self.share = wx.Button(panel, wx.ID_ANY, _("Copy link to clipboard")) self.share = wx.Button(panel, wx.ID_ANY, _("Copy link to clipboard"))
self.share.Enable(False) self.share.Enable(False)
self.spellcheck = wx.Button(panel, wx.ID_ANY, _("Check &spelling...")) self.spellcheck = wx.Button(panel, -1, _("Check &spelling..."), size=wx.DefaultSize)
self.translateButton = wx.Button(panel, wx.ID_ANY, _("&Translate...")) self.translateButton = wx.Button(panel, -1, _(u"&Translate..."), size=wx.DefaultSize)
cancelButton = wx.Button(panel, wx.ID_CANCEL, _("C&lose")) cancelButton = wx.Button(panel, wx.ID_CANCEL, _(u"C&lose"), size=wx.DefaultSize)
cancelButton.SetDefault() cancelButton.SetDefault()
sizer.Add(self.mute, 0, wx.ALL, 5) buttonsBox = wx.BoxSizer(wx.HORIZONTAL)
sizer.Add(self.share, 0, wx.ALL, 5) buttonsBox.Add(self.share, 0, wx.ALL, 5)
sizer.Add(self.spellcheck, 0, wx.ALL, 5) buttonsBox.Add(self.spellcheck, 0, wx.ALL, 5)
sizer.Add(self.translateButton, 0, wx.ALL, 5) buttonsBox.Add(self.translateButton, 0, wx.ALL, 5)
sizer.Add(cancelButton, 0, wx.ALL, 5) buttonsBox.Add(cancelButton, 0, wx.ALL, 5)
return sizer 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())
def set_text(self, text): def set_text(self, text):
self.text.ChangeValue(text) self.text.ChangeValue(text)

View File

@ -76,27 +76,27 @@ class ShowUserProfile(wx.Dialog):
mainSizer = wx.GridSizer(2, 5, 5) mainSizer = wx.GridSizer(2, 5, 5)
# create widgets # create widgets
nameLabel = wx.StaticText(self.panel, label=_("&Name: ")) nameLabel = wx.StaticText(self.panel, label=_("Name: "))
name = self.createTextCtrl(user.display_name, size=(200, 30)) name = self.createTextCtrl(user.display_name, size=(200, 30))
mainSizer.Add(nameLabel, wx.SizerFlags().Center()) mainSizer.Add(nameLabel, wx.SizerFlags().Center())
mainSizer.Add(name, wx.SizerFlags().Center()) mainSizer.Add(name, wx.SizerFlags().Center())
urlLabel = wx.StaticText(self.panel, label=_("&URL: ")) urlLabel = wx.StaticText(self.panel, label=_("URL: "))
url = self.createTextCtrl(user.url, size=(200, 30)) url = self.createTextCtrl(user.url, size=(200, 30))
mainSizer.Add(urlLabel, wx.SizerFlags().Center()) mainSizer.Add(urlLabel, wx.SizerFlags().Center())
mainSizer.Add(url, wx.SizerFlags().Center()) mainSizer.Add(url, wx.SizerFlags().Center())
bioLabel = wx.StaticText(self.panel, label=_("&Bio: ")) bioLabel = wx.StaticText(self.panel, label=_("Bio: "))
bio = self.createTextCtrl(html_filter(user.note), (400, 60)) bio = self.createTextCtrl(html_filter(user.note), (400, 60))
mainSizer.Add(bioLabel, wx.SizerFlags().Center()) mainSizer.Add(bioLabel, wx.SizerFlags().Center())
mainSizer.Add(bio, wx.SizerFlags().Center()) mainSizer.Add(bio, wx.SizerFlags().Center())
joinLabel = wx.StaticText(self.panel, label=_("&Joined at: ")) joinLabel = wx.StaticText(self.panel, label=_("Joined at: "))
joinText = self.createTextCtrl(user.created_at.strftime('%d %B, %Y'), (80, 30)) joinText = self.createTextCtrl(user.created_at.strftime('%d %B, %Y'), (80, 30))
mainSizer.Add(joinLabel, wx.SizerFlags().Center()) mainSizer.Add(joinLabel, wx.SizerFlags().Center())
mainSizer.Add(joinText, wx.SizerFlags().Center()) mainSizer.Add(joinText, wx.SizerFlags().Center())
actions = wx.Button(self.panel, label=_("&Actions")) actions = wx.Button(self.panel, label=_("Actions"))
actions.Bind(wx.EVT_BUTTON, self.onAction) actions.Bind(wx.EVT_BUTTON, self.onAction)
mainSizer.Add(actions, wx.SizerFlags().Center()) mainSizer.Add(actions, wx.SizerFlags().Center())
@ -119,7 +119,7 @@ class ShowUserProfile(wx.Dialog):
self.fields = [] self.fields = []
for num, field in enumerate(user.fields): for num, field in enumerate(user.fields):
labelSizer = wx.BoxSizer(wx.HORIZONTAL) labelSizer = wx.BoxSizer(wx.HORIZONTAL)
labelLabel = wx.StaticText(self.panel, label=_("Field &{} - Label: ").format(num + 1)) labelLabel = wx.StaticText(self.panel, label=_("Field {} - Label: ").format(num + 1))
labelSizer.Add(labelLabel, wx.SizerFlags().Center().Border(wx.ALL, 5)) labelSizer.Add(labelLabel, wx.SizerFlags().Center().Border(wx.ALL, 5))
labelText = self.createTextCtrl(html_filter(field.name), (230, 30), True) labelText = self.createTextCtrl(html_filter(field.name), (230, 30), True)
labelSizer.Add(labelText, wx.SizerFlags().Expand().Border(wx.ALL, 5)) labelSizer.Add(labelText, wx.SizerFlags().Expand().Border(wx.ALL, 5))
@ -134,40 +134,40 @@ class ShowUserProfile(wx.Dialog):
bullSwitch = {True: _('Yes'), False: _('No'), None: _('No')} bullSwitch = {True: _('Yes'), False: _('No'), None: _('No')}
privateSizer = wx.BoxSizer(wx.HORIZONTAL) privateSizer = wx.BoxSizer(wx.HORIZONTAL)
privateLabel = wx.StaticText(self.panel, label=_("&Private account: ")) privateLabel = wx.StaticText(self.panel, label=_("Private account: "))
private = self.createTextCtrl(bullSwitch[user.locked], (30, 30)) private = self.createTextCtrl(bullSwitch[user.locked], (30, 30))
privateSizer.Add(privateLabel, wx.SizerFlags().Center()) privateSizer.Add(privateLabel, wx.SizerFlags().Center())
privateSizer.Add(private, wx.SizerFlags().Center()) privateSizer.Add(private, wx.SizerFlags().Center())
mainSizer.Add(privateSizer, 0, wx.ALL | wx.CENTER) mainSizer.Add(privateSizer, 0, wx.ALL | wx.CENTER)
botSizer = wx.BoxSizer(wx.HORIZONTAL) botSizer = wx.BoxSizer(wx.HORIZONTAL)
botLabel = wx.StaticText(self.panel, label=_("&Bot account: ")) botLabel = wx.StaticText(self.panel, label=_("Bot account: "))
botText = self.createTextCtrl(bullSwitch[user.bot], (30, 30)) botText = self.createTextCtrl(bullSwitch[user.bot], (30, 30))
botSizer.Add(botLabel, wx.SizerFlags().Center()) botSizer.Add(botLabel, wx.SizerFlags().Center())
botSizer.Add(botText, wx.SizerFlags().Center()) botSizer.Add(botText, wx.SizerFlags().Center())
mainSizer.Add(botSizer, 0, wx.ALL | wx.CENTER) mainSizer.Add(botSizer, 0, wx.ALL | wx.CENTER)
discoverSizer = wx.BoxSizer(wx.HORIZONTAL) discoverSizer = wx.BoxSizer(wx.HORIZONTAL)
discoverLabel = wx.StaticText(self.panel, label=_("&Discoverable account: ")) discoverLabel = wx.StaticText(self.panel, label=_("Discoverable account: "))
discoverText = self.createTextCtrl(bullSwitch[user.discoverable], (30, 30)) discoverText = self.createTextCtrl(bullSwitch[user.discoverable], (30, 30))
discoverSizer.Add(discoverLabel, wx.SizerFlags().Center()) discoverSizer.Add(discoverLabel, wx.SizerFlags().Center())
discoverSizer.Add(discoverText, wx.SizerFlags().Center()) discoverSizer.Add(discoverText, wx.SizerFlags().Center())
mainSizer.Add(discoverSizer, 0, wx.ALL | wx.CENTER) mainSizer.Add(discoverSizer, 0, wx.ALL | wx.CENTER)
posts = wx.Button(self.panel, label=_("{} p&osts. Click to open posts timeline").format(user.statuses_count)) posts = wx.Button(self.panel, label=_("{} posts. Click to open posts timeline").format(user.statuses_count))
# posts.SetToolTip(_("Click to open {}'s posts").format(user.display_name)) # posts.SetToolTip(_("Click to open {}'s posts").format(user.display_name))
posts.Bind(wx.EVT_BUTTON, self.onPost) posts.Bind(wx.EVT_BUTTON, self.onPost)
mainSizer.Add(posts, wx.SizerFlags().Center()) mainSizer.Add(posts, wx.SizerFlags().Center())
following = wx.Button(self.panel, label=_("{} &following. Click to open Following timeline").format(user.following_count)) following = wx.Button(self.panel, label=_("{} following. Click to open Following timeline").format(user.following_count))
mainSizer.Add(following, wx.SizerFlags().Center()) mainSizer.Add(following, wx.SizerFlags().Center())
following.Bind(wx.EVT_BUTTON, self.onFollowing) following.Bind(wx.EVT_BUTTON, self.onFollowing)
followers = wx.Button(self.panel, label=_("{} fo&llowers. Click to open followers timeline").format(user.followers_count)) followers = wx.Button(self.panel, label=_("{} followers. Click to open followers timeline").format(user.followers_count))
mainSizer.Add(followers, wx.SizerFlags().Center()) mainSizer.Add(followers, wx.SizerFlags().Center())
followers.Bind(wx.EVT_BUTTON, self.onFollowers) followers.Bind(wx.EVT_BUTTON, self.onFollowers)
close = wx.Button(self.panel, wx.ID_CLOSE, _("&Close")) close = wx.Button(self.panel, wx.ID_CLOSE, _("Close"))
self.SetEscapeId(close.GetId()) self.SetEscapeId(close.GetId())
close.SetDefault() close.SetDefault()
wrapperSizer.Add(mainSizer, 0, wx.CENTER) wrapperSizer.Add(mainSizer, 0, wx.CENTER)

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
import os import os
import requests import requests
from io import BytesIO from io import BytesIO
@ -40,14 +39,14 @@ class UpdateProfileDialog(wx.Dialog):
sizer = wx.BoxSizer(wx.VERTICAL) sizer = wx.BoxSizer(wx.VERTICAL)
# create widgets # create widgets
display_name_label = wx.StaticText(panel, label=_("&Display Name")) display_name_label = wx.StaticText(panel, label=_("Display Name"))
self.display_name = wx.TextCtrl(panel, value=display_name, style= wx.TE_PROCESS_ENTER, size=(200, 30)) self.display_name = wx.TextCtrl(panel, value=display_name, style= wx.TE_PROCESS_ENTER, size=(200, 30))
name_sizer = wx.BoxSizer(wx.HORIZONTAL) name_sizer = wx.BoxSizer(wx.HORIZONTAL)
name_sizer.Add(display_name_label, wx.SizerFlags().Center()) name_sizer.Add(display_name_label, wx.SizerFlags().Center())
name_sizer.Add(self.display_name, wx.SizerFlags().Center()) name_sizer.Add(self.display_name, wx.SizerFlags().Center())
sizer.Add(name_sizer, wx.CENTER) sizer.Add(name_sizer, wx.CENTER)
bio_label = wx.StaticText(panel, label=_("&Bio")) bio_label = wx.StaticText(panel, label=_("Bio"))
self.bio = wx.TextCtrl(panel, value=note, style=wx.TE_PROCESS_ENTER | wx.TE_MULTILINE, size=(400, 60)) self.bio = wx.TextCtrl(panel, value=note, style=wx.TE_PROCESS_ENTER | wx.TE_MULTILINE, size=(400, 60))
bio_sizer = wx.BoxSizer(wx.HORIZONTAL) bio_sizer = wx.BoxSizer(wx.HORIZONTAL)
bio_sizer.Add(bio_label, wx.SizerFlags().Center()) bio_sizer.Add(bio_label, wx.SizerFlags().Center())
@ -68,7 +67,7 @@ class UpdateProfileDialog(wx.Dialog):
self.header_image = wx.StaticBitmap(panel, bitmap=image.ConvertToBitmap()) self.header_image = wx.StaticBitmap(panel, bitmap=image.ConvertToBitmap())
self.header_image.AcceptsFocusFromKeyboard = return_true self.header_image.AcceptsFocusFromKeyboard = return_true
self.change_header = wx.Button(panel, label=_("Change &header")) self.change_header = wx.Button(panel, label=_("Change header"))
header_sizer = wx.BoxSizer(wx.HORIZONTAL) header_sizer = wx.BoxSizer(wx.HORIZONTAL)
header_sizer.Add(header_label, wx.SizerFlags().Center()) header_sizer.Add(header_label, wx.SizerFlags().Center())
header_sizer.Add(self.header_image, wx.SizerFlags().Center()) header_sizer.Add(self.header_image, wx.SizerFlags().Center())
@ -89,7 +88,7 @@ class UpdateProfileDialog(wx.Dialog):
self.avatar_image = wx.StaticBitmap(panel, bitmap=image.ConvertToBitmap()) self.avatar_image = wx.StaticBitmap(panel, bitmap=image.ConvertToBitmap())
self.avatar_image.AcceptsFocusFromKeyboard = return_true self.avatar_image.AcceptsFocusFromKeyboard = return_true
self.change_avatar = wx.Button(panel, label=_("Change &avatar")) self.change_avatar = wx.Button(panel, label=_("Change avatar"))
avatar_sizer = wx.BoxSizer(wx.HORIZONTAL) avatar_sizer = wx.BoxSizer(wx.HORIZONTAL)
avatar_sizer.Add(avatar_label, wx.SizerFlags().Center()) avatar_sizer.Add(avatar_label, wx.SizerFlags().Center())
avatar_sizer.Add(self.avatar_image, wx.SizerFlags().Center()) avatar_sizer.Add(self.avatar_image, wx.SizerFlags().Center())
@ -99,7 +98,7 @@ class UpdateProfileDialog(wx.Dialog):
self.fields = [] self.fields = []
for i in range(1, 5): for i in range(1, 5):
field_sizer = wx.BoxSizer(wx.HORIZONTAL) field_sizer = wx.BoxSizer(wx.HORIZONTAL)
field_label = wx.StaticText(panel, label=_("Field &{}: Label").format(i)) field_label = wx.StaticText(panel, label=_("Field {}: Label").format(i))
field_sizer.Add(field_label, wx.SizerFlags().Center().Border(wx.ALL, 5)) field_sizer.Add(field_label, wx.SizerFlags().Center().Border(wx.ALL, 5))
label_textctrl = wx.TextCtrl(panel, style=wx.TE_PROCESS_ENTER | wx.TE_MULTILINE, size=(200, 30)) label_textctrl = wx.TextCtrl(panel, style=wx.TE_PROCESS_ENTER | wx.TE_MULTILINE, size=(200, 30))
@ -117,11 +116,11 @@ class UpdateProfileDialog(wx.Dialog):
sizer.Add(field_sizer, 0, wx.CENTER) sizer.Add(field_sizer, 0, wx.CENTER)
self.fields.append((label_textctrl, content_textctrl)) self.fields.append((label_textctrl, content_textctrl))
self.locked = wx.CheckBox(panel, label=_("&Private account")) self.locked = wx.CheckBox(panel, label=_("Private account"))
self.locked.SetValue(locked) self.locked.SetValue(locked)
self.bot = wx.CheckBox(panel, label=_("&Bot account")) self.bot = wx.CheckBox(panel, label=_("Bot account"))
self.bot.SetValue(bot) self.bot.SetValue(bot)
self.discoverable = wx.CheckBox(panel, label=_("&Discoverable account")) self.discoverable = wx.CheckBox(panel, label=_("Discoverable account"))
self.discoverable.SetValue(discoverable) self.discoverable.SetValue(discoverable)
sizer.Add(self.locked, wx.SizerFlags().Expand().Border(wx.ALL, 5)) sizer.Add(self.locked, wx.SizerFlags().Expand().Border(wx.ALL, 5))
sizer.Add(self.bot, wx.SizerFlags().Expand().Border(wx.ALL, 5)) sizer.Add(self.bot, wx.SizerFlags().Expand().Border(wx.ALL, 5))

View File

@ -1,33 +0,0 @@
# -*- coding: utf-8 -*-
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)

View File

@ -52,7 +52,6 @@ class mainFrame(wx.Frame):
# buffer menu # buffer menu
self.menubar_buffer = wx.Menu() self.menubar_buffer = wx.Menu()
self.update_buffer = self.menubar_buffer.Append(wx.ID_ANY, _(u"&Update buffer")) self.update_buffer = self.menubar_buffer.Append(wx.ID_ANY, _(u"&Update buffer"))
self.community_timeline = self.menubar_buffer.Append(wx.ID_ANY, _("Create community timeline"))
self.trends = self.menubar_buffer.Append(wx.ID_ANY, _(u"New &trending topics buffer...")) self.trends = self.menubar_buffer.Append(wx.ID_ANY, _(u"New &trending topics buffer..."))
self.filter = self.menubar_buffer.Append(wx.ID_ANY, _(u"Create a &filter")) self.filter = self.menubar_buffer.Append(wx.ID_ANY, _(u"Create a &filter"))
self.manage_filters = self.menubar_buffer.Append(wx.ID_ANY, _(u"&Manage filters")) self.manage_filters = self.menubar_buffer.Append(wx.ID_ANY, _(u"&Manage filters"))

View File

@ -1,7 +1,7 @@
{"current_version": "2025.03.08", {"current_version": "2023.04.13",
"description": "Initial filter support, added pinned posts. Fixed some minor issues.", "description": "Stops support for Twitter session on Feb 9, updates to mastodon sessions. Please avoid using autoupdate as it will not work.",
"date": "unknown", "date": "unknown",
"downloads": "downloads":
{"Windows32": "https://github.com/MCV-Software/TWBlue/releases/download/v2025.03.08/TWBlue_portable_v2024.05.23.zip", {"Windows32": "https://twblue.es/pubs/twblue_x86.zip",
"Windows64": "https://github.com/MCV-Software/TWBlue/releases/download/v2025.03.08/TWBlue_portable_v2025.03.08.zip"} "Windows64": "https://twblue.es/pubs/twblue_x64.zip"}
} }