Compare commits

...

20 Commits

Author SHA1 Message Date
ae0dcc7b21 Generate all versions properly 2022-12-13 15:56:01 -06:00
3adc726f33 Handle properly a psutil exception. Closes #501 2022-12-13 11:16:10 -06:00
a3bc684721 Mastodon: Added reverse timeline ordering to buffers 2022-12-10 18:52:07 -06:00
ffbb67765d Replaced check_pid to use psutil as opposed to deal directly with win32 API 2022-12-10 18:29:59 -06:00
b57a430dc5 Bring back support for Windows 7 on 32-bit portable variant 2022-12-09 16:41:18 -06:00
0f04cfabf9 Fixed small typo on win7 version 2022-12-09 13:27:36 -06:00
3b29cbd746 Ensure python 3.7 installs wxpython for 32 bits 2022-12-09 13:19:54 -06:00
730665b5da fixed a typo 2022-12-09 13:03:37 -06:00
924cc83090 Modified CI for an experiment with Window 7 versions 2022-12-09 12:55:58 -06:00
4d46bab37f Remove fixes for libloader and win32com 2022-12-09 12:36:00 -06:00
a5e075a215 Replace httpcore submodule to fix an issue with googletrans 2022-12-09 12:35:26 -06:00
c9b75925b9 Removed unneeded code 2022-12-08 16:11:25 -06:00
3450e0d38f twitter: Fixed reply keystroke not working on dm buffer 2022-12-08 11:48:04 -06:00
ca83604870 Updated changelog 2022-12-08 11:38:17 -06:00
35b758ecf1 mastodon: Added account settings dialog (except autocompletion settings) 2022-12-08 11:14:14 -06:00
d5ed5f12af mastodon: Added missing settings in config file 2022-12-08 11:10:06 -06:00
eedcb49f3d Mastodon: Added template editor 2022-12-08 11:07:44 -06:00
a1878f10b3 Fixed dm ordering issue 2022-12-06 13:31:09 -06:00
2323c3cac5 Include mastodon default config file on builds 2022-12-06 10:06:27 -06:00
e90c370e73 Add secrets on build stages 2022-12-06 09:12:09 -06:00
19 changed files with 549 additions and 51 deletions

View File

@@ -1,6 +1,7 @@
variables:
GIT_SUBMODULE_STRATEGY: recursive
PYTHON: "C:\\python310\\python.exe"
PYTHON37: "C:\\python37\\python.exe" # for Windows 7 support.
NSIS: "C:\\program files (x86)\\nsis\\makensis.exe"
stages:
@@ -31,6 +32,7 @@ twblue32:
- cd ..\src
- '&$env:PYTHON ..\doc\generator.py'
- '&$env:PYTHON write_version_data.py'
- New-Item "appkeys.py" -ItemType File -Value "twitter_api_key='$TWITTER_API_KEY'`ntwitter_api_secret='$TWITTER_API_SECRET'"
- '&$env:PYTHON setup.py build'
- cd ..
- mkdir artifacts
@@ -70,6 +72,7 @@ twblue64:
- cd ..\src
- '&$env:PYTHON ..\doc\generator.py'
- '&$env:PYTHON write_version_data.py'
- New-Item "appkeys.py" -ItemType File -Value "twitter_api_key='$TWITTER_API_KEY'`ntwitter_api_secret='$TWITTER_API_SECRET'"
- '&$env:PYTHON setup.py build'
- cd ..
- mkdir artifacts
@@ -85,6 +88,44 @@ twblue64:
- artifacts
expire_in: 1 day
twblueWin7:
tags:
- shared-windows
- windows
- windows-1809
before_script:
- Set-Variable -Name "time" -Value (date -Format "%H:%m")
- echo ${time}
- echo "started by ${GITLAB_USER_NAME}"
- choco install python --version 3.7.9 -y -ForceX86
- '&$env:PYTHON37 -V'
- '&$env:PYTHON37 -m pip install --upgrade pip'
- '&$env:PYTHON37 -m pip install --upgrade https://github.com/josephsl/wxpy32whl/blob/main/wxPython-4.2.0-cp37-cp37m-win32.whl?raw=true'
- '&$env:PYTHON37 -m pip install --upgrade -r requirements.txt'
stage: build
interruptible: true
script:
# Create html documentation firstly.
- cd doc
- '&$env:PYTHON37 documentation_importer.py'
- cd ..\src
- '&$env:PYTHON37 ..\doc\generator.py'
- '&$env:PYTHON37 write_version_data.py'
- New-Item "appkeys.py" -ItemType File -Value "twitter_api_key='$TWITTER_API_KEY'`ntwitter_api_secret='$TWITTER_API_SECRET'"
- '&$env:PYTHON37 setup.py build'
- cd ..
- mkdir artifacts
- cd scripts
- '&$env:PYTHON37 make_archive.py'
- cd ..
- move src/twblue.zip artifacts/twblue_windows7_x86.zip
only:
- tags
artifacts:
paths:
- artifacts
expire_in: 1 day
generate_versions:
stage: make_installer
tags:

View File

@@ -2,6 +2,21 @@ TWBlue Changelog
## changes in this version
* per popular request, We will generate a 32-bit portable version of TWBlue available for Windows 7 operating systems. This version will not be supported in our automatic updater, so in case of using such version, you would need to download it manually every time there is a new update. TWBlue will continue to be available for Windows 7 as long as it is possible to build it using Python 3.7.
* Fixed a couple of bugs that were making TWBlue unable to be opened in some computers, related to our translator module and some COM objects handled incorrectly.
* Fixed an issue that was making TWBlue unable to open in certain computers due to errors related to Win32 API'S.
* Twitter:
* Fixed a bug that was making sent direct messages to be placed in received direct messages buffer.
* When quoting a tweet, you can use all 280 characters to send your quoted tweet, as opposed to the 256 characters TWBlue allowed before.
* Fixed a bug that was making TWBlue unable to reply to direct messages by using the "reply" keystroke.
* Mastodon:
* Added account settings dialog.
* Added template editing functionality for mastodon accounts.
* When a post is edited, TWBlue will update the post object in the buffer to reflect the latest edit.
* Fixed a small issue that was preventing TWBlue to display some posts in their corresponding dialog.
## Changes in version 2022.12.6
Most of all changes in this release are focused on adding Mastodon support to TWBlue. The features present to handle Twitter should not have been altered in any way. We were not intended to release this version so soon, but unfortunately, Twitter started to present issues in some regions with one particular API endpoint we were using, making impossible for everyone in such regions to use the application. We will release more updates to fix any possible issue regarding Twitter API, but please take into account that this is sometimes an issue happening in Twitter's servers and while we do our best to make TWBlue work despite those problems, you might encounter glitches from time to time.
* TWBlue now builds with Python 3.10.8. ([#493](https://github.com/MCV-Software/TWBlue/issues/493))

View File

@@ -50,6 +50,7 @@ numpy
pillow
charset-normalizer
demoji
psutil
git+https://github.com/accessibleapps/libloader
git+https://github.com/accessibleapps/platform_utils
git+https://github.com/accessibleapps/accessible_output2

View File

@@ -93,7 +93,10 @@ class BaseBuffer(base.Buffer):
min_id = None
# toDo: Implement reverse timelines properly here.
if (self.name != "favorites" and self.name != "bookmarks") and self.name in self.session.db and len(self.session.db[self.name]) > 0:
min_id = self.session.db[self.name][-1].id
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 = getattr(self.session.api, self.function)(min_id=min_id, limit=count, *self.args, **self.kwargs)
results.reverse()
@@ -152,7 +155,7 @@ class BaseBuffer(base.Buffer):
post = self.compose_function(i, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])
self.buffer.list.insert_item(True, *post)
else:
for i in items:
for i in elements:
post = self.compose_function(i, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])
self.buffer.list.insert_item(False, *post)
self.buffer.list.select_item(selection)
@@ -489,7 +492,7 @@ class BaseBuffer(base.Buffer):
post = self.get_item()
# Update object so we can retrieve newer stats
post = self.session.api.status(id=post.id)
print(post)
# print(post)
msg = messages.viewPost(post, offset_hours=self.session.db["utc_offset"], item_url=self.get_item_url())
def ocr_image(self):

View File

@@ -46,9 +46,6 @@ class ConversationListBuffer(BaseBuffer):
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 != "favorites" and self.name != "bookmarks") and self.name in self.session.db and len(self.session.db[self.name]) > 0:
# min_id = self.session.db[self.name][-1].id
try:
results = getattr(self.session.api, self.function)(min_id=min_id, limit=count, *self.args, **self.kwargs)
results.reverse()
@@ -94,7 +91,7 @@ class ConversationListBuffer(BaseBuffer):
conversation = self.compose_function(i, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])
self.buffer.list.insert_item(True, *conversation)
else:
for i in items:
for i in elements:
conversation = self.compose_function(i, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])
self.buffer.list.insert_item(False, *conversation)
self.buffer.list.select_item(selection)

View File

@@ -16,9 +16,6 @@ class MentionsBuffer(BaseBuffer):
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 != "favorites" and self.name in self.session.db and len(self.session.db[self.name]) > 0:
# min_id = self.session.db[self.name][-1].id
try:
items = getattr(self.session.api, self.function)(min_id=min_id, limit=count, exclude_types=["follow", "favourite", "reblog", "poll", "follow_request"], *self.args, **self.kwargs)
items.reverse()
@@ -64,7 +61,7 @@ class MentionsBuffer(BaseBuffer):
post = self.compose_function(i, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])
self.buffer.list.insert_item(True, *post)
else:
for i in items:
for i in elements:
post = self.compose_function(i, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])
self.buffer.list.insert_item(False, *post)
self.buffer.list.select_item(selection)

View File

@@ -75,7 +75,6 @@ class UserBuffer(BaseBuffer):
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"]
# toDo: Implement reverse timelines properly here.
try:
results = getattr(self.session.api, self.function)(limit=count, *self.args, **self.kwargs)
if hasattr(results, "_pagination_next") and self.name not in self.session.db["pagination_info"]:
@@ -127,7 +126,7 @@ class UserBuffer(BaseBuffer):
post = self.compose_function(i, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])
self.buffer.list.insert_item(True, *post)
else:
for i in items:
for i in elements:
post = self.compose_function(i, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])
self.buffer.list.insert_item(False, *post)
self.buffer.list.select_item(selection)

View File

@@ -457,7 +457,7 @@ class BaseBuffer(base.Buffer):
def _retweet_with_comment(self, tweet, id):
if hasattr(tweet, "retweeted_status"):
tweet = tweet.retweeted_status
retweet = messages.tweet(session=self.session, title=_("Quote"), caption=_("Add your comment to the tweet"), max=256, thread_mode=False)
retweet = messages.tweet(session=self.session, title=_("Quote"), caption=_("Add your comment to the tweet"), thread_mode=False)
if retweet.message.ShowModal() == widgetUtils.OK:
text = retweet.message.text.GetValue()
tweet_data = dict(text=text, attachments=retweet.attachments, poll_period=retweet.poll_period, poll_options=retweet.poll_options)

View File

@@ -87,14 +87,7 @@ class DirectMessagesBuffer(base.BaseBuffer):
output.speak(_(u"%s items retrieved") % (total), True)
def reply(self, *args, **kwargs):
tweet = self.get_right_tweet()
screen_name = self.session.get_user(tweet.message_create["sender_id"]).screen_name
message = messages.reply(session=self.session, title=_("Mention"), caption=_("Mention to %s") % (screen_name,), text="@%s " % (screen_name,), thread_mode=False, users=[screen_name,])
if message.message.ShowModal() == widgetUtils.OK:
tweet_data = message.get_tweet_data()
call_threaded(self.session.send_tweet, tweet_data)
if hasattr(message.message, "destroy"):
message.message.destroy()
return self.send_message()
def onFocus(self, *args, **kwargs):
tweet = self.get_tweet()

View File

@@ -2,12 +2,13 @@
import wx
import logging
from pubsub import pub
from mysc import restart
from wxUI.dialogs.mastodon import dialogs
from wxUI.dialogs.mastodon import search as search_dialogs
from wxUI.dialogs.mastodon import dialogs
from wxUI import commonMessageDialogs
from sessions.twitter import utils
from . import userActions
from . import userActions, settings
log = logging.getLogger("controller.mastodon.handler")
@@ -177,3 +178,13 @@ class Handler(object):
buffer.session.settings["other_buffers"]["following_timelines"].append(user.id)
buffer.session.sound.play("create_timeline.ogg")
buffer.session.settings.write()
def account_settings(self, buffer, controller):
d = settings.accountSettingsController(buffer, controller)
if d.response == wx.ID_OK:
d.save_configuration()
if d.needs_restart == True:
commonMessageDialogs.needs_restart()
buffer.session.settings.write()
buffer.session.save_persistent_data()
restart.restart_program()

View File

@@ -0,0 +1,216 @@
# -*- coding: utf-8 -*-
import os
import threading
import logging
import sound_lib
import paths
import widgetUtils
import output
from collections import OrderedDict
from wxUI import commonMessageDialogs
from wxUI.dialogs.mastodon import configuration
from extra.autocompletionUsers import scan, manage
from extra.ocr import OCRSpace
from controller.settings import globalSettingsController
from . templateEditor import EditTemplate
log = logging.getLogger("Settings")
class accountSettingsController(globalSettingsController):
def __init__(self, buffer, window):
self.user = buffer.session.db["user_name"]
self.buffer = buffer
self.window = window
self.config = buffer.session.settings
self.dialog = configuration.configurationDialog()
self.create_config()
self.needs_restart = False
self.is_started = True
def create_config(self):
self.dialog.create_general_account()
# widgetUtils.connect_event(self.dialog.general.userAutocompletionScan, widgetUtils.BUTTON_PRESSED, self.on_autocompletion_scan)
# widgetUtils.connect_event(self.dialog.general.userAutocompletionManage, widgetUtils.BUTTON_PRESSED, self.on_autocompletion_manage)
self.dialog.set_value("general", "relative_time", self.config["general"]["relative_times"])
self.dialog.set_value("general", "show_screen_names", self.config["general"]["show_screen_names"])
self.dialog.set_value("general", "hide_emojis", self.config["general"]["hide_emojis"])
self.dialog.set_value("general", "itemsPerApiCall", self.config["general"]["max_posts_per_call"])
self.dialog.set_value("general", "reverse_timelines", self.config["general"]["reverse_timelines"])
boost_mode = self.config["general"]["boost_mode"]
if boost_mode == "ask":
self.dialog.set_value("general", "ask_before_boost", True)
else:
self.dialog.set_value("general", "ask_before_boost", False)
self.dialog.set_value("general", "persist_size", str(self.config["general"]["persist_size"]))
self.dialog.set_value("general", "load_cache_in_memory", self.config["general"]["load_cache_in_memory"])
self.dialog.create_reporting()
self.dialog.set_value("reporting", "speech_reporting", self.config["reporting"]["speech_reporting"])
self.dialog.set_value("reporting", "braille_reporting", self.config["reporting"]["braille_reporting"])
post_template = self.config["templates"]["post"]
conversation_template = self.config["templates"]["conversation"]
person_template = self.config["templates"]["person"]
self.dialog.create_templates(post_template=post_template, conversation_template=conversation_template, person_template=person_template)
widgetUtils.connect_event(self.dialog.templates.post, widgetUtils.BUTTON_PRESSED, self.edit_post_template)
widgetUtils.connect_event(self.dialog.templates.conversation, widgetUtils.BUTTON_PRESSED, self.edit_conversation_template)
widgetUtils.connect_event(self.dialog.templates.person, widgetUtils.BUTTON_PRESSED, self.edit_person_template)
self.dialog.create_other_buffers()
buffer_values = self.get_buffers_list()
self.dialog.buffers.insert_buffers(buffer_values)
self.dialog.buffers.connect_hook_func(self.toggle_buffer_active)
widgetUtils.connect_event(self.dialog.buffers.toggle_state, widgetUtils.BUTTON_PRESSED, self.toggle_state)
widgetUtils.connect_event(self.dialog.buffers.up, widgetUtils.BUTTON_PRESSED, self.dialog.buffers.move_up)
widgetUtils.connect_event(self.dialog.buffers.down, widgetUtils.BUTTON_PRESSED, self.dialog.buffers.move_down)
self.input_devices = sound_lib.input.Input.get_device_names()
self.output_devices = sound_lib.output.Output.get_device_names()
self.soundpacks = []
[self.soundpacks.append(i) for i in os.listdir(paths.sound_path()) if os.path.isdir(os.path.join(paths.sound_path(), i)) == True ]
self.dialog.create_sound(self.input_devices, self.output_devices, self.soundpacks)
self.dialog.set_value("sound", "volumeCtrl", int(self.config["sound"]["volume"]*100))
self.dialog.set_value("sound", "input", self.config["sound"]["input_device"])
self.dialog.set_value("sound", "output", self.config["sound"]["output_device"])
self.dialog.set_value("sound", "session_mute", self.config["sound"]["session_mute"])
self.dialog.set_value("sound", "soundpack", self.config["sound"]["current_soundpack"])
self.dialog.set_value("sound", "indicate_audio", self.config["sound"]["indicate_audio"])
self.dialog.set_value("sound", "indicate_img", self.config["sound"]["indicate_img"])
self.dialog.create_extras(OCRSpace.translatable_langs)
language_index = OCRSpace.OcrLangs.index(self.config["mysc"]["ocr_language"])
self.dialog.extras.ocr_lang.SetSelection(language_index)
self.dialog.realize()
self.dialog.set_title(_("Account settings for %s") % (self.user,))
self.response = self.dialog.get_response()
def edit_post_template(self, *args, **kwargs):
template = self.config["templates"]["post"]
control = EditTemplate(template=template, type="post")
result = control.run_dialog()
if result != "": # Template has been saved.
self.config["templates"]["post"] = result
self.config.write()
self.dialog.templates.post.SetLabel(_("Edit template for posts. Current template: {}").format(result))
def edit_conversation_template(self, *args, **kwargs):
template = self.config["templates"]["conversation"]
control = EditTemplate(template=template, type="conversation")
result = control.run_dialog()
if result != "": # Template has been saved.
self.config["templates"]["conversation"] = result
self.config.write()
self.dialog.templates.conversation.SetLabel(_("Edit template for conversations. Current template: {}").format(result))
def edit_person_template(self, *args, **kwargs):
template = self.config["templates"]["person"]
control = EditTemplate(template=template, type="person")
result = control.run_dialog()
if result != "": # Template has been saved.
self.config["templates"]["person"] = result
self.config.write()
self.dialog.templates.person.SetLabel(_("Edit template for persons. Current template: {}").format(result))
def save_configuration(self):
if self.config["general"]["relative_times"] != self.dialog.get_value("general", "relative_time"):
self.needs_restart = True
log.debug("Triggered app restart due to change in relative times.")
self.config["general"]["relative_times"] = self.dialog.get_value("general", "relative_time")
self.config["general"]["show_screen_names"] = self.dialog.get_value("general", "show_screen_names")
self.config["general"]["hide_emojis"] = self.dialog.get_value("general", "hide_emojis")
self.config["general"]["max_posts_per_call"] = self.dialog.get_value("general", "itemsPerApiCall")
if self.config["general"]["load_cache_in_memory"] != self.dialog.get_value("general", "load_cache_in_memory"):
self.config["general"]["load_cache_in_memory"] = self.dialog.get_value("general", "load_cache_in_memory")
self.needs_restart = True
log.debug("Triggered app restart due to change in database strategy management.")
if self.config["general"]["persist_size"] != self.dialog.get_value("general", "persist_size"):
if self.dialog.get_value("general", "persist_size") == '':
self.config["general"]["persist_size"] =-1
else:
try:
self.config["general"]["persist_size"] = int(self.dialog.get_value("general", "persist_size"))
except ValueError:
output.speak("Invalid cache size, setting to default.",True)
self.config["general"]["persist_size"] =1764
if self.config["general"]["reverse_timelines"] != self.dialog.get_value("general", "reverse_timelines"):
self.needs_restart = True
log.debug("Triggered app restart due to change in timeline order.")
self.config["general"]["reverse_timelines"] = self.dialog.get_value("general", "reverse_timelines")
ask_before_boost = self.dialog.get_value("general", "ask_before_boost")
if ask_before_boost == True:
self.config["general"]["boost_mode"] = "ask"
else:
self.config["general"]["boost_mode"] = "direct"
buffers_list = self.dialog.buffers.get_list()
if buffers_list != self.config["general"]["buffer_order"]:
self.needs_restart = True
log.debug("Triggered app restart due to change in buffer ordering.")
self.config["general"]["buffer_order"] = buffers_list
self.config["reporting"]["speech_reporting"] = self.dialog.get_value("reporting", "speech_reporting")
self.config["reporting"]["braille_reporting"] = self.dialog.get_value("reporting", "braille_reporting")
self.config["mysc"]["ocr_language"] = OCRSpace.OcrLangs[self.dialog.extras.ocr_lang.GetSelection()]
if self.config["sound"]["input_device"] != self.dialog.sound.get("input"):
self.config["sound"]["input_device"] = self.dialog.sound.get("input")
try:
self.buffer.session.sound.input.set_device(self.buffer.session.sound.input.find_device_by_name(self.config["sound"]["input_device"]))
except:
self.config["sound"]["input_device"] = "default"
if self.config["sound"]["output_device"] != self.dialog.sound.get("output"):
self.config["sound"]["output_device"] = self.dialog.sound.get("output")
try:
self.buffer.session.sound.output.set_device(self.buffer.session.sound.output.find_device_by_name(self.config["sound"]["output_device"]))
except:
self.config["sound"]["output_device"] = "default"
self.config["sound"]["volume"] = self.dialog.get_value("sound", "volumeCtrl")/100.0
self.config["sound"]["session_mute"] = self.dialog.get_value("sound", "session_mute")
self.config["sound"]["current_soundpack"] = self.dialog.sound.get("soundpack")
self.config["sound"]["indicate_audio"] = self.dialog.get_value("sound", "indicate_audio")
self.config["sound"]["indicate_img"] = self.dialog.get_value("sound", "indicate_img")
self.buffer.session.sound.config = self.config["sound"]
self.buffer.session.sound.check_soundpack()
self.config.write()
def toggle_state(self,*args,**kwargs):
return self.dialog.buffers.change_selected_item()
def on_autocompletion_scan(self, *args, **kwargs):
configuration = scan.autocompletionScan(self.buffer.session.settings, self.buffer, self.window)
to_scan = configuration.show_dialog()
if to_scan == True:
configuration.prepare_progress_dialog()
t = threading.Thread(target=configuration.scan)
t.start()
def on_autocompletion_manage(self, *args, **kwargs):
configuration = manage.autocompletionManage(self.buffer.session)
configuration.show_settings()
def get_buffers_list(self):
all_buffers=OrderedDict()
all_buffers['home']=_("Home")
all_buffers['local'] = _("Local")
all_buffers['federated'] = _("Federated")
all_buffers['mentions']=_("Mentions")
all_buffers['direct_messages']=_("Direct Messages")
all_buffers['sent']=_("Sent")
all_buffers['favorites']=_("Favorites")
all_buffers['bookmarks']=_("Bookmarks")
all_buffers['followers']=_("Followers")
all_buffers['following']=_("Following")
all_buffers['blocked']=_("Blocked users")
all_buffers['muted']=_("Muted users")
all_buffers['notifications']=_("Notifications")
list_buffers = []
hidden_buffers=[]
all_buffers_keys = list(all_buffers.keys())
# Check buffers shown first.
for i in self.config["general"]["buffer_order"]:
if i in all_buffers_keys:
list_buffers.append((i, all_buffers[i], True))
# This second pass will retrieve all hidden buffers.
for i in all_buffers_keys:
if i not in self.config["general"]["buffer_order"]:
hidden_buffers.append((i, all_buffers[i], False))
list_buffers.extend(hidden_buffers)
return list_buffers
def toggle_buffer_active(self, ev):
change = self.dialog.buffers.get_event(ev)
if change == True:
self.dialog.buffers.change_selected_item()

View File

@@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
import re
import wx
from typing import List
from sessions.mastodon.templates import post_variables, conversation_variables, person_variables
from wxUI.dialogs.twitterDialogs import templateDialogs
class EditTemplate(object):
def __init__(self, template: str, type: str) -> None:
super(EditTemplate, self).__init__()
self.default_template = template
if type == "post":
self.variables = post_variables
elif type == "conversation":
self.variables = conversation_variables
else:
self.variables = person_variables
self.template: str = template
def validate_template(self, template: str) -> bool:
used_variables: List[str] = re.findall("\$\w+", template)
validated: bool = True
for var in used_variables:
if var[1:] not in self.variables:
validated = False
return validated
def run_dialog(self) -> str:
dialog = templateDialogs.EditTemplateDialog(template=self.template, variables=self.variables, default_template=self.default_template)
response = dialog.ShowModal()
if response == wx.ID_SAVE:
validated: bool = self.validate_template(dialog.template.GetValue())
if validated == False:
templateDialogs.invalid_template()
self.template = dialog.template.GetValue()
return self.run_dialog()
else:
return dialog.template.GetValue()
else:
return ""

View File

@@ -4,15 +4,15 @@ from __future__ import absolute_import
from __future__ import unicode_literals
import sys
from . import fix_arrow # A few new locales for Three languages in arrow.
from . import fix_libloader # Regenerates comcache properly.
#from . import fix_libloader # Regenerates comcache properly.
from . import fix_urllib3_warnings # Avoiding some SSL warnings related to Twython.
from . import fix_win32com
#from . import fix_win32com
#from . import fix_requests #fix cacert.pem location for TWBlue binary copies
def setup():
fix_arrow.fix()
if hasattr(sys, "frozen"):
fix_libloader.fix()
fix_win32com.fix()
# if hasattr(sys, "frozen"):
# fix_libloader.fix()
# fix_win32com.fix()
# fix_requests.fix()
# else:
# fix_requests.fix(False)

View File

@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
from win32com.client import GetObject
import httpcore
httpcore.SyncHTTPTransport = httpcore.AsyncHTTPProxy
import sys
import os
import platform
@@ -14,6 +15,7 @@ import paths
# ToDo: Remove this soon as this is done already when importing the paths module.
if os.path.exists(os.path.join(paths.app_path(), "Uninstall.exe")):
paths.mode="installed"
import psutil
import commandline
import config
import output
@@ -107,23 +109,19 @@ def donation():
webbrowser.open_new_tab(_("https://twblue.es/donate"))
config.app["app-settings"]["donation_dialog_displayed"] = True
def is_running(pid):
"Check if the process with ID pid is running. Adapted from https://stackoverflow.com/a/568589"
WMI = GetObject('winmgmts:')
processes = WMI.InstancesOf('Win32_Process')
return [process.Properties_('ProcessID').Value for process in processes if process.Properties_('ProcessID').Value == pid]
def check_pid():
"Insures that only one copy of the application is running at a time."
"Ensures that only one copy of the application is running at a time."
pidpath = os.path.join(os.getenv("temp"), "{}.pid".format(application.name))
if os.path.exists(pidpath):
with open(pidpath) as fin:
pid = int(fin.read())
if is_running(pid):
# Display warning dialog
commonMessageDialogs.common_error(_(u"{0} is already running. Close the other instance before starting this one. If you're sure that {0} isn't running, try deleting the file at {1}. If you're unsure of how to do this, contact the {0} developers.").format(application.name, pidpath))
sys.exit(1)
else:
try:
p = psutil.Process(pid=pid)
if p.is_running():
# Display warning dialog
commonMessageDialogs.common_error(_("{0} is already running. Close the other instance before starting this one. If you're sure that {0} isn't running, try deleting the file at {1}. If you're unsure of how to do this, contact the {0} developers.").format(application.name, pidpath))
sys.exit(1)
except psutil.NoSuchProcess:
commonMessageDialogs.dead_pid()
# Write the new PID
with open(pidpath,"w") as cam:

View File

@@ -11,6 +11,7 @@ reverse_timelines = boolean(default=False)
persist_size = integer(default=0)
load_cache_in_memory=boolean(default=True)
show_screen_names = boolean(default=False)
hide_emojis = boolean(default=False)
buffer_order = list(default=list('home', 'local', 'mentions', 'direct_messages', 'sent', 'favorites', 'bookmarks', 'followers', 'following', 'blocked', 'muted', 'notifications'))
boost_mode = string(default="ask")

View File

@@ -94,8 +94,7 @@ class Session(base.baseSession):
objects = self.db["direct_messages"]
sent_objects = self.db["sent_direct_messages"]
for i in data:
# Twitter returns sender_id as str, which must be converted to int in order to match to our user_id object.
if int(i.message_create["sender_id"]) == self.db["user_id"]:
if i.message_create["sender_id"] == self.db["user_id"]:
if "sent_direct_messages" in self.db and utils.find_item(i, self.db["sent_direct_messages"]) == None:
if self.settings["general"]["reverse_timelines"] == False: sent_objects.append(i)
else: sent_objects.insert(0, i)

View File

@@ -42,7 +42,7 @@ build_exe_options = dict(
includes=["enchant.tokenize.en"], # This is not handled automatically by cx_freeze.
include_msvcr=False,
replace_paths = [("*", "")],
include_files=["icon.ico", "conf.defaults", "app-configuration.defaults", "keymaps", "locales", "sounds", "documentation", ("keys/lib", "keys/lib"), find_sound_lib_datafiles(), find_accessible_output2_datafiles()]+get_architecture_files(),
include_files=["icon.ico", "conf.defaults", "app-configuration.defaults", "mastodon.defaults", "keymaps", "locales", "sounds", "documentation", ("keys/lib", "keys/lib"), find_sound_lib_datafiles(), find_accessible_output2_datafiles()]+get_architecture_files(),
packages=["wxUI"],
bin_path_excludes=["C:\\Program Files", "C:\Program Files (x86)"],
)

View File

@@ -1,23 +1,24 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from builtins import str
from past.utils import old_div
import wx
import sys
import application
from . import utils
progress_dialog = None
def available_update_dialog(version, description, date):
dialog = wx.MessageDialog(None, _(u"There's a new %s version available, released on %s. Would you like to download it now?\n\n %s version: %s\n\nChanges:\n%s") % (application.name, date, application.name, version, description), _(u"New version for %s") % application.name, style=wx.YES|wx.NO|wx.ICON_WARNING)
if "3.7" not in sys.version: # Modern operating systems
update_msg = _("There's a new %s version available, released on %s. Would you like to download it now?\n\n %s version: %s\n\nChanges:\n%s") % (application.name, date, application.name, version, description)
styles = wx.YES|wx.NO|wx.ICON_WARNING
else:
update_msg = _("There's a new %s version available, released on %s. Updates are not automatic in Windows 7, so you would need to visit TWBlue's download website to get the latest version.\n\n %s version: %s\n\nChanges:\n%s") % (application.name, date, application.name, version, description)
styles = wx.OK|wx.ICON_WARNING
dialog = wx.MessageDialog(None, update_msg, _("New version for %s") % application.name, style=styles)
if dialog.ShowModal() == wx.ID_YES:
return True
else:
return False
def create_progress_dialog():
return wx.ProgressDialog(_(u"Download in Progress"), _(u"Downloading the new version..."), parent=None, maximum=100)

View File

@@ -0,0 +1,186 @@
# -*- coding: utf-8 -*-
import wx
import widgetUtils
from wxUI.dialogs import baseDialog
# As some panels are the same than those used in Twitter sessions, let's import them directly.
from wxUI.dialogs.configuration import reporting, other_buffers
from multiplatform_widgets import widgets
class generalAccount(wx.Panel, baseDialog.BaseWXDialog):
def __init__(self, parent):
super(generalAccount, self).__init__(parent)
sizer = wx.BoxSizer(wx.VERTICAL)
userAutocompletionBox = wx.StaticBox(self, label=_("User autocompletion settings"))
self.userAutocompletionScan = wx.Button(self, wx.ID_ANY, _("Scan account and add followers and following users to the user autocompletion database"))
self.userAutocompletionScan.Enable(False)
self.userAutocompletionManage = wx.Button(self, wx.ID_ANY, _("Manage autocompletion database"))
self.userAutocompletionManage.Enable(False)
autocompletionSizer = wx.StaticBoxSizer(userAutocompletionBox, wx.HORIZONTAL)
autocompletionSizer.Add(self.userAutocompletionScan, 0, wx.ALL, 5)
autocompletionSizer.Add(self.userAutocompletionManage, 0, wx.ALL, 5)
sizer.Add(autocompletionSizer, 0, wx.ALL, 5)
self.relative_time = wx.CheckBox(self, wx.ID_ANY, _("Relative timestamps"))
sizer.Add(self.relative_time, 0, wx.ALL, 5)
itemsPerCallBox = wx.BoxSizer(wx.HORIZONTAL)
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.SetRange(0, 40)
self.itemsPerApiCall.SetSize(self.itemsPerApiCall.GetBestSize())
itemsPerCallBox.Add(self.itemsPerApiCall, 0, wx.ALL, 5)
sizer.Add(itemsPerCallBox, 0, wx.ALL, 5)
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)
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)
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)
self.hide_emojis = wx.CheckBox(self, wx.ID_ANY, _("hide emojis in usernames"))
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)"))
self.persist_size = wx.TextCtrl(self, -1)
sizer.Add(PersistSizeLabel, 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.SetSizer(sizer)
class templates(wx.Panel, baseDialog.BaseWXDialog):
def __init__(self, parent, post_template, conversation_template, person_template):
super(templates, self).__init__(parent)
sizer = wx.BoxSizer(wx.VERTICAL)
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)
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)
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)
self.SetSizer(sizer)
class sound(wx.Panel):
def __init__(self, parent, input_devices, output_devices, soundpacks):
wx.Panel.__init__(self, parent)
sizer = wx.BoxSizer(wx.VERTICAL)
volume = wx.StaticText(self, -1, _(u"Volume"))
self.volumeCtrl = wx.Slider(self)
# 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
widgetUtils.connect_event(self.volumeCtrl, widgetUtils.KEYPRESS, self.on_keypress)
self.volumeCtrl.SetRange(0, 100)
self.volumeCtrl.SetSize(self.volumeCtrl.GetBestSize())
volumeBox = wx.BoxSizer(wx.HORIZONTAL)
volumeBox.Add(volume, 0, wx.ALL, 5)
volumeBox.Add(self.volumeCtrl, 0, wx.ALL, 5)
sizer.Add(volumeBox, 0, wx.ALL, 5)
self.session_mute = wx.CheckBox(self, -1, _(u"Session mute"))
sizer.Add(self.session_mute, 0, wx.ALL, 5)
output_label = wx.StaticText(self, -1, _(u"Output device"))
self.output = wx.ComboBox(self, -1, choices=output_devices, style=wx.CB_READONLY)
self.output.SetSize(self.output.GetBestSize())
outputBox = wx.BoxSizer(wx.HORIZONTAL)
outputBox.Add(output_label, 0, wx.ALL, 5)
outputBox.Add(self.output, 0, wx.ALL, 5)
sizer.Add(outputBox, 0, wx.ALL, 5)
input_label = wx.StaticText(self, -1, _(u"Input device"))
self.input = wx.ComboBox(self, -1, choices=input_devices, style=wx.CB_READONLY)
self.input.SetSize(self.input.GetBestSize())
inputBox = wx.BoxSizer(wx.HORIZONTAL)
inputBox.Add(input_label, 0, wx.ALL, 5)
inputBox.Add(self.input, 0, wx.ALL, 5)
sizer.Add(inputBox, 0, wx.ALL, 5)
soundBox = wx.BoxSizer(wx.VERTICAL)
soundpack_label = wx.StaticText(self, -1, _(u"Sound pack"))
self.soundpack = wx.ComboBox(self, -1, choices=soundpacks, style=wx.CB_READONLY)
self.soundpack.SetSize(self.soundpack.GetBestSize())
soundBox.Add(soundpack_label, 0, wx.ALL, 5)
soundBox.Add(self.soundpack, 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"))
sizer.Add(self.indicate_audio, 0, wx.ALL, 5)
self.indicate_img = wx.CheckBox(self, -1, _("Indicate posts containing images with sound"))
sizer.Add(self.indicate_img, 0, wx.ALL, 5)
self.SetSizer(sizer)
def on_keypress(self, event, *args, **kwargs):
""" Invert movement of up and down arrow keys when dealing with a wX Slider.
See https://github.com/manuelcortez/TWBlue/issues/261
and http://trac.wxwidgets.org/ticket/2068
"""
keycode = event.GetKeyCode()
if keycode == wx.WXK_UP:
return self.volumeCtrl.SetValue(self.volumeCtrl.GetValue()+1)
elif keycode == wx.WXK_DOWN:
return self.volumeCtrl.SetValue(self.volumeCtrl.GetValue()-1)
event.Skip()
def get(self, control):
return getattr(self, control).GetStringSelection()
class extrasPanel(wx.Panel):
def __init__(self, parent, ocr_languages=[], translation_languages=[]):
super(extrasPanel, self).__init__(parent)
mainSizer = wx.BoxSizer(wx.VERTICAL)
OCRBox = wx.StaticBox(self, label=_(u"Language for OCR"))
self.ocr_lang = wx.ListBox(self, -1, choices=ocr_languages)
self.ocr_lang.SetSize(self.ocr_lang.GetBestSize())
ocrLanguageSizer = wx.StaticBoxSizer(OCRBox, wx.HORIZONTAL)
ocrLanguageSizer.Add(self.ocr_lang, 0, wx.ALL, 5)
mainSizer.Add(ocrLanguageSizer, 0, wx.ALL, 5)
self.SetSizer(mainSizer)
class configurationDialog(baseDialog.BaseWXDialog):
def set_title(self, title):
self.SetTitle(title)
def __init__(self):
super(configurationDialog, self).__init__(None, -1)
self.panel = wx.Panel(self)
self.sizer = wx.BoxSizer(wx.VERTICAL)
self.notebook = wx.Notebook(self.panel)
def create_general_account(self):
self.general = generalAccount(self.notebook)
self.notebook.AddPage(self.general, _(u"General"))
self.general.SetFocus()
def create_reporting(self):
self.reporting = reporting(self.notebook)
self.notebook.AddPage(self.reporting, _(u"Feedback"))
def create_other_buffers(self):
self.buffers = other_buffers(self.notebook)
self.notebook.AddPage(self.buffers, _(u"Buffers"))
def create_templates(self, post_template, conversation_template, person_template):
self.templates = templates(self.notebook, post_template=post_template, conversation_template=conversation_template, person_template=person_template)
self.notebook.AddPage(self.templates, _("Templates"))
def create_sound(self, output_devices, input_devices, soundpacks):
self.sound = sound(self.notebook, output_devices, input_devices, soundpacks)
self.notebook.AddPage(self.sound, _(u"Sound"))
def create_extras(self, ocr_languages=[], translator_languages=[]):
self.extras = extrasPanel(self.notebook, ocr_languages, translator_languages)
self.notebook.AddPage(self.extras, _(u"Extras"))
def realize(self):
self.sizer.Add(self.notebook, 0, wx.ALL, 5)
ok_cancel_box = wx.BoxSizer(wx.HORIZONTAL)
ok = wx.Button(self.panel, wx.ID_OK, _(u"Save"))
ok.SetDefault()
cancel = wx.Button(self.panel, wx.ID_CANCEL, _(u"Close"))
self.SetEscapeId(cancel.GetId())
ok_cancel_box.Add(ok, 0, wx.ALL, 5)
ok_cancel_box.Add(cancel, 0, wx.ALL, 5)
self.sizer.Add(ok_cancel_box, 0, wx.ALL, 5)
self.panel.SetSizer(self.sizer)
self.SetClientSize(self.sizer.CalcMin())
def get_value(self, panel, key):
p = getattr(self, panel)
return getattr(p, key).GetValue()
def set_value(self, panel, key, value):
p = getattr(self, panel)
control = getattr(p, key)
getattr(control, "SetValue")(value)